193 lines
6.3 KiB
C#
193 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// AsyncLocal to pass the password from the patch to the router
|
|
/// </summary>
|
|
public static class AuthContext
|
|
{
|
|
private static readonly AsyncLocal<string?> _currentPassword = new();
|
|
private static readonly AsyncLocal<string?> _currentSessionID = new();
|
|
|
|
public static string? CurrentPassword
|
|
{
|
|
get => _currentPassword.Value;
|
|
set => _currentPassword.Value = value;
|
|
}
|
|
|
|
public static string? CurrentSessionID
|
|
{
|
|
get => _currentSessionID.Value;
|
|
set => _currentSessionID.Value = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Patch HttpRouter.HandleRoute to capture session ID and extract password globally.
|
|
/// </summary>
|
|
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<string>();
|
|
obj.Remove("password");
|
|
body = obj.ToJsonString();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Patch ProfileController.GetMiniProfiles to filter the list based on the authenticated user.
|
|
/// </summary>
|
|
public class ProfileControllerGetMiniProfilesPatch : AbstractPatch
|
|
{
|
|
protected override MethodBase GetTargetMethod()
|
|
{
|
|
return typeof(ProfileController).GetMethod(nameof(ProfileController.GetMiniProfiles))!;
|
|
}
|
|
|
|
[PatchPostfix]
|
|
public static void Postfix(ref List<MiniProfile> __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<MiniProfile>();
|
|
return;
|
|
}
|
|
|
|
var username = dbManager.GetUsernameBySession(sessionID);
|
|
if (string.IsNullOrEmpty(username))
|
|
{
|
|
__result = new List<MiniProfile>();
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Patch LauncherCallbacks.Login to enforce database authentication.
|
|
/// </summary>
|
|
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<string> __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<string>("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<string>("FAILED");
|
|
return false; // Skip original method
|
|
}
|
|
|
|
Console.WriteLine($"[PersonalAuthMod] Login SUCCESS for user: {info.Username}, Session: {sessionId}");
|
|
__result = new ValueTask<string>(sessionId);
|
|
return false; // Skip original method
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Patch LauncherCallbacks.Register to enforce database registration.
|
|
/// </summary>
|
|
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<string> __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<string>("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<string>("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;
|
|
}
|
|
}
|