Editor/Core/Transport.cs
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Net;

namespace TeamCreate;

public static class CollaborationTransportDiagnostics
{
    public static string BoundedStoredDiagnostic(string value)
    {
        return CollaborationDiagnostics.Bounded(value);
    }

    public static string BoundedExceptionDiagnostic(string context, Exception error)
    {
        return CollaborationDiagnostics.DescribeExternalFailure(context, error);
    }
}

public sealed class Wire : IDisposable
{
    public const int MaxBytes = 8388608;

    public const string DecryptionFailure = "Decryption failed. The invite code or session key does not match the host's.";

    public const string PreambleFailure = "The peer did not speak the collaboration encryption protocol (unexpected preamble).";

    public const string PeerClosedFailure = "The peer closed the connection before the session was established.";

    private const int ConnectionSaltBytes = 16;

    private const int PreambleBytes = 20;

    private static readonly byte[] PreambleMagic = new byte[4] { 84, 67, 83, 49 };

    private const string ConnectionKeyInfo = "TeamCreate/v4-conn";

    private readonly Stream stream;

    private readonly IDisposable owner;

    private readonly byte[] sessionKey;

    private readonly bool initiator;

    private readonly byte[] connectionSalt;

    private AeadFraming framing;

    private readonly TaskCompletionSource keyReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

    private readonly CancellationTokenSource stop = new CancellationTokenSource();

    private readonly Channel<byte[]> outgoing = Channel.CreateBounded<byte[]>(256);

    private readonly object failureGate = new object();

    private string failure;

    private bool receivedAny;

    public ConcurrentQueue<Packet> Incoming { get; } = new ConcurrentQueue<Packet>();

    public string Failure
    {
        get
        {
            lock (failureGate)
            {
                return failure;
            }
        }
    }

    public string StoredFailure => (Failure == null) ? null : CollaborationTransportDiagnostics.BoundedStoredDiagnostic(Failure);

    public Wire(TcpClient socket, byte[] sessionKey = null, bool initiator = false)
        : this(OpenTcp(socket), socket, sessionKey, initiator)
    {
    }

    // The framing, encryption and sequencing all live above this constructor and see only a byte
    // stream, so a TCP socket and a relay-carried pipe are interchangeable. `owner` is what closes the
    // underlying resource (the TcpClient, or the relay socket); when it is null the stream closes itself.
    public Wire(Stream stream, IDisposable owner, byte[] sessionKey = null, bool initiator = false)
    {
        this.stream = stream;
        this.owner = owner;
        this.sessionKey = sessionKey;
        this.initiator = initiator;
        if ((sessionKey != null) & initiator)
        {
            connectionSalt = RandomNumberGenerator.GetBytes(16);
            framing = new AeadFraming(DeriveConnectionKey(sessionKey, connectionSalt), initiator);
        }
        _ = Read();
        _ = Write();
    }

    private static Stream OpenTcp(TcpClient socket)
    {
        socket.NoDelay = true;
        return socket.GetStream();
    }

    public const int DefaultConnectSeconds = 8;

    public static async Task<Wire> Connect(string address, int port, byte[] sessionKey = null, bool initiator = false, int timeoutSeconds = DefaultConnectSeconds)
    {
        TcpClient tcp = new TcpClient();
        try
        {
            await tcp.ConnectAsync(address, port).WaitAsync(TimeSpan.FromSeconds(timeoutSeconds));
            return new Wire(tcp, sessionKey, initiator);
        }
        catch
        {
            tcp.Dispose();
            throw;
        }
    }

    internal static byte[] DeriveConnectionKey(byte[] baseKey, byte[] salt)
    {
        if (baseKey == null || baseKey.Length == 0)
        {
            throw new ArgumentException("A connection key requires a non-empty session key.", "baseKey");
        }
        if (salt == null || salt.Length != 16)
        {
            throw new ArgumentOutOfRangeException("salt", $"A connection salt is exactly {16} bytes.");
        }
        return InviteCode.HkdfSha256(baseKey, salt, Encoding.ASCII.GetBytes("TeamCreate/v4-conn"), 32);
    }

