@@ -29,12 +29,12 @@ public class IconCaptchaService
29
29
private const string CaptchaFieldId = "ic-hf-id" ;
30
30
private const string CaptchaFieldHoneypot = "ic-hf-hp" ;
31
31
private const string CaptchaFieldToken = "_iconcaptcha-token" ;
32
-
32
+
33
33
/// <summary>
34
34
/// The default length of a captcha token.
35
35
/// </summary>
36
36
private const int CaptchaTokenLength = 20 ;
37
-
37
+
38
38
/// <summary>
39
39
/// The default image width of a captcha challenge, in pixels.
40
40
/// </summary>
@@ -86,7 +86,7 @@ public class IconCaptchaService
86
86
[ "dark" ] = new ( Mode . dark , new byte [ ] { 64 , 64 , 64 } ) ,
87
87
[ "legacy-dark" ] = new ( Mode . dark , new byte [ ] { 64 , 64 , 64 } ) ,
88
88
} ;
89
-
89
+
90
90
private Random Rand { get ; }
91
91
92
92
private ISessionProvider SessionProvider { get ; }
@@ -101,7 +101,7 @@ public IconCaptchaService(ISessionProvider sessionProvider, IHttpContextAccessor
101
101
Options = options ;
102
102
Rand = new Random ( ) ;
103
103
}
104
-
104
+
105
105
private CaptchaSession _session ;
106
106
private CaptchaSession Session
107
107
{
@@ -123,7 +123,7 @@ private CaptchaSession Session
123
123
return _session ;
124
124
}
125
125
}
126
-
126
+
127
127
/// <summary>
128
128
/// Generates and returns a secure random string which will serve as a CSRF token for the current session. After
129
129
/// generating the token, it will be saved in the global session variable. The length of the token will be
@@ -135,9 +135,9 @@ private CaptchaSession Session
135
135
public string Token ( )
136
136
{
137
137
// Make sure to only generate a token if none exists.
138
- if ( Session . Token != null )
138
+ if ( Session . Token != null )
139
139
return Session . Token ;
140
-
140
+
141
141
// Create a secure captcha session token.
142
142
var bytes = RandomNumberGenerator . GetBytes ( CaptchaTokenLength ) ;
143
143
var token = Convert . ToHexString ( bytes ) ;
@@ -200,7 +200,7 @@ public CaptchaResult GetCaptchaData(Payload payload)
200
200
var iconPositions = new int [ iconAmount ] ;
201
201
var iconIds = new List < int > ( ) ;
202
202
var correctIconId = - 1 ;
203
-
203
+
204
204
// Create a random 'icon position' order.
205
205
var tempPositions = Enumerable . Range ( 0 , iconAmount )
206
206
. OrderBy ( c => Rand . Next ( ) )
@@ -262,7 +262,7 @@ public CaptchaResult GetCaptchaData(Payload payload)
262
262
Id = payload . CaptchaId
263
263
} ;
264
264
}
265
-
265
+
266
266
/// <summary>
267
267
/// Validates the user form submission. If the captcha is incorrect, it
268
268
/// will set the global error variable and return FALSE, else TRUE.
@@ -297,7 +297,7 @@ public void ValidateSubmission()
297
297
{
298
298
throw new IconCaptchaSubmissionException ( 6 , Options . Value . Messages . FormToken ) ;
299
299
}
300
-
300
+
301
301
// Initialize the session.
302
302
var sessionData = CreateSession ( captchaId ) ;
303
303
@@ -351,9 +351,9 @@ public bool ValidateToken(string payloadToken, string headerToken = null)
351
351
var options = Options . Value ;
352
352
353
353
// Only validate if the token option is enabled.
354
- if ( ! options . Token )
354
+ if ( ! options . Token )
355
355
return true ;
356
-
356
+
357
357
var sessionToken = GetToken ( ) ;
358
358
359
359
// If the token is empty but the option is enabled, the token was never requested.
@@ -385,9 +385,9 @@ public bool ValidateToken(string payloadToken, string headerToken = null)
385
385
/// <returns>TRUE if the correct icon was selected, FALSE if not.</returns>
386
386
public bool SetSelectedAnswer ( Payload payload )
387
387
{
388
- if ( payload == null )
388
+ if ( payload == null )
389
389
return false ;
390
-
390
+
391
391
// Check if the captcha ID and required other payload data is set.
392
392
if ( payload . CaptchaId == default || payload . XPos == null || payload . YPos == null || payload . Width == null )
393
393
{
@@ -488,11 +488,9 @@ public async Task GetImage(long identifier)
488
488
var themeIconColor = Options . Value . Themes [ sessionData . Mode ] . Icons ;
489
489
var iconPath = Path . Combine ( iconsDirectoryPath , themeIconColor . ToString ( ) ) ;
490
490
491
- // Generate the captcha image.
492
- await using var placeholderStream = isEmbedded
493
- ? GetType ( ) . Assembly . GetManifestResourceStream ( placeholder )
494
- : File . OpenRead ( placeholder ) ;
491
+ await using var placeholderStream = GetImageStream ( isEmbedded , placeholder ) ;
495
492
493
+ // Generate the captcha image.
496
494
var generatedImage = GenerateImage ( sessionData , iconPath , placeholderStream , isEmbedded ) ;
497
495
498
496
// Set the content type header to the PNG MIME-type.
@@ -511,6 +509,34 @@ public async Task GetImage(long identifier)
511
509
await generatedImage . CopyToAsync ( httpContext . Response . Body ) ;
512
510
}
513
511
512
+ /// <summary>
513
+ /// Retrieves an image stream either from the file system or from embedded resources within the assembly.
514
+ /// </summary>
515
+ /// <param name="isEmbedded">Indicates whether the image is an embedded resource.</param>
516
+ /// <param name="file">The file path or resource name.</param>
517
+ /// <returns>A Stream representing the image.</returns>
518
+ /// <exception cref="FileNotFoundException">Thrown if the embedded resource is not found.</exception>
519
+ private Stream GetImageStream ( bool isEmbedded , string file )
520
+ {
521
+ if ( ! isEmbedded )
522
+ return File . OpenRead ( file ) ;
523
+
524
+ var assembly = GetType ( ) . Assembly ;
525
+
526
+ // Format the resource name to match the embedded resource naming convention
527
+ var resourceRef = file . Replace ( '/' , '.' ) . Replace ( '\\ ' , '.' ) ;
528
+ resourceRef = $ "{ assembly . GetName ( ) . Name } .Assets.{ resourceRef } ";
529
+
530
+ // Retrieve the embedded resource stream or throw an exception if not found
531
+ var stream = assembly . GetManifestResourceStream ( resourceRef ) ;
532
+ if ( stream == null )
533
+ {
534
+ throw new FileNotFoundException ( $ "Resource '{ resourceRef } ' not found in embedded resources.") ;
535
+ }
536
+
537
+ return stream ;
538
+ }
539
+
514
540
/// <summary>
515
541
/// Returns a generated image containing the icons for the current captcha instance. The icons will be copied
516
542
/// onto a placeholder image, located at the $placeholderPath. The icons will be randomly rotated and flipped
@@ -539,11 +565,7 @@ private Stream GenerateImage(CaptchaChallenge challenge,
539
565
id =>
540
566
{
541
567
var icon = Path . Combine ( iconPath , $ "icon-{ id } .png") ;
542
-
543
- using var stream = embeddedFiles
544
- ? GetType ( ) . Assembly . GetManifestResourceStream ( icon )
545
- : File . OpenRead ( icon ) ;
546
-
568
+ using var stream = GetImageStream ( embeddedFiles , icon ) ;
547
569
return CreateImage ( stream ) ;
548
570
} ) ;
549
571
@@ -590,15 +612,15 @@ private Stream GenerateImage(CaptchaChallenge challenge,
590
612
if ( rotateEnabled )
591
613
{
592
614
var degree = Rand . Next ( 1 , 4 ) ;
593
-
615
+
594
616
// Only if the 'degree' is not the same as what it would already be at.
595
617
if ( degree != 4 )
596
618
{
597
619
var rotated = new SKBitmap (
598
620
degree % 2 == 0 ? icon . Width : icon . Height ,
599
621
degree % 2 == 0 ? icon . Height : icon . Width
600
622
) ;
601
-
623
+
602
624
var surface = new SKCanvas ( rotated ) ;
603
625
surface . Translate ( rotated . Width / 2 , rotated . Height / 2 ) ;
604
626
surface . RotateDegrees ( degree * 90 ) ;
@@ -661,7 +683,7 @@ private static SKBitmap CreateImage(Stream file)
661
683
662
684
return SKBitmap . FromImage ( image ) ;
663
685
}
664
-
686
+
665
687
/// <summary>
666
688
/// Returns the clicked icon position based on the X and Y position and the captcha width.
667
689
/// </summary>
@@ -679,7 +701,7 @@ private int DetermineClickedIcon(int clickedXPos, int clickedYPos, int captchaWi
679
701
680
702
return ( int ) Math . Floor ( clickedXPos / ( ( decimal ) captchaWidth / iconAmount ) ) ;
681
703
}
682
-
704
+
683
705
/// <summary>
684
706
/// Calculates the amount of times 1 or more other icons can be present in the captcha image besides the correct icon.
685
707
/// Each other icons should be at least present 1 time more than the correct icon. When calculating the icon
@@ -749,7 +771,7 @@ private CaptchaChallenge CreateSession(long identifier = 0)
749
771
750
772
Session . Add ( identifier , sessionData ) ;
751
773
}
752
-
774
+
753
775
return sessionData ;
754
776
}
755
777
0 commit comments