Files
MoShiro/backend/fleet/src/Fleet.Server.Link/FleetLinkHost.cs
T
justme 904ea2b0ae
ci/woodpecker/push/main-amd64-build Pipeline was successful
refactor: remove the runtime binary binding; every release registration path verifies the CI signature [CI]
The hash a node reports about itself cannot be trusted (a swapped binary can
copy the old hash), so the runtime binding only caught files that were replaced
by something that did not lie. Provenance stays where it can be verified: the
signed release manifest (digest) and the signed SHA256SUMS.

- Remove BinaryBindingService, the node_bindings table (schema v15 drops it),
  BinaryPolicy, the handshake hash/manifest fields and the admin binary column.
- Drop the per-platform "binaries" list from the release manifest, the hash
  scripts and the updater's installed.manifest copy.
- ReleaseService.PublishAsync now verifies the release signature, so the admin
  POST /releases route is no longer an unsigned path; tests sign with a test key.
- Docs: section 16 explains why the runtime check is gone.
2026-10-08 18:30:05 +08:00

187 lines
5.9 KiB
C#

using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Fleet.Core;
using Fleet.Rpc;
using Microsoft.Extensions.Logging;
namespace Fleet.Server;
/// <summary>
/// 服务端连接宿主:在线表 + 服务端发起的调用 + 状态推送通知。每个 WebSocket 对应一个 <see cref="NodeConnection"/>。
/// </summary>
public sealed class FleetLinkHost : IFleetConnections, IStateNotifier
{
private readonly ConcurrentDictionary<NodeId, NodeConnection> _connections = new();
internal FleetLinkHost(
IFleetStore store,
FleetServerOptions options,
TimeProvider time,
ILogger<FleetLinkHost> logger,
IEnumerable<IStateProvider> stateProviders,
IEnumerable<IInbox> inboxes,
IEnumerable<INodeEventSink> sinks,
FleetRouter router,
IServerIdentity? serverIdentity = null)
{
ServerIdentity = serverIdentity;
Store = store;
Options = options;
Time = time;
Logger = logger;
StateProviders = [.. stateProviders];
Inboxes = inboxes.ToDictionary(i => i.Channel);
Sinks = [.. sinks];
Router = router.Inner;
RoleOf = router.RoleOf;
}
internal IFleetStore Store { get; }
/// <summary>服务器的身份密钥;没有时节点连接用旧的明文认证。</summary>
internal IServerIdentity? ServerIdentity { get; }
internal FleetServerOptions Options { get; }
internal TimeProvider Time { get; }
internal ILogger<FleetLinkHost> Logger { get; }
internal IReadOnlyList<IStateProvider> StateProviders { get; }
internal IReadOnlyDictionary<byte, IInbox> Inboxes { get; }
internal IReadOnlyList<INodeEventSink> Sinks { get; }
internal RpcRouter<NodeCallContext> Router { get; }
internal Func<NodeRecord, string?>? RoleOf { get; }
public IReadOnlyCollection<NodeId> OnlineNodes => [.. _connections.Keys];
public bool IsOnline(NodeId node) => _connections.ContainsKey(node);
public NodeSessionInfo? GetSession(NodeId node) =>
_connections.TryGetValue(node, out NodeConnection? c) ? c.Session : null;
/// <summary>处理一条 WebSocket 连接,直到连接结束。</summary>
public async Task RunConnectionAsync(WebSocket socket, CancellationToken ct)
{
using var connection = new NodeConnection(this, socket);
await connection.RunAsync(ct);
}
/// <summary>返回被替换掉的旧连接(如果有)。</summary>
internal NodeConnection? Register(NodeConnection connection)
{
NodeConnection? replaced = null;
_connections.AddOrUpdate(
connection.NodeId,
connection,
(_, existing) =>
{
replaced = existing;
return connection;
});
return replaced;
}
internal void Unregister(NodeConnection connection) =>
_connections.TryRemove(new KeyValuePair<NodeId, NodeConnection>(connection.NodeId, connection));
public async ValueTask<Result<TResult>> RequestAsync<TParams, TResult>(
NodeId node,
string method,
TParams parameters,
JsonTypeInfo<TParams> paramsInfo,
JsonTypeInfo<TResult> resultInfo,
CancellationToken ct)
{
if (!_connections.TryGetValue(node, out NodeConnection? connection))
{
return Result<TResult>.Fail(FleetErrors.NodeOffline, "node is not connected");
}
Result<JsonElement> raw = await connection.RequestAsync(method, JsonSerializer.SerializeToElement(parameters, paramsInfo), ct);
if (!raw.TryGet(out JsonElement value, out FleetError? error))
{
return Result<TResult>.Fail(error);
}
try
{
TResult? result = value.Deserialize(resultInfo);
return result is null
? Result<TResult>.Fail(FleetErrors.Internal, "empty result")
: Result<TResult>.Ok(result);
}
catch (JsonException)
{
return Result<TResult>.Fail(FleetErrors.Internal, "result could not be parsed");
}
}
public async ValueTask<bool> NotifyAsync<TParams>(NodeId node, string method, TParams parameters, JsonTypeInfo<TParams> paramsInfo, CancellationToken ct)
{
if (!_connections.TryGetValue(node, out NodeConnection? connection))
{
return false;
}
return await connection.SendAsync(RpcMessage.Notification(method, parameters, paramsInfo), ct);
}
public async ValueTask CloseAllAsync(string reason)
{
foreach (NodeConnection connection in _connections.Values.ToArray())
{
await connection.CloseAsync(reason);
}
}
public async ValueTask DisconnectAsync(NodeId node, string reason, CancellationToken ct)
{
if (_connections.TryGetValue(node, out NodeConnection? connection))
{
await connection.RevokeAsync(reason, ct);
}
}
public void NotifyChanged(NodeId node)
{
if (_connections.TryGetValue(node, out NodeConnection? connection))
{
connection.SignalStateChanged();
}
}
public void NotifyAllChanged()
{
foreach (NodeConnection connection in _connections.Values)
{
connection.SignalStateChanged();
}
}
}
/// <summary>产品注册的 RPC 方法路由。</summary>
public sealed class FleetRouter
{
public FleetRouter(RpcRouter<NodeCallContext> inner, Func<NodeRecord, string?>? roleOf)
{
Inner = inner;
RoleOf = roleOf;
}
internal RpcRouter<NodeCallContext> Inner { get; }
internal Func<NodeRecord, string?>? RoleOf { get; }
/// <summary>已注册的入站方法名(只读,测试用它核对声明表)。</summary>
public IReadOnlyCollection<string> Methods => Inner.Methods;
public MethodTable Table => Inner.Table;
}