Join codes need no passkey touch and carry only public values (genesis fingerprint, owner credentials, installer hashes). Nodes pin the genesis and owners at enrollment and only follow passkey-signed operator set versions. The panel's server key is listed in the set; pinned nodes require its signature and run the control link under AES-256-GCM. Password sessions carry no capabilities. The updater adopts a next_key from a signed manifest. Bump version to 0.2.0. [CI SKIP]
99 lines
3.2 KiB
C#
99 lines
3.2 KiB
C#
using System.Buffers;
|
|
using System.Net.WebSockets;
|
|
using System.Text.Json;
|
|
using Fleet.Rpc;
|
|
|
|
namespace Fleet.Server;
|
|
|
|
/// <summary>线上的帧:WebSocket 收帧、解析、状态名。</summary>
|
|
internal sealed partial class NodeConnection
|
|
{
|
|
private async ValueTask<Frame?> ReceiveFrameAsync(CancellationToken ct)
|
|
{
|
|
var writer = new ArrayBufferWriter<byte>(1024);
|
|
while (true)
|
|
{
|
|
Memory<byte> buffer = writer.GetMemory(4096);
|
|
ValueWebSocketReceiveResult result = await _socket.ReceiveAsync(buffer, ct);
|
|
if (result.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
writer.Advance(result.Count);
|
|
if (writer.WrittenCount > _host.Options.MaxFrameBytes)
|
|
{
|
|
_closeReason ??= "frame too large";
|
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig, "frame too large", CancellationToken.None);
|
|
return null;
|
|
}
|
|
|
|
if (result.EndOfMessage)
|
|
{
|
|
if (_cipher is { } cipher)
|
|
{
|
|
// 加密链路上只接受通过认证的二进制帧;明文、被改过的、重放的、乱序的一律断开
|
|
if (result.MessageType != WebSocketMessageType.Binary || !cipher.TryOpen(writer.WrittenSpan, out byte kind, out byte[] plain))
|
|
{
|
|
_closeReason ??= "frame failed authentication";
|
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.PolicyViolation, "frame failed authentication", CancellationToken.None);
|
|
return null;
|
|
}
|
|
|
|
return new Frame(kind == 0, plain);
|
|
}
|
|
|
|
return new Frame(result.MessageType == WebSocketMessageType.Text, writer.WrittenSpan.ToArray());
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryParse(byte[] data, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out RpcMessage? message)
|
|
{
|
|
try
|
|
{
|
|
message = JsonSerializer.Deserialize(data, FleetRpcContext.Default.RpcMessage);
|
|
return message is not null;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
message = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryDecode(string text, out byte[] data)
|
|
{
|
|
try
|
|
{
|
|
data = Convert.FromBase64String(text);
|
|
return true;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
data = [];
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string StatusName(NodeStatus status) => status switch
|
|
{
|
|
NodeStatus.Active => "active",
|
|
NodeStatus.Pending => "pending",
|
|
_ => "disabled",
|
|
};
|
|
|
|
private sealed record Frame(bool IsText, byte[] Data);
|
|
}
|
|
|
|
|
|
[System.Text.Json.Serialization.JsonSerializable(typeof(EmptyObject))]
|
|
internal sealed partial class FleetEmptyObjectContext : System.Text.Json.Serialization.JsonSerializerContext;
|
|
|
|
internal sealed class EmptyObject;
|
|
|
|
internal static class FleetEmptyObject
|
|
{
|
|
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<EmptyObject> Info => FleetEmptyObjectContext.Default.EmptyObject;
|
|
}
|