diff --git a/MinecraftClient/ClassicConsoleBackend.cs b/MinecraftClient/ClassicConsoleBackend.cs index ac4912c9..bc8dedd8 100644 --- a/MinecraftClient/ClassicConsoleBackend.cs +++ b/MinecraftClient/ClassicConsoleBackend.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace MinecraftClient { @@ -50,6 +52,13 @@ namespace MinecraftClient return ConsoleInteractive.ConsoleReader.RequestImmediateInput(); } + public Task RequestImmediateInputAsync(CancellationToken cancellationToken) + { + // ConsoleInteractive only exposes a blocking immediate-read API. + // Keep the compatibility boundary here so the wider startup/runtime path can await it. + return Task.Run(ConsoleInteractive.ConsoleReader.RequestImmediateInput, cancellationToken); + } + public string? ReadPassword() { ConsoleInteractive.ConsoleReader.SetInputVisible(false); @@ -58,6 +67,19 @@ namespace MinecraftClient return input; } + public async Task ReadPasswordAsync(CancellationToken cancellationToken) + { + ConsoleInteractive.ConsoleReader.SetInputVisible(false); + try + { + return await RequestImmediateInputAsync(cancellationToken); + } + finally + { + ConsoleInteractive.ConsoleReader.SetInputVisible(true); + } + } + public void ClearInputBuffer() { ConsoleInteractive.ConsoleReader.ClearBuffer(); diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs index 908ec890..8245af13 100644 --- a/MinecraftClient/ConsoleIO.cs +++ b/MinecraftClient/ConsoleIO.cs @@ -76,6 +76,13 @@ namespace MinecraftClient return Backend.ReadPassword(); } + public static Task ReadPasswordAsync(CancellationToken cancellationToken = default) + { + if (BasicIO) + return Task.FromResult(Console.ReadLine()); + return Backend.ReadPasswordAsync(cancellationToken); + } + /// /// Read a line from the standard input /// @@ -86,6 +93,13 @@ namespace MinecraftClient return Backend.RequestImmediateInput(); } + public static Task ReadLineAsync(CancellationToken cancellationToken = default) + { + if (BasicIO) + return Task.FromResult(Console.ReadLine() ?? string.Empty); + return Backend.RequestImmediateInputAsync(cancellationToken); + } + /// /// Debug routine: print all keys pressed in the console /// diff --git a/MinecraftClient/Crypto/AesCfb8Stream.cs b/MinecraftClient/Crypto/AesCfb8Stream.cs index 6a7b2769..381674a5 100644 --- a/MinecraftClient/Crypto/AesCfb8Stream.cs +++ b/MinecraftClient/Crypto/AesCfb8Stream.cs @@ -1,9 +1,10 @@ using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using MinecraftClient.Crypto.AesHandler; namespace MinecraftClient.Crypto { @@ -11,8 +12,7 @@ namespace MinecraftClient.Crypto { public const int blockSize = 16; - private readonly Aes? Aes = null; - private readonly FastAes? FastAes = null; + private readonly IAesHandler aesHandler; private bool inStreamEnded = false; @@ -24,18 +24,7 @@ namespace MinecraftClient.Crypto public AesCfb8Stream(Stream stream, byte[] key) { BaseStream = stream; - - if (FastAes.IsSupported()) - FastAes = new FastAes(key); - else - { - Aes = Aes.Create(); - Aes.BlockSize = 128; - Aes.KeySize = 128; - Aes.Key = key; - Aes.Mode = CipherMode.ECB; - Aes.Padding = PaddingMode.None; - } + aesHandler = AesHandlerFactory.Create(key); Array.Copy(key, ReadStreamIV, 16); Array.Copy(key, WriteStreamIV, 16); @@ -96,10 +85,7 @@ namespace MinecraftClient.Crypto } Span blockOutput = stackalloc byte[blockSize]; - if (FastAes is not null) - FastAes.EncryptEcb(ReadStreamIV, blockOutput); - else - Aes!.EncryptEcb(ReadStreamIV, blockOutput, PaddingMode.None); + aesHandler.EncryptEcb(ReadStreamIV, blockOutput); // Shift left Array.Copy(ReadStreamIV, 1, ReadStreamIV, 0, blockSize - 1); @@ -111,10 +97,7 @@ namespace MinecraftClient.Crypto [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EncryptBlock(ReadOnlySpan blockInput, Span blockOutput) { - if (FastAes is not null) - FastAes.EncryptEcb(blockInput, blockOutput); - else - Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None); + aesHandler.EncryptEcb(blockInput, blockOutput); } [MethodImpl(MethodImplOptions.AggressiveOptimization)] @@ -124,31 +107,39 @@ namespace MinecraftClient.Crypto return 0; Span blockOutput = stackalloc byte[blockSize]; + byte[] inputBuf = ArrayPool.Shared.Rent(blockSize + required); - byte[] inputBuf = new byte[blockSize + required]; - Array.Copy(ReadStreamIV, inputBuf, blockSize); - - for (int readed = 0, curRead; readed < required; readed += curRead) + try { - curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed); - if (curRead == 0) + Array.Copy(ReadStreamIV, inputBuf, blockSize); + + for (int readed = 0, curRead; readed < required; readed += curRead) { - inStreamEnded = true; - return readed; + curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed); + if (curRead == 0) + { + inStreamEnded = true; + Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize); + return readed; + } + + int processEnd = readed + curRead; + for (int idx = readed; idx < processEnd; idx++) + { + ReadOnlySpan blockInput = new(inputBuf, idx, blockSize); + EncryptBlock(blockInput, blockOutput); + buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]); + } } - int processEnd = readed + curRead; - for (int idx = readed; idx < processEnd; idx++) - { - ReadOnlySpan blockInput = new(inputBuf, idx, blockSize); - EncryptBlock(blockInput, blockOutput); - buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]); - } + Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize); + + return required; + } + finally + { + ArrayPool.Shared.Return(inputBuf); } - - Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize); - - return required; } public override long Seek(long offset, SeekOrigin origin) @@ -179,19 +170,27 @@ namespace MinecraftClient.Crypto [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override void Write(byte[] input, int offset, int required) { - byte[] outputBuf = new byte[blockSize + required]; - Array.Copy(WriteStreamIV, outputBuf, blockSize); + byte[] outputBuf = ArrayPool.Shared.Rent(blockSize + required); - Span blockOutput = stackalloc byte[blockSize]; - for (int wirtten = 0; wirtten < required; ++wirtten) + try { - ReadOnlySpan blockInput = new(outputBuf, wirtten, blockSize); - EncryptBlock(blockInput, blockOutput); - outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]); - } - BaseStream.Write(outputBuf, blockSize, required); + Array.Copy(WriteStreamIV, outputBuf, blockSize); - Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize); + Span blockOutput = stackalloc byte[blockSize]; + for (int written = 0; written < required; ++written) + { + ReadOnlySpan blockInput = new(outputBuf, written, blockSize); + EncryptBlock(blockInput, blockOutput); + outputBuf[blockSize + written] = (byte)(blockOutput[0] ^ input[offset + written]); + } + + BaseStream.Write(outputBuf, blockSize, required); + Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize); + } + finally + { + ArrayPool.Shared.Return(outputBuf); + } } [MethodImpl(MethodImplOptions.AggressiveOptimization)] @@ -200,26 +199,34 @@ namespace MinecraftClient.Crypto if (inStreamEnded || buffer.Length == 0) return 0; - byte[] inputBuf = new byte[blockSize + buffer.Length]; - Array.Copy(ReadStreamIV, inputBuf, blockSize); + byte[] inputBuf = ArrayPool.Shared.Rent(blockSize + buffer.Length); - for (int readed = 0; readed < buffer.Length;) + try { - int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken); - if (curRead == 0) + Array.Copy(ReadStreamIV, inputBuf, blockSize); + + for (int readed = 0; readed < buffer.Length;) { - inStreamEnded = true; - Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize); - return readed; + int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken); + if (curRead == 0) + { + inStreamEnded = true; + Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize); + return readed; + } + + int processEnd = readed + curRead; + DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd); + readed = processEnd; } - int processEnd = readed + curRead; - DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd); - readed = processEnd; + Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize); + return buffer.Length; + } + finally + { + ArrayPool.Shared.Return(inputBuf); } - - Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize); - return buffer.Length; } [MethodImpl(MethodImplOptions.AggressiveOptimization)] @@ -228,12 +235,20 @@ namespace MinecraftClient.Crypto if (buffer.Length == 0) return; - byte[] outputBuf = new byte[blockSize + buffer.Length]; - Array.Copy(WriteStreamIV, outputBuf, blockSize); - EncryptToOutputBuffer(buffer, outputBuf); + byte[] outputBuf = ArrayPool.Shared.Rent(blockSize + buffer.Length); - await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken); - Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize); + try + { + Array.Copy(WriteStreamIV, outputBuf, blockSize); + EncryptToOutputBuffer(buffer, outputBuf); + + await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken); + Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize); + } + finally + { + ArrayPool.Shared.Return(outputBuf); + } } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -246,6 +261,16 @@ namespace MinecraftClient.Crypto return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + aesHandler.Dispose(); + } + + base.Dispose(disposing); + } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] private void DecryptToOutputBuffer(byte[] inputBuf, Memory output, int start, int end) { diff --git a/MinecraftClient/Crypto/AesHandler/BasicAes.cs b/MinecraftClient/Crypto/AesHandler/BasicAes.cs new file mode 100644 index 00000000..8c08572f --- /dev/null +++ b/MinecraftClient/Crypto/AesHandler/BasicAes.cs @@ -0,0 +1,31 @@ +using System; +using System.Security.Cryptography; + +namespace MinecraftClient.Crypto.AesHandler; + +public sealed class BasicAes : IAesHandler +{ + private readonly Aes aes; + + public BasicAes(byte[] key) + { + ArgumentNullException.ThrowIfNull(key); + + aes = Aes.Create(); + aes.BlockSize = 128; + aes.KeySize = 128; + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + } + + public override void EncryptEcb(ReadOnlySpan plaintext, Span destination) + { + aes.EncryptEcb(plaintext, destination, PaddingMode.None); + } + + public override void Dispose() + { + aes.Dispose(); + } +} diff --git a/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs b/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs new file mode 100644 index 00000000..a5b8621d --- /dev/null +++ b/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs @@ -0,0 +1,162 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; + +namespace MinecraftClient.Crypto.AesHandler; + +public sealed class FasterAesArm : IAesHandler +{ + private const int BlockSize = 16; + private const int Rounds = 10; + + private readonly byte[] enc; + + public FasterAesArm(ReadOnlySpan key) + { + enc = new byte[(Rounds + 1) * BlockSize]; + + int[] intKey = GenerateKeyExpansion(key); + for (int i = 0; i < intKey.Length; ++i) + { + enc[i * 4 + 0] = (byte)((intKey[i] >> 0) & 0xFF); + enc[i * 4 + 1] = (byte)((intKey[i] >> 8) & 0xFF); + enc[i * 4 + 2] = (byte)((intKey[i] >> 16) & 0xFF); + enc[i * 4 + 3] = (byte)((intKey[i] >> 24) & 0xFF); + } + } + + public static bool IsSupported() + { + return Aes.IsSupported && AdvSimd.IsSupported; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public override void EncryptEcb(ReadOnlySpan plaintext, Span destination) + { + int position = 0; + int left = plaintext.Length; + + Vector128 key0 = Unsafe.ReadUnaligned>(ref enc[0 * BlockSize]); + Vector128 key1 = Unsafe.ReadUnaligned>(ref enc[1 * BlockSize]); + Vector128 key2 = Unsafe.ReadUnaligned>(ref enc[2 * BlockSize]); + Vector128 key3 = Unsafe.ReadUnaligned>(ref enc[3 * BlockSize]); + Vector128 key4 = Unsafe.ReadUnaligned>(ref enc[4 * BlockSize]); + Vector128 key5 = Unsafe.ReadUnaligned>(ref enc[5 * BlockSize]); + Vector128 key6 = Unsafe.ReadUnaligned>(ref enc[6 * BlockSize]); + Vector128 key7 = Unsafe.ReadUnaligned>(ref enc[7 * BlockSize]); + Vector128 key8 = Unsafe.ReadUnaligned>(ref enc[8 * BlockSize]); + Vector128 key9 = Unsafe.ReadUnaligned>(ref enc[9 * BlockSize]); + Vector128 key10 = Unsafe.ReadUnaligned>(ref enc[10 * BlockSize]); + + while (left >= BlockSize) + { + Vector128 block = Unsafe.ReadUnaligned>(ref Unsafe.AsRef(in plaintext[position])); + + block = Aes.Encrypt(block, key0); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key1); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key2); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key3); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key4); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key5); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key6); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key7); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key8); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key9); + block = AdvSimd.Xor(block, key10); + + Unsafe.WriteUnaligned(ref destination[position], block); + + position += BlockSize; + left -= BlockSize; + } + } + + private static int[] GenerateKeyExpansion(ReadOnlySpan rgbKey) + { + int[] encryptKeyExpansion = new int[4 * (Rounds + 1)]; + + int index = 0; + for (int i = 0; i < 4; ++i) + { + int i0 = rgbKey[index++]; + int i1 = rgbKey[index++]; + int i2 = rgbKey[index++]; + int i3 = rgbKey[index++]; + encryptKeyExpansion[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0; + } + + for (int i = 4; i < 4 * (Rounds + 1); ++i) + { + int temp = encryptKeyExpansion[i - 1]; + + if (i % 4 == 0) + { + temp = SubWord(Rot3(temp)); + temp ^= Rcon[(i / 4) - 1]; + } + + encryptKeyExpansion[i] = encryptKeyExpansion[i - 4] ^ temp; + } + + return encryptKeyExpansion; + } + + private static int SubWord(int value) + { + return Sbox[value & 0xFF] + | Sbox[(value >> 8) & 0xFF] << 8 + | Sbox[(value >> 16) & 0xFF] << 16 + | Sbox[(value >> 24) & 0xFF] << 24; + } + + private static int Rot3(int value) + { + return (value << 24 & unchecked((int)0xFF000000)) | (value >> 8 & unchecked((int)0x00FFFFFF)); + } + + private static ReadOnlySpan Sbox => + [ + 99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, + 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, + 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, + 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, + 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, + 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, + 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, + 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, + 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, + 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, + 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, + 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, + 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, + 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, + 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, + 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22 + ]; + + private static ReadOnlySpan Rcon => + [ + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36, + 0x6C, 0xD8, 0xAB, 0x4D, 0x9A, 0x2F, 0x5E, 0xBC, 0x63, 0xC6, + 0x97, 0x35, 0x6A, 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91 + ]; +} diff --git a/MinecraftClient/Crypto/AesHandler/FasterAesX86.cs b/MinecraftClient/Crypto/AesHandler/FasterAesX86.cs new file mode 100644 index 00000000..715f8d9e --- /dev/null +++ b/MinecraftClient/Crypto/AesHandler/FasterAesX86.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace MinecraftClient.Crypto.AesHandler; + +public sealed class FasterAesX86 : IAesHandler +{ + private Vector128[] RoundKeys { get; } + + public FasterAesX86(ReadOnlySpan key) + { + RoundKeys = KeyExpansion(key); + } + + public static bool IsSupported() + { + return Sse2.IsSupported && Aes.IsSupported; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public override void EncryptEcb(ReadOnlySpan plaintext, Span destination) + { + Vector128[] keys = RoundKeys; + + ReadOnlySpan> blocks = MemoryMarshal.Cast>(plaintext); + Span> dest = MemoryMarshal.Cast>(destination); + + _ = keys[10]; + + for (int i = 0; i < blocks.Length; i++) + { + Vector128 b = blocks[i]; + + b = Sse2.Xor(b, keys[0]); + b = Aes.Encrypt(b, keys[1]); + b = Aes.Encrypt(b, keys[2]); + b = Aes.Encrypt(b, keys[3]); + b = Aes.Encrypt(b, keys[4]); + b = Aes.Encrypt(b, keys[5]); + b = Aes.Encrypt(b, keys[6]); + b = Aes.Encrypt(b, keys[7]); + b = Aes.Encrypt(b, keys[8]); + b = Aes.Encrypt(b, keys[9]); + b = Aes.EncryptLast(b, keys[10]); + + dest[i] = b; + } + } + + private static Vector128[] KeyExpansion(ReadOnlySpan key) + { + Vector128[] keys = new Vector128[20]; + + keys[0] = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetReference(key)); + + MakeRoundKey(keys, 1, 0x01); + MakeRoundKey(keys, 2, 0x02); + MakeRoundKey(keys, 3, 0x04); + MakeRoundKey(keys, 4, 0x08); + MakeRoundKey(keys, 5, 0x10); + MakeRoundKey(keys, 6, 0x20); + MakeRoundKey(keys, 7, 0x40); + MakeRoundKey(keys, 8, 0x80); + MakeRoundKey(keys, 9, 0x1B); + MakeRoundKey(keys, 10, 0x36); + + for (int i = 1; i < 10; i++) + keys[10 + i] = Aes.InverseMixColumns(keys[i]); + + return keys; + } + + private static void MakeRoundKey(Vector128[] keys, int index, byte rcon) + { + Vector128 s = keys[index - 1]; + Vector128 t = keys[index - 1]; + + t = Aes.KeygenAssist(t, rcon); + t = Sse2.Shuffle(t.AsUInt32(), 0xFF).AsByte(); + + s = Sse2.Xor(s, Sse2.ShiftLeftLogical128BitLane(s, 4)); + s = Sse2.Xor(s, Sse2.ShiftLeftLogical128BitLane(s, 8)); + + keys[index] = Sse2.Xor(s, t); + } +} diff --git a/MinecraftClient/Crypto/AesHandlerFactory.cs b/MinecraftClient/Crypto/AesHandlerFactory.cs new file mode 100644 index 00000000..0c2e7b30 --- /dev/null +++ b/MinecraftClient/Crypto/AesHandlerFactory.cs @@ -0,0 +1,20 @@ +using System; +using MinecraftClient.Crypto.AesHandler; + +namespace MinecraftClient.Crypto; + +internal static class AesHandlerFactory +{ + public static IAesHandler Create(ReadOnlySpan key) + { + byte[] ownedKey = key.ToArray(); + + if (FasterAesX86.IsSupported()) + return new FasterAesX86(ownedKey); + + if (FasterAesArm.IsSupported()) + return new FasterAesArm(ownedKey); + + return new BasicAes(ownedKey); + } +} diff --git a/MinecraftClient/Crypto/IAesHandler.cs b/MinecraftClient/Crypto/IAesHandler.cs new file mode 100644 index 00000000..1f34ec90 --- /dev/null +++ b/MinecraftClient/Crypto/IAesHandler.cs @@ -0,0 +1,12 @@ +using System; + +namespace MinecraftClient.Crypto; + +public abstract class IAesHandler : IDisposable +{ + public abstract void EncryptEcb(ReadOnlySpan plaintext, Span destination); + + public virtual void Dispose() + { + } +} diff --git a/MinecraftClient/FileMonitor.cs b/MinecraftClient/FileMonitor.cs index 16f590a4..d67610af 100644 --- a/MinecraftClient/FileMonitor.cs +++ b/MinecraftClient/FileMonitor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace MinecraftClient { @@ -12,7 +13,7 @@ namespace MinecraftClient public class FileMonitor : IDisposable { private readonly Tuple? monitor = null; - private readonly Tuple? polling = null; + private readonly Tuple? polling = null; /// /// Create a new FileMonitor and start monitoring @@ -48,9 +49,9 @@ namespace MinecraftClient monitor = null; var cancellationTokenSource = new CancellationTokenSource(); - polling = new Tuple(new Thread(() => PollingThread(folder, filename, handler, cancellationTokenSource.Token)), cancellationTokenSource); - polling.Item1.Name = String.Format("{0} Polling thread: {1}", GetType().Name, Path.Combine(folder, filename)); - polling.Item1.Start(); + polling = new Tuple( + Task.Run(() => PollingLoopAsync(folder, filename, handler, cancellationTokenSource.Token), cancellationTokenSource.Token), + cancellationTokenSource); } } @@ -66,25 +67,29 @@ namespace MinecraftClient } /// - /// Fallback polling thread for use when operating system does not support FileSystemWatcher + /// Fallback polling loop for use when operating system does not support FileSystemWatcher /// /// Folder to monitor /// File name to monitor /// Callback when file changes - private void PollingThread(string folder, string filename, FileSystemEventHandler handler, CancellationToken cancellationToken) + private async Task PollingLoopAsync(string folder, string filename, FileSystemEventHandler handler, CancellationToken cancellationToken) { string filePath = Path.Combine(folder, filename); DateTime lastWrite = GetLastWrite(filePath); - while (!cancellationToken.IsCancellationRequested) + using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(5)); + try { - Thread.Sleep(5000); - DateTime lastWriteNew = GetLastWrite(filePath); - if (lastWriteNew != lastWrite) + while (await periodicTimer.WaitForNextTickAsync(cancellationToken)) { - lastWrite = lastWriteNew; - handler(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, folder, filename)); + DateTime lastWriteNew = GetLastWrite(filePath); + if (lastWriteNew != lastWrite) + { + lastWrite = lastWriteNew; + handler(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, folder, filename)); + } } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } } /// diff --git a/MinecraftClient/IConsoleBackend.cs b/MinecraftClient/IConsoleBackend.cs index b4d52ebf..4759e0f8 100644 --- a/MinecraftClient/IConsoleBackend.cs +++ b/MinecraftClient/IConsoleBackend.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace MinecraftClient { @@ -56,8 +58,12 @@ namespace MinecraftClient string RequestImmediateInput(); + Task RequestImmediateInputAsync(CancellationToken cancellationToken); + string? ReadPassword(); + Task ReadPasswordAsync(CancellationToken cancellationToken); + void ClearInputBuffer(); bool DisplayUserInput { get; set; } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 50b3f627..0ccaaf75 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -369,9 +369,8 @@ namespace MinecraftClient if (ReconnectionAttemptsLeft > 0) { Log.Info(string.Format(Translations.mcc_reconnect, ReconnectionAttemptsLeft)); - Thread.Sleep(5000); ReconnectionAttemptsLeft--; - Program.Restart(); + Program.Restart(5, announceDelay: false); } else if (InternalConfig.InteractiveMode) { @@ -490,9 +489,8 @@ namespace MinecraftClient if (ReconnectionAttemptsLeft > 0) { Log.Info($"Reconnecting... Attempts left: {ReconnectionAttemptsLeft}"); - Thread.Sleep(5000); ReconnectionAttemptsLeft--; - Program.Restart(); + Program.Restart(5, announceDelay: false); } else if (InternalConfig.InteractiveMode) { diff --git a/MinecraftClient/Mcp/MccEmbeddedMcpHost.cs b/MinecraftClient/Mcp/MccEmbeddedMcpHost.cs index a40b7183..b6a9eb70 100644 --- a/MinecraftClient/Mcp/MccEmbeddedMcpHost.cs +++ b/MinecraftClient/Mcp/MccEmbeddedMcpHost.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -12,7 +14,7 @@ public sealed class MccEmbeddedMcpHost { private readonly MccMcpConfig config; private readonly IMccMcpCapabilities capabilities; - private readonly object stateLock = new(); + private readonly SemaphoreSlim stateLock = new(1, 1); private WebApplication? app; public MccEmbeddedMcpHost(MccMcpConfig config, IMccMcpCapabilities capabilities) @@ -25,10 +27,7 @@ public sealed class MccEmbeddedMcpHost { get { - lock (stateLock) - { - return app is not null; - } + return app is not null; } } @@ -36,29 +35,37 @@ public sealed class MccEmbeddedMcpHost public bool Start(out string? error) { - lock (stateLock) + (bool success, string? startError) = StartAsync().GetAwaiter().GetResult(); + error = startError; + return success; + } + + public bool Stop(out string? error) + { + (bool success, string? stopError) = StopAsync().GetAwaiter().GetResult(); + error = stopError; + return success; + } + + public async Task<(bool Success, string? Error)> StartAsync(CancellationToken cancellationToken = default) + { + await stateLock.WaitAsync(cancellationToken); + try { - error = null; if (app is not null) - return true; + return (true, null); string route = NormalizeRoute(config.Transport.Route); string bindHost = string.IsNullOrWhiteSpace(config.Transport.BindHost) ? "127.0.0.1" : config.Transport.BindHost.Trim(); if (config.Transport.Port is < 1 or > 65535) - { - error = "invalid_port"; - return false; - } + return (false, "invalid_port"); string? requiredToken = null; if (config.Transport.RequireAuthToken) { requiredToken = Environment.GetEnvironmentVariable(config.Transport.AuthTokenEnvVar); if (string.IsNullOrWhiteSpace(requiredToken)) - { - error = "missing_auth_token"; - return false; - } + return (false, "missing_auth_token"); } WebApplicationBuilder builder = WebApplication.CreateBuilder(); @@ -96,33 +103,49 @@ public sealed class MccEmbeddedMcpHost } builtApp.MapMcp(route); - builtApp.StartAsync().GetAwaiter().GetResult(); - app = builtApp; - return true; - } - } - - public bool Stop(out string? error) - { - lock (stateLock) - { - error = null; - if (app is null) - return true; try { - app.StopAsync().GetAwaiter().GetResult(); - app.DisposeAsync().AsTask().GetAwaiter().GetResult(); - app = null; - return true; + await builtApp.StartAsync(cancellationToken); + app = builtApp; + return (true, null); } catch { - error = "stop_failed"; - return false; + await builtApp.DisposeAsync(); + throw; } } + finally + { + stateLock.Release(); + } + } + + public async Task<(bool Success, string? Error)> StopAsync(CancellationToken cancellationToken = default) + { + await stateLock.WaitAsync(cancellationToken); + try + { + if (app is null) + return (true, null); + + try + { + await app.StopAsync(cancellationToken); + await app.DisposeAsync(); + app = null; + return (true, null); + } + catch + { + return (false, "stop_failed"); + } + } + finally + { + stateLock.Release(); + } } private static string NormalizeRoute(string route) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 1dedb122..6870ada6 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -577,7 +577,7 @@ namespace MinecraftClient if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !skipPassword && (Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login)))) { - RequestPassword(); + await RequestPasswordAsync(); } startupargs = args; @@ -587,10 +587,10 @@ namespace MinecraftClient /// /// Reduest user to submit password. /// - private static void RequestPassword() + private static async Task RequestPasswordAsync() { ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden); - string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); + string? password = await ConsoleIO.ReadPasswordAsync(); if (string.IsNullOrWhiteSpace(password)) InternalConfig.Account.Password = "-"; else @@ -651,7 +651,7 @@ namespace MinecraftClient if (result != ProtocolHandler.LoginResult.Success && string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !(Config.Main.General.AccountType == LoginType.microsoft)) - RequestPassword(); + await RequestPasswordAsync(); } else ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName)); } @@ -897,27 +897,33 @@ namespace MinecraftClient /// /// Optional delay, in seconds, before restarting /// Optional, keep account and server settings - public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false) + public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false, bool announceDelay = true) { ConsoleIO.Backend?.StopReadThread(); - new Thread(new ThreadStart(delegate + StartLifecycleTask(RestartAsync(delaySeconds, keepAccountAndServerSettings, announceDelay)); + } + + private static async Task RestartAsync(int delaySeconds, bool keepAccountAndServerSettings, bool announceDelay) + { + if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); } + if (offlinePrompt is not null) { - if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); } - if (offlinePrompt is not null) - { - if (ConsoleIO.Backend is not null) - ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler; - offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); - } - if (delaySeconds > 0) - { + if (ConsoleIO.Backend is not null) + ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler; + offlinePrompt.Item2.Cancel(); + offlinePrompt.Item1.Join(); + offlinePrompt = null; + ConsoleIO.Reset(); + } + if (delaySeconds > 0) + { + if (announceDelay) ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds)); - Thread.Sleep(delaySeconds * 1000); - } - ConsoleIO.WriteLine(Translations.mcc_restart); - ReloadSettings(keepAccountAndServerSettings); - InitializeClientAsync().GetAwaiter().GetResult(); - })).Start(); + await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); + } + ConsoleIO.WriteLine(Translations.mcc_restart); + ReloadSettings(keepAccountAndServerSettings); + await InitializeClientAsync(); } public static void DoExit(int exitcode = 0) @@ -946,7 +952,26 @@ namespace MinecraftClient /// public static void Exit(int exitcode = 0) { - new Thread(() => { DoExit(exitcode); }).Start(); + StartLifecycleTask(Task.Run(() => DoExit(exitcode))); + } + + private static void StartLifecycleTask(Task lifecycleTask) + { + _ = ObserveLifecycleTaskAsync(lifecycleTask); + } + + private static async Task ObserveLifecycleTaskAsync(Task lifecycleTask) + { + try + { + await lifecycleTask; + } + catch (Exception ex) + { + SentrySdk.CaptureException(ex); + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + ex); + } } /// diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 578691fb..68a842e3 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -306,7 +306,7 @@ namespace MinecraftClient.Protocol.Handlers byte b; while (true) { - b = socket.ReadDataRAW(1)[0]; + b = socket.ReadByteRAW(); i |= (b & 0x7F) << j++ * 7; if (j > 5) throw new OverflowException("VarInt too big"); if ((b & 0x80) != 128) break; @@ -327,7 +327,7 @@ namespace MinecraftClient.Protocol.Handlers byte b; while (true) { - b = (await socket.ReadDataRAWAsync(1, cancellationToken))[0]; + b = await socket.ReadByteRAWAsync(cancellationToken); i |= (b & 0x7F) << j++ * 7; if (j > 5) throw new OverflowException("VarInt too big"); if ((b & 0x80) != 128) break; diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 9f8ad701..23741df0 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -72,7 +72,7 @@ namespace MinecraftClient.Protocol.Handlers c = Client; } - private void Updater(CancellationToken cancelToken) + private async Task UpdaterAsync(CancellationToken cancelToken) { if (cancelToken.IsCancellationRequested) return; @@ -100,7 +100,7 @@ namespace MinecraftClient.Protocol.Handlers long sleepLength = nextUpdateDue - stopWatch.ElapsedMilliseconds; if (sleepLength > 1) - Thread.Sleep((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds)); + await Task.Delay((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds), cancelToken); } } catch (System.IO.IOException) { } @@ -247,11 +247,7 @@ namespace MinecraftClient.Protocol.Handlers { CancellationTokenSource netReadCts = new(); netReadCancellationTokenSource = netReadCts; - netReadTask = Task.Factory.StartNew( - () => Updater(netReadCts.Token), - netReadCts.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); + netReadTask = Task.Run(() => UpdaterAsync(netReadCts.Token), netReadCts.Token); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index bb31fa84..f554a35a 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -284,7 +284,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Serialized packet/tick loop. /// - private void Updater(CancellationToken cancelToken) + private async Task UpdaterAsync(CancellationToken cancelToken) { if (cancelToken.IsCancellationRequested) return; @@ -316,7 +316,7 @@ namespace MinecraftClient.Protocol.Handlers long sleepLength = nextUpdateDue - stopWatch.ElapsedMilliseconds; if (sleepLength > 1) - Thread.Sleep((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds)); + await Task.Delay((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds), cancelToken); } } catch (ObjectDisposedException) @@ -395,23 +395,9 @@ namespace MinecraftClient.Protocol.Handlers /// will contain raw packet Data internal Tuple> ReadNextPacket() { - var size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size - Queue packetData = new(socketWrapper.ReadDataRAW(size)); //Packet contents - - //Handle packet decompression - if (protocolVersion >= MC_1_8_Version - && compression_treshold >= 0) - { - var sizeUncompressed = dataTypes.ReadNextVarInt(packetData); - if (sizeUncompressed != 0) // != 0 means compressed, let's decompress - { - var toDecompress = packetData.ToArray(); - var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed); - packetData = new Queue(uncompressed); - } - } - - var packetId = dataTypes.ReadNextVarInt(packetData); // Packet ID + var (packetId, packetData) = socketWrapper.GetNextPacket( + protocolVersion >= MC_1_8_Version ? compression_treshold : -1, + dataTypes); if (handler.GetNetworkPacketCaptureEnabled()) handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true); @@ -420,22 +406,10 @@ namespace MinecraftClient.Protocol.Handlers internal async Task>> ReadNextPacketAsync(CancellationToken cancellationToken) { - var size = await dataTypes.ReadNextVarIntRAWAsync(socketWrapper, cancellationToken); //Packet size - Queue packetData = new(await socketWrapper.ReadDataRAWAsync(size, cancellationToken)); //Packet contents - - if (protocolVersion >= MC_1_8_Version - && compression_treshold >= 0) - { - var sizeUncompressed = dataTypes.ReadNextVarInt(packetData); - if (sizeUncompressed != 0) - { - var toDecompress = packetData.ToArray(); - var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed); - packetData = new Queue(uncompressed); - } - } - - var packetId = dataTypes.ReadNextVarInt(packetData); + var (packetId, packetData) = await socketWrapper.GetNextPacketAsync( + protocolVersion >= MC_1_8_Version ? compression_treshold : -1, + dataTypes, + cancellationToken); if (handler.GetNetworkPacketCaptureEnabled()) handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true); @@ -3867,21 +3841,17 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Start the updating thread. Should be called after login success. + /// Start the serialized packet/tick tasks. Should be called after login success. /// private void StartUpdating() { CancellationTokenSource netMainCts = new(); netMainCancellationTokenSource = netMainCts; - netMainTask = Task.Factory.StartNew( - () => Updater(netMainCts.Token), - netMainCts.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); + netMainTask = Task.Run(() => UpdaterAsync(netMainCts.Token), netMainCts.Token); CancellationTokenSource netReaderCts = new(); netReaderCancellationTokenSource = netReaderCts; - netReaderTask = PacketReaderAsync(netReaderCts.Token); + netReaderTask = Task.Run(() => PacketReaderAsync(netReaderCts.Token), netReaderCts.Token); } /// @@ -4381,14 +4351,8 @@ namespace MinecraftClient.Protocol.Handlers var statusRequest = DataTypes.GetVarInt(0); socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(statusRequest.Length), statusRequest)); - // Read Response length - var packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper); - if (packetLength <= 0) - return false; - - // Read the Packet Id - var packetData = new Queue(socketWrapper.ReadDataRAW(packetLength)); - if (dataTypes.ReadNextVarInt(packetData) != 0x00) + var (statusPacketId, packetData) = socketWrapper.GetNextPacket(-1, dataTypes); + if (statusPacketId != 0x00) return false; // Get the Json data @@ -4467,15 +4431,11 @@ namespace MinecraftClient.Protocol.Handlers var pingRequest = dataTypes.ConcatBytes(DataTypes.GetVarInt(0x01), DataTypes.GetLong(pingPayload)); socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(pingRequest.Length), pingRequest)); - packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper); - if (packetLength > 0) + var (pongPacketId, pongPacketData) = socketWrapper.GetNextPacket(-1, dataTypes); + if (pongPacketId == 0x01) { - packetData = new Queue(socketWrapper.ReadDataRAW(packetLength)); - if (dataTypes.ReadNextVarInt(packetData) == 0x01) - { - long pongPayload = dataTypes.ReadNextLong(packetData); - pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload; - } + long pongPayload = dataTypes.ReadNextLong(pongPacketData); + pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload; } } catch diff --git a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs index 6931abdf..c3ed8b15 100644 --- a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs +++ b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using MinecraftClient.Crypto; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers { @@ -12,9 +15,14 @@ namespace MinecraftClient.Protocol.Handlers /// public class SocketWrapper { - readonly TcpClient c; - AesCfb8Stream? s; - bool encrypted = false; + private readonly TcpClient client; + private readonly Stream networkStream; + private readonly SemaphoreSlim sendSemaphore = new(1, 1); + private readonly byte[] singleByteBuffer = new byte[1]; + private AesCfb8Stream? encryptedStream; + private Stream readStream; + private Stream writeStream; + private bool encrypted = false; /// /// Initialize a new SocketWrapper @@ -22,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers /// TcpClient connected to the server public SocketWrapper(TcpClient client) { - c = client; + this.client = client; + networkStream = client.GetStream(); + readStream = writeStream = networkStream; } /// @@ -32,7 +42,7 @@ namespace MinecraftClient.Protocol.Handlers /// Silently dropped connection can only be detected by attempting to read/write data public bool IsConnected() { - return c.Client is not null && c.Connected; + return client.Client is not null && client.Connected; } /// @@ -41,7 +51,7 @@ namespace MinecraftClient.Protocol.Handlers /// TRUE if data is available to read public bool HasDataAvailable() { - return c.Client.Available > 0; + return client.Client.Available > 0; } /// @@ -52,39 +62,21 @@ namespace MinecraftClient.Protocol.Handlers { if (encrypted) throw new InvalidOperationException("Stream is already encrypted!?"); - s = new AesCfb8Stream(c.GetStream(), secretKey); + encryptedStream = new AesCfb8Stream(networkStream, secretKey); + readStream = writeStream = encryptedStream; encrypted = true; } - /// - /// Network reading method. Read bytes from the socket or encrypted socket. - /// - private void Receive(byte[] buffer, int start, int offset, SocketFlags f) + public byte ReadByteRAW() { - int read = 0; - while (read < offset) - { - if (encrypted) - read += s!.Read(buffer, start + read, offset - read); - else - read += c.Client.Receive(buffer, start + read, offset - read, f); - } + readStream.ReadExactly(singleByteBuffer); + return singleByteBuffer[0]; } - private async Task ReceiveAsync(Memory buffer, CancellationToken cancellationToken) + public async ValueTask ReadByteRAWAsync(CancellationToken cancellationToken) { - int read = 0; - while (read < buffer.Length) - { - int currentRead = encrypted - ? await s!.ReadAsync(buffer[read..], cancellationToken) - : await c.GetStream().ReadAsync(buffer[read..], cancellationToken); - - if (currentRead == 0) - throw new IOException("Connection closed."); - - read += currentRead; - } + await readStream.ReadExactlyAsync(singleByteBuffer.AsMemory(0, 1), cancellationToken); + return singleByteBuffer[0]; } /// @@ -96,8 +88,8 @@ namespace MinecraftClient.Protocol.Handlers { if (length > 0) { - byte[] cache = new byte[length]; - Receive(cache, 0, length, SocketFlags.None); + byte[] cache = GC.AllocateUninitializedArray(length); + readStream.ReadExactly(cache); return cache; } return Array.Empty(); @@ -107,14 +99,34 @@ namespace MinecraftClient.Protocol.Handlers { if (length > 0) { - byte[] cache = new byte[length]; - await ReceiveAsync(cache, cancellationToken); + byte[] cache = GC.AllocateUninitializedArray(length); + await readStream.ReadExactlyAsync(cache.AsMemory(0, length), cancellationToken); return cache; } return Array.Empty(); } + internal Tuple> GetNextPacket(int compressionThreshold, DataTypes dataTypes) + { + int packetLength = ReadNextVarIntRaw(); + using PacketReadStream packetStream = new(readStream, packetLength); + byte[] payload = ReadPacketPayload(packetStream, compressionThreshold); + Queue packetData = new(payload); + int packetId = dataTypes.ReadNextVarInt(packetData); + return new(packetId, packetData); + } + + internal async Task>> GetNextPacketAsync(int compressionThreshold, DataTypes dataTypes, CancellationToken cancellationToken) + { + int packetLength = await ReadNextVarIntRawAsync(cancellationToken); + await using PacketReadStream packetStream = new(readStream, packetLength); + byte[] payload = await ReadPacketPayloadAsync(packetStream, compressionThreshold, cancellationToken); + Queue packetData = new(payload); + int packetId = dataTypes.ReadNextVarInt(packetData); + return new(packetId, packetData); + } + /// /// Send raw data to the server. /// @@ -124,10 +136,16 @@ namespace MinecraftClient.Protocol.Handlers if (!IsConnected()) throw new SocketException((int)SocketError.NotConnected); - if (encrypted) - s!.Write(buffer, 0, buffer.Length); - else - c.Client.Send(buffer); + sendSemaphore.Wait(); + try + { + writeStream.Write(buffer, 0, buffer.Length); + writeStream.Flush(); + } + finally + { + sendSemaphore.Release(); + } } public async Task SendDataRAWAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) @@ -135,10 +153,16 @@ namespace MinecraftClient.Protocol.Handlers if (!IsConnected()) throw new SocketException((int)SocketError.NotConnected); - if (encrypted) - await s!.WriteAsync(buffer, cancellationToken); - else - await c.GetStream().WriteAsync(buffer, cancellationToken); + await sendSemaphore.WaitAsync(cancellationToken); + try + { + await writeStream.WriteAsync(buffer, cancellationToken); + await writeStream.FlushAsync(cancellationToken); + } + finally + { + sendSemaphore.Release(); + } } /// @@ -148,12 +172,117 @@ namespace MinecraftClient.Protocol.Handlers { try { - c.Close(); + encryptedStream?.Dispose(); + client.Close(); } catch (SocketException) { } catch (System.IO.IOException) { } catch (NullReferenceException) { } catch (ObjectDisposedException) { } } + + private int ReadNextVarIntRaw() + { + int value = 0; + int position = 0; + + while (true) + { + byte current = ReadByteRAW(); + value |= (current & 0x7F) << position++ * 7; + if (position > 5) + throw new OverflowException("VarInt too big"); + if ((current & 0x80) != 0x80) + return value; + } + } + + private async Task ReadNextVarIntRawAsync(CancellationToken cancellationToken) + { + int value = 0; + int position = 0; + + while (true) + { + byte current = await ReadByteRAWAsync(cancellationToken); + value |= (current & 0x7F) << position++ * 7; + if (position > 5) + throw new OverflowException("VarInt too big"); + if ((current & 0x80) != 0x80) + return value; + } + } + + private static byte[] ReadPacketPayload(PacketReadStream packetStream, int compressionThreshold) + { + if (compressionThreshold >= 0) + { + int uncompressedLength = ReadNextVarIntRaw(packetStream); + if (uncompressedLength > 0) + { + using ZLibStream zlibStream = new(packetStream, CompressionMode.Decompress, leaveOpen: true); + byte[] payload = GC.AllocateUninitializedArray(uncompressedLength); + zlibStream.ReadExactly(payload); + return payload; + } + } + + return packetStream.ReadRemaining(); + } + + private static async Task ReadPacketPayloadAsync(PacketReadStream packetStream, int compressionThreshold, CancellationToken cancellationToken) + { + if (compressionThreshold >= 0) + { + int uncompressedLength = await ReadNextVarIntRawAsync(packetStream, cancellationToken); + if (uncompressedLength > 0) + { + await using ZLibStream zlibStream = new(packetStream, CompressionMode.Decompress, leaveOpen: true); + byte[] payload = GC.AllocateUninitializedArray(uncompressedLength); + await zlibStream.ReadExactlyAsync(payload.AsMemory(0, uncompressedLength), cancellationToken); + return payload; + } + } + + return await packetStream.ReadRemainingAsync(cancellationToken); + } + + private static int ReadNextVarIntRaw(Stream stream) + { + int value = 0; + int position = 0; + + while (true) + { + int current = stream.ReadByte(); + if (current < 0) + throw new IOException("Connection closed."); + + value |= (current & 0x7F) << position++ * 7; + if (position > 5) + throw new OverflowException("VarInt too big"); + if ((current & 0x80) != 0x80) + return value; + } + } + + private static async Task ReadNextVarIntRawAsync(Stream stream, CancellationToken cancellationToken) + { + byte[] buffer = new byte[1]; + int value = 0; + int position = 0; + + while (true) + { + await stream.ReadExactlyAsync(buffer.AsMemory(0, 1), cancellationToken); + byte current = buffer[0]; + + value |= (current & 0x7F) << position++ * 7; + if (position > 5) + throw new OverflowException("VarInt too big"); + if ((current & 0x80) != 0x80) + return value; + } + } } } diff --git a/MinecraftClient/Protocol/PacketPipeline/PacketReadStream.cs b/MinecraftClient/Protocol/PacketPipeline/PacketReadStream.cs new file mode 100644 index 00000000..167eb03b --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/PacketReadStream.cs @@ -0,0 +1,203 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MinecraftClient.Protocol.PacketPipeline; + +internal sealed class PacketReadStream : Stream +{ + private const int DrainBufferSize = 4096; + + private readonly Stream baseStream; + private readonly byte[] singleByteBuffer = new byte[1]; + private int remainingLength; + + public PacketReadStream(Stream baseStream, int packetLength) + { + ArgumentNullException.ThrowIfNull(baseStream); + if (packetLength < 0) + throw new ArgumentOutOfRangeException(nameof(packetLength)); + + this.baseStream = baseStream; + remainingLength = packetLength; + } + + public int RemainingLength => remainingLength; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (remainingLength == 0) + return 0; + + int readLength = Math.Min(count, remainingLength); + int read = baseStream.Read(buffer, offset, readLength); + remainingLength -= read; + return read; + } + + public override int Read(Span buffer) + { + if (remainingLength == 0) + return 0; + + int readLength = Math.Min(buffer.Length, remainingLength); + int read = baseStream.Read(buffer[..readLength]); + remainingLength -= read; + return read; + } + + public override int ReadByte() + { + if (remainingLength == 0) + return -1; + + int value = baseStream.ReadByte(); + if (value == -1) + return -1; + + remainingLength--; + return value; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (remainingLength == 0) + return 0; + + int readLength = Math.Min(buffer.Length, remainingLength); + int read = await baseStream.ReadAsync(buffer[..readLength], cancellationToken); + remainingLength -= read; + return read; + } + + public new async ValueTask ReadExactlyAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (buffer.Length > remainingLength) + throw new OverflowException("Reached the end of the packet."); + + await baseStream.ReadExactlyAsync(buffer, cancellationToken); + remainingLength -= buffer.Length; + } + + public new void ReadExactly(Span buffer) + { + if (buffer.Length > remainingLength) + throw new OverflowException("Reached the end of the packet."); + + baseStream.ReadExactly(buffer); + remainingLength -= buffer.Length; + } + + public byte[] ReadRemaining() + { + if (remainingLength == 0) + return []; + + byte[] buffer = GC.AllocateUninitializedArray(remainingLength); + ReadExactly(buffer); + return buffer; + } + + public async Task ReadRemainingAsync(CancellationToken cancellationToken = default) + { + if (remainingLength == 0) + return []; + + byte[] buffer = GC.AllocateUninitializedArray(remainingLength); + await ReadExactlyAsync(buffer, cancellationToken); + return buffer; + } + + public void DrainRemaining() + { + if (remainingLength == 0) + return; + + byte[] buffer = GC.AllocateUninitializedArray(Math.Min(DrainBufferSize, remainingLength)); + while (remainingLength > 0) + { + int read = baseStream.Read(buffer, 0, Math.Min(buffer.Length, remainingLength)); + if (read <= 0) + throw new EndOfStreamException("Connection closed while draining packet data."); + + remainingLength -= read; + } + } + + public async ValueTask DrainRemainingAsync(CancellationToken cancellationToken = default) + { + if (remainingLength == 0) + return; + + byte[] buffer = GC.AllocateUninitializedArray(Math.Min(DrainBufferSize, remainingLength)); + while (remainingLength > 0) + { + int read = await baseStream.ReadAsync(buffer.AsMemory(0, Math.Min(buffer.Length, remainingLength)), cancellationToken); + if (read <= 0) + throw new EndOfStreamException("Connection closed while draining packet data."); + + remainingLength -= read; + } + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && remainingLength > 0) + { + try + { + DrainRemaining(); + } + catch (IOException) { } + catch (ObjectDisposedException) { } + } + + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (remainingLength > 0) + { + try + { + await DrainRemainingAsync(); + } + catch (IOException) { } + catch (ObjectDisposedException) { } + } + + await base.DisposeAsync(); + } +} diff --git a/MinecraftClient/Tui/TuiConsoleBackend.cs b/MinecraftClient/Tui/TuiConsoleBackend.cs index bee59751..7a12df89 100644 --- a/MinecraftClient/Tui/TuiConsoleBackend.cs +++ b/MinecraftClient/Tui/TuiConsoleBackend.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Threading; using Consolonia; @@ -48,12 +49,11 @@ namespace MinecraftClient.Tui Dispatcher.UIThread.Post(() => view.HandleCtrlC()); }; - new Thread(() => + _ = Task.Run(() => { _viewReady.Wait(); ContinueMccStartup(args); - }) - { Name = "MCC-Main", IsBackground = true }.Start(); + }); AppBuilder builder = AppBuilder.Configure() .UseConsolonia() @@ -206,32 +206,49 @@ namespace MinecraftClient.Tui } public string RequestImmediateInput() + { + return RequestImmediateInputAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public Task RequestImmediateInputAsync(CancellationToken cancellationToken) { if (_shutdownRequested) { - Thread.Sleep(Timeout.Infinite); - return string.Empty; + return Task.FromCanceled(cancellationToken.CanBeCanceled + ? cancellationToken + : new CancellationToken(canceled: true)); } - var mre = new ManualResetEventSlim(false); - string? result = null; + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); void Handler(object? sender, string e) { - result = e; - mre.Set(); + MessageReceived -= Handler; + completion.TrySetResult(e); } MessageReceived += Handler; - mre.Wait(); - MessageReceived -= Handler; - return result ?? string.Empty; + if (cancellationToken.CanBeCanceled) + { + cancellationToken.Register(() => + { + MessageReceived -= Handler; + completion.TrySetCanceled(cancellationToken); + }); + } + + return completion.Task; } public string? ReadPassword() { - return RequestImmediateInput(); + return ReadPasswordAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public async Task ReadPasswordAsync(CancellationToken cancellationToken) + { + return await RequestImmediateInputAsync(cancellationToken); } public void ClearInputBuffer() @@ -267,11 +284,11 @@ namespace MinecraftClient.Tui Dispatcher.UIThread.Post(() => lifetime.Shutdown()); } - new Thread(() => + _ = Task.Run(async () => { - Thread.Sleep(1000); + await Task.Delay(1000); Environment.Exit(0); - }) { Name = "TUI-Exit-Guard", IsBackground = true }.Start(); + }); } private volatile bool _shutdownRequested;