ln.http/ln.http.helpers/HttpCaptcha.cs

291 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using ln.http.route;
using ln.json;
using ln.json.mapping;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace ln.http.helpers
{
public class HttpCaptcha : HttpRoute
{
private Dictionary<Guid, CaptchaInstance> _captchaInstances = new Dictionary<Guid, CaptchaInstance>();
private CaptchaEndpoints _captchaEndpoints;
public HtmlColor[] CaptchaColors { get; } = new HtmlColor[]
{
HtmlColor.Red, HtmlColor.Green, HtmlColor.Blue, HtmlColor.Yellow, HtmlColor.Violett, HtmlColor.turquoise
};
public Random _random = new Random(Environment.TickCount + Thread.CurrentThread.GetHashCode());
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(10);
public int SolutionLength { get; set; }
public int GroupLength { get; set; }
public HttpCaptcha(HttpRouter httpRouter, string mapPath)
:this(httpRouter, mapPath, 25, 5)
{}
public HttpCaptcha(HttpRouter httpRouter, string mapPath, int groupLength, int solutionLength)
: base(HttpMethod.ANY, mapPath)
{
_captchaEndpoints = new CaptchaEndpoints(this);
_routerDelegate = _captchaEndpoints.RouteRequest;
GroupLength = groupLength;
SolutionLength = solutionLength;
httpRouter.Map(this);
}
public CaptchaInstance CreateInstance() => new CaptchaInstance(this);
public bool Authorize(HttpRequest httpRequest)
{
return false;
}
class CaptchaEndpoints : HttpEndpointController
{
private HttpCaptcha _httpCaptcha;
public CaptchaEndpoints(HttpCaptcha httpCaptcha)
{
_httpCaptcha = httpCaptcha;
}
[Map(HttpMethod.GET,"")]
public HttpResponse GetCaptcha()
{
return HttpResponse
.OK()
.Content(
_httpCaptcha
.CreateInstance()
.Json()
);
}
[Map(HttpMethod.GET, "/:instance/:combination")]
public HttpResponse DrawCombination(Guid instance, int combination)
{
int imageSize = 48;
if (!_httpCaptcha._captchaInstances.TryGetValue(instance, out CaptchaInstance captchaInstance) ||
((combination < 0) || (combination >= captchaInstance.Combinations.Length)))
return HttpResponse.NotFound();
CaptchaCombination captchaCombination = captchaInstance.Combinations[combination];
Image<Rgb24> combinationImage = new Image<Rgb24>(imageSize, imageSize, new Rgb24(255,255,255));
IPath combinationPath = null;
switch (captchaCombination.Form)
{
case CaptchaForm.Stern:
combinationPath = new Star((float)(imageSize / 2.0),
(float)(imageSize / 2.0), captchaCombination.Corners,
(float)(imageSize / 6.0), (float)(imageSize / 2.3));
break;
case CaptchaForm.Vieleck:
combinationPath = new RegularPolygon((float)(imageSize / 2.0),
(float)(imageSize / 2.0), captchaCombination.Corners,
(float)(imageSize / 2.3));
break;
default:
return HttpResponse.InternalServerError();
}
combinationImage.Mutate(x => x.Fill(captchaCombination.Color, combinationPath.RotateDegree(_httpCaptcha._random.Next(360))));
StreamedContent streamedContent = new StreamedContent("image/png");
combinationImage.SaveAsPng(streamedContent.ContentStream);
HttpResponse httpResponse = HttpResponse.OK().Content(streamedContent);
return httpResponse;
}
[Map(HttpMethod.POST, "/:instance")]
public HttpResponse Solve(Guid instance, [HttpArgumentSource(HttpArgumentSource.CONTENT)] JSONValue body)
{
if (!_httpCaptcha._captchaInstances.TryGetValue(instance, out CaptchaInstance captchaInstance))
return HttpResponse.NotFound();
if (body is JSONArray jsonSolution)
{
int[] solution = JSONMapper.DefaultMapper.FromJson<int[]>(jsonSolution);
if (captchaInstance.Solve(solution))
{
return HttpResponse.NoContent();
}
return HttpResponse.Forbidden();
}
else
{
return HttpResponse.BadRequest();
}
}
}
public class CaptchaInstance : IDisposable
{
public Guid Guid { get; }
public CaptchaCombination[] Combinations { get; private set; }
public int[] Solution { get; private set; }
private HttpCaptcha _httpCaptcha;
private Timer _timer;
public CaptchaInstance(HttpCaptcha httpCaptcha)
{
_httpCaptcha = httpCaptcha;
Guid = Guid.NewGuid();
_httpCaptcha._captchaInstances.Add(Guid, this);
_timer = new Timer((state) => Dispose(), null, _httpCaptcha.Timeout, TimeSpan.Zero);
Initialize();
}
private void Initialize()
{
HashSet<CaptchaCombination> hashSet = new HashSet<CaptchaCombination>();
while (hashSet.Count < _httpCaptcha.GroupLength)
hashSet.Add(new CaptchaCombination(this._httpCaptcha));
List<CaptchaCombination> combinationPool = hashSet.ToList();
Combinations = new CaptchaCombination[combinationPool.Count];
for (int n = 0; n < Combinations.Length; n++)
{
Combinations[n] = combinationPool[_httpCaptcha._random.Next(combinationPool.Count)];
combinationPool.Remove(Combinations[n]);
}
hashSet.Clear();
while (hashSet.Count < _httpCaptcha.SolutionLength)
hashSet.Add(Combinations[_httpCaptcha._random.Next(Combinations.Length)]);
Solution = hashSet.Select(cs => Array.IndexOf(Combinations, cs)).ToArray();
Console.WriteLine(String.Join('-', Solution));
Array.Sort(Solution);
Console.WriteLine(String.Join('-', Solution));
}
public bool Solve(int[] possibleSolution)
{
lock (this)
{
try
{
possibleSolution = possibleSolution.Distinct().ToArray();
Array.Sort(possibleSolution);
return Solution.SequenceEqual(possibleSolution);
}
finally
{
Dispose();
}
}
}
public JSONObject Json()
{
JSONObject json = new JSONObject()
.Add("uuid", Guid)
.Add("questions", Solution.Select(n => Combinations[n].ToString()).ToArray())
.Add("group", Combinations.Length);
;
return json;
}
public void Dispose()
{
lock (this)
{
_timer.Dispose();
_httpCaptcha?._captchaInstances.Remove(Guid);
}
}
public override string ToString() => String.Join(", ", Solution.Select(n => Combinations[n].ToString()));
}
public class CaptchaCombination
{
public HtmlColor Color { get; }
public int Corners { get; }
public CaptchaForm Form { get; }
public CaptchaCombination(HttpCaptcha httpCaptcha)
{
Color = httpCaptcha.CaptchaColors[httpCaptcha._random.Next(httpCaptcha.CaptchaColors.Length)];
Corners = 4 + httpCaptcha._random.Next(4);
CaptchaForm[] captchaForms = Enum.GetValues<CaptchaForm>();
Form = captchaForms[httpCaptcha._random.Next(captchaForms.Length)];
}
public override bool Equals(object? obj) => obj is CaptchaCombination other && Color.Equals(other.Color) &&
Corners.Equals(other.Corners) && Form.Equals(other.Form);
public override int GetHashCode() => Color.GetHashCode() ^ Corners ^ Form.GetHashCode();
public override string ToString() => $"ein {Form} mit {Corners} {(Form == CaptchaForm.Stern ? "Strahlen" : "Ecken")} in {Color.Name}";
}
public enum CaptchaForm
{
Stern,
Vieleck
}
public class HtmlColor
{
public static readonly HtmlColor Red = new HtmlColor(255, 0, 0, "Rot");
public static readonly HtmlColor Green = new HtmlColor(0, 255, 0, "Grün");
public static readonly HtmlColor Blue = new HtmlColor(0, 0, 255, "Blau");
public static readonly HtmlColor Yellow = new HtmlColor(240, 240, 0, "Gelb");
public static readonly HtmlColor Violett = new HtmlColor(255, 0, 255, "Lila");
public static readonly HtmlColor turquoise = new HtmlColor(0, 255, 255, "Türkis");
public byte R { get; }
public byte G { get; }
public byte B { get; }
public string Name { get; }
public HtmlColor(byte r, byte g, byte b) : this(r, g, b, null)
{
}
public HtmlColor(byte r, byte g, byte b, string name)
{
R = r;
G = g;
B = b;
Name = name;
}
public override string ToString() => $"#{Red:X2}{Green:X2}{Blue:X2}";
public static implicit operator Color(HtmlColor htmlColor) =>
new Rgb24(htmlColor.R, htmlColor.G, htmlColor.B);
}
}
}