Add HTTP(S) proxy (#208)

This commit is contained in:
2023-09-23 10:48:24 +08:00 committed by GitHub
parent 41c644f2af
commit dce7e54675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1648 additions and 74 deletions

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

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
using System;
namespace Eavesdrop
{
[Flags]
public enum Interceptors
{
None = 0,
HTTP = 1,
HTTPS = 2,
Default = (HTTP | HTTPS)
}
}

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

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

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

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

View File

@ -40,6 +40,8 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>11</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@ -49,6 +51,8 @@
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>11</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\IconGrasscutter.ico</ApplicationIcon>
@ -87,6 +91,14 @@
<ItemGroup>
<Compile Include="DispatchServer\DispatchServerAPI.cs" />
<Compile Include="DispatchServer\Model\ServerStatus.cs" />
<Compile Include="Eavesdrop\Certifier.cs" />
<Compile Include="Eavesdrop\Eavesdropper.cs" />
<Compile Include="Eavesdrop\Event Args\RequestInterceptedEventArgs.cs" />
<Compile Include="Eavesdrop\Event Args\ResponseInterceptedEventArgs.cs" />
<Compile Include="Eavesdrop\Interceptors.cs" />
<Compile Include="Eavesdrop\Internals\INETOptions.cs" />
<Compile Include="Eavesdrop\Internals\NativeMethods.cs" />
<Compile Include="Eavesdrop\Network\EavesNode.cs" />
<Compile Include="Forms\FormActivityEditor.cs">
<SubType>Form</SubType>
</Compile>
@ -319,6 +331,7 @@
<Compile Include="Utils\HttpHelper.cs" />
<Compile Include="Utils\Logger.cs" />
<Compile Include="Utils\GithubHelper.cs" />
<Compile Include="Utils\ProxyHelper.cs" />
<Compile Include="Utils\SparseSet.cs" />
<Compile Include="Utils\ToggleParser.cs" />
<Compile Include="Utils\UIUtil.cs" />
@ -669,6 +682,7 @@
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<None Include="Eavesdrop\LICENSE.md" />
<None Include="GrasscutterTools.licenseheader" />
<None Include="GrasscutterTools.snk" />
<None Include="Properties\Settings.settings">

View File

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

View File

@ -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);
}
}
/// <summary>
@ -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 -
/// <summary>
/// 点击代理按钮时触发
/// </summary>
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
}
}

View File

@ -151,7 +151,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LnkLinks.ZOrder" xml:space="preserve">
<value>0</value>
<value>1</value>
</data>
<data name="LnkGOODHelp.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -184,7 +184,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LnkGOODHelp.ZOrder" xml:space="preserve">
<value>1</value>
<value>2</value>
</data>
<data name="LnkInventoryKamera.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -214,7 +214,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LnkInventoryKamera.ZOrder" xml:space="preserve">
<value>2</value>
<value>3</value>
</data>
<data name="LblGOODHelp.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -244,7 +244,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LblGOODHelp.ZOrder" xml:space="preserve">
<value>3</value>
<value>4</value>
</data>
<data name="ButtonOpenGOODImport.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -274,7 +274,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;ButtonOpenGOODImport.ZOrder" xml:space="preserve">
<value>4</value>
<value>5</value>
</data>
<data name="LblHostTip.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -307,7 +307,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LblHostTip.ZOrder" xml:space="preserve">
<value>5</value>
<value>6</value>
</data>
<data name="GrpServerStatus.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -514,7 +514,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;GrpServerStatus.ZOrder" xml:space="preserve">
<value>6</value>
<value>7</value>
</data>
<data name="GrpRemoteCommand.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -923,7 +923,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;GrpRemoteCommand.ZOrder" xml:space="preserve">
<value>7</value>
<value>8</value>
</data>
<data name="TxtHost.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -932,7 +932,7 @@
<value>136, 34</value>
</data>
<data name="TxtHost.Size" type="System.Drawing.Size, System.Drawing">
<value>182, 23</value>
<value>182, 25</value>
</data>
<data name="TxtHost.TabIndex" type="System.Int32, mscorlib">
<value>2</value>
@ -941,13 +941,13 @@
<value>TxtHost</value>
</data>
<data name="&gt;&gt;TxtHost.Type" xml:space="preserve">
<value>System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="&gt;&gt;TxtHost.Parent" xml:space="preserve">
<value>$this</value>
</data>
<data name="&gt;&gt;TxtHost.ZOrder" xml:space="preserve">
<value>8</value>
<value>9</value>
</data>
<data name="BtnQueryServerStatus.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -977,7 +977,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;BtnQueryServerStatus.ZOrder" xml:space="preserve">
<value>9</value>
<value>10</value>
</data>
<data name="LblHost.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>None</value>
@ -1010,7 +1010,34 @@
<value>$this</value>
</data>
<data name="&gt;&gt;LblHost.ZOrder" xml:space="preserve">
<value>10</value>
<value>11</value>
</data>
<data name="BtnProxy.Enabled" type="System.Boolean, mscorlib">
<value>False</value>
</data>
<data name="BtnProxy.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 3</value>
</data>
<data name="BtnProxy.Size" type="System.Drawing.Size, System.Drawing">
<value>120, 25</value>
</data>
<data name="BtnProxy.TabIndex" type="System.Int32, mscorlib">
<value>11</value>
</data>
<data name="BtnProxy.Text" xml:space="preserve">
<value>启动代理</value>
</data>
<data name="&gt;&gt;BtnProxy.Name" xml:space="preserve">
<value>BtnProxy</value>
</data>
<data name="&gt;&gt;BtnProxy.Type" xml:space="preserve">
<value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="&gt;&gt;BtnProxy.Parent" xml:space="preserve">
<value>$this</value>
</data>
<data name="&gt;&gt;BtnProxy.ZOrder" xml:space="preserve">
<value>0</value>
</data>
<metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
@ -1022,6 +1049,6 @@
<value>PageOpenCommand</value>
</data>
<data name="&gt;&gt;$this.Type" xml:space="preserve">
<value>GrasscutterTools.Pages.BasePage, GrasscutterTools, Version=1.7.4.0, Culture=neutral, PublicKeyToken=de2b1c089621e923</value>
<value>GrasscutterTools.Pages.BasePage, GrasscutterTools, Version=1.13.0.0, Culture=neutral, PublicKeyToken=de2b1c089621e923</value>
</data>
</root>

