From 18fd24d2d50c27e41472cf87462da9f1b3afb080 Mon Sep 17 00:00:00 2001 From: ORelio Date: Thu, 3 May 2018 23:51:56 +0200 Subject: [PATCH] Improve session caching - Change SessionCache.db to SessionCache.ini Allows users to view and edit session cache - Automatically import previous SessionCache.db But this file is only read, not updated - Automatically import Minecraft session If you are logged in in Minecraft, no need to login again This is only done if Disk session cache is enabled See #232 and #430 for more information - Disk session cache becomes default The feature is no longer experimental and now recommended as the Mojang login servers now have a severe rate limit Previous default was Memory session cache, not saved to disk --- MinecraftClient/Program.cs | 4 +- MinecraftClient/Protocol/ProtocolHandler.cs | 5 +- .../Protocol/SessionCache/SessionCache.cs | 142 +++++++++++++++--- MinecraftClient/Protocol/SessionToken.cs | 32 ++++ MinecraftClient/Settings.cs | 4 +- 5 files changed, 155 insertions(+), 32 deletions(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 4da8f2d2..42b559a2 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -12,7 +12,7 @@ using MinecraftClient.WinAPI; namespace MinecraftClient { /// - /// Minecraft Console Client by ORelio and Contributors (c) 2012-2017. + /// Minecraft Console Client by ORelio and Contributors (c) 2012-2018. /// Allows to connect to any Minecraft server, send and receive text, automated scripts. /// This source code is released under the CDDL 1.0 License. /// @@ -102,7 +102,7 @@ namespace MinecraftClient { bool cacheLoaded = SessionCache.InitializeDiskCache(); if (Settings.DebugMessages) - ConsoleIO.WriteLineFormatted(cacheLoaded ? "§8Session cache has been successfully loaded from disk." : "§8Cached sessions could not be loaded from disk"); + ConsoleIO.WriteLineFormatted(cacheLoaded ? "§8Session data has been successfully loaded from disk." : "§8No sessions could be loaded from disk"); } //Asking the user to type in missing data such as Username and Password diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 42990604..6e3ea218 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -8,7 +8,6 @@ using System.Net.Sockets; using System.Net.Security; using MinecraftClient.Protocol.Handlers.Forge; - namespace MinecraftClient.Protocol { /// @@ -229,10 +228,10 @@ namespace MinecraftClient.Protocol if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted("§8Debug: Login Request: " + json_request); int code = DoHTTPSPost("authserver.mojang.com", "/authenticate", json_request, ref result); + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Debug: Login Response: " + result); if (code == 200) { - if (Settings.DebugMessages) - ConsoleIO.WriteLineFormatted("§8Debug: Login Response: " + result); if (result.Contains("availableProfiles\":[]}")) { return LoginResult.NotPremium; diff --git a/MinecraftClient/Protocol/SessionCache/SessionCache.cs b/MinecraftClient/Protocol/SessionCache/SessionCache.cs index 7e76c0f0..474cae06 100644 --- a/MinecraftClient/Protocol/SessionCache/SessionCache.cs +++ b/MinecraftClient/Protocol/SessionCache/SessionCache.cs @@ -13,7 +13,16 @@ namespace MinecraftClient.Protocol.SessionCache /// public static class SessionCache { - private const string SessionCacheFile = "SessionCache.db"; + private const string SessionCacheFilePlaintext = "SessionCache.ini"; + private const string SessionCacheFileSerialized = "SessionCache.db"; + private static readonly string SessionCacheFileMinecraft = String.Concat( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Path.DirectorySeparatorChar, + ".minecraft", + Path.DirectorySeparatorChar, + "launcher_profiles.json" + ); + private static Dictionary sessions = new Dictionary(); private static FileSystemWatcher cachemonitor = new FileSystemWatcher(); private static Timer updatetimer = new Timer(100); @@ -74,7 +83,7 @@ namespace MinecraftClient.Protocol.SessionCache { cachemonitor.Path = AppDomain.CurrentDomain.BaseDirectory; cachemonitor.IncludeSubdirectories = false; - cachemonitor.Filter = SessionCacheFile; + cachemonitor.Filter = SessionCacheFilePlaintext; cachemonitor.NotifyFilter = NotifyFilters.LastWrite; cachemonitor.Changed += new FileSystemEventHandler(OnChanged); cachemonitor.EnableRaisingEvents = true; @@ -118,17 +127,71 @@ namespace MinecraftClient.Protocol.SessionCache /// True if data is successfully loaded private static bool LoadFromDisk() { - if (Settings.DebugMessages) - ConsoleIO.WriteLineFormatted("§8Updating session cache from disk"); - - if (File.Exists(SessionCacheFile)) + //Grab sessions in the Minecraft directory + if (File.Exists(SessionCacheFileMinecraft)) { + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Loading Minecraft profiles: " + Path.GetFileName(SessionCacheFileMinecraft)); + Json.JSONData mcSession = new Json.JSONData(Json.JSONData.DataType.String); try { - using (FileStream fs = new FileStream(SessionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read)) + mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft)); + } + catch (IOException) { /* Failed to read file from disk -- ignoring */ } + if (mcSession.Type == Json.JSONData.DataType.Object + && mcSession.Properties.ContainsKey("clientToken") + && mcSession.Properties.ContainsKey("authenticationDatabase")) + { + Guid temp; + string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", ""); + Dictionary sessionItems = mcSession.Properties["authenticationDatabase"].Properties; + foreach (string key in sessionItems.Keys) { - sessions = (Dictionary)formatter.Deserialize(fs); - return true; + if (Guid.TryParseExact(key, "N", out temp)) + { + Dictionary sessionItem = sessionItems[key].Properties; + if (sessionItem.ContainsKey("displayName") + && sessionItem.ContainsKey("accessToken") + && sessionItem.ContainsKey("username") + && sessionItem.ContainsKey("uuid")) + { + string login = sessionItem["username"].StringValue.ToLower(); + try + { + SessionToken session = SessionToken.FromString(String.Join(",", + sessionItem["accessToken"].StringValue, + sessionItem["displayName"].StringValue, + sessionItem["uuid"].StringValue.Replace("-", ""), + clientID + )); + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Loaded session: " + login + ':' + session.ID); + sessions[login] = session; + } + catch (InvalidDataException) { /* Not a valid session */ } + } + } + } + } + } + + //Serialized session cache file in binary format + if (File.Exists(SessionCacheFileSerialized)) + { + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Converting session cache from disk: " + SessionCacheFileSerialized); + + try + { + using (FileStream fs = new FileStream(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + Dictionary sessionsTemp = (Dictionary)formatter.Deserialize(fs); + foreach (KeyValuePair item in sessionsTemp) + { + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Loaded session: " + item.Key + ':' + item.Value.ID); + sessions[item.Key] = item.Value; + } } } catch (IOException ex) @@ -140,7 +203,43 @@ namespace MinecraftClient.Protocol.SessionCache ConsoleIO.WriteLineFormatted("§8Got malformed data while reading session cache from disk: " + ex2.Message); } } - return false; + + //User-editable session cache file in text format + if (File.Exists(SessionCacheFilePlaintext)) + { + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Loading session cache from disk: " + SessionCacheFilePlaintext); + + foreach (string line in File.ReadAllLines(SessionCacheFilePlaintext)) + { + if (!line.Trim().StartsWith("#")) + { + string[] keyValue = line.Split('='); + if (keyValue.Length == 2) + { + try + { + string login = keyValue[0].ToLower(); + SessionToken session = SessionToken.FromString(keyValue[1]); + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Loaded session: " + login + ':' + session.ID); + sessions[login] = session; + } + catch (InvalidDataException e) + { + if (Settings.DebugMessages) + ConsoleIO.WriteLineFormatted("§8Ignoring session token string '" + keyValue[1] + "': " + e.Message); + } + } + else if (Settings.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8Ignoring invalid session token line: " + line); + } + } + } + } + + return sessions.Count > 0; } /// @@ -151,7 +250,7 @@ namespace MinecraftClient.Protocol.SessionCache if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted("§8Saving session cache to disk"); - bool fileexists = File.Exists(SessionCacheFile); + bool fileexists = File.Exists(SessionCacheFilePlaintext); IOException lastEx = null; int attempt = 1; @@ -159,20 +258,13 @@ namespace MinecraftClient.Protocol.SessionCache { try { - using (FileStream fs = new FileStream(SessionCacheFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) - { - cachemonitor.EnableRaisingEvents = false; - - // delete existing file contents - if (fileexists) - { - fs.SetLength(0); - fs.Flush(); - } - - formatter.Serialize(fs, sessions); - cachemonitor.EnableRaisingEvents = true; - } + List sessionCacheLines = new List(); + sessionCacheLines.Add("# Generated by MCC v" + Program.Version + " - Edit at own risk!"); + foreach (KeyValuePair entry in sessions) + sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); + File.WriteAllLines(SessionCacheFilePlaintext, sessionCacheLines); + //if (File.Exists(SessionCacheFileSerialized)) + // File.Delete(SessionCacheFileSerialized); return; } catch (IOException ex) diff --git a/MinecraftClient/Protocol/SessionToken.cs b/MinecraftClient/Protocol/SessionToken.cs index d00824f9..e7b52f64 100644 --- a/MinecraftClient/Protocol/SessionToken.cs +++ b/MinecraftClient/Protocol/SessionToken.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; namespace MinecraftClient.Protocol { @@ -17,5 +19,35 @@ namespace MinecraftClient.Protocol PlayerID = String.Empty; ClientID = String.Empty; } + + public override string ToString() + { + return String.Join(",", ID, PlayerName, PlayerID, ClientID); + } + + public static SessionToken FromString(string tokenString) + { + string[] fields = tokenString.Split(','); + if (fields.Length < 4) + throw new InvalidDataException("Invalid string format"); + + SessionToken session = new SessionToken(); + session.ID = fields[0]; + session.PlayerName = fields[1]; + session.PlayerID = fields[2]; + session.ClientID = fields[3]; + + Guid temp; + if (!Guid.TryParseExact(session.ID, "N", out temp)) + throw new InvalidDataException("Invalid session ID"); + if (!ChatBot.IsValidName(session.PlayerName)) + throw new InvalidDataException("Invalid player name"); + if (!Guid.TryParseExact(session.PlayerID, "N", out temp)) + throw new InvalidDataException("Invalid player ID"); + if (!Guid.TryParseExact(session.ClientID, "N", out temp)) + throw new InvalidDataException("Invalid client ID"); + + return session; + } } } diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 18ec4b22..31ae1b08 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -91,7 +91,7 @@ namespace MinecraftClient public static bool DisplayChatLinks = true; public static bool TerrainAndMovements = false; public static string PrivateMsgsCmdName = "tell"; - public static CacheType SessionCaching = CacheType.None; + public static CacheType SessionCaching = CacheType.Disk; public static bool DebugMessages = false; public static bool ResolveSrvRecords = true; public static bool ResolveSrvRecordsShortTimeout = true; @@ -539,7 +539,7 @@ namespace MinecraftClient + "showxpbarmessages=true # Messages displayed above xp bar\r\n" + "showchatlinks=true # Show links embedded in chat messages\r\n" + "terrainandmovements=false # Uses more ram, cpu, bandwidth\r\n" - + "sessioncache=memory # Use 'none', 'memory' or 'disk' (disk session storing is experimental)\r\n" + + "sessioncache=disk # How to retain session tokens. Use 'none', 'memory' or 'disk'\r\n" + "resolvesrvrecords=fast # Use 'false', 'fast' (5s timeout), or 'true'. Required for joining some servers.\r\n" + "accountlist=accounts.txt # See README > 'Servers and Accounts file' for more info about this file\r\n" + "serverlist=servers.txt # See README > 'Servers and Accounts file' for more info about this file\r\n"