diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index 708e99d969..ef214f83fe 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -12,33 +12,86 @@ static class KeyUtils { private static readonly SHA256 sha256Hash = SHA256.Create(); - private static readonly string certificates = "https://api.minecraftservices.com/player/certificates"; + public static bool AuthServerSupportsProfileKeys(bool isYggdrasil) + { + // Check whether the authentication server supports player profile keys + if (!isYggdrasil) + return true; + + ProxiedWebRequest.Response? response = null; + try + { + var authServer = Settings.Config.Main.General.AuthServer; + var request = new ProxiedWebRequest( + "https://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath) + { + Accept = "application/json" + }; + + response = request.Get(); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(response.Body.ToString()); + } + + // The feature.enable_profile_key flag is 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 + Json.JSONData json = Json.ParseJson(response.Body); + bool enableProfileKey = json.Properties["meta"].Properties.ContainsKey("feature.enable_profile_key") && + json.Properties["meta"].Properties["feature.enable_profile_key"].StringValue == "true"; + if (enableProfileKey) + { + return true; + } + } + 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; + } public static PlayerKeyPair? GetNewProfileKeys(string accessToken, bool isYggdrasil) { + 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 = "https://" + authServer.Host + ":" + authServer.Port + + authServer.AuthlibInjectorAPIPath + "/minecraftservices/player/certificates"; + } + ProxiedWebRequest.Response? response = null; try { - if (!isYggdrasil) + var request = new ProxiedWebRequest(certificatesURL) { - var request = new ProxiedWebRequest(certificates) - { - Accept = "application/json" - }; - request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + Accept = "application/json" + }; + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - response = request.Post("application/json", ""); + response = request.Post("application/json", ""); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(response.Body.ToString()); - } + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(response.Body.ToString()); } - // 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 - Json.JSONData json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body); - // Error here + Json.JSONData json = Json.ParseJson(response!.Body); PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue, sig: json.Properties["publicKeySignature"].StringValue, sigV2: json.Properties["publicKeySignatureV2"].StringValue); @@ -234,30 +287,5 @@ public static string EscapeString(string src) sb.Append(src, start, src.Length - start); return sb.ToString(); } - - public static Json.JSONData 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); - Json.JSONData response = new(Json.JSONData.DataType.Object); - Json.JSONData keyPairObj = new(Json.JSONData.DataType.Object); - keyPairObj.Properties["privateKey"] = new(Json.JSONData.DataType.String){ StringValue = privateKeyPEM }; - keyPairObj.Properties["publicKey"] = new(Json.JSONData.DataType.String){ StringValue = publicKeyPEM }; - - response.Properties["keyPair"] = keyPairObj; - response.Properties["publicKeySignature"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" }; - response.Properties["publicKeySignatureV2"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" }; - string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; - response.Properties["expiresAt"] = new(Json.JSONData.DataType.String){ StringValue = expiresAt.ToString(format) }; - response.Properties["refreshedAfter"] = new(Json.JSONData.DataType.String){ StringValue = refreshedAfter.ToString(format) }; - - return response; - } } } diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index cbf1b1cd90..a49c09e6f7 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -3,6 +3,7 @@ using System.Data.Odbc; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; @@ -603,7 +604,8 @@ private static LoginResult YggdrasiLogin(string user, string pass, out SessionTo 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, ref result); if (code == 200) { if (result.Contains("availableProfiles\":[]}")) @@ -914,7 +916,8 @@ public static LoginResult GetNewYggdrasilToken(SessionToken currentsession, out "\", \"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, ref result); if (code == 200) { if (result == null) @@ -973,11 +976,11 @@ public static bool SessionCheck(string uuid, string accesstoken, string serverha ? Config.Main.General.AuthServer.Host : "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" + string path = type == LoginType.yggdrasil + ? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join" : "/session/minecraft/join"; - int code = DoHTTPSPost(host, port, endpoint, json_request, ref result); + int code = DoHTTPSPost(host, port, path, json_request, ref result); return (code >= 200 && code < 300); } catch @@ -1104,22 +1107,16 @@ public static string GetRealmsWorldServerAddress(string worldId, string username /// 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, ref result); } /// @@ -1130,31 +1127,24 @@ private static int DoHTTPSGet(string host, int port, string endpoint, string coo /// 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) { - 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, 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, enables TLS, does the request, and reads the response. /// /// Request headers and optional body (POST) /// Host to connect to /// 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, ref string result) { string? postResult = null; int statusCode = 520; @@ -1165,41 +1155,52 @@ private static int DoHTTPSRequest(List headers, string host, int port, r { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host)); + + using SocketsHttpHandler handler = new SocketsHttpHandler(); + handler.ConnectCallback = async (ctx, ct) => + { + TcpClient client = ProxyHandler.NewTcpClient(host, port, true); + return client.GetStream(); + }; - 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 HttpClient client = new HttpClient(handler); - if (Settings.Config.Logging.DebugMessages) - foreach (string line in headers) - ConsoleIO.WriteLineFormatted("§8> " + line); + var request = new HttpRequestMessage(method, "https://" + host + ":" + port + path); - stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); - System.IO.StreamReader sr = new(stream); - string raw_result = sr.ReadToEnd(); + 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) + ConsoleIO.WriteLineFormatted("§8> " + request); + + HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult(); + statusCode = (int)(response.StatusCode); + if (statusCode == 204) { - ConsoleIO.WriteLine(""); - foreach (string line in raw_result.Split('\n')) - ConsoleIO.WriteLineFormatted("§8< " + line); + postResult = "No Content"; + } + else + { + postResult = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + } - if (raw_result.StartsWith("HTTP/1.1")) + if (Settings.Config.Logging.DebugMessages) { - 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"; - } + ConsoleIO.WriteLine(""); + foreach (string line in postResult.Split('\n')) + ConsoleIO.WriteLineFormatted("§8< " + line); } - else statusCode = 520; //Web server is returning an unknown error } catch (Exception e) { @@ -1256,4 +1257,4 @@ public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) return dateTime; } } -} \ No newline at end of file +} diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index ca44031449..531abc0c18 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -850,9 +850,18 @@ 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 to opt-out of Sentry error logging. - \ No newline at end of file + diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 684e5a98de..3e9f0b66ba 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -494,9 +494,9 @@ public class GeneralConfig [TomlInlineComment("$Main.General.method$")] public LoginMethod Method = LoginMethod.mcc; + [TomlInlineComment("$Main.General.AuthlibServer$")] - public AuthlibServer AuthServer = new(string.Empty); - + public AuthlibServer AuthServer = new(); public enum LoginType { mojang, microsoft,yggdrasil }; @@ -694,28 +694,37 @@ public ServerInfoConfig(string Host, ushort Port) 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(string Host) + [TomlInlineComment("$AuthlibServer.Host$")] + public 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"; } } }