View File

@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.12.2")]
[assembly: AssemblyFileVersion("1.12.2")]
[assembly: AssemblyVersion("1.13.0")]
[assembly: AssemblyFileVersion("1.13.0")]

View File

@ -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 [字符串的其余部分被截断]&quot;; 的本地化字符串。
///5006:佳 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
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 {
/// <summary>
/// 查找类似 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: [字符串的其余部分被截断]&quot;; 的本地化字符串。
///1067:柯莱
///1068 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
internal static string Avatar {
get {
@ -407,8 +408,7 @@ namespace GrasscutterTools.Properties {
}
/// <summary>
/// 查找类似 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 {
}
/// <summary>
/// 查找类似 // 特殊能力
///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 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// 查找类似 // 机关装置
///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:丘丘人升降 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
internal static string Gadget {
get {
@ -953,10 +953,7 @@ namespace GrasscutterTools.Properties {
}
/// <summary>
/// 查找类似 // Items
///
///
///// 虚拟物品
/// 查找类似 // 虚拟道具
///101:角色经验
///102:冒险阅历
///105:好感经验
@ -1004,7 +1001,8 @@ namespace GrasscutterTools.Properties {
///147:节庆热度
///148:营业收入
///149:可用资金
///150: [字符串的其余部分被截断]&quot;; 的本地化字符串。
///150:巧策灵感
///151:蘑菇宝钱 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
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 [字符串的其余部分被截断]&quot;; 的本地化字符串。
///1032:云海白 [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
internal static string Scene {
get {
@ -1490,6 +1487,24 @@ namespace GrasscutterTools.Properties {
}
}
/// <summary>
/// 查找类似 启动代理 的本地化字符串。
/// </summary>
internal static string StartProxy {
get {
return ResourceManager.GetString("StartProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 关闭代理 的本地化字符串。
/// </summary>
internal static string StopProxy {
get {
return ResourceManager.GetString("StopProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 任务已经启动,无法操作 的本地化字符串。
/// </summary>
@ -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 [字符串的其余部分被截断]&quot;; 的本地化字符串。
///12201: [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
internal static string Weapon {
get {
@ -1596,7 +1611,9 @@ namespace GrasscutterTools.Properties {
}
/// <summary>
/// 查找类似 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 [字符串的其余部分被截断]&quot;; 的本地化字符串。
///11509:yell [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary>
internal static string WeaponColor {
get {

View File

@ -366,4 +366,10 @@ Improvement suggestions have been submitted, please use caution to send emails t
<data name="Gadget" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\en-us\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="StartProxy" xml:space="preserve">
<value>Start Proxy</value>
</data>
<data name="StopProxy" xml:space="preserve">
<value>Stop Proxy</value>
</data>
</root>

View File

@ -375,4 +375,10 @@
<data name="PageHotKey" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="StartProxy" xml:space="preserve">
<value>启动代理</value>
</data>
<data name="StopProxy" xml:space="preserve">
<value>关闭代理</value>
</data>
</root>

View File

@ -354,4 +354,10 @@
<data name="Gadget" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\ru-ru\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="StartProxy" xml:space="preserve">
<value>Запустить прокси</value>
</data>
<data name="StopProxy" xml:space="preserve">
<value>Остановить прокси</value>
</data>
</root>

View File

@ -360,4 +360,10 @@
<data name="Gadget" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zh-tw\Gadget.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="StartProxy" xml:space="preserve">
<value>啟動代理</value>
</data>
<data name="StopProxy" xml:space="preserve">
<value>關閉代理</value>
</data>
</root>

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
**/
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;
}
}

View File

@ -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
{
}
}