From dce7e54675376c0e248c8051170b6edfa0b72120 Mon Sep 17 00:00:00 2001 From: jie65535 Date: Sat, 23 Sep 2023 10:48:24 +0800 Subject: [PATCH] Add HTTP(S) proxy (#208) --- .../GrasscutterTools/Eavesdrop/Certifier.cs | 202 ++++++++++ .../Eavesdrop/Eavesdropper.cs | 200 ++++++++++ .../Event Args/RequestInterceptedEventArgs.cs | 78 ++++ .../ResponseInterceptedEventArgs.cs | 65 ++++ .../Eavesdrop/Interceptors.cs | 14 + .../Eavesdrop/Internals/INETOptions.cs | 241 ++++++++++++ .../Eavesdrop/Internals/NativeMethods.cs | 12 + Source/GrasscutterTools/Eavesdrop/LICENSE.md | 21 + .../Eavesdrop/Network/EavesNode.cs | 365 ++++++++++++++++++ .../GrasscutterTools/GrasscutterTools.csproj | 14 + .../Pages/PageOpenCommand.Designer.cs | 12 +- .../GrasscutterTools/Pages/PageOpenCommand.cs | 67 ++++ .../Pages/PageOpenCommand.resx | 55 ++- .../Properties/AssemblyInfo.cs | 4 +- .../Properties/Resources.Designer.cs | 129 ++++--- .../Properties/Resources.en-us.resx | 6 + .../Properties/Resources.resx | 6 + .../Properties/Resources.ru-ru.resx | 6 + .../Properties/Resources.zh-TW.resx | 6 + Source/GrasscutterTools/Utils/Logger.cs | 11 + Source/GrasscutterTools/Utils/ProxyHelper.cs | 195 ++++++++++ Source/GrasscutterTools/Utils/SystemProxy.cs | 13 + 22 files changed, 1648 insertions(+), 74 deletions(-) create mode 100644 Source/GrasscutterTools/Eavesdrop/Certifier.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Event Args/RequestInterceptedEventArgs.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Event Args/ResponseInterceptedEventArgs.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Interceptors.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs create mode 100644 Source/GrasscutterTools/Eavesdrop/LICENSE.md create mode 100644 Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs create mode 100644 Source/GrasscutterTools/Utils/ProxyHelper.cs create mode 100644 Source/GrasscutterTools/Utils/SystemProxy.cs diff --git a/Source/GrasscutterTools/Eavesdrop/Certifier.cs b/Source/GrasscutterTools/Eavesdrop/Certifier.cs new file mode 100644 index 0000000..ad55cab --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Certifier.cs @@ -0,0 +1,202 @@ +#nullable enable +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Eavesdrop; + +public sealed class Certifier : IDisposable +{ + private readonly X509Store _rootStore, _myStore; + private readonly IDictionary _certificateCache; + + public string Issuer { get; } + public string CertificateAuthorityName { get; } + + public DateTime NotAfter { get; set; } + public DateTime NotBefore { get; set; } + public int KeyLength { get; set; } = 1024; + public bool IsCachingSignedCertificates { get; set; } + + public X509Certificate2? Authority { get; private set; } + + public Certifier() + : this("Eavesdrop") + { } + public Certifier(string issuer) + : this(issuer, $"{issuer} Root Certificate Authority", StoreLocation.CurrentUser) + { } + public Certifier(string issuer, string certificateAuthorityName) + : this(issuer, certificateAuthorityName, StoreLocation.CurrentUser) + { } + public Certifier(string issuer, string certificateAuthorityName, StoreLocation location) + { + _myStore = new X509Store(StoreName.My, location); + _rootStore = new X509Store(StoreName.Root, location); + _certificateCache = new Dictionary(); + + NotBefore = DateTime.Now; + NotAfter = NotBefore.AddMonths(1); + + Issuer = issuer; + CertificateAuthorityName = certificateAuthorityName; + } + + public bool CreateTrustedRootCertificate() + { + return (Authority = InstallCertificate(_rootStore, CertificateAuthorityName)) != null; + } + public bool DestroyTrustedRootCertificate() + { + return DestroyCertificates(_rootStore); + } + public bool ExportTrustedRootCertificate(string path) + { + X509Certificate2? rootCertificate = InstallCertificate(_rootStore, CertificateAuthorityName); + + path = Path.GetFullPath(path); + if (rootCertificate != null) + { + byte[] data = rootCertificate.Export(X509ContentType.Cert); + File.WriteAllBytes(path, data); + } + return File.Exists(path); + } + + public X509Certificate2? GenerateCertificate(string certificateName) + { + return InstallCertificate(_myStore, certificateName); + } + public X509Certificate2 CreateCertificate(string subjectName, string alternateName) + { + using var rsa = RSA.Create(KeyLength); + var certificateRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + if (Authority == null) + { + certificateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + certificateRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(certificateRequest.PublicKey, false)); + + using X509Certificate2 certificate = certificateRequest.CreateSelfSigned(NotBefore.ToUniversalTime(), NotAfter.ToUniversalTime()); + + certificate.FriendlyName = alternateName; + return new X509Certificate2(certificate.Export(X509ContentType.Pfx, string.Empty), string.Empty, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); + } + else + { + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(alternateName); + + certificateRequest.CertificateExtensions.Add(sanBuilder.Build()); + certificateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + certificateRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(certificateRequest.PublicKey, false)); + + using X509Certificate2 certificate = certificateRequest.Create(Authority, Authority.NotBefore, Authority.NotAfter, Guid.NewGuid().ToByteArray()); + using X509Certificate2 certificateWithPrivateKey = certificate.CopyWithPrivateKey(rsa); + + certificateWithPrivateKey.FriendlyName = alternateName; + return new X509Certificate2(certificateWithPrivateKey.Export(X509ContentType.Pfx, string.Empty), string.Empty, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); + } + } + private X509Certificate2? InstallCertificate(X509Store store, string certificateName) + { + if (_certificateCache.TryGetValue(certificateName, out X509Certificate2? certificate)) + { + if (DateTime.Now >= certificate.NotAfter) + { + _certificateCache.Remove(certificateName); + } + else return certificate; + } + lock (store) + { + try + { + store.Open(OpenFlags.ReadWrite); + string subjectName = $"CN={certificateName}, O={Issuer}"; + + certificate = FindCertificates(store, subjectName)?[0]; + if (certificate != null && DateTime.Now >= certificate.NotAfter) + { + if (Authority == null) + { + DestroyCertificates(); + store.Open(OpenFlags.ReadWrite); + } + else + { + store.Remove(certificate); + } + certificate = null; + } + + if (certificate == null) + { + certificate = CreateCertificate(subjectName, certificateName); + if (certificate != null) + { + if (store == _rootStore || IsCachingSignedCertificates) + { + store.Add(certificate); + } + } + } + + return certificate; + } + catch { return certificate = null; } + finally + { + store.Close(); + if (certificate != null && !_certificateCache.ContainsKey(certificateName)) + { + _certificateCache.Add(certificateName, certificate); + } + } + } + } + + public bool DestroyCertificates(X509Store store) + { + lock (store) + { + try + { + store.Open(OpenFlags.ReadWrite); + X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindByIssuerName, Issuer, false); + + store.RemoveRange(certificates); + IEnumerable subjectNames = certificates.Cast().Select(c => c.GetNameInfo(X509NameType.SimpleName, false)); + + foreach (string subjectName in subjectNames) + { + if (!_certificateCache.ContainsKey(subjectName)) continue; + _certificateCache.Remove(subjectName); + } + return true; + } + catch { return false; } + finally { store.Close(); } + } + } + public bool DestroyCertificates() => DestroyCertificates(_myStore) && DestroyCertificates(_rootStore); + + private static X509Certificate2Collection? FindCertificates(X509Store store, string subjectName) + { + X509Certificate2Collection certificates = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, subjectName, false); + + return certificates.Count > 0 ? certificates : null; + } + + public void Dispose() + { + _myStore.Close(); + _rootStore.Close(); + + _myStore.Dispose(); + _rootStore.Dispose(); + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs b/Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs new file mode 100644 index 0000000..d4908a6 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs @@ -0,0 +1,200 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Eavesdrop.Network; + +namespace Eavesdrop +{ + public static class Eavesdropper + { + private static TcpListener _listener; + private static readonly object _stateLock; + + public delegate Task AsyncEventHandler(object sender, TEventArgs e); + + public static event AsyncEventHandler RequestInterceptedAsync; + private static async Task OnRequestInterceptedAsync(RequestInterceptedEventArgs e) + { + Task interceptedTask = RequestInterceptedAsync?.Invoke(null, e); + if (interceptedTask != null) + { + await interceptedTask; + } + } + + public static event AsyncEventHandler ResponseInterceptedAsync; + private static async Task OnResponseInterceptedAsync(ResponseInterceptedEventArgs e) + { + Task interceptedTask = ResponseInterceptedAsync?.Invoke(null, e); + if (interceptedTask != null) + { + await interceptedTask; + } + } + + public static List Overrides { get; } + public static bool IsRunning { get; private set; } + public static Certifier Certifier { get; set; } + + static Eavesdropper() + { + _stateLock = new object(); + + ServicePointManager.Expect100Continue = true; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + + Overrides = new List(); + Certifier = new Certifier("Eavesdrop", "Eavesdrop Root Certificate Authority"); + } + + public static void Terminate() + { + lock (_stateLock) + { + ResetMachineProxy(); + IsRunning = false; + + if (_listener != null) + { + _listener.Stop(); + _listener = null; + } + } + } + public static void Initiate(int port) + { + Initiate(port, Interceptors.Default); + } + public static void Initiate(int port, Interceptors interceptors) + { + Initiate(port, interceptors, true); + } + public static void Initiate(int port, Interceptors interceptors, bool setSystemProxy) + { + lock (_stateLock) + { + Terminate(); + + _listener = new TcpListener(IPAddress.Any, port); + _listener.Start(); + + IsRunning = true; + + Task.Factory.StartNew(InterceptRequestAsync, TaskCreationOptions.LongRunning); + if (setSystemProxy) + { + SetMachineProxy(port, interceptors); + } + } + } + + private static async Task InterceptRequestAsync() + { + while (IsRunning && _listener != null) + { + try + { + TcpClient client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); + Task handleClientAsync = HandleClientAsync(client); + } + catch (ObjectDisposedException) + { + } + } + } + + private static async Task HandleClientAsync(TcpClient client) + { + using var local = new EavesNode(Certifier, client); + WebRequest request = await local.ReadRequestAsync().ConfigureAwait(false); + if (request == null) return; + + HttpContent requestContent = null; + var requestArgs = new RequestInterceptedEventArgs(request); + try + { + requestArgs.Content = requestContent = await local.ReadRequestContentAsync(request).ConfigureAwait(false); + await OnRequestInterceptedAsync(requestArgs).ConfigureAwait(false); + if (requestArgs.Cancel) return; + + request = requestArgs.Request; + if (requestArgs.Content != null) + { + await local.WriteRequestContentAsync(request, requestArgs.Content).ConfigureAwait(false); + } + } + finally + { + requestContent?.Dispose(); + requestArgs.Content?.Dispose(); + } + + WebResponse response = null; + try { response = await request.GetResponseAsync().ConfigureAwait(false); } + catch (WebException ex) { response = ex.Response; } + catch (ProtocolViolationException) + { + response?.Dispose(); + response = null; + } + + if (response == null) return; + HttpContent responseContent = null; + var responseArgs = new ResponseInterceptedEventArgs(request, response); + try + { + responseArgs.Content = responseContent = EavesNode.ReadResponseContent(response); + await OnResponseInterceptedAsync(responseArgs).ConfigureAwait(false); + if (responseArgs.Cancel) return; + + await local.SendResponseAsync(responseArgs.Response, responseArgs.Content).ConfigureAwait(false); + } + finally + { + response.Dispose(); + responseArgs.Response.Dispose(); + + responseContent?.Dispose(); + responseArgs.Content?.Dispose(); + } + } + + private static void ResetMachineProxy() + { + INETOptions.Overrides.Clear(); + INETOptions.IsIgnoringLocalTraffic = false; + + INETOptions.HTTPAddress = null; + INETOptions.HTTPSAddress = null; + INETOptions.IsProxyEnabled = false; + + INETOptions.Save(); + } + private static void SetMachineProxy(int port, Interceptors interceptors) + { + foreach (string @override in Overrides) + { + if (INETOptions.Overrides.Contains(@override)) continue; + INETOptions.Overrides.Add(@override); + } + + string address = ("127.0.0.1:" + port); + if (interceptors.HasFlag(Interceptors.HTTP)) + { + INETOptions.HTTPAddress = address; + } + if (interceptors.HasFlag(Interceptors.HTTPS)) + { + INETOptions.HTTPSAddress = address; + } + INETOptions.IsProxyEnabled = true; + INETOptions.IsIgnoringLocalTraffic = true; + + INETOptions.Save(); + } + } +} diff --git a/Source/GrasscutterTools/Eavesdrop/Event Args/RequestInterceptedEventArgs.cs b/Source/GrasscutterTools/Eavesdrop/Event Args/RequestInterceptedEventArgs.cs new file mode 100644 index 0000000..9e6e613 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Event Args/RequestInterceptedEventArgs.cs @@ -0,0 +1,78 @@ +using System; +using System.Net; +using System.Net.Http; +using System.ComponentModel; + +namespace Eavesdrop +{ + public class RequestInterceptedEventArgs : CancelEventArgs + { + private HttpWebRequest _httpRequest; + + public HttpContent Content { get; set; } + + private WebRequest _request; + public WebRequest Request + { + get => _request; + set + { + _request = value; + _httpRequest = (value as HttpWebRequest); + } + } + + public Uri Uri => Request?.RequestUri; + public CookieContainer CookieContainer => _httpRequest?.CookieContainer; + + public string Method + { + get => Request?.Method; + set + { + if (Request != null) + { + Request.Method = value; + } + } + } + public IWebProxy Proxy + { + get => Request?.Proxy; + set + { + if (Request != null) + { + Request.Proxy = value; + } + } + } + public string ContentType + { + get => Request?.ContentType; + set + { + if (Request != null) + { + Request.ContentType = value; + } + } + } + public WebHeaderCollection Headers + { + get => Request?.Headers; + set + { + if (Request != null) + { + Request.Headers = value; + } + } + } + + public RequestInterceptedEventArgs(WebRequest request) + { + Request = request; + } + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Event Args/ResponseInterceptedEventArgs.cs b/Source/GrasscutterTools/Eavesdrop/Event Args/ResponseInterceptedEventArgs.cs new file mode 100644 index 0000000..303750b --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Event Args/ResponseInterceptedEventArgs.cs @@ -0,0 +1,65 @@ +using System; +using System.Net; +using System.Net.Http; +using System.ComponentModel; +using System.Threading.Tasks; + +using Eavesdrop.Network; + +namespace Eavesdrop +{ + public class ResponseInterceptedEventArgs : CancelEventArgs + { + private WebResponse _response; + public WebResponse Response + { + get => _response; + set + { + _response = value; + if (value is HttpWebResponse httpResponse) + { + CookieContainer = new CookieContainer(); + CookieContainer.Add(httpResponse.Cookies); + } + else CookieContainer = null; + } + } + + public WebRequest Request { get; } + public Uri Uri => Response?.ResponseUri; + + public HttpContent Content { get; set; } + public CookieContainer CookieContainer { get; private set; } + + public string ContentType + { + get => Response?.ContentType; + set + { + if (Response != null) + { + Response.ContentType = value; + } + } + } + public WebHeaderCollection Headers + { + get => Response?.Headers; + set + { + if (Response == null) return; + foreach (string header in value.AllKeys) + { + Response.Headers[header] = value[header]; + } + } + } + + public ResponseInterceptedEventArgs(WebRequest request, WebResponse response) + { + Request = request; + Response = response; + } + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Interceptors.cs b/Source/GrasscutterTools/Eavesdrop/Interceptors.cs new file mode 100644 index 0000000..af8dcb3 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Interceptors.cs @@ -0,0 +1,14 @@ +using System; + +namespace Eavesdrop +{ + [Flags] + public enum Interceptors + { + None = 0, + HTTP = 1, + HTTPS = 2, + + Default = (HTTP | HTTPS) + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs b/Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs new file mode 100644 index 0000000..fa79bbc --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs @@ -0,0 +1,241 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Microsoft.Win32; + +namespace Eavesdrop +{ + public static class INETOptions + { + private static readonly object _stateLock; + private static readonly int _iNetOptionSize; + private static readonly int _iNetPackageSize; + private static readonly RegistryKey _proxyKey; + + public static List Overrides { get; } + + public static string HTTPAddress { get; set; } + public static string HTTPSAddress { get; set; } + + public static bool IsProxyEnabled { get; set; } + public static bool IsIgnoringLocalTraffic { get; set; } + + static INETOptions() + { + _stateLock = new object(); + _iNetOptionSize = Marshal.SizeOf(typeof(INETOption)); + _iNetPackageSize = Marshal.SizeOf(typeof(INETPackage)); + _proxyKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Internet Settings", true); + + Overrides = new List(); + Load(); + } + + public static void Save() + { + lock (_stateLock) + { + var options = new List(3); + string joinedAddresses = (IsProxyEnabled ? GetJoinedAddresses() : null); + string joinedOverrides = (IsProxyEnabled ? GetJoinedOverrides() : null); + + var kind = ProxyKind.PROXY_TYPE_DIRECT; + if (!string.IsNullOrWhiteSpace(joinedAddresses)) + { + options.Add(new INETOption(OptionKind.INTERNET_PER_CONN_PROXY_SERVER, joinedAddresses)); + if (!string.IsNullOrWhiteSpace(joinedOverrides)) + { + options.Add(new INETOption(OptionKind.INTERNET_PER_CONN_PROXY_BYPASS, joinedOverrides)); + } + kind |= ProxyKind.PROXY_TYPE_PROXY; + } + options.Insert(0, new INETOption(OptionKind.INTERNET_PER_CONN_FLAGS, (int)kind)); + + var inetPackage = new INETPackage + { + _optionError = 0, + _size = _iNetPackageSize, + _connection = IntPtr.Zero, + _optionCount = options.Count + }; + + IntPtr optionsPtr = Marshal.AllocCoTaskMem(_iNetOptionSize * options.Count); + for (int i = 0; i < options.Count; ++i) + { + var optionPtr = new IntPtr((IntPtr.Size == 4 ? optionsPtr.ToInt32() : optionsPtr.ToInt64()) + (i * _iNetOptionSize)); + Marshal.StructureToPtr(options[i], optionPtr, false); + } + inetPackage._optionsPtr = optionsPtr; + + IntPtr iNetPackagePtr = Marshal.AllocCoTaskMem(_iNetPackageSize); + Marshal.StructureToPtr(inetPackage, iNetPackagePtr, false); + + int returnvalue = (NativeMethods.InternetSetOption(IntPtr.Zero, 75, iNetPackagePtr, _iNetPackageSize) ? -1 : 0); + if (returnvalue == 0) + { + returnvalue = Marshal.GetLastWin32Error(); + } + + Marshal.FreeCoTaskMem(optionsPtr); + Marshal.FreeCoTaskMem(iNetPackagePtr); + if (returnvalue > 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + NativeMethods.InternetSetOption(IntPtr.Zero, 39, iNetPackagePtr, _iNetPackageSize); + NativeMethods.InternetSetOption(IntPtr.Zero, 37, iNetPackagePtr, _iNetPackageSize); + } + } + public static void Load() + { + lock (_stateLock) + { + LoadAddresses(); + LoadOverrides(); + IsProxyEnabled = (_proxyKey.GetValue("ProxyEnable")?.ToString() == "1"); + } + } + + private static void LoadOverrides() + { + string proxyOverride = _proxyKey.GetValue("ProxyOverride")?.ToString(); + if (string.IsNullOrWhiteSpace(proxyOverride)) return; + + string[] overrides = proxyOverride.Split(';'); + foreach (string @override in overrides) + { + if (@override == "") + { + IsIgnoringLocalTraffic = true; + } + else if (!Overrides.Contains(@override)) + { + Overrides.Add(@override); + } + } + } + private static void LoadAddresses() + { + string proxyServer = _proxyKey.GetValue("ProxyServer")?.ToString(); + if (string.IsNullOrWhiteSpace(proxyServer)) return; + + string[] values = proxyServer.Split(';'); + foreach (string value in values) + { + string[] pair = value.Split('='); + if (pair.Length != 2) + { + HTTPAddress = value; + HTTPSAddress = value; + return; + } + + string address = pair[1]; + string protocol = pair[0]; + switch (protocol) + { + case "http": HTTPAddress = address; break; + case "https": HTTPSAddress = address; break; + } + } + } + + private static string GetJoinedAddresses() + { + var addresses = new List(2); + if (!string.IsNullOrWhiteSpace(HTTPAddress)) + { + addresses.Add("http=" + HTTPAddress); + } + if (!string.IsNullOrWhiteSpace(HTTPSAddress)) + { + addresses.Add("https=" + HTTPSAddress); + } + return string.Join(";", addresses); + } + private static string GetJoinedOverrides() + { + var overrides = new List(Overrides); + if (IsIgnoringLocalTraffic) + { + overrides.Add(""); + } + return string.Join(";", overrides); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct INETOption + { + private readonly OptionKind _kind; + private readonly INETOptionValue _value; + + public INETOption(OptionKind kind, int value) + { + _kind = kind; + _value = CreateValue(value); + } + public INETOption(OptionKind kind, string value) + { + _kind = kind; + _value = CreateValue(value); + } + + private static INETOptionValue CreateValue(int value) + { + return new INETOptionValue + { + _intValue = value + }; + } + private static INETOptionValue CreateValue(string value) + { + return new INETOptionValue + { + _stringPointer = Marshal.StringToHGlobalAuto(value) + }; + } + + [StructLayout(LayoutKind.Explicit)] + private struct INETOptionValue + { + [FieldOffset(0)] + public int _intValue; + + [FieldOffset(0)] + public IntPtr _stringPointer; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME _fileTime; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct INETPackage + { + public int _size; + public IntPtr _connection; + public int _optionCount; + public int _optionError; + public IntPtr _optionsPtr; + } + + [Flags] + private enum ProxyKind + { + PROXY_TYPE_DIRECT = 1, + PROXY_TYPE_PROXY = 2, + PROXY_TYPE_AUTO_PROXY_URL = 4, + PROXY_TYPE_AUTO_DETECT = 8 + } + private enum OptionKind + { + INTERNET_PER_CONN_FLAGS = 1, + INTERNET_PER_CONN_PROXY_SERVER = 2, + INTERNET_PER_CONN_PROXY_BYPASS = 3, + INTERNET_PER_CONN_AUTOCONFIG_URL = 4 + } + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs b/Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs new file mode 100644 index 0000000..31ac008 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.InteropServices; + +namespace Eavesdrop +{ + internal static class NativeMethods + { + [DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength); + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/LICENSE.md b/Source/GrasscutterTools/Eavesdrop/LICENSE.md new file mode 100644 index 0000000..c826c31 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 ArachisH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs b/Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs new file mode 100644 index 0000000..06c78a9 --- /dev/null +++ b/Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs @@ -0,0 +1,365 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Net.Http; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; +using System.IO.Compression; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Authentication; +using System.Text.RegularExpressions; +using System.Security.Cryptography.X509Certificates; + +//using BrotliSharpLib; + +namespace Eavesdrop.Network +{ + public class EavesNode : IDisposable + { + private SslStream _secureStream; + private readonly TcpClient _client; + private readonly Certifier _certifier; + private static readonly Regex _responseCookieSplitter; + + public bool IsSecure => (_secureStream != null); + + static EavesNode() + { + _responseCookieSplitter = new Regex(",(?! )"); + } + public EavesNode(Certifier certifier, TcpClient client) + { + _client = client; + _certifier = certifier; + + _client.NoDelay = true; + } + + public Task ReadRequestAsync() + { + return ReadRequestAsync(null); + } + private async Task ReadRequestAsync(Uri baseUri) + { + string method = null; + var headers = new List(); + string requestUrl = baseUri?.OriginalString; + + string command = ReadNonBufferedLine(); + if (string.IsNullOrWhiteSpace(command)) return null; + + if (string.IsNullOrWhiteSpace(command)) return null; + string[] values = command.Split(' '); + + method = values[0]; + requestUrl += values[1]; + while (_client.Connected) + { + string header = ReadNonBufferedLine(); + if (string.IsNullOrWhiteSpace(header)) break; + + headers.Add(header); + } + + if (method == "CONNECT") + { + baseUri = new Uri("https://" + requestUrl); + await SendResponseAsync(HttpStatusCode.OK).ConfigureAwait(false); + + if (!SecureTunnel(baseUri.Host)) return null; + return await ReadRequestAsync(baseUri).ConfigureAwait(false); + } + else return CreateRequest(method, headers, new Uri(requestUrl)); + } + + public async Task ReadRequestContentAsync(WebRequest request) + { + byte[] payload = await GetPayload(GetStream(), request.ContentLength).ConfigureAwait(false); + if (payload == null) return null; + + //if (request.Headers[HttpRequestHeader.ContentEncoding] == "br") + //{ + // request.Headers[HttpRequestHeader.ContentEncoding] = ""; // No longer encoded. + // payload = Brotli.DecompressBuffer(payload, 0, payload.Length); + //} + return new ByteArrayContent(payload); + } + public async Task WriteRequestContentAsync(WebRequest request, HttpContent content) + { + byte[] payload = null; + if (content is StreamContent streamContent) + { + // TODO: + throw new NotSupportedException(); + } + else payload = await content.ReadAsByteArrayAsync().ConfigureAwait(false); + + //if (request.Headers[HttpRequestHeader.ContentEncoding] == "br") + //{ + // payload = Brotli.CompressBuffer(payload, 0, payload.Length); + //} + + request.ContentLength = payload.Length; + using (Stream output = await request.GetRequestStreamAsync().ConfigureAwait(false)) + { + await output.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false); + } + } + + public Task SendResponseAsync(WebResponse response, HttpContent content) + { + string description = "OK"; + var status = HttpStatusCode.OK; + if (response is HttpWebResponse httpResponse) + { + status = httpResponse.StatusCode; + description = httpResponse.StatusDescription; + } + return SendResponseAsync(status, description, response.Headers, content); + } + public Task SendResponseAsync(HttpStatusCode status, string description = null) + { + return SendResponseAsync(status, (description ?? status.ToString()), null, null); + } + public async Task SendResponseAsync(HttpStatusCode status, string description, WebHeaderCollection headers, HttpContent content) + { + var headerBuilder = new StringBuilder(); + headerBuilder.AppendLine($"HTTP/{HttpVersion.Version10} {(int)status} {description}"); + if (headers != null) + { + foreach (string header in headers.AllKeys) + { + if (header == "Transfer-Encoding") continue; + + string value = headers[header]; + if (string.IsNullOrWhiteSpace(value)) continue; + + if (header.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (string setCookie in _responseCookieSplitter.Split(value)) + { + headerBuilder.AppendLine($"{header}: {setCookie}"); + } + } + else headerBuilder.AppendLine($"{header}: {value}"); + } + } + headerBuilder.AppendLine(); + + byte[] headerData = Encoding.UTF8.GetBytes(headerBuilder.ToString()); + await GetStream().WriteAsync(headerData, 0, headerData.Length).ConfigureAwait(false); + if (content != null) + { + // TODO: If the Content-Encoding header has been changed, re-compress while writing? + Stream input = await content.ReadAsStreamAsync().ConfigureAwait(false); + + int bytesRead = 0; + var buffer = new byte[8192]; + do + { + bytesRead = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + if (_client.Connected && bytesRead > 0) + { + await GetStream().WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + } + else return; + } + while (input.CanRead && _client.Connected); + } + } + + public Stream GetStream() + { + return ((Stream)_secureStream ?? _client.GetStream()); + } + private StreamWriter WrapStreamWriter() + { + return new StreamWriter(GetStream(), Encoding.UTF8, 1024, true); + } + private StreamReader WrapStreamReader(int bufferSize = 1024) + { + return new StreamReader(GetStream(), Encoding.UTF8, true, bufferSize, true); + } + + private string ReadNonBufferedLine() + { + string line = string.Empty; + try + { + using (var binaryInput = new BinaryReader(GetStream(), Encoding.UTF8, true)) + { + do { line += binaryInput.ReadChar(); } + while (!line.EndsWith("\r\n")); + } + } + catch (EndOfStreamException) { line += "\r\n"; } + return line.Substring(0, line.Length - 2); + } + private bool SecureTunnel(string host) + { + try + { + X509Certificate2 certificate = _certifier.GenerateCertificate(host); + + _secureStream = new SslStream(GetStream()); + _secureStream.AuthenticateAsServer(certificate, false, SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, false); + + return true; + } + catch { return false; } + } + private IEnumerable GetCookies(string cookieHeader, string host) + { + foreach (string cookie in cookieHeader.Split(';')) + { + int nameEndIndex = cookie.IndexOf('='); + if (nameEndIndex == -1) continue; + + string name = cookie.Substring(0, nameEndIndex).Trim(); + string value = cookie.Substring(nameEndIndex + 1).Trim(); + + yield return new Cookie(name, value, "/", host); + } + } + private HttpWebRequest CreateRequest(string method, List headers, Uri requestUri) + { + HttpWebRequest request = WebRequest.CreateHttp(requestUri); + request.ProtocolVersion = HttpVersion.Version10; + request.CookieContainer = new CookieContainer(); + request.AllowAutoRedirect = false; + request.KeepAlive = false; + request.Method = method; + request.Proxy = null; + + foreach (string header in headers) + { + int delimiterIndex = header.IndexOf(':'); + if (delimiterIndex == -1) continue; + + string name = header.Substring(0, delimiterIndex); + string value = header.Substring(delimiterIndex + 2); + switch (name.ToLower()) + { + case "range": + case "expect": + case "keep-alive": + case "connection": + case "proxy-connection": break; + + case "host": request.Host = value; break; + case "accept": request.Accept = value; break; + case "referer": request.Referer = value; break; + case "user-agent": request.UserAgent = value; break; + case "content-type": request.ContentType = value; break; + + case "content-length": + { + request.ContentLength = + long.Parse(value, CultureInfo.InvariantCulture); + + break; + } + case "cookie": + { + foreach (Cookie cookie in GetCookies(value, request.Host)) + { + try + { + request.CookieContainer.Add(cookie); + } + catch (CookieException) { } + } + request.Headers[name] = value; + break; + } + case "if-modified-since": + { + request.IfModifiedSince = DateTime.Parse( + value.Split(';')[0], CultureInfo.InvariantCulture); + + break; + } + + case "date": + if (long.TryParse(value, out var timestamp)) + { + request.Date = timestamp > 10_000_000_000L + ? DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime + : DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + } + else + { + request.Date = DateTime.Parse(value); + } + break; + + default: + request.Headers[name] = value; break; + } + } + return request; + } + + public void Dispose() + { + Dispose(true); + } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + GetStream().Dispose(); + _client.Dispose(); + } + } + + public static StreamContent ReadResponseContent(WebResponse response) + { + if (response.ContentLength == 0) + { + response.GetResponseStream().Dispose(); + return null; + } + + Stream input = response.GetResponseStream(); + //if (response is HttpWebResponse httpResponse && !string.IsNullOrWhiteSpace(httpResponse.ContentEncoding)) + //{ + // switch (httpResponse.ContentEncoding) + // { + // //case "br": input = new BrotliStream(input, CompressionMode.Decompress); break; + // case "gzip": input = new GZipStream(input, CompressionMode.Decompress); break; + // case "deflate": input = new DeflateStream(input, CompressionMode.Decompress); break; + // } + // response.Headers.Remove(HttpResponseHeader.ContentLength); + // response.Headers.Remove(HttpResponseHeader.ContentEncoding); + // response.Headers.Add(HttpResponseHeader.TransferEncoding, "chunked"); + //} + return new StreamContent(input, response.ContentLength > 0 ? (int)response.ContentLength : 4096); + } + public static async Task GetPayload(Stream input, long length) + { + if (length < 1) return null; + + int totalBytesRead = 0; + int nullBytesReadCount = 0; + var payload = new byte[length]; + do + { + int bytesLeft = (payload.Length - totalBytesRead); + int bytesRead = await input.ReadAsync(payload, totalBytesRead, bytesLeft).ConfigureAwait(false); + + if (bytesRead > 0) + { + nullBytesReadCount = 0; + totalBytesRead += bytesRead; + } + else if (++nullBytesReadCount >= 2) return null; + } + while (totalBytesRead != payload.Length); + return payload; + } + } +} \ No newline at end of file diff --git a/Source/GrasscutterTools/GrasscutterTools.csproj b/Source/GrasscutterTools/GrasscutterTools.csproj index a96a493..b2adafc 100644 --- a/Source/GrasscutterTools/GrasscutterTools.csproj +++ b/Source/GrasscutterTools/GrasscutterTools.csproj @@ -40,6 +40,8 @@ DEBUG;TRACE prompt 4 + 11 + true AnyCPU @@ -49,6 +51,8 @@ TRACE prompt 4 + 11 + true Resources\IconGrasscutter.ico @@ -87,6 +91,14 @@ + + + + + + + + Form @@ -319,6 +331,7 @@ + @@ -669,6 +682,7 @@ Resources.resx True + diff --git a/Source/GrasscutterTools/Pages/PageOpenCommand.Designer.cs b/Source/GrasscutterTools/Pages/PageOpenCommand.Designer.cs index dab6d6b..b444703 100644 --- a/Source/GrasscutterTools/Pages/PageOpenCommand.Designer.cs +++ b/Source/GrasscutterTools/Pages/PageOpenCommand.Designer.cs @@ -60,6 +60,7 @@ this.TxtHost = new System.Windows.Forms.ComboBox(); this.BtnQueryServerStatus = new System.Windows.Forms.Button(); this.LblHost = new System.Windows.Forms.Label(); + this.BtnProxy = new System.Windows.Forms.Button(); this.GrpServerStatus.SuspendLayout(); this.GrpRemoteCommand.SuspendLayout(); this.TPOpenCommandCheck.SuspendLayout(); @@ -286,8 +287,8 @@ // resources.ApplyResources(this.TxtHost, "TxtHost"); this.TxtHost.Name = "TxtHost"; - this.TxtHost.KeyDown += new System.Windows.Forms.KeyEventHandler(this.TxtHost_KeyDown); this.TxtHost.SelectedIndexChanged += new System.EventHandler(this.TxtHost_SelectedIndexChanged); + this.TxtHost.KeyDown += new System.Windows.Forms.KeyEventHandler(this.TxtHost_KeyDown); // // BtnQueryServerStatus // @@ -301,10 +302,18 @@ resources.ApplyResources(this.LblHost, "LblHost"); this.LblHost.Name = "LblHost"; // + // BtnProxy + // + resources.ApplyResources(this.BtnProxy, "BtnProxy"); + this.BtnProxy.Name = "BtnProxy"; + this.BtnProxy.UseVisualStyleBackColor = true; + this.BtnProxy.Click += new System.EventHandler(this.BtnProxy_Click); + // // PageOpenCommand // resources.ApplyResources(this, "$this"); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.BtnProxy); this.Controls.Add(this.LnkLinks); this.Controls.Add(this.LnkGOODHelp); this.Controls.Add(this.LnkInventoryKamera); @@ -365,5 +374,6 @@ private System.Windows.Forms.ComboBox TxtHost; private System.Windows.Forms.Button BtnQueryServerStatus; private System.Windows.Forms.Label LblHost; + private System.Windows.Forms.Button BtnProxy; } } diff --git a/Source/GrasscutterTools/Pages/PageOpenCommand.cs b/Source/GrasscutterTools/Pages/PageOpenCommand.cs index 36dad17..f14e35a 100644 --- a/Source/GrasscutterTools/Pages/PageOpenCommand.cs +++ b/Source/GrasscutterTools/Pages/PageOpenCommand.cs @@ -25,6 +25,8 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; +using Eavesdrop; + using GrasscutterTools.DispatchServer; using GrasscutterTools.DispatchServer.Model; using GrasscutterTools.Game; @@ -47,6 +49,8 @@ namespace GrasscutterTools.Pages if (DesignMode) return; InitServerRecords(); + if (!string.IsNullOrEmpty(Settings.Default.Host)) + TxtHost.Items.Add(Settings.Default.Host); TxtHost.Items.AddRange(ServerRecords.Select(it => it.Host).ToArray()); NUDRemotePlayerId.Value = Settings.Default.RemoteUid; @@ -57,6 +61,14 @@ namespace GrasscutterTools.Pages TxtToken.Text = Settings.Default.TokenCache; Task.Delay(1000).ContinueWith(_ => ShowTipInRunButton?.Invoke(Resources.TokenRestoredFromCache)); } + + if (string.IsNullOrEmpty(TxtHost.Text)) + { + TxtHost.Items.Add("http://127.0.0.1:443"); + TxtHost.SelectedIndex = 0; + } + + BtnProxy.Text = Resources.StartProxy; } #region - 服务器记录 - @@ -161,6 +173,19 @@ namespace GrasscutterTools.Pages Settings.Default.RemoteUid = NUDRemotePlayerId.Value; Settings.Default.Host = TxtHost.Text; Settings.Default.TokenCache = Common.OC?.Token; + + try + { + Logger.I(TAG, "Stop Proxy"); + ProxyHelper.StopProxy(); + } + catch (Exception ex) + { +#if DEBUG + MessageBox.Show(ex.ToString(), Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); +#endif + Logger.E(TAG, "Stop Proxy Failed.", ex); + } } /// @@ -240,6 +265,7 @@ namespace GrasscutterTools.Pages MessageBox.Show(ex.Message, Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); #endif } + if (isOcEnabled) { LblOpenCommandSupport.Text = "√"; @@ -252,6 +278,8 @@ namespace GrasscutterTools.Pages LblOpenCommandSupport.ForeColor = Color.Red; GrpRemoteCommand.Enabled = false; } + + BtnProxy.Enabled = true; } catch (Exception ex) { @@ -583,5 +611,44 @@ namespace GrasscutterTools.Pages } #endregion - 导入存档 GOOD - + + + #region - 代理 Porxy - + + /// + /// 点击代理按钮时触发 + /// + private void BtnProxy_Click(object sender, EventArgs e) + { + try + { + // 正在运行则关闭 + if (ProxyHelper.IsRunning) + { + ProxyHelper.StopProxy(); + BtnProxy.Text = Resources.StartProxy; + } + else + { + // 创建根证书并检查信任 + if (!ProxyHelper.CheckAndCreateCertifier()) + { + MessageBox.Show("必须先信任根证书才能继续", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + // 启动代理 + ProxyHelper.StartProxy(Common.OC.Host); + BtnProxy.Text = Resources.StopProxy; + } + } + catch (Exception ex) + { + Logger.E(TAG, "Start Proxy failed.", ex); + MessageBox.Show(ex.Message, Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + #endregion } } \ No newline at end of file diff --git a/Source/GrasscutterTools/Pages/PageOpenCommand.resx b/Source/GrasscutterTools/Pages/PageOpenCommand.resx index dd988d4..a925056 100644 --- a/Source/GrasscutterTools/Pages/PageOpenCommand.resx +++ b/Source/GrasscutterTools/Pages/PageOpenCommand.resx @@ -151,7 +151,7 @@ $this - 0 + 1 None @@ -184,7 +184,7 @@ $this - 1 + 2 None @@ -214,7 +214,7 @@ $this - 2 + 3 None @@ -244,7 +244,7 @@ $this - 3 + 4 None @@ -274,7 +274,7 @@ $this - 4 + 5 None @@ -307,7 +307,7 @@ $this - 5 + 6 None @@ -514,7 +514,7 @@ $this - 6 + 7 None @@ -923,7 +923,7 @@ $this - 7 + 8 None @@ -932,7 +932,7 @@ 136, 34 - 182, 23 + 182, 25 2 @@ -941,13 +941,13 @@ TxtHost - System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 $this - 8 + 9 None @@ -977,7 +977,7 @@ $this - 9 + 10 None @@ -1010,7 +1010,34 @@ $this - 10 + 11 + + + False + + + 3, 3 + + + 120, 25 + + + 11 + + + 启动代理 + + + BtnProxy + + + System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 0 True @@ -1022,6 +1049,6 @@ PageOpenCommand - GrasscutterTools.Pages.BasePage, GrasscutterTools, Version=1.7.4.0, Culture=neutral, PublicKeyToken=de2b1c089621e923 + GrasscutterTools.Pages.BasePage, GrasscutterTools, Version=1.13.0.0, Culture=neutral, PublicKeyToken=de2b1c089621e923 \ No newline at end of file diff --git a/Source/GrasscutterTools/Properties/AssemblyInfo.cs b/Source/GrasscutterTools/Properties/AssemblyInfo.cs index 15ba806..76f1cef 100644 --- a/Source/GrasscutterTools/Properties/AssemblyInfo.cs +++ b/Source/GrasscutterTools/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; //可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 //通过使用 "*",如下所示: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.12.2")] -[assembly: AssemblyFileVersion("1.12.2")] \ No newline at end of file +[assembly: AssemblyVersion("1.13.0")] +[assembly: AssemblyFileVersion("1.13.0")] \ No newline at end of file diff --git a/Source/GrasscutterTools/Properties/Resources.Designer.cs b/Source/GrasscutterTools/Properties/Resources.Designer.cs index 555572b..22021d2 100644 --- a/Source/GrasscutterTools/Properties/Resources.Designer.cs +++ b/Source/GrasscutterTools/Properties/Resources.Designer.cs @@ -112,6 +112,8 @@ namespace GrasscutterTools.Properties { ///2020:3.5风花的呼吸 ///2021:3.6盛典与慧业 ///2022:3.7决战!召唤之巅! + ///2023:3.8清夏!乐园?大秘境! + ///2024:4.0机枢巧物前哨战 ///// 1.0 ///1001:海灯节 ///5001:元素烘炉(test) @@ -120,10 +122,7 @@ namespace GrasscutterTools.Properties { ///// 1.1 ///5004:映天之章 ///5005:元素烘炉 - ///5006:佳肴尚温 - ///5007:飞行挑战 - ///5009:古闻之章(钟离传说-1) - ///50 [字符串的其余部分被截断]"; 的本地化字符串。 + ///5006:佳 [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Activity { get { @@ -195,6 +194,8 @@ namespace GrasscutterTools.Properties { ///28:乐园遗落之花 ///29:水仙之梦 ///30:花海甘露之光 + ///31:逐影猎人 + ///32:黄金剧团 ///51:行者之心 ///52:勇士之心 ///53:守护之心 @@ -347,9 +348,9 @@ namespace GrasscutterTools.Properties { /// /// 查找类似 1002:神里绫华 ///1003:琴 - ///1005:空 + ///1005:男主 ///1006:丽莎 - ///1007:荧 + ///1007:女主 ///1014:芭芭拉 ///1015:凯亚 ///1016:迪卢克 @@ -392,13 +393,13 @@ namespace GrasscutterTools.Properties { ///1058:八重神子 ///1059:鹿野院平藏 ///1060:夜兰 - ///1061:绮良良 ///1062:埃洛伊 ///1063:申鹤 ///1064:云堇 ///1065:久岐忍 ///1066:神里绫人 - ///1067: [字符串的其余部分被截断]"; 的本地化字符串。 + ///1067:柯莱 + ///1068 [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Avatar { get { @@ -407,8 +408,7 @@ namespace GrasscutterTools.Properties { } /// - /// 查找类似 1001:4 - ///1002:5 + /// 查找类似 1002:5 ///1003:5 ///1005:5 ///1006:4 @@ -455,6 +455,7 @@ namespace GrasscutterTools.Properties { ///1058:5 ///1059:4 ///1060:5 + ///1061:4 ///1062:5 ///1063:5 ///1064:4 @@ -843,33 +844,32 @@ namespace GrasscutterTools.Properties { } /// - /// 查找类似 // 特殊能力 - ///44000001:使用冲刺后会在原地留下一个n秒后爆炸的能量波 - ///44000002:飞行挑战玩法:空中冲刺 前冲 - ///44000003:飞行挑战玩法:空中冲刺 上冲1 - ///44000004:飞行挑战玩法:空中冲刺 上冲2 - ///44000005:使用冲刺后会在原地留下一个n秒后爆炸的能量波 - ///44000006:范围内回复生命、能量、复活 - ///44000007:使用冲刺后会在原地留下一个n秒后爆炸的能量波 - ///44000008:没有敌人时提升输出 - ///44000009:没有敌人时提升输出 - ///44000010:范围伤害 - ///44000100:备用02 - ///44000101:吐泡泡子弹(远) - ///44000102:吐泡泡子弹(近) - ///44000103:备用02 - ///44000104:备用02 - ///44000105:捉迷藏能量球 - ///44000106:捉迷藏-技能-引导 - ///44000107:捉迷藏-技能-诱饵 - ///44000108:捉迷藏-牢 - ///44000109:捉迷藏-牢 - ///44000110:羽球节 - ///44000111:羽球节 - ///44000112:羽球节 - ///44000113:羽球节 - ///44000114:羽球节 - ///440001 [字符串的其余部分被截断]"; 的本地化字符串。 + /// 查找类似 // 机关装置 + ///40000001:蒸汽桶炮 + ///70220032:风车机关 + ///70220033:风车机关02-永动风车机关 + ///70290584:升降铁栅栏0.8倍版-升降铁栅栏 + ///70310300:风元素交互 水方碑 + ///70310301:火元素交互 水方碑 + ///70320001:旋转喷火机关 + ///70320002:单面喷火机关 + ///70320003:地城循环点光源 + ///70330086:雷元素溢出洞口 + ///70330332:沙漠 火元素方碑-沙漠 火元素方碑 + ///70330400:沙漠 雷元素方碑 + ///70330401:沙漠 冰元素方碑 + ///70330402:沙漠 风元素方碑 + ///70330403:沙漠 水元素方碑 + ///70330404:沙漠 草元素方碑 + ///70330405:沙漠 岩元素方碑 + ///70330441:海市蜃楼-火元素方碑 + ///70350001:地城大门01-原始门(废弃) + ///70350002:地城大门02-地城大门 倒品 大 + ///70350003:地城大门03-地城大门 倒品 小 + ///70350004:升降铁栅栏 + ///70350005:横向机关门 + ///70350006:升降铁栅栏-升降铁栅栏 大 + ///70350007:丘丘人升降 [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Gadget { get { @@ -953,10 +953,7 @@ namespace GrasscutterTools.Properties { } /// - /// 查找类似 // Items - /// - /// - ///// 虚拟物品 + /// 查找类似 // 虚拟道具 ///101:角色经验 ///102:冒险阅历 ///105:好感经验 @@ -1004,7 +1001,8 @@ namespace GrasscutterTools.Properties { ///147:节庆热度 ///148:营业收入 ///149:可用资金 - ///150: [字符串的其余部分被截断]"; 的本地化字符串。 + ///150:巧策灵感 + ///151:蘑菇宝钱 [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Item { get { @@ -1370,6 +1368,7 @@ namespace GrasscutterTools.Properties { ///6:层岩巨渊·地下矿区 ///7:三界路飨祭 ///9:金苹果群岛(2.8) + ///10:Penumbra_LevelStreaming ///1001:移动平台性能测试(test) ///1002:攀爬测试2 ///1003:TheBigWorld @@ -1386,12 +1385,10 @@ namespace GrasscutterTools.Properties { ///1018:Chateau ///1019:洞天云海地城玩法测试(test) ///1023:Level_Yurenzhong - ///1024:黑夜循环地城(test) + ///1024:突破:清扫遗迹中的魔物(test) ///1030:TestIntercept_LiYue ///1031:爬塔丘丘人模板(test) - ///1032:云海白盒测试(test) - ///1033:Indoor_Ly_Bank - ///1 [字符串的其余部分被截断]"; 的本地化字符串。 + ///1032:云海白 [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Scene { get { @@ -1490,6 +1487,24 @@ namespace GrasscutterTools.Properties { } } + /// + /// 查找类似 启动代理 的本地化字符串。 + /// + internal static string StartProxy { + get { + return ResourceManager.GetString("StartProxy", resourceCulture); + } + } + + /// + /// 查找类似 关闭代理 的本地化字符串。 + /// + internal static string StopProxy { + get { + return ResourceManager.GetString("StopProxy", resourceCulture); + } + } + /// /// 查找类似 任务已经启动,无法操作 的本地化字符串。 /// @@ -1574,6 +1589,9 @@ namespace GrasscutterTools.Properties { ///11420:「一心传」名刀 ///11421:「一心传」名刀 ///11422:东花坊时雨 + ///11424:狼牙 + ///11425:海渊终曲 + ///11426:灰河渡手 ///11501:风鹰剑 ///11502:天空之刃 ///11503:苍古自由之誓 @@ -1584,10 +1602,7 @@ namespace GrasscutterTools.Properties { ///11511:圣显之钥 ///11512:裁叶萃光 ///12101:训练大剑 - ///12201:佣兵重剑 - ///12301:铁影阔剑 - ///12302:沐浴龙血的剑 - ///12 [字符串的其余部分被截断]"; 的本地化字符串。 + ///12201: [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string Weapon { get { @@ -1596,7 +1611,9 @@ namespace GrasscutterTools.Properties { } /// - /// 查找类似 11301:blue + /// 查找类似 11101:blue + ///11201:blue + ///11301:blue ///11302:blue ///11303:blue ///11304:blue @@ -1623,17 +1640,15 @@ namespace GrasscutterTools.Properties { ///11420:purple ///11421:purple ///11422:purple + ///11424:purple + ///11425:purple + ///11426:purple ///11501:yellow ///11502:yellow ///11503:yellow ///11504:yellow ///11505:yellow - ///11509:yellow - ///11510:yellow - ///11511:yellow - ///11512:yellow - ///12301:blue - ///12302:bl [字符串的其余部分被截断]"; 的本地化字符串。 + ///11509:yell [字符串的其余部分被截断]"; 的本地化字符串。 /// internal static string WeaponColor { get { diff --git a/Source/GrasscutterTools/Properties/Resources.en-us.resx b/Source/GrasscutterTools/Properties/Resources.en-us.resx index 6bebe71..5fe663d 100644 --- a/Source/GrasscutterTools/Properties/Resources.en-us.resx +++ b/Source/GrasscutterTools/Properties/Resources.en-us.resx @@ -366,4 +366,10 @@ Improvement suggestions have been submitted, please use caution to send emails t ..\Resources\en-us\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + Start Proxy + + + Stop Proxy + \ No newline at end of file diff --git a/Source/GrasscutterTools/Properties/Resources.resx b/Source/GrasscutterTools/Properties/Resources.resx index bcdd9fb..1ea1cb6 100644 --- a/Source/GrasscutterTools/Properties/Resources.resx +++ b/Source/GrasscutterTools/Properties/Resources.resx @@ -375,4 +375,10 @@ 快捷键 + + 启动代理 + + + 关闭代理 + \ No newline at end of file diff --git a/Source/GrasscutterTools/Properties/Resources.ru-ru.resx b/Source/GrasscutterTools/Properties/Resources.ru-ru.resx index b1270c9..5d3d82e 100644 --- a/Source/GrasscutterTools/Properties/Resources.ru-ru.resx +++ b/Source/GrasscutterTools/Properties/Resources.ru-ru.resx @@ -354,4 +354,10 @@ ..\Resources\ru-ru\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + Запустить прокси + + + Остановить прокси + \ No newline at end of file diff --git a/Source/GrasscutterTools/Properties/Resources.zh-TW.resx b/Source/GrasscutterTools/Properties/Resources.zh-TW.resx index 714781f..c4f41f1 100644 --- a/Source/GrasscutterTools/Properties/Resources.zh-TW.resx +++ b/Source/GrasscutterTools/Properties/Resources.zh-TW.resx @@ -360,4 +360,10 @@ ..\Resources\zh-tw\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + 啟動代理 + + + 關閉代理 + \ No newline at end of file diff --git a/Source/GrasscutterTools/Utils/Logger.cs b/Source/GrasscutterTools/Utils/Logger.cs index e65e99d..6bc6fa0 100644 --- a/Source/GrasscutterTools/Utils/Logger.cs +++ b/Source/GrasscutterTools/Utils/Logger.cs @@ -31,15 +31,26 @@ namespace GrasscutterTools.Utils private static void Write(string message) { +#if DEBUG + Console.WriteLine($"{DateTime.Now:mm:ss.fff} {message}"); +#else if (IsSaveLogs) { Console.WriteLine($"{DateTime.Now:mm:ss.fff} {message}"); File.AppendAllText(LogPath, $"{DateTime.Now:mm:ss.fff} {message}{Environment.NewLine}"); } +#endif } private static void Write(string level, string tag, string message) => Write($"<{level}:{tag}> {message}"); + //public static void Debug(string message) => Write("DEBUG", "Proxy", message); + + //public static void Info(string info) => Write("INFO", "Proxy", info); + + //public static void Error(Exception ex) => Write("ERROR", "Proxy", ex.ToString()); + + public static void I(string tag, string info) => Write("INFO", tag, info); public static void W(string tag, string message) => Write("WARR", tag, message); diff --git a/Source/GrasscutterTools/Utils/ProxyHelper.cs b/Source/GrasscutterTools/Utils/ProxyHelper.cs new file mode 100644 index 0000000..2bce984 --- /dev/null +++ b/Source/GrasscutterTools/Utils/ProxyHelper.cs @@ -0,0 +1,195 @@ +/** + * Grasscutter Tools + * Copyright (C) 2023 jie65535 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + **/ + +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Win32; +using Eavesdrop; +using System.Net; + +namespace GrasscutterTools.Utils +{ + internal static class ProxyHelper + { + private const string TAG = "Proxy"; + + #region - Windows API - + + [DllImport("wininet.dll")] + private static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength); + private const int INTERNET_OPTION_SETTINGS_CHANGED = 39; + private const int INTERNET_OPTION_REFRESH = 37; + + #endregion + + #region - System Proxy - + + private const string RegKeyInternetSettings = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; + private const string RegProxyEnable = "ProxyEnable"; + private const string RegProxyServer = "ProxyServer"; + private const string RegProxyOverride = "ProxyOverride"; + + private const string PassBy = + "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*"; + + private static void FlushOs() + { + InternetSetOption(IntPtr.Zero, INTERNET_OPTION_SETTINGS_CHANGED, IntPtr.Zero, 0); + InternetSetOption(IntPtr.Zero, INTERNET_OPTION_REFRESH, IntPtr.Zero, 0); + } + + private static bool _isSetSystemProxy; + + private static void SetSystemProxy(int proxyPort) + { + using (var reg = Registry.CurrentUser.OpenSubKey(RegKeyInternetSettings, true)) + { + reg.SetValue(RegProxyServer, $"http=127.0.0.1:{proxyPort};https=127.0.0.1:{proxyPort}"); + reg.SetValue(RegProxyOverride, PassBy); + reg.SetValue(RegProxyEnable, 1); + } + + _isSetSystemProxy = true; + FlushOs(); + } + + private static void CloseSystemProxy() + { + if (!_isSetSystemProxy) return; + _isSetSystemProxy = false; + + using (var reg = Registry.CurrentUser.OpenSubKey(RegKeyInternetSettings, true)) + reg.SetValue(RegProxyEnable, 0); + FlushOs(); + } + + #endregion + + #region - GS Proxy Server - + + private const string ProxyOverrides = + "localhost;1*;" + //" 127.*;10.*;192.168.*;" + + "*0;*1;*2;*3;*4;*5;*6;*7;*8;*9;" + + "*a;*b;*c;*d;*e;*f;*g;*h;*i;*j;*k;*l;*n;*o;*p;*q;*r;*s;*t;*u;*v;*w;*x;*y;*z" + + "*a.com;*b.com;*c.com;*d.com;*f.com;*g.com;*h.com;*i.com;*j.com;*k.com;*l.com;*m.com;*p.com;*q.com;*r.com;*s.com;*t.com;*u.com;*v.com;*w.com;*x.com;*y.com;*z.com;" + + "*ae.com;*be.com;*ce.com;*de.com;*fe.com;*ge.com;*pe.com;*te.com;*me.com;*le.com;*ve.com;" + + "*ao.com;*bo.com;*eo.com;*go.com;*ke.com;*oo.com;*so.com;*io.com" + + "*an.com;*cn.com;*dn.com;*gn.com;*wn.com;*dn.com;*sn.com;*un.com;*in.com"; + //"*bing*;*google*;*live.com;*office.com;*weibo*;*yahoo*;*taobao*;*go.com;*csdn.com;*msn.com;*aliyun.com;*cdn.com;"; + //"*ttvnw*;*edge*;*microsoft*;*bing*;*google*;*discordapp*;*gstatic.com;*imgur.com;*hub.*;*gitlab.com;*googleapis.com;*facebook.com;*cloudfront.net;*gvt1.com;*jquery.com;*akamai.net;*ultra-rv.com;*youtube*;*ytimg*;*ggpht*;" + + //"*baidu*;*qq*;*sohu*;*weibo*;*163*;*360*;*iqiyi*;*youku*;*bilibili*;*sogou*;*taobao*;*jd*;*zhihu*;*steam*;*ea.com;*csdn*;*.msn.*;*aliyun*;*cdn*;" + + //"*twitter.com;*instagram.com;*wikipedia.org;*yahoo*;*xvideos.com;*whatsapp.com;*live.com;*netflix.com;*office.com;*tiktok.com;*reddit.com;*discord*;*twitch*;*duckduckgo.com"; + + private static string[] urls = + { + "hoyoverse.com", + "mihoyo.com", + "yuanshen.com", + }; + + private static void StartGsProxyServer(int port) + { + if (Eavesdropper.IsRunning) return; + Eavesdropper.Overrides.Clear(); + Eavesdropper.Overrides.AddRange(ProxyOverrides.Split(';')); + Eavesdropper.RequestInterceptedAsync += EavesdropperOnRequestInterceptedAsync; + Eavesdropper.Initiate(port); + } + + private static Task EavesdropperOnRequestInterceptedAsync(object sender, RequestInterceptedEventArgs e) + { + var url = e.Request.RequestUri.OriginalString; + foreach (var mhy in urls) + { + var i = url.IndexOf(mhy, StringComparison.CurrentCultureIgnoreCase); + if (i == -1) continue; + var p = url.IndexOf('/', i + mhy.Length); + var target = p >= 0 ? _gcDispatch + url.Substring(p) : _gcDispatch; + e.Request = RedirectRequest(e.Request as HttpWebRequest, new Uri(target)); + Logger.I(TAG, $"Redirect to {e.Request.RequestUri}"); + return Task.CompletedTask; + } + + Logger.I(TAG, "Direct " + e.Request.RequestUri); + + return Task.CompletedTask; + } + + private static HttpWebRequest RedirectRequest(HttpWebRequest request, Uri newUri) + { + var newRequest = WebRequest.CreateHttp(newUri); + newRequest.ProtocolVersion = request.ProtocolVersion; + newRequest.CookieContainer = request.CookieContainer; + newRequest.AllowAutoRedirect = request.AllowAutoRedirect; + newRequest.KeepAlive = request.KeepAlive; + newRequest.Method = request.Method; + newRequest.Proxy = request.Proxy; + foreach (var name in request.Headers.AllKeys) + { + switch (name.ToLower()) + { + case "host" : newRequest.Host = request.Host; break; + case "accept" : newRequest.Accept = request.Accept; break; + case "referer" : newRequest.Referer = request.Referer; break; + case "user-agent" : newRequest.UserAgent = request.UserAgent; break; + case "content-type" : newRequest.ContentType = request.ContentType; break; + case "content-length" : newRequest.ContentLength = request.ContentLength; break; + case "if-modified-since": newRequest.IfModifiedSince = request.IfModifiedSince; break; + case "date" : newRequest.Date = request.Date; break; + default: newRequest.Headers[name] = request.Headers[name]; break; + } + } + //newRequest.Headers = request.Headers; + return newRequest; + } + + private static void StopGsProxyServer() + { + Eavesdropper.Terminate(); + } + + #endregion + + private const int ProxyServerPort = 8146; + private static string _gcDispatch; + public static void StartProxy(string gcDispatch) + { + _gcDispatch = gcDispatch.TrimEnd('/'); + Logger.I(TAG, "Start Proxy, redirect to " + _gcDispatch); + StartGsProxyServer(ProxyServerPort); + //SetSystemProxy(ProxyServerPort); + } + + public static void StopProxy() + { + Logger.I(TAG, "Stop Proxy"); + //CloseSystemProxy(); + StopGsProxyServer(); + } + + public static bool CheckAndCreateCertifier() + { + Eavesdropper.Certifier = new Certifier("jie65535", "GrasscutterTools Root Certificate Authority"); + return Eavesdropper.Certifier.CreateTrustedRootCertificate(); + } + + public static bool IsRunning => Eavesdropper.IsRunning; + } +} diff --git a/Source/GrasscutterTools/Utils/SystemProxy.cs b/Source/GrasscutterTools/Utils/SystemProxy.cs new file mode 100644 index 0000000..6e8d725 --- /dev/null +++ b/Source/GrasscutterTools/Utils/SystemProxy.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GrasscutterTools.Utils +{ + public class SystemProxy + { + + } +}