kestrel层实现http代理

This commit is contained in:
陈国伟 2022-06-20 17:07:09 +08:00
parent 27c2bf27bd
commit 58f79ddc19
29 changed files with 621 additions and 336 deletions

View File

@ -1,4 +1,4 @@
using FastGithub.HttpServer;
using FastGithub.HttpServer.HttpMiddlewares;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@ -10,14 +10,14 @@ namespace FastGithub
public static class ApplicationBuilderExtensions
{
/// <summary>
/// 使用http代理中间件
/// 使用http代理策略中间件
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseHttpProxy(this IApplicationBuilder app)
public static IApplicationBuilder UseHttpProxyPac(this IApplicationBuilder app)
{
var middleware = app.ApplicationServices.GetRequiredService<HttpProxyMiddleware>();
return app.Use(next => context => middleware.InvokeAsync(context));
var middleware = app.ApplicationServices.GetRequiredService<HttpProxyPacMiddleware>();
return app.Use(next => context => middleware.InvokeAsync(context, next));
}
/// <summary>

View File

@ -1,11 +1,12 @@
using Microsoft.Extensions.Logging;
using FastGithub;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
abstract class CaCertInstallerOfLinux : ICaCertInstaller
{
@ -35,7 +36,7 @@ namespace FastGithub.HttpServer
/// <returns></returns>
public bool IsSupported()
{
return OperatingSystem.IsLinux() && File.Exists(this.CaCertUpdatePath);
return OperatingSystem.IsLinux() && File.Exists(CaCertUpdatePath);
}
/// <summary>
@ -44,7 +45,7 @@ namespace FastGithub.HttpServer
/// <param name="caCertFilePath">证书文件路径</param>
public void Install(string caCertFilePath)
{
var destCertFilePath = Path.Combine(this.CaCertStorePath, Path.GetFileName(caCertFilePath));
var destCertFilePath = Path.Combine(CaCertStorePath, Path.GetFileName(caCertFilePath));
if (File.Exists(destCertFilePath) && File.ReadAllBytes(caCertFilePath).SequenceEqual(File.ReadAllBytes(destCertFilePath)))
{
return;
@ -52,25 +53,25 @@ namespace FastGithub.HttpServer
if (geteuid() != 0)
{
this.logger.LogWarning($"无法自动安装CA证书{caCertFilePath}没有root权限");
logger.LogWarning($"无法自动安装CA证书{caCertFilePath}没有root权限");
return;
}
try
{
Directory.CreateDirectory(this.CaCertStorePath);
foreach (var item in Directory.GetFiles(this.CaCertStorePath, "fastgithub.*"))
Directory.CreateDirectory(CaCertStorePath);
foreach (var item in Directory.GetFiles(CaCertStorePath, "fastgithub.*"))
{
File.Delete(item);
}
File.Copy(caCertFilePath, destCertFilePath, overwrite: true);
Process.Start(this.CaCertUpdatePath).WaitForExit();
this.logger.LogInformation($"已自动向系统安装CA证书{caCertFilePath}");
Process.Start(CaCertUpdatePath).WaitForExit();
logger.LogInformation($"已自动向系统安装CA证书{caCertFilePath}");
}
catch (Exception ex)
{
File.Delete(destCertFilePath);
this.logger.LogWarning(ex.Message, "自动安装CA证书异常");
logger.LogWarning(ex.Message, "自动安装CA证书异常");
}
}
}

View File

@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfLinuxDebian : CaCertInstallerOfLinux
{

View File

@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfLinuxRedHat : CaCertInstallerOfLinux
{

View File

@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfMacOS : ICaCertInstaller
{
@ -27,7 +27,7 @@ namespace FastGithub.HttpServer
/// <param name="caCertFilePath">证书文件路径</param>
public void Install(string caCertFilePath)
{
this.logger.LogWarning($"请手动安装CA证书然后设置信任CA证书{caCertFilePath}");
logger.LogWarning($"请手动安装CA证书然后设置信任CA证书{caCertFilePath}");
}
}
}

View File

@ -2,7 +2,7 @@
using System;
using System.Security.Cryptography.X509Certificates;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfWindows : ICaCertInstaller
{
@ -50,7 +50,7 @@ namespace FastGithub.HttpServer
}
catch (Exception)
{
this.logger.LogWarning($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”");
logger.LogWarning($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”");
}
}
}

View File

@ -19,7 +19,7 @@ using System.Net;
using System.Text;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// 证书生成器

View File

@ -8,7 +8,7 @@ using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// 证书服务
@ -54,17 +54,17 @@ namespace FastGithub.HttpServer
/// </summary>
public bool CreateCaCertIfNotExists()
{
if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
if (File.Exists(CaCerFilePath) && File.Exists(CaKeyFilePath))
{
return false;
}
File.Delete(this.CaCerFilePath);
File.Delete(this.CaKeyFilePath);
File.Delete(CaCerFilePath);
File.Delete(CaKeyFilePath);
var validFrom = DateTime.Today.AddDays(-1);
var validTo = DateTime.Today.AddYears(10);
CertGenerator.GenerateBySelf(new[] { nameof(FastGithub) }, KEY_SIZE_BITS, validFrom, validTo, this.CaCerFilePath, this.CaKeyFilePath);
CertGenerator.GenerateBySelf(new[] { nameof(FastGithub) }, KEY_SIZE_BITS, validFrom, validTo, CaCerFilePath, CaKeyFilePath);
return true;
}
@ -73,14 +73,14 @@ namespace FastGithub.HttpServer
/// </summary>
public void InstallAndTrustCaCert()
{
var installer = this.certInstallers.FirstOrDefault(item => item.IsSupported());
var installer = certInstallers.FirstOrDefault(item => item.IsSupported());
if (installer != null)
{
installer.Install(this.CaCerFilePath);
installer.Install(CaCerFilePath);
}
else
{
this.logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{this.CaCerFilePath}");
logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{CaCerFilePath}");
}
GitConfigSslverify(false);
@ -119,7 +119,7 @@ namespace FastGithub.HttpServer
public X509Certificate2 GetOrCreateServerCert(string? domain)
{
var key = $"{nameof(CertService)}:{domain}";
return this.serverCertCache.GetOrCreate(key, GetOrCreateCert);
return serverCertCache.GetOrCreate(key, GetOrCreateCert);
// 生成域名的1年证书
X509Certificate2 GetOrCreateCert(ICacheEntry entry)
@ -129,7 +129,7 @@ namespace FastGithub.HttpServer
var validTo = DateTime.Today.AddYears(1);
entry.SetAbsoluteExpiration(validTo);
return CertGenerator.GenerateByCa(domains, KEY_SIZE_BITS, validFrom, validTo, this.CaCerFilePath, this.CaKeyFilePath);
return CertGenerator.GenerateByCa(domains, KEY_SIZE_BITS, validFrom, validTo, CaCerFilePath, CaKeyFilePath);
}
}

View File

@ -1,4 +1,4 @@
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// CA证书安装器

View File

@ -7,7 +7,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="1.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="1.1.0" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,68 @@
using FastGithub.Configuration;
using FastGithub.HttpServer.TcpMiddlewares;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// http代理策略中间件
/// </summary>
sealed class HttpProxyPacMiddleware
{
private readonly FastGithubConfig fastGithubConfig;
/// <summary>
/// http代理策略中间件
/// </summary>
/// <param name="fastGithubConfig"></param>
public HttpProxyPacMiddleware(FastGithubConfig fastGithubConfig)
{
this.fastGithubConfig = fastGithubConfig;
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// http请求经过了httpProxy中间件
var proxyFeature = context.Features.Get<IHttpProxyFeature>();
if (proxyFeature != null && proxyFeature.ProxyProtocol == ProxyProtocol.None)
{
var proxyPac = this.CreateProxyPac(context.Request.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
{
await next(context);
}
}
/// <summary>
/// 创建proxypac脚本
/// </summary>
/// <param name="proxyHost"></param>
/// <returns></returns>
private string CreateProxyPac(HostString proxyHost)
{
var buidler = new StringBuilder();
buidler.AppendLine("function FindProxyForURL(url, host){");
buidler.AppendLine($" var fastgithub = 'PROXY {proxyHost}';");
foreach (var domain in fastGithubConfig.GetDomainPatterns())
{
buidler.AppendLine($" if (shExpMatch(host, '{domain}')) return fastgithub;");
}
buidler.AppendLine(" return 'DIRECT';");
buidler.AppendLine("}");
return buidler.ToString();
}
}
}

View File

@ -8,7 +8,7 @@ using System.Net;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Forwarder;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 反向代理中间件
@ -43,7 +43,7 @@ namespace FastGithub.HttpServer
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var host = context.Request.Host;
if (this.TryGetDomainConfig(host, out var domainConfig) == false)
if (TryGetDomainConfig(host, out var domainConfig) == false)
{
await next(context);
}
@ -51,8 +51,8 @@ namespace FastGithub.HttpServer
{
var scheme = context.Request.Scheme;
var destinationPrefix = GetDestinationPrefix(scheme, host, domainConfig.Destination);
var httpClient = this.httpClientFactory.CreateHttpClient(host.Host, domainConfig);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient);
var httpClient = httpClientFactory.CreateHttpClient(host.Host, domainConfig);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Empty);
await HandleErrorAsync(context, error);
}
else
@ -74,7 +74,7 @@ namespace FastGithub.HttpServer
/// <returns></returns>
private bool TryGetDomainConfig(HostString host, [MaybeNullWhen(false)] out DomainConfig domainConfig)
{
if (this.fastGithubConfig.TryGetDomainConfig(host.Host, out domainConfig) == true)
if (fastGithubConfig.TryGetDomainConfig(host.Host, out domainConfig) == true)
{
return true;
}
@ -82,7 +82,7 @@ namespace FastGithub.HttpServer
// 未配置的域名但仍然被解析到本机ip的域名
if (OperatingSystem.IsWindows() && IsDomain(host.Host))
{
this.logger.LogWarning($"域名{host.Host}可能已经被DNS污染如果域名为本机域名请解析为非回环IP");
logger.LogWarning($"域名{host.Host}可能已经被DNS污染如果域名为本机域名请解析为非回环IP");
domainConfig = defaultDomainConfig;
return true;
}
@ -113,7 +113,7 @@ namespace FastGithub.HttpServer
var baseUri = new Uri(defaultValue);
var result = new Uri(baseUri, destination).ToString();
this.logger.LogInformation($"{defaultValue} => {result}");
logger.LogInformation($"{defaultValue} => {result}");
return result;
}

View File

@ -1,4 +1,4 @@
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 请求日志特性

View File

@ -6,7 +6,7 @@ using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 请求日志中间件
@ -55,15 +55,15 @@ namespace FastGithub.HttpServer
var exception = context.GetForwarderErrorFeature()?.Exception;
if (exception == null)
{
this.logger.LogInformation($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms");
logger.LogInformation($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms");
}
else if (IsError(exception))
{
this.logger.LogError($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{exception}");
logger.LogError($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{exception}");
}
else
{
this.logger.LogWarning($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{GetMessage(exception)}");
logger.LogWarning($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{GetMessage(exception)}");
}
}

View File

@ -1,242 +0,0 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Forwarder;
namespace FastGithub.HttpServer
{
/// <summary>
/// http代理中间件
/// </summary>
sealed class HttpProxyMiddleware
{
private const string LOCALHOST = "localhost";
private const int HTTP_PORT = 80;
private const int HTTPS_PORT = 443;
private readonly FastGithubConfig fastGithubConfig;
private readonly IDomainResolver domainResolver;
private readonly IHttpForwarder httpForwarder;
private readonly HttpReverseProxyMiddleware httpReverseProxy;
private readonly HttpMessageInvoker defaultHttpClient;
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
static HttpProxyMiddleware()
{
// https://github.com/dotnet/aspnetcore/issues/37421
var authority = typeof(HttpParser<>).Assembly
.GetType("Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.HttpCharacters")?
.GetField("_authority", BindingFlags.NonPublic | BindingFlags.Static)?
.GetValue(null);
if (authority is bool[] authorityArray)
{
authorityArray['-'] = true;
}
}
/// <summary>
/// http代理中间件
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="domainResolver"></param>
/// <param name="httpForwarder"></param>
/// <param name="httpReverseProxy"></param>
public HttpProxyMiddleware(
FastGithubConfig fastGithubConfig,
IDomainResolver domainResolver,
IHttpForwarder httpForwarder,
HttpReverseProxyMiddleware httpReverseProxy)
{
this.fastGithubConfig = fastGithubConfig;
this.domainResolver = domainResolver;
this.httpForwarder = httpForwarder;
this.httpReverseProxy = httpReverseProxy;
this.defaultHttpClient = new HttpMessageInvoker(CreateDefaultHttpHandler(), disposeHandler: false);
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
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 cancellationToken = context.RequestAborted;
using var connection = await this.CreateConnectionAsync(host, cancellationToken);
var responseFeature = context.Features.Get<IHttpResponseFeature>();
if (responseFeature != null)
{
responseFeature.ReasonPhrase = "Connection Established";
}
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.CompleteAsync();
var transport = context.Features.Get<IConnectionTransportFeature>()?.Transport;
if (transport != null)
{
var task1 = connection.CopyToAsync(transport.Output, cancellationToken);
var task2 = transport.Input.CopyToAsync(connection, cancellationToken);
await Task.WhenAny(task1, task2);
}
}
else
{
await this.httpReverseProxy.InvokeAsync(context, async next =>
{
var destinationPrefix = $"{context.Request.Scheme}://{context.Request.Host}";
await this.httpForwarder.SendAsync(context, destinationPrefix, this.defaultHttpClient);
});
}
}
/// <summary>
/// 是否为fastgithub服务
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
private bool IsFastGithubServer(HostString host)
{
if (host.Port != this.fastGithubConfig.HttpProxyPort)
{
return false;
}
if (host.Host == LOCALHOST)
{
return true;
}
return IPAddress.TryParse(host.Host, out var address) && IPAddress.IsLoopback(address);
}
/// <summary>
/// 创建proxypac脚本
/// </summary>
/// <param name="proxyHost"></param>
/// <returns></returns>
private string CreateProxyPac(HostString proxyHost)
{
var buidler = new StringBuilder();
buidler.AppendLine("function FindProxyForURL(url, host){");
buidler.AppendLine($" var fastgithub = 'PROXY {proxyHost}';");
foreach (var domain in this.fastGithubConfig.GetDomainPatterns())
{
buidler.AppendLine($" if (shExpMatch(host, '{domain}')) return fastgithub;");
}
buidler.AppendLine(" return 'DIRECT';");
buidler.AppendLine("}");
return buidler.ToString();
}
/// <summary>
/// 创建连接
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="AggregateException"></exception>
private async Task<Stream> CreateConnectionAsync(HostString host, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
await foreach (var endPoint in this.GetUpstreamEndPointsAsync(host, cancellationToken))
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
return new NetworkStream(socket, ownsSocket: false);
}
catch (Exception ex)
{
socket.Dispose();
cancellationToken.ThrowIfCancellationRequested();
innerExceptions.Add(ex);
}
}
throw new AggregateException($"无法连接到{host}", innerExceptions);
}
/// <summary>
/// 获取目标终节点
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async IAsyncEnumerable<EndPoint> GetUpstreamEndPointsAsync(HostString host, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var targetHost = host.Host;
var targetPort = host.Port ?? HTTPS_PORT;
if (IPAddress.TryParse(targetHost, out var address) == true)
{
yield return new IPEndPoint(address, targetPort);
}
else if (this.fastGithubConfig.IsMatch(targetHost) == false)
{
yield return new DnsEndPoint(targetHost, targetPort);
}
else if (targetPort == HTTP_PORT)
{
yield return new IPEndPoint(IPAddress.Loopback, GlobalListener.HttpPort);
}
else if (targetPort == HTTPS_PORT)
{
yield return new IPEndPoint(IPAddress.Loopback, GlobalListener.HttpsPort);
}
else
{
var dnsEndPoint = new DnsEndPoint(targetHost, targetPort);
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint, cancellationToken))
{
yield return new IPEndPoint(item, targetPort);
}
}
}
/// <summary>
/// 创建httpHandler
/// </summary>
/// <returns></returns>
private static SocketsHttpHandler CreateDefaultHttpHandler()
{
return new()
{
Proxy = null,
UseProxy = false,
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None
};
}
}
}

View File

@ -1,8 +1,11 @@
using FastGithub.Configuration;
using FastGithub.HttpServer;
using FastGithub.HttpServer.Certs;
using FastGithub.HttpServer.TcpMiddlewares;
using FastGithub.HttpServer.TlsMiddlewares;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -13,7 +16,7 @@ namespace FastGithub
/// <summary>
/// Kestrel扩展
/// </summary>
public static class KestrelServerOptionsExtensions
public static class KestrelServerExtensions
{
/// <summary>
/// 无限制
@ -40,13 +43,21 @@ namespace FastGithub
throw new FastGithubException($"tcp端口{httpProxyPort}已经被其它进程占用,请在配置文件更换{nameof(FastGithubOptions.HttpProxyPort)}为其它端口");
}
var logger = kestrel.GetLogger();
kestrel.ListenLocalhost(httpProxyPort);
logger.LogInformation($"已监听http://localhost:{httpProxyPort}http代理服务启动完成");
kestrel.ListenLocalhost(httpProxyPort, listen =>
{
var proxyMiddleware = kestrel.ApplicationServices.GetRequiredService<HttpProxyMiddleware>();
var tunnelMiddleware = kestrel.ApplicationServices.GetRequiredService<TunnelMiddleware>();
listen.Use(next => context => proxyMiddleware.InvokeAsync(next, context));
listen.UseTls();
listen.Use(next => context => tunnelMiddleware.InvokeAsync(next, context));
});
kestrel.GetLogger().LogInformation($"已监听http://localhost:{httpProxyPort}http代理服务启动完成");
}
/// <summary>
/// 监听ssh反向代理
/// 监听ssh协议代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenSshReverseProxy(this KestrelServerOptions kestrel)
@ -62,7 +73,7 @@ namespace FastGithub
}
/// <summary>
/// 监听git反向代理
/// 监听git协议代理代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenGitReverseProxy(this KestrelServerOptions kestrel)
@ -99,10 +110,6 @@ namespace FastGithub
/// <exception cref="FastGithubException"></exception>
public static void ListenHttpsReverseProxy(this KestrelServerOptions kestrel)
{
var certService = kestrel.ApplicationServices.GetRequiredService<CertService>();
certService.CreateCaCertIfNotExists();
certService.InstallAndTrustCaCert();
var httpsPort = GlobalListener.HttpsPort;
kestrel.ListenLocalhost(httpsPort, listen =>
{
@ -110,10 +117,7 @@ namespace FastGithub
{
listen.UseFlowAnalyze();
}
listen.UseHttps(https =>
{
https.ServerCertificateSelector = (ctx, domain) => certService.GetOrCreateServerCert(domain);
});
listen.UseTls();
});
if (OperatingSystem.IsWindows())
@ -133,5 +137,36 @@ namespace FastGithub
var loggerFactory = kestrel.ApplicationServices.GetRequiredService<ILoggerFactory>();
return loggerFactory.CreateLogger($"{nameof(FastGithub)}.{nameof(HttpServer)}");
}
/// <summary>
/// 使用Tls中间件
/// </summary>
/// <param name="listen"></param>
/// <param name="configureOptions">https配置</param>
/// <returns></returns>
public static ListenOptions UseTls(this ListenOptions listen)
{
var certService = listen.ApplicationServices.GetRequiredService<CertService>();
certService.CreateCaCertIfNotExists();
certService.InstallAndTrustCaCert();
return listen.UseTls(https => https.ServerCertificateSelector = (ctx, domain) => certService.GetOrCreateServerCert(domain));
}
/// <summary>
/// 使用Tls中间件
/// </summary>
/// <param name="listen"></param>
/// <param name="configureOptions">https配置</param>
/// <returns></returns>
private static ListenOptions UseTls(this ListenOptions listen, Action<HttpsConnectionAdapterOptions> configureOptions)
{
var invadeMiddleware = listen.ApplicationServices.GetRequiredService<TlsInvadeMiddleware>();
var restoreMiddleware = listen.ApplicationServices.GetRequiredService<TlsRestoreMiddleware>();
listen.Use(next => context => invadeMiddleware.InvokeAsync(next, context));
listen.UseHttps(configureOptions);
listen.Use(next => context => restoreMiddleware.InvokeAsync(next, context));
return listen;
}
}
}

