ci/woodpecker/push/main-amd64-build Pipeline was successful
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.
96 lines
4.4 KiB
C#
96 lines
4.4 KiB
C#
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");
|
||
}
|
||
}
|