    public void Send(Packet p)
    {
        if (Failure != null)
        {
            throw new IOException(Failure);
        }
        byte[] array = JsonSerializer.SerializeToUtf8Bytes(p);
        int num = ((sessionKey == null) ? 8388608 : 8388580);
        if (array.Length > num || !outgoing.Writer.TryWrite(array))
        {
            throw new IOException("Connection queue or message limit exceeded; edit retained locally");
        }
    }

    private async Task Read()
    {
        try
        {
            byte[] header = new byte[4];
            bool flag = sessionKey != null && !initiator;
            bool flag2 = flag;
            if (flag2)
            {
                flag2 = !(await ReadPreambleAsync(stream, header));
            }
            if (flag2)
            {
                return;
            }
            while (!stop.IsCancellationRequested)
            {
                await stream.ReadExactlyAsync(header, stop.Token);
                int length = BinaryPrimitives.ReadInt32BigEndian(header);
                if (length < 1 || length > 8388608)
                {
                    throw new IOException("Invalid frame size");
                }
                byte[] bytes = new byte[length];
                await stream.ReadExactlyAsync(bytes, stop.Token);
                bytes = OpenFrame(bytes);
                if (bytes == null)
                {
                    break;
                }
                Packet packet = JsonSerializer.Deserialize<Packet>(bytes) ?? throw new IOException("Invalid packet");
                if (Incoming.Count >= 256)
                {
                    throw new IOException("Receive queue exceeded");
                }
                Incoming.Enqueue(packet);
                receivedAny = true;
            }
        }
        catch (Exception ex)
        {
            Exception e = ex;
            RecordLoopFailure(e);
        }
    }

    private async Task<bool> ReadPreambleAsync(Stream stream, byte[] header)
    {
        await stream.ReadExactlyAsync(header, stop.Token);
        int length = BinaryPrimitives.ReadInt32BigEndian(header);
        if (length < 1 || length > 8388608)
        {
            FailSession("The peer did not speak the collaboration encryption protocol (unexpected preamble).");
            return false;
        }
        byte[] body = new byte[length];
        await stream.ReadExactlyAsync(body, stop.Token);
        if (length != 20 || !((ReadOnlySpan<byte>)body.AsSpan(0, PreambleMagic.Length)).SequenceEqual((ReadOnlySpan<byte>)PreambleMagic))
        {
            FailSession(PreambleVerdict(body));
            return false;
        }
        byte[] salt = new byte[16];
        Array.Copy(body, PreambleMagic.Length, salt, 0, 16);
        framing = new AeadFraming(DeriveConnectionKey(sessionKey, salt), initiator);
        keyReady.TrySetResult();
        return true;
    }

    private static string PreambleVerdict(ReadOnlySpan<byte> body)
    {
        return ((body.Length > 0 && body[0] == 123) || IsDirectionTag(body)) ? "Decryption failed. The invite code or session key does not match the host's." : "The peer did not speak the collaboration encryption protocol (unexpected preamble).";
    }

    private static bool IsDirectionTag(ReadOnlySpan<byte> bytes)
    {
        return bytes.Length >= 4 && bytes[0] == 67 && bytes[1] == 84 && bytes[2] == 45 && (bytes[3] == 65 || bytes[3] == 66);
    }

    private static bool IsKeyedTraffic(ReadOnlySpan<byte> bytes)
    {
        return bytes.Length >= PreambleMagic.Length && (bytes.Slice(0, PreambleMagic.Length).SequenceEqual(PreambleMagic) || IsDirectionTag(bytes));
    }

    private byte[] OpenFrame(byte[] bytes)
    {
        if (framing == null)
        {
            if (IsKeyedTraffic(bytes))
            {
                FailSession("Decryption failed. The invite code or session key does not match the host's.");
                return null;
            }
            return bytes;
        }
        try
        {
            return framing.Open(bytes);
        }
        catch (CryptographicException ex)
        {
            // A replay or a gap is a different fault from a wrong key, and telling the user their invite
            // code is wrong when the stream was tampered with would send them looking in the wrong place.
            FailSession(ex.Message == AeadFraming.SequenceFailure ? AeadFraming.SequenceFailure : "Decryption failed. The invite code or session key does not match the host's.");
            return null;
        }
    }

