chore: initial project import

This commit is contained in:
justme committed 2026-10-04 21:46:39 +08:00
commit 26620707a0
347 files changed
+56172

No files matched your search

@@ -0,0 +1,110 @@
using System.Text.RegularExpressions;
using Fleet.Core;
using Fleet.Identity;
namespace Fleet.Server;
/// <summary>注册流程:校验令牌 → 校验持有证明 → 创建节点 → 触发产品挂钩。与 HTTP 无关,便于测试。</summary>
public sealed partial class FleetEnrollmentService
{
private readonly IFleetStore _store;
private readonly IEnumerable<IEnrollmentHook> _hooks;
private readonly FleetServerOptions _options;
private readonly TimeProvider _time;
public FleetEnrollmentService(IFleetStore store, IEnumerable<IEnrollmentHook> hooks, FleetServerOptions options, TimeProvider time)
{
_store = store;
_hooks = hooks;
_options = options;
_time = time;
}
public async ValueTask<Result<EnrollResponse>> EnrollAsync(EnrollRequest request, string? remoteIp, CancellationToken ct)
{
if (string.IsNullOrEmpty(request.JoinToken) || !NamePattern().IsMatch(request.Name))
{
return Result<EnrollResponse>.Fail(FleetErrors.InvalidParams, "join_token and a valid name are required");
}
if (!TryDecode(request.PublicKey, out byte[] publicKey) || !NodeIdentity.IsValidPublicKey(publicKey)
|| !TryDecode(request.TlsSpki, out byte[] tlsSpki) || tlsSpki.Length != 32
|| !TryDecode(request.Proof, out byte[] proof))
{
return Result<EnrollResponse>.Fail(FleetErrors.InvalidParams, "pubkey, tls_spki or proof is malformed");
}
byte[] tokenHash = TokenHash.Sha256(request.JoinToken);
JoinTokenRecord? token = await _store.FindJoinTokenAsync(tokenHash, ct);
long nowMs = _time.GetUtcNow().ToUnixTimeMilliseconds();
if (token is null || token.RevokedAtMs is not null || token.Uses >= token.MaxUses)
{
return Result<EnrollResponse>.Fail(FleetErrors.JoinTokenInvalid, "join token is invalid");
}
if (token.ExpiresAtMs <= nowMs)
{
return Result<EnrollResponse>.Fail(FleetErrors.JoinTokenExpired, "join token has expired");
}
if (!NodeIdentity.Verify(publicKey, proof, SignaturePurpose.Enroll, Convert.ToHexStringLower(tokenHash), request.PublicKey))
{
return Result<EnrollResponse>.Fail(FleetErrors.ProofInvalid, "proof of possession does not verify");
}
if (!await _store.TryConsumeJoinTokenAsync(token.Id, nowMs, ct))
{
return Result<EnrollResponse>.Fail(FleetErrors.JoinTokenInvalid, "join token is invalid");
}
// 同名节点已存在时进入 pending,由管理员选择“接管”或“作为新节点”。
bool nameTaken = await _store.FindEstablishedNodeByNameAsync(request.Name, ct) is not null;
NodeStatus status = token.AutoApprove && !nameTaken ? NodeStatus.Active : NodeStatus.Pending;
var node = new NewNode(
new NodeId(Ulid.NewPrefixed(_options.NodeIdPrefix, _time)),
request.Name,
status,
publicKey,
tlsSpki,
token.Labels,
request.Meta?.GetRawText(),
request.NodeVersion,
nowMs);
Result<NodeRecord> created = await _store.CreateNodeAsync(node, ct);
if (!created.TryGet(out NodeRecord? record, out FleetError? error))
{
return Result<EnrollResponse>.Fail(error);
}
var context = new EnrollmentContext(remoteIp, request.Meta);
foreach (IEnrollmentHook hook in _hooks)
{
await hook.OnEnrolledAsync(record, context, ct);
}
return Result<EnrollResponse>.Ok(new EnrollResponse
{
NodeId = record.Id.Value,
Status = record.Status == NodeStatus.Active ? "active" : "pending",
});
}
private static bool TryDecode(string text, out byte[] data)
{
try
{
data = Convert.FromBase64String(text);
return data.Length > 0;
}
catch (FormatException)
{
data = [];
return false;
}
}
[GeneratedRegex(@"^[\p{L}\p{M}\p{N}._-]{1,64}$")]
private static partial Regex NamePattern();
}