feat!: probes generate their own keys and enroll with a join token; node and peer keys are separate [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 16:57:34 +08:00
1 parent 1ff847e45a
commit a9f562441f
51 files changed
+505 -749

No files matched your search

@@ -40,7 +40,7 @@ public static class AgentEndpoints
group.MapGet("/join-tokens", ListTokensAsync)
.Api("join_tokens_list", "Join tokens", TokensTag).Returns<PageDto<JoinTokenDto>>().ProducesProblems(401, 403);
group.MapPost("/join-tokens", CreateTokenAsync)
.Api("join_tokens_create", "Create a join token; the plaintext is returned only once", TokensTag)
.Api("join_tokens_create", "Create a join token for an agent or a probe (role); the plaintext is returned only once", TokensTag)
.Returns<CreatedJoinTokenDto>(StatusCodes.Status201Created).ProducesProblems(400, 401, 403);
group.MapDelete("/join-tokens/{id}", RevokeTokenAsync)
.Api("join_tokens_revoke", "Revoke a join token; with purge=true delete the record for good", TokensTag)
@@ -48,16 +48,10 @@ public static class AgentEndpoints
group.MapGet("/probes", ListProbesAsync)
.Api("probes_list", "All probes, with the agents each one is connected to", ProbeTag).Returns<PageDto<ProbeDto>>().ProducesProblems(401, 403);
group.MapPost("/probes", CreateProbeAsync)
.Api("probes_create", "Create a probe identity; the private key is returned only once", ProbeTag)
.Returns<ProbeIdentityDto>(StatusCodes.Status201Created).ProducesProblems(400, 401, 403, 409);
group.MapGet("/probes/{id}", GetProbeAsync)
.Api("probes_get", "One probe", ProbeTag).Returns<ProbeDto>().ProducesProblems(401, 403, 404);
group.MapPatch("/probes/{id}", PatchProbeAsync)
.Api("probes_update", "Rename a probe", ProbeTag).Returns<ProbeDto>().ProducesProblems(400, 401, 403, 404, 409);
group.MapPost("/probes/{id}/rotate", RotateProbeAsync)
.Api("probes_rotate", "Give a probe a new key (same id): the old key stops working and its sessions are dropped; the new probe.json is returned only once", ProbeTag)
.Returns<ProbeIdentityDto>().ProducesProblems(401, 403, 404, 409);
group.MapPost("/probes/{id}/revoke", RevokeProbeAsync)
.Api("probes_revoke", "Revoke a probe: every agent drops its session and refuses it; history stays until it expires", ProbeTag)
.Returns<ProbeDto>().ProducesProblems(401, 403, 404);
@@ -122,7 +116,7 @@ public static class AgentEndpoints
{
string baseUrl = options.PublicUrl ?? $"{http.Request.Scheme}://{http.Request.Host}";
Result<CreatedJoinToken> result = await tokens.CreateAsync(
new Application.CreateJoinTokenRequest(request.Note, request.Labels, request.AutoApprove ?? true, request.MaxUses ?? 1, request.ExpiresInHours ?? 24), baseUrl, http.RequestAborted);
new Application.CreateJoinTokenRequest(request.Note, request.Labels, request.AutoApprove ?? true, request.MaxUses ?? 1, request.ExpiresInHours ?? 24, request.Role ?? JoinTokenService.AgentRole), baseUrl, http.RequestAborted);
if (!result.TryGet(out CreatedJoinToken? created, out FleetError? error))
{
return Problems.From(http, error);
@@ -150,19 +144,6 @@ public static class AgentEndpoints
private static async Task<IResult> ListProbesAsync(ProbeService probes, CancellationToken ct) =>
TypedResults.Json(new PageDto<ProbeDto>([.. (await probes.ListAsync(ct)).Select(Mappers.ToDto)], null), ApiJsonContext.Default.PageDtoProbeDto);
private static async Task<IResult> CreateProbeAsync(HttpContext http, ProbeService probes, BackendOptions options, CreateProbeRequest? request, CancellationToken ct)
{
Result<ProbeIdentityFile> result = await probes.CreateAsync(request?.Name, PanelUrl(http, options), ct);
if (!result.TryGet(out ProbeIdentityFile? file, out FleetError? error))
{
return Problems.From(http, error);
}
http.Response.Headers.CacheControl = "no-store";
http.Response.Headers.Location = "/api/admin/probes/" + file.ProbeId.Value;
return TypedResults.Json(ToIdentityDto(file), ApiJsonContext.Default.ProbeIdentityDto, statusCode: StatusCodes.Status201Created);
}
private static async Task<IResult> GetProbeAsync(HttpContext http, ProbeService probes, string id, CancellationToken ct) =>
await probes.GetAsync(id, ct) is { } view
? TypedResults.Json(Mappers.ToDto(view), ApiJsonContext.Default.ProbeDto)
@@ -179,18 +160,6 @@ public static class AgentEndpoints
return result.TryGet(out ProbeView? view, out FleetError? error) ? TypedResults.Json(Mappers.ToDto(view), ApiJsonContext.Default.ProbeDto) : Problems.From(http, error);
}
private static async Task<IResult> RotateProbeAsync(HttpContext http, ProbeService probes, BackendOptions options, string id, CancellationToken ct)
{
Result<ProbeIdentityFile> result = await probes.RotateAsync(id, PanelUrl(http, options), ct);
if (!result.TryGet(out ProbeIdentityFile? file, out FleetError? error))
{
return Problems.From(http, error);
}
http.Response.Headers.CacheControl = "no-store";
return TypedResults.Json(ToIdentityDto(file), ApiJsonContext.Default.ProbeIdentityDto);
}
private static async Task<IResult> RevokeProbeAsync(HttpContext http, ProbeService probes, string id, CancellationToken ct)
{
Result<ProbeView> result = await probes.RevokeAsync(id, ct);
@@ -213,10 +182,4 @@ public static class AgentEndpoints
return dropped ? TypedResults.NoContent() : Problems.Create(http, MoShiroErrors.ProbeOffline, "this probe has no session with this agent");
}
/// <summary>Probe 要连的面板地址:配置的公开地址,没有就用这次请求的地址。</summary>
private static string PanelUrl(HttpContext http, BackendOptions options) => (options.PublicUrl ?? $"{http.Request.Scheme}://{http.Request.Host}").TrimEnd('/');
private static ProbeIdentityDto ToIdentityDto(ProbeIdentityFile file) =>
new(file.ProbeId.Value, file.PrivateKey, file.Server, new ProbeLimitsDto(file.Limits.MaxTestMbps, file.Limits.MaxTotalPps));
}
@@ -154,7 +154,7 @@ public static class Mappers
new(t.Id.Value, t.Scope, t.AgentId?.Value, t.BaseTaskId?.Value, t.Kind, t.Target, t.Params, t.Schedule, t.Enabled, Time(t.UpdatedAtMs));
public static JoinTokenDto ToDto(JoinTokenInfo t) =>
new(t.Id, t.Note, t.Labels, t.AutoApprove, t.MaxUses, t.Uses, Time(t.ExpiresAtMs), Time(t.RevokedAtMs), Time(t.CreatedAtMs));
new(t.Id, t.Note, t.Labels, t.AutoApprove, t.MaxUses, t.Uses, Time(t.ExpiresAtMs), Time(t.RevokedAtMs), Time(t.CreatedAtMs), JoinTokenService.RoleOf(t.Kind));
public static ApiTokenDto ToDto(ApiTokenRecord t) => new(t.Id, t.Name, t.Scope, Time(t.CreatedAtMs), Time(t.LastUsedAtMs), Time(t.RevokedAtMs));
@@ -48,9 +48,10 @@ public sealed record Ipv6DiagnosticDto(bool? AddressConfigured, bool SocketSuppo
/// <summary>merge-patch:缺省或为 null 的字段不修改。</summary>
public sealed record AgentPatchRequest(string? Name, IReadOnlyList<string>? Labels, string? PublicHost, bool? PublicVisible, string? Ipv6Host = null);
public sealed record JoinTokenDto(string Id, string? Note, IReadOnlyList<string> Labels, bool AutoApprove, int MaxUses, int Uses, DateTimeOffset ExpiresAt, DateTimeOffset? RevokedAt, DateTimeOffset CreatedAt);
/// <param name="Role"><c>agent</c> 或 <c>probe</c>:用这个令牌注册出来的是哪种节点。</param>
public sealed record JoinTokenDto(string Id, string? Note, IReadOnlyList<string> Labels, bool AutoApprove, int MaxUses, int Uses, DateTimeOffset ExpiresAt, DateTimeOffset? RevokedAt, DateTimeOffset CreatedAt, string Role = "agent");
public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList<string>? Labels, bool? AutoApprove, int? MaxUses, int? ExpiresInHours);
public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList<string>? Labels, bool? AutoApprove, int? MaxUses, int? ExpiresInHours, string? Role = null);
/// <summary>令牌明文只在创建的响应里出现一次。</summary>
public sealed record InstallCommandsDto(string Linux, string Macos, string Windows);
@@ -72,13 +73,6 @@ public sealed record LinkMatrixDto(bool AutoLink, IReadOnlyList<LinkDto> Items);
public sealed record SetLinkRequest(bool? Enabled);
public sealed record ProbeLimitsDto(double MaxTestMbps, long MaxTotalPps);
/// <summary><c>probe.json</c>:含私钥,只在生成时返回一次。</summary>
public sealed record ProbeIdentityDto(string ProbeId, string PrivateKey, string Server, ProbeLimitsDto Limits);
public sealed record CreateProbeRequest(string? Name);
public sealed record PatchProbeRequest(string? Name);
public sealed record TaskDto(
@@ -56,8 +56,6 @@ namespace MoShiro.Backend.Api;
[JsonSerializable(typeof(PageDto<JoinTokenDto>))]
[JsonSerializable(typeof(CreateJoinTokenRequest))]
[JsonSerializable(typeof(CreatedJoinTokenDto))]
[JsonSerializable(typeof(ProbeIdentityDto))]
[JsonSerializable(typeof(CreateProbeRequest))]
[JsonSerializable(typeof(PatchProbeRequest))]
[JsonSerializable(typeof(ProbeDto))]
[JsonSerializable(typeof(PageDto<HostItemDto>))]