View File

@ -1,4 +1,8 @@
using FastGithub.HttpServer;
using FastGithub.HttpServer.Certs;
using FastGithub.HttpServer.Certs.CaCertInstallers;
using FastGithub.HttpServer.HttpMiddlewares;
using FastGithub.HttpServer.TcpMiddlewares;
using FastGithub.HttpServer.TlsMiddlewares;
using Microsoft.Extensions.DependencyInjection;
namespace FastGithub
{
@ -22,7 +26,17 @@ namespace FastGithub
.AddSingleton<ICaCertInstaller, CaCertInstallerOfWindows>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfLinuxRedHat>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfLinuxDebian>()
// tcp
.AddSingleton<HttpProxyMiddleware>()
.AddSingleton<TunnelMiddleware>()
// tls
.AddSingleton<TlsInvadeMiddleware>()
.AddSingleton<TlsRestoreMiddleware>()
// http
.AddSingleton<HttpProxyPacMiddleware>()
.AddSingleton<RequestLoggingMiddleware>()
.AddSingleton<HttpReverseProxyMiddleware>();
}

View File

@ -1,6 +1,6 @@
using FastGithub.DomainResolve;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// github的git代理处理者

View File

@ -1,6 +1,6 @@
using FastGithub.DomainResolve;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// github的ssh代理处理者