    private async Task Write()
    {
        try
        {
            if (sessionKey != null && !initiator)
            {
                await keyReady.Task.WaitAsync(stop.Token);
            }
            bool preamble = sessionKey != null && initiator;
            await foreach (byte[] jsonBytes in outgoing.Reader.ReadAllAsync(stop.Token))
            {
                if (preamble)
                {
                    preamble = false;
                    await WritePreambleAsync(stream);
                }
                byte[] bytes = ((framing == null) ? jsonBytes : framing.Seal(jsonBytes));
                // Header and body go out as one write. Over TCP that saves a syscall; over the relay it
                // is the difference between one WebSocket message per frame and two.
                byte[] frame = new byte[4 + bytes.Length];
                BinaryPrimitives.WriteInt32BigEndian(frame, bytes.Length);
                bytes.CopyTo(frame, 4);
                await stream.WriteAsync(frame, stop.Token);
            }
        }
        catch (Exception ex)
        {
            Exception e = ex;
            RecordLoopFailure(e);
        }
    }

    private async Task WritePreambleAsync(Stream stream)
    {
        byte[] frame = new byte[24];
        BinaryPrimitives.WriteInt32BigEndian(frame, 20);
        PreambleMagic.CopyTo(frame, 4);
        connectionSalt.CopyTo(frame, 4 + PreambleMagic.Length);
        await stream.WriteAsync(frame, stop.Token);
    }

    private void FailSession(string message)
    {
        if (!TryRecordFailure(message))
        {
            return;
        }
        stop.Cancel();
        CloseTransport();
    }

    private void CloseTransport()
    {
        try
        {
            (owner ?? stream).Dispose();
        }
        catch
        {
        }
        if (owner != null)
        {
            try
            {
                stream.Dispose();
            }
            catch
            {
            }
        }
    }

    private bool TryRecordFailure(string message)
    {
        lock (failureGate)
        {
            if (failure != null)
            {
                return false;
            }
            failure = message;
        }
        return true;
    }

    private void RecordLoopFailure(Exception error)
    {
        if (!stop.IsCancellationRequested)
        {
            TryRecordFailure((receivedAny || !IsPeerClosure(error)) ? CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Transport failed: ", error) : "The peer closed the connection before the session was established.");
        }
    }

    private static bool IsPeerClosure(Exception error)
    {
        bool flag = ((error is EndOfStreamException || error is SocketException) ? true : false);
        return flag || (error is IOException && error.InnerException is SocketException);
    }

    public void Dispose()
    {
        stop.Cancel();
        CloseTransport();
        outgoing.Writer.TryComplete();
    }
}

public sealed class Listener : IDisposable
{
    private readonly TcpListener listener;

    private readonly CancellationTokenSource stop = new CancellationTokenSource();

    private readonly byte[] sessionKey;

    public ConcurrentQueue<Wire> Accepted { get; } = new ConcurrentQueue<Wire>();

    public string Failure { get; private set; }

    public string StoredFailure => (Failure == null) ? null : CollaborationTransportDiagnostics.BoundedStoredDiagnostic(Failure);

    public Listener(string address, int port, byte[] sessionKey = null)
    {
        listener = new TcpListener(IPAddress.Parse(address), port);
        listener.Start();
        this.sessionKey = sessionKey;
        _ = Accept();
    }

    private async Task Accept()
    {
        try
        {
            while (!stop.IsCancellationRequested)
            {
                ConcurrentQueue<Wire> accepted = Accepted;
                accepted.Enqueue(new Wire(await listener.AcceptTcpClientAsync(stop.Token), sessionKey));
            }
        }
        catch (Exception ex)
        {
            Exception e = ex;
            if (!stop.IsCancellationRequested)
            {
                Failure = CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Transport failed: ", e);
            }
        }
    }

    public void Dispose()
    {
        stop.Cancel();
        listener.Stop();
        Wire result;
        while (Accepted.TryDequeue(out result))
        {
            result.Dispose();
        }
    }
}