using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using DnsClient;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Protocol.Session;
using MinecraftClient.Proxy;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig;
namespace MinecraftClient.Protocol
{
///
/// Handle login, session, server ping and provide a protocol handler for interacting with a minecraft server.
///
///
/// Typical update steps for marking a new Minecraft version as supported:
/// - Add protocol ID in GetProtocolHandler()
/// - Add 1.X.X case in MCVer2ProtocolVersion()
///
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)
{
bool foundService = false;
string domainVal = domain;
ushort portVal = port;
if (!String.IsNullOrEmpty(domain) && domain.Any(c => char.IsLetter(c)))
{
AutoTimeout.Perform(() =>
{
try
{
ConsoleIO.WriteLine(string.Format(Translations.mcc_resolve, domainVal));
var lookupClient = new LookupClient();
var response = lookupClient.Query(new DnsQuestion($"_minecraft._tcp.{domainVal}", QueryType.SRV));
if (response.HasError != true && response.Answers.SrvRecords().Any())
{
//Order SRV records by priority and weight, then randomly
var result = response.Answers.SrvRecords()
.OrderBy(record => record.Priority)
.ThenByDescending(record => record.Weight)
.ThenBy(record => Guid.NewGuid())
.First();
string target = result.Target.Value.Trim('.');
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_found, target, result.Port, domainVal));
domainVal = target;
portVal = result.Port;
foundService = true;
}
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_not_found, domainVal, e.GetType().FullName, e.Message));
}
}, TimeSpan.FromSeconds(Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30));
}
domain = domainVal;
port = portVal;
return foundService;
}
///
/// 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 (Protocol18Handler.DoPing(serverIP, serverPort, ref protocolversionTmp, ref forgeInfoTmp)
|| Protocol16Handler.DoPing(serverIP, serverPort, ref protocolversionTmp))
{
success = true;
}
else
ConsoleIO.WriteLineFormatted("§8" + Translations.error_unexpect_response, acceptnewlines: true);
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted(String.Format("§8{0}: {1}", e.GetType().FullName, e.Message));
}
}, TimeSpan.FromSeconds(Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30)))
{
if (protocolversion != 0 && protocolversion != protocolversionTmp)
ConsoleIO.WriteLineFormatted("§8" + Translations.error_version_different, acceptnewlines: true);
if (protocolversion == 0 && protocolversionTmp <= 1)
ConsoleIO.WriteLineFormatted("§8" + Translations.error_no_version_report, acceptnewlines: true);
if (protocolversion == 0)
protocolversion = protocolversionTmp;
forgeInfo = forgeInfoTmp;
return success;
}
else
{
ConsoleIO.WriteLineFormatted("§8" + Translations.error_connection_timeout, acceptnewlines: true);
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, 335, 338, 340, 393, 401, 404, 477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762 };
if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1)
return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo);
throw new NotSupportedException(string.Format(Translations.exception_version_unsupport, ProtocolVersion));
}
///
/// 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.0":
case "1.0.0":
case "1.0.1":
return 22;
case "1.1":
case "1.1.0":
return 23;
case "1.2":
case "1.2.0":
case "1.2.1":
case "1.2.2":
case "1.2.3":
return 28;
case "1.2.4":
case "1.2.5":
return 29;
case "1.3":
case "1.3.0":
case "1.3.1":
case "1.3.2":
return 39;
case "1.4":
case "1.4.0":
case "1.4.1":
case "1.4.2":
return 48; // 47 conflicts with 1.8
case "1.4.3":
return 48;
case "1.4.4":
case "1.4.5":
return 49;
case "1.4.6":
case "1.4.7":
return 51;
case "1.5":
case "1.5.0":
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":
return 73;
case "1.6.2":
return 74;
case "1.6.3":
return 77;
case "1.6.4":
return 78;
case "1.7":
case "1.7.0":
case "1.7.1":
return 3;
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;
case "1.12":
case "1.12.0":
return 335;
case "1.12.1":
return 338;
case "1.12.2":
return 340;
case "1.13":
case "1.13.0":
return 393;
case "1.13.1":
return 401;
case "1.13.2":
return 404;
case "1.14":
case "1.14.0":
return 477;
case "1.14.1":
return 480;
case "1.14.2":
return 485;
case "1.14.3":
return 490;
case "1.14.4":
return 498;
case "1.15":
case "1.15.0":
return 573;
case "1.15.1":
return 575;
case "1.15.2":
return 578;
case "1.16":
case "1.16.0":
return 735;
case "1.16.1":
return 736;
case "1.16.2":
return 751;
case "1.16.3":
return 753;
case "1.16.4":
case "1.16.5":
return 754;
case "1.17":
case "1.17.0":
return 755;
case "1.17.1":
return 756;
case "1.18":
case "1.18.0":
case "1.18.1":
return 757;
case "1.18.2":
return 758;
case "1.19":
case "1.19.0":
return 759;
case "1.19.1":
case "1.19.2":
return 760;
case "1.19.3":
return 761;
case "1.19.4":
return 762;
default:
return 0;
}
}
else
{
try
{
return Int32.Parse(MCVersion, NumberStyles.Any, CultureInfo.CurrentCulture);
}
catch
{
return 0;
}
}
}
///
/// Convert a network protocol version number to human-readable Minecraft version number
///
/// Some Minecraft versions share the same protocol number. In that case, the lowest version for that protocol is returned.
/// The Minecraft protocol version number
/// The 1.X.X version number, or 0.0 if could not determine protocol version
public static string ProtocolVersion2MCVer(int protocol)
{
switch (protocol)
{
case 22: return "1.0";
case 23: return "1.1";
case 28: return "1.2.3";
case 29: return "1.2.5";
case 39: return "1.3.2";
// case 47: return "1.4.2";
case 48: return "1.4.3";
case 49: return "1.4.5";
case 51: return "1.4.6";
case 60: return "1.5.1";
case 62: return "1.5.2";
case 72: return "1.6";
case 73: return "1.6.1";
case 3: return "1.7.1";
case 4: return "1.7.2";
case 5: return "1.7.6";
case 47: return "1.8";
case 107: return "1.9";
case 108: return "1.9.1";
case 109: return "1.9.2";
case 110: return "1.9.3";
case 210: return "1.10";
case 315: return "1.11";
case 316: return "1.11.1";
case 335: return "1.12";
case 338: return "1.12.1";
case 340: return "1.12.2";
case 393: return "1.13";
case 401: return "1.13.1";
case 404: return "1.13.2";
case 477: return "1.14";
case 480: return "1.14.1";
case 485: return "1.14.2";
case 490: return "1.14.3";
case 498: return "1.14.4";
case 573: return "1.15";
case 575: return "1.15.1";
case 578: return "1.15.2";
case 735: return "1.16";
case 736: return "1.16.1";
case 751: return "1.16.2";
case 753: return "1.16.3";
case 754: return "1.16.5";
case 755: return "1.17";
case 756: return "1.17.1";
case 757: return "1.18.1";
case 758: return "1.18.2";
case 759: return "1.19";
case 760: return "1.19.2";
case 761: return "1.19.3";
case 762: return "1.19.4";
default: return "0.0";
}
}
///
/// Check if we can force-enable Forge support for a Minecraft version without using server Ping
///
/// Minecraft protocol version
/// TRUE if we can force-enable Forge support without using server Ping
public static bool ProtocolMayForceForge(int protocol)
{
return Protocol18Forge.ServerMayForceForge(protocol);
}
///
/// Server Info: Consider Forge to be enabled regardless of server Ping
///
/// Minecraft protocol version
/// ForgeInfo item stating that Forge is enabled
public static ForgeInfo ProtocolForceForge(int protocol)
{
return Protocol18Forge.ServerForceForge(protocol);
}
public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError, UserCancel };
public enum AccountType { Mojang, Microsoft };
///
/// Allows to login to a premium Minecraft account using the Yggdrasil authentication scheme.
///
/// Login
/// 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, LoginType type, out SessionToken session)
{
if (type == LoginType.microsoft)
{
if (Config.Main.General.Method == LoginMethod.mcc)
return MicrosoftMCCLogin(user, pass, out session);
else
return MicrosoftBrowserLogin(out session, user);
}
else if (type == LoginType.mojang)
{
return MojangLogin(user, pass, out session);
}
else throw new InvalidOperationException("Account type must be Mojang or Microsoft");
}
///
/// Login using Mojang account. Will be outdated after account migration
///
///
///
///
///
private static LoginResult MojangLogin(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
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"].StringValue;
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
}
}
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("§8" + string.Format(Translations.error_http_code, code));
return LoginResult.OtherError;
}
}
catch (System.Security.Authentication.AuthenticationException e)
{
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + e.ToString());
}
return LoginResult.SSLError;
}
catch (System.IO.IOException e)
{
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + e.ToString());
}
if (e.Message.Contains("authentication"))
{
return LoginResult.SSLError;
}
else return LoginResult.OtherError;
}
catch (Exception e)
{
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + e.ToString());
}
return LoginResult.OtherError;
}
}
///
/// Sign-in to Microsoft Account without using browser. Only works if 2FA is disabled.
/// Might not work well in some rare cases.
///
///
///
///
///
private static LoginResult MicrosoftMCCLogin(string email, string password, out SessionToken session)
{
try
{
var msaResponse = XboxLive.UserLogin(email, password, XboxLive.PreAuth());
// Remove refresh token for MCC sign method
msaResponse.RefreshToken = string.Empty;
return MicrosoftLogin(msaResponse, out session);
}
catch (Exception e)
{
session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") };
ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
}
return LoginResult.WrongPassword; // Might not always be wrong password
}
}
///
/// Sign-in to Microsoft Account by asking user to open sign-in page using browser.
///
///
/// The downside is this require user to copy and paste lengthy content from and to console.
/// Sign-in page: 218 chars
/// Response URL: around 1500 chars
///
///
///
public static LoginResult MicrosoftBrowserLogin(out SessionToken session, string loginHint = "")
{
if (string.IsNullOrEmpty(loginHint))
Microsoft.OpenBrowser(Microsoft.SignInUrl);
else
Microsoft.OpenBrowser(Microsoft.GetSignInUrlWithHint(loginHint));
ConsoleIO.WriteLine(Translations.mcc_browser_open);
ConsoleIO.WriteLine("\n" + Microsoft.SignInUrl + "\n");
ConsoleIO.WriteLine(Translations.mcc_browser_login_code);
string code = ConsoleIO.ReadLine();
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft"));
var msaResponse = Microsoft.RequestAccessToken(code);
return MicrosoftLogin(msaResponse, out session);
}
public static LoginResult MicrosoftLoginRefresh(string refreshToken, out SessionToken session)
{
var msaResponse = Microsoft.RefreshAccessToken(refreshToken);
return MicrosoftLogin(msaResponse, out session);
}
private static LoginResult MicrosoftLogin(Microsoft.LoginResponse msaResponse, out SessionToken session)
{
session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") };
try
{
var xblResponse = XboxLive.XblAuthenticate(msaResponse);
var xsts = XboxLive.XSTSAuthenticate(xblResponse); // Might throw even password correct
string accessToken = MinecraftWithXbox.LoginWithXbox(xsts.UserHash, xsts.Token);
bool hasGame = MinecraftWithXbox.UserHasGame(accessToken);
if (hasGame)
{
var profile = MinecraftWithXbox.GetUserProfile(accessToken);
session.PlayerName = profile.UserName;
session.PlayerID = profile.UUID;
session.ID = accessToken;
session.RefreshToken = msaResponse.RefreshToken;
InternalConfig.Account.Login = msaResponse.Email;
return LoginResult.Success;
}
else
{
return LoginResult.NotPremium;
}
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
}
return LoginResult.WrongPassword; // Might not always be wrong password
}
}
///
/// Validates whether accessToken must be refreshed
///
/// Session token to validate
/// Returns the status of the token (Valid, Invalid, etc.)
public static LoginResult GetTokenValidation(SessionToken session)
{
var payload = JwtPayloadDecode.GetPayload(session.ID);
var json = Json.ParseJson(payload);
var expTimestamp = long.Parse(json.Properties["exp"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
var now = DateTime.Now;
var tokenExp = UnixTimeStampToDateTime(expTimestamp);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine("Access token expiration time is " + tokenExp.ToString());
}
if (now < tokenExp)
{
// Still valid
return LoginResult.Success;
}
else
{
// Token expired
return LoginResult.LoginRequired;
}
}
///
/// Refreshes invalid token
///
/// Login
/// In case of successful token refresh, will contain session information for multiplayer
/// Returns the status of the new token request (Success, Failure, etc.)
public static LoginResult GetNewToken(SessionToken currentsession, out SessionToken session)
{
session = 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
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"].StringValue;
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
}
}
else if (code == 403 && result.Contains("InvalidToken"))
{
return LoginResult.InvalidToken;
}
else
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_auth, 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 (code >= 200 && code < 300);
}
catch { return false; }
}
///
/// Retrieve available Realms worlds of a player and display them
///
/// Player Minecraft username
/// Player UUID
/// Access token
/// List of ID of available Realms worlds
public static List RealmsListWorlds(string username, string uuid, string accesstoken)
{
List realmsWorldsResult = new(); // Store world ID
try
{
string result = "";
string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username, Program.MCHighestVersion);
DoHTTPSGet("pc.realms.minecraft.net", "/worlds", cookies, ref result);
Json.JSONData realmsWorlds = Json.ParseJson(result);
if (realmsWorlds.Properties.ContainsKey("servers")
&& realmsWorlds.Properties["servers"].Type == Json.JSONData.DataType.Array
&& realmsWorlds.Properties["servers"].DataArray.Count > 0)
{
List availableWorlds = new(); // Store string to print
int index = 0;
foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray)
{
if (realmsServer.Properties.ContainsKey("name")
&& realmsServer.Properties.ContainsKey("owner")
&& realmsServer.Properties.ContainsKey("id")
&& realmsServer.Properties.ContainsKey("expired"))
{
if (realmsServer.Properties["expired"].StringValue == "false")
{
availableWorlds.Add(String.Format("[{0}] {2} ({3}) - {1}",
index++,
realmsServer.Properties["id"].StringValue,
realmsServer.Properties["name"].StringValue,
realmsServer.Properties["owner"].StringValue));
realmsWorldsResult.Add(realmsServer.Properties["id"].StringValue);
}
}
}
if (availableWorlds.Count > 0)
{
ConsoleIO.WriteLine(Translations.mcc_realms_available);
foreach (var world in availableWorlds)
ConsoleIO.WriteLine(world);
ConsoleIO.WriteLine(Translations.mcc_realms_join);
}
}
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + e.StackTrace);
}
}
return realmsWorldsResult;
}
///
/// Get the server address of a Realms world by world ID
///
/// The world ID of the Realms world
/// Player Minecraft username
/// Player UUID
/// Access token
/// Server address (host:port) or empty string if failure
public static string GetRealmsWorldServerAddress(string worldId, string username, string uuid, string accesstoken)
{
try
{
string result = "";
string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username, Program.MCHighestVersion);
int statusCode = DoHTTPSGet("pc.realms.minecraft.net", "/worlds/v1/" + worldId + "/join/pc", cookies, ref result);
if (statusCode == 200)
{
Json.JSONData serverAddress = Json.ParseJson(result);
if (serverAddress.Properties.ContainsKey("address"))
return serverAddress.Properties["address"].StringValue;
else
{
ConsoleIO.WriteLine(Translations.error_realms_ip_error);
return "";
}
}
else
{
ConsoleIO.WriteLine(Translations.error_realms_access_denied);
return "";
}
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + e.StackTrace);
}
return "";
}
}
///
/// 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()
{
"GET " + endpoint + " HTTP/1.1",
"Cookie: " + cookies,
"Cache-Control: no-cache",
"Pragma: no-cache",
"Host: " + host,
"User-Agent: Java/1.6.0_27",
"Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7",
"Connection: close",
"",
""
};
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()
{
"POST " + endpoint + " HTTP/1.1",
"Host: " + host,
"User-Agent: MCC/" + Program.Version,
"Content-Type: application/json",
"Content-Length: " + Encoding.ASCII.GetBytes(request).Length,
"Connection: close",
"",
request
};
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
{
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host));
TcpClient client = ProxyHandler.NewTcpClient(host, 443, true);
SslStream stream = new(client.GetStream());
stream.AuthenticateAsClient(host, null, SslProtocols.Tls12, true); // Enable TLS 1.2. Hotfix for #1780
if (Settings.Config.Logging.DebugMessages)
foreach (string line in headers)
ConsoleIO.WriteLineFormatted("§8> " + line);
stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray())));
System.IO.StreamReader sr = new(stream);
string raw_result = sr.ReadToEnd();
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine("");
foreach (string line in raw_result.Split('\n'))
ConsoleIO.WriteLineFormatted("§8< " + line);
}
if (raw_result.StartsWith("HTTP/1.1"))
{
postResult = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..];
statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture);
}
else statusCode = 520; //Web server is returning an unknown error
}
catch (Exception e)
{
if (e is not System.Threading.ThreadAbortException)
{
exception = e;
}
}
}, TimeSpan.FromSeconds(30));
if (postResult != null)
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();
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();
}
///
/// Convert a TimeStamp (in second) to DateTime object
///
/// TimeStamp in second
/// DateTime object of the TimeStamp
public static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{
// Unix timestamp is seconds past epoch
DateTime dateTime = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
return dateTime;
}
}
}