From 57c53be09f2107b1bbe0da209d9a6a2536d9e512 Mon Sep 17 00:00:00 2001 From: initsuj Date: Wed, 2 Mar 2016 17:11:15 -0700 Subject: [PATCH] caching works. needs documentation and testing --- MinecraftClient/Cache/AuthCacheHandler.cs | 12 -- MinecraftClient/Cache/CacheType.cs | 4 + MinecraftClient/Cache/SessionCache.cs | 139 ++++++++++++++++++++ MinecraftClient/MinecraftClient.csproj | 5 +- MinecraftClient/Program.cs | 83 ++++++++---- MinecraftClient/Protocol/ProtocolHandler.cs | 70 +++++----- MinecraftClient/Protocol/SessionToken.cs | 14 ++ MinecraftClient/Settings.cs | 8 +- 8 files changed, 262 insertions(+), 73 deletions(-) delete mode 100644 MinecraftClient/Cache/AuthCacheHandler.cs create mode 100644 MinecraftClient/Cache/CacheType.cs create mode 100644 MinecraftClient/Cache/SessionCache.cs create mode 100644 MinecraftClient/Protocol/SessionToken.cs diff --git a/MinecraftClient/Cache/AuthCacheHandler.cs b/MinecraftClient/Cache/AuthCacheHandler.cs deleted file mode 100644 index 8b1748b1..00000000 --- a/MinecraftClient/Cache/AuthCacheHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace MinecraftClient.Cache -{ - public static class AuthCacheHandler - { - public enum Type { NONE, MEMORY, DISK }; - } -} diff --git a/MinecraftClient/Cache/CacheType.cs b/MinecraftClient/Cache/CacheType.cs new file mode 100644 index 00000000..9f49cd1c --- /dev/null +++ b/MinecraftClient/Cache/CacheType.cs @@ -0,0 +1,4 @@ +namespace MinecraftClient.Cache +{ + public enum CacheType { NONE, MEMORY, DISK }; +} diff --git a/MinecraftClient/Cache/SessionCache.cs b/MinecraftClient/Cache/SessionCache.cs new file mode 100644 index 00000000..3d83be40 --- /dev/null +++ b/MinecraftClient/Cache/SessionCache.cs @@ -0,0 +1,139 @@ +using MinecraftClient.Protocol; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.Security.Cryptography; +using System.Text; + +namespace MinecraftClient.Cache +{ + public static class SessionCache + { + const string filename = "cache.bin"; + private static Dictionary sessions = new Dictionary(); + private static FileSystemWatcher cachemonitor = new FileSystemWatcher(); + + private static BinaryFormatter formatter = new BinaryFormatter(); + + public static bool Contains(string login) + { + return sessions.ContainsKey(login); + } + + public static void Store(string login, SessionToken session) + { + if (Contains(login)) + { + sessions[login] = session; + } + else + { + sessions.Add(login, session); + } + + if (Settings.CacheType == CacheType.DISK) + { + SaveToDisk(); + } + } + + public static SessionToken Get(string login) + { + return sessions[login]; + } + + public static bool LoadFromDisk() + { + cachemonitor.Path = AppDomain.CurrentDomain.BaseDirectory; + cachemonitor.IncludeSubdirectories = false; + cachemonitor.Filter = filename; + cachemonitor.NotifyFilter = NotifyFilters.LastWrite; + cachemonitor.Changed += new FileSystemEventHandler(OnChanged); + cachemonitor.EnableRaisingEvents = true; + + return ReadCacheFile(); + } + + public static void OnChanged(object source, FileSystemEventArgs e) + { + ReadCacheFile(); + } + + private static bool ReadCacheFile() + { + if (File.Exists(filename)) + { + try + { + using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + sessions = (Dictionary)formatter.Deserialize(fs); + return true; + } + } + catch (IOException ex) + { + Console.WriteLine("Error reading cached sessions from disk: " + ex.Message); + } + catch (SerializationException) + { + Console.WriteLine("Error getting sessions from cache file "); + } + } + return false; + } + + public static void SaveToDisk() + { + bool fileexists = File.Exists(filename); + + using (FileStream fs = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) + { + cachemonitor.EnableRaisingEvents = false; + if (fileexists) + { + fs.SetLength(0); + fs.Flush(); + } + + formatter.Serialize(fs, sessions); + cachemonitor.EnableRaisingEvents = true; + } + + } + + private static byte[] GetHash(FileStream fs, bool resetposition = true) + { + using (var md5 = MD5.Create()) + { + long pos = fs.Position; + byte[] hash = md5.ComputeHash(fs); + + fs.Position = resetposition ? pos : fs.Position; + return hash; + } + } + + private static bool HashesEqual(byte[] hash1, byte[] hash2) + { + if (hash1.Length != hash2.Length) + { + return false; + } + + for (int i = 0; i < hash1.Length; i++) + { + if (hash1[i] != hash2[i]) + { + return false; + } + } + + return true; + } + + } +} diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 538e2066..be23d2ed 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -63,6 +63,7 @@ + @@ -72,7 +73,8 @@ - + + @@ -149,6 +151,7 @@ + diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index bfac6c20..083e0d29 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -85,6 +85,12 @@ namespace MinecraftClient Console.Title = Settings.ExpandVars(Settings.ConsoleTitle); } + //Load cached sessions from disk if necessary + if (Settings.CacheType == Cache.CacheType.DISK) + { + ConsoleIO.WriteLineFormatted(Cache.SessionCache.LoadFromDisk() ? "Cached sessions loaded." : "§8Cached sessions could not be loaded from disk"); + } + //Asking the user to type in missing data such as Username and Password if (Settings.Login == "") @@ -92,58 +98,91 @@ namespace MinecraftClient Console.Write(ConsoleIO.basicIO ? "Please type the username of your choice.\n" : "Username : "); Settings.Login = Console.ReadLine(); } - if (Settings.Password == "") + if (Settings.Password == "" && (Settings.CacheType == Cache.CacheType.NONE || !Cache.SessionCache.Contains(Settings.Login))) { - Console.Write(ConsoleIO.basicIO ? "Please type the password for " + Settings.Login + ".\n" : "Password : "); - Settings.Password = ConsoleIO.basicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); - if (Settings.Password == "") { Settings.Password = "-"; } - if (!ConsoleIO.basicIO) - { - //Hide password length - Console.CursorTop--; Console.Write("Password : <******>"); - for (int i = 19; i < Console.BufferWidth; i++) { Console.Write(' '); } - } + RequestPassword(); } startupargs = args; InitializeClient(); } + /// + /// Reduest user to submit password. + /// + private static void RequestPassword() + { + Console.Write(ConsoleIO.basicIO ? "Please type the password for " + Settings.Login + ".\n" : "Password : "); + Settings.Password = ConsoleIO.basicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); + if (Settings.Password == "") { Settings.Password = "-"; } + if (!ConsoleIO.basicIO) + { + //Hide password length + Console.CursorTop--; Console.Write("Password : <******>"); + for (int i = 19; i < Console.BufferWidth; i++) { Console.Write(' '); } + } + } + /// /// Start a new Client /// private static void InitializeClient() { - ProtocolHandler.LoginResult result; - Settings.Username = Settings.Login; - string sessionID = ""; - string clientID = ""; - string UUID = ""; + SessionToken session = new SessionToken(); + + ProtocolHandler.LoginResult result = ProtocolHandler.LoginResult.LoginRequired; if (Settings.Password == "-") { ConsoleIO.WriteLineFormatted("§8You chose to run in offline mode."); result = ProtocolHandler.LoginResult.Success; - sessionID = "0"; + session.PlayerID = "0"; + session.PlayerName = Settings.Login; } else { - Console.WriteLine("Connecting to Minecraft.net..."); - result = ProtocolHandler.GetLogin(ref Settings.Username, Settings.Password, ref sessionID, ref clientID, ref UUID); + // Validate cached session or login new session. + if (Settings.CacheType != Cache.CacheType.NONE && Cache.SessionCache.Contains(Settings.Login)) + { + session = Cache.SessionCache.Get(Settings.Login); + result = ProtocolHandler.GetTokenValidation(session); + + if (result != ProtocolHandler.LoginResult.Success && Settings.Password == "") + { + RequestPassword(); + } + + Console.WriteLine("Cached session is " + (result == ProtocolHandler.LoginResult.Success ? "valid." : "invalid.")); + + } + + if (result != ProtocolHandler.LoginResult.Success) + { + Console.WriteLine("Connecting to Minecraft.net..."); + result = ProtocolHandler.GetLogin(Settings.Login, Settings.Password, out session); + + if (result == ProtocolHandler.LoginResult.Success && Settings.CacheType != Cache.CacheType.NONE) + { + Cache.SessionCache.Store(Settings.Login, session); + } + } + } if (result == ProtocolHandler.LoginResult.Success) { + Settings.Username = session.PlayerName; + if (Settings.ConsoleTitle != "") Console.Title = Settings.ExpandVars(Settings.ConsoleTitle); if (Settings.playerHeadAsIcon) ConsoleIcon.setPlayerIconAsync(Settings.Username); - Console.WriteLine("Success. (session ID: " + sessionID + ')'); + Console.WriteLine("Success. (session ID: " + session.ID + ')'); - //ProtocolHandler.RealmsListWorlds(Settings.Username, UUID, sessionID); //TODO REMOVE + //ProtocolHandler.RealmsListWorlds(Settings.Username, PlayerID, sessionID); //TODO REMOVE if (Settings.ServerIP == "") { @@ -194,9 +233,9 @@ namespace MinecraftClient //Start the main TCP client if (Settings.SingleCommand != "") { - Client = new McTcpClient(Settings.Username, UUID, sessionID, Settings.ServerIP, Settings.ServerPort, protocolversion, forgeInfo, Settings.SingleCommand); + Client = new McTcpClient(session.PlayerName, session.PlayerID, session.ID, Settings.ServerIP, Settings.ServerPort, protocolversion, forgeInfo, Settings.SingleCommand); } - else Client = new McTcpClient(Settings.Username, UUID, sessionID, protocolversion, forgeInfo, Settings.ServerIP, Settings.ServerPort); + else Client = new McTcpClient(session.PlayerName, session.PlayerID, session.ID, protocolversion, forgeInfo, Settings.ServerIP, Settings.ServerPort); //Update console title if (Settings.ConsoleTitle != "") diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index e3e43016..6fa2a5e4 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -141,7 +141,7 @@ namespace MinecraftClient.Protocol } } - public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium }; + 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. @@ -150,19 +150,18 @@ namespace MinecraftClient.Protocol /// 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 UUID, needed for multiplayer + /// Will contain the player's PlayerID, needed for multiplayer /// Returns the status of the login (Success, Failure, etc.) - public static LoginResult GetLogin(ref string user, string pass, ref string accesstoken, ref string clienttoken, ref string uuid) + public static LoginResult GetLogin(string user, string pass, out SessionToken session) { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + try { - string result = ""; - if (clienttoken == string.Empty) - { - clienttoken = Guid.NewGuid().ToString().Replace("-",""); - } - string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" + jsonEncode(user) + "\", \"password\": \"" + jsonEncode(pass) + "\", \"clientToken\": \"" + jsonEncode(clienttoken) + "\" }"; + 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) { @@ -173,11 +172,11 @@ namespace MinecraftClient.Protocol else { string[] temp = result.Split(new string[] { "accessToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { accesstoken = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { session.ID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "name\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { user = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { session.PlayerName = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "availableProfiles\":[{\"id\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { uuid = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { session.PlayerID = temp[1].Split('"')[0]; } return LoginResult.Success; } } @@ -226,33 +225,33 @@ namespace MinecraftClient.Protocol /// Will contain the cached client token created on login /// Returns the status of the token (Valid, Invalid, etc.) /// - public static ValidationResult GetTokenValidation(string accesstoken, string clienttoken) + public static LoginResult GetTokenValidation(SessionToken session) { try { string result = ""; - string json_request = "{\"accessToken\": \"" + jsonEncode(accesstoken) + "\", \"clientToken\": \"" + jsonEncode(clienttoken) + "\" }"; + 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 ValidationResult.Validated; + return LoginResult.Success; } else if (code == 403) { - return ValidationResult.NewTokenRequired; + return LoginResult.LoginRequired; } else { - return ValidationResult.Error; + return LoginResult.OtherError; } } catch { - return ValidationResult.Error; + return LoginResult.OtherError; } } - public enum NewTokenResult { Success, InvalidToken, NullError, OtherError} + public enum NewTokenResult { Success, InvalidToken, NullError, OtherError } /// /// Refreshes invalid token @@ -260,45 +259,48 @@ namespace MinecraftClient.Protocol /// 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 UUID, needed for multiplayer + /// Will contain the player's PlayerID, needed for multiplayer /// Returns the status of the new token request (Success, Failure, etc.) /// - public static NewTokenResult GetNewToken(ref string user, ref string accesstoken, ref string clienttoken, ref string uuid) + public static LoginResult GetNewToken(SessionToken currentsession, out SessionToken newsession) { + newsession = new SessionToken(); try { string result = ""; - string json_request = "{ \"accessToken\": \"" + jsonEncode(accesstoken) + "\", \"clientToken\": \"" + jsonEncode(clienttoken) + "\", \"selectedProfile\": { \"id\": \"" + jsonEncode(uuid) + "\", \"name\": \"" + jsonEncode(user) + "\" } }"; + 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 NewTokenResult.NullError; - }else{ + if (result == null) + { + return LoginResult.NullError; + } + else { string[] temp = result.Split(new string[] { "accessToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { accesstoken = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { newsession.ID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "clientToken\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { clienttoken = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { newsession.ClientID = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "name\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { user = temp[1].Split('"')[0]; } + if (temp.Length >= 2) { newsession.PlayerName = temp[1].Split('"')[0]; } temp = result.Split(new string[] { "selectedProfile\":[{\"id\":\"" }, StringSplitOptions.RemoveEmptyEntries); - if (temp.Length >= 2) { uuid = temp[1].Split('"')[0]; } - return NewTokenResult.Success; + if (temp.Length >= 2) { newsession.PlayerID = temp[1].Split('"')[0]; } + return LoginResult.Success; } } - else if(code == 403 && result.Contains("InvalidToken")) + else if (code == 403 && result.Contains("InvalidToken")) { - return NewTokenResult.InvalidToken; + return LoginResult.InvalidToken; } else { ConsoleIO.WriteLineFormatted("§8Got error code from server while refreshing authentication: " + code); - return NewTokenResult.OtherError; + return LoginResult.OtherError; } } catch { - return NewTokenResult.OtherError; + return LoginResult.OtherError; } } diff --git a/MinecraftClient/Protocol/SessionToken.cs b/MinecraftClient/Protocol/SessionToken.cs new file mode 100644 index 00000000..2d48a704 --- /dev/null +++ b/MinecraftClient/Protocol/SessionToken.cs @@ -0,0 +1,14 @@ +using System; + + +namespace MinecraftClient.Protocol +{ + [Serializable] + public class SessionToken + { + public string ID { get; set; } = string.Empty; + public string PlayerName { get; set; } = string.Empty; + public string PlayerID { get; set; } = string.Empty; + public string ClientID { get; set; } = string.Empty; + } +} diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 51972956..0d86cdf2 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -30,7 +30,7 @@ namespace MinecraftClient public static string ConsoleTitle = ""; //Cache Settings - public static Cache.AuthCacheHandler.Type CacheType = Cache.AuthCacheHandler.Type.NONE; + public static Cache.CacheType CacheType = Cache.CacheType.NONE; //Proxy Settings public static bool ProxyEnabledLogin = false; @@ -204,9 +204,9 @@ namespace MinecraftClient break; case "accountcache": - if(argValue == "none") { CacheType = Cache.AuthCacheHandler.Type.NONE; } - else if(argValue == "memory") { CacheType = Cache.AuthCacheHandler.Type.MEMORY; } - else if(argValue == "disk") { CacheType = Cache.AuthCacheHandler.Type.DISK; } + if (argValue == "none") { CacheType = Cache.CacheType.NONE; } + else if (argValue == "memory") { CacheType = Cache.CacheType.MEMORY; } + else if (argValue == "disk") { CacheType = Cache.CacheType.DISK; } break; case "accountlist":