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");
}
}