View File

@ -0,0 +1,135 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 正向代理中间件
/// </summary>
sealed class HttpProxyMiddleware
{
private readonly HttpParser<HttpRequestHandler> httpParser = new();
private readonly byte[] http200 = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n");
private readonly byte[] http400 = Encoding.ASCII.GetBytes("HTTP/1.1 400 Bad Request\r\n\r\n");
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="next"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
var result = await context.Transport.Input.ReadAsync();
var httpRequest = this.GetHttpRequestHandler(result, out var consumed);
// 协议错误
if (consumed == 0L)
{
await context.Transport.Output.WriteAsync(this.http400, context.ConnectionClosed);
}
else
{
// 隧道代理连接请求
if (httpRequest.ProxyProtocol == ProxyProtocol.TunnelProxy)
{
var position = result.Buffer.GetPosition(consumed);
context.Transport.Input.AdvanceTo(position);
await context.Transport.Output.WriteAsync(this.http200, context.ConnectionClosed);
}
else
{
var position = result.Buffer.Start;
context.Transport.Input.AdvanceTo(position);
}
context.Features.Set<IHttpProxyFeature>(httpRequest);
await next(context);
}
}
/// <summary>
/// 获取http请求处理者
/// </summary>
/// <param name="result"></param>
/// <param name="consumed"></param>
/// <returns></returns>
private HttpRequestHandler GetHttpRequestHandler(ReadResult result, out long consumed)
{
var handler = new HttpRequestHandler();
var reader = new SequenceReader<byte>(result.Buffer);
if (this.httpParser.ParseRequestLine(handler, ref reader) &&
this.httpParser.ParseHeaders(handler, ref reader))
{
consumed = reader.Consumed;
}
else
{
consumed = 0L;
}
return handler;
}
/// <summary>
/// 代理请求处理器
/// </summary>
private class HttpRequestHandler : IHttpRequestLineHandler, IHttpHeadersHandler, IHttpProxyFeature
{
private HttpMethod method;
public HostString ProxyHost { get; private set; }
public ProxyProtocol ProxyProtocol
{
get
{
if (this.ProxyHost.HasValue == false)
{
return ProxyProtocol.None;
}
if (this.method == HttpMethod.Connect)
{
return ProxyProtocol.TunnelProxy;
}
return ProxyProtocol.HttpProxy;
}
}
void IHttpRequestLineHandler.OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine)
{
this.method = versionAndMethod.Method;
var host = Encoding.ASCII.GetString(startLine.Slice(targetPath.Offset, targetPath.Length));
if (versionAndMethod.Method == HttpMethod.Connect)
{
this.ProxyHost = HostString.FromUriComponent(host);
}
else if (Uri.TryCreate(host, UriKind.Absolute, out var uri))
{
this.ProxyHost = HostString.FromUriComponent(uri);
}
}
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
}
void IHttpHeadersHandler.OnHeadersComplete(bool endStream)
{
}
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
}
void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
}
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Http;
namespace FastGithub.HttpServer.TcpMiddlewares
{
interface IHttpProxyFeature
{
HostString ProxyHost { get; }
ProxyProtocol ProxyProtocol { get; }
}
}

