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 { /// /// Wrapper for handling unencrypted & encrypted socket /// public class SocketWrapper { 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 /// /// TcpClient connected to the server public SocketWrapper(TcpClient client) { this.client = client; networkStream = client.GetStream(); readStream = writeStream = networkStream; } /// /// Check if the socket is still connected /// /// TRUE if still connected /// Silently dropped connection can only be detected by attempting to read/write data public bool IsConnected() { return client.Client is not null && client.Connected; } /// /// Check if the socket has data available to read /// /// TRUE if data is available to read public bool HasDataAvailable() { return client.Client.Available > 0; } /// /// Switch network reading/writing to an encrypted stream /// /// AES secret key public void SwitchToEncrypted(byte[] secretKey) { if (encrypted) throw new InvalidOperationException("Stream is already encrypted!?"); encryptedStream = new AesCfb8Stream(networkStream, secretKey); readStream = writeStream = encryptedStream; encrypted = true; } public byte ReadByteRAW() { readStream.ReadExactly(singleByteBuffer); return singleByteBuffer[0]; } public async ValueTask ReadByteRAWAsync(CancellationToken cancellationToken) { await readStream.ReadExactlyAsync(singleByteBuffer.AsMemory(0, 1), cancellationToken); return singleByteBuffer[0]; } /// /// Read some data from the server. /// /// Amount of bytes to read /// The data read from the network as an array public byte[] ReadDataRAW(int length) { if (length > 0) { byte[] cache = GC.AllocateUninitializedArray(length); readStream.ReadExactly(cache); return cache; } return Array.Empty(); } public async Task ReadDataRAWAsync(int length, CancellationToken cancellationToken) { if (length > 0) { 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. /// /// data to send public void SendDataRAW(byte[] buffer) { if (!IsConnected()) throw new SocketException((int)SocketError.NotConnected); sendSemaphore.Wait(); try { writeStream.Write(buffer, 0, buffer.Length); writeStream.Flush(); } finally { sendSemaphore.Release(); } } public async Task SendDataRAWAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { if (!IsConnected()) throw new SocketException((int)SocketError.NotConnected); await sendSemaphore.WaitAsync(cancellationToken); try { await writeStream.WriteAsync(buffer, cancellationToken); await writeStream.FlushAsync(cancellationToken); } finally { sendSemaphore.Release(); } } /// /// Disconnect from the server /// public void Disconnect() { try { 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; } } } }