Compare commits
No commits in common. "master" and "2.1.4" have entirely different histories.
63
.gitattributes
vendored
63
.gitattributes
vendored
@ -1,63 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# 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
21
.github/ISSUE_TEMPLATE/bug-----.md
vendored
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
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
363
.gitignore
vendored
@ -1,363 +0,0 @@
|
|||||||
## 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
|
|
@ -1,15 +0,0 @@
|
|||||||
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.
|
|
@ -1,857 +0,0 @@
|
|||||||
|
|
||||||
##############################################
|
|
||||||
# #
|
|
||||||
# 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'
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
@ -1,65 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
@ -1,109 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
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("当前无可用的端口");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,359 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
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}]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
<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>
|
|
@ -1,28 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,12 +0,0 @@
|
|||||||
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))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace FastGithub.FlowAnalyze
|
|
||||||
{
|
|
||||||
[JsonSerializable(typeof(FlowStatistics))]
|
|
||||||
public partial class FlowStatisticsContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
namespace FastGithub.FlowAnalyze
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 流量类型
|
|
||||||
/// </summary>
|
|
||||||
public enum FlowType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 读取
|
|
||||||
/// </summary>
|
|
||||||
Read,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 写入
|
|
||||||
/// </summary>
|
|
||||||
Wirte
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\FastGithub.DomainResolve\FastGithub.DomainResolve.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,58 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,236 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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())
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
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证书异常");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
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}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
@ -1,68 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace FastGithub.HttpServer.HttpMiddlewares
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 请求日志特性
|
|
||||||
/// </summary>
|
|
||||||
public interface IRequestLoggingFeature
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 是否启用
|
|
||||||
/// </summary>
|
|
||||||
bool Enable { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
|
||||||
{
|
|
||||||
interface IHttpProxyFeature
|
|
||||||
{
|
|
||||||
HostString ProxyHost { get; }
|
|
||||||
|
|
||||||
ProxyProtocol ProxyProtocol { get; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
namespace FastGithub.HttpServer.TcpMiddlewares
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 代理协议
|
|
||||||
/// </summary>
|
|
||||||
enum ProxyProtocol
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 无代理
|
|
||||||
/// </summary>
|
|
||||||
None,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// http代理
|
|
||||||
/// </summary>
|
|
||||||
HttpProxy,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 隧道代理
|
|
||||||
/// </summary>
|
|
||||||
TunnelProxy
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
@ -1,25 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
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