View File

@ -0,0 +1,23 @@
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 代理协议
/// </summary>
enum ProxyProtocol
{
/// <summary>
/// 无代理
/// </summary>
None,
/// <summary>
/// http代理
/// </summary>
HttpProxy,
/// <summary>
/// 隧道代理
/// </summary>
TunnelProxy
}
}

View File

@ -9,10 +9,10 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// tcp反射代理处理者
/// tcp协议代理处理者
/// </summary>
abstract class TcpReverseProxyHandler : ConnectionHandler
{
@ -21,7 +21,7 @@ namespace FastGithub.HttpServer
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
/// <summary>
/// tcp反射代理处理者
/// tcp协议代理处理者
/// </summary>
/// <param name="domainResolver"></param>
/// <param name="endPoint"></param>
@ -39,7 +39,7 @@ namespace FastGithub.HttpServer
public override async Task OnConnectedAsync(ConnectionContext context)
{
var cancellationToken = context.ConnectionClosed;
using var connection = await this.CreateConnectionAsync(cancellationToken);
using var connection = await CreateConnectionAsync(cancellationToken);
var task1 = connection.CopyToAsync(context.Transport.Output, cancellationToken);
var task2 = context.Transport.Input.CopyToAsync(connection, cancellationToken);
await Task.WhenAny(task1, task2);
@ -54,14 +54,14 @@ namespace FastGithub.HttpServer
private async Task<Stream> CreateConnectionAsync(CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
await foreach (var address in this.domainResolver.ResolveAsync(this.endPoint, cancellationToken))
await foreach (var address in domainResolver.ResolveAsync(endPoint, cancellationToken))
{
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var timeoutTokenSource = new CancellationTokenSource(connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
await socket.ConnectAsync(address, this.endPoint.Port, linkedTokenSource.Token);
await socket.ConnectAsync(address, endPoint.Port, linkedTokenSource.Token);
return new NetworkStream(socket, ownsSocket: false);
}
catch (Exception ex)
@ -71,7 +71,7 @@ namespace FastGithub.HttpServer
innerExceptions.Add(ex);
}
}
throw new AggregateException($"无法连接到{this.endPoint.Host}:{this.endPoint.Port}", innerExceptions);
throw new AggregateException($"无法连接到{endPoint.Host}:{endPoint.Port}", innerExceptions);
}
}
}

