Skip to content

Commit

Permalink
✨🐛 新增 网络加速 二级代理支持
Browse files Browse the repository at this point in the history
修复 Request Header 中存在非 ASCII 字符导致代理异常的状况
close BeyondDimension#2753 BeyondDimension#2523 BeyondDimension#1661 BeyondDimension#488
fix BeyondDimension#1825
  • Loading branch information
rmbadmin committed Jun 28, 2024
1 parent 1eca159 commit 6761502
Show file tree
Hide file tree
Showing 17 changed files with 1,447 additions and 1,044 deletions.
4 changes: 2 additions & 2 deletions build/settings_v4_app.json
Original file line number Diff line number Diff line change
Expand Up @@ -964,8 +964,8 @@
},
{
"TypeName": "bool",
"PropertyName": "UseDoh",
"DefaultValue": "true",
"PropertyName": "UseDoh2",
"DefaultValue": "false",
"DefaultValueIsConst": true,
"Summary": "启用 DNS over HTTPS",
"IsRegionOrEndregion": null,
Expand Down
1 change: 1 addition & 0 deletions src/BD.WTTS.Client.Avalonia/UI/Views/Pages/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
<TextBlock Text="{Binding Path=User.NickName, Mode=OneWay, Source={x:Static s:UserService.Current}}" />
<Border
Padding="5,1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CornerRadius="2"
IsVisible="{Binding Path=User.UserType, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:UserType.Sponsor}}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ sealed class LifetimeHttpHandler : DelegatingHandler

public LifeTimeKey LifeTimeKey { get; }

public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
public LifetimeHttpHandler(IDomainResolver domainResolver, IWebProxy webProxy, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
{
LifeTimeKey = lifeTimeKey;
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver);
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver, webProxy);
timer = new Timer(OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ReverseProxyHttpClient : HttpMessageInvoker
// = new(new ProductHeaderValue(Constants.HARDCODED_APP_NAME_NEW + "_" + DateTimeOffset.UtcNow.Ticks, ThisAssembly.Version));

public ReverseProxyHttpClient(IDomainConfig domainConfig, IDomainResolver domainResolver)
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver), disposeHandler: true)
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver, HttpNoProxy.Instance), disposeHandler: true)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
{
readonly IDomainConfig domainConfig;
readonly IDomainResolver domainResolver;
readonly IWebProxy webProxy;
readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);

public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver)
public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver, IWebProxy webProxy)
{
this.domainConfig = domainConfig;
this.domainResolver = domainResolver;
this.webProxy = webProxy;
InnerHandler = CreateSocketsHttpHandler();
}

Expand Down Expand Up @@ -53,12 +55,199 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
Proxy = HttpNoProxy.Instance,
UseProxy = false,
PreAuthenticate = false,
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
ConnectCallback = ConnectCallback,
EnableMultipleHttp2Connections = true,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8,
ResponseHeaderEncodingSelector = (_, _) => Encoding.UTF8,
};

private async ValueTask<Stream> ConnectThroughProxyAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (context.InitialRequestMessage.RequestUri == null)
{
throw new InvalidOperationException("Request URI is null");
}
var proxyUri = webProxy.GetProxy(context.InitialRequestMessage.RequestUri);
if (proxyUri == null)
{
throw new InvalidOperationException("Proxy URI is null");
}

var proxyEndPoint = new DnsEndPoint(proxyUri.Host, proxyUri.Port);
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(proxyEndPoint, cancellationToken);

var stream = new NetworkStream(socket, ownsSocket: true);

// 确定代理类型
var proxyType = DetermineProxyType(proxyUri);

switch (proxyType)
{
case ExternalProxyType.Http:
await ConnectHttpProxy(stream, context, cancellationToken);
break;
case ExternalProxyType.Socks4:
await ConnectSocks4Proxy(stream, context, cancellationToken);
break;
case ExternalProxyType.Socks5:
await ConnectSocks5Proxy(stream, context, cancellationToken);
break;
}

if (context.InitialRequestMessage.RequestUri.Scheme == "https" || context.InitialRequestMessage.RequestUri.Port == 443)
{
var requestContext = context.InitialRequestMessage.GetRequestContext();
var tlsSniValue = requestContext.TlsSniValue;
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = tlsSniValue.Value,
RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true,
}, cancellationToken);

