using System.Net; using System.Text.Json; using Fleet.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace Fleet.Server; public static class FleetEnrollmentEndpoints { private const int MaxBodyBytes = 64 * 1024; /// 注册端点:POST {path},使用加入令牌认证。需要在 DI 中注册 。 public static IEndpointRouteBuilder MapFleetEnrollment(this IEndpointRouteBuilder app, string path) { app.MapPost(path, HandleAsync) .WithName("fleet_enroll") .WithTags("agent") .WithSummary("Enroll a node with a join token") .DisableAntiforgery(); // 注册之前取信任链:节点先确认这个面板的操作员集合和委托确实是它固定的创世签出来的,再把自己的密钥交出去 app.MapPost(path + "/trust", HandleTrustAsync) .WithName("fleet_enroll_trust") .WithTags("agent") .WithSummary("The operator set chain and the grant of a join token, for the node to verify before it enrolls") .DisableAntiforgery(); return app; } private static async Task HandleTrustAsync(HttpContext http, FleetEnrollmentService service) { TrustRequest? request; try { request = await JsonSerializer.DeserializeAsync(http.Request.Body, EnrollmentJsonContext.Default.TrustRequest, http.RequestAborted); } catch (JsonException) { request = null; } if (request is null || string.IsNullOrEmpty(request.JoinToken)) { return Problem(StatusCodes.Status400BadRequest, FleetErrors.InvalidParams, "join_token is required"); } Result result = await service.GetTrustAsync(request.JoinToken, http.RequestAborted); if (!result.TryGet(out TrustBundle? bundle, out FleetError? error)) { return Problem(error.Code == FleetErrors.JoinTokenExpired ? StatusCodes.Status410Gone : StatusCodes.Status401Unauthorized, error.Code, error.Message); } http.Response.Headers.CacheControl = "no-store"; return Results.Json(bundle, EnrollmentJsonContext.Default.TrustBundle); } private static async Task HandleAsync(HttpContext http, FleetEnrollmentService service) { EnrollRequest? request; try { request = await ReadBodyAsync(http, http.RequestAborted); } catch (JsonException) { request = null; } if (request is null) { return Problem(StatusCodes.Status400BadRequest, FleetErrors.InvalidParams, "request body is not valid"); } Result result = await service.EnrollAsync(request, ClientIp(http), http.RequestAborted); if (result.TryGet(out EnrollResponse? response, out FleetError? error)) { return Results.Json(response, EnrollmentJsonContext.Default.EnrollResponse, statusCode: StatusCodes.Status201Created); } int status = error.Code == FleetErrors.JoinTokenInvalid ? StatusCodes.Status401Unauthorized : error.Code == FleetErrors.JoinTokenExpired ? StatusCodes.Status410Gone : error.Code == FleetErrors.ProofInvalid ? StatusCodes.Status403Forbidden : error.Code == FleetErrors.NameConflict ? StatusCodes.Status409Conflict : error.Code == FleetErrors.InvalidParams ? StatusCodes.Status400BadRequest : StatusCodes.Status500InternalServerError; return Problem(status, error.Code, error.Message); } private static string? ClientIp(HttpContext http) => http.RequestServices.GetService() is { } source ? source.Resolve(http.Connection.RemoteIpAddress, name => http.Request.Headers[name].Select(v => v ?? "")) : http.Connection.RemoteIpAddress?.ToString(); private static async Task ReadBodyAsync(HttpContext http, CancellationToken ct) { if (http.Request.ContentLength > MaxBodyBytes) { return null; } using var buffer = new MemoryStream(); byte[] chunk = new byte[4096]; int read; while ((read = await http.Request.Body.ReadAsync(chunk, ct)) > 0) { buffer.Write(chunk, 0, read); if (buffer.Length > MaxBodyBytes) { return null; } } return JsonSerializer.Deserialize(buffer.ToArray(), EnrollmentJsonContext.Default.EnrollRequest); } private static IResult Problem(int status, ErrorCode code, string detail) => Results.Json( new FleetProblem { Title = ((HttpStatusCode)status).ToString(), Status = status, Code = code.Value, Detail = detail }, EnrollmentJsonContext.Default.FleetProblem, contentType: "application/problem+json", statusCode: status); } public static class FleetEnrollmentServiceCollectionExtensions { public static IServiceCollection AddFleetEnrollment(this IServiceCollection services) => services.AddSingleton(); }