From 290d246a4fd19f58b84546bf36d73dbb21948f21 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Thu, 5 Mar 2026 14:38:13 +0100 Subject: [PATCH] add PoW captcha server implementation (based on capjs) --- zero/Resources/cap_wasm_bg_0_0_6.wasm | Bin 0 -> 36032 bytes zero/Security/CaptchaOptions.cs | 46 +++++++++++++++ zero/Security/CaptchaTagHelper.cs | 41 +++++++++++++ zero/Security/CaptchaValidator.cs | 30 ++++++++++ zero/Security/StaticCaptchaService.cs | 16 +++++ zero/Security/ValidatorExtensions.cs | 15 +++++ zero/Security/ZeroSecurityModule.cs | 55 ++++++++++++++++++ .../German/FluentValidationGermanLanguage.cs | 3 +- zero/Validation/ValidatorExtensions.cs | 2 +- zero/ZeroBuilder.cs | 2 + zero/zero.csproj | 5 ++ 11 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 zero/Resources/cap_wasm_bg_0_0_6.wasm create mode 100644 zero/Security/CaptchaOptions.cs create mode 100644 zero/Security/CaptchaTagHelper.cs create mode 100644 zero/Security/CaptchaValidator.cs create mode 100644 zero/Security/StaticCaptchaService.cs create mode 100644 zero/Security/ValidatorExtensions.cs create mode 100644 zero/Security/ZeroSecurityModule.cs diff --git a/zero/Resources/cap_wasm_bg_0_0_6.wasm b/zero/Resources/cap_wasm_bg_0_0_6.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e2134b54caf5aa5de2949f9ede4a2161bec6fb51 GIT binary patch literal 36032 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%0D(yL90=dpwc6 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 +{ + public static CaptchaLocalizationOptions English { get; } = new() + { + ["initial-state"] = "Verify you're human", + ["verifying-label"] = "Verifying...", + ["solved-label"] = "You're human", + ["error-label"] = "Error", + ["troubleshooting-label"] = "Troubleshooting", + ["wasm-disabled"] = "Enable WASM for significantly faster solving", + ["verify-aria-label"] = "Click to verify you're a human", + ["verifying-aria-label"] = "Verifying, please wait", + ["verified-aria-label"] = "Verified", + ["error-aria-label"] = "An error occurred, please try again", + }; + + public static CaptchaLocalizationOptions German { get; } = new() + { + ["initial-state"] = "Bestätige, dass du kein Bot bist", + ["verifying-label"] = "Wird überprüft...", + ["solved-label"] = "Erfolgreich", + ["error-label"] = "Fehler", + ["troubleshooting-label"] = "Fehlerbehebung", + ["wasm-disabled"] = "Aktiviere WASM für eine deutlich schnellere Lösung", + ["verify-aria-label"] = "Klicke, um zu bestätigen, dass du ein Mensch bist", + ["verifying-aria-label"] = "Wird überprüft, bitte warten", + ["verified-aria-label"] = "Bestätigt", + ["error-aria-label"] = "Ein Fehler ist aufgetreten, bitte versuche es erneut", + }; +} \ No newline at end of file diff --git a/zero/Security/CaptchaTagHelper.cs b/zero/Security/CaptchaTagHelper.cs new file mode 100644 index 00000000..795c90f3 --- /dev/null +++ b/zero/Security/CaptchaTagHelper.cs @@ -0,0 +1,41 @@ +using System.IO; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using zero.Security; + +namespace zero.TagHelpers; + +[HtmlTargetElement("app-captcha", TagStructure = TagStructure.NormalOrSelfClosing)] +public class CaptchaTagHelper(IOptionsMonitor options) : TagHelper +{ + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } = null!; + + private readonly CaptchaOptions _options = options.CurrentValue; + + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (!_options.Enabled) + { + output.SuppressOutput(); + return; + } + output.TagName = "cap-widget"; + output.Attributes.SetAttribute("class", "cap-widget"); + output.Attributes.SetAttribute("data-cap-api-endpoint", _options.Endpoint); + output.Attributes.SetAttribute("data-cap-hidden-field-name", _options.HiddenFieldName); + + string wasmFilePath =_options.Endpoint + "/cap.wasm"; + output.PreElement.AppendHtml($""); + + foreach ((string key, string value) in _options.Localization) + { + output.Attributes.SetAttribute($"data-cap-i18n-{key}", value); + } + } +} \ No newline at end of file diff --git a/zero/Security/CaptchaValidator.cs b/zero/Security/CaptchaValidator.cs new file mode 100644 index 00000000..c3dc1c03 --- /dev/null +++ b/zero/Security/CaptchaValidator.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using FluentValidation.Validators; +using PowCapServer.Abstractions; +using zero.Validation.Validators; + +namespace zero.Security; + +public sealed class CaptchaValidator : AsyncPropertyValidator +{ + public override string Name => "CaptchaValidator"; + + protected override string GetDefaultMessageTemplate(string errorCode) => Localized(errorCode, Name); + + public override async Task IsValidAsync(ValidationContext context, string value, CancellationToken cancellation) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ICaptchaService captchaService = StaticCaptchaService.Get(); + + if (captchaService is null) + { + return false; + } + + return await captchaService.ValidateCaptchaTokenAsync(value, cancellation); + } +} \ No newline at end of file diff --git a/zero/Security/StaticCaptchaService.cs b/zero/Security/StaticCaptchaService.cs new file mode 100644 index 00000000..1b791db6 --- /dev/null +++ b/zero/Security/StaticCaptchaService.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using PowCapServer.Abstractions; + +namespace zero.Security; + +internal static class StaticCaptchaService +{ + private static IServiceProvider Services { get; set; } + + public static void Configure(IServiceProvider services) + { + Services = services; + } + + public static ICaptchaService Get() => Services.GetRequiredService(); +} \ No newline at end of file diff --git a/zero/Security/ValidatorExtensions.cs b/zero/Security/ValidatorExtensions.cs new file mode 100644 index 00000000..006428fd --- /dev/null +++ b/zero/Security/ValidatorExtensions.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using zero.Security; + +namespace zero.Validation; + +public static partial class ValidatorExtensions +{ + /// + /// Validates a captcha token + /// + public static IRuleBuilderOptions Captcha(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetAsyncValidator(new CaptchaValidator()); + } +} diff --git a/zero/Security/ZeroSecurityModule.cs b/zero/Security/ZeroSecurityModule.cs new file mode 100644 index 00000000..b4fe573f --- /dev/null +++ b/zero/Security/ZeroSecurityModule.cs @@ -0,0 +1,55 @@ +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace zero.Security; + +internal class ZeroSecurityModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + IConfigurationSection captchaSection = configuration.GetSection("Zero:Captcha"); + services.AddOptions().Bind(captchaSection); + + services.AddPowCapServer(options => + { + CaptchaOptions captchaOptions = captchaSection.Get() ?? new CaptchaOptions(); + options.Default = captchaOptions; + }); + } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + IOptionsMonitor options = serviceProvider.GetRequiredService>(); + string endpointPrefix = options.CurrentValue.Endpoint; + + app.UseRouting(); + app.MapPowCapServer(endpointPrefix); + app.UseEndpoints(endpoints => + { + endpoints.MapGet(endpointPrefix + "/cap.wasm", async context => + { + Assembly assembly = typeof(ZeroSecurityModule).GetTypeInfo().Assembly; + Stream resource = assembly.GetManifestResourceStream("zero.Resources.cap_wasm_bg_0_0_6.wasm"); + + if (resource is null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + await using (resource) + { + context.Response.ContentType = "application/wasm"; + await resource.CopyToAsync(context.Response.Body, context.RequestAborted); + } + }); + }); + StaticCaptchaService.Configure(serviceProvider); + } +} \ No newline at end of file diff --git a/zero/Validation/German/FluentValidationGermanLanguage.cs b/zero/Validation/German/FluentValidationGermanLanguage.cs index f83bd567..d981f933 100644 --- a/zero/Validation/German/FluentValidationGermanLanguage.cs +++ b/zero/Validation/German/FluentValidationGermanLanguage.cs @@ -34,6 +34,7 @@ public class FluentValidationGermanLanguage { "MinimumLength_Simple", "Die Länge muss größer oder gleich {MinLength} sein." }, { "MaximumLength_Simple", "Die Länge muss kleiner oder gleich {MaxLength} sein." }, { "ExactLength_Simple", "Dieses Feld muss genau {MaxLength} lang sein." }, - { "InclusiveBetween_Simple", "Der Wert muss zwischen {From} and {To} sein." } + { "InclusiveBetween_Simple", "Der Wert muss zwischen {From} and {To} sein." }, + { "CaptchaValidator", "Das Catpcha wurde nicht erfolgreich gelöst." } }; } \ No newline at end of file diff --git a/zero/Validation/ValidatorExtensions.cs b/zero/Validation/ValidatorExtensions.cs index 99b23e69..e85c3e40 100644 --- a/zero/Validation/ValidatorExtensions.cs +++ b/zero/Validation/ValidatorExtensions.cs @@ -3,7 +3,7 @@ using zero.Validation.Validators; namespace zero.Validation; -public static class ValidatorExtensions +public static partial class ValidatorExtensions { /// /// Validate a color input as HEX (#aabbccdd or #aabbcc or #abc) diff --git a/zero/ZeroBuilder.cs b/zero/ZeroBuilder.cs index 4c251191..9b9a3643 100644 --- a/zero/ZeroBuilder.cs +++ b/zero/ZeroBuilder.cs @@ -9,6 +9,7 @@ using zero.Metadata; using zero.Mvc; using zero.Numbers; using zero.Routing; +using zero.Security; namespace zero; @@ -68,6 +69,7 @@ public class ZeroBuilder Modules.Add(); Modules.Add(); Modules.Add(); + Modules.Add(); Modules.ConfigureServices(services, configuration); diff --git a/zero/zero.csproj b/zero/zero.csproj index a7ce3195..f5253c99 100644 --- a/zero/zero.csproj +++ b/zero/zero.csproj @@ -16,7 +16,12 @@ + + + + + \ No newline at end of file