This commit is contained in:
Anon 2026-04-04 00:32:07 +02:00
parent 1ca023be36
commit 3a4d8951d5
20 changed files with 1511 additions and 389 deletions

View file

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Mapping;
@ -313,6 +315,27 @@ namespace MinecraftClient.Protocol.Handlers
return i;
}
/// <summary>
/// Read an integer from the network asynchronously.
/// </summary>
/// <returns>The integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public async Task<int> ReadNextVarIntRAWAsync(SocketWrapper socket, CancellationToken cancellationToken)
{
int i = 0;
int j = 0;
byte b;
while (true)
{
b = (await socket.ReadDataRAWAsync(1, cancellationToken))[0];
i |= (b & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b & 0x80) != 128) break;
}
return i;
}
/// <summary>
/// Read an integer from a cache of bytes and remove it from the cache
/// </summary>

View file

@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
@ -29,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers
readonly IMinecraftComHandler handler;
private bool encrypted = false;
private readonly int protocolversion;
private Tuple<Thread, CancellationTokenSource>? netRead = null;
private Task? netReadTask;
private CancellationTokenSource? netReadCancellationTokenSource;
private int netReadThreadId = -1;
Crypto.AesCfb8Stream? s;
readonly TcpClient c;
@ -69,15 +72,15 @@ namespace MinecraftClient.Protocol.Handlers
c = Client;
}
private void Updater(object? o)
private void Updater(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
if (cancelToken.IsCancellationRequested)
return;
try
{
netReadThreadId = Environment.CurrentManagedThreadId;
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
Stopwatch stopWatch = Stopwatch.StartNew();
long nextUpdateDue = 0;
@ -104,6 +107,8 @@ namespace MinecraftClient.Protocol.Handlers
catch (SocketException) { }
catch (ObjectDisposedException) { }
catch (OperationCanceledException) { }
catch (Exception) { }
finally { netReadThreadId = -1; }
if (cancelToken.IsCancellationRequested)
return;
@ -240,9 +245,13 @@ namespace MinecraftClient.Protocol.Handlers
private void StartUpdating()
{
netRead = new(new Thread(new ParameterizedThreadStart(Updater)), new CancellationTokenSource());
netRead.Item1.Name = "ProtocolPacketHandler";
netRead.Item1.Start(netRead.Item2.Token);
CancellationTokenSource netReadCts = new();
netReadCancellationTokenSource = netReadCts;
netReadTask = Task.Factory.StartNew(
() => Updater(netReadCts.Token),
netReadCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
/// <summary>
@ -251,7 +260,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>Net read thread ID</returns>
public int GetNetMainThreadId()
{
return netRead is not null ? netRead.Item1.ManagedThreadId : -1;
return netReadThreadId;
}
public bool SendCookieResponse(string name, byte[]? data)
@ -268,9 +277,9 @@ namespace MinecraftClient.Protocol.Handlers
{
try
{
if (netRead is not null)
if (netReadCancellationTokenSource is not null)
{
netRead.Item2.Cancel();
netReadCancellationTokenSource.Cancel();
c.Close();
}
}
@ -519,7 +528,8 @@ namespace MinecraftClient.Protocol.Handlers
Receive(pid, 0, 1, SocketFlags.None);
while (pid[0] == 0xFA) //Skip some early plugin messages
{
ProcessPacket(pid[0]);
using (MainThreadExecutionScope.Enter(handler))
ProcessPacket(pid[0]);
Receive(pid, 0, 1, SocketFlags.None);
}
if (pid[0] == 0xFD)
@ -559,8 +569,7 @@ namespace MinecraftClient.Protocol.Handlers
if (session.ServerPublicKey is not null && session.SessionPreCheckTask is not null
&& serverIDhash == session.ServerIDhash && Enumerable.SequenceEqual(serverPublicKey, session.ServerPublicKey))
{
session.SessionPreCheckTask.Wait();
if (session.SessionPreCheckTask.Result) // PreCheck Successed
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
needCheckSession = false;
}
@ -633,7 +642,8 @@ namespace MinecraftClient.Protocol.Handlers
Receive(pid, 0, 1, SocketFlags.None);
while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages
{
ProcessPacket(pid[0]);
using (MainThreadExecutionScope.Enter(handler))
ProcessPacket(pid[0]);
Receive(pid, 0, 1, SocketFlags.None);
}
if (pid[0] == (byte)1)

View file

@ -9,6 +9,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Inventory;
using MinecraftClient.Inventory.ItemPalettes;
@ -117,8 +118,11 @@ namespace MinecraftClient.Protocol.Handlers
readonly PacketTypePalette packetPalette;
readonly SocketWrapper socketWrapper;
readonly DataTypes dataTypes;
Tuple<Thread, CancellationTokenSource>? netMain = null; // main thread
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
private Task? netMainTask;
private CancellationTokenSource? netMainCancellationTokenSource;
private int netMainThreadId = -1;
private Task? netReaderTask;
private CancellationTokenSource? netReaderCancellationTokenSource;
readonly ILogger log;
readonly RandomNumberGenerator randomGen;
private bool legacyAchievementsInitialized;
@ -278,17 +282,17 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Separate thread. Network reading loop.
/// Serialized packet/tick loop.
/// </summary>
private void Updater(object? o)
private void Updater(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
if (cancelToken.IsCancellationRequested)
return;
try
{
netMainThreadId = Environment.CurrentManagedThreadId;
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
Stopwatch stopWatch = Stopwatch.StartNew();
long nextUpdateDue = 0;
while (!packetQueue.IsAddingCompleted)
@ -330,6 +334,13 @@ namespace MinecraftClient.Protocol.Handlers
catch (System.IO.IOException)
{
}
catch (Exception)
{
}
finally
{
netMainThreadId = -1;
}
if (cancelToken.IsCancellationRequested)
return;
@ -340,20 +351,13 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary>
/// Read and decompress packets.
/// </summary>
internal void PacketReader(object? o)
internal async Task PacketReaderAsync(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested)
while (!cancelToken.IsCancellationRequested)
{
try
{
while (socketWrapper.HasDataAvailable())
{
packetQueue.Add(ReadNextPacket(), cancelToken);
if (cancelToken.IsCancellationRequested)
break;
}
packetQueue.Add(await ReadNextPacketAsync(cancelToken), cancelToken);
}
catch (OperationCanceledException)
{
@ -375,11 +379,10 @@ namespace MinecraftClient.Protocol.Handlers
{
break;
}
if (cancelToken.IsCancellationRequested)
catch (Exception)
{
break;
Thread.Sleep(10);
}
}
packetQueue.CompleteAdding();
@ -415,6 +418,30 @@ namespace MinecraftClient.Protocol.Handlers
return new(packetId, packetData);
}
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);
if (handler.GetNetworkPacketCaptureEnabled())
handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true);
return new(packetId, packetData);
}
/// <summary>
/// Handle the given packet
/// </summary>
@ -3844,19 +3871,17 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
private void StartUpdating()
{
Thread threadUpdater = new(new ParameterizedThreadStart(Updater))
{
Name = "ProtocolPacketHandler"
};
netMain = new Tuple<Thread, CancellationTokenSource>(threadUpdater, new CancellationTokenSource());
threadUpdater.Start(netMain.Item2.Token);
CancellationTokenSource netMainCts = new();
netMainCancellationTokenSource = netMainCts;
netMainTask = Task.Factory.StartNew(
() => Updater(netMainCts.Token),
netMainCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Thread threadReader = new(new ParameterizedThreadStart(PacketReader))
{
Name = "ProtocolPacketReader"
};
netReader = new Tuple<Thread, CancellationTokenSource>(threadReader, new CancellationTokenSource());
threadReader.Start(netReader.Item2.Token);
CancellationTokenSource netReaderCts = new();
netReaderCancellationTokenSource = netReaderCts;
netReaderTask = PacketReaderAsync(netReaderCts.Token);
}
/// <summary>
@ -3865,7 +3890,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>Net read thread ID</returns>
public int GetNetMainThreadId()
{
return netMain is not null ? netMain.Item1.ManagedThreadId : -1;
return netMainThreadId;
}
/// <summary>
@ -3875,14 +3900,14 @@ namespace MinecraftClient.Protocol.Handlers
{
try
{
if (netMain is not null)
if (netMainCancellationTokenSource is not null)
{
netMain.Item2.Cancel();
netMainCancellationTokenSource.Cancel();
}
if (netReader is not null)
if (netReaderCancellationTokenSource is not null)
{
netReader.Item2.Cancel();
netReaderCancellationTokenSource.Cancel();
socketWrapper.Disconnect();
}
}
@ -4106,7 +4131,8 @@ namespace MinecraftClient.Protocol.Handlers
return true; //No need to check session or start encryption
}
default:
HandlePacket(packetId, packetData);
using (MainThreadExecutionScope.Enter(handler))
HandlePacket(packetId, packetData);
break;
}
}
@ -4133,8 +4159,7 @@ namespace MinecraftClient.Protocol.Handlers
&& serverIDhash == session.ServerIDhash &&
serverPublicKey.SequenceEqual(session.ServerPublicKey))
{
session.SessionPreCheckTask.Wait();
if (session.SessionPreCheckTask.Result) // PreCheck Success
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
needCheckSession = false;
}
@ -4256,7 +4281,8 @@ namespace MinecraftClient.Protocol.Handlers
return true;
}
default:
HandlePacket(packetId, packetData);
using (MainThreadExecutionScope.Enter(handler))
HandlePacket(packetId, packetData);
break;
}
}

