Initial version
This commit is contained in:
commit
e5b6226a85
68 changed files with 7338 additions and 0 deletions
54
DataTransfer.Client/Shared/ConnectionCode.cs
Normal file
54
DataTransfer.Client/Shared/ConnectionCode.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DataTransfer.Client.Shared;
|
||||
|
||||
/// <summary>Generates and parses the one-time codes used to pair two participants.</summary>
|
||||
public static class ConnectionCode
|
||||
{
|
||||
public const int Length = 10;
|
||||
|
||||
// Ambiguous characters (0/O, 1/I) are excluded. 32 symbols ^ 10 = 50 bits of entropy.
|
||||
private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
public static string Generate() => RandomNumberGenerator.GetString(Alphabet, Length);
|
||||
|
||||
/// <summary>Accepts user input case-insensitively, ignoring whitespace and dashes.</summary>
|
||||
public static bool TryNormalize(string? input, [NotNullWhen(true)] out string? code)
|
||||
{
|
||||
code = null;
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<char> buffer = stackalloc char[Length];
|
||||
var count = 0;
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '-')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var upper = char.ToUpperInvariant(c);
|
||||
if (count == Length || !Alphabet.Contains(upper))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[count++] = upper;
|
||||
}
|
||||
|
||||
if (count != Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
code = new string(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string Format(string code) =>
|
||||
code.Length == Length ? $"{code[..5]}-{code[5..]}" : code;
|
||||
}
|
||||
150
DataTransfer.Client/Shared/TransferProtocol.cs
Normal file
150
DataTransfer.Client/Shared/TransferProtocol.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue