- new projects WebDrop.FileApi (file service), WebDrop.FileProtocol (shared tickets and receipts) and WebDrop.Sync (WebDAV) - file content of any size goes to the file service; chat, avatars and space structure stay on the main backend - tickets issued by the main backend, receipts by the file service, deletion queue and daily reconciliation - daily database snapshot and file sync to external storage (OpenList/WebDAV) with a 30 day retention - reorganize Core, Storage and Api into feature folders (namespaces unchanged) - web client uploads file frames to the file service and reads them with tickets
76 lines
3.2 KiB
C#
76 lines
3.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 用 Cap(https://github.com/tiagozip/cap)校验创建频道的验证码。
|
|
/// 工作量证明的出题、求解、验证全部由 Cap 完成,本项目不实现任何 PoW。
|
|
///
|
|
/// 流程:浏览器上的 Cap widget 直接向 Cap 取挑战、求解、换得 token,随创建请求放进 X-WD-Cap;
|
|
/// 这里只做一件事:把 token 交给 Cap 的 siteverify(与 reCAPTCHA / hCaptcha 兼容的接口)确认。
|
|
/// </summary>
|
|
public sealed class CapCaptchaVerifier(HttpClient http, CaptchaOptions options, ILogger<CapCaptchaVerifier> 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);
|
|
}
|
|
}
|
|
}
|