HttpClientHandler多ip连接尝试
This commit is contained in:
parent
e912f06cd4
commit
dee494b754
@ -17,7 +17,7 @@ namespace FastGithub.DomainResolve
|
||||
{
|
||||
private readonly IPEndPoint dns;
|
||||
private readonly IRequestResolver resolver;
|
||||
private readonly TimeSpan timeout = TimeSpan.FromSeconds(5d);
|
||||
private readonly int timeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// DNS客户端
|
||||
@ -28,7 +28,7 @@ namespace FastGithub.DomainResolve
|
||||
this.dns = dns;
|
||||
this.resolver = dns.Port == 53
|
||||
? new TcpRequestResolver(dns)
|
||||
: new UdpRequestResolver(dns, new TcpRequestResolver(dns));
|
||||
: new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -46,10 +46,7 @@ namespace FastGithub.DomainResolve
|
||||
};
|
||||
request.Questions.Add(new Question(new Domain(domain), RecordType.A));
|
||||
var clientRequest = new ClientRequest(this.resolver, request);
|
||||
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.timeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
var response = await clientRequest.Resolve(linkedTokenSource.Token);
|
||||
var response = await clientRequest.Resolve(cancellationToken);
|
||||
return response.AnswerRecords.OfType<IPAddressResourceRecord>().Select(item => item.IPAddress).ToArray();
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -18,21 +19,14 @@ namespace FastGithub.DomainResolve
|
||||
/// </summary>
|
||||
sealed class DomainResolver : IDomainResolver
|
||||
{
|
||||
private readonly IMemoryCache domainResolveCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
private readonly IMemoryCache disableIPAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
|
||||
private readonly DnscryptProxy dnscryptProxy;
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<DomainResolver> logger;
|
||||
|
||||
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d);
|
||||
private readonly TimeSpan disableIPExpiration = TimeSpan.FromMinutes(2d);
|
||||
|
||||
private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(10d);
|
||||
private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(2d);
|
||||
private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d);
|
||||
|
||||
private readonly ConcurrentDictionary<DnsEndPoint, SemaphoreSlim> semaphoreSlims = new();
|
||||
private readonly ConcurrentDictionary<IPEndPoint, SemaphoreSlim> semaphoreSlims = new();
|
||||
private readonly IMemoryCache ipEndPointAvailableCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
private readonly TimeSpan ipEndPointExpiration = TimeSpan.FromMinutes(2d);
|
||||
private readonly TimeSpan ipEndPointConnectTimeout = TimeSpan.FromSeconds(5d);
|
||||
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
@ -41,7 +35,6 @@ namespace FastGithub.DomainResolve
|
||||
/// <param name="fastGithubConfig"></param>
|
||||
/// <param name="logger"></param>
|
||||
public DomainResolver(
|
||||
|
||||
DnscryptProxy dnscryptProxy,
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<DomainResolver> logger)
|
||||
@ -51,39 +44,58 @@ namespace FastGithub.DomainResolve
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置ip不可用
|
||||
/// </summary>
|
||||
/// <param name="address">ip</param>
|
||||
public void SetDisabled(IPAddress address)
|
||||
{
|
||||
this.disableIPAddressCache.Set(address, address, this.disableIPExpiration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新域名解析结果
|
||||
/// </summary>
|
||||
/// <param name="domain">域名</param>
|
||||
public void FlushDomain(DnsEndPoint domain)
|
||||
{
|
||||
this.domainResolveCache.Remove(domain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <exception cref="FastGithubException"></exception>
|
||||
/// <returns></returns>
|
||||
public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default)
|
||||
public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1));
|
||||
await foreach (var address in this.ResolveAsync(domain.Host, cancellationToken))
|
||||
{
|
||||
if (await this.IsAvailableAsync(new IPEndPoint(address, domain.Port), cancellationToken))
|
||||
{
|
||||
return address;
|
||||
}
|
||||
}
|
||||
throw new FastGithubException($"解析不到{domain.Host}可用的IP");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证远程节点是否可连接
|
||||
/// </summary>
|
||||
/// <param name="ipEndPoint"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> IsAvailableAsync(IPEndPoint ipEndPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = this.semaphoreSlims.GetOrAdd(ipEndPoint, _ => new SemaphoreSlim(1, 1));
|
||||
try
|
||||
{
|
||||
await semaphore.WaitAsync(CancellationToken.None);
|
||||
return await this.ResolveCoreAsync(domain, cancellationToken);
|
||||
if (this.ipEndPointAvailableCache.TryGetValue<bool>(ipEndPoint, out var available))
|
||||
{
|
||||
return available;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.ipEndPointConnectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
await socket.ConnectAsync(ipEndPoint, linkedTokenSource.Token);
|
||||
available = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
available = false;
|
||||
}
|
||||
|
||||
this.ipEndPointAvailableCache.Set(ipEndPoint, available, ipEndPointExpiration);
|
||||
return available;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -91,89 +103,46 @@ namespace FastGithub.DomainResolve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <exception cref="FastGithubException"></exception>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress> ResolveCoreAsync(DnsEndPoint domain, CancellationToken cancellationToken)
|
||||
{
|
||||
if (domain.Host == "localhost")
|
||||
{
|
||||
return IPAddress.Loopback;
|
||||
}
|
||||
|
||||
if (this.domainResolveCache.TryGetValue<IPAddress>(domain, out var address) && address != null)
|
||||
{
|
||||
return address;
|
||||
}
|
||||
|
||||
var expiration = this.dnscryptExpiration;
|
||||
address = await this.LookupByDnscryptAsync(domain, cancellationToken);
|
||||
|
||||
if (address == null)
|
||||
{
|
||||
expiration = this.fallbackExpiration;
|
||||
address = await this.LookupByFallbackAsync(domain, cancellationToken);
|
||||
}
|
||||
|
||||
if (address == null)
|
||||
{
|
||||
throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试");
|
||||
}
|
||||
|
||||
// 往往是被污染的dns
|
||||
if (address.Equals(IPAddress.Loopback) == true)
|
||||
{
|
||||
expiration = this.loopbackExpiration;
|
||||
}
|
||||
|
||||
this.domainResolveCache.Set(domain, address, expiration);
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Dnscrypt查找ip
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="domain">域名</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress?> LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken)
|
||||
public async IAsyncEnumerable<IPAddress> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var dns = this.dnscryptProxy.LocalEndPoint;
|
||||
if (dns == null)
|
||||
if (domain == "localhost")
|
||||
{
|
||||
return null;
|
||||
yield return IPAddress.Loopback;
|
||||
yield break;
|
||||
}
|
||||
|
||||
var dnsClient = new DnsClient(dns);
|
||||
var address = await this.LookupAsync(dnsClient, domain, cancellationToken);
|
||||
return address ?? await this.LookupAsync(dnsClient, domain, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回退查找ip
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress?> LookupByFallbackAsync(DnsEndPoint domain, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var dns in this.fastGithubConfig.FallbackDns)
|
||||
var hashSet = new HashSet<IPAddress>();
|
||||
var cryptDns = this.dnscryptProxy.LocalEndPoint;
|
||||
if (cryptDns != null)
|
||||
{
|
||||
var dnsClient = new DnsClient(dns);
|
||||
var address = await this.LookupAsync(dnsClient, domain, cancellationToken);
|
||||
if (address != null)
|
||||
var dnsClient = new DnsClient(cryptDns);
|
||||
foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken))
|
||||
{
|
||||
return address;
|
||||
if (hashSet.Add(address) == true)
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var fallbackDns in this.fastGithubConfig.FallbackDns)
|
||||
{
|
||||
var dnsClient = new DnsClient(fallbackDns);
|
||||
foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken))
|
||||
{
|
||||
if (hashSet.Add(address) == true)
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -183,92 +152,20 @@ namespace FastGithub.DomainResolve
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress?> LookupAsync(DnsClient dnsClient, DnsEndPoint domain, CancellationToken cancellationToken)
|
||||
private async Task<IPAddress[]> LookupAsync(DnsClient dnsClient, string domain, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addresses = await dnsClient.LookupAsync(domain.Host, cancellationToken);
|
||||
var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
|
||||
|
||||
if (address == null)
|
||||
{
|
||||
this.logger.LogWarning($"{dnsClient}解析不到{domain.Host}可用的ip解析");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.LogInformation($"{dnsClient}: {domain.Host}->{address}");
|
||||
}
|
||||
return address;
|
||||
var addresses = await dnsClient.LookupAsync(domain, cancellationToken);
|
||||
var items = string.Join(", ", addresses.Select(item => item.ToString()));
|
||||
this.logger.LogInformation($"{dnsClient}:{domain}->[{items}]");
|
||||
return addresses;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
this.logger.LogWarning($"{dnsClient}无法解析{domain.Host}:{ex.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最快的ip
|
||||
/// </summary>
|
||||
/// <param name="addresses"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress?> FindFastValueAsync(IEnumerable<IPAddress> addresses, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
addresses = addresses.Where(IsEnableIPAddress).ToArray();
|
||||
if (addresses.Any() == false)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (port <= 0)
|
||||
{
|
||||
return addresses.FirstOrDefault();
|
||||
}
|
||||
|
||||
var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken));
|
||||
var fastTask = await Task.WhenAny(tasks);
|
||||
return await fastTask;
|
||||
|
||||
bool IsEnableIPAddress(IPAddress address)
|
||||
{
|
||||
return this.disableIPAddressCache.TryGetValue(address, out _) == false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 验证远程节点是否可连接
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns></returns>
|
||||
private async Task<IPAddress?> IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
await socket.ConnectAsync(address, port, linkedTokenSource.Token);
|
||||
return address;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
this.SetDisabled(address);
|
||||
return default;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
this.SetDisabled(address);
|
||||
await Task.Delay(this.connectTimeout, cancellationToken);
|
||||
return default;
|
||||
this.logger.LogWarning($"{dnsClient}无法解析{domain}:{ex.Message}");
|
||||
return Array.Empty<IPAddress>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -10,16 +11,12 @@ namespace FastGithub.DomainResolve
|
||||
public interface IDomainResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置ip不可用
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="address">ip</param>
|
||||
void SetDisabled(IPAddress address);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新域名解析结果
|
||||
/// </summary>
|
||||
/// <param name="domain">域名</param>
|
||||
void FlushDomain(DnsEndPoint domain);
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
@ -27,6 +24,6 @@ namespace FastGithub.DomainResolve
|
||||
/// <param name="domain">域名</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<IPAddress> ResolveAsync(string domain, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,13 @@ using FastGithub.DomainResolve;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -27,7 +28,7 @@ namespace FastGithub.Http
|
||||
/// HttpClientHandler
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
@ -41,27 +42,7 @@ namespace FastGithub.Http
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.ProcessRequestAsync(request, cancellationToken);
|
||||
return await this.SendRequestAsync(request, cancellationToken);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
this.InterceptRequestException(request, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理请求
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
if (uri == null)
|
||||
@ -70,91 +51,22 @@ namespace FastGithub.Http
|
||||
}
|
||||
|
||||
// 请求上下文信息
|
||||
var context = new RequestContext
|
||||
{
|
||||
Domain = uri.Host,
|
||||
IsHttps = uri.Scheme == Uri.UriSchemeHttps,
|
||||
TlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom()
|
||||
};
|
||||
request.SetRequestContext(context);
|
||||
var isHttps = uri.Scheme == Uri.UriSchemeHttps;
|
||||
var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
|
||||
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
|
||||
|
||||
// 解析ip,替换https为http
|
||||
var uriBuilder = new UriBuilder(uri)
|
||||
{
|
||||
Scheme = Uri.UriSchemeHttp
|
||||
};
|
||||
// 设置请求host,修改协议为http
|
||||
request.Headers.Host = uri.Host;
|
||||
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
|
||||
|
||||
if (uri.HostNameType == UriHostNameType.Dns)
|
||||
{
|
||||
if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) == false)
|
||||
{
|
||||
var endPoint = new DnsEndPoint(uri.Host, uri.Port);
|
||||
address = await this.domainResolver.ResolveAsync(endPoint, cancellationToken);
|
||||
}
|
||||
uriBuilder.Host = address.ToString();
|
||||
request.Headers.Host = context.Domain;
|
||||
context.TlsSniValue = context.TlsSniValue.WithIPAddress(address);
|
||||
}
|
||||
request.RequestUri = uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送请求
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.domainConfig.Timeout != null)
|
||||
{
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
return await base.SendAsync(request, linkedTokenSource.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拦截请求异常
|
||||
/// 查找TimedOut的ip地址添加到黑名单
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="exception"></param>
|
||||
private void InterceptRequestException(HttpRequestMessage request, HttpRequestException exception)
|
||||
{
|
||||
if (request.RequestUri == null || IsTimedOutSocketError(exception) == false)
|
||||
{
|
||||
return;
|
||||
return base.SendAsync(request, linkedTokenSource.Token);
|
||||
}
|
||||
|
||||
if (IPAddress.TryParse(request.RequestUri.Host, out var address))
|
||||
{
|
||||
this.domainResolver.SetDisabled(address);
|
||||
}
|
||||
|
||||
if (request.Headers.Host != null)
|
||||
{
|
||||
this.domainResolver.FlushDomain(new DnsEndPoint(request.Headers.Host, request.RequestUri.Port));
|
||||
}
|
||||
|
||||
|
||||
static bool IsTimedOutSocketError(HttpRequestException exception)
|
||||
{
|
||||
var inner = exception.InnerException;
|
||||
while (inner != null)
|
||||
{
|
||||
if (inner is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
inner = inner.InnerException;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -170,48 +82,106 @@ namespace FastGithub.Http
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false,
|
||||
AutomaticDecompression = DecompressionMethods.None,
|
||||
ConnectCallback = async (context, cancellationToken) =>
|
||||
{
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
|
||||
var stream = new NetworkStream(socket, ownsSocket: true);
|
||||
|
||||
var requestContext = context.InitialRequestMessage.GetRequestContext();
|
||||
if (requestContext.IsHttps == false)
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
|
||||
{
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
|
||||
TargetHost = requestContext.TlsSniValue.Value,
|
||||
RemoteCertificateValidationCallback = ValidateServerCertificate
|
||||
}, cancellationToken);
|
||||
return sslStream;
|
||||
|
||||
|
||||
bool ValidateServerCertificate(object sender, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
|
||||
{
|
||||
if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
|
||||
{
|
||||
if (this.domainConfig.TlsIgnoreNameMismatch == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var domain = requestContext.Domain;
|
||||
var dnsNames = ReadDnsNames(cert);
|
||||
return dnsNames.Any(dns => IsMatch(dns, domain));
|
||||
}
|
||||
|
||||
return errors == SslPolicyErrors.None;
|
||||
}
|
||||
}
|
||||
ConnectCallback = this.ConnectCallback
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接回调
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var innerExceptions = new List<Exception>();
|
||||
var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
|
||||
|
||||
await foreach (var ipEndPoint in ipEndPoints)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.ConnectAsync(context, ipEndPoint, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
innerExceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AggregateException("没有可连接成功的IP", innerExceptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 建立连接
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="ipEndPoint"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext context, IPEndPoint ipEndPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(ipEndPoint, cancellationToken);
|
||||
var stream = new NetworkStream(socket, ownsSocket: true);
|
||||
|
||||
var requestContext = context.InitialRequestMessage.GetRequestContext();
|
||||
if (requestContext.IsHttps == false)
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
var tlsSniValue = requestContext.TlsSniValue.WithIPAddress(ipEndPoint.Address);
|
||||
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
|
||||
{
|
||||
TargetHost = tlsSniValue.Value,
|
||||
RemoteCertificateValidationCallback = ValidateServerCertificate
|
||||
}, cancellationToken);
|
||||
|
||||
return sslStream;
|
||||
|
||||
// 验证证书有效性
|
||||
bool ValidateServerCertificate(object sender, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
|
||||
{
|
||||
if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
|
||||
{
|
||||
if (this.domainConfig.TlsIgnoreNameMismatch == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var domain = context.DnsEndPoint.Host;
|
||||
var dnsNames = ReadDnsNames(cert);
|
||||
return dnsNames.Any(dns => IsMatch(dns, domain));
|
||||
}
|
||||
|
||||
return errors == SslPolicyErrors.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析为IPEndPoint
|
||||
/// </summary>
|
||||
/// <param name="dnsEndPoint"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async IAsyncEnumerable<IPEndPoint> GetIPEndPointsAsync(DnsEndPoint dnsEndPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) ||
|
||||
IPAddress.TryParse(dnsEndPoint.Host, out address))
|
||||
{
|
||||
yield return new IPEndPoint(address, dnsEndPoint.Port);
|
||||
}
|
||||
else
|
||||
{
|
||||
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint.Host, cancellationToken))
|
||||
{
|
||||
yield return new IPEndPoint(item, dnsEndPoint.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取使用的DNS名称
|
||||
/// </summary>
|
||||
|
||||
@ -10,16 +10,22 @@ namespace FastGithub.Http
|
||||
/// <summary>
|
||||
/// 获取或设置是否为https请求
|
||||
/// </summary>
|
||||
public bool IsHttps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求的域名
|
||||
/// </summary>
|
||||
public string? Domain { get; set; }
|
||||
public bool IsHttps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置Sni值
|
||||
/// </summary>
|
||||
public TlsSniPattern TlsSniValue { get; set; }
|
||||
public TlsSniPattern TlsSniValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求上下文
|
||||
/// </summary>
|
||||
/// <param name="isHttps"></param>
|
||||
/// <param name="tlsSniValue"></param>
|
||||
public RequestContext(bool isHttps, TlsSniPattern tlsSniValue)
|
||||
{
|
||||
IsHttps = isHttps;
|
||||
TlsSniValue = tlsSniValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -74,7 +73,7 @@ namespace FastGithub.HttpServer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exception is IOException ioException && ioException.InnerException is ConnectionAbortedException)
|
||||
if (HasInnerException<ConnectionAbortedException>(exception))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -82,6 +81,26 @@ namespace FastGithub.HttpServer
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否有内部异常异常
|
||||
/// </summary>
|
||||
/// <typeparam name="TInnerException"></typeparam>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private static bool HasInnerException<TInnerException>(Exception exception) where TInnerException : Exception
|
||||
{
|
||||
var inner = exception.InnerException;
|
||||
while (inner != null)
|
||||
{
|
||||
if (inner is TInnerException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
inner = inner.InnerException;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取异常信息
|
||||
/// </summary>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"HttpProxyPort": 38457, // http代理端口,linux/osx平台使用
|
||||
"FallbackDns": [ // dnscrypt-proxy不可用时使用
|
||||
"114.114.114.114:53",
|
||||
"8.8.8.8:53"
|
||||
"119.29.29.29:53"
|
||||
],
|
||||
"DomainConfigs": {
|
||||
"*.fastgithub.com": { // 域名的*表示除.之外0到多个任意字符
|
||||
|
||||
Loading…
Reference in New Issue
Block a user