// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Linq; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Text; namespace Duplicati.Server.WebServer { /// /// Helper class for creating Captcha images /// public static class CaptchaUtil { /// /// A lookup string with characters to use /// private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679"; /// /// A range of possible brush colors /// private static readonly Brush[] BRUSH_COLORS = typeof(Brushes) .GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) .Where(x => x.PropertyType == typeof(Brush)) .Select(x => x.GetValue(null, null) as Brush) .Where(x => x != null) .ToArray(); /// /// Approximate the size in pixels of text drawn at the given fontsize /// private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize) { using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) { return (int) graphics.MeasureString(text, font).Width; } } /// /// Creates a random answer. /// /// The random answer. /// The list of allowed chars, supply a character multiple times to change frequency. /// The minimum answer length. /// The maximum answer length. public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12) { allowedchars = allowedchars ?? DEFAULT_CHARS; var rnd = new Random(); var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1); if (len <= 0) throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0"); return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray()); } /// /// Creates a captcha image. /// /// The captcha image. /// The captcha solution string. /// The size of the image, omit to get a size based on the string. /// The size of the font used to create the captcha, in pixels. public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40) { var fontfamily = FontFamily.GenericSansSerif; var text_width = ApproxTextWidth(answer, fontfamily, fontsize); if (size.Width == 0 || size.Height == 0) size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2)); var bmp = new Bitmap(size.Width, size.Height); var rnd = new Random(); var stray_x = fontsize / 2; var stray_y = size.Height / 4; var ans_stray_x = fontsize / 3; var ans_stray_y = size.Height / 6; using (var graphics = Graphics.FromImage(bmp)) using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue)) { graphics.Clear(Color.White); graphics.TextRenderingHint = TextRenderingHint.AntiAlias; // Apply a some background string to make it hard to do OCR foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow }) using (var brush = new SolidBrush(color)) graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y)); var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x); // Create a vertical background lines for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing) using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y)); spacing = (size.Height / fontsize) + rnd.Next(0, stray_y); // Create a horizontal background lines for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing) using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y)); // Draw the actual answer graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y)); return bmp; } } } }