using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using HarmonyLib; using SPTarkov.Reflection.Patching; using SPTarkov.Server.Core.Routers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Launcher; using SPTarkov.Server.Core.Callbacks; using SPTarkov.Server.Core.Controllers; using System.Threading; namespace PersonalAuthMod; /// /// AsyncLocal to pass the password from the patch to the router /// public static class AuthContext { private static readonly AsyncLocal _currentPassword = new(); private static readonly AsyncLocal _currentSessionID = new(); public static string? CurrentPassword { get => _currentPassword.Value; set => _currentPassword.Value = value; } public static string? CurrentSessionID { get => _currentSessionID.Value; set => _currentSessionID.Value = value; } } /// /// Patch HttpRouter.HandleRoute to capture session ID and extract password globally. /// public class HttpRouterHandleRoutePatch : AbstractPatch { protected override MethodBase GetTargetMethod() { return typeof(HttpRouter).GetMethod("HandleRoute", BindingFlags.NonPublic | BindingFlags.Instance)!; } [PatchPrefix] public static void Prefix(MongoId sessionID, ref string? body) { // Capture the session ID for other patches (like profile filtering) AuthContext.CurrentSessionID = sessionID.ToString(); if (string.IsNullOrEmpty(body)) { return; } if (!body.Contains("\"password\"")) { return; } try { var node = JsonNode.Parse(body); if (node is JsonObject obj && obj.TryGetPropertyValue("password", out var passwordNode)) { AuthContext.CurrentPassword = passwordNode?.GetValue(); obj.Remove("password"); body = obj.ToJsonString(); } } catch { } } } /// /// Patch ProfileController.GetMiniProfiles to filter the list based on the authenticated user. /// public class ProfileControllerGetMiniProfilesPatch : AbstractPatch { protected override MethodBase GetTargetMethod() { return typeof(ProfileController).GetMethod(nameof(ProfileController.GetMiniProfiles))!; } [PatchPostfix] public static void Postfix(ref List __result) { var sessionID = AuthContext.CurrentSessionID; if (string.IsNullOrEmpty(sessionID) || __result == null) { return; } var dbManager = PersonalAuthMod.Instance?.DbManager; if (dbManager == null || !dbManager.ValidateSession(sessionID)) { // If session is invalid, return empty list (isolation) __result = new List(); return; } var username = dbManager.GetUsernameBySession(sessionID); if (string.IsNullOrEmpty(username)) { __result = new List(); return; } // Filter the list to only include the user's own profile int before = __result.Count; __result = __result.Where(p => p.Username == username).ToList(); if (before != __result.Count) { Console.WriteLine($"[PersonalAuthMod] Isolated profiles for {username}: {before} -> {__result.Count}"); } } } /// /// Patch LauncherCallbacks.Login to enforce database authentication. /// public class LauncherCallbacksLoginPatch : AbstractPatch { protected override MethodBase GetTargetMethod() { return typeof(LauncherCallbacks).GetMethod(nameof(LauncherCallbacks.Login))!; } [PatchPrefix] public static bool Prefix(string url, LoginRequestData info, MongoId sessionID, ref ValueTask __result) { var password = AuthContext.CurrentPassword; Console.WriteLine($"[PersonalAuthMod] Login Patch - User: {info.Username}, Password provided? {!string.IsNullOrEmpty(password)}"); if (string.IsNullOrWhiteSpace(info.Username) || string.IsNullOrWhiteSpace(password)) { __result = new ValueTask("FAILED"); return false; // Skip original method } var sessionId = PersonalAuthMod.Instance?.DbManager.LoginUser(info.Username, password); if (sessionId == null) { Console.WriteLine($"[PersonalAuthMod] Login FAILED for user: {info.Username}"); __result = new ValueTask("FAILED"); return false; // Skip original method } Console.WriteLine($"[PersonalAuthMod] Login SUCCESS for user: {info.Username}, Session: {sessionId}"); __result = new ValueTask(sessionId); return false; // Skip original method } } /// /// Patch LauncherCallbacks.Register to enforce database registration. /// public class LauncherCallbacksRegisterPatch : AbstractPatch { protected override MethodBase GetTargetMethod() { return typeof(LauncherCallbacks).GetMethod(nameof(LauncherCallbacks.Register))!; } [PatchPrefix] public static bool Prefix(string url, RegisterData info, MongoId sessionID, ref ValueTask __result) { var password = AuthContext.CurrentPassword; Console.WriteLine($"[PersonalAuthMod] Register Patch - User: {info.Username}, Password provided? {!string.IsNullOrEmpty(password)}"); if (string.IsNullOrWhiteSpace(info.Username) || string.IsNullOrWhiteSpace(password)) { __result = new ValueTask("FAILED"); return false; // Skip original method } if (PersonalAuthMod.Instance?.DbManager.RegisterUser(info.Username, password) == false) { Console.WriteLine($"[PersonalAuthMod] Register FAILED for user: {info.Username} (Already exists or DB error)"); __result = new ValueTask("FAILED"); return false; // Skip original method } Console.WriteLine($"[PersonalAuthMod] Register SUCCESS for user: {info.Username}"); // Allow original method to run (it will create the local profile) return true; } }