diff --git a/FastGithub.Core/DnsConfig.cs b/FastGithub.Core/DnsConfig.cs index c3ae014..a302617 100644 --- a/FastGithub.Core/DnsConfig.cs +++ b/FastGithub.Core/DnsConfig.cs @@ -18,7 +18,7 @@ namespace FastGithub /// /// 端口 /// - public int Port { get; set; } + public int Port { get; set; } = 53; /// /// 转换为IPEndPoint diff --git a/FastGithub.Core/DomainConfig.cs b/FastGithub.Core/DomainConfig.cs index 16de37f..aba77b3 100644 --- a/FastGithub.Core/DomainConfig.cs +++ b/FastGithub.Core/DomainConfig.cs @@ -1,4 +1,6 @@ -namespace FastGithub +using System; + +namespace FastGithub { /// /// 域名配置 @@ -12,9 +14,8 @@ /// /// 目的地 - /// 支持ip或域名 - /// 留空则本域名 + /// 格式为相对或绝对uri /// - public string? Destination { get; set; } + public Uri? Destination { get; set; } } } diff --git a/FastGithub.Core/FastGithubConfig.cs b/FastGithub.Core/FastGithubConfig.cs new file mode 100644 index 0000000..7f9e748 --- /dev/null +++ b/FastGithub.Core/FastGithubConfig.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; + +namespace FastGithub +{ + /// + /// FastGithub配置 + /// + public class FastGithubConfig + { + private readonly Dictionary domainConfigs; + + /// + /// 获取信任dns + /// + public IPEndPoint TrustedDns { get; } + + /// + /// 获取非信任dns + /// + public IPEndPoint UnTrustedDns { get; } + + /// + /// FastGithub配置 + /// + /// + public FastGithubConfig(FastGithubOptions options) + { + this.TrustedDns = options.TrustedDns.ToIPEndPoint(); + this.UnTrustedDns = options.UntrustedDns.ToIPEndPoint(); + this.domainConfigs = options.DomainConfigs.ToDictionary(kv => new DomainMatch(kv.Key), kv => kv.Value); + } + + /// + /// 是否匹配指定的域名 + /// + /// + /// + public bool IsMatch(string domain) + { + return this.domainConfigs.Keys.Any(item => item.IsMatch(domain)); + } + + /// + /// 尝试获取域名配置 + /// + /// + /// + /// + public bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out DomainConfig domainConfig) + { + var key = this.domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(domain)); + if (key == null) + { + domainConfig = default; + return false; + } + return this.domainConfigs.TryGetValue(key, out domainConfig); + } + } +} diff --git a/FastGithub.Core/FastGithubOptions.cs b/FastGithub.Core/FastGithubOptions.cs index 41c6869..3ab9e7c 100644 --- a/FastGithub.Core/FastGithubOptions.cs +++ b/FastGithub.Core/FastGithubOptions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Collections.Generic; namespace FastGithub { @@ -10,13 +7,6 @@ namespace FastGithub /// public class FastGithubOptions { - /// - /// 域名 - /// - private DomainMatch[]? domainMatches; - private IPEndPoint? trustedDnsIPEndPoint; - private IPEndPoint? unTrustedDnsIPEndPoint; - /// /// 受信任的dns服务 /// @@ -28,50 +18,29 @@ namespace FastGithub public DnsConfig UntrustedDns { get; set; } = new DnsConfig { IPAddress = "114.114.114.114", Port = 53 }; /// - /// 代理的域名表达式 + /// 代理的域名配置 /// - public HashSet DomainPatterns { get; set; } = new(); + public Dictionary DomainConfigs { get; set; } = new(); + + /// - /// 验证选项值 + /// 初始化选项为配置 /// /// - public void Validate() + public void InitConfig() { - this.trustedDnsIPEndPoint = this.TrustedDns.ToIPEndPoint(); - this.unTrustedDnsIPEndPoint = this.UntrustedDns.ToIPEndPoint(); - this.domainMatches = this.DomainPatterns.Select(item => new DomainMatch(item)).ToArray(); - } - - - /// - /// 受信任的dns服务节点 - /// - public IPEndPoint GetTrustedDns() - { - return this.trustedDnsIPEndPoint ?? throw new InvalidOperationException(); + this.fastGithubConfig = new FastGithubConfig(this); } /// - /// 不受信任的dns服务节点 + /// 配置 /// - public IPEndPoint GetUnTrustedDns() - { - return this.unTrustedDnsIPEndPoint ?? throw new InvalidOperationException(); - } + private FastGithubConfig? fastGithubConfig; /// - /// 是否匹配指定的域名 + /// 获取配置 /// - /// - /// - public bool IsMatch(string domain) - { - if (this.domainMatches == null) - { - throw new InvalidOperationException(); - } - return this.domainMatches.Any(item => item.IsMatch(domain)); - } + public FastGithubConfig Config => this.fastGithubConfig!; } } diff --git a/FastGithub.Dns/DnsServerHostedService.cs b/FastGithub.Dns/DnsServerHostedService.cs index d4217c2..7d1b7c0 100644 --- a/FastGithub.Dns/DnsServerHostedService.cs +++ b/FastGithub.Dns/DnsServerHostedService.cs @@ -80,7 +80,7 @@ namespace FastGithub.Dns } this.logger.LogInformation("dns服务启动成功"); - var secondary = IPAddress.Parse(options.CurrentValue.UntrustedDns.IPAddress); + var secondary = options.CurrentValue.Config.UnTrustedDns.Address; this.dnsAddresses = this.SetNameServers(IPAddress.Loopback, secondary); FlushResolverCache(); diff --git a/FastGithub.Dns/RequestResolver.cs b/FastGithub.Dns/RequestResolver.cs index 0b526c1..4de57ea 100644 --- a/FastGithub.Dns/RequestResolver.cs +++ b/FastGithub.Dns/RequestResolver.cs @@ -32,7 +32,7 @@ namespace FastGithub.Dns { this.options = options; this.logger = logger; - this.untrustedResolver = new UdpRequestResolver(options.CurrentValue.GetTrustedDns()); + this.untrustedResolver = new UdpRequestResolver(options.CurrentValue.Config.TrustedDns); } /// @@ -56,7 +56,7 @@ namespace FastGithub.Dns } var domain = question.Name; - if (this.options.CurrentValue.IsMatch(domain.ToString()) == true) + if (this.options.CurrentValue.Config.IsMatch(domain.ToString()) == true) { var localAddress = remoteEndPointRequest.GetLocalAddress() ?? IPAddress.Loopback; var record = new IPAddressResourceRecord(domain, localAddress, this.ttl); diff --git a/FastGithub.ReverseProxy/ReverseProxyApplicationBuilderExtensions.cs b/FastGithub.ReverseProxy/ReverseProxyApplicationBuilderExtensions.cs index d3d9bc8..afa3ae4 100644 --- a/FastGithub.ReverseProxy/ReverseProxyApplicationBuilderExtensions.cs +++ b/FastGithub.ReverseProxy/ReverseProxyApplicationBuilderExtensions.cs @@ -1,10 +1,6 @@ using FastGithub.ReverseProxy; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System.Net.Http; -using Yarp.ReverseProxy.Forwarder; namespace FastGithub { @@ -20,39 +16,8 @@ namespace FastGithub /// public static IApplicationBuilder UseHttpsReverseProxy(this IApplicationBuilder app) { - var httpForwarder = app.ApplicationServices.GetRequiredService(); - var httpClientHanlder = app.ApplicationServices.GetRequiredService(); - var options = app.ApplicationServices.GetRequiredService>(); - - app.Use(next => async context => - { - var host = context.Request.Host.Host; - if (options.CurrentValue.IsMatch(host) == false) - { - await context.Response.WriteAsJsonAsync(new { message = $"不支持以{host}访问" }); - return; - } - - var port = context.Request.Host.Port ?? 443; - var destinationPrefix = $"https://{host}:{port}/"; - var httpClient = new HttpMessageInvoker(httpClientHanlder, disposeHandler: false); - var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient); - - if (error != ForwarderError.None) - { - var errorFeature = context.GetForwarderErrorFeature(); - if (errorFeature != null) - { - await context.Response.WriteAsJsonAsync(new - { - error = error.ToString(), - message = errorFeature.Exception?.Message - }); - } - } - }); - - return app; + var middleware = app.ApplicationServices.GetRequiredService(); + return app.Use(next => context => middleware.InvokeAsync(context)); } } } diff --git a/FastGithub.ReverseProxy/ReverseProxyMiddleware.cs b/FastGithub.ReverseProxy/ReverseProxyMiddleware.cs new file mode 100644 index 0000000..f4015c0 --- /dev/null +++ b/FastGithub.ReverseProxy/ReverseProxyMiddleware.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Yarp.ReverseProxy.Forwarder; + +namespace FastGithub.ReverseProxy +{ + /// + /// 反向代理中间件 + /// + sealed class ReverseProxyMiddleware + { + private readonly IHttpForwarder httpForwarder; + private readonly SniHttpClientHanlder sniHttpClientHanlder; + private readonly NoSniHttpClientHanlder noSniHttpClientHanlder; + private readonly IOptionsMonitor options; + + public ReverseProxyMiddleware( + IHttpForwarder httpForwarder, + SniHttpClientHanlder sniHttpClientHanlder, + NoSniHttpClientHanlder noSniHttpClientHanlder, + IOptionsMonitor options) + { + this.httpForwarder = httpForwarder; + this.sniHttpClientHanlder = sniHttpClientHanlder; + this.noSniHttpClientHanlder = noSniHttpClientHanlder; + this.options = options; + } + + /// + /// 处理请求 + /// + /// + /// + public async Task InvokeAsync(HttpContext context) + { + var host = context.Request.Host.Host; + if (this.options.CurrentValue.Config.TryGetDomainConfig(host, out var domainConfig) == false) + { + await context.Response.WriteAsJsonAsync(new + { + error = ForwarderError.NoAvailableDestinations.ToString(), + message = $"不支持https反向代理{host}这个域名" + }); + return; + } + + var destinationPrefix = GetDestinationPrefix(host, domainConfig.Destination); + var httpClient = domainConfig.NoSni + ? new HttpMessageInvoker(this.noSniHttpClientHanlder, disposeHandler: false) + : new HttpMessageInvoker(this.sniHttpClientHanlder, disposeHandler: false); + + var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient); + await ResponseErrorAsync(context, error); + } + + /// + /// 获取目标前缀 + /// + /// + /// + /// + private static string GetDestinationPrefix(string host, Uri? destination) + { + var defaultValue = $"https://{host}/"; + if (destination == null) + { + return defaultValue; + } + + var baseUri = new Uri(defaultValue); + return new Uri(baseUri, destination).ToString(); + } + + /// + /// 写入错误信息 + /// + /// + /// + /// + private static async Task ResponseErrorAsync(HttpContext context, ForwarderError error) + { + if (error == ForwarderError.None) + { + return; + } + + var errorFeature = context.GetForwarderErrorFeature(); + if (errorFeature == null) + { + return; + } + + await context.Response.WriteAsJsonAsync(new + { + error = error.ToString(), + message = errorFeature.Exception?.Message + }); + } + } +} diff --git a/FastGithub.ReverseProxy/ReverseProxyServiceCollectionExtensions.cs b/FastGithub.ReverseProxy/ReverseProxyServiceCollectionExtensions.cs index cbd98ee..4f7c55e 100644 --- a/FastGithub.ReverseProxy/ReverseProxyServiceCollectionExtensions.cs +++ b/FastGithub.ReverseProxy/ReverseProxyServiceCollectionExtensions.cs @@ -19,7 +19,9 @@ namespace FastGithub .AddMemoryCache() .AddHttpForwarder() .AddSingleton() - .AddTransient(); + .AddTransient() + .AddTransient() + .AddSingleton(); } } } diff --git a/FastGithub.ReverseProxy/SniHttpClientHanlder.cs b/FastGithub.ReverseProxy/SniHttpClientHanlder.cs new file mode 100644 index 0000000..c909a5f --- /dev/null +++ b/FastGithub.ReverseProxy/SniHttpClientHanlder.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.ReverseProxy +{ + /// + /// 携带Sni的的HttpClientHandler + /// + class SniHttpClientHanlder : DelegatingHandler + { + private readonly DomainResolver domainResolver; + private readonly ILogger logger; + + /// + /// 携带Sni的HttpClientHandler + /// + /// + public SniHttpClientHanlder( + DomainResolver domainResolver, + ILogger logger) + { + this.domainResolver = domainResolver; + this.logger = logger; + + this.InnerHandler = new SocketsHttpHandler + { + Proxy = null, + UseProxy = false, + AllowAutoRedirect = false, + }; + } + + /// + /// 替换域名为ip + /// + /// + /// + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var uri = request.RequestUri; + if (uri != null && uri.HostNameType == UriHostNameType.Dns) + { + var address = await this.domainResolver.ResolveAsync(uri.Host, cancellationToken); + this.logger.LogInformation($"[{address}--Sni->{uri.Host}]"); + + var builder = new UriBuilder(uri) + { + Host = address.ToString() + }; + request.RequestUri = builder.Uri; + request.Headers.Host = uri.Host; + } + return await base.SendAsync(request, cancellationToken); + } + } +} diff --git a/FastGithub/Program.cs b/FastGithub/Program.cs index 3b77699..0abbd6e 100644 --- a/FastGithub/Program.cs +++ b/FastGithub/Program.cs @@ -39,7 +39,7 @@ namespace FastGithub .AddDnscryptProxy() .AddOptions() .Bind(ctx.Configuration.GetSection(nameof(FastGithub))) - .PostConfigure(opt => opt.Validate()); + .PostConfigure(opt => opt.InitConfig()); }) .ConfigureWebHostDefaults(web => { diff --git a/FastGithub/appsettings.json b/FastGithub/appsettings.json index 3e75f2e..e61a0d3 100644 --- a/FastGithub/appsettings.json +++ b/FastGithub/appsettings.json @@ -1,23 +1,71 @@ { "FastGithub": { - "TrustedDns": { // ڽ׼ȷ + "TrustedDns": { // ڽDomainConfigs׼ȷ "IPAddress": "127.0.0.1", "Port": 5533 // 5533ָdnscrypt-proxy }, - "UnTrustedDns": { // ڽòٶȿ + "UnTrustedDns": { // ڽDomainConfigsٶȿ "IPAddress": "114.114.114.114", "Port": 53 }, - "DomainPatterns": [ // *ʾ0ַ - "github.com", - "githubstatus.com", - "*.github.com", - "*.github.io", - "*.githubapp.com", - "*.githubassets.com", - "*.githubusercontent.com", - "*github*.s3.amazonaws.com" - ] + "DomainConfigs": { // *ʾ0ַ + "github.com": { + "NoSni": true, + "Destination": null + }, + "githubstatus.com": { + "NoSni": true, + "Destination": null + }, + "*.github.com": { + "NoSni": true, + "Destination": null + }, + "*.github.io": { + "NoSni": true, + "Destination": null + }, + "*.githubapp.com": { + "NoSni": true, + "Destination": null + }, + "*.githubassets.com": { + "NoSni": true, + "Destination": null + }, + "*.githubusercontent.com": { + "NoSni": true, + "Destination": null + }, + "*github*.s3.amazonaws.com": { + "NoSni": true, + "Destination": null + }, + "ajax.googleapis.com": { + "NoSni": false, + "Destination": "https://gapis.geekzu.org/ajax/" + }, + "fonts.googleapis.com": { + "NoSni": false, + "Destination": "https://fonts.geekzu.org/" + }, + "themes.googleusercontent.com": { + "NoSni": false, + "Destination": "https://gapis.geekzu.org/g-themes/" + }, + "fonts.gstatic.com": { + "NoSni": false, + "Destination": "https://gapis.geekzu.org/g-fonts/" + }, + "secure.gravatar.com": { + "NoSni": false, + "Destination": "https://sdn.geekzu.org/" + }, + "*.gravatar.com": { + "NoSni": false, + "Destination": "https://fdn.geekzu.org/" + } + } }, "Logging": { "LogLevel": {