diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 7da5bcfc..e5bf6572 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -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); + /// /// Start a new Client /// @@ -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 { diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs index 4c84ce47..e91806ba 100644 --- a/MinecraftClient/Protocol/MicrosoftAuthentication.cs +++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs @@ -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, diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index 381af81f..ca19e243 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -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; diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index f231976f..2ebf3ad7 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -27,6 +27,13 @@ namespace MinecraftClient.Protocol /// public static class ProtocolHandler { + public enum AuthlibServerValidationResult + { + Valid, + Unreachable, + InvalidResponse + } + /// /// Perform a DNS lookup for a Minecraft Service using the specified domain name /// @@ -134,6 +141,37 @@ namespace MinecraftClient.Protocol } } + /// + /// Verifies that an authlib-injector URL is reachable and returns its metadata document. + /// + 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; + } + } + /// /// Get a protocol handler for the specified Minecraft version /// @@ -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 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); + } + /// /// Make a POST request to the specified endpoint of the Mojang API /// @@ -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); + } + /// /// Make a POST request to the specified endpoint of the Mojang API /// @@ -1392,4 +1452,4 @@ namespace MinecraftClient.Protocol return dateTime; } } -} \ No newline at end of file +} diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 41c1eee0..5c86e6b0 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -2011,6 +2011,15 @@ namespace MinecraftClient { return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture); } } + + /// + /// 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.. + /// + internal static string Main_General_AuthServerUrl { + get { + return ResourceManager.GetString("Main.General.AuthServerUrl", resourceCulture); + } + } /// /// Looks up a localized string similar to Yggdrasil authlib multi-user selection.. diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 42e3ee0c..f109fd03 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -983,6 +983,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be authlib-injector authentication server to use for Yggdrasil accounts + + Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path. + Domain name or IP address diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 8a0d373e..9ad05c2f 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -5998,6 +5998,24 @@ namespace MinecraftClient { return ResourceManager.GetString("mcc.connecting", resourceCulture); } } + + /// + /// Looks up a localized string similar to Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil. + /// + internal static string mcc_auth_method_prompt { + get { + return ResourceManager.GetString("mcc.auth_method_prompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please choose 1, 2, or 3.. + /// + internal static string mcc_auth_method_invalid { + get { + return ResourceManager.GetString("mcc.auth_method_invalid", resourceCulture); + } + } /// /// 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); } } + + /// + /// Looks up a localized string similar to Authlib server host:. + /// + internal static string mcc_yggdrasil_host { + get { + return ResourceManager.GetString("mcc.yggdrasil_host", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authlib server port [{0}]:. + /// + internal static string mcc_yggdrasil_port { + get { + return ResourceManager.GetString("mcc.yggdrasil_port", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authlib-Injector API path [{0}]:. + /// + internal static string mcc_yggdrasil_api_path { + get { + return ResourceManager.GetString("mcc.yggdrasil_api_path", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use HTTPS? [Y/n]. + /// + internal static string mcc_yggdrasil_use_https { + get { + return ResourceManager.GetString("mcc.yggdrasil_use_https", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authlib server host cannot be empty.. + /// + internal static string mcc_yggdrasil_invalid_host { + get { + return ResourceManager.GetString("mcc.yggdrasil_invalid_host", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authlib server port must be between 1 and 65535.. + /// + internal static string mcc_yggdrasil_invalid_port { + get { + return ResourceManager.GetString("mcc.yggdrasil_invalid_port", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please answer yes or no.. + /// + internal static string mcc_yggdrasil_invalid_yes_no { + get { + return ResourceManager.GetString("mcc.yggdrasil_invalid_yes_no", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authlib-injector URL:. + /// + internal static string mcc_yggdrasil_url { + get { + return ResourceManager.GetString("mcc.yggdrasil_url", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authlib-injector URL must be an absolute HTTP or HTTPS URL.. + /// + internal static string mcc_yggdrasil_invalid_url { + get { + return ResourceManager.GetString("mcc.yggdrasil_invalid_url", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not reach the authlib-injector server. Check the URL and try again.. + /// + internal static string mcc_yggdrasil_server_unreachable { + get { + return ResourceManager.GetString("mcc.yggdrasil_server_unreachable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The server did not return valid authlib-injector metadata. Check the URL and try again.. + /// + internal static string mcc_yggdrasil_server_invalid { + get { + return ResourceManager.GetString("mcc.yggdrasil_server_invalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The configured authlib-injector URL must be an absolute HTTP or HTTPS URL.. + /// + internal static string config_auth_server_url_invalid { + get { + return ResourceManager.GetString("config.auth_server_url_invalid", resourceCulture); + } + } /// /// Looks up a localized string similar to You are dead. Type '{0}respawn' to respawn.. diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 0fc9f081..3586a8be 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2004,6 +2004,27 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Connecting to {0}... + + Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil + + + Please choose 1, 2, or 3. + + + Authlib-injector URL: + + + The authlib-injector URL must be an absolute HTTP or HTTPS URL. + + + Could not reach the authlib-injector server. Check the URL and try again. + + + The server did not return valid authlib-injector metadata. Check the URL and try again. + + + The configured authlib-injector URL must be an absolute HTTP or HTTPS URL. + 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. @@ -2097,6 +2118,27 @@ Type '{0}quit' to leave the server. Password(invisible): + + Authlib server host: + + + Authlib server port [{0}]: + + + Authlib-Injector API path [{0}]: + + + Use HTTPS? [Y/n] + + + The authlib server host cannot be empty. + + + The authlib server port must be between 1 and 65535. + + + Please answer yes or no. + You are dead. Type '{0}respawn' to respawn. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index a5ce88af..c594a336 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -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); + } + /// /// Load settings from the command line /// @@ -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 }; diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 008f22d3..4c9dc52d 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -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`