using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Timers; using static MinecraftClient.Settings; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; namespace MinecraftClient.Protocol.Session { /// /// Handle sessions caching and storage. /// public static class SessionCache { 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 FileMonitor? cachemonitor; private static readonly Dictionary sessions = new(); private static readonly Timer updatetimer = new(100); private static readonly List> pendingadds = new(); private static readonly BinaryFormatter formatter = new(); /// /// Retrieve whether SessionCache contains a session for the given login. /// /// User login used with Minecraft.net /// TRUE if session is available public static bool Contains(string login) { return sessions.ContainsKey(login); } /// /// Store a session and save it to disk if required. /// /// User login used with Minecraft.net /// User session token used with Minecraft.net public static void Store(string login, SessionToken session) { if (Contains(login)) { sessions[login] = session; } else { sessions.Add(login, session); } if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true) { pendingadds.Add(new KeyValuePair(login, session)); } else if (Config.Main.Advanced.SessionCache == CacheType.disk) { SaveToDisk(); } } /// /// Retrieve a session token for the given login. /// /// User login used with Minecraft.net /// SessionToken for given login public static SessionToken Get(string login) { return sessions[login]; } /// /// Initialize cache monitoring to keep cache updated with external changes. /// /// TRUE if session tokens are seeded from file public static bool InitializeDiskCache() { cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged)); updatetimer.Elapsed += HandlePending; return LoadFromDisk(); } /// /// Reloads cache on external cache file change. /// /// Sender /// Event data private static void OnChanged(object sender, FileSystemEventArgs e) { updatetimer.Stop(); updatetimer.Start(); } /// /// Called after timer elapsed. Reads disk cache and adds new/modified sessions back. /// /// Sender /// Event data private static void HandlePending(object? sender, ElapsedEventArgs e) { updatetimer.Stop(); LoadFromDisk(); foreach (KeyValuePair pending in pendingadds.ToArray()) { Store(pending.Key, pending.Value); pendingadds.Remove(pending); } } /// /// Reads cache file and loads SessionTokens into SessionCache. /// /// True if data is successfully loaded private static bool LoadFromDisk() { //Grab sessions in the Minecraft directory if (File.Exists(SessionCacheFileMinecraft)) { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft))); Json.JSONData mcSession = new(Json.JSONData.DataType.String); try { 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")) { string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", ""); Dictionary sessionItems = mcSession.Properties["authenticationDatabase"].Properties; foreach (string key in sessionItems.Keys) { if (Guid.TryParseExact(key, "N", out Guid temp)) { Dictionary sessionItem = sessionItems[key].Properties; if (sessionItem.ContainsKey("displayName") && sessionItem.ContainsKey("accessToken") && sessionItem.ContainsKey("username") && sessionItem.ContainsKey("uuid")) { string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue); try { SessionToken session = SessionToken.FromString(String.Join(",", sessionItem["accessToken"].StringValue, sessionItem["displayName"].StringValue, sessionItem["uuid"].StringValue.Replace("-", ""), clientID )); if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID)); sessions[login] = session; } catch (InvalidDataException) { /* Not a valid session */ } } } } } } //Serialized session cache file in binary format if (File.Exists(SessionCacheFileSerialized)) { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized)); try { using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read); #pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete // Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only. Dictionary sessionsTemp = (Dictionary)formatter.Deserialize(fs); #pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete foreach (KeyValuePair item in sessionsTemp) { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID)); sessions[item.Key] = item.Value; } } catch (IOException ex) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message)); } catch (SerializationException ex2) { ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message)); } } //User-editable session cache file in text format if (File.Exists(SessionCacheFilePlaintext)) { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext)); try { foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext)) { if (!line.Trim().StartsWith("#")) { string[] keyValue = line.Split('='); if (keyValue.Length == 2) { try { string login = Settings.ToLowerIfNeed(keyValue[0]); SessionToken session = SessionToken.FromString(keyValue[1]); if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID)); sessions[login] = session; } catch (InvalidDataException e) { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message)); } } else if (Config.Logging.DebugMessages) { ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line)); } } } } catch (IOException e) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message)); } } return sessions.Count > 0; } /// /// Saves SessionToken's from SessionCache into cache file. /// private static void SaveToDisk() { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true); List sessionCacheLines = new() { "# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!", "# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey" }; foreach (KeyValuePair entry in sessions) sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); try { FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines); } catch (IOException e) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); } } } }