using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using WebDrop.Core; namespace WebDrop.Api; public sealed class SiteverifyRequest { [JsonPropertyName("secret")] public required string Secret { get; init; } [JsonPropertyName("response")] public required string Response { get; init; } } public sealed class SiteverifyResponse { [JsonPropertyName("success")] public bool Success { get; init; } } // Backend-to-Cap DTOs are not part of the browser API contract. [JsonSerializable(typeof(SiteverifyRequest))] [JsonSerializable(typeof(SiteverifyResponse))] public sealed partial class CaptchaJson : JsonSerializerContext; /// /// 用 Cap(https://github.com/tiagozip/cap)校验创建频道的验证码。 /// 工作量证明的出题、求解、验证全部由 Cap 完成,本项目不实现任何 PoW。 /// /// 流程:浏览器上的 Cap widget 直接向 Cap 取挑战、求解、换得 token,随创建请求放进 X-WD-Cap; /// 这里只做一件事:把 token 交给 Cap 的 siteverify(与 reCAPTCHA / hCaptcha 兼容的接口)确认。 /// public sealed class CapCaptchaVerifier(HttpClient http, CaptchaOptions options, ILogger logger) : ICaptchaVerifier { // token 来自客户端,先限制长度,避免把任意大的内容转发给第三方 private const int MaxTokenLength = 2048; public bool Enabled => true; public async Task VerifyAsync(string? token, CancellationToken ct) { if (string.IsNullOrEmpty(token) || token.Length > MaxTokenLength) throw new WebDropException(ErrorCode.CaptchaRequired); var body = JsonSerializer.Serialize( new SiteverifyRequest { Secret = options.Secret, Response = token }, CaptchaJson.Default.SiteverifyRequest); try { using var request = new HttpRequestMessage(HttpMethod.Post, options.SiteverifyUrl) { Content = new StringContent(body, Encoding.UTF8, "application/json"), }; using var response = await http.SendAsync(request, ct).ConfigureAwait(false); // 非 2xx(站点密钥写错、Cap 出错等)是我们这边的配置或依赖问题,不是用户没通过验证 if (!response.IsSuccessStatusCode) { logger.LogError("Cap siteverify 返回了 {Status}", (int)response.StatusCode); throw new WebDropException(ErrorCode.CaptchaUnavailable); } var result = await JsonSerializer.DeserializeAsync( await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), CaptchaJson.Default.SiteverifyResponse, ct).ConfigureAwait(false); if (result is not { Success: true }) throw new WebDropException(ErrorCode.CaptchaRequired); } catch (Exception e) when (e is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException && !ct.IsCancellationRequested) { // 只记录异常类型:不记录 token、密钥或响应内容 logger.LogError("Cap siteverify 调用失败:{Type}", e.GetType().Name); throw new WebDropException(ErrorCode.CaptchaUnavailable); } } }