View File

@ -0,0 +1,132 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 隧道中间件
/// </summary>
sealed class TunnelMiddleware
{
private readonly FastGithubConfig fastGithubConfig;
private readonly IDomainResolver domainResolver;
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
/// <summary>
/// 隧道中间件
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="domainResolver"></param>
public TunnelMiddleware(
FastGithubConfig fastGithubConfig,
IDomainResolver domainResolver)
{
this.fastGithubConfig = fastGithubConfig;
this.domainResolver = domainResolver;
}
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="next"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
var proxyFeature = context.Features.Get<IHttpProxyFeature>();
if (proxyFeature == null || // 非代理
proxyFeature.ProxyProtocol != ProxyProtocol.TunnelProxy || //非隧道代理
context.Features.Get<ITlsConnectionFeature>() != null) // 经过隧道的https
{
await next(context);
}
else
{
var transport = context.Features.Get<IConnectionTransportFeature>()?.Transport;
if (transport != null)
{
var cancellationToken = context.ConnectionClosed;
using var connection = await this.CreateConnectionAsync(proxyFeature.ProxyHost, cancellationToken);
var task1 = connection.CopyToAsync(transport.Output, cancellationToken);
var task2 = transport.Input.CopyToAsync(connection, cancellationToken);
await Task.WhenAny(task1, task2);
}
}
}
/// <summary>
/// 创建连接
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="AggregateException"></exception>
private async Task<Stream> CreateConnectionAsync(HostString host, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
await foreach (var endPoint in this.GetUpstreamEndPointsAsync(host, cancellationToken))
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
return new NetworkStream(socket, ownsSocket: true);
}
catch (Exception ex)
{
socket.Dispose();
cancellationToken.ThrowIfCancellationRequested();
innerExceptions.Add(ex);
}
}
throw new AggregateException($"无法连接到{host}", innerExceptions);
}
/// <summary>
/// 获取目标终节点
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async IAsyncEnumerable<EndPoint> GetUpstreamEndPointsAsync(HostString host, [EnumeratorCancellation] CancellationToken cancellationToken)
{
const int HTTPS_PORT = 443;
var targetHost = host.Host;
var targetPort = host.Port ?? HTTPS_PORT;
if (IPAddress.TryParse(targetHost, out var address) == true)
{
yield return new IPEndPoint(address, targetPort);
}
else if (this.fastGithubConfig.IsMatch(targetHost) == false)
{
yield return new DnsEndPoint(targetHost, targetPort);
}
else
{
var dnsEndPoint = new DnsEndPoint(targetHost, targetPort);
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint, cancellationToken))
{
yield return new IPEndPoint(item, targetPort);
}
}
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// 假冒的TlsConnectionFeature
/// </summary>
sealed class FakeTlsConnectionFeature : ITlsConnectionFeature
{
public static FakeTlsConnectionFeature Instance { get; } = new FakeTlsConnectionFeature();
public X509Certificate2? ClientCertificate
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.Buffers;
using System.IO.Pipelines;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// https入侵中间件
/// </summary>
sealed class TlsInvadeMiddleware
{
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
// 连接不是tls
if (await IsTlsConnectionAsync(context) == false)
{
// 没有任何tls中间件执行过
if (context.Features.Get<ITlsConnectionFeature>() == null)
{
// 设置假的ITlsConnectionFeature迫使https中间件跳过自身的工作
context.Features.Set<ITlsConnectionFeature>(FakeTlsConnectionFeature.Instance);
}
}
await next(context);
}
/// <summary>
/// 是否为tls协议
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static async Task<bool> IsTlsConnectionAsync(ConnectionContext context)
{
try
{
var result = await context.Transport.Input.ReadAtLeastAsync(2, context.ConnectionClosed);
var state = IsTlsProtocol(result);
context.Transport.Input.AdvanceTo(result.Buffer.Start);
return state;
}
catch
{
return false;
}
static bool IsTlsProtocol(ReadResult result)
{
var reader = new SequenceReader<byte>(result.Buffer);
return reader.TryRead(out var firstByte) &&
reader.TryRead(out var nextByte) &&
firstByte == 0x16 &&
nextByte == 0x3;
}
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// https恢复中间件
/// </summary>
sealed class TlsRestoreMiddleware
{
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
if (context.Features.Get<ITlsConnectionFeature>() == FakeTlsConnectionFeature.Instance)
{
// 擦除入侵
context.Features.Set<ITlsConnectionFeature>(null);
}
await next(context);
}
}
}

