567 lines
26 KiB
C#
567 lines
26 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GameCult.Caching;
|
|
using GameCult.Networking;
|
|
|
|
namespace GameCult.Mesh
|
|
{
|
|
/// <summary>Identity served by one dedicated content listener.</summary>
|
|
public sealed class CultMeshTcpContentServerOptions
|
|
{
|
|
public string VerseId { get; set; } = string.Empty;
|
|
public string AuthorityRuntimeId { get; set; } = string.Empty;
|
|
public string RouteGeneration { get; set; } = string.Empty;
|
|
public CultMeshSessionProofSigner? ProofSigner { get; set; }
|
|
|
|
internal void Validate()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(VerseId)) throw new InvalidOperationException("Content Verse identity is required.");
|
|
if (string.IsNullOrWhiteSpace(AuthorityRuntimeId)) throw new InvalidOperationException("Content authority runtime identity is required.");
|
|
if (string.IsNullOrWhiteSpace(RouteGeneration)) throw new InvalidOperationException("Content route generation is required.");
|
|
}
|
|
}
|
|
|
|
/// <summary>Creates dedicated TCP streams for immutable CultMesh content.</summary>
|
|
public sealed class CultMeshTcpContentTransportConnector : ICultMeshContentTransportConnector
|
|
{
|
|
public const string Scheme = "cultmesh-content+tcp";
|
|
private readonly TimeSpan _connectTimeout;
|
|
private int _connectCount;
|
|
|
|
public CultMeshTcpContentTransportConnector(TimeSpan? connectTimeout = null)
|
|
{
|
|
_connectTimeout = connectTimeout ?? TimeSpan.FromSeconds(5);
|
|
if (_connectTimeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(connectTimeout));
|
|
}
|
|
|
|
public string ConnectorId => "tcp-content";
|
|
public int Priority => 0;
|
|
public int ConnectCount => Volatile.Read(ref _connectCount);
|
|
|
|
public bool CanConnect(CultMeshTransportCandidate candidate) =>
|
|
TryParseEndpoint(candidate?.Endpoint, out _, out _);
|
|
|
|
public async Task<ICultMeshContentTransport> ConnectAsync(
|
|
CultMeshTransportCandidate candidate,
|
|
CultMeshSessionTarget target,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (candidate == null) throw new ArgumentNullException(nameof(candidate));
|
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
|
if (!TryParseEndpoint(candidate.Endpoint, out var host, out var port))
|
|
throw new NotSupportedException($"TCP content connector does not support '{candidate.Endpoint}'.");
|
|
|
|
var client = new TcpClient();
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeout.CancelAfter(_connectTimeout);
|
|
try
|
|
{
|
|
await CultMeshTcpContentWire.AwaitAsync(client.ConnectAsync(host, port), timeout.Token)
|
|
.ConfigureAwait(false);
|
|
await CultMeshTcpContentWire.VerifyAuthorityAsync(
|
|
client.GetStream(), target, candidate, timeout.Token).ConfigureAwait(false);
|
|
Interlocked.Increment(ref _connectCount);
|
|
return new CultMeshTcpContentTransport(candidate.Endpoint, client, target, candidate.Generation);
|
|
}
|
|
catch
|
|
{
|
|
client.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
internal static bool TryParseEndpoint(string? endpoint, out string host, out int port)
|
|
{
|
|
host = string.Empty;
|
|
port = 0;
|
|
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) ||
|
|
!string.Equals(uri.Scheme, Scheme, StringComparison.OrdinalIgnoreCase) ||
|
|
string.IsNullOrWhiteSpace(uri.Host) || uri.Port <= 0)
|
|
return false;
|
|
host = uri.Host;
|
|
port = uri.Port;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal sealed class CultMeshTcpContentTransport : ICultMeshContentTransport, ICultMeshVerifiedTransport
|
|
{
|
|
private readonly TcpClient _client;
|
|
private readonly NetworkStream _stream;
|
|
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
|
private readonly ConcurrentDictionary<string, PendingCopy> _pending = new(StringComparer.Ordinal);
|
|
private readonly CancellationTokenSource _shutdown = new();
|
|
private readonly Task _receiveLoop;
|
|
private readonly string _verseId;
|
|
private readonly string _authorityRuntimeId;
|
|
private readonly string _routeGeneration;
|
|
private bool _disposed;
|
|
|
|
public CultMeshTcpContentTransport(
|
|
string endpoint,
|
|
TcpClient client,
|
|
CultMeshSessionTarget target,
|
|
string routeGeneration)
|
|
{
|
|
Endpoint = endpoint;
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
_verseId = target?.VerseId ?? throw new ArgumentNullException(nameof(target));
|
|
_authorityRuntimeId = target.AuthorityRuntimeId;
|
|
_routeGeneration = routeGeneration ?? string.Empty;
|
|
_stream = client.GetStream();
|
|
_receiveLoop = ReceiveLoopAsync(_shutdown.Token);
|
|
}
|
|
|
|
public string TransportId => "tcp-content";
|
|
public string Endpoint { get; }
|
|
|
|
public bool IsVerifiedFor(
|
|
string verseId,
|
|
string authorityRuntimeId,
|
|
string protocolId,
|
|
string routeGeneration) =>
|
|
string.Equals(_verseId, verseId, StringComparison.Ordinal) &&
|
|
string.Equals(_authorityRuntimeId, authorityRuntimeId, StringComparison.Ordinal) &&
|
|
string.Equals(CultMeshProtocols.Content.Value, protocolId, StringComparison.Ordinal) &&
|
|
string.Equals(_routeGeneration, routeGeneration, StringComparison.Ordinal);
|
|
|
|
public async Task CopyChunkToAsync(
|
|
CultMeshCdnChunkRef chunk,
|
|
Stream destination,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_disposed) throw new ObjectDisposedException(nameof(CultMeshTcpContentTransport));
|
|
if (chunk == null) throw new ArgumentNullException(nameof(chunk));
|
|
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
|
if (!destination.CanWrite) throw new ArgumentException("Content destination must be writable.", nameof(destination));
|
|
|
|
var messageId = Guid.NewGuid().ToString("N");
|
|
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var pending = new PendingCopy(destination, completion, cancellationToken);
|
|
if (!_pending.TryAdd(messageId, pending))
|
|
throw new InvalidOperationException("Duplicate TCP content request identity.");
|
|
try
|
|
{
|
|
var request = new CultMeshContentChunkRequestMessage
|
|
{
|
|
MessageId = messageId,
|
|
ChunkHash = CultMeshCdn.NormalizeHash(chunk.ChunkHash, nameof(chunk.ChunkHash)),
|
|
RecordKey = string.IsNullOrWhiteSpace(chunk.RecordKey)
|
|
? CultMeshCdnArtifactChunk.CreateRecordKey(chunk.ChunkHash).Value
|
|
: chunk.RecordKey,
|
|
ExpectedSizeBytes = chunk.SizeBytes
|
|
};
|
|
var payload = CultNetSchemaMessageSerialization.Serialize(request);
|
|
await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
await CultMeshTcpContentWire.WriteFrameAsync(_stream, payload, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
_sendLock.Release();
|
|
}
|
|
await CultMeshTcpContentWire.AwaitAsync(completion.Task, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
_pending.TryRemove(messageId, out _);
|
|
pending.Dispose();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_shutdown.Cancel();
|
|
_client.Dispose();
|
|
var error = new ObjectDisposedException(nameof(CultMeshTcpContentTransport));
|
|
foreach (var pending in _pending.Values) pending.Completion.TrySetException(error);
|
|
_pending.Clear();
|
|
_sendLock.Dispose();
|
|
_shutdown.Dispose();
|
|
}
|
|
|
|
private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
var headerBytes = await CultMeshTcpContentWire.ReadFrameAsync(_stream, cancellationToken).ConfigureAwait(false);
|
|
var response = CultNetSchemaMessageSerialization.Deserialize(headerBytes) as CultMeshContentChunkResponseMessage
|
|
?? throw new InvalidDataException("TCP content response used an unexpected schema.");
|
|
if (!_pending.TryGetValue(response.MessageId ?? string.Empty, out var pending))
|
|
throw new InvalidDataException("TCP content response did not match an active request.");
|
|
if (!response.Found)
|
|
{
|
|
pending.Completion.TrySetException(new FileNotFoundException(response.Error, response.ChunkHash));
|
|
continue;
|
|
}
|
|
await CultMeshTcpContentWire.CopyExactlyAsync(
|
|
_stream,
|
|
pending.Destination,
|
|
response.SizeBytes,
|
|
pending.CancellationToken).ConfigureAwait(false);
|
|
pending.Completion.TrySetResult(true);
|
|
}
|
|
}
|
|
catch (Exception error) when (!_disposed)
|
|
{
|
|
foreach (var pending in _pending.Values) pending.Completion.TrySetException(error);
|
|
}
|
|
}
|
|
|
|
private sealed class PendingCopy : IDisposable
|
|
{
|
|
private readonly CancellationTokenRegistration _registration;
|
|
|
|
public PendingCopy(
|
|
Stream destination,
|
|
TaskCompletionSource<bool> completion,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Destination = destination;
|
|
Completion = completion;
|
|
CancellationToken = cancellationToken;
|
|
_registration = cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
|
|
}
|
|
|
|
public Stream Destination { get; }
|
|
public TaskCompletionSource<bool> Completion { get; }
|
|
public CancellationToken CancellationToken { get; }
|
|
public void Dispose() => _registration.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serves content-addressed chunks over a dedicated TCP byte stream. Typed headers
|
|
/// remain CultNet schema messages; chunk bytes are streamed outside those messages.
|
|
/// </summary>
|
|
public sealed class CultMeshTcpContentServer : IDisposable
|
|
{
|
|
private readonly TcpListener _listener;
|
|
private readonly Func<string, CultMeshCdnArtifactChunk?> _resolveChunk;
|
|
private readonly Func<string, bool> _canServeHash;
|
|
private readonly CultMeshTcpContentServerOptions _options;
|
|
private readonly CancellationTokenSource _shutdown = new();
|
|
private readonly ConcurrentDictionary<TcpClient, byte> _clients = new();
|
|
private readonly Task _acceptLoop;
|
|
private long _chunkRequestsServed;
|
|
private bool _disposed;
|
|
|
|
public CultMeshTcpContentServer(
|
|
TcpListener listener,
|
|
CultCache content,
|
|
CultMeshTcpContentServerOptions options,
|
|
Func<string, bool>? canServeHash = null)
|
|
: this(listener, CreateCacheResolver(content), options, canServeHash)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a dedicated TCP content server over provider-owned immutable chunks.
|
|
/// The resolver receives a normalized SHA-256 hash and may return <see langword="null"/>
|
|
/// when that chunk is unavailable.
|
|
/// </summary>
|
|
public CultMeshTcpContentServer(
|
|
TcpListener listener,
|
|
Func<string, CultMeshCdnArtifactChunk?> resolveChunk,
|
|
CultMeshTcpContentServerOptions options,
|
|
Func<string, bool>? canServeHash = null)
|
|
{
|
|
_listener = listener ?? throw new ArgumentNullException(nameof(listener));
|
|
_resolveChunk = resolveChunk ?? throw new ArgumentNullException(nameof(resolveChunk));
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_options.Validate();
|
|
_canServeHash = canServeHash ?? (_ => true);
|
|
_listener.Start();
|
|
_acceptLoop = AcceptLoopAsync(_shutdown.Token);
|
|
}
|
|
|
|
public IPEndPoint LocalEndPoint => (IPEndPoint)_listener.LocalEndpoint;
|
|
public long ChunkRequestsServed => Interlocked.Read(ref _chunkRequestsServed);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_shutdown.Cancel();
|
|
_listener.Stop();
|
|
foreach (var client in _clients.Keys) client.Dispose();
|
|
_clients.Clear();
|
|
_shutdown.Dispose();
|
|
}
|
|
|
|
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
|
|
_clients.TryAdd(client, 0);
|
|
_ = ServeClientAsync(client, cancellationToken);
|
|
}
|
|
}
|
|
catch (Exception) when (_disposed || cancellationToken.IsCancellationRequested)
|
|
{
|
|
}
|
|
}
|
|
|
|
private async Task ServeClientAsync(TcpClient client, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using var stream = client.GetStream();
|
|
if (!await AcceptAuthorityAsync(stream, cancellationToken).ConfigureAwait(false)) return;
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
var requestBytes = await CultMeshTcpContentWire.ReadFrameAsync(stream, cancellationToken).ConfigureAwait(false);
|
|
var request = CultNetSchemaMessageSerialization.Deserialize(requestBytes) as CultMeshContentChunkRequestMessage
|
|
?? throw new InvalidDataException("TCP content request used an unexpected schema.");
|
|
await SendChunkAsync(stream, request, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception) when (_disposed || cancellationToken.IsCancellationRequested || !client.Connected)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
_clients.TryRemove(client, out _);
|
|
client.Dispose();
|
|
}
|
|
}
|
|
|
|
private async Task<bool> AcceptAuthorityAsync(Stream stream, CancellationToken cancellationToken)
|
|
{
|
|
var requestBytes = await CultMeshTcpContentWire.ReadFrameAsync(stream, cancellationToken).ConfigureAwait(false);
|
|
var request = CultNetSchemaMessageSerialization.Deserialize(requestBytes) as CultMeshSessionOpenMessage
|
|
?? throw new InvalidDataException("TCP content session did not begin with an authority handshake.");
|
|
var accepted = !string.IsNullOrWhiteSpace(request.MessageId) &&
|
|
!string.IsNullOrWhiteSpace(request.SourceRuntimeId) &&
|
|
IsNonce(request.ClientNonce) &&
|
|
string.Equals(request.VerseId, _options.VerseId, StringComparison.Ordinal) &&
|
|
string.Equals(request.AuthorityRuntimeId, _options.AuthorityRuntimeId, StringComparison.Ordinal) &&
|
|
string.Equals(request.ProtocolId, CultMeshProtocols.Content.Value, StringComparison.Ordinal) &&
|
|
string.Equals(request.RouteGeneration, _options.RouteGeneration, StringComparison.Ordinal);
|
|
var response = new CultMeshSessionAcceptedMessage
|
|
{
|
|
MessageId = request.MessageId,
|
|
Accepted = accepted,
|
|
VerseId = _options.VerseId,
|
|
AuthorityRuntimeId = _options.AuthorityRuntimeId,
|
|
ProtocolId = CultMeshProtocols.Content.Value,
|
|
RouteGeneration = _options.RouteGeneration,
|
|
ClientNonce = request.ClientNonce ?? string.Empty,
|
|
ProviderKeyId = accepted ? _options.ProofSigner?.ProviderKeyId ?? string.Empty : string.Empty,
|
|
ProviderSignature = accepted && _options.ProofSigner != null
|
|
? _options.ProofSigner.Sign(request)
|
|
: string.Empty,
|
|
Error = accepted ? null : "Content session target does not match this authority route."
|
|
};
|
|
await CultMeshTcpContentWire.WriteFrameAsync(
|
|
stream,
|
|
CultNetSchemaMessageSerialization.Serialize(response),
|
|
cancellationToken).ConfigureAwait(false);
|
|
return accepted;
|
|
}
|
|
|
|
private static bool IsNonce(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return false;
|
|
try { return Convert.FromBase64String(value).Length == 32; }
|
|
catch (FormatException) { return false; }
|
|
}
|
|
|
|
private async Task SendChunkAsync(
|
|
Stream stream,
|
|
CultMeshContentChunkRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var response = new CultMeshContentChunkResponseMessage
|
|
{
|
|
MessageId = request.MessageId,
|
|
ChunkHash = request.ChunkHash,
|
|
Payload = Array.Empty<byte>()
|
|
};
|
|
CultMeshCdnArtifactChunk? chunk = null;
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.MessageId))
|
|
throw new InvalidDataException("Content chunk request requires a message identity.");
|
|
var hash = CultMeshCdn.NormalizeHash(request.ChunkHash, nameof(request.ChunkHash));
|
|
if (!_canServeHash(hash))
|
|
throw new UnauthorizedAccessException("Content access policy rejected the requested hash.");
|
|
var key = CultMeshCdnArtifactChunk.CreateRecordKey(hash);
|
|
if (!string.IsNullOrWhiteSpace(request.RecordKey) &&
|
|
!string.Equals(request.RecordKey, key.Value, StringComparison.Ordinal))
|
|
throw new InvalidDataException("Content chunk record key disagrees with its content hash.");
|
|
chunk = _resolveChunk(hash)
|
|
?? throw new FileNotFoundException("Content chunk is not available.", key.Value);
|
|
CultMeshCdn.ValidateChunkPayload(new CultMeshCdnChunkRef
|
|
{
|
|
ChunkHash = hash,
|
|
RecordKey = key.Value,
|
|
SizeBytes = request.ExpectedSizeBytes
|
|
}, chunk);
|
|
response.Found = true;
|
|
response.ChunkHash = hash;
|
|
response.SizeBytes = chunk.SizeBytes;
|
|
Interlocked.Increment(ref _chunkRequestsServed);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
response.Found = false;
|
|
response.Error = error.GetType().Name + ": " + error.Message;
|
|
}
|
|
|
|
await CultMeshTcpContentWire.WriteFrameAsync(
|
|
stream,
|
|
CultNetSchemaMessageSerialization.Serialize(response),
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (chunk != null)
|
|
{
|
|
await stream.WriteAsync(chunk.Payload, 0, chunk.Payload.Length, cancellationToken).ConfigureAwait(false);
|
|
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static Func<string, CultMeshCdnArtifactChunk?> CreateCacheResolver(CultCache content)
|
|
{
|
|
if (content == null) throw new ArgumentNullException(nameof(content));
|
|
return hash => content.Get<CultMeshCdnArtifactChunk>(CultMeshCdnArtifactChunk.CreateRecordKey(hash));
|
|
}
|
|
}
|
|
|
|
internal static class CultMeshTcpContentWire
|
|
{
|
|
private const int HeaderBytes = 4;
|
|
private const int MaximumHeaderBytes = 1024 * 1024;
|
|
|
|
public static async Task AwaitAsync(Task task, CancellationToken cancellationToken)
|
|
{
|
|
if (!cancellationToken.CanBeCanceled)
|
|
{
|
|
await task.ConfigureAwait(false);
|
|
return;
|
|
}
|
|
var cancelled = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
using (cancellationToken.Register(() => cancelled.TrySetResult(true)))
|
|
{
|
|
if (await Task.WhenAny(task, cancelled.Task).ConfigureAwait(false) != task)
|
|
throw new OperationCanceledException(cancellationToken);
|
|
}
|
|
await task.ConfigureAwait(false);
|
|
}
|
|
|
|
public static async Task WriteFrameAsync(
|
|
Stream stream,
|
|
byte[] payload,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (payload.Length > MaximumHeaderBytes)
|
|
throw new InvalidDataException("TCP content control frame exceeds its size bound.");
|
|
var header = new byte[HeaderBytes];
|
|
BinaryPrimitives.WriteUInt32BigEndian(header, checked((uint)payload.Length));
|
|
await stream.WriteAsync(header, 0, header.Length, cancellationToken).ConfigureAwait(false);
|
|
await stream.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false);
|
|
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public static async Task VerifyAuthorityAsync(
|
|
Stream stream,
|
|
CultMeshSessionTarget target,
|
|
CultMeshTransportCandidate candidate,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var request = new CultMeshSessionOpenMessage
|
|
{
|
|
MessageId = Guid.NewGuid().ToString("N"),
|
|
SourceRuntimeId = "cultmesh-content-client",
|
|
VerseId = target.VerseId,
|
|
AuthorityRuntimeId = target.AuthorityRuntimeId,
|
|
ProtocolId = CultMeshProtocols.Content.Value,
|
|
RouteGeneration = candidate.Generation,
|
|
ClientNonce = CreateNonce()
|
|
};
|
|
await WriteFrameAsync(stream, CultNetSchemaMessageSerialization.Serialize(request), cancellationToken)
|
|
.ConfigureAwait(false);
|
|
var responseBytes = await ReadFrameAsync(stream, cancellationToken).ConfigureAwait(false);
|
|
var response = CultNetSchemaMessageSerialization.Deserialize(responseBytes) as CultMeshSessionAcceptedMessage
|
|
?? throw new InvalidDataException("TCP content authority handshake returned an unexpected schema.");
|
|
if (!string.Equals(response.MessageId, request.MessageId, StringComparison.Ordinal) ||
|
|
!response.Accepted ||
|
|
!string.Equals(response.VerseId, target.VerseId, StringComparison.Ordinal) ||
|
|
!string.Equals(response.AuthorityRuntimeId, target.AuthorityRuntimeId, StringComparison.Ordinal) ||
|
|
!string.Equals(response.ProtocolId, CultMeshProtocols.Content.Value, StringComparison.Ordinal) ||
|
|
!string.Equals(response.RouteGeneration, candidate.Generation, StringComparison.Ordinal) ||
|
|
candidate.VerifiedAuthority == null ||
|
|
!CultMeshAuthorityProof.VerifySession(request, response, candidate.VerifiedAuthority))
|
|
throw new CultMeshSessionException(new CultMeshSessionFailure(
|
|
CultMeshSessionFailureReason.Authority,
|
|
response.Error ?? "TCP content endpoint did not prove the selected authority route.",
|
|
candidate.Endpoint));
|
|
}
|
|
|
|
private static string CreateNonce()
|
|
{
|
|
var nonce = new byte[32];
|
|
using (var random = System.Security.Cryptography.RandomNumberGenerator.Create()) random.GetBytes(nonce);
|
|
return Convert.ToBase64String(nonce);
|
|
}
|
|
|
|
public static async Task<byte[]> ReadFrameAsync(Stream stream, CancellationToken cancellationToken)
|
|
{
|
|
var header = new byte[HeaderBytes];
|
|
await ReadExactlyAsync(stream, header, cancellationToken).ConfigureAwait(false);
|
|
var size = checked((int)BinaryPrimitives.ReadUInt32BigEndian(header));
|
|
if (size <= 0 || size > MaximumHeaderBytes)
|
|
throw new InvalidDataException("TCP content control frame has an invalid size.");
|
|
var payload = new byte[size];
|
|
await ReadExactlyAsync(stream, payload, cancellationToken).ConfigureAwait(false);
|
|
return payload;
|
|
}
|
|
|
|
public static async Task CopyExactlyAsync(
|
|
Stream source,
|
|
Stream destination,
|
|
long bytes,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (bytes < 0) throw new InvalidDataException("TCP content body size cannot be negative.");
|
|
var buffer = new byte[81920];
|
|
var remaining = bytes;
|
|
while (remaining > 0)
|
|
{
|
|
var requested = (int)Math.Min(buffer.Length, remaining);
|
|
var read = await source.ReadAsync(buffer, 0, requested, cancellationToken).ConfigureAwait(false);
|
|
if (read == 0) throw new EndOfStreamException();
|
|
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
|
remaining -= read;
|
|
}
|
|
}
|
|
|
|
private static async Task ReadExactlyAsync(
|
|
Stream stream,
|
|
byte[] buffer,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var offset = 0;
|
|
while (offset < buffer.Length)
|
|
{
|
|
var read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
if (read == 0) throw new EndOfStreamException();
|
|
offset += read;
|
|
}
|
|
}
|
|
}
|
|
}
|