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";
}
}
}