MikuSB.Loader

This commit is contained in:
Kei-Luna
2026-05-13 07:19:45 +09:00
parent e92b214161
commit d9b16fb55d
13 changed files with 466 additions and 275 deletions

View File

@@ -9,6 +9,7 @@ public class ConfigContainer
public PathConfig Path { get; set; } = new();
public ServerOption ServerOption { get; set; } = new();
public ProxyOptions Proxy { get; set; } = new();
public LoaderOptions Loader { get; set; } = new();
}
public class HttpServerConfig
@@ -88,8 +89,12 @@ public class ProxyOptions
public bool Enabled { get; set; } = true;
public int Port { get; set; } = 18888;
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 string ProxyOverride { get; set; } = "localhost;127.*;10.*;192.168.*;<local>";
}
public class LoaderOptions
{
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;
}

View File

@@ -131,6 +131,7 @@ public class CommandTextCHS
public GirlTextCHS Girl { get; } = new();
public GiveAllTextCHS GiveAll { get; } = new();
public DebugTextCHS Debug { get; } = new();
public GameCommandTextCHS Game { get; } = new();
}
#endregion
@@ -272,6 +273,14 @@ public class DebugTextCHS
public string FileDisabled => "个人调试文件输出已禁用。";
}
public class GameCommandTextCHS
{
public string Desc => "使用补丁注入启动已配置的游戏";
public string Usage => "用法: /game [额外游戏参数]";
public string Started => "游戏已启动。PID: {0}";
public string Failed => "游戏启动失败: {0}";
}
#endregion
#endregion

View File

@@ -131,6 +131,7 @@ public class CommandTextCHT
public GirlTextCHT Girl { get; } = new();
public GiveAllTextCHT GiveAll { get; } = new();
public DebugTextCHT Debug { get; } = new();
public GameCommandTextCHT Game { get; } = new();
}
#endregion
@@ -272,6 +273,14 @@ public class DebugTextCHT
public string FileDisabled => "個人調試檔案輸出已停用。";
}
public class GameCommandTextCHT
{
public string Desc => "使用補丁注入啟動已配置的遊戲";
public string Usage => "用法: /game [額外遊戲參數]";
public string Started => "遊戲已啟動。PID: {0}";
public string Failed => "遊戲啟動失敗: {0}";
}
#endregion
#endregion

View File

@@ -90,6 +90,7 @@ public class CommandTextEN
public GirlTextEN Girl { get; } = new();
public GiveAllTextEN GiveAll { get; } = new();
public DebugTextEN Debug { get; } = new();
public GameCommandTextEN Game { get; } = new();
}
#endregion
@@ -238,6 +239,14 @@ public class DebugTextEN
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

View File

@@ -38,5 +38,22 @@
"DebugMessage": true,
"DebugDetailMessage": true,
"DebugNoHandlerPacket": true
},
"Proxy": {
"Enabled": true,
"Port": 18888,
"ServerHttpPort": 21500
},
"Loader": {
"GamePath": "",
"PatchPaths": [
"Patch\\MikuSB-Patch.dll"
],
"Arguments": [
"-FeatureLevelES31",
"-channelid=seasun",
"-NoSplash"
],
"SetAllProxy": true
}
}

View 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));
}
}
}

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\MikuSB.Loader\MikuSB.Loader.csproj" />
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
<ProjectReference Include="..\Proto\Proto.csproj" />
</ItemGroup>

View 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;
}
}

View 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>

View File

@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy", "Proxy\Proxy.csproj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Loader", "MikuSB.Loader\MikuSB.Loader.csproj", "{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -50,7 +50,6 @@
Targets="Restore;Publish"
RemoveProperties="PublishProfile"
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
<Copy
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
DestinationFiles="$(PublishDir)MikuSB.Updater.exe"

View File

@@ -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);
}
}

View File

@@ -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);
}