return sslStream;
}

return stream;
}

private ExternalProxyType DetermineProxyType(Uri proxyUri)
{
return proxyUri.Scheme.ToLower() switch
{
"socks4" => ExternalProxyType.Socks4,
"socks5" => ExternalProxyType.Socks5,
_ => ExternalProxyType.Http
};

throw new NotImplementedException("Proxy type determination needs to be implemented");
}

private async Task ConnectHttpProxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;
var authHeader = credentials != null
? $"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{credentials.UserName}:{credentials.Password}"))}\r\n"
: string.Empty;

var connectRequest = $"CONNECT {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port} HTTP/1.1\r\n" +
$"Host: {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port}\r\n" +
$"{authHeader}\r\n";

var connectBytes = Encoding.ASCII.GetBytes(connectRequest);
await stream.WriteAsync(connectBytes, cancellationToken);

var buffer = new byte[1024];
var bytesRead = await stream.ReadAsync(buffer, cancellationToken);
var response = Encoding.ASCII.GetString(buffer, 0, bytesRead);

if (!response.StartsWith("HTTP/1.1 200"))
{
throw new Exception($"Proxy connection failed: {response}");
}
}

private async Task ConnectSocks4Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;
var userId = credentials?.UserName ?? string.Empty;

var destinationAddress = await GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken).FirstAsync();
var portBytes = BitConverter.GetBytes((ushort)context.DnsEndPoint.Port);
if (BitConverter.IsLittleEndian) Array.Reverse(portBytes);

var request = new byte[] { 0x04, 0x01 }
.Concat(portBytes)
.Concat(destinationAddress.Address.GetAddressBytes())
.Concat(Encoding.ASCII.GetBytes(userId))
.Concat(new byte[] { 0x00 })
.ToArray();

await stream.WriteAsync(request, cancellationToken);

var response = new byte[8];
await stream.ReadAsync(response, cancellationToken);

if (response[1] != 0x5A)
{
throw new Exception($"SOCKS4 proxy connection failed: {response[1]}");
}
}

private async Task ConnectSocks5Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var credentials = webProxy.Credentials as NetworkCredential;

// SOCKS5 握手
byte[] authMethod = credentials != null ? new byte[] { 0x02 } : new byte[] { 0x00 };
await stream.WriteAsync(new byte[] { 0x05, 0x01 }.Concat(authMethod).ToArray(), cancellationToken);

var response = new byte[2];
await stream.ReadAsync(response, cancellationToken);

if (response[0] != 0x05)
{
throw new Exception("SOCKS5 handshake failed");
}

// 如果需要身份验证
if (response[1] == 0x02 && credentials != null)
{
var userBytes = Encoding.ASCII.GetBytes(credentials.UserName);
var passBytes = Encoding.ASCII.GetBytes(credentials.Password);
var authRequest = new byte[] { 0x01, (byte)userBytes.Length }
.Concat(userBytes)
.Concat(new byte[] { (byte)passBytes.Length })
.Concat(passBytes)
.ToArray();

await stream.WriteAsync(authRequest, cancellationToken);

var authResponse = new byte[2];
await stream.ReadAsync(authResponse, cancellationToken);

if (authResponse[1] != 0x00)
{
throw new Exception("SOCKS5 authentication failed");
}
}

// 发送连接请求
byte[] addressBytes;
byte addressType;
if (IPAddress.TryParse(context.DnsEndPoint.Host, out IPAddress? ipAddress))
{
addressBytes = ipAddress.GetAddressBytes();
addressType = (byte)(ipAddress.AddressFamily == AddressFamily.InterNetwork ? 1 : 4);
}
else
{
addressBytes = Encoding.ASCII.GetBytes(context.DnsEndPoint.Host);
addressType = 3; // 域名
}

var request = new byte[] { 0x05, 0x01, 0x00, addressType }
.Concat(addressType == 3 ? new[] { (byte)addressBytes.Length } : Array.Empty<byte>())
.Concat(addressBytes)
.Concat(BitConverter.GetBytes((ushort)context.DnsEndPoint.Port).Reverse())
.ToArray();

await stream.WriteAsync(request, cancellationToken);

var connectResponse = new byte[10];
await stream.ReadAsync(connectResponse, cancellationToken);

if (connectResponse[1] != 0x00)
{
throw new Exception($"SOCKS5 connection failed: {connectResponse[1]}");
}
}

/// <summary>
/// 连接回调
/// </summary>
Expand All @@ -67,6 +256,11 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
/// <returns></returns>
async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (webProxy != null && !HttpNoProxy.IsNoProxy(webProxy))
{
return await ConnectThroughProxyAsync(context, cancellationToken);
}

var innerExceptions = new List<Exception>();
var ipEndPoints = GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.HttpServer/HttpReverseProxyMiddleware.cs

using Microsoft.AspNetCore.Http;
using Yarp.ReverseProxy.Forwarder;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -52,7 +53,20 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)

if (TryGetDomainConfig(url, out var domainConfig) == false)
{
await next(context);
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
{
var httpClient = httpClientFactory.CreateHttpClient("GlobalProxy", defaultDomainConfig);
var destinationPrefix = GetDestinationPrefix(context.Request.Scheme, context.Request.Host, null);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Empty);
if (error != ForwarderError.None)
{
await HandleErrorAsync(context, error);
}
}
else
{
await next(context);
}
return;
}

Expand Down Expand Up @@ -95,22 +109,22 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
context.Request.Headers.UserAgent = domainConfig.UserAgent.Replace("${origin}", context.Request.Headers.UserAgent, StringComparison.OrdinalIgnoreCase);
}

var forwarderRequestConfig = new ForwarderRequestConfig()
{
Version = context.Request.Protocol switch
{
var protocol when protocol.StartsWith("HTTP/2") => System.Net.HttpVersion.Version20,
var protocol when protocol.StartsWith("HTTP/3") => System.Net.HttpVersion.Version30,
_ => System.Net.HttpVersion.Version11,
},
};
//var forwarderRequestConfig = new ForwarderRequestConfig()
//{
// Version = context.Request.Protocol switch
// {
// var protocol when protocol.StartsWith("HTTP/2") => System.Net.HttpVersion.Version20,
// var protocol when protocol.StartsWith("HTTP/3") => System.Net.HttpVersion.Version30,
// _ => System.Net.HttpVersion.Version11,
// },
//};

if (domainConfig.IsServerSideProxy)
{
SetWattHeaders(context, reverseProxyConfig.Service.ServerSideProxyToken);
}

var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, forwarderRequestConfig, HttpTransformer.Empty);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Empty);

if (error != ForwarderError.None)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.Http/HttpClientFactory.cs

// ReSharper disable once CheckNamespace
using System.Net;

namespace BD.WTTS.Services.Implementation;

sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
{
readonly IDomainResolver domainResolver;
readonly IWebProxy webProxy;

/// <summary>
/// 首次生命周期
Expand All @@ -27,9 +30,19 @@ sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
/// </summary>
readonly ConcurrentDictionary<LifeTimeKey, Lazy<LifetimeHttpHandler>> httpHandlerLazyCache = new();

public ReverseProxyHttpClientFactory(IDomainResolver domainResolver)
public ReverseProxyHttpClientFactory(IDomainResolver domainResolver, IReverseProxyConfig reverseProxyConfig)
{
this.domainResolver = domainResolver;
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
{
this.webProxy = new WebProxy($"{reverseProxyConfig.Service.TwoLevelAgentProxyType}://{reverseProxyConfig.Service.TwoLevelAgentIp}:{reverseProxyConfig.Service.TwoLevelAgentPortId}", true, null, null);
if (!string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentUserName) || !string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentPassword))
{
webProxy.Credentials = new NetworkCredential(reverseProxyConfig.Service.TwoLevelAgentUserName, reverseProxyConfig.Service.TwoLevelAgentPassword);
}
}
else
this.webProxy = HttpNoProxy.Instance;
}

public ReverseProxyHttpClient CreateHttpClient(string domain, IDomainConfig domainConfig)
Expand All @@ -43,7 +56,7 @@ Lazy<LifetimeHttpHandler> CreateFirstLifetimeHttpHandlerLazy(LifeTimeKey lifeTim
}

LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime)
=> new(domainResolver, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);
=> new(domainResolver, webProxy, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);

void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler)
{
Expand Down
Loading

0 comments on commit 6761502

Please sign in to comment.