mirror of
https://github.com/jie65535/GrasscutterCommandGenerator.git
synced 2025-10-20 19:39:47 +08:00
Add HTTP(S) proxy (#208)
This commit is contained in:
202
Source/GrasscutterTools/Eavesdrop/Certifier.cs
Normal file
202
Source/GrasscutterTools/Eavesdrop/Certifier.cs
Normal file
@@ -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<string, X509Certificate2> _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<string, X509Certificate2>();
|
||||
|
||||
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<string> subjectNames = certificates.Cast<X509Certificate2>().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();
|
||||
}
|
||||
}
|
200
Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs
Normal file
200
Source/GrasscutterTools/Eavesdrop/Eavesdropper.cs
Normal file
@@ -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<TEventArgs>(object sender, TEventArgs e);
|
||||
|
||||
public static event AsyncEventHandler<RequestInterceptedEventArgs> RequestInterceptedAsync;
|
||||
private static async Task OnRequestInterceptedAsync(RequestInterceptedEventArgs e)
|
||||
{
|
||||
Task interceptedTask = RequestInterceptedAsync?.Invoke(null, e);
|
||||
if (interceptedTask != null)
|
||||
{
|
||||
await interceptedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public static event AsyncEventHandler<ResponseInterceptedEventArgs> ResponseInterceptedAsync;
|
||||
private static async Task OnResponseInterceptedAsync(ResponseInterceptedEventArgs e)
|
||||
{
|
||||
Task interceptedTask = ResponseInterceptedAsync?.Invoke(null, e);
|
||||
if (interceptedTask != null)
|
||||
{
|
||||
await interceptedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<string> 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<string>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
14
Source/GrasscutterTools/Eavesdrop/Interceptors.cs
Normal file
14
Source/GrasscutterTools/Eavesdrop/Interceptors.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Eavesdrop
|
||||
{
|
||||
[Flags]
|
||||
public enum Interceptors
|
||||
{
|
||||
None = 0,
|
||||
HTTP = 1,
|
||||
HTTPS = 2,
|
||||
|
||||
Default = (HTTP | HTTPS)
|
||||
}
|
||||
}
|
241
Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs
Normal file
241
Source/GrasscutterTools/Eavesdrop/Internals/INETOptions.cs
Normal file
@@ -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<string> 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<string>();
|
||||
Load();
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
var options = new List<INETOption>(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 == "<local>")
|
||||
{
|
||||
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<string>(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<string>(Overrides);
|
||||
if (IsIgnoringLocalTraffic)
|
||||
{
|
||||
overrides.Add("<local>");
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
12
Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs
Normal file
12
Source/GrasscutterTools/Eavesdrop/Internals/NativeMethods.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
21
Source/GrasscutterTools/Eavesdrop/LICENSE.md
Normal file
21
Source/GrasscutterTools/Eavesdrop/LICENSE.md
Normal file
@@ -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.
|
365
Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs
Normal file
365
Source/GrasscutterTools/Eavesdrop/Network/EavesNode.cs
Normal file
@@ -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<HttpWebRequest> ReadRequestAsync()
|
||||
{
|
||||
return ReadRequestAsync(null);
|
||||
}
|
||||
private async Task<HttpWebRequest> ReadRequestAsync(Uri baseUri)
|
||||
{
|
||||
string method = null;
|
||||
var headers = new List<string>();
|
||||
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<ByteArrayContent> 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<Cookie> 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<string> 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<byte[]> 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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user