View File

@ -67,11 +67,10 @@ namespace FastGithub
webBuilder.UseKestrel(kestrel =>
{
kestrel.NoLimit();
kestrel.ListenHttpsReverseProxy();
kestrel.ListenHttpReverseProxy();
if (OperatingSystem.IsWindows())
{
kestrel.ListenHttpsReverseProxy();
kestrel.ListenHttpReverseProxy();
kestrel.ListenSshReverseProxy();
kestrel.ListenGitReverseProxy();
}

View File

@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
namespace FastGithub
@ -53,20 +52,13 @@ namespace FastGithub
/// <param name="app"></param>
public void Configure(IApplicationBuilder app)
{
var httpProxyPort = app.ApplicationServices.GetRequiredService<IOptions<FastGithubOptions>>().Value.HttpProxyPort;
app.MapWhen(context => context.Connection.LocalPort == httpProxyPort, appBuilder =>
{
appBuilder.UseHttpProxy();
});
app.UseHttpProxyPac();
app.UseRequestLogging();
app.UseHttpReverseProxy();
app.MapWhen(context => context.Connection.LocalPort != httpProxyPort, appBuilder =>
{
appBuilder.UseRequestLogging();
appBuilder.UseHttpReverseProxy();
appBuilder.UseRouting();
appBuilder.DisableRequestLogging();
appBuilder.UseEndpoints(endpoint =>
app.UseRouting();
app.DisableRequestLogging();
app.UseEndpoints(endpoint =>
{
endpoint.MapGet("/flowStatistics", context =>
{
@ -74,7 +66,6 @@ namespace FastGithub
return context.Response.WriteAsJsonAsync(flowStatistics);
});
});
});
}
}
}