diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index 604385b0..bc2ad08f 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Text; -using System.Text.Json.Nodes; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Message; using static MinecraftClient.Protocol.Message.LastSeenMessageList; @@ -13,42 +12,87 @@ namespace MinecraftClient.Protocol.ProfileKey { private static readonly SHA256 sha256Hash = SHA256.Create(); - private static readonly string certificates = "https://api.minecraftservices.com/player/certificates"; - - public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) + /// + /// Check whether the authentication server supports player profile keys. + /// For Yggdrasil servers, this fetches the authlib-injector metadata and checks the + /// feature.enable_profile_key flag documented at + /// https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83 + /// + public static bool AuthServerSupportsProfileKeys(bool isYggdrasil) { + if (!isYggdrasil) + return true; + ProxiedWebRequest.Response? response = null; try { - if (!isYggdrasil && string.IsNullOrWhiteSpace(accessToken)) - return null; - - if (!isYggdrasil) + var authServer = Settings.Config.Main.General.AuthServer; + var request = new ProxiedWebRequest( + (authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath) { - var request = new ProxiedWebRequest(certificates) - { - Accept = "application/json" - }; - request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + Accept = "application/json" + }; - response = request.Post("application/json", ""); + response = request.Get(); + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLine(response.Body.ToString()); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(response.Body.ToString()); - } + var json = Json.ParseJson(response.Body); + bool enableProfileKey = json?["meta"]?["feature.enable_profile_key"]?.GetStringValue() == "true"; + return enableProfileKey; + } + catch (Exception e) + { + int code = response == null ? 0 : response.StatusCode; + ConsoleIO.WriteLineFormatted("§cFetch authlib-injector metadata failed: HttpCode = " + code + ", Error = " + e.Message); + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return false; + } - if (response.StatusCode < 200 || response.StatusCode >= 300) - { - throw new InvalidOperationException(string.IsNullOrWhiteSpace(response.Body) - ? "Certificate endpoint returned an error response." - : response.Body); - } + public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) + { + if (string.IsNullOrWhiteSpace(accessToken)) + return null; + + if (!AuthServerSupportsProfileKeys(isYggdrasil)) + { + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLine("AuthServer does not support profile keys, will not attempt to fetch them."); + return null; + } + + string certificatesURL = "https://api.minecraftservices.com/player/certificates"; + if (isYggdrasil) + { + var authServer = Settings.Config.Main.General.AuthServer; + certificatesURL = (authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port + + authServer.AuthlibInjectorAPIPath + "/minecraftservices/player/certificates"; + } + + ProxiedWebRequest.Response? response = null; + try + { + var request = new ProxiedWebRequest(certificatesURL) + { + Accept = "application/json" + }; + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + + response = request.Post("application/json", ""); + + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLine(response.Body.ToString()); + + if (response.StatusCode < 200 || response.StatusCode >= 300) + { + throw new InvalidOperationException(string.IsNullOrWhiteSpace(response.Body) + ? "Certificate endpoint returned an error response." + : response.Body); } - // see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49 - // POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response - var json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body); + var json = Json.ParseJson(response.Body); if (json?["keyPair"]?["publicKey"] == null || json["keyPair"]?["privateKey"] == null || json["publicKeySignature"] == null @@ -59,7 +103,6 @@ namespace MinecraftClient.Protocol.ProfileKey throw new InvalidOperationException("Certificate endpoint returned an unexpected payload."); } - // Error here PublicKey publicKey = new(pemKey: json!["keyPair"]!["publicKey"]!.GetStringValue(), sig: json["publicKeySignature"]!.GetStringValue(), sigV2: json["publicKeySignatureV2"]!.GetStringValue()); @@ -75,9 +118,7 @@ namespace MinecraftClient.Protocol.ProfileKey int code = response == null ? 0 : response.StatusCode; ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message); if (Settings.Config.Logging.DebugMessages) - { ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); - } return null; } } @@ -214,31 +255,5 @@ namespace MinecraftClient.Protocol.ProfileKey // Delegate to the shared Json.EscapeString backed by System.Text.Json public static string EscapeString(string src) => Json.EscapeString(src); - - public static JsonNode MakeDummyResponse() - { - RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048); - var mimePublicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); - var mimePrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); - string publicKeyPEM = $"-----BEGIN RSA PUBLIC KEY-----\n{mimePublicKey}\n-----END RSA PUBLIC KEY-----\n"; - string privateKeyPEM = $"-----BEGIN RSA PRIVATE KEY-----\n{mimePrivateKey}\n-----END RSA PRIVATE KEY-----\n"; - DateTime now = DateTime.UtcNow; - DateTime expiresAt = now.AddHours(48); - DateTime refreshedAfter = now.AddHours(36); - string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; - - return new JsonObject - { - ["keyPair"] = new JsonObject - { - ["privateKey"] = privateKeyPEM, - ["publicKey"] = publicKeyPEM - }, - ["publicKeySignature"] = "AA==", - ["publicKeySignatureV2"] = "AA==", - ["expiresAt"] = expiresAt.ToString(format), - ["refreshedAfter"] = refreshedAfter.ToString(format) - }; - } } } diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 32edcf6b..7d395876 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Data.Odbc; using System.Globalization; using System.Linq; -using System.Net.Security; +using System.Net.Http; using System.Net.Sockets; -using System.Security.Authentication; using System.Text; using DnsClient; using MinecraftClient.Protocol.Handlers; @@ -657,7 +656,8 @@ namespace MinecraftClient.Protocol JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port, - "/api/yggdrasil/authserver/authenticate", json_request, ref result); + Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/authenticate", json_request, + Config.Main.General.AuthServer.UseHttps, ref result); if (code == 200) { if (result.Contains("availableProfiles\":[]}")) @@ -972,7 +972,8 @@ namespace MinecraftClient.Protocol "\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) + "\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }"; int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port, - "/api/yggdrasil/authserver/refresh", json_request, ref result); + Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/refresh", json_request, + Config.Main.General.AuthServer.UseHttps, ref result); if (code == 200) { if (result == null) @@ -1031,10 +1032,11 @@ namespace MinecraftClient.Protocol : "sessionserver.mojang.com"; int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443; string endpoint = type == LoginType.yggdrasil - ? "/api/yggdrasil/sessionserver/session/minecraft/join" + ? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join" : "/session/minecraft/join"; - int code = DoHTTPSPost(host, port, endpoint, json_request, ref result); + bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true; + int code = DoHTTPSPost(host, port, endpoint, json_request, useHttps, ref result); return (code >= 200 && code < 300); } catch @@ -1156,61 +1158,68 @@ namespace MinecraftClient.Protocol /// Make a HTTPS GET request to the specified endpoint of the Mojang API /// /// Host to connect to - /// Endpoint for making the request + /// Port to connect on + /// Path for making the request /// Cookies for making the request /// Request result /// HTTP Status code - private static int DoHTTPSGet(string host, int port, string endpoint, string cookies, ref string result) + private static int DoHTTPSGet(string host, int port, string path, string cookies, ref string result) { - List http_request = new() + Dictionary headers = new() { - "GET " + endpoint + " HTTP/1.1", - "Cookie: " + cookies, - "Cache-Control: no-cache", - "Pragma: no-cache", - "Host: " + host, - "User-Agent: Java/1.6.0_27", - "Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7", - "Connection: close", - "", - "" + { "Cookie", cookies }, + { "Cache-Control", "no-cache" }, + { "Pragma", "no-cache" }, + { "User-Agent", "Java/1.6.0_27" } }; - return DoHTTPSRequest(http_request, host, port, ref result); + return DoHTTPSRequest(HttpMethod.Get, host, port, path, headers, null, useHttps: true, ref result); } /// - /// Make a HTTPS POST request to the specified endpoint of the Mojang API + /// Make a POST request to the specified endpoint of the Mojang API /// /// Host to connect to - /// Endpoint for making the request - /// Request payload + /// Port to connect on + /// Path for making the request + /// Request payload /// Request result /// HTTP Status code - private static int DoHTTPSPost(string host, int port, string endpoint, string request, ref string result) + private static int DoHTTPSPost(string host, int port, string path, string body, ref string result) + => DoHTTPSPost(host, port, path, body, useHttps: true, ref result); + + /// + /// Make a POST request to the specified endpoint of the Mojang API + /// + /// Host to connect to + /// Port to connect on + /// Path for making the request + /// Request payload + /// Whether to use HTTPS (true) or plain HTTP (false) + /// Request result + /// HTTP Status code + private static int DoHTTPSPost(string host, int port, string path, string body, bool useHttps, ref string result) { - List http_request = new() + Dictionary headers = new() { - "POST " + endpoint + " HTTP/1.1", - "Host: " + host, - "User-Agent: MCC/" + Program.Version, - "Content-Type: application/json", - "Content-Length: " + Encoding.ASCII.GetBytes(request).Length, - "Connection: close", - "", - request + { "User-Agent", "MCC/" + Program.Version }, + { "Content-Type", "application/json" } }; - return DoHTTPSRequest(http_request, host, port, ref result); + return DoHTTPSRequest(HttpMethod.Post, host, port, path, headers, body, useHttps, ref result); } /// - /// Manual HTTPS request since we must directly use a TcpClient because of the proxy. - /// This method connects to the server, enables SSL, do the request and read the response. + /// This method connects to the server and performs an HTTP or HTTPS request via proxy if configured. /// - /// Request headers and optional body (POST) + /// HTTP method /// Host to connect to + /// Port to connect on + /// Request path + /// Request headers + /// Optional request body (POST) + /// Whether to use HTTPS (true) or plain HTTP (false) /// Request result /// HTTP Status code - private static int DoHTTPSRequest(List headers, string host, int port, ref string result) + private static int DoHTTPSRequest(HttpMethod method, string host, int port, string path, Dictionary headers, string? body, bool useHttps, ref string result) { string? postResult = null; int statusCode = 520; @@ -1222,40 +1231,45 @@ namespace MinecraftClient.Protocol if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host)); - TcpClient client = ProxyHandler.NewTcpClient(host, port, true); - SslStream stream = new(client.GetStream()); - stream.AuthenticateAsClient(host, null, SslProtocols.Tls12, - true); // Enable TLS 1.2. Hotfix for #1780 + using SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.ConnectCallback = async (ctx, ct) => + { + TcpClient client = ProxyHandler.NewTcpClient(host, port, true); + return client.GetStream(); + }; + + using HttpClient client = new HttpClient(handler); + + string scheme = useHttps ? "https" : "http"; + var request = new HttpRequestMessage(method, scheme + "://" + host + ":" + port + path); + + var 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 != null) + request.Content = new StringContent(body, Encoding.UTF8, contentType); if (Settings.Config.Logging.DebugMessages) - foreach (string line in headers) - ConsoleIO.WriteLineFormatted("§8> " + line); + ConsoleIO.WriteLineFormatted("§8> " + request); - stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); - System.IO.StreamReader sr = new(stream); - string raw_result = sr.ReadToEnd(); + HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult(); + statusCode = (int)response.StatusCode; + + postResult = statusCode == 204 + ? "No Content" + : response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); if (Settings.Config.Logging.DebugMessages) { ConsoleIO.WriteLine(""); - foreach (string line in raw_result.Split('\n')) + foreach (string line in postResult.Split('\n')) ConsoleIO.WriteLineFormatted("§8< " + line); } - - if (raw_result.StartsWith("HTTP/1.1")) - { - statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); - if (statusCode != 204) - { - var splited = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..].Split("\r\n"); - postResult = splited[1] + splited[3]; - } - else - { - postResult = "No Content"; - } - } - else statusCode = 520; //Web server is returning an unknown error } catch (Exception e) { diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 5908d88c..53be0a22 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -850,7 +850,19 @@ If the connection to the Minecraft game server is blocked by the firewall, set E Ignore invalid player name - Yggdrasil authlib server domain name and port. + authlib-injector authentication server to use for Yggdrasil accounts + + + Domain name or IP address + + + Port to connect on + + + Path component of the authlib-injector API location. Refer to the authlib-injector documentation for more info. + + + Set to false if your authlib-injector server uses plain HTTP (e.g. for local testing without TLS). Set to false to opt-out of Sentry error logging. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 3bf0913b..0fc34523 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -495,7 +495,7 @@ namespace MinecraftClient [TomlInlineComment("$Main.General.method$")] public LoginMethod Method = LoginMethod.mcc; [TomlInlineComment("$Main.General.AuthlibServer$")] - public AuthlibServer AuthServer = new(string.Empty); + public AuthlibServer AuthServer = new(); [TomlInlineComment("$Main.General.AuthlibUser$")] public string AuthUser = ""; @@ -709,34 +709,39 @@ namespace MinecraftClient this.Port = Port; } } - public struct AuthlibServer + [TomlDoNotInlineObject] + public class AuthlibServer { - public string Host = string.Empty; - public int Port = 443; + [NonSerialized] + private string _host = string.Empty; - public AuthlibServer() + [TomlInlineComment("$AuthlibServer.Host$")] + public string Host { - Host = string.Empty; - Port = 443; - } - - public AuthlibServer(string Host) - { - string[] sip = Host.Split(new[] { ":", ":" }, StringSplitOptions.None); - this.Host = sip[0]; - - if (sip.Length > 1) + get => _host; + set { - try { this.Port = Convert.ToUInt16(sip[1]); } - catch (FormatException) { } + string[] split = value.Split(new[] { ":", ":" }, StringSplitOptions.None); + if (split.Length >= 1) + _host = split[0]; + if (split.Length >= 2) + { + try { Port = Convert.ToUInt16(split[1]); } + catch (FormatException) { } + } } } - public AuthlibServer(string Host, ushort Port) - { - this.Host = Host.Split(new[] { ":", ":" }, StringSplitOptions.None)[0]; - this.Port = Port; - } + [TomlInlineComment("$AuthlibServer.Port$")] + public int Port = 443; + + [TomlInlineComment("$AuthlibServer.AuthlibInjectorAPIPath$")] + public string AuthlibInjectorAPIPath = "/api/yggdrasil"; + + [TomlInlineComment("$AuthlibServer.UseHttps$")] + public bool UseHttps = true; + + } } }