using System; using System.Collections.Generic; using System.Linq; using System.Text; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Proxy; using System.Net.Sockets; using System.Net.Security; using MinecraftClient.Protocol.Handlers.Forge; namespace MinecraftClient.Protocol { /// /// Handle login, session, server ping and provide a protocol handler for interacting with a minecraft server. /// public static class ProtocolHandler { /// /// Perform a DNS lookup for a Minecraft Service using the specified domain name /// /// Input domain name, updated with target host if any, else left untouched /// Updated with target port if any, else left untouched /// TRUE if a Minecraft Service was found. public static bool MinecraftServiceLookup(ref string domain, ref ushort port) { if (!String.IsNullOrEmpty(domain) && domain.Any(c => char.IsLetter(c))) { try { Console.WriteLine("Resolving {0}...", domain); var response = new DnDns.Query.DnsQueryRequest().Resolve("_minecraft._tcp." + domain, DnDns.Enums.NsType.SRV, DnDns.Enums.NsClass.ANY, System.Net.Sockets.ProtocolType.Tcp); var records = response.Answers //Order SRV records by priority and weight, then randomly .Where(record => record is DnDns.Records.SrvRecord) .Select(record => (DnDns.Records.SrvRecord)record) .OrderBy(record => record.Priority) .ThenByDescending(record => record.Weight) .ThenBy(record => Guid.NewGuid()); if (records.Any()) { var result = records.First(); string target = result.HostName.Trim('.'); ConsoleIO.WriteLineFormatted(String.Format("§8Found server {0}:{1} for domain {2}", target, result.Port, domain)); domain = target; port = result.Port; return true; } } catch (Exception e) { ConsoleIO.WriteLineFormatted(String.Format("§8Failed to perform SRV lookup for {0}\n{1}: {2}", domain, e.GetType().FullName, e.Message)); } } return false; } /// /// Retrieve information about a Minecraft server /// /// Server IP to ping /// Server Port to ping /// Will contain protocol version, if ping successful /// TRUE if ping was successful public static bool GetServerInfo(string serverIP, ushort serverPort, ref int protocolversion, ref ForgeInfo forgeInfo) { bool success = false; int protocolversionTmp = 0; ForgeInfo forgeInfoTmp = null; if (AutoTimeout.Perform(() => { try { if (Protocol16Handler.doPing(serverIP, serverPort, ref protocolversionTmp) || Protocol18Handler.doPing(serverIP, serverPort, ref protocolversionTmp, ref forgeInfoTmp)) { success = true; } else ConsoleIO.WriteLineFormatted("§8Unexpected response from the server (is that a Minecraft server?)"); } catch (Exception e) { ConsoleIO.WriteLineFormatted(String.Format("§8{0}: {1}", e.GetType().FullName, e.Message)); } }, TimeSpan.FromSeconds(Settings.ResolveSrvRecordsShortTimeout ? 5 : 30))) { protocolversion = protocolversionTmp; forgeInfo = forgeInfoTmp; return success; } else { ConsoleIO.WriteLineFormatted("§8A timeout occured while attempting to connect to this IP."); return false; } } /// /// Get a protocol handler for the specified Minecraft version /// /// Tcp Client connected to the server /// Protocol version to handle /// Handler with the appropriate callbacks /// public static IMinecraftCom GetProtocolHandler(TcpClient Client, int ProtocolVersion, ForgeInfo forgeInfo, IMinecraftComHandler Handler) { int[] supportedVersions_Protocol16 = { 51, 60, 61, 72, 73, 74, 78 }; if (Array.IndexOf(supportedVersions_Protocol16, ProtocolVersion) > -1) return new Protocol16Handler(Client, ProtocolVersion, Handler); int[] supportedVersions_Protocol18 = { 4, 5, 47, 107, 108, 109, 110, 210, 315, 316 }; if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1) return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo); throw new NotSupportedException("The protocol version no." + ProtocolVersion + " is not supported."); } /// /// Convert a human-readable Minecraft version number to network protocol version number /// /// The Minecraft version number /// The protocol version number or 0 if could not determine protocol version: error, unknown, not supported public static int MCVer2ProtocolVersion(string MCVersion) { if (MCVersion.Contains('.')) { switch (MCVersion.Split(' ')[0].Trim()) { case "1.4.6": case "1.4.7": return 51; case "1.5.1": return 60; case "1.5.2": return 61; case "1.6": case "1.6.0": return 72; case "1.6.1": case "1.6.2": case "1.6.3": case "1.6.4": return 73; case "1.7.2": case "1.7.3": case "1.7.4": case "1.7.5": return 4; case "1.7.6": case "1.7.7": case "1.7.8": case "1.7.9": case "1.7.10": return 5; case "1.8": case "1.8.0": case "1.8.1": case "1.8.2": case "1.8.3": case "1.8.4": case "1.8.5": case "1.8.6": case "1.8.7": case "1.8.8": case "1.8.9": return 47; case "1.9": case "1.9.0": return 107; case "1.9.1": return 108; case "1.9.2": return 109; case "1.9.3": case "1.9.4": return 110; case "1.10": case "1.10.0": case "1.10.1": case "1.10.2": return 210; case "1.11": case "1.11.0": return 315; case "1.11.1": case "1.11.2": return 316; default: return 0; } } else { try { return Int32.Parse(MCVersion); } catch { return 0; } } } public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, NullError }; /// /// Allows to login to a premium Minecraft account using the Yggdrasil authentication scheme. /// /// Login /// Password /// Will contain the access token returned by Minecraft.net, if the login is successful /// Will contain the client token generated before sending to Minecraft.net /// Will contain the player's PlayerID, needed for multiplayer /// Returns the status of the login (Success, Failure, etc.) public static LoginResult GetLogin(string user, string pass, out SessionToken session) { session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; try { string result = ""; string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" + JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; int code = DoHTTPSPost("authserver.mojang.com", "/authenticate", json_request, ref result); if (code == 200) { if (result.Contains("availableProfiles\":[]}")) { return LoginResult.NotPremium; } else { string[] temp = result.Split(new string[] { "accessToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { session.ID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "name\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { session.PlayerName = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "availableProfiles\":[{\"id\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { session.PlayerID = temp[1].Split('"')[0]; } return LoginResult.Success; } } else if (code == 403) { if (result.Contains("UserMigratedException")) { return LoginResult.AccountMigrated; } else return LoginResult.WrongPassword; } else if (code == 503) { return LoginResult.ServiceUnavailable; } else { ConsoleIO.WriteLineFormatted("§8Got error code from server: " + code); return LoginResult.OtherError; } } catch (System.Security.Authentication.AuthenticationException) { return LoginResult.SSLError; } catch (System.IO.IOException e) { if (e.Message.Contains("authentication")) { return LoginResult.SSLError; } else return LoginResult.OtherError; } catch { return LoginResult.OtherError; } } /// /// Validates whether accessToken must be refreshed /// /// Will contain the cached access token previously returned by Minecraft.net /// Will contain the cached client token created on login /// Returns the status of the token (Valid, Invalid, etc.) public static LoginResult GetTokenValidation(SessionToken session) { try { string result = ""; string json_request = "{\"accessToken\": \"" + JsonEncode(session.ID) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; int code = DoHTTPSPost("authserver.mojang.com", "/validate", json_request, ref result); if (code == 204) { return LoginResult.Success; } else if (code == 403) { return LoginResult.LoginRequired; } else { return LoginResult.OtherError; } } catch { return LoginResult.OtherError; } } /// /// Refreshes invalid token /// /// Login /// Will contain the new access token returned by Minecraft.net, if the refresh is successful /// Will contain the client token generated before sending to Minecraft.net /// Will contain the player's PlayerID, needed for multiplayer /// Returns the status of the new token request (Success, Failure, etc.) public static LoginResult GetNewToken(SessionToken currentsession, out SessionToken newsession) { newsession = new SessionToken(); try { string result = ""; string json_request = "{ \"accessToken\": \"" + JsonEncode(currentsession.ID) + "\", \"clientToken\": \"" + JsonEncode(currentsession.ClientID) + "\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) + "\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }"; int code = DoHTTPSPost("authserver.mojang.com", "/refresh", json_request, ref result); if (code == 200) { if (result == null) { return LoginResult.NullError; } else { string[] temp = result.Split(new string[] { "accessToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { newsession.ID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "clientToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { newsession.ClientID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "name\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { newsession.PlayerName = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "selectedProfile\":[{\"id\":\"" }, StringSplitOptions.RemoveEmptyEntries); if (temp.Length >= 2) { newsession.PlayerID = temp[1].Split('"')[0]; } return LoginResult.Success; } } else if (code == 403 && result.Contains("InvalidToken")) { return LoginResult.InvalidToken; } else { ConsoleIO.WriteLineFormatted("§8Got error code from server while refreshing authentication: " + code); return LoginResult.OtherError; } } catch { return LoginResult.OtherError; } } /// /// Check session using Mojang's Yggdrasil authentication scheme. Allows to join an online-mode server /// /// Username /// Session ID /// Server ID /// TRUE if session was successfully checked public static bool SessionCheck(string uuid, string accesstoken, string serverhash) { try { string result = ""; string json_request = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid + "\",\"serverId\":\"" + serverhash + "\"}"; int code = DoHTTPSPost("sessionserver.mojang.com", "/session/minecraft/join", json_request, ref result); return (result == ""); } catch { return false; } } //Test method currently not working //See https://github.com/ORelio/Minecraft-Console-Client/issues/51 public static void RealmsListWorlds(string username, string uuid, string accesstoken) { string result = ""; string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username, Program.MCHighestVersion); DoHTTPSGet("mcoapi.minecraft.net", "/worlds", cookies, ref result); Console.WriteLine(result); } /// /// Make a HTTPS GET request to the specified endpoint of the Mojang API /// /// Host to connect to /// Endpoint for making the request /// Cookies for making the request /// Request result /// HTTP Status code private static int DoHTTPSGet(string host, string endpoint, string cookies, ref string result) { List http_request = new List(); http_request.Add("GET " + endpoint + " HTTP/1.1"); http_request.Add("Cookie: " + cookies); http_request.Add("Cache-Control: no-cache"); http_request.Add("Pragma: no-cache"); http_request.Add("Host: " + host); http_request.Add("User-Agent: Java/1.6.0_27"); http_request.Add("Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7"); http_request.Add("Connection: close"); http_request.Add(""); return DoHTTPSRequest(http_request, host, ref result); } /// /// Make a HTTPS POST request to the specified endpoint of the Mojang API /// /// Host to connect to /// Endpoint for making the request /// Request payload /// Request result /// HTTP Status code private static int DoHTTPSPost(string host, string endpoint, string request, ref string result) { List http_request = new List(); http_request.Add("POST " + endpoint + " HTTP/1.1"); http_request.Add("Host: " + host); http_request.Add("User-Agent: MCC/" + Program.Version); http_request.Add("Content-Type: application/json"); http_request.Add("Content-Length: " + Encoding.ASCII.GetBytes(request).Length); http_request.Add("Connection: close"); http_request.Add(""); http_request.Add(request); return DoHTTPSRequest(http_request, host, ref result); } /// /// Manual HTTPS request since we must directly use a TcpClient because of the proxy. /// This method connects to the server, enables SSL, do the request and read the response. /// /// Request headers and optional body (POST) /// Host to connect to /// Request result /// HTTP Status code private static int DoHTTPSRequest(List headers, string host, ref string result) { string postResult = null; int statusCode = 520; Exception exception = null; AutoTimeout.Perform(() => { try { TcpClient client = ProxyHandler.newTcpClient(host, 443, true); SslStream stream = new SslStream(client.GetStream()); stream.AuthenticateAsClient(host); stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); System.IO.StreamReader sr = new System.IO.StreamReader(stream); string raw_result = sr.ReadToEnd(); if (raw_result.StartsWith("HTTP/1.1")) { postResult = raw_result.Substring(raw_result.IndexOf("\r\n\r\n") + 4); statusCode = Settings.str2int(raw_result.Split(' ')[1]); } else statusCode = 520; //Web server is returning an unknown error } catch (Exception e) { if (!(e is System.Threading.ThreadAbortException)) { exception = e; } } }, TimeSpan.FromSeconds(30)); result = postResult; if (exception != null) throw exception; return statusCode; } /// /// Encode a string to a json string. /// Will convert special chars to \u0000 unicode escape sequences. /// /// Source text /// Encoded text private static string JsonEncode(string text) { StringBuilder result = new StringBuilder(); foreach (char c in text) { if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { result.Append(c); } else { result.AppendFormat(@"\u{0:x4}", (int)c); } } return result.ToString(); } } }