using System.Security.Cryptography; using System.Text; using Fleet.Core; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using MoShiro.Backend.Application; using MoShiro.Protocol; namespace MoShiro.Backend.Api; internal static class QueryHelpers { /// 查询的统一超时(默认 10 s):超时返回 503,而不是让请求一直挂着。 public static async Task GuardedAsync(HttpContext http, Func> work) { BackendOptions options = http.RequestServices.GetRequiredService(); using var timeout = CancellationTokenSource.CreateLinkedTokenSource(http.RequestAborted); timeout.CancelAfter(options.QueryTimeout); try { return await work(timeout.Token); } catch (OperationCanceledException) when (!http.RequestAborted.IsCancellationRequested) { return Problems.Create(http, MoShiroErrors.StorageUnavailable, null); } } /// /// 缓存策略:已经结束的绝对区间(to 早于当前时间 − 2 分钟)可以永久缓存,并带 ETag;相对时间段不缓存。 /// 返回非空时表示命中 If-None-Match,直接返回 304。 /// public static IResult? ApplyCaching(HttpContext http, QueryRange range, long nowMs, string material) { bool closed = !range.IsRelative && range.RequestedToMs < nowMs - (2 * Steps.Minute); if (!closed) { http.Response.Headers.CacheControl = "no-store"; return null; } string etag = ETagOf(http.Request.Path + http.Request.QueryString.Value + material); http.Response.Headers.CacheControl = "public, max-age=31536000, immutable"; http.Response.Headers.ETag = etag; return MatchesIfNoneMatch(http, etag) ? TypedResults.StatusCode(StatusCodes.Status304NotModified) : null; } /// 目录:max-age=300,并带 ETag。 public static IResult? ApplyCatalogCaching(HttpContext http, string material) { string etag = ETagOf(http.Request.Path + material); http.Response.Headers.CacheControl = "public, max-age=300"; http.Response.Headers.ETag = etag; return MatchesIfNoneMatch(http, etag) ? TypedResults.StatusCode(StatusCodes.Status304NotModified) : null; } public static string ETagOf(string material) => "\"" + Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(material)))[..32] + "\""; private static bool MatchesIfNoneMatch(HttpContext http, string etag) { foreach (string? candidate in http.Request.Headers.IfNoneMatch) { if (candidate is not null && candidate.Split(',', StringSplitOptions.TrimEntries).Contains(etag)) { return true; } } return false; } /// 线路 id(agent~probe,或只有一个 Probe 时的 Agent id)→ 可以展示的线路。否则 404 / 422(不暴露被隐藏的线路存在与否)。 public static async ValueTask> FindVisibleLineAsync(HttpContext http, LineService lines, string id, CancellationToken ct) { Result found = await lines.ResolveAsync(id, ct); if (!found.TryGet(out LineView? line, out FleetError? error)) { return Result.Fail(error); } return line.Agent.Agent.Status == Fleet.Server.NodeStatus.Active && (line.Agent.Agent.PublicVisible || PublicGuard.CanSeeHidden(http)) && line.Probe.RevokedAtMs is null ? found : Result.Fail(MoShiroErrors.AgentNotFound, "line not found"); } /// 找到可以展示的线路:已激活,并且(对外可见,或者访问者已认证)。否则 404(不暴露被隐藏的线路存在与否)。 public static async ValueTask> FindVisibleAgentAsync(HttpContext http, AgentService agents, string id, CancellationToken ct) { AgentView? view = await agents.GetAsync(new NodeId(id), ct); return view is { Agent.Status: Fleet.Server.NodeStatus.Active } && (view.Agent.PublicVisible || PublicGuard.CanSeeHidden(http)) ? Result.Ok(view) : Result.Fail(MoShiroErrors.AgentNotFound, "agent not found"); } }