From c04c597abab1f6b87ee6250642aaee06aeb9622e Mon Sep 17 00:00:00 2001 From: ReinforceZwei <39955851+ReinforceZwei@users.noreply.github.com> Date: Thu, 7 Jan 2021 04:14:51 +0800 Subject: [PATCH] Implement Microsoft account login (#1397) * Implement Microsoft account login * Create proxied web request class * Whole bunch of code that doesn't work * I finally FIXED IT It took me 2 hours to resolve the problem * Fill the missed method summary * Remove some unused code * Revert http version * Remove JSON parsing bug workaround Not needed anymore as per e06438b582def5b52fbffda7691da95f9f3b2c3e * Remove comment asking about clientID Client ID is used for session token refreshes. Random UUID without hyphens Co-authored-by: ORelio --- MinecraftClient/MinecraftClient.csproj | 2 + MinecraftClient/Program.cs | 4 +- .../Protocol/MicrosoftAuthentication.cs | 345 ++++++++++++++++++ MinecraftClient/Protocol/ProtocolHandler.cs | 54 ++- MinecraftClient/Protocol/ProxiedWebRequest.cs | 282 ++++++++++++++ .../Resources/config/MinecraftClient.ini | 1 + MinecraftClient/Resources/lang/en.ini | 2 +- MinecraftClient/Settings.cs | 4 + 8 files changed, 690 insertions(+), 4 deletions(-) create mode 100644 MinecraftClient/Protocol/MicrosoftAuthentication.cs create mode 100644 MinecraftClient/Protocol/ProxiedWebRequest.cs diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 7930538d..b2095270 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -214,6 +214,8 @@ + + diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 7363929d..70a3024b 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -201,8 +201,8 @@ namespace MinecraftClient if (result != ProtocolHandler.LoginResult.Success) { - Translations.WriteLine("mcc.connecting"); - result = ProtocolHandler.GetLogin(Settings.Login, Settings.Password, out session); + Translations.WriteLine("mcc.connecting", Settings.AccountType == ProtocolHandler.AccountType.Mojang ? "Minecraft.net" : "Microsoft"); + result = ProtocolHandler.GetLogin(Settings.Login, Settings.Password, Settings.AccountType, out session); if (result == ProtocolHandler.LoginResult.Success && Settings.SessionCaching != CacheType.None) { diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs new file mode 100644 index 00000000..3592642b --- /dev/null +++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Specialized; + +namespace MinecraftClient.Protocol +{ + class XboxLive + { + private readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en"; + private readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate"; + private readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize"; + + private readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; + + private Regex ppft = new Regex("sFTTag:'.*value=\"(.*)\"\\/>'"); + private Regex urlPost = new Regex("urlPost:'(.+?(?=\'))"); + private Regex confirm = new Regex("identity\\/confirm"); + + /// + /// Pre-authentication + /// + /// This step is to get the login page for later use + /// + public PreAuthResponse PreAuth() + { + var request = new ProxiedWebRequest(authorize); + request.UserAgent = userAgent; + var response = request.Get(); + + string html = response.Body; + + string PPFT = ppft.Match(html).Groups[1].Value; + string urlPost = this.urlPost.Match(html).Groups[1].Value; + + if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost)) + { + throw new Exception("Fail to extract PPFT or urlPost"); + } + //Console.WriteLine("PPFT: {0}", PPFT); + //Console.WriteLine(); + //Console.WriteLine("urlPost: {0}", urlPost); + + return new PreAuthResponse() + { + UrlPost = urlPost, + PPFT = PPFT, + Cookie = response.Cookies + }; + } + + /// + /// Perform login request + /// + /// This step is to send the login request by using the PreAuth response + /// Microsoft account email + /// Account password + /// + /// + public UserLoginResponse UserLogin(string email, string password, PreAuthResponse preAuth) + { + var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie); + request.UserAgent = userAgent; + + string postData = "login=" + Uri.EscapeDataString(email) + + "&loginfmt=" + Uri.EscapeDataString(email) + + "&passwd=" + Uri.EscapeDataString(password) + + "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT); + + var response = request.Post("application/x-www-form-urlencoded", postData); + + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + + if (response.StatusCode >= 300 && response.StatusCode <= 399) + { + string url = response.Headers.Get("Location"); + string hash = url.Split('#')[1]; + + var request2 = new ProxiedWebRequest(url); + var response2 = request2.Get(); + + if (response2.StatusCode != 200) + { + throw new Exception("Authentication failed"); + } + + if (string.IsNullOrEmpty(hash)) + { + if (confirm.IsMatch(response2.Body)) + { + throw new Exception("Activity confirmation required"); + } + else throw new Exception("Invalid credentials or 2FA enabled"); + } + var dict = Request.ParseQueryString(hash); + + //foreach (var pair in dict) + //{ + // Console.WriteLine("{0}: {1}", pair.Key, pair.Value); + //} + + return new UserLoginResponse() + { + AccessToken = dict["access_token"], + RefreshToken = dict["refresh_token"], + ExpiresIn = int.Parse(dict["expires_in"]) + }; + } + else + { + throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode); + } + } + + /// + /// Xbox Live Authenticate + /// + /// + /// + public XblAuthenticateResponse XblAuthenticate(UserLoginResponse loginResponse) + { + var request = new ProxiedWebRequest(xbl); + request.UserAgent = userAgent; + request.Accept = "application/json"; + request.Headers.Add("x-xbl-contract-version", "0"); + + string payload = "{" + + "\"Properties\": {" + + "\"AuthMethod\": \"RPS\"," + + "\"SiteName\": \"user.auth.xboxlive.com\"," + + "\"RpsTicket\": \"" + loginResponse.AccessToken + "\"" + + "}," + + "\"RelyingParty\": \"http://auth.xboxlive.com\"," + + "\"TokenType\": \"JWT\"" + + "}"; + var response = request.Post("application/json", payload); + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + if (response.StatusCode == 200) + { + string jsonString = response.Body; + //Console.WriteLine(jsonString); + + Json.JSONData json = Json.ParseJson(jsonString); + string token = json.Properties["Token"].StringValue; + string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue; + return new XblAuthenticateResponse() + { + Token = token, + UserHash = userHash + }; + } + else + { + throw new Exception("XBL Authentication failed"); + } + } + + /// + /// XSTS Authenticate + /// + /// (Don't ask me what is XSTS, I DONT KNOW) + /// + /// + public XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse) + { + var request = new ProxiedWebRequest(xsts); + request.UserAgent = userAgent; + request.Accept = "application/json"; + request.Headers.Add("x-xbl-contract-version", "1"); + + string payload = "{" + + "\"Properties\": {" + + "\"SandboxId\": \"RETAIL\"," + + "\"UserTokens\": [" + + "\"" + xblResponse.Token + "\"" + + "]" + + "}," + + "\"RelyingParty\": \"rp://api.minecraftservices.com/\"," + + "\"TokenType\": \"JWT\"" + + "}"; + var response = request.Post("application/json", payload); + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + if (response.StatusCode == 200) + { + string jsonString = response.Body; + Json.JSONData json = Json.ParseJson(jsonString); + string token = json.Properties["Token"].StringValue; + string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue; + return new XSTSAuthenticateResponse() + { + Token = token, + UserHash = userHash + }; + } + else + { + if (response.StatusCode == 401) + { + Json.JSONData json = Json.ParseJson(response.Body); + if (json.Properties["XErr"].StringValue == "2148916233") + { + throw new Exception("The account doesn't have an Xbox account"); + } + else if (json.Properties["XErr"].StringValue == "2148916238") + { + throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult"); + } + else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue); + } + else + { + throw new Exception("XSTS Authentication failed"); + } + } + } + + public struct PreAuthResponse + { + public string UrlPost; + public string PPFT; + public NameValueCollection Cookie; + } + + public struct UserLoginResponse + { + public string AccessToken; + public string RefreshToken; + public int ExpiresIn; + } + + public struct XblAuthenticateResponse + { + public string Token; + public string UserHash; + } + + public struct XSTSAuthenticateResponse + { + public string Token; + public string UserHash; + } + } + + class MinecraftWithXbox + { + private readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox"; + private readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore"; + private readonly string profile = "https://api.minecraftservices.com/minecraft/profile"; + + /// + /// Login to Minecraft using the XSTS token and user hash obtained before + /// + /// + /// + /// + public string LoginWithXbox(string userHash, string xstsToken) + { + var request = new ProxiedWebRequest(loginWithXbox); + request.Accept = "application/json"; + + string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}"; + var response = request.Post("application/json", payload); + + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + + string jsonString = response.Body; + Json.JSONData json = Json.ParseJson(jsonString); + return json.Properties["access_token"].StringValue; + } + + /// + /// Check if user own Minecraft by access token + /// + /// + /// True if the user own the game + public bool UserHasGame(string accessToken) + { + var request = new ProxiedWebRequest(ownership); + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + var response = request.Get(); + + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + + string jsonString = response.Body; + Json.JSONData json = Json.ParseJson(jsonString); + return json.Properties["items"].DataArray.Count > 0; + } + + public UserProfile GetUserProfile(string accessToken) + { + var request = new ProxiedWebRequest(profile); + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + var response = request.Get(); + + if (Settings.DebugMessages) + { + ConsoleIO.WriteLine(response.ToString()); + } + + string jsonString = response.Body; + Json.JSONData json = Json.ParseJson(jsonString); + return new UserProfile() + { + UUID = json.Properties["id"].StringValue, + UserName = json.Properties["name"].StringValue + }; + } + + public struct UserProfile + { + public string UUID; + public string UserName; + } + } + + /// + /// Helper class + /// + static class Request + { + static public Dictionary ParseQueryString(string query) + { + return query.Split('&') + .ToDictionary(c => c.Split('=')[0], + c => Uri.UnescapeDataString(c.Split('=')[1])); + } + } +} diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 90a26d3e..e0db734e 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -328,6 +328,7 @@ namespace MinecraftClient.Protocol } public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError }; + public enum AccountType { Mojang, Microsoft }; /// /// Allows to login to a premium Minecraft account using the Yggdrasil authentication scheme. @@ -336,7 +337,20 @@ namespace MinecraftClient.Protocol /// Password /// In case of successful login, will contain session information for multiplayer /// Returns the status of the login (Success, Failure, etc.) - public static LoginResult GetLogin(string user, string pass, out SessionToken session) + public static LoginResult GetLogin(string user, string pass, AccountType type, out SessionToken session) + { + if (type == AccountType.Microsoft) + { + return MicrosoftLogin(user, pass, out session); + } + else if (type == AccountType.Mojang) + { + return MojangLogin(user, pass, out session); + } + else throw new InvalidOperationException("Account type must be Mojang or Microsoft"); + } + + private static LoginResult MojangLogin(string user, string pass, out SessionToken session) { session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; @@ -415,6 +429,44 @@ namespace MinecraftClient.Protocol } } + private static LoginResult MicrosoftLogin(string email, string password, out SessionToken session) + { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + var ms = new XboxLive(); + var mc = new MinecraftWithXbox(); + + try + { + var msaResponse = ms.UserLogin(email, password, ms.PreAuth()); + var xblResponse = ms.XblAuthenticate(msaResponse); + var xsts = ms.XSTSAuthenticate(xblResponse); // Might throw even password correct + + string accessToken = mc.LoginWithXbox(xsts.UserHash, xsts.Token); + bool hasGame = mc.UserHasGame(accessToken); + if (hasGame) + { + var profile = mc.GetUserProfile(accessToken); + session.PlayerName = profile.UserName; + session.PlayerID = profile.UUID; + session.ID = accessToken; + return LoginResult.Success; + } + else + { + return LoginResult.NotPremium; + } + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); + if (Settings.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return LoginResult.WrongPassword; // Might not always be wrong password + } + } + /// /// Validates whether accessToken must be refreshed /// diff --git a/MinecraftClient/Protocol/ProxiedWebRequest.cs b/MinecraftClient/Protocol/ProxiedWebRequest.cs new file mode 100644 index 00000000..a5ce102c --- /dev/null +++ b/MinecraftClient/Protocol/ProxiedWebRequest.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Collections.Specialized; +using System.Net.Sockets; +using MinecraftClient.Proxy; +using System.Net.Security; + +namespace MinecraftClient.Protocol +{ + /// + /// Create a new http request and optionally with proxy according to setting + /// + public class ProxiedWebRequest + { + private readonly string httpVersion = "HTTP/1.0"; // Use 1.0 here because 1.1 server may send chunked data + + private Uri uri; + private string host { get => uri.Host; } + private int port { get => uri.Port; } + private string path { get => uri.PathAndQuery; } + private bool isSecure { get => uri.Scheme == "https"; } + + public NameValueCollection Headers = new NameValueCollection(); + + public string UserAgent { get => Headers.Get("User-Agent"); set => Headers.Set("User-Agent", value); } + public string Accept { get => Headers.Get("Accept"); set => Headers.Set("Accept", value); } + public string Cookie { set => Headers.Set("Cookie", value); } + + /// + /// Create a new http request + /// + /// Target URL + public ProxiedWebRequest(string url) + { + uri = new Uri(url); + SetupBasicHeaders(); + } + + /// + /// Create a new http request with cookies + /// + /// Target URL + /// Cookies to use + public ProxiedWebRequest(string url, NameValueCollection cookies) + { + uri = new Uri(url); + Headers.Add("Cookie", GetCookieString(cookies)); + SetupBasicHeaders(); + } + + /// + /// Setup some basic headers + /// + private void SetupBasicHeaders() + { + Headers.Add("Host", host); + Headers.Add("User-Agent", "MCC/" + Program.Version); + Headers.Add("Accept", "*/*"); + Headers.Add("Connection", "close"); + } + + /// + /// Perform GET request and get the response. Proxy is handled automatically + /// + /// + public Response Get() + { + return Send("GET"); + } + + /// + /// Perform POST request and get the response. Proxy is handled automatically + /// + /// The content type of request body + /// Request body + /// + public Response Post(string contentType, string body) + { + Headers.Add("Content-Type", contentType); + // Calculate length + Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString()); + return Send("POST", body); + } + + /// + /// Send a http request to the server. Proxy is handled automatically + /// + /// Method in string representation + /// Optional request body + /// + private Response Send(string method, string body = "") + { + List requestMessage = new List() + { + string.Format("{0} {1} {2}", method.ToUpper(), path, httpVersion) // Request line + }; + foreach (string key in Headers) // Headers + { + var value = Headers[key]; + requestMessage.Add(string.Format("{0}: {1}", key, value)); + } + requestMessage.Add(""); // + if (body != "") + { + requestMessage.Add(body); + } + else requestMessage.Add(""); // + if (Settings.DebugMessages) + { + foreach (string l in requestMessage) + { + ConsoleIO.WriteLine("< " + l); + } + } + Response response = Response.Empty(); + AutoTimeout.Perform(() => + { + TcpClient client = ProxyHandler.newTcpClient(host, port, true); + Stream stream; + if (isSecure) + { + stream = new SslStream(client.GetStream()); + ((SslStream)stream).AuthenticateAsClient(host); + } + else + { + stream = client.GetStream(); + } + string h = string.Join("\r\n", requestMessage.ToArray()); + byte[] data = Encoding.ASCII.GetBytes(h); + stream.Write(data, 0, data.Length); + stream.Flush(); + StreamReader sr = new StreamReader(stream); + string rawResult = sr.ReadToEnd(); + response = ParseResponse(rawResult); + try + { + sr.Close(); + stream.Close(); + client.Close(); + } catch { } + }, + TimeSpan.FromSeconds(30)); + return response; + } + + /// + /// Parse a raw response string to response object + /// + /// raw response string + /// + private Response ParseResponse(string raw) + { + int statusCode; + string responseBody = ""; + NameValueCollection headers = new NameValueCollection(); + NameValueCollection cookies = new NameValueCollection(); + if (raw.StartsWith("HTTP/1.1") || raw.StartsWith("HTTP/1.0")) + { + Queue msg = new Queue(raw.Split(new string[] { "\r\n" }, StringSplitOptions.None)); + statusCode = int.Parse(msg.Dequeue().Split(' ')[1]); + + while (msg.Peek() != "") + { + string[] header = msg.Dequeue().Split(new char[] { ':' }, 2); // Split first ':' only + string key = header[0].ToLower(); // Key is case-insensitive + string value = header[1]; + if (key == "set-cookie") + { + string[] cookie = value.Split(';'); // cookie options are ignored + string[] tmp = cookie[0].Split(new char[] { '=' }, 2); // Split first '=' only + string cname = tmp[0].Trim(); + string cvalue = tmp[1].Trim(); + cookies.Add(cname, cvalue); + } + else + { + headers.Add(key, value.Trim()); + } + } + msg.Dequeue(); + if (msg.Count > 0) + responseBody = msg.Dequeue(); + + return new Response() + { + StatusCode = statusCode, + Body = responseBody, + Headers = headers, + Cookies = cookies + }; + } + else + { + return new Response() + { + StatusCode = 520, // 502 - Web Server Returned an Unknown Error + Body = "", + Headers = headers, + Cookies = cookies + }; + } + } + + /// + /// Get the cookie string representation to use in header + /// + /// + /// + private static string GetCookieString(NameValueCollection cookies) + { + var sb = new StringBuilder(); + foreach (string key in cookies) + { + var value = cookies[key]; + sb.Append(string.Format("{0}={1}; ", key, value)); + } + string result = sb.ToString(); + return result.Remove(result.Length - 2); // Remove "; " at the end + } + + /// + /// Basic response object + /// + public class Response + { + public int StatusCode; + public string Body; + public NameValueCollection Headers; + public NameValueCollection Cookies; + + /// + /// Get an empty response object + /// + /// + public static Response Empty() + { + return new Response() + { + StatusCode = 204, // 204 - No content + Body = "", + Headers = new NameValueCollection(), + Cookies = new NameValueCollection() + }; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine("Status code: " + StatusCode); + sb.AppendLine("Headers:"); + foreach (string key in Headers) + { + sb.AppendLine(string.Format(" {0}: {1}", key, Headers[key])); + } + if (Cookies.Count > 0) + { + sb.AppendLine(); + sb.AppendLine("Cookies: "); + foreach (string key in Cookies) + { + sb.AppendLine(string.Format(" {0}={1}", key, Cookies[key])); + } + } + if (Body != "") + { + sb.AppendLine(); + if (Body.Length > 200) + { + sb.AppendLine("Body: (Truncated to 200 characters)"); + } + else sb.AppendLine("Body: "); + sb.AppendLine(Body.Length > 200 ? Body.Substring(0, 200) + "..." : Body); + } + return sb.ToString(); + } + } + } +} diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 27213e42..7e3b1e7e 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -9,6 +9,7 @@ login= password= serverip= +type=mojang # Account type. mojang or microsoft # Advanced settings diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 3ede6c67..d0485933 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -8,7 +8,7 @@ mcc.password_hidden=Password : {0} mcc.offline=§8You chose to run in offline mode. mcc.session_invalid=§8Cached session is invalid or expired. mcc.session_valid=§8Cached session is still valid for {0}. -mcc.connecting=Connecting to Minecraft.net... +mcc.connecting=Connecting to {0}... mcc.ip=Server IP : mcc.use_version=§8Using Minecraft version {0} (protocol v{1}) mcc.unknown_version=§8Unknown or not supported MC version {0}.\nSwitching to autodetection mode. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index e2496952..40aaf572 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -25,6 +25,7 @@ namespace MinecraftClient public static string Login = ""; public static string Username = ""; public static string Password = ""; + public static ProtocolHandler.AccountType AccountType = ProtocolHandler.AccountType.Mojang; public static string ServerIP = ""; public static ushort ServerPort = 25565; public static string ServerVersion = ""; @@ -262,6 +263,9 @@ namespace MinecraftClient { case "login": Login = argValue; break; case "password": Password = argValue; break; + case "type": AccountType = argValue == "mojang" + ? ProtocolHandler.AccountType.Mojang + : ProtocolHandler.AccountType.Microsoft; break; case "serverip": if (!SetServerIP(argValue)) serverAlias = argValue; ; break; case "singlecommand": SingleCommand = argValue; break; case "language": Language = argValue; break;