View file

@ -1,5 +1,8 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.Handlers
@ -68,6 +71,22 @@ namespace MinecraftClient.Protocol.Handlers
}
}
private async Task ReceiveAsync(Memory<byte> buffer, 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;
}
}
/// <summary>
/// Read some data from the server.
/// </summary>
@ -84,6 +103,18 @@ namespace MinecraftClient.Protocol.Handlers
return Array.Empty<byte>();
}
public async Task<byte[]> ReadDataRAWAsync(int length, CancellationToken cancellationToken)
{
if (length > 0)
{
byte[] cache = new byte[length];
await ReceiveAsync(cache, cancellationToken);
return cache;
}
return Array.Empty<byte>();
}
/// <summary>
/// Send raw data to the server.
/// </summary>
@ -99,6 +130,17 @@ namespace MinecraftClient.Protocol.Handlers
c.Client.Send(buffer);
}
public async Task SendDataRAWAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
if (!IsConnected())
throw new SocketException((int)SocketError.NotConnected);
if (encrypted)
await s!.WriteAsync(buffer, cancellationToken);
else
await c.GetStream().WriteAsync(buffer, cancellationToken);
}
/// <summary>
/// Disconnect from the server
/// </summary>

View file

@ -7,6 +7,7 @@ using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using static MinecraftClient.Settings;
@ -231,6 +232,8 @@ namespace MinecraftClient.Protocol.Message
/// Specify whether translation rules have been loaded
/// </summary>
private static bool RulesInitialized = false;
private static readonly Lock RulesInitializationLock = new();
private static Task? RulesRefreshTask = null;
/// <summary>
/// Set of translation rules for formatting text
@ -243,23 +246,25 @@ namespace MinecraftClient.Protocol.Message
/// </summary>
public static void InitTranslations()
{
if (!RulesInitialized)
lock (RulesInitializationLock)
{
InitRules();
if (RulesInitialized)
return;
RulesInitialized = true;
RulesRefreshTask = InitRulesAsync();
_ = ObserveInitRulesAsync(RulesRefreshTask);
}
}
/// <summary>
/// Internal rule initialization method. Looks for local rule file or download it from Mojang asset servers.
/// Internal rule initialization method. Looks for local rule file and refreshes it from Mojang asset servers if needed.
/// </summary>
private static void InitRules()
private static async Task InitRulesAsync()
{
if (Config.Main.Advanced.Language == "en_us")
{
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
TranslationRules = LoadEmbeddedTranslationRules();
return;
}
@ -269,21 +274,9 @@ namespace MinecraftClient.Protocol.Message
string languageFilePath = "lang" + Path.DirectorySeparatorChar + Config.Main.Advanced.Language + ".json";
// Load the external dictionary of translation rules or display an error message
if (File.Exists(languageFilePath))
{
try
{
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
}
catch (IOException)
{
}
catch (JsonException)
{
}
}
if (TryLoadTranslationRulesFromFile(languageFilePath, out Dictionary<string, string>? translationRules))
TranslationRules = translationRules;
else TranslationRules = LoadEmbeddedTranslationRules();
if (TranslationRules.TryGetValue("Version", out string? version) &&
version == Settings.TranslationsFile_Version)
@ -296,14 +289,12 @@ namespace MinecraftClient.Protocol.Message
// Try downloading language file from Mojang's servers?
ConsoleIO.WriteLineFormatted(
"§8" + string.Format(Translations.chat_download, Config.Main.Advanced.Language));
HttpClient httpClient = new();
using HttpClient httpClient = new();
try
{
Task<string> fetch_index = httpClient.GetStringAsync(TranslationsFile_Website_Index);
fetch_index.Wait();
Match match = Regex.Match(fetch_index.Result,
string fetchIndex = await httpClient.GetStringAsync(TranslationsFile_Website_Index);
Match match = Regex.Match(fetchIndex,
$"minecraft/lang/{Config.Main.Advanced.Language}.json" + @""":\s\{""hash"":\s""([\d\w]{40})""");
fetch_index.Dispose();
if (match.Success && match.Groups.Count == 2)
{
string hash = match.Groups[1].Value;
@ -312,22 +303,19 @@ namespace MinecraftClient.Protocol.Message
ConsoleIO.WriteLineFormatted(
string.Format(Translations.chat_request, translation_file_location));
Task<Dictionary<string, string>?> fetckFileTask =
httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
fetckFileTask.Wait();
if (fetckFileTask.Result is not null && fetckFileTask.Result.Count > 0)
Dictionary<string, string>? fetchedFile =
await httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
if (fetchedFile is not null && fetchedFile.Count > 0)
{
TranslationRules = fetckFileTask.Result;
TranslationRules = fetchedFile;
TranslationRules["Version"] = TranslationsFile_Version;
File.WriteAllText(languageFilePath,
await File.WriteAllTextAsync(languageFilePath,
JsonSerializer.Serialize(TranslationRules, typeof(Dictionary<string, string>)),
Encoding.UTF8);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.chat_done, languageFilePath));
return;
}
fetckFileTask.Dispose();
}
else
{
@ -350,17 +338,52 @@ namespace MinecraftClient.Protocol.Message
if (Config.Logging.DebugMessages && !string.IsNullOrEmpty(e.StackTrace))
ConsoleIO.WriteLine(e.StackTrace);
}
finally
{
httpClient.Dispose();
}
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
TranslationRules = LoadEmbeddedTranslationRules();
ConsoleIO.WriteLine(Translations.chat_use_default);
}
private static async Task ObserveInitRulesAsync(Task initRulesTask)
{
try
{
await initRulesTask;
}
catch (Exception e)
{
TranslationRules = LoadEmbeddedTranslationRules();
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(e.ToString());
}
}
private static Dictionary<string, string> LoadEmbeddedTranslationRules()
{
return JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
}
private static bool TryLoadTranslationRulesFromFile(string languageFilePath, out Dictionary<string, string>? translationRules)
{
translationRules = null;
if (!File.Exists(languageFilePath))
return false;
try
{
translationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
return translationRules is not null;
}
catch (IOException)
{
return false;
}
catch (JsonException)
{
return false;
}
}
public static string? TranslateString(string rulename)
{
if (TranslationRules.TryGetValue(rulename, out string? result))
@ -617,4 +640,4 @@ namespace MinecraftClient.Protocol.Message
return formatting + message + extraBuilder.ToString();
}
}
}
}

