mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 03:03:58 +00:00
MikuSB.Loader
This commit is contained in:
@@ -9,6 +9,7 @@ public class ConfigContainer
|
|||||||
public PathConfig Path { get; set; } = new();
|
public PathConfig Path { get; set; } = new();
|
||||||
public ServerOption ServerOption { get; set; } = new();
|
public ServerOption ServerOption { get; set; } = new();
|
||||||
public ProxyOptions Proxy { get; set; } = new();
|
public ProxyOptions Proxy { get; set; } = new();
|
||||||
|
public LoaderOptions Loader { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HttpServerConfig
|
public class HttpServerConfig
|
||||||
@@ -88,8 +89,12 @@ public class ProxyOptions
|
|||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
public int Port { get; set; } = 18888;
|
public int Port { get; set; } = 18888;
|
||||||
public int ServerHttpPort { get; set; } = 21500;
|
public int ServerHttpPort { get; set; } = 21500;
|
||||||
public bool InstallRootCertificate { get; set; } = false;
|
}
|
||||||
public bool ManageSystemProxy { get; set; } = false;
|
|
||||||
public bool RestoreSystemProxyOnStop { get; set; } = false;
|
public class LoaderOptions
|
||||||
public string ProxyOverride { get; set; } = "localhost;127.*;10.*;192.168.*;<local>";
|
{
|
||||||
|
public string GamePath { get; set; } = "";
|
||||||
|
public string[] PatchPaths { get; set; } = [@"Patch\MikuSB-Patch.dll"];
|
||||||
|
public string[] Arguments { get; set; } = ["-FeatureLevelES31", "-channelid=seasun", "-NoSplash"];
|
||||||
|
public bool SetAllProxy { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ public class CommandTextCHS
|
|||||||
public GirlTextCHS Girl { get; } = new();
|
public GirlTextCHS Girl { get; } = new();
|
||||||
public GiveAllTextCHS GiveAll { get; } = new();
|
public GiveAllTextCHS GiveAll { get; } = new();
|
||||||
public DebugTextCHS Debug { get; } = new();
|
public DebugTextCHS Debug { get; } = new();
|
||||||
|
public GameCommandTextCHS Game { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -272,6 +273,14 @@ public class DebugTextCHS
|
|||||||
public string FileDisabled => "个人调试文件输出已禁用。";
|
public string FileDisabled => "个人调试文件输出已禁用。";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class GameCommandTextCHS
|
||||||
|
{
|
||||||
|
public string Desc => "使用补丁注入启动已配置的游戏";
|
||||||
|
public string Usage => "用法: /game [额外游戏参数]";
|
||||||
|
public string Started => "游戏已启动。PID: {0}";
|
||||||
|
public string Failed => "游戏启动失败: {0}";
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ public class CommandTextCHT
|
|||||||
public GirlTextCHT Girl { get; } = new();
|
public GirlTextCHT Girl { get; } = new();
|
||||||
public GiveAllTextCHT GiveAll { get; } = new();
|
public GiveAllTextCHT GiveAll { get; } = new();
|
||||||
public DebugTextCHT Debug { get; } = new();
|
public DebugTextCHT Debug { get; } = new();
|
||||||
|
public GameCommandTextCHT Game { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -272,6 +273,14 @@ public class DebugTextCHT
|
|||||||
public string FileDisabled => "個人調試檔案輸出已停用。";
|
public string FileDisabled => "個人調試檔案輸出已停用。";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class GameCommandTextCHT
|
||||||
|
{
|
||||||
|
public string Desc => "使用補丁注入啟動已配置的遊戲";
|
||||||
|
public string Usage => "用法: /game [額外遊戲參數]";
|
||||||
|
public string Started => "遊戲已啟動。PID: {0}";
|
||||||
|
public string Failed => "遊戲啟動失敗: {0}";
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ public class CommandTextEN
|
|||||||
public GirlTextEN Girl { get; } = new();
|
public GirlTextEN Girl { get; } = new();
|
||||||
public GiveAllTextEN GiveAll { get; } = new();
|
public GiveAllTextEN GiveAll { get; } = new();
|
||||||
public DebugTextEN Debug { get; } = new();
|
public DebugTextEN Debug { get; } = new();
|
||||||
|
public GameCommandTextEN Game { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -238,6 +239,14 @@ public class DebugTextEN
|
|||||||
public string FileDisabled => "Personal debug file output disabled.";
|
public string FileDisabled => "Personal debug file output disabled.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class GameCommandTextEN
|
||||||
|
{
|
||||||
|
public string Desc => "Launch the configured game with patch injection";
|
||||||
|
public string Usage => "Usage: /game [extra game args]";
|
||||||
|
public string Started => "Game launched. PID: {0}";
|
||||||
|
public string Failed => "Failed to launch game: {0}";
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -38,5 +38,22 @@
|
|||||||
"DebugMessage": true,
|
"DebugMessage": true,
|
||||||
"DebugDetailMessage": true,
|
"DebugDetailMessage": true,
|
||||||
"DebugNoHandlerPacket": true
|
"DebugNoHandlerPacket": true
|
||||||
|
},
|
||||||
|
"Proxy": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Port": 18888,
|
||||||
|
"ServerHttpPort": 21500
|
||||||
|
},
|
||||||
|
"Loader": {
|
||||||
|
"GamePath": "",
|
||||||
|
"PatchPaths": [
|
||||||
|
"Patch\\MikuSB-Patch.dll"
|
||||||
|
],
|
||||||
|
"Arguments": [
|
||||||
|
"-FeatureLevelES31",
|
||||||
|
"-channelid=seasun",
|
||||||
|
"-NoSplash"
|
||||||
|
],
|
||||||
|
"SetAllProxy": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
GameServer/Command/Commands/CommandGame.cs
Normal file
29
GameServer/Command/Commands/CommandGame.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using MikuSB.Enums.Player;
|
||||||
|
using MikuSB.Internationalization;
|
||||||
|
using MikuSB.Loader;
|
||||||
|
using MikuSB.Util;
|
||||||
|
|
||||||
|
namespace MikuSB.GameServer.Command.Commands;
|
||||||
|
|
||||||
|
[CommandInfo("game", "Game.Command.Game.Desc", "Game.Command.Game.Usage", [], [PermEnum.Admin, PermEnum.Support])]
|
||||||
|
public class CommandGame : ICommands
|
||||||
|
{
|
||||||
|
private static readonly Logger Logger = new("CommandManager");
|
||||||
|
|
||||||
|
[CommandDefault]
|
||||||
|
public async ValueTask Launch(CommandArg arg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pid = GameLaunchService.Launch(arg.Args.ToArray());
|
||||||
|
var message = I18NManager.Translate("Game.Command.Game.Started", pid.ToString());
|
||||||
|
Logger.Info(message);
|
||||||
|
await arg.SendMsg(message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error("Failed to launch game.", ex);
|
||||||
|
await arg.SendMsg(I18NManager.Translate("Game.Command.Game.Failed", ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\MikuSB.Loader\MikuSB.Loader.csproj" />
|
||||||
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
|
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
|
||||||
<ProjectReference Include="..\Proto\Proto.csproj" />
|
<ProjectReference Include="..\Proto\Proto.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
357
MikuSB.Loader/GameLaunchService.cs
Normal file
357
MikuSB.Loader/GameLaunchService.cs
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using MikuSB.Util;
|
||||||
|
|
||||||
|
namespace MikuSB.Loader;
|
||||||
|
|
||||||
|
public static class GameLaunchService
|
||||||
|
{
|
||||||
|
public static int Launch(params string[] extraGameArguments)
|
||||||
|
{
|
||||||
|
ConfigManager.LoadConfig();
|
||||||
|
var options = LaunchOptions.FromConfig(extraGameArguments);
|
||||||
|
return Launch(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Launch(LaunchOptions options)
|
||||||
|
{
|
||||||
|
var startupInfo = new STARTUPINFOW
|
||||||
|
{
|
||||||
|
cb = Marshal.SizeOf<STARTUPINFOW>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var commandLine = BuildCommandLine(options.GamePath, options.GameArguments);
|
||||||
|
var workingDirectory = options.WorkingDirectory ?? Path.GetDirectoryName(options.GamePath)
|
||||||
|
?? throw new InvalidOperationException("Unable to determine working directory.");
|
||||||
|
|
||||||
|
var environment = BuildEnvironmentBlock(options.EnvironmentVariables);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!CreateProcessW(
|
||||||
|
lpApplicationName: options.GamePath,
|
||||||
|
lpCommandLine: commandLine,
|
||||||
|
lpProcessAttributes: IntPtr.Zero,
|
||||||
|
lpThreadAttributes: IntPtr.Zero,
|
||||||
|
bInheritHandles: false,
|
||||||
|
dwCreationFlags: CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT,
|
||||||
|
lpEnvironment: environment,
|
||||||
|
lpCurrentDirectory: workingDirectory,
|
||||||
|
lpStartupInfo: ref startupInfo,
|
||||||
|
lpProcessInformation: out var processInfo))
|
||||||
|
{
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create game process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var patchPath in options.PatchPaths)
|
||||||
|
InjectDll(processInfo.hProcess, patchPath);
|
||||||
|
|
||||||
|
if (ResumeThread(processInfo.hThread) == uint.MaxValue)
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to resume game process.");
|
||||||
|
|
||||||
|
return processInfo.dwProcessId;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(processInfo.hThread);
|
||||||
|
CloseHandle(processInfo.hProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (environment != IntPtr.Zero)
|
||||||
|
Marshal.FreeHGlobal(environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InjectDll(IntPtr processHandle, string dllPath)
|
||||||
|
{
|
||||||
|
var dllBytes = Encoding.Unicode.GetBytes(dllPath + '\0');
|
||||||
|
var remoteBuffer = VirtualAllocEx(
|
||||||
|
processHandle,
|
||||||
|
IntPtr.Zero,
|
||||||
|
(nuint)dllBytes.Length,
|
||||||
|
AllocationType.Commit | AllocationType.Reserve,
|
||||||
|
MemoryProtection.ReadWrite);
|
||||||
|
|
||||||
|
if (remoteBuffer == IntPtr.Zero)
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAllocEx failed.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!WriteProcessMemory(processHandle, remoteBuffer, dllBytes, dllBytes.Length, out var written) ||
|
||||||
|
written.ToInt64() != dllBytes.Length)
|
||||||
|
{
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "WriteProcessMemory failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var kernel32 = GetModuleHandleW("kernel32.dll");
|
||||||
|
if (kernel32 == IntPtr.Zero)
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetModuleHandleW(kernel32.dll) failed.");
|
||||||
|
|
||||||
|
var loadLibrary = GetProcAddress(kernel32, "LoadLibraryW");
|
||||||
|
if (loadLibrary == IntPtr.Zero)
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetProcAddress(LoadLibraryW) failed.");
|
||||||
|
|
||||||
|
var remoteThread = CreateRemoteThread(
|
||||||
|
processHandle,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
loadLibrary,
|
||||||
|
remoteBuffer,
|
||||||
|
0,
|
||||||
|
out _);
|
||||||
|
|
||||||
|
if (remoteThread == IntPtr.Zero)
|
||||||
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateRemoteThread failed.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var waitResult = WaitForSingleObject(remoteThread, 10_000);
|
||||||
|
if (waitResult != 0)
|
||||||
|
throw new Win32Exception($"Remote LoadLibraryW timed out or failed: {waitResult}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(remoteThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualFreeEx(processHandle, remoteBuffer, 0, FreeType.Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCommandLine(string exePath, IReadOnlyList<string> gameArgs)
|
||||||
|
{
|
||||||
|
var parts = new List<string> { Quote(exePath) };
|
||||||
|
parts.AddRange(gameArgs.Select(Quote));
|
||||||
|
return string.Join(' ', parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntPtr BuildEnvironmentBlock(IReadOnlyDictionary<string, string> variables)
|
||||||
|
{
|
||||||
|
if (variables.Count == 0)
|
||||||
|
return IntPtr.Zero;
|
||||||
|
|
||||||
|
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
||||||
|
merged[(string)entry.Key] = entry.Value?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
foreach (var pair in variables)
|
||||||
|
merged[pair.Key] = pair.Value;
|
||||||
|
|
||||||
|
var payload = string.Join('\0', merged.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(x => $"{x.Key}={x.Value}")) + "\0\0";
|
||||||
|
|
||||||
|
return Marshal.StringToHGlobalUni(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Quote(string value)
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
return "\"\"";
|
||||||
|
|
||||||
|
if (!value.Any(char.IsWhiteSpace) && !value.Contains('"'))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum CreationFlags : uint
|
||||||
|
{
|
||||||
|
CREATE_SUSPENDED = 0x00000004,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum AllocationType : uint
|
||||||
|
{
|
||||||
|
Commit = 0x1000,
|
||||||
|
Reserve = 0x2000
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum MemoryProtection : uint
|
||||||
|
{
|
||||||
|
ReadWrite = 0x04
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum FreeType : uint
|
||||||
|
{
|
||||||
|
Release = 0x8000
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct STARTUPINFOW
|
||||||
|
{
|
||||||
|
public int cb;
|
||||||
|
public string? lpReserved;
|
||||||
|
public string? lpDesktop;
|
||||||
|
public string? lpTitle;
|
||||||
|
public int dwX;
|
||||||
|
public int dwY;
|
||||||
|
public int dwXSize;
|
||||||
|
public int dwYSize;
|
||||||
|
public int dwXCountChars;
|
||||||
|
public int dwYCountChars;
|
||||||
|
public int dwFillAttribute;
|
||||||
|
public int dwFlags;
|
||||||
|
public short wShowWindow;
|
||||||
|
public short cbReserved2;
|
||||||
|
public IntPtr lpReserved2;
|
||||||
|
public IntPtr hStdInput;
|
||||||
|
public IntPtr hStdOutput;
|
||||||
|
public IntPtr hStdError;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct PROCESS_INFORMATION
|
||||||
|
{
|
||||||
|
public IntPtr hProcess;
|
||||||
|
public IntPtr hThread;
|
||||||
|
public int dwProcessId;
|
||||||
|
public int dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern bool CreateProcessW(
|
||||||
|
string? lpApplicationName,
|
||||||
|
string lpCommandLine,
|
||||||
|
IntPtr lpProcessAttributes,
|
||||||
|
IntPtr lpThreadAttributes,
|
||||||
|
bool bInheritHandles,
|
||||||
|
CreationFlags dwCreationFlags,
|
||||||
|
IntPtr lpEnvironment,
|
||||||
|
string? lpCurrentDirectory,
|
||||||
|
ref STARTUPINFOW lpStartupInfo,
|
||||||
|
out PROCESS_INFORMATION lpProcessInformation);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern IntPtr VirtualAllocEx(
|
||||||
|
IntPtr hProcess,
|
||||||
|
IntPtr lpAddress,
|
||||||
|
nuint dwSize,
|
||||||
|
AllocationType flAllocationType,
|
||||||
|
MemoryProtection flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool VirtualFreeEx(
|
||||||
|
IntPtr hProcess,
|
||||||
|
IntPtr lpAddress,
|
||||||
|
nuint dwSize,
|
||||||
|
FreeType dwFreeType);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool WriteProcessMemory(
|
||||||
|
IntPtr hProcess,
|
||||||
|
IntPtr lpBaseAddress,
|
||||||
|
byte[] lpBuffer,
|
||||||
|
int nSize,
|
||||||
|
out IntPtr lpNumberOfBytesWritten);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern IntPtr GetModuleHandleW(string lpModuleName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||||
|
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern IntPtr CreateRemoteThread(
|
||||||
|
IntPtr hProcess,
|
||||||
|
IntPtr lpThreadAttributes,
|
||||||
|
nuint dwStackSize,
|
||||||
|
IntPtr lpStartAddress,
|
||||||
|
IntPtr lpParameter,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
out int lpThreadId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern uint ResumeThread(IntPtr hThread);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LaunchOptions
|
||||||
|
{
|
||||||
|
public required string GamePath { get; init; }
|
||||||
|
public required IReadOnlyList<string> PatchPaths { get; init; }
|
||||||
|
public string? WorkingDirectory { get; init; }
|
||||||
|
public required IReadOnlyList<string> GameArguments { get; init; }
|
||||||
|
public required IReadOnlyDictionary<string, string> EnvironmentVariables { get; init; }
|
||||||
|
|
||||||
|
public static LaunchOptions FromConfig(IEnumerable<string>? extraGameArguments = null)
|
||||||
|
{
|
||||||
|
var config = ConfigManager.Config;
|
||||||
|
var gamePath = ResolvePath(config.Loader.GamePath, AppContext.BaseDirectory);
|
||||||
|
var patchPaths = ResolvePatchPaths(config.Loader.PatchPaths, AppContext.BaseDirectory);
|
||||||
|
var gameArgs = new List<string>(config.Loader.Arguments ?? []);
|
||||||
|
if (extraGameArguments is not null)
|
||||||
|
gameArgs.AddRange(extraGameArguments.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
|
|
||||||
|
var env = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (config.Loader.SetAllProxy && config.Proxy.Enabled)
|
||||||
|
env["ALL_PROXY"] = $"socks5h://127.0.0.1:{config.Proxy.Port}";
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(gamePath))
|
||||||
|
throw new InvalidOperationException("Loader.GamePath is not configured.");
|
||||||
|
if (!File.Exists(gamePath))
|
||||||
|
throw new FileNotFoundException("Game executable not found.", gamePath);
|
||||||
|
if (patchPaths.Count == 0)
|
||||||
|
throw new InvalidOperationException("At least one patch path is required.");
|
||||||
|
|
||||||
|
foreach (var patchPath in patchPaths)
|
||||||
|
{
|
||||||
|
if (!File.Exists(patchPath))
|
||||||
|
throw new FileNotFoundException("Patch DLL not found.", patchPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var workingDirectory = Path.GetDirectoryName(gamePath);
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDirectory) || !Directory.Exists(workingDirectory))
|
||||||
|
throw new DirectoryNotFoundException($"Working directory not found: {workingDirectory}");
|
||||||
|
|
||||||
|
return new LaunchOptions
|
||||||
|
{
|
||||||
|
GamePath = Path.GetFullPath(gamePath),
|
||||||
|
PatchPaths = patchPaths,
|
||||||
|
WorkingDirectory = Path.GetFullPath(workingDirectory),
|
||||||
|
GameArguments = gameArgs,
|
||||||
|
EnvironmentVariables = env
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolvePath(string? value, string baseDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Path.IsPathRooted(value)
|
||||||
|
? value
|
||||||
|
: Path.GetFullPath(Path.Combine(baseDirectory, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ResolvePatchPaths(IEnumerable<string>? values, string baseDirectory)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
if (values is null)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
var resolved = ResolvePath(value, baseDirectory);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
|
result.Add(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
MikuSB.Loader/MikuSB.Loader.csproj
Normal file
16
MikuSB.Loader/MikuSB.Loader.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>MikuSB.Loader</AssemblyName>
|
||||||
|
<RootNamespace>MikuSB.Loader</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy", "Proxy\Proxy.csproj
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Loader", "MikuSB.Loader\MikuSB.Loader.csproj", "{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -62,6 +64,10 @@ Global
|
|||||||
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
Targets="Restore;Publish"
|
Targets="Restore;Publish"
|
||||||
RemoveProperties="PublishProfile"
|
RemoveProperties="PublishProfile"
|
||||||
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
|
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
|
||||||
|
|
||||||
<Copy
|
<Copy
|
||||||
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
|
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
|
||||||
DestinationFiles="$(PublishDir)MikuSB.Updater.exe"
|
DestinationFiles="$(PublishDir)MikuSB.Updater.exe"
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using MikuSB.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace MikuSB.Proxy;
|
|
||||||
|
|
||||||
public sealed class ProxyCertificateAuthority
|
|
||||||
{
|
|
||||||
private const string Password = "MikuSB.Proxy.LocalCA";
|
|
||||||
private readonly ConcurrentDictionary<string, X509Certificate2> _serverCertificates = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly ILogger<ProxyCertificateAuthority> _logger;
|
|
||||||
private readonly ProxyOptions _options;
|
|
||||||
private readonly X509Certificate2 _rootCertificate;
|
|
||||||
|
|
||||||
public ProxyCertificateAuthority(IOptions<ProxyOptions> options, ILogger<ProxyCertificateAuthority> logger)
|
|
||||||
{
|
|
||||||
_options = options.Value;
|
|
||||||
_logger = logger;
|
|
||||||
_rootCertificate = LoadOrCreateRootCertificate();
|
|
||||||
|
|
||||||
if (_options.InstallRootCertificate)
|
|
||||||
InstallRootCertificate();
|
|
||||||
else
|
|
||||||
_logger.LogWarning(
|
|
||||||
"MikuSB proxy root certificate is not installed automatically. Import {CertificatePath} into CurrentUser Root to enable HTTPS interception.",
|
|
||||||
RootCerPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RootCerPath => Path.Join(CertificateDirectory, "MikuSB.Proxy.Root.cer");
|
|
||||||
public string RootCerPemPath => Path.Join(CertificateDirectory, "MikuSB.Proxy.Root.pem");
|
|
||||||
|
|
||||||
private static string CertificateDirectory => Path.Combine(AppContext.BaseDirectory, "proxy-certs");
|
|
||||||
|
|
||||||
public X509Certificate2 GetServerCertificate(string host)
|
|
||||||
{
|
|
||||||
host = host.Trim().TrimEnd('.').ToLowerInvariant();
|
|
||||||
return _serverCertificates.GetOrAdd(host, CreateServerCertificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate2 LoadOrCreateRootCertificate()
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(CertificateDirectory);
|
|
||||||
var pfxPath = Path.Combine(CertificateDirectory, "MikuSB.Proxy.Root.pfx");
|
|
||||||
|
|
||||||
if (File.Exists(pfxPath))
|
|
||||||
{
|
|
||||||
var existing = X509CertificateLoader.LoadPkcs12(
|
|
||||||
File.ReadAllBytes(pfxPath),
|
|
||||||
Password,
|
|
||||||
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet);
|
|
||||||
|
|
||||||
if (!File.Exists(RootCerPath))
|
|
||||||
File.WriteAllBytes(RootCerPath, existing.Export(X509ContentType.Cert));
|
|
||||||
|
|
||||||
if (!File.Exists(RootCerPemPath))
|
|
||||||
{
|
|
||||||
string pemString = existing.ExportCertificatePem();
|
|
||||||
File.WriteAllText(RootCerPemPath, pemString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var rsa = RSA.Create(4096);
|
|
||||||
var request = new CertificateRequest(
|
|
||||||
"CN=MikuSB Proxy Root CA",
|
|
||||||
rsa,
|
|
||||||
HashAlgorithmName.SHA256,
|
|
||||||
RSASignaturePadding.Pkcs1);
|
|
||||||
|
|
||||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
|
||||||
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.DigitalSignature, true));
|
|
||||||
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
|
||||||
|
|
||||||
var root = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(10));
|
|
||||||
var exportable = X509CertificateLoader.LoadPkcs12(
|
|
||||||
root.Export(X509ContentType.Pfx, Password),
|
|
||||||
Password,
|
|
||||||
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet);
|
|
||||||
|
|
||||||
File.WriteAllBytes(pfxPath, exportable.Export(X509ContentType.Pfx, Password));
|
|
||||||
File.WriteAllBytes(RootCerPath, exportable.Export(X509ContentType.Cert));
|
|
||||||
_logger.LogInformation("Created MikuSB proxy root certificate at {CertificatePath}", RootCerPath);
|
|
||||||
|
|
||||||
File.WriteAllText(RootCerPemPath, exportable.ExportCertificatePem());
|
|
||||||
_logger.LogInformation("Created MikuSB proxy root certificate (PEM) at {CertificatePath}", RootCerPemPath);
|
|
||||||
return exportable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate2 CreateServerCertificate(string host)
|
|
||||||
{
|
|
||||||
using var rsa = RSA.Create(2048);
|
|
||||||
var request = new CertificateRequest($"CN={host}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
||||||
|
|
||||||
var san = new SubjectAlternativeNameBuilder();
|
|
||||||
if (IPAddress.TryParse(host, out var address))
|
|
||||||
san.AddIpAddress(address);
|
|
||||||
else
|
|
||||||
san.AddDnsName(host);
|
|
||||||
|
|
||||||
request.CertificateExtensions.Add(san.Build());
|
|
||||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
|
||||||
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
|
|
||||||
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([new Oid("1.3.6.1.5.5.7.3.1")], false));
|
|
||||||
|
|
||||||
var serial = RandomNumberGenerator.GetBytes(16);
|
|
||||||
using var certificate = request.Create(
|
|
||||||
_rootCertificate,
|
|
||||||
DateTimeOffset.UtcNow.AddDays(-1),
|
|
||||||
DateTimeOffset.UtcNow.AddYears(2),
|
|
||||||
serial);
|
|
||||||
|
|
||||||
return X509CertificateLoader.LoadPkcs12(
|
|
||||||
certificate.CopyWithPrivateKey(rsa).Export(X509ContentType.Pfx),
|
|
||||||
null,
|
|
||||||
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InstallRootCertificate()
|
|
||||||
{
|
|
||||||
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
|
|
||||||
store.Open(OpenFlags.ReadWrite);
|
|
||||||
var existing = store.Certificates.Find(X509FindType.FindByThumbprint, _rootCertificate.Thumbprint, false);
|
|
||||||
if (existing.Count > 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
store.Add(_rootCertificate);
|
|
||||||
_logger.LogWarning("Installed MikuSB proxy root certificate into CurrentUser Root store. Thumbprint={Thumbprint}", _rootCertificate.Thumbprint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using MikuSB.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace MikuSB.Proxy;
|
|
||||||
|
|
||||||
public sealed class WindowsSystemProxyService(
|
|
||||||
IOptions<ProxyOptions> options,
|
|
||||||
ILogger<WindowsSystemProxyService> logger) : IHostedService, IDisposable
|
|
||||||
{
|
|
||||||
private const string InternetSettingsPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
|
|
||||||
private readonly ProxyOptions _options = options.Value;
|
|
||||||
private ConsoleCtrlHandler? _consoleCtrlHandler;
|
|
||||||
private int _proxyDisabled;
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!_options.Enabled || !_options.ManageSystemProxy)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
logger.LogWarning("System proxy management is only supported on Windows");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(InternetSettingsPath, writable: true);
|
|
||||||
if (key is null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Unable to open Windows Internet Settings registry key");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyServer = $"http=127.0.0.1:{_options.Port};https=127.0.0.1:{_options.Port}";
|
|
||||||
|
|
||||||
key.SetValue("ProxyEnable", 1, RegistryValueKind.DWord);
|
|
||||||
key.SetValue("ProxyServer", proxyServer, RegistryValueKind.String);
|
|
||||||
key.SetValue("ProxyOverride", _options.ProxyOverride, RegistryValueKind.String);
|
|
||||||
NotifyProxySettingsChanged();
|
|
||||||
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
|
|
||||||
RegisterConsoleCtrlHandler();
|
|
||||||
|
|
||||||
logger.LogWarning("Windows system proxy enabled for MikuSB: {ProxyServer}", proxyServer);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!_options.Enabled || !_options.ManageSystemProxy || !_options.RestoreSystemProxyOnStop)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
DisableSystemProxy();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
|
|
||||||
UnregisterConsoleCtrlHandler();
|
|
||||||
|
|
||||||
if (_options.Enabled && _options.ManageSystemProxy && _options.RestoreSystemProxyOnStop)
|
|
||||||
DisableSystemProxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnProcessExit(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_options.Enabled && _options.ManageSystemProxy && _options.RestoreSystemProxyOnStop)
|
|
||||||
DisableSystemProxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisableSystemProxy()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _proxyDisabled, 1) == 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(InternetSettingsPath, writable: true);
|
|
||||||
if (key is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
key.SetValue("ProxyEnable", 0, RegistryValueKind.DWord);
|
|
||||||
key.DeleteValue("ProxyServer", throwOnMissingValue: false);
|
|
||||||
key.DeleteValue("ProxyOverride", throwOnMissingValue: false);
|
|
||||||
NotifyProxySettingsChanged();
|
|
||||||
logger.LogWarning("Windows system proxy disabled for MikuSB shutdown");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterConsoleCtrlHandler()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
return;
|
|
||||||
|
|
||||||
_consoleCtrlHandler = OnConsoleCtrl;
|
|
||||||
SetConsoleCtrlHandler(_consoleCtrlHandler, add: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnregisterConsoleCtrlHandler()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows() || _consoleCtrlHandler is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SetConsoleCtrlHandler(_consoleCtrlHandler, add: false);
|
|
||||||
_consoleCtrlHandler = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool OnConsoleCtrl(int signal)
|
|
||||||
{
|
|
||||||
if (_options.Enabled && _options.ManageSystemProxy && _options.RestoreSystemProxyOnStop)
|
|
||||||
DisableSystemProxy();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void NotifyProxySettingsChanged()
|
|
||||||
{
|
|
||||||
InternetSetOption(IntPtr.Zero, 39, IntPtr.Zero, 0);
|
|
||||||
InternetSetOption(IntPtr.Zero, 37, IntPtr.Zero, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate bool ConsoleCtrlHandler(int signal);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler handler, bool add);
|
|
||||||
|
|
||||||
[DllImport("wininet.dll", SetLastError = true)]
|
|
||||||
private static extern bool InternetSetOption(IntPtr internet, int option, IntPtr buffer, int bufferLength);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user