using FastGithub.Configuration; using FastGithub.DomainResolve; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Options; using System.IO.Pipelines; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using Yarp.ReverseProxy.Forwarder; namespace FastGithub.HttpServer { /// /// http代理中间件 /// sealed class HttpProxyMiddleware { private const string LOOPBACK = "127.0.0.1"; private const string LOCALHOST = "localhost"; private readonly FastGithubConfig fastGithubConfig; private readonly IDomainResolver domainResolver; private readonly IHttpForwarder httpForwarder; private readonly IOptions options; private readonly HttpMessageInvoker httpClient; /// /// http代理中间件 /// /// /// /// /// public HttpProxyMiddleware( FastGithubConfig fastGithubConfig, IDomainResolver domainResolver, IHttpForwarder httpForwarder, IOptions options) { this.fastGithubConfig = fastGithubConfig; this.domainResolver = domainResolver; this.httpForwarder = httpForwarder; this.options = options; this.httpClient = new HttpMessageInvoker(CreateHttpHandler(), disposeHandler: false); } /// /// 处理请求 /// /// /// public async Task InvokeAsync(HttpContext context) { var host = context.Request.Host; if (this.IsFastGithubServer(host) == true) { var proxyPac = this.CreateProxyPac(host); context.Response.ContentType = "application/x-ns-proxy-autoconfig"; context.Response.Headers.Add("Content-Disposition", $"attachment;filename=proxy.pac"); await context.Response.WriteAsync(proxyPac); } else if (context.Request.Method == HttpMethods.Connect) { var endpoint = await this.GetTargetEndPointAsync(host); using var targetSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); await targetSocket.ConnectAsync(endpoint); context.Response.StatusCode = StatusCodes.Status200OK; context.Features.Get().ReasonPhrase = "Connection Established"; await context.Response.CompleteAsync(); var transport = context.Features.Get()?.Transport; if (transport != null) { var targetStream = new NetworkStream(targetSocket, ownsSocket: false); var task1 = targetStream.CopyToAsync(transport.Output); var task2 = transport.Input.CopyToAsync(targetStream); await Task.WhenAny(task1, task2); } } else { var destinationPrefix = $"{context.Request.Scheme}://{context.Request.Host}"; await this.httpForwarder.SendAsync(context, destinationPrefix, this.httpClient); } } /// /// 是否为fastgithub服务 /// /// /// private bool IsFastGithubServer(HostString host) { if (host.Host == LOOPBACK || host.Host == LOCALHOST) { return host.Port == null || host.Port == this.options.Value.HttpProxyPort; } return false; } /// /// 创建proxypac脚本 /// /// /// private string CreateProxyPac(HostString host) { var buidler = new StringBuilder(); buidler.AppendLine("function FindProxyForURL(url, host){"); buidler.AppendLine($" var proxy = 'PROXY {host}';"); foreach (var domain in this.fastGithubConfig.GetDomainPatterns()) { buidler.AppendLine($" if (shExpMatch(host, '{domain}')) return proxy;"); } buidler.AppendLine(" return 'DIRECT';"); buidler.AppendLine("}"); return buidler.ToString(); } /// /// 获取目标终节点 /// /// /// private async Task GetTargetEndPointAsync(HostString host) { const int HTTPS_PORT = 443; var targetHost = host.Host; var targetPort = host.Port ?? HTTPS_PORT; if (IPAddress.TryParse(targetHost, out var address) == true) { return new IPEndPoint(address, targetPort); } // 不关心的域名,直接使用系统dns if (this.fastGithubConfig.IsMatch(targetHost) == false) { return new DnsEndPoint(targetHost, targetPort); } // 目标端口为443,走https代理中间人 if (targetPort == HTTPS_PORT && targetHost != "ssh.github.com") { return new IPEndPoint(IPAddress.Loopback, HttpsReverseProxyPort.Value); } // dns优选 address = await this.domainResolver.ResolveAsync(new DnsEndPoint(targetHost, targetPort)); return new IPEndPoint(address, targetPort); } /// /// 创建httpHandler /// /// private static SocketsHttpHandler CreateHttpHandler() { return new() { Proxy = null, UseProxy = false, UseCookies = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None }; } } }