mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2025-11-07 17:36:07 +00:00
Refactoring to asynchronous. (partially completed)
This commit is contained in:
parent
7ee08092d4
commit
096ea0c70c
72 changed files with 6033 additions and 5080 deletions
|
|
@ -1,9 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.ServiceModel.Channels;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using MinecraftClient.Protocol.ProfileKey;
|
||||
using MinecraftClient.Scripting;
|
||||
using static MinecraftClient.Settings;
|
||||
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
|
||||
|
||||
|
|
@ -12,58 +22,88 @@ namespace MinecraftClient.Protocol.Session
|
|||
/// <summary>
|
||||
/// Handle sessions caching and storage.
|
||||
/// </summary>
|
||||
public static class SessionCache
|
||||
public static partial 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"
|
||||
);
|
||||
public class Cache
|
||||
{
|
||||
[JsonInclude]
|
||||
public Dictionary<string, SessionToken> SessionTokens = new();
|
||||
|
||||
private static FileMonitor? cachemonitor;
|
||||
private static readonly Dictionary<string, SessionToken> sessions = new();
|
||||
private static readonly Timer updatetimer = new(100);
|
||||
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
|
||||
private static readonly BinaryFormatter formatter = new();
|
||||
[JsonInclude]
|
||||
public Dictionary<string, PlayerKeyPair> ProfileKeys = new();
|
||||
|
||||
[JsonInclude]
|
||||
public Dictionary<string, ServerInfo> ServerKeys = new();
|
||||
|
||||
public record ServerInfo
|
||||
{
|
||||
public ServerInfo(string serverIDhash, byte[] serverPublicKey)
|
||||
{
|
||||
ServerIDhash = serverIDhash;
|
||||
ServerPublicKey = serverPublicKey;
|
||||
}
|
||||
|
||||
public string? ServerIDhash { init; get; }
|
||||
public byte[]? ServerPublicKey { init; get; }
|
||||
}
|
||||
}
|
||||
|
||||
private static Cache cache = new();
|
||||
|
||||
private const string SessionCacheFileJson = "SessionCache.json";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
|
||||
public static async Task ReadCacheSessionAsync()
|
||||
{
|
||||
if (File.Exists(SessionCacheFileJson))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFileJson));
|
||||
|
||||
FileStream fileStream = File.OpenRead(SessionCacheFileJson);
|
||||
|
||||
try
|
||||
{
|
||||
Cache? diskCache = (Cache?)await JsonSerializer.DeserializeAsync(fileStream, typeof(Cache), JsonOptions);
|
||||
|
||||
if (diskCache != null)
|
||||
{
|
||||
cache = diskCache;
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, cache.SessionTokens.Count, cache.ProfileKeys.Count));
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
|
||||
}
|
||||
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve whether SessionCache contains a session for the given login.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>TRUE if session is available</returns>
|
||||
public static bool Contains(string login)
|
||||
public static bool ContainsSession(string login)
|
||||
{
|
||||
return sessions.ContainsKey(login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store a session and save it to disk if required.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <param name="session">User session token used with Minecraft.net</param>
|
||||
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<string, SessionToken>(login, session));
|
||||
}
|
||||
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
|
||||
{
|
||||
SaveToDisk();
|
||||
}
|
||||
return cache.SessionTokens.ContainsKey(login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,202 +111,92 @@ namespace MinecraftClient.Protocol.Session
|
|||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>SessionToken for given login</returns>
|
||||
public static SessionToken Get(string login)
|
||||
public static Tuple<SessionToken?, PlayerKeyPair?> GetSession(string login)
|
||||
{
|
||||
return sessions[login];
|
||||
cache.SessionTokens.TryGetValue(login, out SessionToken? sessionToken);
|
||||
cache.ProfileKeys.TryGetValue(login, out PlayerKeyPair? playerKeyPair);
|
||||
return new(sessionToken, playerKeyPair);
|
||||
}
|
||||
|
||||
public static Cache.ServerInfo? GetServerInfo(string server)
|
||||
{
|
||||
if (cache.ServerKeys.TryGetValue(server, out Cache.ServerInfo? info))
|
||||
return info;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize cache monitoring to keep cache updated with external changes.
|
||||
/// Store a session and save it to disk if required.
|
||||
/// </summary>
|
||||
/// <returns>TRUE if session tokens are seeded from file</returns>
|
||||
public static bool InitializeDiskCache()
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <param name="newSession">User session token used with Minecraft.net</param>
|
||||
public static async Task StoreSessionAsync(string login, SessionToken? sessionToken, PlayerKeyPair? profileKey)
|
||||
{
|
||||
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
|
||||
updatetimer.Elapsed += HandlePending;
|
||||
return LoadFromDisk();
|
||||
if (sessionToken != null)
|
||||
cache.SessionTokens[login] = sessionToken;
|
||||
if (profileKey != null)
|
||||
cache.ProfileKeys[login] = profileKey;
|
||||
|
||||
if (Config.Main.Advanced.SessionCache == CacheType.disk)
|
||||
await SaveToDisk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads cache on external cache file change.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void OnChanged(object sender, FileSystemEventArgs e)
|
||||
public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
updatetimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void HandlePending(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
LoadFromDisk();
|
||||
|
||||
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
|
||||
{
|
||||
Store(pending.Key, pending.Value);
|
||||
pendingadds.Remove(pending);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads cache file and loads SessionTokens into SessionCache.
|
||||
/// </summary>
|
||||
/// <returns>True if data is successfully loaded</returns>
|
||||
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<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
|
||||
foreach (string key in sessionItems.Keys)
|
||||
{
|
||||
if (Guid.TryParseExact(key, "N", out Guid temp))
|
||||
{
|
||||
Dictionary<string, Json.JSONData> 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<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
|
||||
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
|
||||
foreach (KeyValuePair<string, SessionToken> 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;
|
||||
cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves SessionToken's from SessionCache into cache file.
|
||||
/// </summary>
|
||||
private static void SaveToDisk()
|
||||
private static async Task SaveToDisk()
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
|
||||
|
||||
List<string> sessionCacheLines = new()
|
||||
foreach ((string login, SessionToken session) in cache.SessionTokens)
|
||||
{
|
||||
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
|
||||
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
|
||||
};
|
||||
foreach (KeyValuePair<string, SessionToken> entry in sessions)
|
||||
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
|
||||
if (!GetJwtRegex().IsMatch(session.ID))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!ChatBot.IsValidName(session.PlayerName))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!Guid.TryParseExact(session.PlayerID, "N", out _))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!Guid.TryParseExact(session.ClientID, "N", out _))
|
||||
cache.SessionTokens.Remove(login);
|
||||
// No validation on refresh token because it is custom format token (not Jwt)
|
||||
}
|
||||
|
||||
foreach ((string login, PlayerKeyPair profileKey) in cache.ProfileKeys)
|
||||
{
|
||||
if (profileKey.NeedRefresh())
|
||||
cache.ProfileKeys.Remove(login);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
|
||||
FileStream fileStream = File.Open(SessionCacheFileJson, FileMode.Create);
|
||||
|
||||
await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"/* Generated by MCC v{Program.Version} - Keep it secret & Edit at own risk! */{Environment.NewLine}"));
|
||||
|
||||
await JsonSerializer.SerializeAsync(fileStream, cache, typeof(Cache), JsonOptions);
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
fileStream.Close();
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$", RegexOptions.Compiled)]
|
||||
private static partial Regex GetJwtRegex();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue