Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 69 additions & 41 deletions MinecraftClient/Protocol/ProfileKey/KeyUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
}
117 changes: 59 additions & 58 deletions MinecraftClient/Protocol/ProtocolHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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\":[]}"))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1104,22 +1107,16 @@ public static string GetRealmsWorldServerAddress(string worldId, string username
/// <param name="cookies">Cookies for making the request</param>
/// <param name="result">Request result</param>
/// <returns>HTTP Status code</returns>
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<String> http_request = new()
Dictionary<string, string> 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);
}

/// <summary>
Expand All @@ -1130,31 +1127,24 @@ private static int DoHTTPSGet(string host, int port, string endpoint, string coo
/// <param name="request">Request payload</param>
/// <param name="result">Request result</param>
/// <returns>HTTP Status code</returns>
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<String> http_request = new()
Dictionary<string, string> 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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="headers">Request headers and optional body (POST)</param>
/// <param name="host">Host to connect to</param>
/// <param name="result">Request result</param>
/// <returns>HTTP Status code</returns>
private static int DoHTTPSRequest(List<string> headers, string host, int port, ref string result)
private static int DoHTTPSRequest(HttpMethod method, string host, int port, string path, Dictionary<string, string> headers, string? body, ref string result)
{
string? postResult = null;
int statusCode = 520;
Expand All @@ -1165,41 +1155,52 @@ private static int DoHTTPSRequest(List<string> 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)
{
Expand Down Expand Up @@ -1256,4 +1257,4 @@ public static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
return dateTime;
}
}
}
}
13 changes: 11 additions & 2 deletions MinecraftClient/Resources/ConfigComments/ConfigComments.resx
Original file line number Diff line number Diff line change
Expand Up @@ -850,9 +850,18 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
<value>Ignore invalid player name</value>
</data>
<data name="Main.General.AuthlibServer" xml:space="preserve">
<value>Yggdrasil authlib server domain name and port.</value>
<value>authlib-injector authentication server to use for Yggdrasil accounts</value>
</data>
<data name="AuthlibServer.Host" xml:space="preserve">
<value>Domain name or IP address</value>
</data>
<data name="AuthlibServer.Port" xml:space="preserve">
<value>Port to connect on</value>
</data>
<data name="AuthlibServer.AuthlibInjectorAPIPath" xml:space="preserve">
<value>Path component of the authlib-injector API location. Refer to the authlib-injector documentation for more info.</value>
</data>
<data name="Main.Advanced.enable_sentry" xml:space="preserve">
<value>Set to false to opt-out of Sentry error logging.</value>
</data>
</root>
</root>
Loading