View file

@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient.Protocol
{
@ -37,7 +38,13 @@ namespace MinecraftClient.Protocol
{
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
postData = string.Format(postData, clientId, code);
return RequestToken(postData);
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
public static Task<LoginResponse> RequestAccessTokenAsync(string code)
{
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
return RequestTokenAsync(string.Format(postData, clientId, code));
}
/// <summary>
@ -49,7 +56,13 @@ namespace MinecraftClient.Protocol
{
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
postData = string.Format(postData, clientId, refreshToken);
return RequestToken(postData);
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
public static Task<LoginResponse> RefreshAccessTokenAsync(string refreshToken)
{
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
return RequestTokenAsync(string.Format(postData, clientId, refreshToken));
}
/// <summary>
@ -58,6 +71,11 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <returns>Device code response for user to complete authentication</returns>
public static DeviceCodeResponse RequestDeviceCode()
{
return RequestDeviceCodeAsync().GetAwaiter().GetResult();
}
public static async Task<DeviceCodeResponse> RequestDeviceCodeAsync(CancellationToken cancellationToken = default)
{
string postData = string.Format("client_id={0}&scope=XboxLive.signin%20offline_access%20openid%20email", clientId);
@ -65,7 +83,7 @@ namespace MinecraftClient.Protocol
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
if (jsonData?["error"] is not null)
@ -93,6 +111,11 @@ namespace MinecraftClient.Protocol
/// <param name="interval">Polling interval in seconds</param>
/// <returns>Login response with access token and refresh token</returns>
public static LoginResponse PollDeviceCodeToken(string deviceCode, int expiresIn, int interval)
{
return PollDeviceCodeTokenAsync(deviceCode, expiresIn, interval).GetAwaiter().GetResult();
}
public static async Task<LoginResponse> PollDeviceCodeTokenAsync(string deviceCode, int expiresIn, int interval, CancellationToken cancellationToken = default)
{
// Per OAuth 2.0 device code spec, server may respond with "slow_down" requiring
// the client to increase its polling interval by this amount
@ -107,13 +130,13 @@ namespace MinecraftClient.Protocol
while (stopwatch.Elapsed.TotalSeconds < expiresIn)
{
Thread.Sleep(pollInterval * 1000);
await Task.Delay(TimeSpan.FromSeconds(pollInterval), cancellationToken);
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
if (jsonData?["error"] is not null)
@ -173,12 +196,17 @@ namespace MinecraftClient.Protocol
/// <param name="postData">Complete POST data for the request</param>
/// <returns></returns>
private static LoginResponse RequestToken(string postData)
{
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
private static async Task<LoginResponse> RequestTokenAsync(string postData, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
// Error handling
@ -271,6 +299,11 @@ namespace MinecraftClient.Protocol
/// <param name="loginResponse"></param>
/// <returns></returns>
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
{
return XblAuthenticateAsync(loginResponse).GetAwaiter().GetResult();
}
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(Microsoft.LoginResponse loginResponse, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(xbl)
{
@ -291,7 +324,7 @@ namespace MinecraftClient.Protocol
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
@ -321,6 +354,11 @@ namespace MinecraftClient.Protocol
/// <param name="xblResponse"></param>
/// <returns></returns>
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
{
return XSTSAuthenticateAsync(xblResponse).GetAwaiter().GetResult();
}
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(XblAuthenticateResponse xblResponse, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(xsts)
{
@ -339,7 +377,7 @@ namespace MinecraftClient.Protocol
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
@ -404,6 +442,11 @@ namespace MinecraftClient.Protocol
/// <param name="xstsToken"></param>
/// <returns></returns>
public static string LoginWithXbox(string userHash, string xstsToken)
{
return LoginWithXboxAsync(userHash, xstsToken).GetAwaiter().GetResult();
}
public static async Task<string> LoginWithXboxAsync(string userHash, string xstsToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(loginWithXbox)
{
@ -411,7 +454,7 @@ namespace MinecraftClient.Protocol
};
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
@ -430,10 +473,15 @@ namespace MinecraftClient.Protocol
/// <param name="accessToken"></param>
/// <returns>True if the user own the game</returns>
public static bool UserHasGame(string accessToken)
{
return UserHasGameAsync(accessToken).GetAwaiter().GetResult();
}
public static async Task<bool> UserHasGameAsync(string accessToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(ownership);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
var response = await request.GetAsync(cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
@ -446,10 +494,15 @@ namespace MinecraftClient.Protocol
}
public static UserProfile GetUserProfile(string accessToken)
{
return GetUserProfileAsync(accessToken).GetAwaiter().GetResult();
}
public static async Task<UserProfile> GetUserProfileAsync(string accessToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(profile);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
var response = await request.GetAsync(cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{

View file

@ -7,6 +7,8 @@ using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DnsClient;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.Forge;
@ -1108,6 +1110,43 @@ namespace MinecraftClient.Protocol
}
}
public static async Task<bool> SessionCheckAsync(string uuid, string accesstoken, string serverhash, LoginType type)
{
try
{
string jsonRequest = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid +
"\",\"serverId\":\"" + serverhash + "\"}";
string host = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.Host
: "sessionserver.mojang.com";
int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443;
string endpoint = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join"
: "/session/minecraft/join";
bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
var response = await DoHTTPSRequestAsync(
HttpMethod.Post,
host,
port,
endpoint,
new Dictionary<string, string>
{
{ "Accept", "application/json" },
{ "Content-Type", "application/json" }
},
jsonRequest,
useHttps,
CancellationToken.None);
return response.StatusCode >= 200 && response.StatusCode < 300;
}
catch
{
return false;
}
}
/// <summary>
/// Retrieve available Realms worlds of a player and display them
/// </summary>
@ -1349,6 +1388,57 @@ namespace MinecraftClient.Protocol
return statusCode;
}
private static async Task<(int StatusCode, string Result)> DoHTTPSRequestAsync(HttpMethod method, string host, int port, string path, Dictionary<string, string> headers, string? body, bool useHttps, CancellationToken cancellationToken)
{
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host));
using SocketsHttpHandler handler = new();
handler.ConnectCallback = async (ctx, ct) =>
{
TcpClient client = ProxyHandler.NewTcpClient(host, port, true);
return client.GetStream();
};
using HttpClient client = new(handler);
string scheme = useHttps ? "https" : "http";
using HttpRequestMessage request = new(method, scheme + "://" + host + ":" + port + path);
string contentType = "text/plain";
foreach (var header in headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
contentType = header.Value;
}
if (body is not null)
request.Content = new StringContent(body, Encoding.UTF8, contentType);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8> " + request);
using CancellationTokenSource timeoutCancellationTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
using HttpResponseMessage response = await client.SendAsync(request, timeoutCancellationTokenSource.Token);
int statusCode = (int)response.StatusCode;
string responseBody = statusCode == 204
? "No Content"
: await response.Content.ReadAsStringAsync(timeoutCancellationTokenSource.Token);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine("");
foreach (string line in responseBody.Split('\n'))
ConsoleIO.WriteLineFormatted("§8< " + line);
}
return (statusCode, responseBody);
}
/// <summary>
/// Encode a string to a json string.
/// Will convert special chars to \u0000 unicode escape sequences.
@ -1389,4 +1479,4 @@ namespace MinecraftClient.Protocol
return dateTime;
}
}
}
}

View file

@ -3,6 +3,8 @@ using System.Collections.Specialized;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Proxy;
namespace MinecraftClient.Protocol
@ -72,6 +74,12 @@ namespace MinecraftClient.Protocol
/// </summary>
public Response Get() => Send(HttpMethod.Get);
/// <summary>
/// Perform GET request asynchronously. Proxy is handled automatically.
/// </summary>
public Task<Response> GetAsync(CancellationToken cancellationToken = default) =>
SendAsync(HttpMethod.Get, cancellationToken: cancellationToken);
/// <summary>
/// Perform POST request. Proxy is handled automatically.
/// </summary>
@ -79,6 +87,14 @@ namespace MinecraftClient.Protocol
/// <param name="body">Request body</param>
public Response Post(string contentType, string body) => Send(HttpMethod.Post, contentType, body);
/// <summary>
/// Perform POST request asynchronously. Proxy is handled automatically.
/// </summary>
/// <param name="contentType">The content type of request body</param>
/// <param name="body">Request body</param>
public Task<Response> PostAsync(string contentType, string body, CancellationToken cancellationToken = default) =>
SendAsync(HttpMethod.Post, contentType, body, cancellationToken);
/// <summary>
/// Send an HTTP request. Proxy is configured automatically from Settings.
/// </summary>
@ -144,6 +160,66 @@ namespace MinecraftClient.Protocol
}
}
/// <summary>
/// Send an HTTP request asynchronously. Proxy is configured automatically from Settings.
/// </summary>
private async Task<Response> SendAsync(HttpMethod method, string? contentType = null, string? body = null, CancellationToken cancellationToken = default)
{
using var handler = CreateHandler();
using var client = new HttpClient(handler);
using var request = new HttpRequestMessage(method, _uri);
foreach (string key in Headers)
{
if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Host", StringComparison.OrdinalIgnoreCase))
continue;
request.Headers.TryAddWithoutValidation(key, Headers[key]);
}
if (body is not null)
request.Content = new StringContent(body, Encoding.UTF8, contentType ?? "text/plain");
if (Debug)
{
ConsoleIO.WriteLine($"< {method} {_uri}");
foreach (string key in Headers)
ConsoleIO.WriteLine($"< {key}: {Headers[key]}");
}
try
{
using var httpResponse = await client.SendAsync(request, cancellationToken);
string responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
var responseHeaders = new NameValueCollection();
foreach (var header in httpResponse.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
foreach (var header in httpResponse.Content.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
var cookies = new NameValueCollection();
foreach (Cookie cookie in handler.CookieContainer.GetCookies(_uri))
{
if (!cookie.Expired)
cookies.Add(cookie.Name, cookie.Value);
}
return new Response((int)httpResponse.StatusCode, responseBody, responseHeaders, cookies);
}
catch (HttpRequestException ex)
{
if (Debug)
ConsoleIO.WriteLine("HTTP error: " + ex.Message);
return Response.Empty();
}
}
/// <summary>
/// Create a SocketsHttpHandler with proxy support from ProxyHandler settings.
/// </summary>
@ -231,4 +307,4 @@ namespace MinecraftClient.Protocol
}
}
}
}
}

View file

@ -54,6 +54,16 @@ namespace MinecraftClient.Protocol.Session
return false;
}
public async Task<bool> SessionPreCheckAsync(LoginType type)
{
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey is null)
return false;
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
return await ProtocolHandler.SessionCheckAsync(PlayerID, ID, serverHash, type);
}
public override string ToString()
{
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,