add PoW captcha server implementation (based on capjs)
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
using PowCapServer;
|
||||
|
||||
namespace zero.Security;
|
||||
|
||||
public class CaptchaOptions : PowCapConfig
|
||||
{
|
||||
public string Endpoint { get; set; } = "/api/captcha";
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public string HiddenFieldName { get; set; } = "CaptchaToken";
|
||||
|
||||
public CaptchaLocalizationOptions Localization { get; set; } = CaptchaLocalizationOptions.German;
|
||||
}
|
||||
|
||||
|
||||
public class CaptchaLocalizationOptions : Dictionary<string, string>
|
||||
{
|
||||
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",
|
||||
};
|
||||
}
|
||||
@@ -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<CaptchaOptions> 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($"<script>window.CAP_CUSTOM_WASM_URL = '{wasmFilePath}';</script>");
|
||||
|
||||
foreach ((string key, string value) in _options.Localization)
|
||||
{
|
||||
output.Attributes.SetAttribute($"data-cap-i18n-{key}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using PowCapServer.Abstractions;
|
||||
using zero.Validation.Validators;
|
||||
|
||||
namespace zero.Security;
|
||||
|
||||
public sealed class CaptchaValidator<T> : AsyncPropertyValidator<T, string>
|
||||
{
|
||||
public override string Name => "CaptchaValidator";
|
||||
|
||||
protected override string GetDefaultMessageTemplate(string errorCode) => Localized(errorCode, Name);
|
||||
|
||||
public override async Task<bool> IsValidAsync(ValidationContext<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ICaptchaService>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentValidation;
|
||||
using zero.Security;
|
||||
|
||||
namespace zero.Validation;
|
||||
|
||||
public static partial class ValidatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a captcha token
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Captcha<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetAsyncValidator(new CaptchaValidator<T>());
|
||||
}
|
||||
}
|
||||
@@ -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<CaptchaOptions>().Bind(captchaSection);
|
||||
|
||||
services.AddPowCapServer(options =>
|
||||
{
|
||||
CaptchaOptions captchaOptions = captchaSection.Get<CaptchaOptions>() ?? new CaptchaOptions();
|
||||
options.Default = captchaOptions;
|
||||
});
|
||||
}
|
||||
|
||||
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
|
||||
{
|
||||
IOptionsMonitor<CaptchaOptions> options = serviceProvider.GetRequiredService<IOptionsMonitor<CaptchaOptions>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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." }
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using zero.Validation.Validators;
|
||||
|
||||
namespace zero.Validation;
|
||||
|
||||
public static class ValidatorExtensions
|
||||
public static partial class ValidatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Validate a color input as HEX (#aabbccdd or #aabbcc or #abc)
|
||||
|
||||
@@ -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<ZeroNumberModule>();
|
||||
Modules.Add<ZeroRoutingModule>();
|
||||
Modules.Add<ZeroMetadataModule>();
|
||||
Modules.Add<ZeroSecurityModule>();
|
||||
|
||||
Modules.ConfigureServices(services, configuration);
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="PowCapServer.AspNetCore" Version="2.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\cap_wasm_bg_0_0_6.wasm" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user