Files
MoShiro/backend/src/MoShiro.Backend.Api/Common/QueryHelpers.cs
T
justme 2e146d4da7
ci/woodpecker/push/main-amd64-build Pipeline was successful
refactor!: rename the project from Monitor to MoShiro [CI]
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
2026-10-07 02:03:25 +08:00

96 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>查询的统一超时(默认 10 s):超时返回 503,而不是让请求一直挂着。</summary>
public static async Task<IResult> GuardedAsync(HttpContext http, Func<CancellationToken, Task<IResult>> work)
{
BackendOptions options = http.RequestServices.GetRequiredService<BackendOptions>();
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);
}
}
/// <summary>
/// 缓存策略:已经结束的绝对区间(<c>to</c> 早于当前时间 − 2 分钟)可以永久缓存,并带 ETag;相对时间段不缓存。
/// 返回非空时表示命中 If-None-Match,直接返回 304。
/// </summary>
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;
}
/// <summary>目录:<c>max-age=300</c>,并带 ETag。</summary>
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;
}
/// <summary>线路 id(<c>agent~probe</c>,或只有一个 Probe 时的 Agent id)→ 可以展示的线路。否则 404 / 422(不暴露被隐藏的线路存在与否)。</summary>
public static async ValueTask<Result<LineView>> FindVisibleLineAsync(HttpContext http, LineService lines, string id, CancellationToken ct)
{
Result<LineView> found = await lines.ResolveAsync(id, ct);
if (!found.TryGet(out LineView? line, out FleetError? error))
{
return Result<LineView>.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<LineView>.Fail(MoShiroErrors.AgentNotFound, "line not found");
}
/// <summary>找到可以展示的线路:已激活,并且(对外可见,或者访问者已认证)。否则 404(不暴露被隐藏的线路存在与否)。</summary>
public static async ValueTask<Result<AgentView>> 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<AgentView>.Ok(view)
: Result<AgentView>.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
}