From fc6dae3c9e3a42dc28fac9639872cf6b2bd90d8b Mon Sep 17 00:00:00 2001 From: malik masis Date: Fri, 9 Dec 2022 14:45:09 +0300 Subject: [PATCH] Moved captcha from the app layer to web --- .../Volo.CmsKit.Public.Application.csproj | 2 - .../Security/Captcha/CaptchaException.cs | 15 ++ .../Security/Captcha/CaptchaOptions.cs | 66 +++++++ .../Security/Captcha/CaptchaOutput.cs | 32 ++++ .../Security/Captcha/EncoderTypes.cs | 7 + .../Security/Captcha/RandomTextGenerator.cs | 73 ++++++++ .../Captcha/SimpleMathsCaptchaGenerator.cs | 175 ++++++++++++++++++ .../Volo.CmsKit.Public.Web.csproj | 3 +- 8 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs create mode 100644 modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj index b092c3f511..c062ee451a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj @@ -9,8 +9,6 @@ - - diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs new file mode 100644 index 0000000000..7914e85b02 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; +using Volo.Abp; + +namespace Volo.CmsKit.Public.Web.Security.Captcha; + +public class CaptchaException : UserFriendlyException +{ + public CaptchaException(string message) : base(message) + { + } + + public CaptchaException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) + { + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs new file mode 100644 index 0000000000..3db5aa06b6 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs @@ -0,0 +1,66 @@ +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; + +namespace Volo.CmsKit.Public.Web.Security.Captcha; + +public class CaptchaOptions +{ + /// + /// Default fonts are "Arial", "Verdana", "Times New Roman" in Windows. These fonts must exist in the target OS. + /// + public string[] FontFamilies { get; set; } = new string[] { "Arial", "Verdana", "Times New Roman" }; + + public Color[] TextColor { get; set; } = new Color[] + { + Color.Blue, Color.Black, Color.Black, Color.Brown, Color.Gray, Color.Green + }; + public Color[] DrawLinesColor { get; set; } = new Color[] + { + Color.Blue, Color.Black, Color.Black, Color.Brown, Color.Gray, Color.Green + }; + + public float MinLineThickness { get; set; } = 0.7f; + + public float MaxLineThickness { get; set; } = 2.0f; + + public ushort Width { get; set; } = 180; + + public ushort Height { get; set; } = 70; + + public ushort NoiseRate { get; set; } = 500; + + public Color[] NoiseRateColor { get; set; } = new Color[] { Color.Gray }; + + public byte FontSize { get; set; } = 32; + + public FontStyle FontStyle { get; set; } = FontStyle.Regular; + + public EncoderTypes EncoderType { get; set; } = EncoderTypes.Png; + + public IImageEncoder Encoder => RandomTextGenerator.GetEncoder(EncoderType); + + public byte DrawLines { get; set; } = 2; + + public byte MaxRotationDegrees { get; set; } = 4; + + public int Number1MinValue { get; set; } = 1; + + public int Number1MaxValue { get; set; } = 99; + + public int Number2MinValue { get; set; } = 1; + + public int Number2MaxValue { get; set; } = 99; + + public CaptchaOptions() + { + + } + public CaptchaOptions(int number1MinValue, int number1MaxValue, int number2MinValue, int number2MaxValue) + { + Number1MinValue = number1MinValue; + Number1MaxValue = number1MaxValue; + Number2MinValue = number2MinValue; + Number1MaxValue = number2MaxValue; + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs new file mode 100644 index 0000000000..3a483aa46a --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs @@ -0,0 +1,32 @@ +using System; + +namespace Volo.CmsKit.Public.Web.Security.Captcha; + +public class CaptchaOutput +{ + public Guid Id { get; set; } + public string Text { get; set; } + public byte[] ImageBytes { get; set; } + public int Result { get; set; } +} + +public class CaptchaInput +{ + public int Number1 { get; set; } + public int Number2 { get; set; } +} + +public class CaptchaRequest +{ + public CaptchaInput Input { get; set; } + public CaptchaOutput Output { get; set; } + + public CaptchaRequest() + { + Input = new CaptchaInput(); + Output = new CaptchaOutput + { + Id = Guid.NewGuid() + }; + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs new file mode 100644 index 0000000000..60308946f0 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs @@ -0,0 +1,7 @@ +namespace Volo.CmsKit.Public.Web.Security.Captcha; + +public enum EncoderTypes +{ + Jpeg, + Png, +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs new file mode 100644 index 0000000000..df5818f88c --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs @@ -0,0 +1,73 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; + +namespace Volo.CmsKit.Public.Web.Security.Captcha; +public static class RandomTextGenerator +{ + private static readonly char[] AllowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVXYZW23456789".ToCharArray(); + + public static IImageEncoder GetEncoder(EncoderTypes encoderType) + { + IImageEncoder encoder = encoderType switch + { + EncoderTypes.Png => new PngEncoder(), + EncoderTypes.Jpeg => new JpegEncoder(), + _ => throw new ArgumentException($"Encoder '{encoderType}' not found!") + }; + + return encoder; + } + + public static string GetRandomText(int size) + { + var data = new byte[4 * size]; + using (var crypto = new RNGCryptoServiceProvider()) + { + crypto.GetBytes(data); + } + + var result = new StringBuilder(size); + for (var i = 0; i < size; i++) + { + var rnd = BitConverter.ToUInt32(data, i * 4); + var idx = rnd % AllowedChars.Length; + result.Append(AllowedChars[idx]); + } + + return result.ToString(); + } + + public static string GetUniqueKey(int size, char[] chars) + { + var data = new byte[4 * size]; + using (var crypto = new RNGCryptoServiceProvider()) + { + crypto.GetBytes(data); + } + + var result = new StringBuilder(size); + + for (var i = 0; i < size; i++) + { + var rnd = BitConverter.ToUInt32(data, i * 4); + var idx = rnd % chars.Length; + result.Append(chars[idx]); + } + + return result.ToString(); + } + + public static float GenerateNextFloat(double min = -3.40282347E+38, double max = 3.40282347E+38) + { + var random = new Random(); + var range = max - min; + var sample = random.NextDouble(); + var scaled = sample * range + min; + var result = (float)scaled; + return result; + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs new file mode 100644 index 0000000000..b9268a7f6a --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Volo.Abp.DependencyInjection; +using Color = SixLabors.ImageSharp.Color; +using PointF = SixLabors.ImageSharp.PointF; + +namespace Volo.CmsKit.Public.Web.Security.Captcha; + +public class SimpleMathsCaptchaGenerator : ISingletonDependency +{ + private static Dictionary Session { get; set; } = new Dictionary(); + + public CaptchaOutput Generate() + { + return Generate(options: null, number1: null, number2: null); + } + + public CaptchaOutput Generate(CaptchaOptions options) + { + return Generate(options, number1: null, number2: null); + } + + /// + /// Creates a simple captcha code. + /// + /// Options for captcha generation + /// First number for maths operation + /// Second number for maths operation + /// + public CaptchaOutput Generate(CaptchaOptions options, int? number1, int? number2) + { + var random = new Random(); + options ??= new CaptchaOptions(); + + number1 ??= random.Next(options.Number1MinValue, options.Number1MaxValue); + number2 ??= random.Next(options.Number2MinValue, options.Number2MaxValue); + + var text = number1 + "+" + number2; + var request = new CaptchaRequest + { + Input = + { + Number1 = number1.Value, + Number2 = number2.Value + }, + Output = + { + Text = text, + Result = Calculate(number1.Value, number2.Value), + ImageBytes = GenerateInternal(text, options) + } + }; + + Session[request.Output.Id] = request; + return request.Output; + } + + private static int Calculate(int number1, int number2) + { + return number1 + number2; + } + + public void Validate(Guid requestId, int value) + { + var request = Session[requestId]; + if (request.Output.Result != value) + { + throw new CaptchaException("The captcha code doesn't match text on the picture! Please try again."); + } + } + + public void Validate(Guid requestId, string value) + { + if (int.TryParse(value, out var captchaInput)) + { + Validate(requestId, captchaInput); + } + else + { + throw new CaptchaException("The captcha code is missing!"); + } + } + + private byte[] GenerateInternal(string stringText, CaptchaOptions options) + { + byte[] result; + + using (var image = new Image(options.Width, options.Height)) + { + float position = 0; + var random = new Random(); + var startWith = (byte)random.Next(5, 10); + image.Mutate(ctx => ctx.BackgroundColor(Color.Transparent)); + var fontName = options.FontFamilies[random.Next(0, options.FontFamilies.Length)]; + var font = SystemFonts.CreateFont(fontName, options.FontSize, options.FontStyle); + + foreach (var character in stringText) + { + var text = character.ToString(); + var color = options.TextColor[random.Next(0, options.TextColor.Length)]; + var location = new PointF(startWith + position, random.Next(6, 13)); + image.Mutate(ctx => ctx.DrawText(text, font, color, location)); + position += TextMeasurer.Measure(character.ToString(), new RendererOptions(font, location)).Width; + } + + //add rotation + var rotation = GetRotation(options); + image.Mutate(ctx => ctx.Transform(rotation)); + + // add the dynamic image to original image + var size = (ushort)TextMeasurer.Measure(stringText, new RendererOptions(font)).Width; + var img = new Image(size + 15, options.Height); + img.Mutate(ctx => ctx.BackgroundColor(Color.White)); + + Parallel.For(0, options.DrawLines, i => + { + var x0 = random.Next(0, random.Next(0, 30)); + var y0 = random.Next(10, img.Height); + + var x1 = random.Next(30, img.Width); + var y1 = random.Next(0, img.Height); + + img.Mutate(ctx => + ctx.DrawLines(options.TextColor[random.Next(0, options.TextColor.Length)], + RandomTextGenerator.GenerateNextFloat(options.MinLineThickness, options.MaxLineThickness), + new PointF[] { new PointF(x0, y0), new PointF(x1, y1) }) + ); + }); + + img.Mutate(ctx => ctx.DrawImage(image, 0.80f)); + + Parallel.For(0, options.NoiseRate, i => + { + var x0 = random.Next(0, img.Width); + var y0 = random.Next(0, img.Height); + img.Mutate( + ctx => ctx + .DrawLines(options.NoiseRateColor[random.Next(0, options.NoiseRateColor.Length)], + RandomTextGenerator.GenerateNextFloat(0.5, 1.5), new PointF[] { new Vector2(x0, y0), new Vector2(x0, y0) }) + ); + }); + + img.Mutate(x => + { + x.Resize(options.Width, options.Height); + }); + + using (var ms = new MemoryStream()) + { + img.Save(ms, options.Encoder); + result = ms.ToArray(); + } + } + + return result; + } + + private static AffineTransformBuilder GetRotation(CaptchaOptions options) + { + var random = new Random(); + var width = random.Next(10, options.Width); + var height = random.Next(10, options.Height); + var pointF = new PointF(width, height); + var rotationDegrees = random.Next(0, options.MaxRotationDegrees); + return new AffineTransformBuilder().PrependRotationDegrees(rotationDegrees, pointF); + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj index ce31515b0e..7aaa2eda5e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj @@ -15,10 +15,11 @@ - + +