Compare commits
No commits in common. "2.1.4" and "master" have entirely different histories.
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
21
.github/ISSUE_TEMPLATE/bug-----.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/bug-----.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug发现与报告
|
||||
about: 写一份报告来帮助我们改进
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Bug描述**
|
||||
Bug的详细描述内容
|
||||
|
||||
**重现步骤**
|
||||
1. xxx
|
||||
2. yyy
|
||||
3. zzz
|
||||
|
||||
|
||||
**软件信息**
|
||||
- 操作系统: [e.g. win10-x64]
|
||||
- FastGithub: [e.g. v2.0.0]
|
363
.gitignore
vendored
Normal file
363
.gitignore
vendored
Normal file
@ -0,0 +1,363 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
15
@dnscrypt-proxy/LICENSE
Normal file
15
@dnscrypt-proxy/LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018-2021, Frank Denis <j at pureftpd dot org>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
857
@dnscrypt-proxy/dnscrypt-proxy.toml
Normal file
857
@dnscrypt-proxy/dnscrypt-proxy.toml
Normal file
@ -0,0 +1,857 @@
|
||||
|
||||
##############################################
|
||||
# #
|
||||
# dnscrypt-proxy configuration #
|
||||
# #
|
||||
##############################################
|
||||
|
||||
## This is an example configuration file.
|
||||
## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml"
|
||||
##
|
||||
## Online documentation is available here: https://dnscrypt.info/doc
|
||||
|
||||
|
||||
|
||||
##################################
|
||||
# Global settings #
|
||||
##################################
|
||||
|
||||
## List of servers to use
|
||||
##
|
||||
## Servers from the "public-resolvers" source (see down below) can
|
||||
## be viewed here: https://dnscrypt.info/public-servers
|
||||
##
|
||||
## The proxy will automatically pick working servers from this list.
|
||||
## Note that the require_* filters do NOT apply when using this setting.
|
||||
##
|
||||
## By default, this list is empty and all registered servers matching the
|
||||
## require_* filters will be used instead.
|
||||
##
|
||||
## Remove the leading # first to enable this; lines starting with # are ignored.
|
||||
|
||||
# server_names = ['scaleway-fr', 'google', 'yandex', 'cloudflare']
|
||||
|
||||
|
||||
## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6.
|
||||
## Example with both IPv4 and IPv6:
|
||||
## listen_addresses = ['127.0.0.1:53', '[::1]:53']
|
||||
##
|
||||
## To listen to all IPv4 addresses, use `listen_addresses = ['0.0.0.0:53']`
|
||||
## To listen to all IPv4+IPv6 addresses, use `listen_addresses = ['[::]:53']`
|
||||
|
||||
listen_addresses = ['127.0.0.1:53']
|
||||
|
||||
|
||||
## Maximum number of simultaneous client connections to accept
|
||||
|
||||
max_clients = 250
|
||||
|
||||
|
||||
## Switch to a different system user after listening sockets have been created.
|
||||
## Note (1): this feature is currently unsupported on Windows.
|
||||
## Note (2): this feature is not compatible with systemd socket activation.
|
||||
## Note (3): when using -pidfile, the PID file directory must be writable by the new user
|
||||
|
||||
# user_name = 'nobody'
|
||||
|
||||
|
||||
## Require servers (from remote sources) to satisfy specific properties
|
||||
|
||||
# Use servers reachable over IPv4
|
||||
ipv4_servers = true
|
||||
|
||||
# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity
|
||||
ipv6_servers = false
|
||||
|
||||
# Use servers implementing the DNSCrypt protocol
|
||||
dnscrypt_servers = true
|
||||
|
||||
# Use servers implementing the DNS-over-HTTPS protocol
|
||||
doh_servers = true
|
||||
|
||||
# Use servers implementing the Oblivious DoH protocol
|
||||
odoh_servers = false
|
||||
|
||||
|
||||
## Require servers defined by remote sources to satisfy specific properties
|
||||
|
||||
# Server must support DNS security extensions (DNSSEC)
|
||||
require_dnssec = false
|
||||
|
||||
# Server must not log user queries (declarative)
|
||||
require_nolog = true
|
||||
|
||||
# Server must not enforce its own blocklist (for parental control, ads blocking...)
|
||||
require_nofilter = true
|
||||
|
||||
# Server names to avoid even if they match all criteria
|
||||
disabled_server_names = []
|
||||
|
||||
|
||||
## Always use TCP to connect to upstream servers.
|
||||
## This can be useful if you need to route everything through Tor.
|
||||
## Otherwise, leave this to `false`, as it doesn't improve security
|
||||
## (dnscrypt-proxy will always encrypt everything even using UDP), and can
|
||||
## only increase latency.
|
||||
|
||||
force_tcp = false
|
||||
|
||||
|
||||
## SOCKS proxy
|
||||
## Uncomment the following line to route all TCP connections to a local Tor node
|
||||
## Tor doesn't support UDP, so set `force_tcp` to `true` as well.
|
||||
|
||||
# proxy = 'socks5://127.0.0.1:9050'
|
||||
|
||||
|
||||
## HTTP/HTTPS proxy
|
||||
## Only for DoH servers
|
||||
|
||||
# http_proxy = 'http://127.0.0.1:8888'
|
||||
|
||||
|
||||
## How long a DNS query will wait for a response, in milliseconds.
|
||||
## If you have a network with *a lot* of latency, you may need to
|
||||
## increase this. Startup may be slower if you do so.
|
||||
## Don't increase it too much. 10000 is the highest reasonable value.
|
||||
|
||||
timeout = 5000
|
||||
|
||||
|
||||
## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds
|
||||
|
||||
keepalive = 30
|
||||
|
||||
|
||||
## Add EDNS-client-subnet information to outgoing queries
|
||||
##
|
||||
## Multiple networks can be listed; they will be randomly chosen.
|
||||
## These networks don't have to match your actual networks.
|
||||
|
||||
# edns_client_subnet = ["0.0.0.0/0", "2001:db8::/32"]
|
||||
|
||||
|
||||
## Response for blocked queries. Options are `refused`, `hinfo` (default) or
|
||||
## an IP response. To give an IP response, use the format `a:<IPv4>,aaaa:<IPv6>`.
|
||||
## Using the `hinfo` option means that some responses will be lies.
|
||||
## Unfortunately, the `hinfo` option appears to be required for Android 8+
|
||||
|
||||
# blocked_query_response = 'refused'
|
||||
|
||||
|
||||
## Load-balancing strategy: 'p2' (default), 'ph', 'p<n>', 'first' or 'random'
|
||||
## Randomly choose 1 of the fastest 2, half, n, 1 or all live servers by latency.
|
||||
## The response quality still depends on the server itself.
|
||||
|
||||
# lb_strategy = 'p2'
|
||||
|
||||
## Set to `true` to constantly try to estimate the latency of all the resolvers
|
||||
## and adjust the load-balancing parameters accordingly, or to `false` to disable.
|
||||
## Default is `true` that makes 'p2' `lb_strategy` work well.
|
||||
|
||||
# lb_estimator = true
|
||||
|
||||
|
||||
## Log level (0-6, default: 2 - 0 is very verbose, 6 only contains fatal errors)
|
||||
|
||||
# log_level = 2
|
||||
|
||||
|
||||
## Log file for the application, as an alternative to sending logs to
|
||||
## the standard system logging service (syslog/Windows event log).
|
||||
##
|
||||
## This file is different from other log files, and will not be
|
||||
## automatically rotated by the application.
|
||||
|
||||
# log_file = 'dnscrypt-proxy.log'
|
||||
|
||||
|
||||
## When using a log file, only keep logs from the most recent launch.
|
||||
|
||||
# log_file_latest = true
|
||||
|
||||
|
||||
## Use the system logger (syslog on Unix, Event Log on Windows)
|
||||
|
||||
# use_syslog = true
|
||||
|
||||
|
||||
## Delay, in minutes, after which certificates are reloaded
|
||||
|
||||
cert_refresh_delay = 240
|
||||
|
||||
|
||||
## DNSCrypt: Create a new, unique key for every single DNS query
|
||||
## This may improve privacy but can also have a significant impact on CPU usage
|
||||
## Only enable if you don't have a lot of network load
|
||||
|
||||
# dnscrypt_ephemeral_keys = false
|
||||
|
||||
|
||||
## DoH: Disable TLS session tickets - increases privacy but also latency
|
||||
|
||||
# tls_disable_session_tickets = false
|
||||
|
||||
|
||||
## DoH: Use a specific cipher suite instead of the server preference
|
||||
## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||
## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
## 4865 = TLS_AES_128_GCM_SHA256
|
||||
## 4867 = TLS_CHACHA20_POLY1305_SHA256
|
||||
##
|
||||
## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...),
|
||||
## the following suite improves performance.
|
||||
## This may also help on Intel CPUs running 32-bit operating systems.
|
||||
##
|
||||
## Keep tls_cipher_suite empty if you have issues fetching sources or
|
||||
## connecting to some DoH servers. Google and Cloudflare are fine with it.
|
||||
|
||||
# tls_cipher_suite = [52392, 49199]
|
||||
|
||||
|
||||
## Bootstrap resolvers
|
||||
##
|
||||
## These are normal, non-encrypted DNS resolvers, that will be only used
|
||||
## for one-shot queries when retrieving the initial resolvers list and if
|
||||
## the system DNS configuration doesn't work.
|
||||
##
|
||||
## No user queries will ever be leaked through these resolvers, and they will
|
||||
## not be used after IP addresses of DoH resolvers have been found (if you are
|
||||
## using DoH).
|
||||
##
|
||||
## They will never be used if lists have already been cached, and if the stamps
|
||||
## of the configured servers already include IP addresses (which is the case for
|
||||
## most of DoH servers, and for all DNSCrypt servers and relays).
|
||||
##
|
||||
## They will not be used if the configured system DNS works, or after the
|
||||
## proxy already has at least one usable secure resolver.
|
||||
##
|
||||
## Resolvers supporting DNSSEC are recommended, and, if you are using
|
||||
## DoH, bootstrap resolvers should ideally be operated by a different entity
|
||||
## than the DoH servers you will be using, especially if you have IPv6 enabled.
|
||||
##
|
||||
## People in China may want to use 114.114.114.114:53 here.
|
||||
## Other popular options include 8.8.8.8, 9.9.9.9 and 1.1.1.1.
|
||||
##
|
||||
## If more than one resolver is specified, they will be tried in sequence.
|
||||
##
|
||||
## TL;DR: put valid standard resolver addresses here. Your actual queries will
|
||||
## not be sent there. If you're using DNSCrypt or Anonymized DNS and your
|
||||
## lists are up to date, these resolvers will not even be used.
|
||||
|
||||
bootstrap_resolvers = ['9.9.9.9:53', '8.8.8.8:53']
|
||||
|
||||
|
||||
## Always use the bootstrap resolver before the system DNS settings.
|
||||
|
||||
ignore_system_dns = true
|
||||
|
||||
|
||||
## Maximum time (in seconds) to wait for network connectivity before
|
||||
## initializing the proxy.
|
||||
## Useful if the proxy is automatically started at boot, and network
|
||||
## connectivity is not guaranteed to be immediately available.
|
||||
## Use 0 to not test for connectivity at all (not recommended),
|
||||
## and -1 to wait as much as possible.
|
||||
|
||||
netprobe_timeout = 60
|
||||
|
||||
## Address and port to try initializing a connection to, just to check
|
||||
## if the network is up. It can be any address and any port, even if
|
||||
## there is nothing answering these on the other side. Just don't use
|
||||
## a local address, as the goal is to check for Internet connectivity.
|
||||
## On Windows, a datagram with a single, nul byte will be sent, only
|
||||
## when the system starts.
|
||||
## On other operating systems, the connection will be initialized
|
||||
## but nothing will be sent at all.
|
||||
|
||||
netprobe_address = '9.9.9.9:53'
|
||||
|
||||
|
||||
## Offline mode - Do not use any remote encrypted servers.
|
||||
## The proxy will remain fully functional to respond to queries that
|
||||
## plugins can handle directly (forwarding, cloaking, ...)
|
||||
|
||||
# offline_mode = false
|
||||
|
||||
|
||||
## Additional data to attach to outgoing queries.
|
||||
## These strings will be added as TXT records to queries.
|
||||
## Do not use, except on servers explicitly asking for extra data
|
||||
## to be present.
|
||||
## encrypted-dns-server can be configured to use this for access control
|
||||
## in the [access_control] section
|
||||
|
||||
# query_meta = ['key1:value1', 'key2:value2', 'token:MySecretToken']
|
||||
|
||||
|
||||
## Automatic log files rotation
|
||||
|
||||
# Maximum log files size in MB - Set to 0 for unlimited.
|
||||
log_files_max_size = 10
|
||||
|
||||
# How long to keep backup files, in days
|
||||
log_files_max_age = 7
|
||||
|
||||
# Maximum log files backups to keep (or 0 to keep all backups)
|
||||
log_files_max_backups = 1
|
||||
|
||||
|
||||
|
||||
#########################
|
||||
# Filters #
|
||||
#########################
|
||||
|
||||
## Note: if you are using dnsmasq, disable the `dnssec` option in dnsmasq if you
|
||||
## configure dnscrypt-proxy to do any kind of filtering (including the filters
|
||||
## below and blocklists).
|
||||
## You can still choose resolvers that do DNSSEC validation.
|
||||
|
||||
|
||||
## Immediately respond to IPv6-related queries with an empty response
|
||||
## This makes things faster when there is no IPv6 connectivity, but can
|
||||
## also cause reliability issues with some stub resolvers.
|
||||
|
||||
block_ipv6 = false
|
||||
|
||||
|
||||
## Immediately respond to A and AAAA queries for host names without a domain name
|
||||
|
||||
block_unqualified = true
|
||||
|
||||
|
||||
## Immediately respond to queries for local zones instead of leaking them to
|
||||
## upstream resolvers (always causing errors or timeouts).
|
||||
|
||||
block_undelegated = true
|
||||
|
||||
|
||||
## TTL for synthetic responses sent when a request has been blocked (due to
|
||||
## IPv6 or blocklists).
|
||||
|
||||
reject_ttl = 10
|
||||
|
||||
|
||||
|
||||
##################################################################################
|
||||
# Route queries for specific domains to a dedicated set of servers #
|
||||
##################################################################################
|
||||
|
||||
## See the `example-forwarding-rules.txt` file for an example
|
||||
|
||||
# forwarding_rules = 'forwarding-rules.txt'
|
||||
|
||||
|
||||
|
||||
###############################
|
||||
# Cloaking rules #
|
||||
###############################
|
||||
|
||||
## Cloaking returns a predefined address for a specific name.
|
||||
## In addition to acting as a HOSTS file, it can also return the IP address
|
||||
## of a different name. It will also do CNAME flattening.
|
||||
##
|
||||
## See the `example-cloaking-rules.txt` file for an example
|
||||
|
||||
# cloaking_rules = 'cloaking-rules.txt'
|
||||
|
||||
## TTL used when serving entries in cloaking-rules.txt
|
||||
|
||||
# cloak_ttl = 600
|
||||
|
||||
|
||||
|
||||
###########################
|
||||
# DNS cache #
|
||||
###########################
|
||||
|
||||
## Enable a DNS cache to reduce latency and outgoing traffic
|
||||
|
||||
cache = true
|
||||
|
||||
|
||||
## Cache size
|
||||
|
||||
cache_size = 4096
|
||||
|
||||
|
||||
## Minimum TTL for cached entries
|
||||
|
||||
cache_min_ttl = 60
|
||||
|
||||
|
||||
## Maximum TTL for cached entries
|
||||
|
||||
cache_max_ttl = 600
|
||||
|
||||
|
||||
## Minimum TTL for negatively cached entries
|
||||
|
||||
cache_neg_min_ttl = 60
|
||||
|
||||
|
||||
## Maximum TTL for negatively cached entries
|
||||
|
||||
cache_neg_max_ttl = 600
|
||||
|
||||
|
||||
|
||||
########################################
|
||||
# Captive portal handling #
|
||||
########################################
|
||||
|
||||
[captive_portals]
|
||||
|
||||
## A file that contains a set of names used by operating systems to
|
||||
## check for connectivity and captive portals, along with hard-coded
|
||||
## IP addresses to return.
|
||||
|
||||
# map_file = 'example-captive-portals.txt'
|
||||
|
||||
|
||||
|
||||
##################################
|
||||
# Local DoH server #
|
||||
##################################
|
||||
|
||||
[local_doh]
|
||||
|
||||
## dnscrypt-proxy can act as a local DoH server. By doing so, web browsers
|
||||
## requiring a direct connection to a DoH server in order to enable some
|
||||
## features will enable these, without bypassing your DNS proxy.
|
||||
|
||||
## Addresses that the local DoH server should listen to
|
||||
|
||||
# listen_addresses = ['127.0.0.1:3000']
|
||||
|
||||
|
||||
## Path of the DoH URL. This is not a file, but the part after the hostname
|
||||
## in the URL. By convention, `/dns-query` is frequently chosen.
|
||||
## For each `listen_address` the complete URL to access the server will be:
|
||||
## `https://<listen_address><path>` (ex: `https://127.0.0.1/dns-query`)
|
||||
|
||||
# path = '/dns-query'
|
||||
|
||||
|
||||
## Certificate file and key - Note that the certificate has to be trusted.
|
||||
## See the documentation (wiki) for more information.
|
||||
|
||||
# cert_file = 'localhost.pem'
|
||||
# cert_key_file = 'localhost.pem'
|
||||
|
||||
|
||||
|
||||
###############################
|
||||
# Query logging #
|
||||
###############################
|
||||
|
||||
## Log client queries to a file
|
||||
|
||||
[query_log]
|
||||
|
||||
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
||||
## Can be set to /dev/stdout in order to log to the standard output.
|
||||
|
||||
# file = 'query.log'
|
||||
|
||||
|
||||
## Query log format (currently supported: tsv and ltsv)
|
||||
|
||||
format = 'tsv'
|
||||
|
||||
|
||||
## Do not log these query types, to reduce verbosity. Keep empty to log everything.
|
||||
|
||||
# ignored_qtypes = ['DNSKEY', 'NS']
|
||||
|
||||
|
||||
|
||||
############################################
|
||||
# Suspicious queries logging #
|
||||
############################################
|
||||
|
||||
## Log queries for nonexistent zones
|
||||
## These queries can reveal the presence of malware, broken/obsolete applications,
|
||||
## and devices signaling their presence to 3rd parties.
|
||||
|
||||
[nx_log]
|
||||
|
||||
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
||||
|
||||
# file = 'nx.log'
|
||||
|
||||
|
||||
## Query log format (currently supported: tsv and ltsv)
|
||||
|
||||
format = 'tsv'
|
||||
|
||||
|
||||
|
||||
######################################################
|
||||
# Pattern-based blocking (blocklists) #
|
||||
######################################################
|
||||
|
||||
## Blocklists are made of one pattern per line. Example of valid patterns:
|
||||
##
|
||||
## example.com
|
||||
## =example.com
|
||||
## *sex*
|
||||
## ads.*
|
||||
## ads*.example.*
|
||||
## ads*.example[0-9]*.com
|
||||
##
|
||||
## Example blocklist files can be found at https://download.dnscrypt.info/blocklists/
|
||||
## A script to build blocklists from public feeds can be found in the
|
||||
## `utils/generate-domains-blocklists` directory of the dnscrypt-proxy source code.
|
||||
|
||||
[blocked_names]
|
||||
|
||||
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
||||
|
||||
# blocked_names_file = 'blocked-names.txt'
|
||||
|
||||
|
||||
## Optional path to a file logging blocked queries
|
||||
|
||||
# log_file = 'blocked-names.log'
|
||||
|
||||
|
||||
## Optional log format: tsv or ltsv (default: tsv)
|
||||
|
||||
# log_format = 'tsv'
|
||||
|
||||
|
||||
|
||||
###########################################################
|
||||
# Pattern-based IP blocking (IP blocklists) #
|
||||
###########################################################
|
||||
|
||||
## IP blocklists are made of one pattern per line. Example of valid patterns:
|
||||
##
|
||||
## 127.*
|
||||
## fe80:abcd:*
|
||||
## 192.168.1.4
|
||||
|
||||
[blocked_ips]
|
||||
|
||||
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
||||
|
||||
# blocked_ips_file = 'blocked-ips.txt'
|
||||
|
||||
|
||||
## Optional path to a file logging blocked queries
|
||||
|
||||
# log_file = 'blocked-ips.log'
|
||||
|
||||
|
||||
## Optional log format: tsv or ltsv (default: tsv)
|
||||
|
||||
# log_format = 'tsv'
|
||||
|
||||
|
||||
|
||||
######################################################
|
||||
# Pattern-based allow lists (blocklists bypass) #
|
||||
######################################################
|
||||
|
||||
## Allowlists support the same patterns as blocklists
|
||||
## If a name matches an allowlist entry, the corresponding session
|
||||
## will bypass names and IP filters.
|
||||
##
|
||||
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
|
||||
|
||||
[allowed_names]
|
||||
|
||||
## Path to the file of allow list rules (absolute, or relative to the same directory as the config file)
|
||||
|
||||
# allowed_names_file = 'allowed-names.txt'
|
||||
|
||||
|
||||
## Optional path to a file logging allowed queries
|
||||
|
||||
# log_file = 'allowed-names.log'
|
||||
|
||||
|
||||
## Optional log format: tsv or ltsv (default: tsv)
|
||||
|
||||
# log_format = 'tsv'
|
||||
|
||||
|
||||
|
||||
#########################################################
|
||||
# Pattern-based allowed IPs lists (blocklists bypass) #
|
||||
#########################################################
|
||||
|
||||
## Allowed IP lists support the same patterns as IP blocklists
|
||||
## If an IP response matches an allow ip entry, the corresponding session
|
||||
## will bypass IP filters.
|
||||
##
|
||||
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
|
||||
|
||||
[allowed_ips]
|
||||
|
||||
## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file)
|
||||
|
||||
# allowed_ips_file = 'allowed-ips.txt'
|
||||
|
||||
|
||||
## Optional path to a file logging allowed queries
|
||||
|
||||
# log_file = 'allowed-ips.log'
|
||||
|
||||
## Optional log format: tsv or ltsv (default: tsv)
|
||||
|
||||
# log_format = 'tsv'
|
||||
|
||||
|
||||
|
||||
##########################################
|
||||
# Time access restrictions #
|
||||
##########################################
|
||||
|
||||
## One or more weekly schedules can be defined here.
|
||||
## Patterns in the name-based blocked_names file can optionally be followed with @schedule_name
|
||||
## to apply the pattern 'schedule_name' only when it matches a time range of that schedule.
|
||||
##
|
||||
## For example, the following rule in a blocklist file:
|
||||
## *.youtube.* @time-to-sleep
|
||||
## would block access to YouTube during the times defined by the 'time-to-sleep' schedule.
|
||||
##
|
||||
## {after='21:00', before= '7:00'} matches 0:00-7:00 and 21:00-0:00
|
||||
## {after= '9:00', before='18:00'} matches 9:00-18:00
|
||||
|
||||
[schedules]
|
||||
|
||||
# [schedules.'time-to-sleep']
|
||||
# mon = [{after='21:00', before='7:00'}]
|
||||
# tue = [{after='21:00', before='7:00'}]
|
||||
# wed = [{after='21:00', before='7:00'}]
|
||||
# thu = [{after='21:00', before='7:00'}]
|
||||
# fri = [{after='23:00', before='7:00'}]
|
||||
# sat = [{after='23:00', before='7:00'}]
|
||||
# sun = [{after='21:00', before='7:00'}]
|
||||
|
||||
# [schedules.'work']
|
||||
# mon = [{after='9:00', before='18:00'}]
|
||||
# tue = [{after='9:00', before='18:00'}]
|
||||
# wed = [{after='9:00', before='18:00'}]
|
||||
# thu = [{after='9:00', before='18:00'}]
|
||||
# fri = [{after='9:00', before='17:00'}]
|
||||
|
||||
|
||||
|
||||
#########################
|
||||
# Servers #
|
||||
#########################
|
||||
|
||||
## Remote lists of available servers
|
||||
## Multiple sources can be used simultaneously, but every source
|
||||
## requires a dedicated cache file.
|
||||
##
|
||||
## Refer to the documentation for URLs of public sources.
|
||||
##
|
||||
## A prefix can be prepended to server names in order to
|
||||
## avoid collisions if different sources share the same for
|
||||
## different servers. In that case, names listed in `server_names`
|
||||
## must include the prefixes.
|
||||
##
|
||||
## If the `urls` property is missing, cache files and valid signatures
|
||||
## must already be present. This doesn't prevent these cache files from
|
||||
## expiring after `refresh_delay` hours.
|
||||
## Cache freshness is checked every 24 hours, so values for 'refresh_delay'
|
||||
## of less than 24 hours will have no effect.
|
||||
## A maximum delay of 168 hours (1 week) is imposed to ensure cache freshness.
|
||||
|
||||
[sources]
|
||||
|
||||
## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers
|
||||
|
||||
[sources.'public-resolvers']
|
||||
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://download.dnscrypt.net/resolvers-list/v3/public-resolvers.md']
|
||||
cache_file = 'public-resolvers.md'
|
||||
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||
refresh_delay = 72
|
||||
prefix = ''
|
||||
|
||||
## Anonymized DNS relays
|
||||
|
||||
[sources.'relays']
|
||||
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md']
|
||||
cache_file = 'relays.md'
|
||||
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||
refresh_delay = 72
|
||||
prefix = ''
|
||||
|
||||
## ODoH (Oblivious DoH) servers and relays
|
||||
|
||||
# [sources.'odoh-servers']
|
||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-servers.md']
|
||||
# cache_file = 'odoh-servers.md'
|
||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||
# refresh_delay = 24
|
||||
# prefix = ''
|
||||
# [sources.'odoh-relays']
|
||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-relays.md']
|
||||
# cache_file = 'odoh-relays.md'
|
||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||
# refresh_delay = 24
|
||||
# prefix = ''
|
||||
|
||||
## Quad9
|
||||
|
||||
# [sources.quad9-resolvers]
|
||||
# urls = ['https://www.quad9.net/quad9-resolvers.md']
|
||||
# minisign_key = 'RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJPPKxCZMoEQYpmoysKUN'
|
||||
# cache_file = 'quad9-resolvers.md'
|
||||
# prefix = 'quad9-'
|
||||
|
||||
## Another example source, with resolvers censoring some websites not appropriate for children
|
||||
## This is a subset of the `public-resolvers` list, so enabling both is useless
|
||||
|
||||
# [sources.'parental-control']
|
||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://download.dnscrypt.net/resolvers-list/v3/parental-control.md']
|
||||
# cache_file = 'parental-control.md'
|
||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||
|
||||
|
||||
|
||||
#########################################
|
||||
# Servers with known bugs #
|
||||
#########################################
|
||||
|
||||
[broken_implementations]
|
||||
|
||||
# Cisco servers currently cannot handle queries larger than 1472 bytes, and don't
|
||||
# truncate reponses larger than questions as expected by the DNSCrypt protocol.
|
||||
# This prevents large responses from being received over UDP and over relays.
|
||||
#
|
||||
# Older versions of the `dnsdist` server software had a bug with queries larger
|
||||
# than 1500 bytes. This is fixed since `dnsdist` version 1.5.0, but
|
||||
# some server may still run an outdated version.
|
||||
#
|
||||
# The list below enables workarounds to make non-relayed usage more reliable
|
||||
# until the servers are fixed.
|
||||
|
||||
fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6']
|
||||
|
||||
|
||||
|
||||
#################################################################
|
||||
# Certificate-based client authentication for DoH #
|
||||
#################################################################
|
||||
|
||||
# Use a X509 certificate to authenticate yourself when connecting to DoH servers.
|
||||
# This is only useful if you are operating your own, private DoH server(s).
|
||||
# 'creds' maps servers to certificates, and supports multiple entries.
|
||||
# If you are not using the standard root CA, an optional "root_ca"
|
||||
# property set to the path to a root CRT file can be added to a server entry.
|
||||
|
||||
[doh_client_x509_auth]
|
||||
|
||||
#
|
||||
# creds = [
|
||||
# { server_name='*', client_cert='client.crt', client_key='client.key' }
|
||||
# ]
|
||||
|
||||
|
||||
|
||||
################################
|
||||
# Anonymized DNS #
|
||||
################################
|
||||
|
||||
[anonymized_dns]
|
||||
|
||||
## Routes are indirect ways to reach DNSCrypt servers.
|
||||
##
|
||||
## A route maps a server name ("server_name") to one or more relays that will be
|
||||
## used to connect to that server.
|
||||
##
|
||||
## A relay can be specified as a DNS Stamp (either a relay stamp, or a
|
||||
## DNSCrypt stamp) or a server name.
|
||||
##
|
||||
## The following example routes "example-server-1" via `anon-example-1` or `anon-example-2`,
|
||||
## and "example-server-2" via the relay whose relay DNS stamp is
|
||||
## "sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM".
|
||||
##
|
||||
## !!! THESE ARE JUST EXAMPLES !!!
|
||||
##
|
||||
## Review the list of available relays from the "relays.md" file, and, for each
|
||||
## server you want to use, define the relays you want connections to go through.
|
||||
##
|
||||
## Carefully choose relays and servers so that they are run by different entities.
|
||||
##
|
||||
## "server_name" can also be set to "*" to define a default route, for all servers:
|
||||
## { server_name='*', via=['anon-example-1', 'anon-example-2'] }
|
||||
##
|
||||
## If a route is ["*"], the proxy automatically picks a relay on a distinct network.
|
||||
## { server_name='*', via=['*'] } is also an option, but is likely to be suboptimal.
|
||||
##
|
||||
## Manual selection is always recommended over automatic selection, so that you can
|
||||
## select (relay,server) pairs that work well and fit your own criteria (close by or
|
||||
## in different countries, operated by different entities, on distinct ISPs...)
|
||||
|
||||
# routes = [
|
||||
# { server_name='example-server-1', via=['anon-example-1', 'anon-example-2'] },
|
||||
# { server_name='example-server-2', via=['sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM'] }
|
||||
# ]
|
||||
|
||||
|
||||
# Skip resolvers incompatible with anonymization instead of using them directly
|
||||
|
||||
skip_incompatible = false
|
||||
|
||||
|
||||
# If public server certificates for a non-conformant server cannot be
|
||||
# retrieved via a relay, try getting them directly. Actual queries
|
||||
# will then always go through relays.
|
||||
|
||||
# direct_cert_fallback = false
|
||||
|
||||
|
||||
|
||||
###############################
|
||||
# DNS64 #
|
||||
###############################
|
||||
|
||||
## DNS64 is a mechanism for synthesizing AAAA records from A records.
|
||||
## It is used with an IPv6/IPv4 translator to enable client-server
|
||||
## communication between an IPv6-only client and an IPv4-only server,
|
||||
## without requiring any changes to either the IPv6 or the IPv4 node,
|
||||
## for the class of applications that work through NATs.
|
||||
##
|
||||
## There are two options to synthesize such records:
|
||||
## Option 1: Using a set of static IPv6 prefixes;
|
||||
## Option 2: By discovering the IPv6 prefix from DNS64-enabled resolver.
|
||||
##
|
||||
## If both options are configured - only static prefixes are used.
|
||||
## (Ref. RFC6147, RFC6052, RFC7050)
|
||||
##
|
||||
## Do not enable unless you know what DNS64 is and why you need it, or else
|
||||
## you won't be able to connect to anything at all.
|
||||
|
||||
[dns64]
|
||||
|
||||
## (Option 1) Static prefix(es) as Pref64::/n CIDRs.
|
||||
# prefix = ['64:ff9b::/96']
|
||||
|
||||
## (Option 2) DNS64-enabled resolver(s) to discover Pref64::/n CIDRs.
|
||||
## These resolvers are used to query for Well-Known IPv4-only Name (WKN) "ipv4only.arpa." to discover only.
|
||||
## Set with your ISP's resolvers in case of custom prefixes (other than Well-Known Prefix 64:ff9b::/96).
|
||||
## IMPORTANT: Default resolvers listed below support Well-Known Prefix 64:ff9b::/96 only.
|
||||
# resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53']
|
||||
|
||||
|
||||
|
||||
########################################
|
||||
# Static entries #
|
||||
########################################
|
||||
|
||||
## Optional, local, static list of additional servers
|
||||
## Mostly useful for testing your own servers.
|
||||
|
||||
[static]
|
||||
|
||||
# [static.'myserver']
|
||||
# stamp = 'sdns://AQcAAAAAAAAAAAAQMi5kbnNjcnlwdC1jZXJ0Lg'
|
BIN
@dnscrypt-proxy/linux-arm64/dnscrypt-proxy
Normal file
BIN
@dnscrypt-proxy/linux-arm64/dnscrypt-proxy
Normal file
Binary file not shown.
BIN
@dnscrypt-proxy/linux-x64/dnscrypt-proxy
Normal file
BIN
@dnscrypt-proxy/linux-x64/dnscrypt-proxy
Normal file
Binary file not shown.
BIN
@dnscrypt-proxy/osx-arm64/dnscrypt-proxy
Normal file
BIN
@dnscrypt-proxy/osx-arm64/dnscrypt-proxy
Normal file
Binary file not shown.
BIN
@dnscrypt-proxy/osx-x64/dnscrypt-proxy
Normal file
BIN
@dnscrypt-proxy/osx-x64/dnscrypt-proxy
Normal file
Binary file not shown.
BIN
@dnscrypt-proxy/win-x64/dnscrypt-proxy.exe
Normal file
BIN
@dnscrypt-proxy/win-x64/dnscrypt-proxy.exe
Normal file
Binary file not shown.
16
Directory.Build.props
Normal file
16
Directory.Build.props
Normal file
@ -0,0 +1,16 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>2.1.5</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>
|
||||
<Description>github加速神器</Description>
|
||||
<Copyright>https://github.com/dotnetcore/FastGithub</Copyright>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
</Project>
|
65
FastGithub.Configuration/DomainConfig.cs
Normal file
65
FastGithub.Configuration/DomainConfig.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名配置
|
||||
/// </summary>
|
||||
public record DomainConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否发送SNI
|
||||
/// </summary>
|
||||
public bool TlsSni { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义SNI值的表达式
|
||||
/// </summary>
|
||||
public string? TlsSniPattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否忽略服务器证书域名不匹配
|
||||
/// 当不发送SNI时服务器可能发回域名不匹配的证书
|
||||
/// </summary>
|
||||
public bool TlsIgnoreNameMismatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用的ip地址
|
||||
/// </summary>
|
||||
public IPAddress? IPAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求超时时长
|
||||
/// </summary>
|
||||
public TimeSpan? Timeout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 目的地
|
||||
/// 格式为相对或绝对uri
|
||||
/// </summary>
|
||||
public Uri? Destination { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义响应
|
||||
/// </summary>
|
||||
public ResponseConfig? Response { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取TlsSniPattern
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public TlsSniPattern GetTlsSniPattern()
|
||||
{
|
||||
if (this.TlsSni == false)
|
||||
{
|
||||
return Configuration.TlsSniPattern.None;
|
||||
}
|
||||
if (string.IsNullOrEmpty(this.TlsSniPattern))
|
||||
{
|
||||
return Configuration.TlsSniPattern.Domain;
|
||||
}
|
||||
return new TlsSniPattern(this.TlsSniPattern);
|
||||
}
|
||||
}
|
||||
}
|
96
FastGithub.Configuration/DomainPattern.cs
Normal file
96
FastGithub.Configuration/DomainPattern.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示域名表达式
|
||||
/// *表示除.之外任意0到多个字符
|
||||
/// </summary>
|
||||
public class DomainPattern : IComparable<DomainPattern>
|
||||
{
|
||||
private readonly Regex regex;
|
||||
private readonly string domainPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 域名表达式
|
||||
/// *表示除.之外任意0到多个字符
|
||||
/// </summary>
|
||||
/// <param name="domainPattern">域名表达式</param>
|
||||
public DomainPattern(string domainPattern)
|
||||
{
|
||||
this.domainPattern = domainPattern;
|
||||
var regexPattern = Regex.Escape(domainPattern).Replace(@"\*", @"[^\.]*");
|
||||
this.regex = new Regex($"^{regexPattern}$", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 与目标比较
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
public int CompareTo(DomainPattern? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var segmentsX = this.domainPattern.Split('.');
|
||||
var segmentsY = other.domainPattern.Split('.');
|
||||
var value = segmentsX.Length - segmentsY.Length;
|
||||
if (value != 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
for (var i = segmentsX.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var x = segmentsX[i];
|
||||
var y = segmentsY[i];
|
||||
|
||||
value = Compare(x, y);
|
||||
if (value == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个分段
|
||||
/// </summary>
|
||||
/// <param name="x">abc</param>
|
||||
/// <param name="y">abc*</param>
|
||||
/// <returns></returns>
|
||||
private static int Compare(string x, string y)
|
||||
{
|
||||
var valueX = x.Replace('*', char.MaxValue);
|
||||
var valueY = y.Replace('*', char.MaxValue);
|
||||
return valueX.CompareTo(valueY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否与指定域名匹配
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsMatch(string domain)
|
||||
{
|
||||
return this.regex.IsMatch(domain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为文本
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.domainPattern;
|
||||
}
|
||||
}
|
||||
}
|
8
FastGithub.Configuration/FastGithub.Configuration.csproj
Normal file
8
FastGithub.Configuration/FastGithub.Configuration.csproj
Normal file
@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
109
FastGithub.Configuration/FastGithubConfig.cs
Normal file
109
FastGithub.Configuration/FastGithubConfig.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// FastGithub配置
|
||||
/// </summary>
|
||||
public class FastGithubConfig
|
||||
{
|
||||
private SortedDictionary<DomainPattern, DomainConfig> domainConfigs;
|
||||
private ConcurrentDictionary<string, DomainConfig?> domainConfigCache;
|
||||
|
||||
/// <summary>
|
||||
/// http代理端口
|
||||
/// </summary>
|
||||
public int HttpProxyPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回退的dns
|
||||
/// </summary>
|
||||
public IPEndPoint[] FallbackDns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// FastGithub配置
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="logger"></param>
|
||||
public FastGithubConfig(IOptionsMonitor<FastGithubOptions> options)
|
||||
{
|
||||
var opt = options.CurrentValue;
|
||||
|
||||
this.HttpProxyPort = opt.HttpProxyPort;
|
||||
this.FallbackDns = opt.FallbackDns;
|
||||
this.domainConfigs = ConvertDomainConfigs(opt.DomainConfigs);
|
||||
this.domainConfigCache = new ConcurrentDictionary<string, DomainConfig?>();
|
||||
|
||||
options.OnChange(opt => this.Update(opt));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配置
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
private void Update(FastGithubOptions options)
|
||||
{
|
||||
this.HttpProxyPort = options.HttpProxyPort;
|
||||
this.FallbackDns = options.FallbackDns;
|
||||
this.domainConfigs = ConvertDomainConfigs(options.DomainConfigs);
|
||||
this.domainConfigCache = new ConcurrentDictionary<string, DomainConfig?>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置转换
|
||||
/// </summary>
|
||||
/// <param name="domainConfigs"></param>
|
||||
/// <returns></returns>
|
||||
private static SortedDictionary<DomainPattern, DomainConfig> ConvertDomainConfigs(Dictionary<string, DomainConfig> domainConfigs)
|
||||
{
|
||||
var result = new SortedDictionary<DomainPattern, DomainConfig>();
|
||||
foreach (var kv in domainConfigs)
|
||||
{
|
||||
result.Add(new DomainPattern(kv.Key), kv.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否匹配指定的域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsMatch(string domain)
|
||||
{
|
||||
return this.TryGetDomainConfig(domain, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取域名配置
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out DomainConfig value)
|
||||
{
|
||||
value = this.domainConfigCache.GetOrAdd(domain, GetDomainConfig);
|
||||
return value != null;
|
||||
|
||||
DomainConfig? GetDomainConfig(string domain)
|
||||
{
|
||||
var key = this.domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(domain));
|
||||
return key == null ? null : this.domainConfigs[key];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有域名表达式
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public DomainPattern[] GetDomainPatterns()
|
||||
{
|
||||
return this.domainConfigs.Keys.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
29
FastGithub.Configuration/FastGithubException.cs
Normal file
29
FastGithub.Configuration/FastGithubException.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示FastGithub异常
|
||||
/// </summary>
|
||||
public class FastGithubException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// FastGithub异常
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public FastGithubException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FastGithub异常
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="innerException"></param>
|
||||
public FastGithubException(string message, Exception? innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
27
FastGithub.Configuration/FastGithubOptions.cs
Normal file
27
FastGithub.Configuration/FastGithubOptions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// FastGithub的配置
|
||||
/// </summary>
|
||||
public class FastGithubOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// http代理端口
|
||||
/// </summary>
|
||||
public int HttpProxyPort { get; set; } = 38457;
|
||||
|
||||
/// <summary>
|
||||
/// 回退的dns
|
||||
/// </summary>
|
||||
public IPEndPoint[] FallbackDns { get; set; } = Array.Empty<IPEndPoint>();
|
||||
|
||||
/// <summary>
|
||||
/// 代理的域名配置
|
||||
/// </summary>
|
||||
public Dictionary<string, DomainConfig> DomainConfigs { get; set; } = new();
|
||||
}
|
||||
}
|
137
FastGithub.Configuration/GlobalListener.cs
Normal file
137
FastGithub.Configuration/GlobalListener.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 监听器
|
||||
/// </summary>
|
||||
public static class GlobalListener
|
||||
{
|
||||
private static readonly IPGlobalProperties global = IPGlobalProperties.GetIPGlobalProperties();
|
||||
private static readonly HashSet<int> tcpListenPorts = GetListenPorts(global.GetActiveTcpListeners);
|
||||
private static readonly HashSet<int> udpListenPorts = GetListenPorts(global.GetActiveUdpListeners);
|
||||
|
||||
/// <summary>
|
||||
/// ssh端口
|
||||
/// </summary>
|
||||
public static int SshPort { get; } = GetAvailableTcpPort(22);
|
||||
|
||||
/// <summary>
|
||||
/// git端口
|
||||
/// </summary>
|
||||
public static int GitPort { get; } = GetAvailableTcpPort(9418);
|
||||
|
||||
/// <summary>
|
||||
/// http端口
|
||||
/// </summary>
|
||||
public static int HttpPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(80) : GetAvailableTcpPort(3880);
|
||||
|
||||
/// <summary>
|
||||
/// https端口
|
||||
/// </summary>
|
||||
public static int HttpsPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(443) : GetAvailableTcpPort(38443);
|
||||
|
||||
/// <summary>
|
||||
/// 获取已监听的端口
|
||||
/// </summary>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
private static HashSet<int> GetListenPorts(Func<IPEndPoint[]> func)
|
||||
{
|
||||
var hashSet = new HashSet<int>();
|
||||
try
|
||||
{
|
||||
foreach (var endpoint in func())
|
||||
{
|
||||
hashSet.Add(endpoint.Port);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return hashSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是可以监听TCP
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static bool CanListenTcp(int port)
|
||||
{
|
||||
return tcpListenPorts.Contains(port) == false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是可以监听UDP
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static bool CanListenUdp(int port)
|
||||
{
|
||||
return udpListenPorts.Contains(port) == false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是可以监听TCP和Udp
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static bool CanListen(int port)
|
||||
{
|
||||
return CanListenTcp(port) && CanListenUdp(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用的随机Tcp端口
|
||||
/// </summary>
|
||||
/// <param name="minPort"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetAvailableTcpPort(int minPort)
|
||||
{
|
||||
return GetAvailablePort(CanListenTcp, minPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用的随机Udp端口
|
||||
/// </summary>
|
||||
/// <param name="minPort"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetAvailableUdpPort(int minPort)
|
||||
{
|
||||
return GetAvailablePort(CanListenUdp, minPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用的随机端口
|
||||
/// </summary>
|
||||
/// <param name="minPort"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetAvailablePort(int minPort)
|
||||
{
|
||||
return GetAvailablePort(CanListen, minPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用端口
|
||||
/// </summary>
|
||||
/// <param name="canFunc"></param>
|
||||
/// <param name="minPort"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="FastGithubException"></exception>
|
||||
private static int GetAvailablePort(Func<int, bool> canFunc, int minPort)
|
||||
{
|
||||
for (var port = minPort; port < IPEndPoint.MaxPort; port++)
|
||||
{
|
||||
if (canFunc(port) == true)
|
||||
{
|
||||
return port;
|
||||
}
|
||||
}
|
||||
throw new FastGithubException("当前无可用的端口");
|
||||
}
|
||||
}
|
||||
}
|
86
FastGithub.Configuration/LoggerExtensions.cs
Normal file
86
FastGithub.Configuration/LoggerExtensions.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志插值字符串扩展
|
||||
/// </summary>
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void Log(this ILogger logger, LogLevel level, FormattableString formattableString)
|
||||
=> logger.Log(level, formattableString.Format, formattableString.GetArguments());
|
||||
|
||||
/// <summary>
|
||||
/// 输出日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void Log(this ILogger logger, LogLevel level, Exception? error, FormattableString formattableString)
|
||||
=> logger.Log(level, error, formattableString.Format, formattableString.GetArguments());
|
||||
|
||||
/// <summary>
|
||||
/// 输出Trace日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogTrace(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Trace, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出Debug日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogDebug(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Debug, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出Information日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogInformation(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Information, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出Warning日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogWarning(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Warning, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogError(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Error, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogError(this ILogger logger, Exception error, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Error, error, formattableString);
|
||||
|
||||
/// <summary>
|
||||
/// 输出Critical日志
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="formattableString"></param>
|
||||
public static void LogCritical(this ILogger logger, FormattableString formattableString)
|
||||
=> logger.Log(LogLevel.Critical, formattableString);
|
||||
}
|
||||
}
|
23
FastGithub.Configuration/ResponseConfig.cs
Normal file
23
FastGithub.Configuration/ResponseConfig.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 响应配置
|
||||
/// </summary>
|
||||
public record ResponseConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态码
|
||||
/// </summary>
|
||||
public int StatusCode { get; init; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// 内容类型
|
||||
/// </summary>
|
||||
public string ContentType { get; init; } = "text/plain;charset=utf-8";
|
||||
|
||||
/// <summary>
|
||||
/// 内容的值
|
||||
/// </summary>
|
||||
public string? ContentValue { get; init; }
|
||||
}
|
||||
}
|
27
FastGithub.Configuration/ServiceCollectionExtensions.cs
Normal file
27
FastGithub.Configuration/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加配置服务
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddConfiguration(this IServiceCollection services)
|
||||
{
|
||||
TypeConverterBinder.Bind(val => IPAddress.Parse(val), val => val?.ToString());
|
||||
TypeConverterBinder.Bind(val => IPEndPoint.Parse(val), val => val?.ToString());
|
||||
|
||||
services.TryAddSingleton<FastGithubConfig>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
86
FastGithub.Configuration/TlsSniPattern.cs
Normal file
86
FastGithub.Configuration/TlsSniPattern.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Sni自定义值表达式
|
||||
/// @domain变量表示取域名值
|
||||
/// @ipadress变量表示取ip
|
||||
/// @random变量表示取随机值
|
||||
/// </summary>
|
||||
public struct TlsSniPattern
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取表示式值
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 无SNI
|
||||
/// </summary>
|
||||
public static TlsSniPattern None { get; } = new TlsSniPattern(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// 域名SNI
|
||||
/// </summary>
|
||||
public static TlsSniPattern Domain { get; } = new TlsSniPattern("@domain");
|
||||
|
||||
/// <summary>
|
||||
/// IP值的SNI
|
||||
/// </summary>
|
||||
public static TlsSniPattern IPAddress { get; } = new TlsSniPattern("@ipaddress");
|
||||
|
||||
/// <summary>
|
||||
/// 随机值的SNI
|
||||
/// </summary>
|
||||
public static TlsSniPattern Random { get; } = new TlsSniPattern("@random");
|
||||
|
||||
/// <summary>
|
||||
/// Sni自定义值表达式
|
||||
/// </summary>
|
||||
/// <param name="value">表示式值</param>
|
||||
public TlsSniPattern(string? value)
|
||||
{
|
||||
this.Value = value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
public TlsSniPattern WithDomain(string domain)
|
||||
{
|
||||
var value = this.Value.Replace(Domain.Value, domain, StringComparison.OrdinalIgnoreCase);
|
||||
return new TlsSniPattern(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新ip地址
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
public TlsSniPattern WithIPAddress(IPAddress address)
|
||||
{
|
||||
var value = this.Value.Replace(IPAddress.Value, address.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
return new TlsSniPattern(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新随机数
|
||||
/// </summary>
|
||||
public TlsSniPattern WithRandom()
|
||||
{
|
||||
var value = this.Value.Replace(Random.Value, Environment.TickCount64.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
return new TlsSniPattern(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为文本
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Value;
|
||||
}
|
||||
}
|
||||
}
|
95
FastGithub.Configuration/TypeConverterBinder.cs
Normal file
95
FastGithub.Configuration/TypeConverterBinder.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace FastGithub.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// TypeConverter类型转换绑定器
|
||||
/// </summary>
|
||||
static class TypeConverterBinder
|
||||
{
|
||||
private static readonly Dictionary<Type, Binder> binders = new();
|
||||
|
||||
/// <summary>
|
||||
/// 绑定转换器到指定类型
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="reader"></param>
|
||||
/// <param name="writer"></param>
|
||||
public static void Bind<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Func<string, T?> reader, Func<T?, string?> writer)
|
||||
{
|
||||
binders[typeof(T)] = new Binder<T>(reader, writer);
|
||||
|
||||
var converterType = typeof(TypeConverter<>).MakeGenericType(typeof(T));
|
||||
if (TypeDescriptor.GetConverter(typeof(T)).GetType() != converterType)
|
||||
{
|
||||
TypeDescriptor.AddAttributes(typeof(T), new TypeConverterAttribute(converterType));
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class Binder
|
||||
{
|
||||
public abstract object? Read(string value);
|
||||
|
||||
public abstract string? Write(object? value);
|
||||
}
|
||||
|
||||
|
||||
private class Binder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : Binder
|
||||
{
|
||||
private readonly Func<string, T?> reader;
|
||||
private readonly Func<T?, string?> writer;
|
||||
|
||||
public Binder(Func<string, T?> reader, Func<T?, string?> writer)
|
||||
{
|
||||
this.reader = reader;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public override object? Read(string value)
|
||||
{
|
||||
return this.reader(value);
|
||||
}
|
||||
|
||||
public override string? Write(object? value)
|
||||
{
|
||||
return this.writer((T?)value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class TypeConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is string stringVal)
|
||||
{
|
||||
if (stringVal.Equals(string.Empty))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
else if (binders.TryGetValue(typeof(T), out var binder))
|
||||
{
|
||||
return binder.Read(stringVal);
|
||||
}
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
return destinationType == typeof(T) && binders.TryGetValue(destinationType, out var binder)
|
||||
? binder.Write(value)
|
||||
: base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
359
FastGithub.DomainResolve/DnsClient.cs
Normal file
359
FastGithub.DomainResolve/DnsClient.cs
Normal file
@ -0,0 +1,359 @@
|
||||
using DNS.Client;
|
||||
using DNS.Client.RequestResolver;
|
||||
using DNS.Protocol;
|
||||
using DNS.Protocol.ResourceRecords;
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
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;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// DNS客户端
|
||||
/// </summary>
|
||||
sealed class DnsClient
|
||||
{
|
||||
private const int DNS_PORT = 53;
|
||||
private const string LOCALHOST = "localhost";
|
||||
|
||||
private readonly DnscryptProxy dnscryptProxy;
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<DnsClient> logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> semaphoreSlims = new();
|
||||
private readonly IMemoryCache dnsStateCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
private readonly IMemoryCache dnsLookupCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
|
||||
private readonly TimeSpan stateExpiration = TimeSpan.FromMinutes(5d);
|
||||
private readonly TimeSpan minTimeToLive = TimeSpan.FromSeconds(30d);
|
||||
private readonly TimeSpan maxTimeToLive = TimeSpan.FromMinutes(10d);
|
||||
|
||||
private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(4d).TotalMilliseconds;
|
||||
private static readonly TimeSpan tcpConnectTimeout = TimeSpan.FromSeconds(2d);
|
||||
|
||||
private record LookupResult(IList<IPAddress> Addresses, TimeSpan TimeToLive);
|
||||
|
||||
/// <summary>
|
||||
/// DNS客户端
|
||||
/// </summary>
|
||||
/// <param name="dnscryptProxy"></param>
|
||||
/// <param name="fastGithubConfig"></param>
|
||||
/// <param name="logger"></param>
|
||||
public DnsClient(
|
||||
DnscryptProxy dnscryptProxy,
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<DnsClient> logger)
|
||||
{
|
||||
this.dnscryptProxy = dnscryptProxy;
|
||||
this.fastGithubConfig = fastGithubConfig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="endPoint">远程结节</param>
|
||||
/// <param name="fastSort">是否使用快速排序</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, bool fastSort, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var hashSet = new HashSet<IPAddress>();
|
||||
await foreach (var dns in this.GetDnsServersAsync(cancellationToken))
|
||||
{
|
||||
var addresses = await this.LookupAsync(dns, endPoint, fastSort, cancellationToken);
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (hashSet.Add(address) == true)
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取dns服务
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async IAsyncEnumerable<IPEndPoint> GetDnsServersAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var cryptDns = this.dnscryptProxy.LocalEndPoint;
|
||||
if (cryptDns != null)
|
||||
{
|
||||
yield return cryptDns;
|
||||
yield return cryptDns;
|
||||
}
|
||||
|
||||
foreach (var dns in this.fastGithubConfig.FallbackDns)
|
||||
{
|
||||
if (await this.IsDnsAvailableAsync(dns, cancellationToken))
|
||||
{
|
||||
yield return dns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取dns是否可用
|
||||
/// </summary>
|
||||
/// <param name="dns"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async ValueTask<bool> IsDnsAvailableAsync(IPEndPoint dns, CancellationToken cancellationToken)
|
||||
{
|
||||
if (dns.Port != DNS_PORT)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.dnsStateCache.TryGetValue<bool>(dns, out var available))
|
||||
{
|
||||
return available;
|
||||
}
|
||||
|
||||
var key = dns.ToString();
|
||||
var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
await semaphore.WaitAsync(CancellationToken.None);
|
||||
|
||||
try
|
||||
{
|
||||
using var timeoutTokenSource = new CancellationTokenSource(tcpConnectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
|
||||
using var socket = new Socket(dns.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(dns, linkedTokenSource.Token);
|
||||
return this.dnsStateCache.Set(dns, true, this.stateExpiration);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return this.dnsStateCache.Set(dns, false, this.stateExpiration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="dns"></param>
|
||||
/// <param name="endPoint"></param>
|
||||
/// <param name="fastSort"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IList<IPAddress>> LookupAsync(IPEndPoint dns, DnsEndPoint endPoint, bool fastSort, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{dns}/{endPoint}";
|
||||
var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
await semaphore.WaitAsync(CancellationToken.None);
|
||||
|
||||
try
|
||||
{
|
||||
if (this.dnsLookupCache.TryGetValue<IList<IPAddress>>(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
var result = await this.LookupCoreAsync(dns, endPoint, fastSort, cancellationToken);
|
||||
return this.dnsLookupCache.Set(key, result.Addresses, result.TimeToLive);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Array.Empty<IPAddress>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning($"{endPoint.Host}@{dns}->{ex.Message}");
|
||||
var expiration = IsSocketException(ex) ? this.maxTimeToLive : this.minTimeToLive;
|
||||
return this.dnsLookupCache.Set(key, Array.Empty<IPAddress>(), expiration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为Socket异常
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
/// <returns></returns>
|
||||
private static bool IsSocketException(Exception ex)
|
||||
{
|
||||
if (ex is SocketException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var inner = ex.InnerException;
|
||||
return inner != null && IsSocketException(inner);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="dns"></param>
|
||||
/// <param name="endPoint"></param>
|
||||
/// <param name="fastSort"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<LookupResult> LookupCoreAsync(IPEndPoint dns, DnsEndPoint endPoint, bool fastSort, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (endPoint.Host == LOCALHOST)
|
||||
{
|
||||
var loopbacks = new List<IPAddress>();
|
||||
if (Socket.OSSupportsIPv4 == true)
|
||||
{
|
||||
loopbacks.Add(IPAddress.Loopback);
|
||||
}
|
||||
if (Socket.OSSupportsIPv6 == true)
|
||||
{
|
||||
loopbacks.Add(IPAddress.IPv6Loopback);
|
||||
}
|
||||
return new LookupResult(loopbacks, TimeSpan.MaxValue);
|
||||
}
|
||||
|
||||
var resolver = dns.Port == DNS_PORT
|
||||
? (IRequestResolver)new TcpRequestResolver(dns)
|
||||
: new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.resolveTimeout);
|
||||
|
||||
var addressRecords = await GetAddressRecordsAsync(resolver, endPoint.Host, cancellationToken);
|
||||
var addresses = (IList<IPAddress>)addressRecords
|
||||
.Where(item => IPAddress.IsLoopback(item.IPAddress) == false)
|
||||
.Select(item => item.IPAddress)
|
||||
.ToArray();
|
||||
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
return new LookupResult(addresses, this.minTimeToLive);
|
||||
}
|
||||
|
||||
if (fastSort == true)
|
||||
{
|
||||
addresses = await OrderByConnectAnyAsync(addresses, endPoint.Port, cancellationToken);
|
||||
}
|
||||
|
||||
var timeToLive = addressRecords.Min(item => item.TimeToLive);
|
||||
if (timeToLive <= TimeSpan.Zero)
|
||||
{
|
||||
timeToLive = this.minTimeToLive;
|
||||
}
|
||||
else if (timeToLive > this.maxTimeToLive)
|
||||
{
|
||||
timeToLive = this.maxTimeToLive;
|
||||
}
|
||||
|
||||
return new LookupResult(addresses, timeToLive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取IP记录
|
||||
/// </summary>
|
||||
/// <param name="resolver"></param>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task<IList<IPAddressResourceRecord>> GetAddressRecordsAsync(IRequestResolver resolver, string domain, CancellationToken cancellationToken)
|
||||
{
|
||||
var addressRecords = new List<IPAddressResourceRecord>();
|
||||
if (Socket.OSSupportsIPv4 == true)
|
||||
{
|
||||
var records = await GetRecordsAsync(RecordType.A);
|
||||
addressRecords.AddRange(records);
|
||||
}
|
||||
|
||||
if (Socket.OSSupportsIPv6 == true)
|
||||
{
|
||||
var records = await GetRecordsAsync(RecordType.AAAA);
|
||||
addressRecords.AddRange(records);
|
||||
}
|
||||
return addressRecords;
|
||||
|
||||
|
||||
async Task<IEnumerable<IPAddressResourceRecord>> GetRecordsAsync(RecordType recordType)
|
||||
{
|
||||
var request = new Request
|
||||
{
|
||||
RecursionDesired = true,
|
||||
OperationCode = OperationCode.Query
|
||||
};
|
||||
|
||||
request.Questions.Add(new Question(new Domain(domain), recordType));
|
||||
var clientRequest = new ClientRequest(resolver, request);
|
||||
var response = await clientRequest.Resolve(cancellationToken);
|
||||
return response.AnswerRecords.OfType<IPAddressResourceRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 连接速度排序
|
||||
/// </summary>
|
||||
/// <param name="addresses"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task<IList<IPAddress>> OrderByConnectAnyAsync(IList<IPAddress> addresses, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
if (addresses.Count <= 1)
|
||||
{
|
||||
return addresses;
|
||||
}
|
||||
|
||||
using var controlTokenSource = new CancellationTokenSource(tcpConnectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, controlTokenSource.Token);
|
||||
|
||||
var connectTasks = addresses.Select(address => ConnectAsync(address, port, linkedTokenSource.Token));
|
||||
var fastestAddress = await await Task.WhenAny(connectTasks);
|
||||
controlTokenSource.Cancel();
|
||||
|
||||
if (fastestAddress == null || addresses.First().Equals(fastestAddress))
|
||||
{
|
||||
return addresses;
|
||||
}
|
||||
|
||||
var list = new List<IPAddress> { fastestAddress };
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address.Equals(fastestAddress) == false)
|
||||
{
|
||||
list.Add(address);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接指定ip和端口
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task<IPAddress?> ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(address, port, cancellationToken);
|
||||
return address;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
154
FastGithub.DomainResolve/DnscryptProxy.cs
Normal file
154
FastGithub.DomainResolve/DnscryptProxy.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static PInvoke.AdvApi32;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// DnscryptProxy服务
|
||||
/// </summary>
|
||||
sealed class DnscryptProxy
|
||||
{
|
||||
private readonly ILogger<DnscryptProxy> logger;
|
||||
private readonly string processName;
|
||||
private readonly string serviceName;
|
||||
private readonly string exeFilePath;
|
||||
private readonly string tomlFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// 相关进程
|
||||
/// </summary>
|
||||
private Process? process;
|
||||
|
||||
/// <summary>
|
||||
/// 获取监听的节点
|
||||
/// </summary>
|
||||
public IPEndPoint? LocalEndPoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// DnscryptProxy服务
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public DnscryptProxy(ILogger<DnscryptProxy> logger)
|
||||
{
|
||||
const string PATH = "dnscrypt-proxy";
|
||||
const string NAME = "dnscrypt-proxy";
|
||||
|
||||
this.logger = logger;
|
||||
this.processName = NAME;
|
||||
this.serviceName = $"{nameof(FastGithub)}.{NAME}";
|
||||
this.exeFilePath = Path.Combine(PATH, OperatingSystem.IsWindows() ? $"{NAME}.exe" : NAME);
|
||||
this.tomlFilePath = Path.Combine(PATH, $"{NAME}.toml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动dnscrypt-proxy
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.StartCoreAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning($"{this.processName}启动失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动dnscrypt-proxy
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task StartCoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var port = GlobalListener.GetAvailablePort(5533);
|
||||
var localEndPoint = new IPEndPoint(IPAddress.Loopback, port);
|
||||
|
||||
await TomlUtil.SetListensAsync(this.tomlFilePath, localEndPoint, cancellationToken);
|
||||
await TomlUtil.SetLogLevelAsync(this.tomlFilePath, 6, cancellationToken);
|
||||
await TomlUtil.SetLBStrategyAsync(this.tomlFilePath, "ph", cancellationToken);
|
||||
await TomlUtil.SetMinMaxTTLAsync(this.tomlFilePath, TimeSpan.FromMinutes(1d), TimeSpan.FromMinutes(2d), cancellationToken);
|
||||
|
||||
if (OperatingSystem.IsWindows() && Environment.UserInteractive == false)
|
||||
{
|
||||
ServiceInstallUtil.StopAndDeleteService(this.serviceName);
|
||||
ServiceInstallUtil.InstallAndStartService(this.serviceName, this.exeFilePath, ServiceStartType.SERVICE_DEMAND_START);
|
||||
this.process = Process.GetProcessesByName(this.processName).FirstOrDefault(item => item.SessionId == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.process = StartDnscryptProxy();
|
||||
}
|
||||
|
||||
if (this.process != null)
|
||||
{
|
||||
this.LocalEndPoint = localEndPoint;
|
||||
this.process.EnableRaisingEvents = true;
|
||||
this.process.Exited += (s, e) => this.LocalEndPoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && Environment.UserInteractive == false)
|
||||
{
|
||||
ServiceInstallUtil.StopAndDeleteService(this.serviceName);
|
||||
}
|
||||
|
||||
if (this.process != null && this.process.HasExited == false)
|
||||
{
|
||||
this.process.Kill();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning($"{this.processName}停止失败:{ex.Message }");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.LocalEndPoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动DnscryptProxy进程
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Process? StartDnscryptProxy()
|
||||
{
|
||||
return Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = this.exeFilePath,
|
||||
WorkingDirectory = Path.GetDirectoryName(this.exeFilePath),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为字符串
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.processName;
|
||||
}
|
||||
}
|
||||
}
|
73
FastGithub.DomainResolve/DomainResolveHostedService.cs
Normal file
73
FastGithub.DomainResolve/DomainResolveHostedService.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名解析后台服务
|
||||
/// </summary>
|
||||
sealed class DomainResolveHostedService : BackgroundService
|
||||
{
|
||||
private readonly DnscryptProxy dnscryptProxy;
|
||||
private readonly IDomainResolver domainResolver;
|
||||
private readonly ILogger<DomainResolveHostedService> logger;
|
||||
private readonly TimeSpan dnscryptProxyInitDelay = TimeSpan.FromSeconds(5d);
|
||||
private readonly TimeSpan testPeriodTimeSpan = TimeSpan.FromSeconds(1d);
|
||||
|
||||
/// <summary>
|
||||
/// 域名解析后台服务
|
||||
/// </summary>
|
||||
/// <param name="dnscryptProxy"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
public DomainResolveHostedService(
|
||||
DnscryptProxy dnscryptProxy,
|
||||
IDomainResolver domainResolver,
|
||||
ILogger<DomainResolveHostedService> logger)
|
||||
{
|
||||
this.dnscryptProxy = dnscryptProxy;
|
||||
this.domainResolver = domainResolver;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台任务
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.dnscryptProxy.StartAsync(stoppingToken);
|
||||
await Task.Delay(dnscryptProxyInitDelay, stoppingToken);
|
||||
|
||||
while (stoppingToken.IsCancellationRequested == false)
|
||||
{
|
||||
await this.domainResolver.TestSpeedAsync(stoppingToken);
|
||||
await Task.Delay(this.testPeriodTimeSpan, stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "域名解析异常");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.dnscryptProxy.Stop();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
103
FastGithub.DomainResolve/DomainResolver.cs
Normal file
103
FastGithub.DomainResolve/DomainResolver.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
/// </summary>
|
||||
sealed class DomainResolver : IDomainResolver
|
||||
{
|
||||
private const int MAX_IP_COUNT = 3;
|
||||
private readonly DnsClient dnsClient;
|
||||
private readonly PersistenceService persistence;
|
||||
private readonly IPAddressService addressService;
|
||||
private readonly ILogger<DomainResolver> logger;
|
||||
private readonly ConcurrentDictionary<DnsEndPoint, IPAddress[]> dnsEndPointAddress = new();
|
||||
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
/// </summary>
|
||||
/// <param name="dnsClient"></param>
|
||||
/// <param name="persistence"></param>
|
||||
/// <param name="addressService"></param>
|
||||
/// <param name="logger"></param>
|
||||
public DomainResolver(
|
||||
DnsClient dnsClient,
|
||||
PersistenceService persistence,
|
||||
IPAddressService addressService,
|
||||
ILogger<DomainResolver> logger)
|
||||
{
|
||||
this.dnsClient = dnsClient;
|
||||
this.persistence = persistence;
|
||||
this.addressService = addressService;
|
||||
this.logger = logger;
|
||||
|
||||
foreach (var endPoint in persistence.ReadDnsEndPoints())
|
||||
{
|
||||
this.dnsEndPointAddress.TryAdd(endPoint, Array.Empty<IPAddress>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="endPoint">节点</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.dnsEndPointAddress.TryGetValue(endPoint, out var addresses) && addresses.Length > 0)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.dnsEndPointAddress.TryAdd(endPoint, Array.Empty<IPAddress>()))
|
||||
{
|
||||
await this.persistence.WriteDnsEndPointsAsync(this.dnsEndPointAddress.Keys, cancellationToken);
|
||||
}
|
||||
|
||||
await foreach (var adddress in this.dnsClient.ResolveAsync(endPoint, fastSort: true, cancellationToken))
|
||||
{
|
||||
yield return adddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对所有节点进行测速
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task TestSpeedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var keyValue in this.dnsEndPointAddress.OrderBy(item => item.Value.Length))
|
||||
{
|
||||
var dnsEndPoint = keyValue.Key;
|
||||
var oldAddresses = keyValue.Value;
|
||||
|
||||
var newAddresses = await this.addressService.GetAddressesAsync(dnsEndPoint, oldAddresses, cancellationToken);
|
||||
this.dnsEndPointAddress[dnsEndPoint] = newAddresses;
|
||||
|
||||
var oldSegmentums = oldAddresses.Take(MAX_IP_COUNT);
|
||||
var newSegmentums = newAddresses.Take(MAX_IP_COUNT);
|
||||
if (oldSegmentums.SequenceEqual(newSegmentums) == false)
|
||||
{
|
||||
var addressArray = string.Join(", ", newSegmentums.Select(item => item.ToString()));
|
||||
this.logger.LogInformation($"{dnsEndPoint.Host}:{dnsEndPoint.Port}->[{addressArray}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
FastGithub.DomainResolve/FastGithub.DomainResolve.csproj
Normal file
21
FastGithub.DomainResolve/FastGithub.DomainResolve.csproj
Normal file
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PInvoke.AdvApi32" Version="0.7.124" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="DNS" Version="7.0.0" />
|
||||
<PackageReference Include="Tommy" Version="3.1.2" />
|
||||
<ProjectReference Include="..\FastGithub.Configuration\FastGithub.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../@dnscrypt-proxy/*" Link="dnscrypt-proxy/%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="../@dnscrypt-proxy/$(RuntimeIdentifier)/*" Link="dnscrypt-proxy/%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
28
FastGithub.DomainResolve/IDomainResolver.cs
Normal file
28
FastGithub.DomainResolve/IDomainResolver.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
/// </summary>
|
||||
public interface IDomainResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析所有ip
|
||||
/// </summary>
|
||||
/// <param name="endPoint">节点</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 对所有节点进行测速
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task TestSpeedAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
142
FastGithub.DomainResolve/IPAddressService.cs
Normal file
142
FastGithub.DomainResolve/IPAddressService.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// IP服务
|
||||
/// 域名IP关系缓存10分钟
|
||||
/// IPEndPoint时延缓存5分钟
|
||||
/// IPEndPoint连接超时5秒
|
||||
/// </summary>
|
||||
sealed class IPAddressService
|
||||
{
|
||||
private record DomainAddress(string Domain, IPAddress Address);
|
||||
private readonly TimeSpan domainAddressExpiration = TimeSpan.FromMinutes(10d);
|
||||
private readonly IMemoryCache domainAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
|
||||
private record AddressElapsed(IPAddress Address, TimeSpan Elapsed);
|
||||
private readonly TimeSpan problemElapsedExpiration = TimeSpan.FromMinutes(1d);
|
||||
private readonly TimeSpan normalElapsedExpiration = TimeSpan.FromMinutes(5d);
|
||||
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d);
|
||||
private readonly IMemoryCache addressElapsedCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
|
||||
|
||||
private readonly DnsClient dnsClient;
|
||||
|
||||
/// <summary>
|
||||
/// IP服务
|
||||
/// </summary>
|
||||
/// <param name="dnsClient"></param>
|
||||
public IPAddressService(DnsClient dnsClient)
|
||||
{
|
||||
this.dnsClient = dnsClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 并行获取可连接的IP
|
||||
/// </summary>
|
||||
/// <param name="dnsEndPoint"></param>
|
||||
/// <param name="oldAddresses"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IPAddress[]> GetAddressesAsync(DnsEndPoint dnsEndPoint, IEnumerable<IPAddress> oldAddresses, CancellationToken cancellationToken)
|
||||
{
|
||||
var ipEndPoints = new HashSet<IPEndPoint>();
|
||||
|
||||
// 历史未过期的IP节点
|
||||
foreach (var address in oldAddresses)
|
||||
{
|
||||
var domainAddress = new DomainAddress(dnsEndPoint.Host, address);
|
||||
if (this.domainAddressCache.TryGetValue(domainAddress, out _))
|
||||
{
|
||||
ipEndPoints.Add(new IPEndPoint(address, dnsEndPoint.Port));
|
||||
}
|
||||
}
|
||||
|
||||
// 新解析出的IP节点
|
||||
await foreach (var address in this.dnsClient.ResolveAsync(dnsEndPoint, fastSort: false, cancellationToken))
|
||||
{
|
||||
ipEndPoints.Add(new IPEndPoint(address, dnsEndPoint.Port));
|
||||
var domainAddress = new DomainAddress(dnsEndPoint.Host, address);
|
||||
this.domainAddressCache.Set(domainAddress, default(object), this.domainAddressExpiration);
|
||||
}
|
||||
|
||||
if (ipEndPoints.Count == 0)
|
||||
{
|
||||
return Array.Empty<IPAddress>();
|
||||
}
|
||||
|
||||
var addressElapsedTasks = ipEndPoints.Select(item => this.GetAddressElapsedAsync(item, cancellationToken));
|
||||
var addressElapseds = await Task.WhenAll(addressElapsedTasks);
|
||||
|
||||
return addressElapseds
|
||||
.Where(item => item.Elapsed < TimeSpan.MaxValue)
|
||||
.OrderBy(item => item.Elapsed)
|
||||
.Select(item => item.Address)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取IP节点的时延
|
||||
/// </summary>
|
||||
/// <param name="endPoint"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<AddressElapsed> GetAddressElapsedAsync(IPEndPoint endPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.addressElapsedCache.TryGetValue<AddressElapsed>(endPoint, out var addressElapsed))
|
||||
{
|
||||
return addressElapsed;
|
||||
}
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
|
||||
|
||||
addressElapsed = new AddressElapsed(endPoint.Address, stopWatch.Elapsed);
|
||||
return this.addressElapsedCache.Set(endPoint, addressElapsed, this.normalElapsedExpiration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
addressElapsed = new AddressElapsed(endPoint.Address, TimeSpan.MaxValue);
|
||||
var expiration = IsLocalNetworkProblem(ex) ? this.problemElapsedExpiration : this.normalElapsedExpiration;
|
||||
return this.addressElapsedCache.Set(endPoint, addressElapsed, expiration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopWatch.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为本机网络问题
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
/// <returns></returns>
|
||||
private static bool IsLocalNetworkProblem(Exception ex)
|
||||
{
|
||||
if (ex is not SocketException socketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var code = socketException.SocketErrorCode;
|
||||
return code == SocketError.NetworkDown || code == SocketError.NetworkUnreachable;
|
||||
}
|
||||
}
|
||||
}
|
121
FastGithub.DomainResolve/PersistenceService.cs
Normal file
121
FastGithub.DomainResolve/PersistenceService.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名持久化
|
||||
/// </summary>
|
||||
sealed partial class PersistenceService
|
||||
{
|
||||
private static readonly string dataFile = "dnsendpoints.json";
|
||||
private static readonly SemaphoreSlim dataLocker = new(1, 1);
|
||||
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<PersistenceService> logger;
|
||||
|
||||
|
||||
private record EndPointItem(string Host, int Port);
|
||||
|
||||
[JsonSerializable(typeof(EndPointItem[]))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
private partial class EndPointItemsContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 域名持久化
|
||||
/// </summary>
|
||||
/// <param name="fastGithubConfig"></param>
|
||||
/// <param name="logger"></param>
|
||||
public PersistenceService(
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<PersistenceService> logger)
|
||||
{
|
||||
this.fastGithubConfig = fastGithubConfig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 读取保存的节点
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IList<DnsEndPoint> ReadDnsEndPoints()
|
||||
{
|
||||
if (File.Exists(dataFile) == false)
|
||||
{
|
||||
return Array.Empty<DnsEndPoint>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dataLocker.Wait();
|
||||
|
||||
var utf8Json = File.ReadAllBytes(dataFile);
|
||||
var endPointItems = JsonSerializer.Deserialize(utf8Json, EndPointItemsContext.Default.EndPointItemArray);
|
||||
if (endPointItems == null)
|
||||
{
|
||||
return Array.Empty<DnsEndPoint>();
|
||||
}
|
||||
|
||||
var dnsEndPoints = new List<DnsEndPoint>();
|
||||
foreach (var item in endPointItems)
|
||||
{
|
||||
if (this.fastGithubConfig.IsMatch(item.Host) == true)
|
||||
{
|
||||
dnsEndPoints.Add(new DnsEndPoint(item.Host, item.Port));
|
||||
}
|
||||
}
|
||||
return dnsEndPoints;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning(ex.Message, "读取dns记录异常");
|
||||
return Array.Empty<DnsEndPoint>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
dataLocker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存节点到文件
|
||||
/// </summary>
|
||||
/// <param name="dnsEndPoints"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task WriteDnsEndPointsAsync(IEnumerable<DnsEndPoint> dnsEndPoints, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dataLocker.WaitAsync(CancellationToken.None);
|
||||
|
||||
var endPointItems = dnsEndPoints.Select(item => new EndPointItem(item.Host, item.Port)).ToArray();
|
||||
var utf8Json = JsonSerializer.SerializeToUtf8Bytes(endPointItems, EndPointItemsContext.Default.EndPointItemArray);
|
||||
await File.WriteAllBytesAsync(dataFile, utf8Json, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning(ex.Message, "保存dns记录异常");
|
||||
}
|
||||
finally
|
||||
{
|
||||
dataLocker.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
FastGithub.DomainResolve/ServiceCollectionExtensions.cs
Normal file
28
FastGithub.DomainResolve/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using FastGithub.DomainResolve;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册域名解析相关服务
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddDomainResolve(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<DnsClient>();
|
||||
services.TryAddSingleton<DnscryptProxy>();
|
||||
services.TryAddSingleton<PersistenceService>();
|
||||
services.TryAddSingleton<IPAddressService>();
|
||||
services.TryAddSingleton<IDomainResolver, DomainResolver>();
|
||||
services.AddHostedService<DomainResolveHostedService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
88
FastGithub.DomainResolve/ServiceInstallUtil.cs
Normal file
88
FastGithub.DomainResolve/ServiceInstallUtil.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using static PInvoke.AdvApi32;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
public static class ServiceInstallUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装并启动服务
|
||||
/// </summary>
|
||||
/// <param name="serviceName"></param>
|
||||
/// <param name="binaryPath"></param>
|
||||
/// <param name="startType"></param>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static bool InstallAndStartService(string serviceName, string binaryPath, ServiceStartType startType = ServiceStartType.SERVICE_AUTO_START)
|
||||
{
|
||||
using var hSCManager = OpenSCManager(null, null, ServiceManagerAccess.SC_MANAGER_ALL_ACCESS);
|
||||
if (hSCManager.IsInvalid == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hService = OpenService(hSCManager, serviceName, ServiceAccess.SERVICE_ALL_ACCESS);
|
||||
if (hService.IsInvalid == true)
|
||||
{
|
||||
hService = CreateService(
|
||||
hSCManager,
|
||||
serviceName,
|
||||
serviceName,
|
||||
ServiceAccess.SERVICE_ALL_ACCESS,
|
||||
ServiceType.SERVICE_WIN32_OWN_PROCESS,
|
||||
startType,
|
||||
ServiceErrorControl.SERVICE_ERROR_NORMAL,
|
||||
Path.GetFullPath(binaryPath),
|
||||
lpLoadOrderGroup: null,
|
||||
lpdwTagId: 0,
|
||||
lpDependencies: null,
|
||||
lpServiceStartName: null,
|
||||
lpPassword: null);
|
||||
}
|
||||
|
||||
if (hService.IsInvalid == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using (hService)
|
||||
{
|
||||
return StartService(hService, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止并删除服务
|
||||
/// </summary>
|
||||
/// <param name="serviceName"></param>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static bool StopAndDeleteService(string serviceName)
|
||||
{
|
||||
using var hSCManager = OpenSCManager(null, null, ServiceManagerAccess.SC_MANAGER_ALL_ACCESS);
|
||||
if (hSCManager.IsInvalid == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var hService = OpenService(hSCManager, serviceName, ServiceAccess.SERVICE_ALL_ACCESS);
|
||||
if (hService.IsInvalid == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var status = new SERVICE_STATUS();
|
||||
if (QueryServiceStatus(hService, ref status) == true)
|
||||
{
|
||||
if (status.dwCurrentState != ServiceState.SERVICE_STOP_PENDING &&
|
||||
status.dwCurrentState != ServiceState.SERVICE_STOPPED)
|
||||
{
|
||||
ControlService(hService, ServiceControl.SERVICE_CONTROL_STOP, ref status);
|
||||
}
|
||||
}
|
||||
|
||||
return DeleteService(hService);
|
||||
}
|
||||
}
|
||||
}
|
98
FastGithub.DomainResolve/TomlUtil.cs
Normal file
98
FastGithub.DomainResolve/TomlUtil.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tommy;
|
||||
|
||||
namespace FastGithub.DomainResolve
|
||||
{
|
||||
/// <summary>
|
||||
/// doml配置工具
|
||||
/// </summary>
|
||||
static class TomlUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置监听地址
|
||||
/// </summary>
|
||||
/// <param name="tomlPath"></param>
|
||||
/// <param name="endpoint"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static Task SetListensAsync(string tomlPath, IPEndPoint endpoint, CancellationToken cancellationToken)
|
||||
{
|
||||
var value = new TomlArray
|
||||
{
|
||||
endpoint.ToString()
|
||||
};
|
||||
return SetAsync(tomlPath, "listen_addresses", value, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置日志等级
|
||||
/// </summary>
|
||||
/// <param name="tomlPath"></param>
|
||||
/// <param name="logLevel"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static Task SetLogLevelAsync(string tomlPath, int logLevel, CancellationToken cancellationToken)
|
||||
{
|
||||
return SetAsync(tomlPath, "log_level", new TomlInteger { Value = logLevel }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置负载均衡模式
|
||||
/// </summary>
|
||||
/// <param name="tomlPath"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static Task SetLBStrategyAsync(string tomlPath, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
return SetAsync(tomlPath, "lb_strategy", new TomlString { Value = value }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置TTL
|
||||
/// </summary>
|
||||
/// <param name="tomlPath"></param>
|
||||
/// <param name="minTTL"></param>
|
||||
/// <param name="maxTTL"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task SetMinMaxTTLAsync(string tomlPath, TimeSpan minTTL, TimeSpan maxTTL, CancellationToken cancellationToken)
|
||||
{
|
||||
var minValue = new TomlInteger { Value = (int)minTTL.TotalSeconds };
|
||||
var maxValue = new TomlInteger { Value = (int)maxTTL.TotalSeconds };
|
||||
|
||||
await SetAsync(tomlPath, "cache_min_ttl", minValue, cancellationToken);
|
||||
await SetAsync(tomlPath, "cache_neg_min_ttl", minValue, cancellationToken);
|
||||
await SetAsync(tomlPath, "cache_max_ttl", maxValue, cancellationToken);
|
||||
await SetAsync(tomlPath, "cache_neg_max_ttl", maxValue, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定键的值
|
||||
/// </summary>
|
||||
/// <param name="tomlPath"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task SetAsync(string tomlPath, string key, TomlNode value, CancellationToken cancellationToken)
|
||||
{
|
||||
var toml = await File.ReadAllTextAsync(tomlPath, cancellationToken);
|
||||
var reader = new StringReader(toml);
|
||||
var tomlTable = TOML.Parse(reader);
|
||||
tomlTable[key] = value;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var writer = new StringWriter(builder);
|
||||
tomlTable.WriteTo(writer);
|
||||
toml = builder.ToString();
|
||||
|
||||
await File.WriteAllTextAsync(tomlPath, toml, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
44
FastGithub.FlowAnalyze/DelegatingDuplexPipe.cs
Normal file
44
FastGithub.FlowAnalyze/DelegatingDuplexPipe.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
class DelegatingDuplexPipe<TDelegatingStream> : IDuplexPipe, IAsyncDisposable where TDelegatingStream : DelegatingStream
|
||||
{
|
||||
private bool disposed;
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
public PipeReader Input { get; }
|
||||
|
||||
public PipeWriter Output { get; }
|
||||
|
||||
public DelegatingDuplexPipe(IDuplexPipe duplexPipe, Func<Stream, TDelegatingStream> delegatingStreamFactory) :
|
||||
this(duplexPipe, new StreamPipeReaderOptions(leaveOpen: true), new StreamPipeWriterOptions(leaveOpen: true), delegatingStreamFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public DelegatingDuplexPipe(IDuplexPipe duplexPipe, StreamPipeReaderOptions readerOptions, StreamPipeWriterOptions writerOptions, Func<Stream, TDelegatingStream> delegatingStreamFactory)
|
||||
{
|
||||
var delegatingStream = delegatingStreamFactory(duplexPipe.AsStream());
|
||||
this.Input = PipeReader.Create(delegatingStream, readerOptions);
|
||||
this.Output = PipeWriter.Create(delegatingStream, writerOptions);
|
||||
}
|
||||
|
||||
public virtual async ValueTask DisposeAsync()
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.disposed == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
await this.Input.CompleteAsync();
|
||||
await this.Output.CompleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
142
FastGithub.FlowAnalyze/DelegatingStream.cs
Normal file
142
FastGithub.FlowAnalyze/DelegatingStream.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
abstract class DelegatingStream : Stream
|
||||
{
|
||||
protected Stream Inner { get; }
|
||||
|
||||
public DelegatingStream(Stream inner)
|
||||
{
|
||||
this.Inner = inner;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Inner.CanRead;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Inner.CanSeek;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Inner.CanWrite;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Inner.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Inner.Position;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.Inner.Position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
this.Inner.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this.Inner.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return this.Inner.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override int Read(Span<byte> destination)
|
||||
{
|
||||
return this.Inner.Read(destination);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return this.Inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.Inner.ReadAsync(destination, cancellationToken);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return this.Inner.Seek(offset, origin);
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
this.Inner.SetLength(value);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
this.Inner.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
this.Inner.Write(source);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return this.Inner.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.Inner.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return TaskToApm.End<int>(asyncResult);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
TaskToApm.End(asyncResult);
|
||||
}
|
||||
}
|
||||
}
|
175
FastGithub.FlowAnalyze/DuplexPipeStreamExtensions.cs
Normal file
175
FastGithub.FlowAnalyze/DuplexPipeStreamExtensions.cs
Normal file
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
static class DuplexPipeStreamExtensions
|
||||
{
|
||||
public static Stream AsStream(this IDuplexPipe duplexPipe, bool throwOnCancelled = false)
|
||||
{
|
||||
return new DuplexPipeStream(duplexPipe, throwOnCancelled);
|
||||
}
|
||||
|
||||
private class DuplexPipeStream : Stream
|
||||
{
|
||||
private readonly PipeReader input;
|
||||
private readonly PipeWriter output;
|
||||
private readonly bool throwOnCancelled;
|
||||
private volatile bool cancelCalled;
|
||||
|
||||
public DuplexPipeStream(IDuplexPipe duplexPipe, bool throwOnCancelled = false)
|
||||
{
|
||||
this.input = duplexPipe.Input;
|
||||
this.output = duplexPipe.Output;
|
||||
this.throwOnCancelled = throwOnCancelled;
|
||||
}
|
||||
|
||||
public void CancelPendingRead()
|
||||
{
|
||||
this.cancelCalled = true;
|
||||
this.input.CancelPendingRead();
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
ValueTask<int> vt = ReadAsyncInternal(new Memory<byte>(buffer, offset, count), default);
|
||||
return vt.IsCompleted ?
|
||||
vt.Result :
|
||||
vt.AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ReadAsyncInternal(destination, cancellationToken);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
await this.output.WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.output.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.output.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
|
||||
private async ValueTask<int> ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await this.input.ReadAsync(cancellationToken);
|
||||
var readableBuffer = result.Buffer;
|
||||
try
|
||||
{
|
||||
if (this.throwOnCancelled && result.IsCanceled && this.cancelCalled)
|
||||
{
|
||||
// Reset the bool
|
||||
this.cancelCalled = false;
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
// buffer.Count is int
|
||||
var count = (int)Math.Min(readableBuffer.Length, destination.Length);
|
||||
readableBuffer = readableBuffer.Slice(0, count);
|
||||
readableBuffer.CopyTo(destination.Span);
|
||||
return count;
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.input.AdvanceTo(readableBuffer.End, readableBuffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return TaskToApm.End<int>(asyncResult);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
TaskToApm.End(asyncResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
FastGithub.FlowAnalyze/FastGithub.FlowAnalyze.csproj
Normal file
7
FastGithub.FlowAnalyze/FastGithub.FlowAnalyze.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
12
FastGithub.FlowAnalyze/FlowAnalyzeDuplexPipe.cs
Normal file
12
FastGithub.FlowAnalyze/FlowAnalyzeDuplexPipe.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.IO.Pipelines;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
sealed class FlowAnalyzeDuplexPipe : DelegatingDuplexPipe<FlowAnalyzeStream>
|
||||
{
|
||||
public FlowAnalyzeDuplexPipe(IDuplexPipe duplexPipe, IFlowAnalyzer flowAnalyzer) :
|
||||
base(duplexPipe, stream => new FlowAnalyzeStream(stream, flowAnalyzer))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
71
FastGithub.FlowAnalyze/FlowAnalyzeStream.cs
Normal file
71
FastGithub.FlowAnalyze/FlowAnalyzeStream.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
sealed class FlowAnalyzeStream : DelegatingStream
|
||||
{
|
||||
private readonly IFlowAnalyzer flowAnalyzer;
|
||||
|
||||
public FlowAnalyzeStream(Stream inner, IFlowAnalyzer flowAnalyzer)
|
||||
: base(inner)
|
||||
{
|
||||
this.flowAnalyzer = flowAnalyzer;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int read = base.Read(buffer, offset, count);
|
||||
this.flowAnalyzer.OnFlow(FlowType.Read, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
public override int Read(Span<byte> destination)
|
||||
{
|
||||
int read = base.Read(destination);
|
||||
this.flowAnalyzer.OnFlow(FlowType.Read, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int read = await base.ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
|
||||
this.flowAnalyzer.OnFlow(FlowType.Read, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int read = await base.ReadAsync(destination, cancellationToken);
|
||||
this.flowAnalyzer.OnFlow(FlowType.Read, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
this.flowAnalyzer.OnFlow(FlowType.Wirte, count);
|
||||
base.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
this.flowAnalyzer.OnFlow(FlowType.Wirte, source.Length);
|
||||
base.Write(source);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
this.flowAnalyzer.OnFlow(FlowType.Wirte, count);
|
||||
return base.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.flowAnalyzer.OnFlow(FlowType.Wirte, source.Length);
|
||||
return base.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
104
FastGithub.FlowAnalyze/FlowAnalyzer.cs
Normal file
104
FastGithub.FlowAnalyze/FlowAnalyzer.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
sealed class FlowAnalyzer : IFlowAnalyzer
|
||||
{
|
||||
private const int INTERVAL_SECONDS = 5;
|
||||
private readonly FlowQueues readQueues = new(INTERVAL_SECONDS);
|
||||
private readonly FlowQueues writeQueues = new(INTERVAL_SECONDS);
|
||||
|
||||
/// <summary>
|
||||
/// 收到数据
|
||||
/// </summary>
|
||||
/// <param name="flowType"></param>
|
||||
/// <param name="length"></param>
|
||||
public void OnFlow(FlowType flowType, int length)
|
||||
{
|
||||
if (flowType == FlowType.Read)
|
||||
{
|
||||
this.readQueues.OnFlow(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.writeQueues.OnFlow(length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取流量分析
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public FlowStatistics GetFlowStatistics()
|
||||
{
|
||||
return new FlowStatistics
|
||||
{
|
||||
TotalRead = this.readQueues.TotalBytes,
|
||||
TotalWrite = this.writeQueues.TotalBytes,
|
||||
ReadRate = this.readQueues.GetRate(),
|
||||
WriteRate = this.writeQueues.GetRate()
|
||||
};
|
||||
}
|
||||
|
||||
private class FlowQueues
|
||||
{
|
||||
private int cleaning = 0;
|
||||
private long totalBytes = 0L;
|
||||
private record QueueItem(long Ticks, int Length);
|
||||
private readonly ConcurrentQueue<QueueItem> queues = new();
|
||||
|
||||
private readonly int intervalSeconds;
|
||||
|
||||
public long TotalBytes => this.totalBytes;
|
||||
|
||||
public FlowQueues(int intervalSeconds)
|
||||
{
|
||||
this.intervalSeconds = intervalSeconds;
|
||||
}
|
||||
|
||||
public void OnFlow(int length)
|
||||
{
|
||||
Interlocked.Add(ref this.totalBytes, length);
|
||||
this.CleanInvalidRecords();
|
||||
this.queues.Enqueue(new QueueItem(Environment.TickCount64, length));
|
||||
}
|
||||
|
||||
public double GetRate()
|
||||
{
|
||||
this.CleanInvalidRecords();
|
||||
return (double)this.queues.Sum(item => item.Length) / this.intervalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除无效记录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool CleanInvalidRecords()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this.cleaning, 1, 0) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ticks = Environment.TickCount64;
|
||||
while (this.queues.TryPeek(out var item))
|
||||
{
|
||||
if (ticks - item.Ticks < this.intervalSeconds * 1000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.queues.TryDequeue(out _);
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref this.cleaning, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
FastGithub.FlowAnalyze/FlowStatistics.cs
Normal file
28
FastGithub.FlowAnalyze/FlowStatistics.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
/// <summary>
|
||||
/// 流量统计
|
||||
/// </summary>
|
||||
public record FlowStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取总读上行
|
||||
/// </summary>
|
||||
public long TotalRead { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取总下行
|
||||
/// </summary>
|
||||
public long TotalWrite { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取读取速率
|
||||
/// </summary>
|
||||
public double ReadRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取写入速率
|
||||
/// </summary>
|
||||
public double WriteRate { get; init; }
|
||||
}
|
||||
}
|
9
FastGithub.FlowAnalyze/FlowStatisticsContext.cs
Normal file
9
FastGithub.FlowAnalyze/FlowStatisticsContext.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
[JsonSerializable(typeof(FlowStatistics))]
|
||||
public partial class FlowStatisticsContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
18
FastGithub.FlowAnalyze/FlowType.cs
Normal file
18
FastGithub.FlowAnalyze/FlowType.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
/// <summary>
|
||||
/// 流量类型
|
||||
/// </summary>
|
||||
public enum FlowType
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取
|
||||
/// </summary>
|
||||
Read,
|
||||
|
||||
/// <summary>
|
||||
/// 写入
|
||||
/// </summary>
|
||||
Wirte
|
||||
}
|
||||
}
|
21
FastGithub.FlowAnalyze/IFlowAnalyzer.cs
Normal file
21
FastGithub.FlowAnalyze/IFlowAnalyzer.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
/// <summary>
|
||||
/// 流量分析器
|
||||
/// </summary>
|
||||
public interface IFlowAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// 收到数据
|
||||
/// </summary>
|
||||
/// <param name="flowType"></param>
|
||||
/// <param name="length"></param>
|
||||
void OnFlow(FlowType flowType, int length);
|
||||
|
||||
/// <summary>
|
||||
/// 获取速率
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
FlowStatistics GetFlowStatistics();
|
||||
}
|
||||
}
|
37
FastGithub.FlowAnalyze/ListenOptionsExtensions.cs
Normal file
37
FastGithub.FlowAnalyze/ListenOptionsExtensions.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using FastGithub.FlowAnalyze;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// ListenOptions扩展
|
||||
/// </summary>
|
||||
public static class ListenOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用流量分析中间件
|
||||
/// </summary>
|
||||
/// <param name="listen"></param>
|
||||
/// <returns></returns>
|
||||
public static ListenOptions UseFlowAnalyze(this ListenOptions listen)
|
||||
{
|
||||
var flowAnalyzer = listen.ApplicationServices.GetRequiredService<IFlowAnalyzer>();
|
||||
listen.Use(next => async context =>
|
||||
{
|
||||
var oldTransport = context.Transport;
|
||||
try
|
||||
{
|
||||
await using var loggingDuplexPipe = new FlowAnalyzeDuplexPipe(context.Transport, flowAnalyzer);
|
||||
context.Transport = loggingDuplexPipe;
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Transport = oldTransport;
|
||||
}
|
||||
});
|
||||
return listen;
|
||||
}
|
||||
}
|
||||
}
|
21
FastGithub.FlowAnalyze/ServiceCollectionExtensions.cs
Normal file
21
FastGithub.FlowAnalyze/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using FastGithub.FlowAnalyze;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// ServiceCollection扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加流量分析
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddFlowAnalyze(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton<IFlowAnalyzer, FlowAnalyzer>();
|
||||
}
|
||||
}
|
||||
}
|
106
FastGithub.FlowAnalyze/TaskToApm.cs
Normal file
106
FastGithub.FlowAnalyze/TaskToApm.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.FlowAnalyze
|
||||
{
|
||||
static class TaskToApm
|
||||
{
|
||||
/// <summary>
|
||||
/// Marshals the Task as an IAsyncResult, using the supplied callback and state
|
||||
/// to implement the APM pattern.
|
||||
/// </summary>
|
||||
/// <param name="task">The Task to be marshaled.</param>
|
||||
/// <param name="callback">The callback to be invoked upon completion.</param>
|
||||
/// <param name="state">The state to be stored in the IAsyncResult.</param>
|
||||
/// <returns>An IAsyncResult to represent the task's asynchronous operation.</returns>
|
||||
public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) =>
|
||||
new TaskAsyncResult(task, state, callback);
|
||||
|
||||
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
|
||||
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
|
||||
public static void End(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is TaskAsyncResult twar)
|
||||
{
|
||||
twar._task.GetAwaiter().GetResult();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
|
||||
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
|
||||
public static TResult End<TResult>(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is TaskAsyncResult twar && twar._task is Task<TResult> task)
|
||||
{
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
/// <summary>Provides a simple IAsyncResult that wraps a Task.</summary>
|
||||
/// <remarks>
|
||||
/// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state,
|
||||
/// but that's very rare, in particular in a situation where someone cares about allocation, and always
|
||||
/// using TaskAsyncResult simplifies things and enables additional optimizations.
|
||||
/// </remarks>
|
||||
internal sealed class TaskAsyncResult : IAsyncResult
|
||||
{
|
||||
/// <summary>The wrapped Task.</summary>
|
||||
internal readonly Task _task;
|
||||
/// <summary>Callback to invoke when the wrapped task completes.</summary>
|
||||
private readonly AsyncCallback? _callback;
|
||||
|
||||
/// <summary>Initializes the IAsyncResult with the Task to wrap and the associated object state.</summary>
|
||||
/// <param name="task">The Task to wrap.</param>
|
||||
/// <param name="state">The new AsyncState value.</param>
|
||||
/// <param name="callback">Callback to invoke when the wrapped task completes.</param>
|
||||
internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback)
|
||||
{
|
||||
Debug.Assert(task != null);
|
||||
_task = task;
|
||||
AsyncState = state;
|
||||
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
// Synchronous completion. Invoke the callback. No need to store it.
|
||||
CompletedSynchronously = true;
|
||||
callback?.Invoke(this);
|
||||
}
|
||||
else if (callback != null)
|
||||
{
|
||||
// Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in
|
||||
// order to avoid running synchronously if the task has already completed by the time we get here but still run
|
||||
// synchronously as part of the task's completion if the task completes after (the more common case).
|
||||
_callback = callback;
|
||||
_task.ConfigureAwait(continueOnCapturedContext: false)
|
||||
.GetAwaiter()
|
||||
.OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the callback.</summary>
|
||||
private void InvokeCallback()
|
||||
{
|
||||
Debug.Assert(!CompletedSynchronously);
|
||||
Debug.Assert(_callback != null);
|
||||
_callback.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>Gets a user-defined object that qualifies or contains information about an asynchronous operation.</summary>
|
||||
public object? AsyncState { get; }
|
||||
/// <summary>Gets a value that indicates whether the asynchronous operation completed synchronously.</summary>
|
||||
/// <remarks>This is set lazily based on whether the <see cref="_task"/> has completed by the time this object is created.</remarks>
|
||||
public bool CompletedSynchronously { get; }
|
||||
/// <summary>Gets a value that indicates whether the asynchronous operation has completed.</summary>
|
||||
public bool IsCompleted => _task.IsCompleted;
|
||||
/// <summary>Gets a <see cref="WaitHandle"/> that is used to wait for an asynchronous operation to complete.</summary>
|
||||
public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle;
|
||||
}
|
||||
}
|
||||
}
|
7
FastGithub.Http/FastGithub.Http.csproj
Normal file
7
FastGithub.Http/FastGithub.Http.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FastGithub.DomainResolve\FastGithub.DomainResolve.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
58
FastGithub.Http/HttpClient.cs
Normal file
58
FastGithub.Http/HttpClient.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using FastGithub.Configuration;
|
||||
using FastGithub.DomainResolve;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示http客户端
|
||||
/// </summary>
|
||||
public class HttpClient : HttpMessageInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// 插入的UserAgent标记
|
||||
/// </summary>
|
||||
private readonly static ProductInfoHeaderValue userAgent = new(new ProductHeaderValue(nameof(FastGithub), "1.0"));
|
||||
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClient(DomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
: this(new HttpClientHandler(domainConfig, domainResolver), disposeHandler: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
/// <param name="handler"></param>
|
||||
/// <param name="disposeHandler"></param>
|
||||
public HttpClient(HttpMessageHandler handler, bool disposeHandler)
|
||||
: base(handler, disposeHandler)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送请求
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Headers.UserAgent.Contains(userAgent))
|
||||
{
|
||||
throw new FastGithubException($"由于{request.RequestUri}实际指向了{nameof(FastGithub)}自身,{nameof(FastGithub)}已中断本次转发");
|
||||
}
|
||||
request.Headers.UserAgent.Add(userAgent);
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
response.Headers.Server.TryParseAdd(nameof(FastGithub));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
90
FastGithub.Http/HttpClientFactory.cs
Normal file
90
FastGithub.Http/HttpClientFactory.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using FastGithub.Configuration;
|
||||
using FastGithub.DomainResolve;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
sealed class HttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly IDomainResolver domainResolver;
|
||||
|
||||
/// <summary>
|
||||
/// 首次生命周期
|
||||
/// </summary>
|
||||
private readonly TimeSpan firstLiftTime = TimeSpan.FromSeconds(10d);
|
||||
|
||||
/// <summary>
|
||||
/// 非首次生命周期
|
||||
/// </summary>
|
||||
private readonly TimeSpan nextLifeTime = TimeSpan.FromSeconds(100d);
|
||||
|
||||
/// <summary>
|
||||
/// LifetimeHttpHandler清理器
|
||||
/// </summary>
|
||||
private readonly LifetimeHttpHandlerCleaner httpHandlerCleaner = new();
|
||||
|
||||
/// <summary>
|
||||
/// LazyOf(LifetimeHttpHandler)缓存
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<LifeTimeKey, Lazy<LifetimeHttpHandler>> httpHandlerLazyCache = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClientFactory(IDomainResolver domainResolver)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建httpClient
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
public HttpClient CreateHttpClient(string domain, DomainConfig domainConfig)
|
||||
{
|
||||
var lifeTimeKey = new LifeTimeKey(domain, domainConfig);
|
||||
var lifetimeHttpHandler = this.httpHandlerLazyCache.GetOrAdd(lifeTimeKey, CreateLifetimeHttpHandlerLazy).Value;
|
||||
return new HttpClient(lifetimeHttpHandler, disposeHandler: false);
|
||||
|
||||
Lazy<LifetimeHttpHandler> CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey)
|
||||
{
|
||||
return new Lazy<LifetimeHttpHandler>(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.firstLiftTime), true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建LifetimeHttpHandler
|
||||
/// </summary>
|
||||
/// <param name="lifeTimeKey"></param>
|
||||
/// <param name="lifeTime"></param>
|
||||
/// <returns></returns>
|
||||
private LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime)
|
||||
{
|
||||
return new LifetimeHttpHandler(this.domainResolver, lifeTimeKey, lifeTime, this.OnLifetimeHttpHandlerDeactivate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当有httpHandler失效时
|
||||
/// </summary>
|
||||
/// <param name="lifetimeHttpHandler">httpHandler</param>
|
||||
private void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler)
|
||||
{
|
||||
var lifeTimeKey = lifetimeHttpHandler.LifeTimeKey;
|
||||
this.httpHandlerLazyCache[lifeTimeKey] = CreateLifetimeHttpHandlerLazy(lifeTimeKey);
|
||||
this.httpHandlerCleaner.Add(lifetimeHttpHandler);
|
||||
|
||||
Lazy<LifetimeHttpHandler> CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey)
|
||||
{
|
||||
return new Lazy<LifetimeHttpHandler>(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.nextLifeTime), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
236
FastGithub.Http/HttpClientHandler.cs
Normal file
236
FastGithub.Http/HttpClientHandler.cs
Normal file
@ -0,0 +1,236 @@
|
||||
using FastGithub.Configuration;
|
||||
using FastGithub.DomainResolve;
|
||||
using System;
|
||||
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.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpClientHandler
|
||||
/// </summary>
|
||||
class HttpClientHandler : DelegatingHandler
|
||||
{
|
||||
private readonly DomainConfig domainConfig;
|
||||
private readonly IDomainResolver domainResolver;
|
||||
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
|
||||
|
||||
/// <summary>
|
||||
/// HttpClientHandler
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
{
|
||||
this.domainConfig = domainConfig;
|
||||
this.domainResolver = domainResolver;
|
||||
this.InnerHandler = this.CreateSocketsHttpHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送请求
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
if (uri == null)
|
||||
{
|
||||
throw new FastGithubException("必须指定请求的URI");
|
||||
}
|
||||
|
||||
// 请求上下文信息
|
||||
var isHttps = uri.Scheme == Uri.UriSchemeHttps;
|
||||
var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
|
||||
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
|
||||
|
||||
// 设置请求头host,修改协议为http
|
||||
request.Headers.Host = uri.Host;
|
||||
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
|
||||
|
||||
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);
|
||||
}
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建转发代理的httpHandler
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private SocketsHttpHandler CreateSocketsHttpHandler()
|
||||
{
|
||||
return new SocketsHttpHandler
|
||||
{
|
||||
Proxy = null,
|
||||
UseProxy = false,
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false,
|
||||
AutomaticDecompression = DecompressionMethods.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
|
||||
{
|
||||
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
|
||||
return await this.ConnectAsync(context, ipEndPoint, linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
innerExceptions.Add(new HttpConnectTimeoutException(ipEndPoint.Address));
|
||||
}
|
||||
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(ipEndPoint.AddressFamily, 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(dnsEndPoint.Host, out var address))
|
||||
{
|
||||
yield return new IPEndPoint(address, dnsEndPoint.Port);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.domainConfig.IPAddress != null)
|
||||
{
|
||||
yield return new IPEndPoint(this.domainConfig.IPAddress, dnsEndPoint.Port);
|
||||
}
|
||||
|
||||
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint, cancellationToken))
|
||||
{
|
||||
yield return new IPEndPoint(item, dnsEndPoint.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取使用的DNS名称
|
||||
/// </summary>
|
||||
/// <param name="cert"></param>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<string> ReadDnsNames(X509Certificate? cert)
|
||||
{
|
||||
if (cert is X509Certificate2 x509)
|
||||
{
|
||||
var extension = x509.Extensions.OfType<X509SubjectAlternativeNameExtension>().FirstOrDefault();
|
||||
if (extension != null)
|
||||
{
|
||||
return extension.EnumerateDnsNames();
|
||||
}
|
||||
}
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比较域名
|
||||
/// </summary>
|
||||
/// <param name="dnsName"></param>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
21
FastGithub.Http/HttpConnectTimeoutException.cs
Normal file
21
FastGithub.Http/HttpConnectTimeoutException.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// http连接超时异常
|
||||
/// </summary>
|
||||
sealed class HttpConnectTimeoutException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// http连接超时异常
|
||||
/// </summary>
|
||||
/// <param name="address">连接的ip</param>
|
||||
public HttpConnectTimeoutException(IPAddress address)
|
||||
: base(address.ToString())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
18
FastGithub.Http/IHttpClientFactory.cs
Normal file
18
FastGithub.Http/IHttpClientFactory.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using FastGithub.Configuration;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// httpClient工厂
|
||||
/// </summary>
|
||||
public interface IHttpClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建httpClient
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
HttpClient CreateHttpClient(string domain, DomainConfig domainConfig);
|
||||
}
|
||||
}
|
31
FastGithub.Http/LifeTimeKey.cs
Normal file
31
FastGithub.Http/LifeTimeKey.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using FastGithub.Configuration;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 生命周期的Key
|
||||
/// </summary>
|
||||
record LifeTimeKey
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名
|
||||
/// </summary>
|
||||
public string Domain { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 域名配置
|
||||
/// </summary>
|
||||
public DomainConfig DomainConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 生命周期的Key
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="domainConfig"></param>
|
||||
public LifeTimeKey(string domain, DomainConfig domainConfig)
|
||||
{
|
||||
this.Domain = domain;
|
||||
this.DomainConfig = domainConfig;
|
||||
}
|
||||
}
|
||||
}
|
49
FastGithub.Http/LifetimeHttpHandler.cs
Normal file
49
FastGithub.Http/LifetimeHttpHandler.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using FastGithub.DomainResolve;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示自主管理生命周期的的HttpMessageHandler
|
||||
/// </summary>
|
||||
sealed class LifetimeHttpHandler : DelegatingHandler
|
||||
{
|
||||
private readonly Timer timer;
|
||||
|
||||
public LifeTimeKey LifeTimeKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 具有生命周期的HttpHandler
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="lifeTimeKey"></param>
|
||||
/// <param name="lifeTime"></param>
|
||||
/// <param name="deactivateAction"></param>
|
||||
public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
|
||||
{
|
||||
this.LifeTimeKey = lifeTimeKey;
|
||||
this.InnerHandler = new HttpClientHandler(lifeTimeKey.DomainConfig, domainResolver);
|
||||
this.timer = new Timer(this.OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// timer触发时
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
private void OnTimerCallback(object? state)
|
||||
{
|
||||
this.timer.Dispose();
|
||||
((Action<LifetimeHttpHandler>)(state!))(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 这里不释放资源
|
||||
/// </summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
133
FastGithub.Http/LifetimeHttpHandlerCleaner.cs
Normal file
133
FastGithub.Http/LifetimeHttpHandlerCleaner.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示LifetimeHttpHandler清理器
|
||||
/// </summary>
|
||||
sealed class LifetimeHttpHandlerCleaner
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前监视生命周期的记录的数量
|
||||
/// </summary>
|
||||
private int trackingEntryCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 监视生命周期的记录队列
|
||||
/// </summary>
|
||||
private readonly ConcurrentQueue<TrackingEntry> trackingEntries = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置清理的时间间隔
|
||||
/// 默认10s
|
||||
/// </summary>
|
||||
public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromSeconds(10d);
|
||||
|
||||
/// <summary>
|
||||
/// 添加要清除的httpHandler
|
||||
/// </summary>
|
||||
/// <param name="handler">httpHandler</param>
|
||||
public void Add(LifetimeHttpHandler handler)
|
||||
{
|
||||
var entry = new TrackingEntry(handler);
|
||||
this.trackingEntries.Enqueue(entry);
|
||||
|
||||
// 从0变为1,要启动清理作业
|
||||
if (Interlocked.Increment(ref this.trackingEntryCount) == 1)
|
||||
{
|
||||
this.StartCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动清理作业
|
||||
/// </summary>
|
||||
private async void StartCleanup()
|
||||
{
|
||||
await Task.Yield();
|
||||
while (this.Cleanup() == false)
|
||||
{
|
||||
await Task.Delay(this.CleanupInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理失效的拦截器
|
||||
/// 返回是否完全清理
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool Cleanup()
|
||||
{
|
||||
var cleanCount = this.trackingEntries.Count;
|
||||
for (var i = 0; i < cleanCount; i++)
|
||||
{
|
||||
this.trackingEntries.TryDequeue(out var entry);
|
||||
Debug.Assert(entry != null);
|
||||
|
||||
if (entry.CanDispose == false)
|
||||
{
|
||||
this.trackingEntries.Enqueue(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Dispose();
|
||||
if (Interlocked.Decrement(ref this.trackingEntryCount) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 表示监视生命周期的记录
|
||||
/// </summary>
|
||||
private class TrackingEntry : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于释放资源的对象
|
||||
/// </summary>
|
||||
private readonly IDisposable disposable;
|
||||
|
||||
/// <summary>
|
||||
/// 监视对象的弱引用
|
||||
/// </summary>
|
||||
private readonly WeakReference weakReference;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否可以释放资源
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool CanDispose => this.weakReference.IsAlive == false;
|
||||
|
||||
/// <summary>
|
||||
/// 监视生命周期的记录
|
||||
/// </summary>
|
||||
/// <param name="handler">激活状态的httpHandler</param>
|
||||
public TrackingEntry(LifetimeHttpHandler handler)
|
||||
{
|
||||
this.disposable = handler.InnerHandler!;
|
||||
this.weakReference = new WeakReference(handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.disposable.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
FastGithub.Http/RequestContext.cs
Normal file
31
FastGithub.Http/RequestContext.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using FastGithub.Configuration;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示请求上下文
|
||||
/// </summary>
|
||||
sealed class RequestContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置是否为https请求
|
||||
/// </summary>
|
||||
public bool IsHttps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置Sni值
|
||||
/// </summary>
|
||||
public TlsSniPattern TlsSniValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求上下文
|
||||
/// </summary>
|
||||
/// <param name="isHttps"></param>
|
||||
/// <param name="tlsSniValue"></param>
|
||||
public RequestContext(bool isHttps, TlsSniPattern tlsSniValue)
|
||||
{
|
||||
IsHttps = isHttps;
|
||||
TlsSniValue = tlsSniValue;
|
||||
}
|
||||
}
|
||||
}
|
35
FastGithub.Http/RequestContextExtensions.cs
Normal file
35
FastGithub.Http/RequestContextExtensions.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求上下文扩展
|
||||
/// </summary>
|
||||
static class RequestContextExtensions
|
||||
{
|
||||
private static readonly HttpRequestOptionsKey<RequestContext> key = new(nameof(RequestContext));
|
||||
|
||||
/// <summary>
|
||||
/// 设置RequestContext
|
||||
/// </summary>
|
||||
/// <param name="httpRequestMessage"></param>
|
||||
/// <param name="requestContext"></param>
|
||||
public static void SetRequestContext(this HttpRequestMessage httpRequestMessage, RequestContext requestContext)
|
||||
{
|
||||
httpRequestMessage.Options.Set(key, requestContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取RequestContext
|
||||
/// </summary>
|
||||
/// <param name="httpRequestMessage"></param>
|
||||
/// <returns></returns>
|
||||
public static RequestContext GetRequestContext(this HttpRequestMessage httpRequestMessage)
|
||||
{
|
||||
return httpRequestMessage.Options.TryGetValue(key, out var requestContext)
|
||||
? requestContext
|
||||
: throw new InvalidOperationException($"请先调用{nameof(SetRequestContext)}");
|
||||
}
|
||||
}
|
||||
}
|
23
FastGithub.Http/ServiceCollectionExtensions.cs
Normal file
23
FastGithub.Http/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using FastGithub.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加HttpClient相关服务
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHttpClient(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IHttpClientFactory, HttpClientFactory>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
63
FastGithub.HttpServer/ApplicationBuilderExtensions.cs
Normal file
63
FastGithub.HttpServer/ApplicationBuilderExtensions.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using FastGithub.HttpServer.HttpMiddlewares;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// ApplicationBuilder扩展
|
||||
/// </summary>
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用http代理策略中间件
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseHttpProxyPac(this IApplicationBuilder app)
|
||||
{
|
||||
var middleware = app.ApplicationServices.GetRequiredService<HttpProxyPacMiddleware>();
|
||||
return app.Use(next => context => middleware.InvokeAsync(context, next));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用请求日志中间件
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
|
||||
{
|
||||
var middleware = app.ApplicationServices.GetRequiredService<RequestLoggingMiddleware>();
|
||||
return app.Use(next => context => middleware.InvokeAsync(context, next));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用请求日志中间件
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder DisableRequestLogging(this IApplicationBuilder app)
|
||||
{
|
||||
return app.Use(next => context =>
|
||||
{
|
||||
var loggingFeature = context.Features.Get<IRequestLoggingFeature>();
|
||||
if (loggingFeature != null)
|
||||
{
|
||||
loggingFeature.Enable = false;
|
||||
}
|
||||
return next(context);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用反向代理中间件
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseHttpReverseProxy(this IApplicationBuilder app)
|
||||
{
|
||||
var middleware = app.ApplicationServices.GetRequiredService<HttpReverseProxyMiddleware>();
|
||||
return app.Use(next => context => middleware.InvokeAsync(context, next));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
using FastGithub;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs.CaCertInstallers
|
||||
{
|
||||
abstract class CaCertInstallerOfLinux : ICaCertInstaller
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
/// <summary>
|
||||
/// 更新工具文件名
|
||||
/// </summary>
|
||||
protected abstract string CaCertUpdatePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书根目录
|
||||
/// </summary>
|
||||
protected abstract string CaCertStorePath { get; }
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern uint geteuid();
|
||||
|
||||
public CaCertInstallerOfLinux(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsSupported()
|
||||
{
|
||||
return OperatingSystem.IsLinux() && File.Exists(CaCertUpdatePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装ca证书
|
||||
/// </summary>
|
||||
/// <param name="caCertFilePath">证书文件路径</param>
|
||||
public void Install(string caCertFilePath)
|
||||
{
|
||||
var destCertFilePath = Path.Combine(CaCertStorePath, Path.GetFileName(caCertFilePath));
|
||||
if (File.Exists(destCertFilePath) && File.ReadAllBytes(caCertFilePath).SequenceEqual(File.ReadAllBytes(destCertFilePath)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (geteuid() != 0)
|
||||
{
|
||||
logger.LogWarning($"无法自动安装CA证书{caCertFilePath}:没有root权限");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(CaCertStorePath);
|
||||
foreach (var item in Directory.GetFiles(CaCertStorePath, "fastgithub.*"))
|
||||
{
|
||||
File.Delete(item);
|
||||
}
|
||||
File.Copy(caCertFilePath, destCertFilePath, overwrite: true);
|
||||
Process.Start(CaCertUpdatePath).WaitForExit();
|
||||
logger.LogInformation($"已自动向系统安装CA证书{caCertFilePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.Delete(destCertFilePath);
|
||||
logger.LogWarning(ex.Message, "自动安装CA证书异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs.CaCertInstallers
|
||||
{
|
||||
sealed class CaCertInstallerOfLinuxDebian : CaCertInstallerOfLinux
|
||||
{
|
||||
protected override string CaCertUpdatePath => "/usr/sbin/update-ca-certificates";
|
||||
|
||||
protected override string CaCertStorePath => "/usr/local/share/ca-certificates";
|
||||
|
||||
public CaCertInstallerOfLinuxDebian(ILogger<CaCertInstallerOfLinuxDebian> logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs.CaCertInstallers
|
||||
{
|
||||
sealed class CaCertInstallerOfLinuxRedHat : CaCertInstallerOfLinux
|
||||
{
|
||||
protected override string CaCertUpdatePath => "/usr/bin/update-ca-trust";
|
||||
|
||||
protected override string CaCertStorePath => "/etc/pki/ca-trust/source/anchors";
|
||||
|
||||
public CaCertInstallerOfLinuxRedHat(ILogger<CaCertInstallerOfLinuxRedHat> logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs.CaCertInstallers
|
||||
{
|
||||
sealed class CaCertInstallerOfMacOS : ICaCertInstaller
|
||||
{
|
||||
private readonly ILogger<CaCertInstallerOfMacOS> logger;
|
||||
|
||||
public CaCertInstallerOfMacOS(ILogger<CaCertInstallerOfMacOS> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsSupported()
|
||||
{
|
||||
return OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装ca证书
|
||||
/// </summary>
|
||||
/// <param name="caCertFilePath">证书文件路径</param>
|
||||
public void Install(string caCertFilePath)
|
||||
{
|
||||
logger.LogWarning($"请手动安装CA证书然后设置信任CA证书{caCertFilePath}");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs.CaCertInstallers
|
||||
{
|
||||
sealed class CaCertInstallerOfWindows : ICaCertInstaller
|
||||
{
|
||||
private readonly ILogger<CaCertInstallerOfWindows> logger;
|
||||
|
||||
public CaCertInstallerOfWindows(ILogger<CaCertInstallerOfWindows> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsSupported()
|
||||
{
|
||||
return OperatingSystem.IsWindows();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装ca证书
|
||||
/// </summary>
|
||||
/// <param name="caCertFilePath">证书文件路径</param>
|
||||
public void Install(string caCertFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
|
||||
var caCert = new X509Certificate2(caCertFilePath);
|
||||
var subjectName = caCert.Subject[3..];
|
||||
foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
|
||||
{
|
||||
if (item.Thumbprint != caCert.Thumbprint)
|
||||
{
|
||||
store.Remove(item);
|
||||
}
|
||||
}
|
||||
if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
|
||||
{
|
||||
store.Add(caCert);
|
||||
}
|
||||
store.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
logger.LogWarning($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
155
FastGithub.HttpServer/Certs/CertGenerator.cs
Normal file
155
FastGithub.HttpServer/Certs/CertGenerator.cs
Normal file
@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs
|
||||
{
|
||||
/// <summary>
|
||||
/// 证书生成器
|
||||
/// </summary>
|
||||
static class CertGenerator
|
||||
{
|
||||
private static readonly Oid tlsServerOid = new("1.3.6.1.5.5.7.3.1");
|
||||
private static readonly Oid tlsClientOid = new("1.3.6.1.5.5.7.3.2");
|
||||
|
||||
/// <summary>
|
||||
/// 生成ca证书
|
||||
/// </summary>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <param name="notBefore"></param>
|
||||
/// <param name="notAfter"></param>
|
||||
/// <param name="rsaKeySizeInBits"></param>
|
||||
/// <param name="pathLengthConstraint"></param>
|
||||
/// <returns></returns>
|
||||
public static X509Certificate2 CreateCACertificate(
|
||||
X500DistinguishedName subjectName,
|
||||
DateTimeOffset notBefore,
|
||||
DateTimeOffset notAfter,
|
||||
int rsaKeySizeInBits = 2048,
|
||||
int pathLengthConstraint = 1)
|
||||
{
|
||||
using var rsa = RSA.Create(rsaKeySizeInBits);
|
||||
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
var basicConstraints = new X509BasicConstraintsExtension(true, pathLengthConstraint > 0, pathLengthConstraint, true);
|
||||
request.CertificateExtensions.Add(basicConstraints);
|
||||
|
||||
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true);
|
||||
request.CertificateExtensions.Add(keyUsage);
|
||||
|
||||
var oids = new OidCollection { tlsServerOid, tlsClientOid };
|
||||
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
|
||||
request.CertificateExtensions.Add(enhancedKeyUsage);
|
||||
|
||||
var dnsBuilder = new SubjectAlternativeNameBuilder();
|
||||
dnsBuilder.Add(subjectName.Name[3..]);
|
||||
request.CertificateExtensions.Add(dnsBuilder.Build());
|
||||
|
||||
var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
|
||||
request.CertificateExtensions.Add(subjectKeyId);
|
||||
|
||||
return request.CreateSelfSigned(notBefore, notAfter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成服务器证书
|
||||
/// </summary>
|
||||
/// <param name="issuerCertificate"></param>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <param name="extraDnsNames"></param>
|
||||
/// <param name="notBefore"></param>
|
||||
/// <param name="notAfter"></param>
|
||||
/// <param name="rsaKeySizeInBits"></param>
|
||||
/// <returns></returns>
|
||||
public static X509Certificate2 CreateEndCertificate(
|
||||
X509Certificate2 issuerCertificate,
|
||||
X500DistinguishedName subjectName,
|
||||
IEnumerable<string>? extraDnsNames = default,
|
||||
DateTimeOffset? notBefore = default,
|
||||
DateTimeOffset? notAfter = default,
|
||||
int rsaKeySizeInBits = 2048)
|
||||
{
|
||||
using var rsa = RSA.Create(rsaKeySizeInBits);
|
||||
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
var basicConstraints = new X509BasicConstraintsExtension(false, false, 0, true);
|
||||
request.CertificateExtensions.Add(basicConstraints);
|
||||
|
||||
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true);
|
||||
request.CertificateExtensions.Add(keyUsage);
|
||||
|
||||
var oids = new OidCollection { tlsServerOid, tlsClientOid };
|
||||
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
|
||||
request.CertificateExtensions.Add(enhancedKeyUsage);
|
||||
|
||||
var authorityKeyId = GetAuthorityKeyIdentifierExtension(issuerCertificate);
|
||||
request.CertificateExtensions.Add(authorityKeyId);
|
||||
|
||||
var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
|
||||
request.CertificateExtensions.Add(subjectKeyId);
|
||||
|
||||
var dnsBuilder = new SubjectAlternativeNameBuilder();
|
||||
dnsBuilder.Add(subjectName.Name[3..]);
|
||||
|
||||
if (extraDnsNames != null)
|
||||
{
|
||||
foreach (var dnsName in extraDnsNames)
|
||||
{
|
||||
dnsBuilder.Add(dnsName);
|
||||
}
|
||||
}
|
||||
|
||||
var dnsNames = dnsBuilder.Build();
|
||||
request.CertificateExtensions.Add(dnsNames);
|
||||
|
||||
if (notBefore == null || notBefore.Value < issuerCertificate.NotBefore)
|
||||
{
|
||||
notBefore = issuerCertificate.NotBefore;
|
||||
}
|
||||
|
||||
if (notAfter == null || notAfter.Value > issuerCertificate.NotAfter)
|
||||
{
|
||||
notAfter = issuerCertificate.NotAfter;
|
||||
}
|
||||
|
||||
var serialNumber = BitConverter.GetBytes(Random.Shared.NextInt64());
|
||||
using var certOnly = request.Create(issuerCertificate, notBefore.Value, notAfter.Value, serialNumber);
|
||||
return certOnly.CopyWithPrivateKey(rsa);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void Add(this SubjectAlternativeNameBuilder builder, string name)
|
||||
{
|
||||
if (IPAddress.TryParse(name, out var address))
|
||||
{
|
||||
builder.AddIpAddress(address);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddDnsName(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static X509Extension GetAuthorityKeyIdentifierExtension(X509Certificate2 certificate)
|
||||
{
|
||||
var extension = new X509SubjectKeyIdentifierExtension(certificate.PublicKey, false);
|
||||
#if NET7_0_OR_GREATER
|
||||
return X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(extension);
|
||||
#else
|
||||
var subjectKeyIdentifier = extension.RawData.AsSpan(2);
|
||||
var rawData = new byte[subjectKeyIdentifier.Length + 4];
|
||||
rawData[0] = 0x30;
|
||||
rawData[1] = 0x16;
|
||||
rawData[2] = 0x80;
|
||||
rawData[3] = 0x14;
|
||||
subjectKeyIdentifier.CopyTo(rawData);
|
||||
|
||||
return new X509Extension("2.5.29.35", rawData, false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
172
FastGithub.HttpServer/Certs/CertService.cs
Normal file
172
FastGithub.HttpServer/Certs/CertService.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
namespace FastGithub.HttpServer.Certs
|
||||
{
|
||||
/// <summary>
|
||||
/// 证书服务
|
||||
/// </summary>
|
||||
sealed class CertService
|
||||
{
|
||||
private const string CACERT_PATH = "cacert";
|
||||
private readonly IMemoryCache serverCertCache;
|
||||
private readonly IEnumerable<ICaCertInstaller> certInstallers;
|
||||
private readonly ILogger<CertService> logger;
|
||||
private X509Certificate2? caCert;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取证书文件路径
|
||||
/// </summary>
|
||||
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/fastgithub.crt" : $"{CACERT_PATH}/fastgithub.cer";
|
||||
|
||||
/// <summary>
|
||||
/// 获取私钥文件路径
|
||||
/// </summary>
|
||||
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/fastgithub.key";
|
||||
|
||||
/// <summary>
|
||||
/// 证书服务
|
||||
/// </summary>
|
||||
/// <param name="serverCertCache"></param>
|
||||
/// <param name="certInstallers"></param>
|
||||
/// <param name="logger"></param>
|
||||
public CertService(
|
||||
IMemoryCache serverCertCache,
|
||||
IEnumerable<ICaCertInstaller> certInstallers,
|
||||
ILogger<CertService> logger)
|
||||
{
|
||||
this.serverCertCache = serverCertCache;
|
||||
this.certInstallers = certInstallers;
|
||||
this.logger = logger;
|
||||
Directory.CreateDirectory(CACERT_PATH);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成CA证书
|
||||
/// </summary>
|
||||
public bool CreateCaCertIfNotExists()
|
||||
{
|
||||
if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
File.Delete(this.CaCerFilePath);
|
||||
File.Delete(this.CaKeyFilePath);
|
||||
|
||||
var notBefore = DateTimeOffset.Now.AddDays(-1);
|
||||
var notAfter = DateTimeOffset.Now.AddYears(10);
|
||||
|
||||
var subjectName = new X500DistinguishedName($"CN={nameof(FastGithub)}");
|
||||
this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);
|
||||
|
||||
var privateKeyPem = this.caCert.GetRSAPrivateKey()?.ExportRSAPrivateKeyPem();
|
||||
File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);
|
||||
|
||||
var certPem = this.caCert.ExportCertificatePem();
|
||||
File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装和信任CA证书
|
||||
/// </summary>
|
||||
public void InstallAndTrustCaCert()
|
||||
{
|
||||
var installer = this.certInstallers.FirstOrDefault(item => item.IsSupported());
|
||||
if (installer != null)
|
||||
{
|
||||
installer.Install(this.CaCerFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{this.CaCerFilePath}");
|
||||
}
|
||||
|
||||
GitConfigSslverify(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置ssl验证
|
||||
/// </summary>
|
||||
/// <param name="value">是否验证</param>
|
||||
/// <returns></returns>
|
||||
public static bool GitConfigSslverify(bool value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = $"config --global http.sslverify {value.ToString().ToLower()}",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取颁发给指定域名的证书
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
public X509Certificate2 GetOrCreateServerCert(string? domain)
|
||||
{
|
||||
if (this.caCert == null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
|
||||
this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
|
||||
}
|
||||
|
||||
var key = $"{nameof(CertService)}:{domain}";
|
||||
var endCert = this.serverCertCache.GetOrCreate(key, GetOrCreateCert);
|
||||
return endCert!;
|
||||
|
||||
// 生成域名的1年证书
|
||||
X509Certificate2 GetOrCreateCert(ICacheEntry entry)
|
||||
{
|
||||
var notBefore = DateTimeOffset.Now.AddDays(-1);
|
||||
var notAfter = DateTimeOffset.Now.AddYears(1);
|
||||
entry.SetAbsoluteExpiration(notAfter);
|
||||
|
||||
var extraDomains = GetExtraDomains();
|
||||
|
||||
var subjectName = new X500DistinguishedName($"CN={domain}");
|
||||
var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);
|
||||
|
||||
// 重新初始化证书,以兼容win平台不能使用内存证书
|
||||
return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<string> GetExtraDomains()
|
||||
{
|
||||
yield return Environment.MachineName;
|
||||
yield return IPAddress.Loopback.ToString();
|
||||
yield return IPAddress.IPv6Loopback.ToString();
|
||||
}
|
||||
}
|
||||
}
|
20
FastGithub.HttpServer/Certs/ICaCertInstaller.cs
Normal file
20
FastGithub.HttpServer/Certs/ICaCertInstaller.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace FastGithub.HttpServer.Certs
|
||||
{
|
||||
/// <summary>
|
||||
/// CA证书安装器
|
||||
/// </summary>
|
||||
interface ICaCertInstaller
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否支持
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool IsSupported();
|
||||
|
||||
/// <summary>
|
||||
/// 安装ca证书
|
||||
/// </summary>
|
||||
/// <param name="caCertFilePath">证书文件路径</param>
|
||||
void Install(string caCertFilePath);
|
||||
}
|
||||
}
|
17
FastGithub.HttpServer/FastGithub.HttpServer.csproj
Normal file
17
FastGithub.HttpServer/FastGithub.HttpServer.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FastGithub.FlowAnalyze\FastGithub.FlowAnalyze.csproj" />
|
||||
<ProjectReference Include="..\FastGithub.Http\FastGithub.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
using FastGithub.Configuration;
|
||||
using FastGithub.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Yarp.ReverseProxy.Forwarder;
|
||||
|
||||
namespace FastGithub.HttpServer.HttpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// 反向代理中间件
|
||||
/// </summary>
|
||||
sealed class HttpReverseProxyMiddleware
|
||||
{
|
||||
private static readonly DomainConfig defaultDomainConfig = new() { TlsSni = true };
|
||||
|
||||
private readonly IHttpForwarder httpForwarder;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<HttpReverseProxyMiddleware> logger;
|
||||
|
||||
public HttpReverseProxyMiddleware(
|
||||
IHttpForwarder httpForwarder,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<HttpReverseProxyMiddleware> logger)
|
||||
{
|
||||
this.httpForwarder = httpForwarder;
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.fastGithubConfig = fastGithubConfig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理请求
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="next"?
|
||||
/// <returns></returns>
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
var host = context.Request.Host;
|
||||
if (TryGetDomainConfig(host, out var domainConfig) == false)
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
else if (domainConfig.Response == null)
|
||||
{
|
||||
var scheme = context.Request.Scheme;
|
||||
var destinationPrefix = GetDestinationPrefix(scheme, host, domainConfig.Destination);
|
||||
var httpClient = httpClientFactory.CreateHttpClient(host.Host, domainConfig);
|
||||
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Empty);
|
||||
await HandleErrorAsync(context, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = domainConfig.Response.StatusCode;
|
||||
context.Response.ContentType = domainConfig.Response.ContentType;
|
||||
if (domainConfig.Response.ContentValue != null)
|
||||
{
|
||||
await context.Response.WriteAsync(domainConfig.Response.ContentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取域名的DomainConfig
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
private bool TryGetDomainConfig(HostString host, [MaybeNullWhen(false)] out DomainConfig domainConfig)
|
||||
{
|
||||
if (fastGithubConfig.TryGetDomainConfig(host.Host, out domainConfig) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未配置的域名,但仍然被解析到本机ip的域名
|
||||
if (OperatingSystem.IsWindows() && IsDomain(host.Host))
|
||||
{
|
||||
logger.LogWarning($"域名{host.Host}可能已经被DNS污染,如果域名为本机域名,请解析为非回环IP");
|
||||
domainConfig = defaultDomainConfig;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
// 是否为域名
|
||||
static bool IsDomain(string host)
|
||||
{
|
||||
return IPAddress.TryParse(host, out _) == false && host.Contains('.');
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标前缀
|
||||
/// </summary>
|
||||
/// <param name="scheme"></param>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="destination"></param>
|
||||
/// <returns></returns>
|
||||
private string GetDestinationPrefix(string scheme, HostString host, Uri? destination)
|
||||
{
|
||||
var defaultValue = $"{scheme}://{host}/";
|
||||
if (destination == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var baseUri = new Uri(defaultValue);
|
||||
var result = new Uri(baseUri, destination).ToString();
|
||||
logger.LogInformation($"{defaultValue} => {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理错误信息
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task HandleErrorAsync(HttpContext context, ForwarderError error)
|
||||
{
|
||||
if (error == ForwarderError.None || context.Response.HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
error = error.ToString(),
|
||||
message = context.GetForwarderErrorFeature()?.Exception?.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
namespace FastGithub.HttpServer.HttpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求日志特性
|
||||
/// </summary>
|
||||
public interface IRequestLoggingFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
bool Enable { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.HttpServer.HttpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求日志中间件
|
||||
/// </summary>
|
||||
sealed class RequestLoggingMiddleware
|
||||
{
|
||||
private readonly ILogger<RequestLoggingMiddleware> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 请求日志中间件
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public RequestLoggingMiddleware(ILogger<RequestLoggingMiddleware> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行请求
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="next"></param>
|
||||
/// <returns></returns>
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
var feature = new RequestLoggingFeature();
|
||||
context.Features.Set<IRequestLoggingFeature>(feature);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
}
|
||||
|
||||
if (feature.Enable == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var exception = context.GetForwarderErrorFeature()?.Exception;
|
||||
if (exception == null)
|
||||
{
|
||||
logger.LogInformation($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
}
|
||||
else if (IsError(exception))
|
||||
{
|
||||
logger.LogError($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{exception}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{GetMessage(exception)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为错误
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private static bool IsError(Exception exception)
|
||||
{
|
||||
if (exception is OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasInnerException<ConnectionAbortedException>(exception))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetMessage(Exception exception)
|
||||
{
|
||||
var ex = exception;
|
||||
var builder = new StringBuilder();
|
||||
|
||||
while (ex != null)
|
||||
{
|
||||
var type = ex.GetType();
|
||||
builder.Append(type.Namespace).Append('.').Append(type.Name).Append(": ").AppendLine(ex.Message);
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private class RequestLoggingFeature : IRequestLoggingFeature
|
||||
{
|
||||
public bool Enable { get; set; } = true;
|
||||
}
|
||||
}
|
||||
}
|
185
FastGithub.HttpServer/KestrelServerExtensions.cs
Normal file
185
FastGithub.HttpServer/KestrelServerExtensions.cs
Normal file
@ -0,0 +1,185 @@
|
||||
using FastGithub.Configuration;
|
||||
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;
|
||||
using System;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// Kestrel扩展
|
||||
/// </summary>
|
||||
public static class KestrelServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 无限制
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
public static void NoLimit(this KestrelServerOptions kestrel)
|
||||
{
|
||||
kestrel.Limits.MaxRequestBodySize = null;
|
||||
kestrel.Limits.MinResponseDataRate = null;
|
||||
kestrel.Limits.MinRequestBodyDataRate = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听http代理
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
public static void ListenHttpProxy(this KestrelServerOptions kestrel)
|
||||
{
|
||||
var options = kestrel.ApplicationServices.GetRequiredService<IOptions<FastGithubOptions>>().Value;
|
||||
var httpProxyPort = options.HttpProxyPort;
|
||||
|
||||
if (GlobalListener.CanListenTcp(httpProxyPort) == false)
|
||||
{
|
||||
throw new FastGithubException($"tcp端口{httpProxyPort}已经被其它进程占用,请在配置文件更换{nameof(FastGithubOptions.HttpProxyPort)}为其它端口");
|
||||
}
|
||||
|
||||
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协议代理
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
public static void ListenSshReverseProxy(this KestrelServerOptions kestrel)
|
||||
{
|
||||
var sshPort = GlobalListener.SshPort;
|
||||
kestrel.ListenLocalhost(sshPort, listen =>
|
||||
{
|
||||
listen.UseFlowAnalyze();
|
||||
listen.UseConnectionHandler<GithubSshReverseProxyHandler>();
|
||||
});
|
||||
|
||||
kestrel.GetLogger().LogInformation($"已监听ssh://localhost:{sshPort},github的ssh反向代理服务启动完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听git协议代理代理
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
public static void ListenGitReverseProxy(this KestrelServerOptions kestrel)
|
||||
{
|
||||
var gitPort = GlobalListener.GitPort;
|
||||
kestrel.ListenLocalhost(gitPort, listen =>
|
||||
{
|
||||
listen.UseFlowAnalyze();
|
||||
listen.UseConnectionHandler<GithubGitReverseProxyHandler>();
|
||||
});
|
||||
|
||||
kestrel.GetLogger().LogInformation($"已监听git://localhost:{gitPort},github的git反向代理服务启动完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听http反向代理
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
public static void ListenHttpReverseProxy(this KestrelServerOptions kestrel)
|
||||
{
|
||||
var httpPort = GlobalListener.HttpPort;
|
||||
kestrel.ListenLocalhost(httpPort);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
kestrel.GetLogger().LogInformation($"已监听http://localhost:{httpPort},http反向代理服务启动完成");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听https反向代理
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
/// <exception cref="FastGithubException"></exception>
|
||||
public static void ListenHttpsReverseProxy(this KestrelServerOptions kestrel)
|
||||
{
|
||||
var httpsPort = GlobalListener.HttpsPort;
|
||||
kestrel.ListenLocalhost(httpsPort, listen =>
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
listen.UseFlowAnalyze();
|
||||
}
|
||||
listen.UseTls();
|
||||
});
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var logger = kestrel.GetLogger();
|
||||
logger.LogInformation($"已监听https://localhost:{httpsPort},https反向代理服务启动完成");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取日志
|
||||
/// </summary>
|
||||
/// <param name="kestrel"></param>
|
||||
/// <returns></returns>
|
||||
private static ILogger GetLogger(this KestrelServerOptions kestrel)
|
||||
{
|
||||
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(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, Func<string, X509Certificate2> certFactory)
|
||||
{
|
||||
var invadeMiddleware = listen.ApplicationServices.GetRequiredService<TlsInvadeMiddleware>();
|
||||
var restoreMiddleware = listen.ApplicationServices.GetRequiredService<TlsRestoreMiddleware>();
|
||||
|
||||
listen.Use(next => context => invadeMiddleware.InvokeAsync(next, context));
|
||||
listen.UseHttps(new TlsHandshakeCallbackOptions
|
||||
{
|
||||
OnConnection = context =>
|
||||
{
|
||||
var options = new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = certFactory(context.ClientHelloInfo.ServerName)
|
||||
};
|
||||
return ValueTask.FromResult(options);
|
||||
},
|
||||
});
|
||||
listen.Use(next => context => restoreMiddleware.InvokeAsync(next, context));
|
||||
return listen;
|
||||
}
|
||||
}
|
||||
}
|
44
FastGithub.HttpServer/ServiceCollectionExtensions.cs
Normal file
44
FastGithub.HttpServer/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,44 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// http反向代理的服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加http反向代理
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddReverseProxy(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddMemoryCache()
|
||||
.AddHttpForwarder()
|
||||
.AddSingleton<CertService>()
|
||||
.AddSingleton<ICaCertInstaller, CaCertInstallerOfMacOS>()
|
||||
.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>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using FastGithub.DomainResolve;
|
||||
|
||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// github的git代理处理者
|
||||
/// </summary>
|
||||
sealed class GithubGitReverseProxyHandler : TcpReverseProxyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// github的git代理处理者
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
public GithubGitReverseProxyHandler(IDomainResolver domainResolver)
|
||||
: base(domainResolver, new("github.com", 9418))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using FastGithub.DomainResolve;
|
||||
|
||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// github的ssh代理处理者
|
||||
/// </summary>
|
||||
sealed class GithubSshReverseProxyHandler : TcpReverseProxyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// github的ssh代理处理者
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
public GithubSshReverseProxyHandler(IDomainResolver domainResolver)
|
||||
: base(domainResolver, new("github.com", 22))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
135
FastGithub.HttpServer/TcpMiddlewares/HttpProxyMiddleware.cs
Normal file
135
FastGithub.HttpServer/TcpMiddlewares/HttpProxyMiddleware.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
FastGithub.HttpServer/TcpMiddlewares/IHttpProxyFeature.cs
Normal file
11
FastGithub.HttpServer/TcpMiddlewares/IHttpProxyFeature.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
||||
{
|
||||
interface IHttpProxyFeature
|
||||
{
|
||||
HostString ProxyHost { get; }
|
||||
|
||||
ProxyProtocol ProxyProtocol { get; }
|
||||
}
|
||||
}
|
23
FastGithub.HttpServer/TcpMiddlewares/ProxyProtocol.cs
Normal file
23
FastGithub.HttpServer/TcpMiddlewares/ProxyProtocol.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// 代理协议
|
||||
/// </summary>
|
||||
enum ProxyProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// 无代理
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// http代理
|
||||
/// </summary>
|
||||
HttpProxy,
|
||||
|
||||
/// <summary>
|
||||
/// 隧道代理
|
||||
/// </summary>
|
||||
TunnelProxy
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
using FastGithub.DomainResolve;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
||||
{
|
||||
/// <summary>
|
||||
/// tcp协议代理处理者
|
||||
/// </summary>
|
||||
abstract class TcpReverseProxyHandler : ConnectionHandler
|
||||
{
|
||||
private readonly IDomainResolver domainResolver;
|
||||
private readonly DnsEndPoint endPoint;
|
||||
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
|
||||
|
||||
/// <summary>
|
||||
/// tcp协议代理处理者
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="endPoint"></param>
|
||||
public TcpReverseProxyHandler(IDomainResolver domainResolver, DnsEndPoint endPoint)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
this.endPoint = endPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tcp连接后
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task OnConnectedAsync(ConnectionContext context)
|
||||
{
|
||||
var cancellationToken = context.ConnectionClosed;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建连接
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="AggregateException"></exception>
|
||||
private async Task<Stream> CreateConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var innerExceptions = new List<Exception>();
|
||||
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(connectTimeout);
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
|
||||
await socket.ConnectAsync(address, endPoint.Port, linkedTokenSource.Token);
|
||||
return new NetworkStream(socket, ownsSocket: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
socket.Dispose();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
innerExceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
throw new AggregateException($"无法连接到{endPoint.Host}:{endPoint.Port}", innerExceptions);
|
||||
}
|
||||
}
|
||||
}
|
132
FastGithub.HttpServer/TcpMiddlewares/TunnelMiddleware.cs
Normal file
132
FastGithub.HttpServer/TcpMiddlewares/TunnelMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
64
FastGithub.HttpServer/TlsMiddlewares/TlsInvadeMiddleware.cs
Normal file
64
FastGithub.HttpServer/TlsMiddlewares/TlsInvadeMiddleware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
FastGithub.HttpServer/TlsMiddlewares/TlsRestoreMiddleware.cs
Normal file
27
FastGithub.HttpServer/TlsMiddlewares/TlsRestoreMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
157
FastGithub.PacketIntercept/Dns/DnsInterceptor.cs
Normal file
157
FastGithub.PacketIntercept/Dns/DnsInterceptor.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using DNS.Protocol;
|
||||
using DNS.Protocol.ResourceRecords;
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WindivertDotnet;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Dns
|
||||
{
|
||||
/// <summary>
|
||||
/// dns拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class DnsInterceptor : IDnsInterceptor
|
||||
{
|
||||
private static readonly Filter filter = Filter.True.And(f => f.Udp.DstPort == 53);
|
||||
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<DnsInterceptor> logger;
|
||||
|
||||
private readonly TimeSpan ttl = TimeSpan.FromMinutes(5d);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新DNS缓存
|
||||
/// </summary>
|
||||
[DllImport("dnsapi.dll", EntryPoint = "DnsFlushResolverCache", SetLastError = true)]
|
||||
private static extern void DnsFlushResolverCache();
|
||||
|
||||
/// <summary>
|
||||
/// dns拦截器
|
||||
/// </summary>
|
||||
/// <param name="fastGithubConfig"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="options"></param>
|
||||
public DnsInterceptor(
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<DnsInterceptor> logger,
|
||||
IOptionsMonitor<FastGithubOptions> options)
|
||||
{
|
||||
this.fastGithubConfig = fastGithubConfig;
|
||||
this.logger = logger;
|
||||
|
||||
options.OnChange(_ => DnsFlushResolverCache());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DNS拦截
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="Win32Exception"></exception>
|
||||
/// <returns></returns>
|
||||
public async Task InterceptAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var divert = new WinDivert(filter, WinDivertLayer.Network);
|
||||
using var packet = new WinDivertPacket();
|
||||
using var addr = new WinDivertAddress();
|
||||
|
||||
DnsFlushResolverCache();
|
||||
cancellationToken.Register(DnsFlushResolverCache);
|
||||
|
||||
while (cancellationToken.IsCancellationRequested == false)
|
||||
{
|
||||
await divert.RecvAsync(packet, addr, cancellationToken);
|
||||
try
|
||||
{
|
||||
this.ModifyDnsPacket(packet, addr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await divert.SendAsync(packet, addr, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改DNS数据包
|
||||
/// </summary>
|
||||
/// <param name="packet"></param>
|
||||
/// <param name="addr"></param>
|
||||
unsafe private void ModifyDnsPacket(WinDivertPacket packet, WinDivertAddress addr)
|
||||
{
|
||||
var result = packet.GetParseResult();
|
||||
var requestPayload = result.DataSpan.ToArray();
|
||||
|
||||
if (TryParseRequest(requestPayload, out var request) == false ||
|
||||
request.OperationCode != OperationCode.Query ||
|
||||
request.Questions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var question = request.Questions.First();
|
||||
if (question.Type != RecordType.A && question.Type != RecordType.AAAA)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var domain = question.Name;
|
||||
if (this.fastGithubConfig.IsMatch(question.Name.ToString()) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// dns响应数据
|
||||
var response = Response.FromRequest(request);
|
||||
var loopback = question.Type == RecordType.A ? IPAddress.Loopback : IPAddress.IPv6Loopback;
|
||||
var record = new IPAddressResourceRecord(domain, loopback, this.ttl);
|
||||
response.AnswerRecords.Add(record);
|
||||
|
||||
// 修改payload
|
||||
var writer = packet.GetWriter(packet.Length - result.DataLength);
|
||||
writer.Write(response.ToArray());
|
||||
|
||||
packet.ReverseEndPoint();
|
||||
packet.ApplyLengthToHeaders();
|
||||
packet.CalcChecksums(addr);
|
||||
packet.CalcOutboundFlag(addr);
|
||||
|
||||
addr.Flags |= WinDivertAddressFlag.Impostor;
|
||||
this.logger.LogInformation($"{domain}->{loopback}");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 尝试解析请求
|
||||
/// </summary>
|
||||
/// <param name="payload"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
static bool TryParseRequest(byte[] payload, [MaybeNullWhen(false)] out Request request)
|
||||
{
|
||||
try
|
||||
{
|
||||
request = Request.FromArray(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
request = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
115
FastGithub.PacketIntercept/Dns/HostsConflictSolver.cs
Normal file
115
FastGithub.PacketIntercept/Dns/HostsConflictSolver.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Dns
|
||||
{
|
||||
/// <summary>
|
||||
/// host文件冲解决者
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class HostsConflictSolver : IDnsConflictSolver
|
||||
{
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<HostsConflictSolver> logger;
|
||||
|
||||
/// <summary>
|
||||
/// host文件冲解决者
|
||||
/// </summary>
|
||||
/// <param name="fastGithubConfig"></param>
|
||||
/// <param name="logger"></param>
|
||||
public HostsConflictSolver(
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<HostsConflictSolver> logger)
|
||||
{
|
||||
this.fastGithubConfig = fastGithubConfig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解决冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task SolveAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var hostsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts");
|
||||
if (File.Exists(hostsPath) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Encoding hostsEncoding;
|
||||
var hasConflicting = false;
|
||||
var hostsBuilder = new StringBuilder();
|
||||
using (var fileStream = new FileStream(hostsPath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
while (streamReader.EndOfStream == false)
|
||||
{
|
||||
var line = await streamReader.ReadLineAsync(cancellationToken);
|
||||
if (this.IsConflictingLine(line))
|
||||
{
|
||||
hasConflicting = true;
|
||||
hostsBuilder.AppendLine($"# {line}");
|
||||
}
|
||||
else
|
||||
{
|
||||
hostsBuilder.AppendLine(line);
|
||||
}
|
||||
}
|
||||
hostsEncoding = streamReader.CurrentEncoding;
|
||||
}
|
||||
|
||||
|
||||
if (hasConflicting == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(hostsPath, hostsBuilder.ToString(), hostsEncoding, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning($"无法解决hosts文件冲突:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为冲突的行
|
||||
/// </summary>
|
||||
/// <param name="line"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsConflictingLine(string? line)
|
||||
{
|
||||
if (line == null || line.TrimStart().StartsWith("#"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var items = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (items.Length < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var domain = items[1];
|
||||
return this.fastGithubConfig.IsMatch(domain);
|
||||
}
|
||||
}
|
||||
}
|
161
FastGithub.PacketIntercept/Dns/ProxyConflictSolver.cs
Normal file
161
FastGithub.PacketIntercept/Dns/ProxyConflictSolver.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Dns
|
||||
{
|
||||
/// <summary>
|
||||
/// 代理冲突解决者
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class ProxyConflictSolver : IDnsConflictSolver
|
||||
{
|
||||
private const int INTERNET_OPTION_REFRESH = 37;
|
||||
private const int INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95;
|
||||
|
||||
private const char PROXYOVERRIDE_SEPARATOR = ';';
|
||||
private const string PROXYOVERRIDE_KEY = "ProxyOverride";
|
||||
private const string INTERNET_SETTINGS = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
|
||||
|
||||
private readonly IOptions<FastGithubOptions> options;
|
||||
private readonly ILogger<ProxyConflictSolver> logger;
|
||||
|
||||
[DllImport("wininet.dll")]
|
||||
private static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 代理冲突解决者
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="logger"></param>
|
||||
public ProxyConflictSolver(
|
||||
IOptions<FastGithubOptions> options,
|
||||
ILogger<ProxyConflictSolver> logger)
|
||||
{
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解决冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task SolveAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.SetToProxyOvride();
|
||||
this.CheckProxyConflict();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.RemoveFromProxyOvride();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加到ProxyOvride
|
||||
/// </summary>
|
||||
private void SetToProxyOvride()
|
||||
{
|
||||
using var settings = Registry.CurrentUser.OpenSubKey(INTERNET_SETTINGS, writable: true);
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var items = this.options.Value.DomainConfigs.Keys.ToHashSet();
|
||||
foreach (var item in GetProxyOvride(settings))
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
SetProxyOvride(settings, items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从ProxyOvride移除
|
||||
/// </summary>
|
||||
private void RemoveFromProxyOvride()
|
||||
{
|
||||
using var settings = Registry.CurrentUser.OpenSubKey(INTERNET_SETTINGS, writable: true);
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var proxyOvride = GetProxyOvride(settings);
|
||||
var items = proxyOvride.Except(this.options.Value.DomainConfigs.Keys);
|
||||
SetProxyOvride(settings, items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测代理冲突
|
||||
/// </summary>
|
||||
private void CheckProxyConflict()
|
||||
{
|
||||
var systemProxy = HttpClient.DefaultProxy;
|
||||
if (systemProxy == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var domain in this.options.Value.DomainConfigs.Keys)
|
||||
{
|
||||
var destination = new Uri($"https://{domain.Replace('*', 'a')}");
|
||||
var proxyServer = systemProxy.GetProxy(destination);
|
||||
if (proxyServer != null)
|
||||
{
|
||||
this.logger.LogError($"由于系统设置了代理{proxyServer},{nameof(FastGithub)}无法加速{domain}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取ProxyOverride
|
||||
/// </summary>
|
||||
/// <param name="registryKey"></param>
|
||||
/// <returns></returns>
|
||||
private static string[] GetProxyOvride(RegistryKey registryKey)
|
||||
{
|
||||
var value = registryKey.GetValue(PROXYOVERRIDE_KEY, null)?.ToString();
|
||||
if (value == null)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return value
|
||||
.Split(PROXYOVERRIDE_SEPARATOR, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(item => item.Trim())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置ProxyOverride
|
||||
/// </summary>
|
||||
/// <param name="registryKey"></param>
|
||||
/// <param name="items"></param>
|
||||
private static void SetProxyOvride(RegistryKey registryKey, IEnumerable<string> items)
|
||||
{
|
||||
var value = string.Join(PROXYOVERRIDE_SEPARATOR, items);
|
||||
registryKey.SetValue(PROXYOVERRIDE_KEY, value, RegistryValueKind.String);
|
||||
InternetSetOption(IntPtr.Zero, INTERNET_OPTION_PROXY_SETTINGS_CHANGED, IntPtr.Zero, 0);
|
||||
InternetSetOption(IntPtr.Zero, INTERNET_OPTION_REFRESH, IntPtr.Zero, 0);
|
||||
}
|
||||
}
|
||||
}
|
94
FastGithub.PacketIntercept/DnsInterceptHostedService.cs
Normal file
94
FastGithub.PacketIntercept/DnsInterceptHostedService.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept
|
||||
{
|
||||
/// <summary>
|
||||
/// dns拦截后台服务
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class DnsInterceptHostedService : BackgroundService
|
||||
{
|
||||
private readonly IDnsInterceptor dnsInterceptor;
|
||||
private readonly IEnumerable<IDnsConflictSolver> conflictSolvers;
|
||||
private readonly ILogger<DnsInterceptHostedService> logger;
|
||||
private readonly IHost host;
|
||||
|
||||
/// <summary>
|
||||
/// dns拦截后台服务
|
||||
/// </summary>
|
||||
/// <param name="dnsInterceptor"></param>
|
||||
/// <param name="conflictSolvers"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="host"></param>
|
||||
public DnsInterceptHostedService(
|
||||
IDnsInterceptor dnsInterceptor,
|
||||
IEnumerable<IDnsConflictSolver> conflictSolvers,
|
||||
ILogger<DnsInterceptHostedService> logger,
|
||||
IHost host)
|
||||
{
|
||||
this.dnsInterceptor = dnsInterceptor;
|
||||
this.conflictSolvers = conflictSolvers;
|
||||
this.logger = logger;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动时处理冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var solver in this.conflictSolvers)
|
||||
{
|
||||
await solver.SolveAsync(cancellationToken);
|
||||
}
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止时恢复冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var solver in this.conflictSolvers)
|
||||
{
|
||||
await solver.RestoreAsync(cancellationToken);
|
||||
}
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// dns后台
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.dnsInterceptor.InterceptAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == 995)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "dns拦截器异常");
|
||||
await this.host.StopAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
FastGithub.PacketIntercept/FastGithub.PacketIntercept.csproj
Normal file
16
FastGithub.PacketIntercept/FastGithub.PacketIntercept.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="DNS" Version="7.0.0" />
|
||||
<PackageReference Include="WindivertDotnet" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FastGithub.Configuration\FastGithub.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
25
FastGithub.PacketIntercept/IDnsConflictSolver.cs
Normal file
25
FastGithub.PacketIntercept/IDnsConflictSolver.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept
|
||||
{
|
||||
/// <summary>
|
||||
/// Dns冲突解决者
|
||||
/// </summary>
|
||||
interface IDnsConflictSolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 解决冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task SolveAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 恢复冲突
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task RestoreAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
18
FastGithub.PacketIntercept/IDnsInterceptor.cs
Normal file
18
FastGithub.PacketIntercept/IDnsInterceptor.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept
|
||||
{
|
||||
/// <summary>
|
||||
/// dns拦截器接口
|
||||
/// </summary>
|
||||
interface IDnsInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 拦截数据包
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task InterceptAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
18
FastGithub.PacketIntercept/ITcpInterceptor.cs
Normal file
18
FastGithub.PacketIntercept/ITcpInterceptor.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.PacketIntercept
|
||||
{
|
||||
/// <summary>
|
||||
/// tcp拦截器接口
|
||||
/// </summary>
|
||||
interface ITcpInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 拦截数据包
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task InterceptAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
37
FastGithub.PacketIntercept/ServiceCollectionExtensions.cs
Normal file
37
FastGithub.PacketIntercept/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using FastGithub.PacketIntercept;
|
||||
using FastGithub.PacketIntercept.Dns;
|
||||
using FastGithub.PacketIntercept.Tcp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册数据包拦截器
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static IServiceCollection AddPacketIntercept(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDnsConflictSolver, HostsConflictSolver>();
|
||||
services.AddSingleton<IDnsConflictSolver, ProxyConflictSolver>();
|
||||
services.TryAddSingleton<IDnsInterceptor, DnsInterceptor>();
|
||||
services.AddHostedService<DnsInterceptHostedService>();
|
||||
|
||||
services.AddSingleton<ITcpInterceptor, SshInterceptor>();
|
||||
services.AddSingleton<ITcpInterceptor, GitInterceptor>();
|
||||
services.AddSingleton<ITcpInterceptor, HttpInterceptor>();
|
||||
services.AddSingleton<ITcpInterceptor, HttpsInterceptor>();
|
||||
services.AddHostedService<TcpInterceptHostedService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
22
FastGithub.PacketIntercept/Tcp/GitInterceptor.cs
Normal file
22
FastGithub.PacketIntercept/Tcp/GitInterceptor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// git拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class GitInterceptor : TcpInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// git拦截器
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public GitInterceptor(ILogger<HttpInterceptor> logger)
|
||||
: base(9418, GlobalListener.GitPort, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
FastGithub.PacketIntercept/Tcp/HttpInterceptor.cs
Normal file
22
FastGithub.PacketIntercept/Tcp/HttpInterceptor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// http拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class HttpInterceptor : TcpInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// http拦截器
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public HttpInterceptor(ILogger<HttpInterceptor> logger)
|
||||
: base(80, GlobalListener.HttpPort, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
FastGithub.PacketIntercept/Tcp/HttpsInterceptor.cs
Normal file
22
FastGithub.PacketIntercept/Tcp/HttpsInterceptor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// https拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class HttpsInterceptor : TcpInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// https拦截器
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public HttpsInterceptor(ILogger<HttpsInterceptor> logger)
|
||||
: base(443, GlobalListener.HttpsPort, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
FastGithub.PacketIntercept/Tcp/SshInterceptor.cs
Normal file
22
FastGithub.PacketIntercept/Tcp/SshInterceptor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using FastGithub.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// ssh拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
sealed class SshInterceptor : TcpInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// ssh拦截器
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public SshInterceptor(ILogger<HttpInterceptor> logger)
|
||||
: base(22, GlobalListener.SshPort, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
104
FastGithub.PacketIntercept/Tcp/TcpInterceptor.cs
Normal file
104
FastGithub.PacketIntercept/Tcp/TcpInterceptor.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WindivertDotnet;
|
||||
|
||||
namespace FastGithub.PacketIntercept.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// tcp拦截器
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
abstract class TcpInterceptor : ITcpInterceptor
|
||||
{
|
||||
private readonly Filter filter;
|
||||
private readonly ushort oldServerPort;
|
||||
private readonly ushort newServerPort;
|
||||
private readonly ILogger logger;
|
||||
|
||||
/// <summary>
|
||||
/// tcp拦截器
|
||||
/// </summary>
|
||||
/// <param name="oldServerPort">修改前的服务器端口</param>
|
||||
/// <param name="newServerPort">修改后的服务器端口</param>
|
||||
/// <param name="logger"></param>
|
||||
public TcpInterceptor(int oldServerPort, int newServerPort, ILogger logger)
|
||||
{
|
||||
this.filter = Filter.True
|
||||
.And(f => f.Network.Loopback)
|
||||
.And(f => f.Tcp.DstPort == oldServerPort || f.Tcp.SrcPort == newServerPort);
|
||||
|
||||
this.oldServerPort = (ushort)oldServerPort;
|
||||
this.newServerPort = (ushort)newServerPort;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拦截指定端口的数据包
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="Win32Exception"></exception>
|
||||
public async Task InterceptAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.oldServerPort == this.newServerPort)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var divert = new WinDivert(this.filter, WinDivertLayer.Network);
|
||||
using var packet = new WinDivertPacket();
|
||||
using var addr = new WinDivertAddress();
|
||||
|
||||
if (Socket.OSSupportsIPv4)
|
||||
{
|
||||
this.logger.LogInformation($"{IPAddress.Loopback}:{this.oldServerPort} <=> {IPAddress.Loopback}:{this.newServerPort}");
|
||||
}
|
||||
if (Socket.OSSupportsIPv6)
|
||||
{
|
||||
this.logger.LogInformation($"{IPAddress.IPv6Loopback}:{this.oldServerPort} <=> {IPAddress.IPv6Loopback}:{this.newServerPort}");
|
||||
}
|
||||
|
||||
while (cancellationToken.IsCancellationRequested == false)
|
||||
{
|
||||
await divert.RecvAsync(packet, addr, cancellationToken);
|
||||
try
|
||||
{
|
||||
this.ModifyTcpPacket(packet, addr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogWarning(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await divert.SendAsync(packet, addr, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改tcp数据端口的端口
|
||||
/// </summary>
|
||||
/// <param name="packet"></param>
|
||||
/// <param name="addr"></param>
|
||||
unsafe private void ModifyTcpPacket(WinDivertPacket packet, WinDivertAddress addr)
|
||||
{
|
||||
var result = packet.GetParseResult();
|
||||
if (result.TcpHeader->DstPort == oldServerPort)
|
||||
{
|
||||
result.TcpHeader->DstPort = this.newServerPort;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TcpHeader->SrcPort = oldServerPort;
|
||||
}
|
||||
addr.Flags |= WinDivertAddressFlag.Impostor;
|
||||
packet.CalcChecksums(addr);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user