mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: Added first-time guided login + simplifed Yggdrasil configuration
feat: add guided login and authlib URL setup
This commit is contained in:
commit
6e4bca5073
10 changed files with 528 additions and 66 deletions
|
|
@ -58,6 +58,7 @@ namespace MinecraftClient
|
|||
private static int offlinePromptActive;
|
||||
private static int exitOnFailurePending;
|
||||
private static string settingsIniPath = "MinecraftClient.ini";
|
||||
private static AuthenticationSelection? pendingAuthenticationSelection;
|
||||
|
||||
// [SENTRY]
|
||||
// Setting this string to an empty string will disable Sentry
|
||||
|
|
@ -565,15 +566,19 @@ namespace MinecraftClient
|
|||
// Setup exit cleaning code
|
||||
ExitCleanUp.Add(() => { DoExit(); });
|
||||
|
||||
if (HasNoConfiguredLoginDetails())
|
||||
{
|
||||
if (!PromptForAuthenticationSelection())
|
||||
return;
|
||||
}
|
||||
|
||||
//Asking the user to type in missing data such as Username and Password
|
||||
bool useBrowser = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser;
|
||||
bool useDeviceCode = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.mcc;
|
||||
bool skipPassword = useBrowser || useDeviceCode;
|
||||
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser)
|
||||
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !skipPassword)
|
||||
{
|
||||
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
|
||||
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
|
||||
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
|
||||
if (!RequestLogin())
|
||||
{
|
||||
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
|
||||
return;
|
||||
|
|
@ -603,6 +608,145 @@ namespace MinecraftClient
|
|||
InternalConfig.Account.Password = password;
|
||||
}
|
||||
|
||||
private static bool HasNoConfiguredLoginDetails()
|
||||
=> string.IsNullOrWhiteSpace(InternalConfig.Account.Login)
|
||||
&& string.IsNullOrWhiteSpace(InternalConfig.Account.Password);
|
||||
|
||||
private static bool PromptForAuthenticationSelection()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_auth_method_prompt);
|
||||
string selection = ConsoleIO.ReadLine().Trim();
|
||||
|
||||
switch (selection.ToLowerInvariant())
|
||||
{
|
||||
case "1":
|
||||
case "offline":
|
||||
BeginAuthenticationSelection(LoginType.mojang, LoginMethod.mcc);
|
||||
if (!RequestLogin())
|
||||
{
|
||||
DiscardAuthenticationSelection();
|
||||
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
|
||||
return false;
|
||||
}
|
||||
|
||||
InternalConfig.Account.Password = "-";
|
||||
return true;
|
||||
|
||||
case "2":
|
||||
case "online":
|
||||
case "microsoft":
|
||||
BeginAuthenticationSelection(LoginType.microsoft, LoginMethod.mcc);
|
||||
return true;
|
||||
|
||||
case "3":
|
||||
case "yggdrasil":
|
||||
BeginAuthenticationSelection(LoginType.yggdrasil, LoginMethod.mcc);
|
||||
if (!RequestLogin() || !RequestRequiredPassword())
|
||||
{
|
||||
DiscardAuthenticationSelection();
|
||||
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RequestAuthlibServer())
|
||||
{
|
||||
DiscardAuthenticationSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
ConsoleIO.WriteLine(Translations.mcc_auth_method_invalid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeginAuthenticationSelection(LoginType accountType, LoginMethod method)
|
||||
{
|
||||
pendingAuthenticationSelection ??= new AuthenticationSelection(
|
||||
Config.Main.General.AccountType,
|
||||
Config.Main.General.Method,
|
||||
Config.Main.General.AuthServerUrl);
|
||||
|
||||
Config.Main.General.AccountType = accountType;
|
||||
Config.Main.General.Method = method;
|
||||
}
|
||||
|
||||
private static bool RequestLogin()
|
||||
{
|
||||
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
|
||||
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
|
||||
return !string.IsNullOrWhiteSpace(InternalConfig.Account.Login);
|
||||
}
|
||||
|
||||
private static bool RequestRequiredPassword()
|
||||
{
|
||||
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden);
|
||||
string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
return false;
|
||||
|
||||
InternalConfig.Account.Password = password;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool RequestAuthlibServer()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_url);
|
||||
string authServerUrl = ConsoleIO.ReadLine().Trim();
|
||||
if (!Config.Main.General.TrySetAuthServerUrl(authServerUrl)
|
||||
|| !Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_invalid_url);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (ProtocolHandler.ValidateAuthlibServer(authServerUri))
|
||||
{
|
||||
case ProtocolHandler.AuthlibServerValidationResult.Valid:
|
||||
return true;
|
||||
case ProtocolHandler.AuthlibServerValidationResult.Unreachable:
|
||||
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_unreachable);
|
||||
break;
|
||||
default:
|
||||
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_invalid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PersistAuthenticationSelection(SessionToken session)
|
||||
{
|
||||
if (pendingAuthenticationSelection is null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
|
||||
InternalConfig.Account.Login = session.PlayerName;
|
||||
|
||||
Config.Main.General.Account = InternalConfig.Account;
|
||||
WriteBackSettings();
|
||||
pendingAuthenticationSelection = null;
|
||||
}
|
||||
|
||||
private static void DiscardAuthenticationSelection()
|
||||
{
|
||||
if (pendingAuthenticationSelection is not AuthenticationSelection selection)
|
||||
return;
|
||||
|
||||
Config.Main.General.AccountType = selection.AccountType;
|
||||
Config.Main.General.Method = selection.Method;
|
||||
Config.Main.General.AuthServerUrl = selection.AuthServerUrl;
|
||||
pendingAuthenticationSelection = null;
|
||||
}
|
||||
|
||||
private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Start a new Client
|
||||
/// </summary>
|
||||
|
|
@ -664,17 +808,26 @@ namespace MinecraftClient
|
|||
|
||||
if (result != ProtocolHandler.LoginResult.Success)
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServer.Host)));
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServerUrl)));
|
||||
result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session);
|
||||
}
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none)
|
||||
SessionCache.Store(loginLower, session);
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
{
|
||||
PersistAuthenticationSelection(session);
|
||||
loginLower = ToLowerIfNeed(InternalConfig.Account.Login);
|
||||
|
||||
if (Config.Main.Advanced.SessionCache != CacheType.none)
|
||||
SessionCache.Store(loginLower, session);
|
||||
}
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
|
||||
}
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
PersistAuthenticationSelection(session);
|
||||
|
||||
if (result == ProtocolHandler.LoginResult.Success)
|
||||
{
|
||||
InternalConfig.Username = session.PlayerName;
|
||||
|
|
@ -859,6 +1012,7 @@ namespace MinecraftClient
|
|||
}
|
||||
else
|
||||
{
|
||||
DiscardAuthenticationSelection();
|
||||
string failureMessage = Translations.error_login;
|
||||
string failureReason = result switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ namespace MinecraftClient.Protocol
|
|||
// Extract email from JWT id_token
|
||||
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
|
||||
var jsonPayload = Json.ParseJson(payload);
|
||||
string email = jsonPayload!["email"]!.GetStringValue();
|
||||
string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
|
||||
|
||||
return new LoginResponse()
|
||||
{
|
||||
|
|
@ -195,7 +195,7 @@ namespace MinecraftClient.Protocol
|
|||
// Extract email from JWT
|
||||
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
|
||||
var jsonPayload = Json.ParseJson(payload);
|
||||
string email = jsonPayload!["email"]!.GetStringValue();
|
||||
string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
|
||||
return new LoginResponse()
|
||||
{
|
||||
Email = email,
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
ProxiedWebRequest.Response? response = null;
|
||||
try
|
||||
{
|
||||
var authServer = Settings.Config.Main.General.AuthServer;
|
||||
var request = new ProxiedWebRequest(
|
||||
(authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath)
|
||||
if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
return false;
|
||||
|
||||
var request = new ProxiedWebRequest(authServerUri.AbsoluteUri)
|
||||
{
|
||||
Accept = "application/json"
|
||||
};
|
||||
|
|
@ -66,9 +67,10 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
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";
|
||||
if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
return null;
|
||||
|
||||
certificatesURL = new Uri(authServerUri, "minecraftservices/player/certificates").AbsoluteUri;
|
||||
}
|
||||
|
||||
ProxiedWebRequest.Response? response = null;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ namespace MinecraftClient.Protocol
|
|||
/// </remarks>
|
||||
public static class ProtocolHandler
|
||||
{
|
||||
public enum AuthlibServerValidationResult
|
||||
{
|
||||
Valid,
|
||||
Unreachable,
|
||||
InvalidResponse
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a DNS lookup for a Minecraft Service using the specified domain name
|
||||
/// </summary>
|
||||
|
|
@ -134,6 +141,37 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an authlib-injector URL is reachable and returns its metadata document.
|
||||
/// </summary>
|
||||
public static AuthlibServerValidationResult ValidateAuthlibServer(Uri authServerUri)
|
||||
{
|
||||
string result = string.Empty;
|
||||
int statusCode;
|
||||
try
|
||||
{
|
||||
statusCode = DoHTTPSGet(authServerUri, ref result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return AuthlibServerValidationResult.Unreachable;
|
||||
}
|
||||
|
||||
if (statusCode != 200)
|
||||
return AuthlibServerValidationResult.InvalidResponse;
|
||||
|
||||
try
|
||||
{
|
||||
return Json.ParseJson(result)?["meta"]?["implementationName"]?.GetStringValue() is { Length: > 0 }
|
||||
? AuthlibServerValidationResult.Valid
|
||||
: AuthlibServerValidationResult.InvalidResponse;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return AuthlibServerValidationResult.InvalidResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a protocol handler for the specified Minecraft version
|
||||
/// </summary>
|
||||
|
|
@ -714,9 +752,10 @@ namespace MinecraftClient.Protocol
|
|||
string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" +
|
||||
JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) +
|
||||
"\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }";
|
||||
int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
|
||||
Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/authenticate", json_request,
|
||||
Config.Main.General.AuthServer.UseHttps, ref result);
|
||||
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
return LoginResult.OtherError;
|
||||
|
||||
int code = DoHTTPSPost(authServerUri, "authserver/authenticate", json_request, ref result);
|
||||
if (code == 200)
|
||||
{
|
||||
if (result.Contains("availableProfiles\":[]}"))
|
||||
|
|
@ -922,7 +961,8 @@ namespace MinecraftClient.Protocol
|
|||
session.PlayerID = profile.UUID;
|
||||
session.ID = accessToken;
|
||||
session.RefreshToken = msaResponse.RefreshToken;
|
||||
InternalConfig.Account.Login = msaResponse.Email;
|
||||
if (!string.IsNullOrWhiteSpace(msaResponse.Email))
|
||||
InternalConfig.Account.Login = msaResponse.Email;
|
||||
return LoginResult.Success;
|
||||
}
|
||||
else
|
||||
|
|
@ -1037,9 +1077,10 @@ namespace MinecraftClient.Protocol
|
|||
"\", \"clientToken\": \"" + JsonEncode(currentsession.ClientID) +
|
||||
"\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) +
|
||||
"\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }";
|
||||
int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
|
||||
Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/refresh", json_request,
|
||||
Config.Main.General.AuthServer.UseHttps, ref result);
|
||||
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
return LoginResult.OtherError;
|
||||
|
||||
int code = DoHTTPSPost(authServerUri, "authserver/refresh", json_request, ref result);
|
||||
if (code == 200)
|
||||
{
|
||||
if (result is null)
|
||||
|
|
@ -1093,16 +1134,18 @@ namespace MinecraftClient.Protocol
|
|||
string result = "";
|
||||
string json_request = "{\"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";
|
||||
int code;
|
||||
if (type == LoginType.yggdrasil)
|
||||
{
|
||||
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
|
||||
return false;
|
||||
|
||||
bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
|
||||
int code = DoHTTPSPost(host, port, endpoint, json_request, useHttps, ref result);
|
||||
code = DoHTTPSPost(authServerUri, "sessionserver/session/minecraft/join", json_request, ref result);
|
||||
}
|
||||
else
|
||||
{
|
||||
code = DoHTTPSPost("sessionserver.mojang.com", 443, "/session/minecraft/join", json_request, ref result);
|
||||
}
|
||||
return (code >= 200 && code < 300);
|
||||
}
|
||||
catch
|
||||
|
|
@ -1241,6 +1284,16 @@ namespace MinecraftClient.Protocol
|
|||
return DoHTTPSRequest(HttpMethod.Get, host, port, path, headers, null, useHttps: true, ref result);
|
||||
}
|
||||
|
||||
private static int DoHTTPSGet(Uri requestUri, ref string result)
|
||||
{
|
||||
Dictionary<string, string> headers = new()
|
||||
{
|
||||
{ "User-Agent", "MCC/" + Program.Version }
|
||||
};
|
||||
return DoHTTPSRequest(HttpMethod.Get, requestUri.Host, requestUri.Port, requestUri.PathAndQuery, headers,
|
||||
null, requestUri.Scheme == Uri.UriSchemeHttps, ref result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make a POST request to the specified endpoint of the Mojang API
|
||||
/// </summary>
|
||||
|
|
@ -1253,6 +1306,13 @@ namespace MinecraftClient.Protocol
|
|||
private static int DoHTTPSPost(string host, int port, string path, string body, ref string result)
|
||||
=> DoHTTPSPost(host, port, path, body, useHttps: true, ref result);
|
||||
|
||||
private static int DoHTTPSPost(Uri baseUri, string relativePath, string body, ref string result)
|
||||
{
|
||||
Uri requestUri = new(baseUri, relativePath);
|
||||
return DoHTTPSPost(requestUri.Host, requestUri.Port, requestUri.PathAndQuery, body,
|
||||
requestUri.Scheme == Uri.UriSchemeHttps, ref result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make a POST request to the specified endpoint of the Mojang API
|
||||
/// </summary>
|
||||
|
|
@ -1392,4 +1452,4 @@ namespace MinecraftClient.Protocol
|
|||
return dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,15 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path..
|
||||
/// </summary>
|
||||
internal static string Main_General_AuthServerUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("Main.General.AuthServerUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Yggdrasil authlib multi-user selection..
|
||||
|
|
|
|||
|
|
@ -983,6 +983,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
|
|||
<data name="Main.General.AuthlibServer" xml:space="preserve">
|
||||
<value>authlib-injector authentication server to use for Yggdrasil accounts</value>
|
||||
</data>
|
||||
<data name="Main.General.AuthServerUrl" xml:space="preserve">
|
||||
<value>Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path.</value>
|
||||
</data>
|
||||
<data name="AuthlibServer.Host" xml:space="preserve">
|
||||
<value>Domain name or IP address</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -5998,6 +5998,24 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("mcc.connecting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil.
|
||||
/// </summary>
|
||||
internal static string mcc_auth_method_prompt {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.auth_method_prompt", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please choose 1, 2, or 3..
|
||||
/// </summary>
|
||||
internal static string mcc_auth_method_invalid {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.auth_method_invalid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart..
|
||||
|
|
@ -6271,6 +6289,114 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("mcc.password_hidden", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authlib server host:.
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_host {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_host", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authlib server port [{0}]:.
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_port {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_port", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authlib-Injector API path [{0}]:.
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_api_path {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_api_path", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use HTTPS? [Y/n].
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_use_https {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_use_https", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The authlib server host cannot be empty..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_invalid_host {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_invalid_host", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The authlib server port must be between 1 and 65535..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_invalid_port {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_invalid_port", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please answer yes or no..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_invalid_yes_no {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_invalid_yes_no", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Authlib-injector URL:.
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_url {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_url", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The authlib-injector URL must be an absolute HTTP or HTTPS URL..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_invalid_url {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_invalid_url", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Could not reach the authlib-injector server. Check the URL and try again..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_server_unreachable {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_server_unreachable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The server did not return valid authlib-injector metadata. Check the URL and try again..
|
||||
/// </summary>
|
||||
internal static string mcc_yggdrasil_server_invalid {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.yggdrasil_server_invalid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The configured authlib-injector URL must be an absolute HTTP or HTTPS URL..
|
||||
/// </summary>
|
||||
internal static string config_auth_server_url_invalid {
|
||||
get {
|
||||
return ResourceManager.GetString("config.auth_server_url_invalid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are dead. Type '{0}respawn' to respawn..
|
||||
|
|
|
|||
|
|
@ -2004,6 +2004,27 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="mcc.connecting" xml:space="preserve">
|
||||
<value>Connecting to {0}...</value>
|
||||
</data>
|
||||
<data name="mcc.auth_method_prompt" xml:space="preserve">
|
||||
<value>Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil</value>
|
||||
</data>
|
||||
<data name="mcc.auth_method_invalid" xml:space="preserve">
|
||||
<value>Please choose 1, 2, or 3.</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_url" xml:space="preserve">
|
||||
<value>Authlib-injector URL:</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_invalid_url" xml:space="preserve">
|
||||
<value>The authlib-injector URL must be an absolute HTTP or HTTPS URL.</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_server_unreachable" xml:space="preserve">
|
||||
<value>Could not reach the authlib-injector server. Check the URL and try again.</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_server_invalid" xml:space="preserve">
|
||||
<value>The server did not return valid authlib-injector metadata. Check the URL and try again.</value>
|
||||
</data>
|
||||
<data name="config.auth_server_url_invalid" xml:space="preserve">
|
||||
<value>The configured authlib-injector URL must be an absolute HTTP or HTTPS URL.</value>
|
||||
</data>
|
||||
<data name="mcc.console_mode_tui_recommendation" xml:space="preserve">
|
||||
<value>Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.</value>
|
||||
</data>
|
||||
|
|
@ -2097,6 +2118,27 @@ Type '{0}quit' to leave the server.</value>
|
|||
<data name="mcc.password_hidden" xml:space="preserve">
|
||||
<value>Password(invisible): </value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_host" xml:space="preserve">
|
||||
<value>Authlib server host:</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_port" xml:space="preserve">
|
||||
<value>Authlib server port [{0}]:</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_api_path" xml:space="preserve">
|
||||
<value>Authlib-Injector API path [{0}]:</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_use_https" xml:space="preserve">
|
||||
<value>Use HTTPS? [Y/n]</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_invalid_host" xml:space="preserve">
|
||||
<value>The authlib server host cannot be empty.</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_invalid_port" xml:space="preserve">
|
||||
<value>The authlib server port must be between 1 and 65535.</value>
|
||||
</data>
|
||||
<data name="mcc.yggdrasil_invalid_yes_no" xml:space="preserve">
|
||||
<value>Please answer yes or no.</value>
|
||||
</data>
|
||||
<data name="mcc.player_dead" xml:space="preserve">
|
||||
<value>You are dead. Type '{0}respawn' to respawn.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ namespace MinecraftClient
|
|||
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
string tomlString = TomletMain.TomlStringFrom(Config);
|
||||
Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
|
||||
tomlString = RemoveLegacyAuthServerSection(tomlString);
|
||||
|
||||
string[] tomlList = tomlString.Split('\n');
|
||||
StringBuilder newConfig = new();
|
||||
|
|
@ -272,6 +273,19 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
private static string RemoveLegacyAuthServerSection(string tomlString)
|
||||
{
|
||||
const string legacySection = "[Main.General.AuthServer]";
|
||||
int sectionStart = tomlString.IndexOf(legacySection, StringComparison.Ordinal);
|
||||
if (sectionStart < 0)
|
||||
return tomlString;
|
||||
|
||||
int nextSection = tomlString.IndexOf("\n[", sectionStart + legacySection.Length, StringComparison.Ordinal);
|
||||
return nextSection < 0
|
||||
? tomlString[..sectionStart]
|
||||
: tomlString.Remove(sectionStart, nextSection - sectionStart + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load settings from the command line
|
||||
/// </summary>
|
||||
|
|
@ -656,6 +670,8 @@ namespace MinecraftClient
|
|||
|
||||
General.Account.Login ??= string.Empty;
|
||||
General.Account.Password ??= string.Empty;
|
||||
if (!General.MigrateLegacyAuthServer())
|
||||
ConsoleIO.WriteLogLine(Translations.config_auth_server_url_invalid);
|
||||
if (!InternalConfig.KeepAccountSettings)
|
||||
{
|
||||
if (Advanced.AccountList.TryGetValue(General.Account.Login, out AccountInfoConfig account))
|
||||
|
|
@ -753,12 +769,72 @@ namespace MinecraftClient
|
|||
|
||||
[TomlInlineComment("$Main.General.method$")]
|
||||
public LoginMethod Method = LoginMethod.mcc;
|
||||
|
||||
[TomlInlineComment("$Main.General.AuthServerUrl$")]
|
||||
public string AuthServerUrl = string.Empty;
|
||||
|
||||
// Retained only to deserialize pre-URL configs. WriteToFile() removes this legacy section.
|
||||
[TomlInlineComment("$Main.General.AuthlibServer$")]
|
||||
public AuthlibServer AuthServer = new();
|
||||
|
||||
[TomlInlineComment("$Main.General.AuthlibUser$")]
|
||||
public string AuthUser = "";
|
||||
|
||||
public bool MigrateLegacyAuthServer()
|
||||
{
|
||||
AuthServerUrl ??= string.Empty;
|
||||
if (TrySetAuthServerUrl(AuthServerUrl))
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AuthServerUrl))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AuthServer.Host))
|
||||
{
|
||||
string path = AuthServer.AuthlibInjectorAPIPath ?? string.Empty;
|
||||
if (!path.StartsWith('/'))
|
||||
path = '/' + path;
|
||||
|
||||
string legacyUrl = $"{(AuthServer.UseHttps ? "https" : "http")}://{AuthServer.Host}:{AuthServer.Port}{path}";
|
||||
return TrySetAuthServerUrl(legacyUrl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TrySetAuthServerUrl(string url)
|
||||
{
|
||||
if (!TryGetNormalizedAuthServerUri(url, out Uri? authServerUri))
|
||||
return false;
|
||||
|
||||
AuthServerUrl = authServerUri.AbsoluteUri;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetAuthServerUri([NotNullWhen(true)] out Uri? authServerUri)
|
||||
=> TryGetNormalizedAuthServerUri(AuthServerUrl, out authServerUri);
|
||||
|
||||
private static bool TryGetNormalizedAuthServerUri(string? url, [NotNullWhen(true)] out Uri? authServerUri)
|
||||
{
|
||||
authServerUri = null;
|
||||
if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out Uri? parsedUri)
|
||||
|| (!parsedUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
|
||||
&& !parsedUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
|| string.IsNullOrWhiteSpace(parsedUri.Host)
|
||||
|| !string.IsNullOrEmpty(parsedUri.UserInfo)
|
||||
|| !string.IsNullOrEmpty(parsedUri.Query)
|
||||
|| !string.IsNullOrEmpty(parsedUri.Fragment))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var builder = new UriBuilder(parsedUri)
|
||||
{
|
||||
Path = parsedUri.AbsolutePath.TrimEnd('/') + "/"
|
||||
};
|
||||
authServerUri = builder.Uri;
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum LoginType { mojang, microsoft, yggdrasil };
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,16 @@ Coordinate = { x = 145, y = 64, z = 2045 }
|
|||
AccountType = "microsoft"
|
||||
```
|
||||
|
||||
#### Interactive login
|
||||
|
||||
If both `Account.Login` and `Account.Password` are empty when MCC starts, it asks which account type to use instead of assuming Microsoft login.
|
||||
|
||||
- Choose Offline to enter an in-game username. MCC saves the account as an offline account.
|
||||
- Choose Online (Microsoft) to continue with the device-code sign-in. MCC shows the code and link, then opens the sign-in page in your browser.
|
||||
- Choose Yggdrasil to enter the username, password, and authlib-injector URL for the account server. MCC checks the URL before it continues. If the address is malformed, unreachable, or does not return authlib-injector metadata, MCC explains the problem and asks for another URL.
|
||||
|
||||
After a successful login, MCC saves the account details and selected account type in the configuration file. Later starts use those saved details and do not show this prompt. To choose a different method, edit or clear the account settings in the configuration file.
|
||||
|
||||
#### `Method`
|
||||
|
||||
- **Description:**
|
||||
|
|
@ -154,52 +164,32 @@ Coordinate = { x = 145, y = 64, z = 2045 }
|
|||
Method = "mcc"
|
||||
```
|
||||
|
||||
#### `AuthServer`
|
||||
#### `AuthServerUrl`
|
||||
|
||||
- **Description:**
|
||||
|
||||
This subsection is used when `AccountType` is set to `yggdrasil`. It points MCC at the authlib/Yggdrasil server used for login, session checks, and profile key requests.
|
||||
Use this setting with `AccountType = "yggdrasil"`. It is the complete base URL of the authlib-injector or Yggdrasil service MCC uses for login, session checks, and profile key requests.
|
||||
|
||||
MCC now writes this as a dedicated TOML subsection instead of an inline table:
|
||||
The URL must start with `http://` or `https://` and include any path used by the service. For example, an authlib-injector server may use `/authlib-injector/` as its base path.
|
||||
|
||||
```toml
|
||||
[Main.General.AuthServer]
|
||||
```
|
||||
An existing multi-field Yggdrasil configuration is converted to this URL automatically. When MCC writes the configuration after migration, it removes the previous Yggdrasil section.
|
||||
|
||||
`Host` accepts either a plain host name or a `host:port` pair. If you include the port there, MCC updates `Port` to match.
|
||||
- **Type:** `string`
|
||||
|
||||
`AuthlibInjectorAPIPath` defaults to `/api/yggdrasil`. Change it if your authlib-injector server uses a different prefix, such as `/authlib-injector`.
|
||||
|
||||
`UseHttps` defaults to `true`. Set it to `false` if your local or development auth server only exposes plain HTTP.
|
||||
|
||||
- **Type:** `section`
|
||||
|
||||
- **Default:**
|
||||
|
||||
```toml
|
||||
[Main.General.AuthServer]
|
||||
Port = 443
|
||||
AuthlibInjectorAPIPath = "/api/yggdrasil"
|
||||
UseHttps = true
|
||||
Host = ""
|
||||
```
|
||||
- **Default:** `""`
|
||||
|
||||
- **Example:**
|
||||
|
||||
```
|
||||
[Main.General.AuthServer]
|
||||
Host = "auth.example.com"
|
||||
Port = 443
|
||||
AuthlibInjectorAPIPath = "/api/yggdrasil"
|
||||
UseHttps = true
|
||||
```toml
|
||||
Account = { Login = "player@example.com", Password = "password" }
|
||||
AccountType = "yggdrasil"
|
||||
AuthServerUrl = "https://auth.example.com/api/yggdrasil/"
|
||||
```
|
||||
|
||||
```
|
||||
[Main.General.AuthServer]
|
||||
Host = "127.0.0.1"
|
||||
Port = 25585
|
||||
AuthlibInjectorAPIPath = "/authlib-injector"
|
||||
UseHttps = false
|
||||
```toml
|
||||
Account = { Login = "player", Password = "password" }
|
||||
AccountType = "yggdrasil"
|
||||
AuthServerUrl = "http://127.0.0.1:25585/authlib-injector/"
|
||||
```
|
||||
|
||||
#### `AuthUser`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue