2026-03-26 02:35:43 +08:00
|
|
|
using System;
|
2026-04-04 12:26:27 +02:00
|
|
|
using System.Collections.Generic;
|
2026-04-04 00:32:07 +02:00
|
|
|
using System.IO;
|
2026-04-04 12:26:27 +02:00
|
|
|
using System.IO.Compression;
|
2019-04-29 22:30:43 +02:00
|
|
|
using System.Net.Sockets;
|
2026-04-04 00:32:07 +02:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2019-04-29 22:30:43 +02:00
|
|
|
using MinecraftClient.Crypto;
|
2026-04-04 12:26:27 +02:00
|
|
|
using MinecraftClient.Protocol.PacketPipeline;
|
2019-04-29 22:30:43 +02:00
|
|
|
|
|
|
|
|
namespace MinecraftClient.Protocol.Handlers
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wrapper for handling unencrypted & encrypted socket
|
|
|
|
|
/// </summary>
|
2024-09-01 20:42:39 +02:00
|
|
|
public class SocketWrapper
|
2019-04-29 22:30:43 +02:00
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
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;
|
2019-04-29 22:30:43 +02:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize a new SocketWrapper
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="client">TcpClient connected to the server</param>
|
|
|
|
|
public SocketWrapper(TcpClient client)
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
this.client = client;
|
|
|
|
|
networkStream = client.GetStream();
|
|
|
|
|
readStream = writeStream = networkStream;
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Check if the socket is still connected
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>TRUE if still connected</returns>
|
|
|
|
|
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
|
|
|
|
|
public bool IsConnected()
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
return client.Client is not null && client.Connected;
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Check if the socket has data available to read
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>TRUE if data is available to read</returns>
|
|
|
|
|
public bool HasDataAvailable()
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
return client.Client.Available > 0;
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Switch network reading/writing to an encrypted stream
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="secretKey">AES secret key</param>
|
|
|
|
|
public void SwitchToEncrypted(byte[] secretKey)
|
|
|
|
|
{
|
|
|
|
|
if (encrypted)
|
|
|
|
|
throw new InvalidOperationException("Stream is already encrypted!?");
|
2026-04-04 12:26:27 +02:00
|
|
|
encryptedStream = new AesCfb8Stream(networkStream, secretKey);
|
|
|
|
|
readStream = writeStream = encryptedStream;
|
2022-10-02 18:31:08 +08:00
|
|
|
encrypted = true;
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:26:27 +02:00
|
|
|
public byte ReadByteRAW()
|
2019-04-29 22:30:43 +02:00
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
readStream.ReadExactly(singleByteBuffer);
|
|
|
|
|
return singleByteBuffer[0];
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:26:27 +02:00
|
|
|
public async ValueTask<byte> ReadByteRAWAsync(CancellationToken cancellationToken)
|
2026-04-04 00:32:07 +02:00
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
await readStream.ReadExactlyAsync(singleByteBuffer.AsMemory(0, 1), cancellationToken);
|
|
|
|
|
return singleByteBuffer[0];
|
2026-04-04 00:32:07 +02:00
|
|
|
}
|
|
|
|
|
|
2019-04-29 22:30:43 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Read some data from the server.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="length">Amount of bytes to read</param>
|
|
|
|
|
/// <returns>The data read from the network as an array</returns>
|
|
|
|
|
public byte[] ReadDataRAW(int length)
|
|
|
|
|
{
|
|
|
|
|
if (length > 0)
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
byte[] cache = GC.AllocateUninitializedArray<byte>(length);
|
|
|
|
|
readStream.ReadExactly(cache);
|
2019-04-29 22:30:43 +02:00
|
|
|
return cache;
|
|
|
|
|
}
|
2022-08-15 23:55:44 +08:00
|
|
|
return Array.Empty<byte>();
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 00:32:07 +02:00
|
|
|
public async Task<byte[]> ReadDataRAWAsync(int length, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (length > 0)
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
byte[] cache = GC.AllocateUninitializedArray<byte>(length);
|
|
|
|
|
await readStream.ReadExactlyAsync(cache.AsMemory(0, length), cancellationToken);
|
2026-04-04 00:32:07 +02:00
|
|
|
return cache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.Empty<byte>();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 22:28:05 +02:00
|
|
|
internal IncomingPacket GetNextPacket(int compressionThreshold, DataTypes dataTypes)
|
2026-04-04 12:26:27 +02:00
|
|
|
{
|
|
|
|
|
int packetLength = ReadNextVarIntRaw();
|
|
|
|
|
using PacketReadStream packetStream = new(readStream, packetLength);
|
|
|
|
|
byte[] payload = ReadPacketPayload(packetStream, compressionThreshold);
|
2026-04-04 22:28:05 +02:00
|
|
|
var packetData = new PacketReader(payload);
|
2026-04-04 12:26:27 +02:00
|
|
|
int packetId = dataTypes.ReadNextVarInt(packetData);
|
2026-04-04 22:28:05 +02:00
|
|
|
return new(packetId, packetData.CopyRemaining());
|
2026-04-04 12:26:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 22:28:05 +02:00
|
|
|
internal async Task<IncomingPacket> GetNextPacketAsync(int compressionThreshold, DataTypes dataTypes, CancellationToken cancellationToken)
|
2026-04-04 12:26:27 +02:00
|
|
|
{
|
|
|
|
|
int packetLength = await ReadNextVarIntRawAsync(cancellationToken);
|
|
|
|
|
await using PacketReadStream packetStream = new(readStream, packetLength);
|
|
|
|
|
byte[] payload = await ReadPacketPayloadAsync(packetStream, compressionThreshold, cancellationToken);
|
2026-04-04 22:28:05 +02:00
|
|
|
var packetData = new PacketReader(payload);
|
2026-04-04 12:26:27 +02:00
|
|
|
int packetId = dataTypes.ReadNextVarInt(packetData);
|
2026-04-04 22:28:05 +02:00
|
|
|
return new(packetId, packetData.CopyRemaining());
|
2026-04-04 12:26:27 +02:00
|
|
|
}
|
|
|
|
|
|
2019-04-29 22:30:43 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Send raw data to the server.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="buffer">data to send</param>
|
|
|
|
|
public void SendDataRAW(byte[] buffer)
|
|
|
|
|
{
|
2026-03-26 02:35:43 +08:00
|
|
|
if (!IsConnected())
|
|
|
|
|
throw new SocketException((int)SocketError.NotConnected);
|
|
|
|
|
|
2026-04-04 12:26:27 +02:00
|
|
|
sendSemaphore.Wait();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
writeStream.Write(buffer, 0, buffer.Length);
|
|
|
|
|
writeStream.Flush();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
sendSemaphore.Release();
|
|
|
|
|
}
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 00:32:07 +02:00
|
|
|
public async Task SendDataRAWAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (!IsConnected())
|
|
|
|
|
throw new SocketException((int)SocketError.NotConnected);
|
|
|
|
|
|
2026-04-04 12:26:27 +02:00
|
|
|
await sendSemaphore.WaitAsync(cancellationToken);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await writeStream.WriteAsync(buffer, cancellationToken);
|
|
|
|
|
await writeStream.FlushAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
sendSemaphore.Release();
|
|
|
|
|
}
|
2026-04-04 00:32:07 +02:00
|
|
|
}
|
|
|
|
|
|
2019-04-29 22:30:43 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Disconnect from the server
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Disconnect()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-04-04 12:26:27 +02:00
|
|
|
encryptedStream?.Dispose();
|
|
|
|
|
client.Close();
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
catch (SocketException) { }
|
|
|
|
|
catch (System.IO.IOException) { }
|
|
|
|
|
catch (NullReferenceException) { }
|
|
|
|
|
catch (ObjectDisposedException) { }
|
|
|
|
|
}
|
2026-04-04 12:26:27 +02:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-29 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
}
|