using MinecraftClient.Protocol; using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Timers; 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 Dictionary sessions = new Dictionary(); private static Timer updatetimer = new Timer(100); private static List> pendingadds = new List>(); private static BinaryFormatter formatter = new BinaryFormatter(); /// /// 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 (Settings.SessionCaching == CacheType.Disk && updatetimer.Enabled == true) { pendingadds.Add(new KeyValuePair(login, session)); } else if (Settings.SessionCaching == 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 (Settings.DebugMessages) ConsoleIO.WriteLineFormatted(Translations.Get("cache.loading", Path.GetFileName(SessionCacheFileMinecraft))); Json.JSONData mcSession = new Json.JSONData(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")) { Guid temp; 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 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(Translations.Get("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 (Settings.DebugMessages) ConsoleIO.WriteLineFormatted(Translations.Get("cache.converting", 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(Translations.Get("cache.loaded", item.Key, item.Value.ID)); sessions[item.Key] = item.Value; } } } catch (IOException ex) { ConsoleIO.WriteLineFormatted(Translations.Get("cache.read_fail", ex.Message)); } catch (SerializationException ex2) { ConsoleIO.WriteLineFormatted(Translations.Get("cache.malformed", ex2.Message)); } } //User-editable session cache file in text format if (File.Exists(SessionCacheFilePlaintext)) { if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted(Translations.Get("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 = keyValue[0].ToLower(); SessionToken session = SessionToken.FromString(keyValue[1]); if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted(Translations.Get("cache.loaded", login, session.ID)); sessions[login] = session; } catch (InvalidDataException e) { if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted(Translations.Get("cache.ignore_string", keyValue[1], e.Message)); } } else if (Settings.DebugMessages) { ConsoleIO.WriteLineFormatted(Translations.Get("cache.ignore_line", line)); } } } } catch (IOException e) { ConsoleIO.WriteLineFormatted(Translations.Get("cache.read_fail_plain", e.Message)); } } return sessions.Count > 0; } /// /// Saves SessionToken's from SessionCache into cache file. /// private static void SaveToDisk() { if (Settings.DebugMessages) Translations.WriteLineFormatted("cache.saving"); List sessionCacheLines = new List(); sessionCacheLines.Add("# Generated by MCC v" + Program.Version + " - Edit at own risk!"); sessionCacheLines.Add("# Login=SessionID,PlayerName,UUID,ClientID"); foreach (KeyValuePair entry in sessions) sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); try { FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines); } catch (IOException e) { ConsoleIO.WriteLineFormatted(Translations.Get("cache.save_fail", e.Message)); } } } }