mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Faster AES, Packet Stream, more async changes
This commit is contained in:
parent
3b7c9c8510
commit
3a20f2e235
19 changed files with 1009 additions and 272 deletions
|
|
@ -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<string> 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<string?> ReadPasswordAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(false);
|
||||
try
|
||||
{
|
||||
return await RequestImmediateInputAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearInputBuffer()
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.ClearBuffer();
|
||||
|
|
|
|||
|
|
@ -76,6 +76,13 @@ namespace MinecraftClient
|
|||
return Backend.ReadPassword();
|
||||
}
|
||||
|
||||
public static Task<string?> ReadPasswordAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (BasicIO)
|
||||
return Task.FromResult<string?>(Console.ReadLine());
|
||||
return Backend.ReadPasswordAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a line from the standard input
|
||||
/// </summary>
|
||||
|
|
@ -86,6 +93,13 @@ namespace MinecraftClient
|
|||
return Backend.RequestImmediateInput();
|
||||
}
|
||||
|
||||
public static Task<string> ReadLineAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (BasicIO)
|
||||
return Task.FromResult(Console.ReadLine() ?? string.Empty);
|
||||
return Backend.RequestImmediateInputAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug routine: print all keys pressed in the console
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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<byte> 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<byte> blockInput, Span<byte> 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<byte> blockOutput = stackalloc byte[blockSize];
|
||||
byte[] inputBuf = ArrayPool<byte>.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<byte> 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<byte> 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<byte>.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<byte>.Shared.Rent(blockSize + required);
|
||||
|
||||
Span<byte> blockOutput = stackalloc byte[blockSize];
|
||||
for (int wirtten = 0; wirtten < required; ++wirtten)
|
||||
try
|
||||
{
|
||||
ReadOnlySpan<byte> 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<byte> blockOutput = stackalloc byte[blockSize];
|
||||
for (int written = 0; written < required; ++written)
|
||||
{
|
||||
ReadOnlySpan<byte> 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<byte>.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<byte>.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<byte>.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<byte>.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<byte>.Shared.Return(outputBuf);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<int> 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<byte> output, int start, int end)
|
||||
{
|
||||
|
|
|
|||
31
MinecraftClient/Crypto/AesHandler/BasicAes.cs
Normal file
31
MinecraftClient/Crypto/AesHandler/BasicAes.cs
Normal file
|
|
@ -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<byte> plaintext, Span<byte> destination)
|
||||
{
|
||||
aes.EncryptEcb(plaintext, destination, PaddingMode.None);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
aes.Dispose();
|
||||
}
|
||||
}
|
||||
162
MinecraftClient/Crypto/AesHandler/FasterAesArm.cs
Normal file
162
MinecraftClient/Crypto/AesHandler/FasterAesArm.cs
Normal file
|
|
@ -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<byte> 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<byte> plaintext, Span<byte> destination)
|
||||
{
|
||||
int position = 0;
|
||||
int left = plaintext.Length;
|
||||
|
||||
Vector128<byte> key0 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[0 * BlockSize]);
|
||||
Vector128<byte> key1 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[1 * BlockSize]);
|
||||
Vector128<byte> key2 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[2 * BlockSize]);
|
||||
Vector128<byte> key3 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[3 * BlockSize]);
|
||||
Vector128<byte> key4 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[4 * BlockSize]);
|
||||
Vector128<byte> key5 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[5 * BlockSize]);
|
||||
Vector128<byte> key6 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[6 * BlockSize]);
|
||||
Vector128<byte> key7 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[7 * BlockSize]);
|
||||
Vector128<byte> key8 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[8 * BlockSize]);
|
||||
Vector128<byte> key9 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[9 * BlockSize]);
|
||||
Vector128<byte> key10 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[10 * BlockSize]);
|
||||
|
||||
while (left >= BlockSize)
|
||||
{
|
||||
Vector128<byte> block = Unsafe.ReadUnaligned<Vector128<byte>>(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<byte> 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<byte> 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<int> 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
|
||||
];
|
||||
}
|
||||
89
MinecraftClient/Crypto/AesHandler/FasterAesX86.cs
Normal file
89
MinecraftClient/Crypto/AesHandler/FasterAesX86.cs
Normal file
|
|
@ -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<byte>[] RoundKeys { get; }
|
||||
|
||||
public FasterAesX86(ReadOnlySpan<byte> key)
|
||||
{
|
||||
RoundKeys = KeyExpansion(key);
|
||||
}
|
||||
|
||||
public static bool IsSupported()
|
||||
{
|
||||
return Sse2.IsSupported && Aes.IsSupported;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public override void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
|
||||
{
|
||||
Vector128<byte>[] keys = RoundKeys;
|
||||
|
||||
ReadOnlySpan<Vector128<byte>> blocks = MemoryMarshal.Cast<byte, Vector128<byte>>(plaintext);
|
||||
Span<Vector128<byte>> dest = MemoryMarshal.Cast<byte, Vector128<byte>>(destination);
|
||||
|
||||
_ = keys[10];
|
||||
|
||||
for (int i = 0; i < blocks.Length; i++)
|
||||
{
|
||||
Vector128<byte> 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<byte>[] KeyExpansion(ReadOnlySpan<byte> key)
|
||||
{
|
||||
Vector128<byte>[] keys = new Vector128<byte>[20];
|
||||
|
||||
keys[0] = Unsafe.ReadUnaligned<Vector128<byte>>(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<byte>[] keys, int index, byte rcon)
|
||||
{
|
||||
Vector128<byte> s = keys[index - 1];
|
||||
Vector128<byte> 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);
|
||||
}
|
||||
}
|
||||
20
MinecraftClient/Crypto/AesHandlerFactory.cs
Normal file
20
MinecraftClient/Crypto/AesHandlerFactory.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using MinecraftClient.Crypto.AesHandler;
|
||||
|
||||
namespace MinecraftClient.Crypto;
|
||||
|
||||
internal static class AesHandlerFactory
|
||||
{
|
||||
public static IAesHandler Create(ReadOnlySpan<byte> key)
|
||||
{
|
||||
byte[] ownedKey = key.ToArray();
|
||||
|
||||
if (FasterAesX86.IsSupported())
|
||||
return new FasterAesX86(ownedKey);
|
||||
|
||||
if (FasterAesArm.IsSupported())
|
||||
return new FasterAesArm(ownedKey);
|
||||
|
||||
return new BasicAes(ownedKey);
|
||||
}
|
||||
}
|
||||
12
MinecraftClient/Crypto/IAesHandler.cs
Normal file
12
MinecraftClient/Crypto/IAesHandler.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Crypto;
|
||||
|
||||
public abstract class IAesHandler : IDisposable
|
||||
{
|
||||
public abstract void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FileSystemWatcher, CancellationTokenSource>? monitor = null;
|
||||
private readonly Tuple<Thread, CancellationTokenSource>? polling = null;
|
||||
private readonly Tuple<Task, CancellationTokenSource>? polling = null;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new FileMonitor and start monitoring
|
||||
|
|
@ -48,9 +49,9 @@ namespace MinecraftClient
|
|||
|
||||
monitor = null;
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
polling = new Tuple<Thread, CancellationTokenSource>(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, CancellationTokenSource>(
|
||||
Task.Run(() => PollingLoopAsync(folder, filename, handler, cancellationTokenSource.Token), cancellationTokenSource.Token),
|
||||
cancellationTokenSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,25 +67,29 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback polling thread for use when operating system does not support FileSystemWatcher
|
||||
/// Fallback polling loop for use when operating system does not support FileSystemWatcher
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder to monitor</param>
|
||||
/// <param name="filename">File name to monitor</param>
|
||||
/// <param name="handler">Callback when file changes</param>
|
||||
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) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
|
|
@ -56,8 +58,12 @@ namespace MinecraftClient
|
|||
|
||||
string RequestImmediateInput();
|
||||
|
||||
Task<string> RequestImmediateInputAsync(CancellationToken cancellationToken);
|
||||
|
||||
string? ReadPassword();
|
||||
|
||||
Task<string?> ReadPasswordAsync(CancellationToken cancellationToken);
|
||||
|
||||
void ClearInputBuffer();
|
||||
|
||||
bool DisplayUserInput { get; set; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <summary>
|
||||
/// Reduest user to submit password.
|
||||
/// </summary>
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="delaySeconds">Optional delay, in seconds, before restarting</param>
|
||||
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
|
||||
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
|
|||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <summary>
|
||||
/// Serialized packet/tick loop.
|
||||
/// </summary>
|
||||
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
|
|||
/// <param name="packetData">will contain raw packet Data</param>
|
||||
internal Tuple<int, Queue<byte>> ReadNextPacket()
|
||||
{
|
||||
var size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size
|
||||
Queue<byte> 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<byte>(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<Tuple<int, Queue<byte>>> ReadNextPacketAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var size = await dataTypes.ReadNextVarIntRAWAsync(socketWrapper, cancellationToken); //Packet size
|
||||
Queue<byte> 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<byte>(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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the updating thread. Should be called after login success.
|
||||
/// Start the serialized packet/tick tasks. Should be called after login success.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<byte>(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<byte>(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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new SocketWrapper
|
||||
|
|
@ -22,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="client">TcpClient connected to the server</param>
|
||||
public SocketWrapper(TcpClient client)
|
||||
{
|
||||
c = client;
|
||||
this.client = client;
|
||||
networkStream = client.GetStream();
|
||||
readStream = writeStream = networkStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -32,7 +42,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
|
||||
public bool IsConnected()
|
||||
{
|
||||
return c.Client is not null && c.Connected;
|
||||
return client.Client is not null && client.Connected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -41,7 +51,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <returns>TRUE if data is available to read</returns>
|
||||
public bool HasDataAvailable()
|
||||
{
|
||||
return c.Client.Available > 0;
|
||||
return client.Client.Available > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network reading method. Read bytes from the socket or encrypted socket.
|
||||
/// </summary>
|
||||
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<byte> buffer, CancellationToken cancellationToken)
|
||||
public async ValueTask<byte> 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];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<byte>(length);
|
||||
readStream.ReadExactly(cache);
|
||||
return cache;
|
||||
}
|
||||
return Array.Empty<byte>();
|
||||
|
|
@ -107,14 +99,34 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
if (length > 0)
|
||||
{
|
||||
byte[] cache = new byte[length];
|
||||
await ReceiveAsync(cache, cancellationToken);
|
||||
byte[] cache = GC.AllocateUninitializedArray<byte>(length);
|
||||
await readStream.ReadExactlyAsync(cache.AsMemory(0, length), cancellationToken);
|
||||
return cache;
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
internal Tuple<int, Queue<byte>> GetNextPacket(int compressionThreshold, DataTypes dataTypes)
|
||||
{
|
||||
int packetLength = ReadNextVarIntRaw();
|
||||
using PacketReadStream packetStream = new(readStream, packetLength);
|
||||
byte[] payload = ReadPacketPayload(packetStream, compressionThreshold);
|
||||
Queue<byte> packetData = new(payload);
|
||||
int packetId = dataTypes.ReadNextVarInt(packetData);
|
||||
return new(packetId, packetData);
|
||||
}
|
||||
|
||||
internal async Task<Tuple<int, Queue<byte>>> 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<byte> packetData = new(payload);
|
||||
int packetId = dataTypes.ReadNextVarInt(packetData);
|
||||
return new(packetId, packetData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send raw data to the server.
|
||||
/// </summary>
|
||||
|
|
@ -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<byte> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<int> 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<byte>(uncompressedLength);
|
||||
zlibStream.ReadExactly(payload);
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
return packetStream.ReadRemaining();
|
||||
}
|
||||
|
||||
private static async Task<byte[]> 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<byte>(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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
203
MinecraftClient/Protocol/PacketPipeline/PacketReadStream.cs
Normal file
203
MinecraftClient/Protocol/PacketPipeline/PacketReadStream.cs
Normal file
|
|
@ -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<byte> 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<int> ReadAsync(Memory<byte> 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<byte> 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<byte> 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<byte>(remainingLength);
|
||||
ReadExactly(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadRemainingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (remainingLength == 0)
|
||||
return [];
|
||||
|
||||
byte[] buffer = GC.AllocateUninitializedArray<byte>(remainingLength);
|
||||
await ReadExactlyAsync(buffer, cancellationToken);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void DrainRemaining()
|
||||
{
|
||||
if (remainingLength == 0)
|
||||
return;
|
||||
|
||||
byte[] buffer = GC.AllocateUninitializedArray<byte>(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<byte>(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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MccTuiApp>()
|
||||
.UseConsolonia()
|
||||
|
|
@ -206,32 +206,49 @@ namespace MinecraftClient.Tui
|
|||
}
|
||||
|
||||
public string RequestImmediateInput()
|
||||
{
|
||||
return RequestImmediateInputAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<string> RequestImmediateInputAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_shutdownRequested)
|
||||
{
|
||||
Thread.Sleep(Timeout.Infinite);
|
||||
return string.Empty;
|
||||
return Task.FromCanceled<string>(cancellationToken.CanBeCanceled
|
||||
? cancellationToken
|
||||
: new CancellationToken(canceled: true));
|
||||
}
|
||||
|
||||
var mre = new ManualResetEventSlim(false);
|
||||
string? result = null;
|
||||
TaskCompletionSource<string> 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<string?> 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue