using FastGithub.Configuration; using FastGithub.DomainResolve; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; namespace FastGithub.Http { /// /// HttpClientHandler /// class HttpClientHandler : DelegatingHandler { private readonly DomainConfig domainConfig; private readonly IDomainResolver domainResolver; private readonly TimeSpan timedOutIPAddressExpiration = TimeSpan.FromMinutes(10d); /// /// HttpClientHandler /// /// /// public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver) { this.domainResolver = domainResolver; this.domainConfig = domainConfig; this.InnerHandler = this.CreateSocketsHttpHandler(); } /// /// 发送请求 /// /// /// /// protected override async Task 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; } } /// /// 处理请求 /// /// /// /// private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var uri = request.RequestUri; if (uri == null) { throw new FastGithubException("必须指定请求的URI"); } // 请求上下文信息 var context = new RequestContext { Domain = uri.Host, IsHttps = uri.Scheme == Uri.UriSchemeHttps, TlsSniPattern = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom() }; request.SetRequestContext(context); // 解析ip,替换https为http var uriBuilder = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }; 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.TlsSniPattern = context.TlsSniPattern.WithIPAddress(address); } request.RequestUri = uriBuilder.Uri; } /// /// 发送请求 /// /// /// /// private async Task 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); } } /// /// 拦截请求异常 /// 查找TimedOut的ip地址添加到黑名单 /// /// /// private void InterceptRequestException(HttpRequestMessage request, HttpRequestException exception) { if (request.RequestUri == null || exception.InnerException is not SocketException socketException || socketException.SocketErrorCode != SocketError.TimedOut) { return; } if (IPAddress.TryParse(request.RequestUri.Host, out var address)) { this.domainResolver.SetDisabled(address, this.timedOutIPAddressExpiration); } if (request.Headers.Host != null) { this.domainResolver.FlushDomain(new DnsEndPoint(request.Headers.Host, request.RequestUri.Port)); } } /// /// 创建转发代理的httpHandler /// /// private SocketsHttpHandler CreateSocketsHttpHandler() { return new SocketsHttpHandler { Proxy = null, UseProxy = false, 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.TlsSniPattern.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; } } }; } /// /// 读取使用的DNS名称 /// /// /// private static IEnumerable ReadDnsNames(X509Certificate? cert) { if (cert == null) { yield break; } var parser = new Org.BouncyCastle.X509.X509CertificateParser(); var x509Cert = parser.ReadCertificate(cert.GetRawCertData()); var subjects = x509Cert.GetSubjectAlternativeNames(); foreach (var subject in subjects) { if (subject is IList list) { if (list.Count >= 2 && list[0] is int nameType && nameType == 2) { var dnsName = list[1]?.ToString(); if (dnsName != null) { yield return dnsName; } } } } } /// /// 比较域名 /// /// /// /// private static bool IsMatch(string dnsName, string? domain) { if (domain == null) { return false; } if (dnsName == domain) { return true; } if (dnsName[0] == '*') { return domain.EndsWith(dnsName[1..]); } return false; } } }