// 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;
}
}
}
}