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

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