DataTransfer/DataTransfer.Client/Shared/TransferProtocol.cs
2026-10-15 20:09:11 +02:00

150 lines
No EOL
5.5 KiB
C#

namespace DataTransfer.Client.Shared;
/// <summary>Constants shared by the relay server and the browser client.</summary>
public static class TransferProtocol
{
public const string HubPath = "/hubs/transfer";
/// <summary>HTTP endpoint returning the current <see cref="ServerStatus" />.</summary>
public const string StatusPath = "/api/status";
/// <summary>Plaintext size of one file chunk. Files are encrypted and relayed chunk by chunk.</summary>
public const int FileChunkBytes = 256 * 1024;
/// <summary>Upper bound of a single encrypted frame (largest chunk plus framing and AES-GCM overhead).</summary>
public const int MaxFrameBytes = FileChunkBytes + 1024;
/// <summary>
/// SignalR limit for one incoming message: the largest frame base64 encoded inside JSON, plus room for the
/// envelope. Kept tight because the server may buffer one such message per connected browser.
/// </summary>
public const long MaxHubMessageBytes = (MaxFrameBytes + 2) / 3 * 4 + 4 * 1024;
public const long MaxFileBytes = 100L * 1024 * 1024;
public const int MaxMessageLength = 10_000;
/// <summary>
/// Maximum number of messages and files one side may send per connection. The receiver keeps all of them until
/// the session is reset and closes the connection when the peer exceeds it.
/// </summary>
public const int MaxItemsPerConnection = 1_000;
/// <summary>Maximum total size of the files one side may send per connection; enforced like <see cref="MaxItemsPerConnection" />.</summary>
public const long MaxFileBytesPerConnection = 1024L * 1024 * 1024;
}
/// <summary>
/// Methods the browser invokes on the relay hub. Each client may invoke at most
/// <c>Transfer:MaxInvocationsPerSecond</c> methods per second; further invocations are delayed.
/// </summary>
public interface ITransferHub
{
/// <summary>
/// Returns a new proof-of-work challenge for <see cref="CreateConnection" />, or <c>null</c> if the server doesn't
/// require one. It replaces any previous challenge of the connection.
/// </summary>
Task<BotCheckChallenge?> GetBotCheckChallenge();
/// <param name="botCheckSolution">The number that solves the latest challenge; any value if none is required.</param>
Task<CreateConnectionResult> CreateConnection(int botCheckSolution);
Task<JoinConnectionResult> JoinConnection(string code);
/// <summary>
/// Relays an opaque frame to the peer: first the plaintext key-exchange messages, then end-to-end encrypted
/// frames.
/// </summary>
Task<SendStatus> Send(byte[] frame);
Task LeaveConnection();
}
/// <summary>Methods the relay hub invokes on the browser.</summary>
public interface ITransferHubClient
{
/// <summary>Someone joined the creator's connection; the key exchange follows via <see cref="Receive" />.</summary>
Task PeerJoined();
Task Receive(byte[] frame);
Task ConnectionClosed(ConnectionClosedReason reason);
/// <summary>A queued create request got a free slot; the connection now exists.</summary>
Task ConnectionCreated(CreateConnectionResult result);
/// <summary>The 1-based queue position of a waiting create request changed.</summary>
Task QueuePositionChanged(int position);
}
/// <param name="Code">The connection code, if <paramref name="Status" /> is <see cref="CreateStatus.Created" />.</param>
/// <param name="JoinTimeout">The connection is closed unless someone joins within this period.</param>
/// <param name="QueuePosition">
/// 1-based position in the queue, if <paramref name="Status" /> is
/// <see cref="CreateStatus.Queued" />.
/// </param>
public sealed record CreateConnectionResult(
CreateStatus Status,
string? Code,
TimeSpan JoinTimeout,
TimeSpan InactivityTimeout,
int QueuePosition);
public enum CreateStatus
{
Created,
/// <summary>The connection limit is reached; the connection is created as soon as a slot becomes free.</summary>
Queued,
/// <summary>The connection limit is reached and the queue is full.</summary>
ServerBusy,
/// <summary>The network address of the browser already has the maximum number of connections or queued requests.</summary>
TooManyConnections,
/// <summary>The proof-of-work solution was missing, wrong, reused or too late.</summary>
BotCheckFailed
}
/// <summary>
/// Proof of work against creating connections in bulk: find the number between 0 and <paramref name="MaxNumber" />
/// for which the lowercase hex SHA-256 of the UTF-8 string <c>Salt + number</c> equals <paramref name="Hash" />.
/// </summary>
public sealed record BotCheckChallenge(string Salt, string Hash, int MaxNumber);
/// <summary>Current load of the relay server.</summary>
public sealed record ServerStatus(int ActiveConnections, int MaxConnections, int QueuedClients);
public sealed record JoinConnectionResult(JoinStatus Status, TimeSpan InactivityTimeout);
public enum JoinStatus
{
Joined,
InvalidCode,
NotFound,
AlreadyInUse,
/// <summary>Too many browsers from the same network address are connected to the server.</summary>
TooManyConnections
}
public enum SendStatus
{
/// <summary>The frame was relayed to the peer.</summary>
Delivered,
WaitingForPeer,
NotConnected,
InvalidSize,
/// <summary>The connection relayed its maximum amount of data and was closed.</summary>
TransferLimitReached
}
public enum ConnectionClosedReason
{
PeerLeft,
Inactivity,
MaximumLifetime,
TransferLimitReached
}