diff --git a/.github/ISSUE_TEMPLATE/bug-----.md b/.github/ISSUE_TEMPLATE/bug-----.md new file mode 100644 index 00000000..a0be7667 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-----.md @@ -0,0 +1,21 @@ +--- +name: Bug发现与报告 +about: 写一份报告来帮助我们改进 +title: '' +labels: '' +assignees: '' + +--- + +**Bug描述** +Bug的详细描述内容 + +**重现步骤** +1. xxx +2. yyy +3. zzz + + +**软件信息** + - 操作系统: [e.g. win10-x64] + - FastGithub: [e.g. v2.0.0] diff --git a/@dnscrypt-proxy/LICENSE b/@dnscrypt-proxy/LICENSE new file mode 100644 index 00000000..cf873f5e --- /dev/null +++ b/@dnscrypt-proxy/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2018-2021, Frank Denis + +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. diff --git a/@dnscrypt-proxy/dnscrypt-proxy.toml b/@dnscrypt-proxy/dnscrypt-proxy.toml new file mode 100644 index 00000000..d4102d76 --- /dev/null +++ b/@dnscrypt-proxy/dnscrypt-proxy.toml @@ -0,0 +1,857 @@ + +############################################## +# # +# dnscrypt-proxy configuration # +# # +############################################## + +## This is an example configuration file. +## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml" +## +## Online documentation is available here: https://dnscrypt.info/doc + + + +################################## +# Global settings # +################################## + +## List of servers to use +## +## Servers from the "public-resolvers" source (see down below) can +## be viewed here: https://dnscrypt.info/public-servers +## +## The proxy will automatically pick working servers from this list. +## Note that the require_* filters do NOT apply when using this setting. +## +## By default, this list is empty and all registered servers matching the +## require_* filters will be used instead. +## +## Remove the leading # first to enable this; lines starting with # are ignored. + +# server_names = ['scaleway-fr', 'google', 'yandex', 'cloudflare'] + + +## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. +## Example with both IPv4 and IPv6: +## listen_addresses = ['127.0.0.1:53', '[::1]:53'] +## +## To listen to all IPv4 addresses, use `listen_addresses = ['0.0.0.0:53']` +## To listen to all IPv4+IPv6 addresses, use `listen_addresses = ['[::]:53']` + +listen_addresses = ['127.0.0.1:53'] + + +## Maximum number of simultaneous client connections to accept + +max_clients = 250 + + +## Switch to a different system user after listening sockets have been created. +## Note (1): this feature is currently unsupported on Windows. +## Note (2): this feature is not compatible with systemd socket activation. +## Note (3): when using -pidfile, the PID file directory must be writable by the new user + +# user_name = 'nobody' + + +## Require servers (from remote sources) to satisfy specific properties + +# Use servers reachable over IPv4 +ipv4_servers = true + +# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity +ipv6_servers = false + +# Use servers implementing the DNSCrypt protocol +dnscrypt_servers = true + +# Use servers implementing the DNS-over-HTTPS protocol +doh_servers = true + +# Use servers implementing the Oblivious DoH protocol +odoh_servers = false + + +## Require servers defined by remote sources to satisfy specific properties + +# Server must support DNS security extensions (DNSSEC) +require_dnssec = false + +# Server must not log user queries (declarative) +require_nolog = true + +# Server must not enforce its own blocklist (for parental control, ads blocking...) +require_nofilter = true + +# Server names to avoid even if they match all criteria +disabled_server_names = [] + + +## Always use TCP to connect to upstream servers. +## This can be useful if you need to route everything through Tor. +## Otherwise, leave this to `false`, as it doesn't improve security +## (dnscrypt-proxy will always encrypt everything even using UDP), and can +## only increase latency. + +force_tcp = false + + +## SOCKS proxy +## Uncomment the following line to route all TCP connections to a local Tor node +## Tor doesn't support UDP, so set `force_tcp` to `true` as well. + +# proxy = 'socks5://127.0.0.1:9050' + + +## HTTP/HTTPS proxy +## Only for DoH servers + +# http_proxy = 'http://127.0.0.1:8888' + + +## How long a DNS query will wait for a response, in milliseconds. +## If you have a network with *a lot* of latency, you may need to +## increase this. Startup may be slower if you do so. +## Don't increase it too much. 10000 is the highest reasonable value. + +timeout = 5000 + + +## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds + +keepalive = 30 + + +## Add EDNS-client-subnet information to outgoing queries +## +## Multiple networks can be listed; they will be randomly chosen. +## These networks don't have to match your actual networks. + +# edns_client_subnet = ["0.0.0.0/0", "2001:db8::/32"] + + +## Response for blocked queries. Options are `refused`, `hinfo` (default) or +## an IP response. To give an IP response, use the format `a:,aaaa:`. +## 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', '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://` (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' diff --git a/@dnscrypt-proxy/linux-arm64/dnscrypt-proxy b/@dnscrypt-proxy/linux-arm64/dnscrypt-proxy new file mode 100644 index 00000000..ac117c83 Binary files /dev/null and b/@dnscrypt-proxy/linux-arm64/dnscrypt-proxy differ diff --git a/@dnscrypt-proxy/linux-x64/dnscrypt-proxy b/@dnscrypt-proxy/linux-x64/dnscrypt-proxy new file mode 100644 index 00000000..170f503d Binary files /dev/null and b/@dnscrypt-proxy/linux-x64/dnscrypt-proxy differ diff --git a/@dnscrypt-proxy/osx-arm64/dnscrypt-proxy b/@dnscrypt-proxy/osx-arm64/dnscrypt-proxy new file mode 100644 index 00000000..c78561b4 Binary files /dev/null and b/@dnscrypt-proxy/osx-arm64/dnscrypt-proxy differ diff --git a/@dnscrypt-proxy/osx-x64/dnscrypt-proxy b/@dnscrypt-proxy/osx-x64/dnscrypt-proxy new file mode 100644 index 00000000..a4e468b1 Binary files /dev/null and b/@dnscrypt-proxy/osx-x64/dnscrypt-proxy differ diff --git a/@dnscrypt-proxy/win-x64/dnscrypt-proxy.exe b/@dnscrypt-proxy/win-x64/dnscrypt-proxy.exe new file mode 100644 index 00000000..8ccb040d Binary files /dev/null and b/@dnscrypt-proxy/win-x64/dnscrypt-proxy.exe differ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..eb74ff8f --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,16 @@ + + + 2.1.5 + enable + net7.0 + true + github加速神器 + https://github.com/dotnetcore/FastGithub + win-x64 + + + + none + false + + diff --git a/FastGithub.Configuration/DomainConfig.cs b/FastGithub.Configuration/DomainConfig.cs new file mode 100644 index 00000000..6059e36b --- /dev/null +++ b/FastGithub.Configuration/DomainConfig.cs @@ -0,0 +1,65 @@ +using System; +using System.Net; + +namespace FastGithub.Configuration +{ + /// + /// 域名配置 + /// + public record DomainConfig + { + /// + /// 是否发送SNI + /// + public bool TlsSni { get; init; } + + /// + /// 自定义SNI值的表达式 + /// + public string? TlsSniPattern { get; init; } + + /// + /// 是否忽略服务器证书域名不匹配 + /// 当不发送SNI时服务器可能发回域名不匹配的证书 + /// + public bool TlsIgnoreNameMismatch { get; init; } + + /// + /// 使用的ip地址 + /// + public IPAddress? IPAddress { get; init; } + + /// + /// 请求超时时长 + /// + public TimeSpan? Timeout { get; init; } + + /// + /// 目的地 + /// 格式为相对或绝对uri + /// + public Uri? Destination { get; init; } + + /// + /// 自定义响应 + /// + public ResponseConfig? Response { get; init; } + + /// + /// 获取TlsSniPattern + /// + /// + 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); + } + } +} diff --git a/FastGithub.Configuration/DomainPattern.cs b/FastGithub.Configuration/DomainPattern.cs new file mode 100644 index 00000000..0e841f24 --- /dev/null +++ b/FastGithub.Configuration/DomainPattern.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.RegularExpressions; + +namespace FastGithub.Configuration +{ + /// + /// 表示域名表达式 + /// *表示除.之外任意0到多个字符 + /// + public class DomainPattern : IComparable + { + private readonly Regex regex; + private readonly string domainPattern; + + /// + /// 域名表达式 + /// *表示除.之外任意0到多个字符 + /// + /// 域名表达式 + public DomainPattern(string domainPattern) + { + this.domainPattern = domainPattern; + var regexPattern = Regex.Escape(domainPattern).Replace(@"\*", @"[^\.]*"); + this.regex = new Regex($"^{regexPattern}$", RegexOptions.IgnoreCase); + } + + /// + /// 与目标比较 + /// + /// + /// + 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; + } + + + /// + /// 比较两个分段 + /// + /// abc + /// abc* + /// + private static int Compare(string x, string y) + { + var valueX = x.Replace('*', char.MaxValue); + var valueY = y.Replace('*', char.MaxValue); + return valueX.CompareTo(valueY); + } + + /// + /// 是否与指定域名匹配 + /// + /// + /// + public bool IsMatch(string domain) + { + return this.regex.IsMatch(domain); + } + + /// + /// 转换为文本 + /// + /// + public override string ToString() + { + return this.domainPattern; + } + } +} diff --git a/FastGithub.Configuration/FastGithub.Configuration.csproj b/FastGithub.Configuration/FastGithub.Configuration.csproj new file mode 100644 index 00000000..c18dd9e7 --- /dev/null +++ b/FastGithub.Configuration/FastGithub.Configuration.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/FastGithub.Configuration/FastGithubConfig.cs b/FastGithub.Configuration/FastGithubConfig.cs new file mode 100644 index 00000000..5cfe02c4 --- /dev/null +++ b/FastGithub.Configuration/FastGithubConfig.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; + +namespace FastGithub.Configuration +{ + /// + /// FastGithub配置 + /// + public class FastGithubConfig + { + private SortedDictionary domainConfigs; + private ConcurrentDictionary domainConfigCache; + + /// + /// http代理端口 + /// + public int HttpProxyPort { get; set; } + + /// + /// 回退的dns + /// + public IPEndPoint[] FallbackDns { get; set; } + + /// + /// FastGithub配置 + /// + /// + /// + public FastGithubConfig(IOptionsMonitor options) + { + var opt = options.CurrentValue; + + this.HttpProxyPort = opt.HttpProxyPort; + this.FallbackDns = opt.FallbackDns; + this.domainConfigs = ConvertDomainConfigs(opt.DomainConfigs); + this.domainConfigCache = new ConcurrentDictionary(); + + options.OnChange(opt => this.Update(opt)); + } + + /// + /// 更新配置 + /// + /// + private void Update(FastGithubOptions options) + { + this.HttpProxyPort = options.HttpProxyPort; + this.FallbackDns = options.FallbackDns; + this.domainConfigs = ConvertDomainConfigs(options.DomainConfigs); + this.domainConfigCache = new ConcurrentDictionary(); + } + + /// + /// 配置转换 + /// + /// + /// + private static SortedDictionary ConvertDomainConfigs(Dictionary domainConfigs) + { + var result = new SortedDictionary(); + foreach (var kv in domainConfigs) + { + result.Add(new DomainPattern(kv.Key), kv.Value); + } + return result; + } + + /// + /// 是否匹配指定的域名 + /// + /// + /// + public bool IsMatch(string domain) + { + return this.TryGetDomainConfig(domain, out _); + } + + /// + /// 尝试获取域名配置 + /// + /// + /// + /// + 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]; + } + } + + /// + /// 获取所有域名表达式 + /// + /// + public DomainPattern[] GetDomainPatterns() + { + return this.domainConfigs.Keys.ToArray(); + } + } +} diff --git a/FastGithub.Configuration/FastGithubException.cs b/FastGithub.Configuration/FastGithubException.cs new file mode 100644 index 00000000..7a4eb46d --- /dev/null +++ b/FastGithub.Configuration/FastGithubException.cs @@ -0,0 +1,29 @@ +using System; + +namespace FastGithub.Configuration +{ + /// + /// 表示FastGithub异常 + /// + public class FastGithubException : Exception + { + /// + /// FastGithub异常 + /// + /// + public FastGithubException(string message) + : base(message) + { + } + + /// + /// FastGithub异常 + /// + /// + /// + public FastGithubException(string message, Exception? innerException) + : base(message, innerException) + { + } + } +} diff --git a/FastGithub.Configuration/FastGithubOptions.cs b/FastGithub.Configuration/FastGithubOptions.cs new file mode 100644 index 00000000..a4af722b --- /dev/null +++ b/FastGithub.Configuration/FastGithubOptions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace FastGithub.Configuration +{ + /// + /// FastGithub的配置 + /// + public class FastGithubOptions + { + /// + /// http代理端口 + /// + public int HttpProxyPort { get; set; } = 38457; + + /// + /// 回退的dns + /// + public IPEndPoint[] FallbackDns { get; set; } = Array.Empty(); + + /// + /// 代理的域名配置 + /// + public Dictionary DomainConfigs { get; set; } = new(); + } +} diff --git a/FastGithub.Configuration/GlobalListener.cs b/FastGithub.Configuration/GlobalListener.cs new file mode 100644 index 00000000..9724f050 --- /dev/null +++ b/FastGithub.Configuration/GlobalListener.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; + +namespace FastGithub.Configuration +{ + /// + /// 监听器 + /// + public static class GlobalListener + { + private static readonly IPGlobalProperties global = IPGlobalProperties.GetIPGlobalProperties(); + private static readonly HashSet tcpListenPorts = GetListenPorts(global.GetActiveTcpListeners); + private static readonly HashSet udpListenPorts = GetListenPorts(global.GetActiveUdpListeners); + + /// + /// ssh端口 + /// + public static int SshPort { get; } = GetAvailableTcpPort(22); + + /// + /// git端口 + /// + public static int GitPort { get; } = GetAvailableTcpPort(9418); + + /// + /// http端口 + /// + public static int HttpPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(80) : GetAvailableTcpPort(3880); + + /// + /// https端口 + /// + public static int HttpsPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(443) : GetAvailableTcpPort(38443); + + /// + /// 获取已监听的端口 + /// + /// + /// + private static HashSet GetListenPorts(Func func) + { + var hashSet = new HashSet(); + try + { + foreach (var endpoint in func()) + { + hashSet.Add(endpoint.Port); + } + } + catch (Exception) + { + } + return hashSet; + } + + /// + /// 是可以监听TCP + /// + /// + /// + public static bool CanListenTcp(int port) + { + return tcpListenPorts.Contains(port) == false; + } + + /// + /// 是可以监听UDP + /// + /// + /// + public static bool CanListenUdp(int port) + { + return udpListenPorts.Contains(port) == false; + } + + /// + /// 是可以监听TCP和Udp + /// + /// + /// + public static bool CanListen(int port) + { + return CanListenTcp(port) && CanListenUdp(port); + } + + /// + /// 获取可用的随机Tcp端口 + /// + /// + /// + public static int GetAvailableTcpPort(int minPort) + { + return GetAvailablePort(CanListenTcp, minPort); + } + + /// + /// 获取可用的随机Udp端口 + /// + /// + /// + public static int GetAvailableUdpPort(int minPort) + { + return GetAvailablePort(CanListenUdp, minPort); + } + + /// + /// 获取可用的随机端口 + /// + /// + /// + public static int GetAvailablePort(int minPort) + { + return GetAvailablePort(CanListen, minPort); + } + + /// + /// 获取可用端口 + /// + /// + /// + /// + /// + private static int GetAvailablePort(Func canFunc, int minPort) + { + for (var port = minPort; port < IPEndPoint.MaxPort; port++) + { + if (canFunc(port) == true) + { + return port; + } + } + throw new FastGithubException("当前无可用的端口"); + } + } +} diff --git a/FastGithub.Configuration/LoggerExtensions.cs b/FastGithub.Configuration/LoggerExtensions.cs new file mode 100644 index 00000000..f920cc86 --- /dev/null +++ b/FastGithub.Configuration/LoggerExtensions.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace FastGithub +{ + /// + /// 日志插值字符串扩展 + /// + public static class LoggerExtensions + { + /// + /// 输出日志 + /// + /// + /// + /// + public static void Log(this ILogger logger, LogLevel level, FormattableString formattableString) + => logger.Log(level, formattableString.Format, formattableString.GetArguments()); + + /// + /// 输出日志 + /// + /// + /// + /// + /// + public static void Log(this ILogger logger, LogLevel level, Exception? error, FormattableString formattableString) + => logger.Log(level, error, formattableString.Format, formattableString.GetArguments()); + + /// + /// 输出Trace日志 + /// + /// + /// + public static void LogTrace(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Trace, formattableString); + + /// + /// 输出Debug日志 + /// + /// + /// + public static void LogDebug(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Debug, formattableString); + + /// + /// 输出Information日志 + /// + /// + /// + public static void LogInformation(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Information, formattableString); + + /// + /// 输出Warning日志 + /// + /// + /// + public static void LogWarning(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Warning, formattableString); + + /// + /// 输出日志 + /// + /// + /// + public static void LogError(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Error, formattableString); + + /// + /// 输出日志 + /// + /// + /// + public static void LogError(this ILogger logger, Exception error, FormattableString formattableString) + => logger.Log(LogLevel.Error, error, formattableString); + + /// + /// 输出Critical日志 + /// + /// + /// + public static void LogCritical(this ILogger logger, FormattableString formattableString) + => logger.Log(LogLevel.Critical, formattableString); + } +} diff --git a/FastGithub.Configuration/ResponseConfig.cs b/FastGithub.Configuration/ResponseConfig.cs new file mode 100644 index 00000000..fd2e7bc5 --- /dev/null +++ b/FastGithub.Configuration/ResponseConfig.cs @@ -0,0 +1,23 @@ +namespace FastGithub.Configuration +{ + /// + /// 响应配置 + /// + public record ResponseConfig + { + /// + /// 状态码 + /// + public int StatusCode { get; init; } = 200; + + /// + /// 内容类型 + /// + public string ContentType { get; init; } = "text/plain;charset=utf-8"; + + /// + /// 内容的值 + /// + public string? ContentValue { get; init; } + } +} diff --git a/FastGithub.Configuration/ServiceCollectionExtensions.cs b/FastGithub.Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..bffca495 --- /dev/null +++ b/FastGithub.Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Net; + +namespace FastGithub +{ + /// + /// 服务注册扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 添加配置服务 + /// + /// + /// + 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(); + return services; + } + } +} diff --git a/FastGithub.Configuration/TlsSniPattern.cs b/FastGithub.Configuration/TlsSniPattern.cs new file mode 100644 index 00000000..90d346a6 --- /dev/null +++ b/FastGithub.Configuration/TlsSniPattern.cs @@ -0,0 +1,86 @@ +using System; +using System.Net; + +namespace FastGithub.Configuration +{ + /// + /// Sni自定义值表达式 + /// @domain变量表示取域名值 + /// @ipadress变量表示取ip + /// @random变量表示取随机值 + /// + public struct TlsSniPattern + { + /// + /// 获取表示式值 + /// + public string Value { get; } + + /// + /// 无SNI + /// + public static TlsSniPattern None { get; } = new TlsSniPattern(string.Empty); + + /// + /// 域名SNI + /// + public static TlsSniPattern Domain { get; } = new TlsSniPattern("@domain"); + + /// + /// IP值的SNI + /// + public static TlsSniPattern IPAddress { get; } = new TlsSniPattern("@ipaddress"); + + /// + /// 随机值的SNI + /// + public static TlsSniPattern Random { get; } = new TlsSniPattern("@random"); + + /// + /// Sni自定义值表达式 + /// + /// 表示式值 + public TlsSniPattern(string? value) + { + this.Value = value ?? string.Empty; + } + + /// + /// 更新域名 + /// + /// + public TlsSniPattern WithDomain(string domain) + { + var value = this.Value.Replace(Domain.Value, domain, StringComparison.OrdinalIgnoreCase); + return new TlsSniPattern(value); + } + + /// + /// 更新ip地址 + /// + /// + public TlsSniPattern WithIPAddress(IPAddress address) + { + var value = this.Value.Replace(IPAddress.Value, address.ToString(), StringComparison.OrdinalIgnoreCase); + return new TlsSniPattern(value); + } + + /// + /// 更新随机数 + /// + public TlsSniPattern WithRandom() + { + var value = this.Value.Replace(Random.Value, Environment.TickCount64.ToString(), StringComparison.OrdinalIgnoreCase); + return new TlsSniPattern(value); + } + + /// + /// 转换为文本 + /// + /// + public override string ToString() + { + return this.Value; + } + } +} diff --git a/FastGithub.Configuration/TypeConverterBinder.cs b/FastGithub.Configuration/TypeConverterBinder.cs new file mode 100644 index 00000000..f7dfd33d --- /dev/null +++ b/FastGithub.Configuration/TypeConverterBinder.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace FastGithub.Configuration +{ + /// + /// TypeConverter类型转换绑定器 + /// + static class TypeConverterBinder + { + private static readonly Dictionary binders = new(); + + /// + /// 绑定转换器到指定类型 + /// + /// + /// + /// + public static void Bind<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Func reader, Func writer) + { + binders[typeof(T)] = new Binder(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 reader; + private readonly Func writer; + + public Binder(Func reader, Func 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); + } + } + } +} diff --git a/FastGithub.Core/FastGithub.Core.csproj b/FastGithub.Core/FastGithub.Core.csproj deleted file mode 100644 index bbb414c6..00000000 --- a/FastGithub.Core/FastGithub.Core.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net5.0 - enable - FastGithub - - - - - - - diff --git a/FastGithub.Core/IMiddleware.cs b/FastGithub.Core/IMiddleware.cs deleted file mode 100644 index 5eeb445e..00000000 --- a/FastGithub.Core/IMiddleware.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace FastGithub -{ - /// - /// 定义中间件的接口 - /// - /// - public interface IMiddleware - { - /// - /// 执行中间件 - /// - /// 上下文 - /// 下一个中间件 - /// - Task InvokeAsync(TContext context, Func next); - } -} diff --git a/FastGithub.Core/IPipelineBuilder.cs b/FastGithub.Core/IPipelineBuilder.cs deleted file mode 100644 index 2c681183..00000000 --- a/FastGithub.Core/IPipelineBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace FastGithub -{ - /// - /// 定义中间件管道创建者的接口 - /// - /// 中间件上下文 - public interface IPipelineBuilder - { - /// - /// 获取服务提供者 - /// - IServiceProvider AppServices { get; } - - /// - /// 使用中间件 - /// - /// - /// - /// - /// - IPipelineBuilder Use() where TMiddleware : class, IMiddleware; - - /// - /// 使用中间件 - /// - /// - /// - /// - /// - IPipelineBuilder Use(Func, Task> middleware); - - /// - /// 使用中间件 - /// - /// 中间件 - /// - IPipelineBuilder Use(Func, InvokeDelegate> middleware); - - /// - /// 创建所有中间件执行处理者 - /// - /// - InvokeDelegate Build(); - - /// - /// 使用默认配制创建新的PipelineBuilder - /// - /// - IPipelineBuilder New(); - } -} diff --git a/FastGithub.Core/InvokeDelegate.cs b/FastGithub.Core/InvokeDelegate.cs deleted file mode 100644 index 985d433f..00000000 --- a/FastGithub.Core/InvokeDelegate.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading.Tasks; - -namespace FastGithub -{ - /// - /// 表示所有中间件执行委托 - /// - /// 中间件上下文类型 - /// 中间件上下文 - /// - public delegate Task InvokeDelegate(TContext context); -} diff --git a/FastGithub.Core/OptionsAttribute.cs b/FastGithub.Core/OptionsAttribute.cs deleted file mode 100644 index 65dadb60..00000000 --- a/FastGithub.Core/OptionsAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace FastGithub -{ - /// - /// 表示选项特性 - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class OptionsAttribute : Attribute - { - public string? SessionKey { get; } - - /// - /// 选项特性 - /// - public OptionsAttribute() - { - } - - /// - /// 选项特性 - /// - /// - public OptionsAttribute(string sessionKey) - { - this.SessionKey = sessionKey; - } - } -} diff --git a/FastGithub.Core/PipelineBuilder.cs b/FastGithub.Core/PipelineBuilder.cs deleted file mode 100644 index ae5cf02e..00000000 --- a/FastGithub.Core/PipelineBuilder.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace FastGithub -{ - /// - /// 表示中间件创建者 - /// - public class PipelineBuilder : IPipelineBuilder - { - private readonly InvokeDelegate completedHandler; - private readonly List, InvokeDelegate>> middlewares = new List, InvokeDelegate>>(); - - /// - /// 获取服务提供者 - /// - public IServiceProvider AppServices { get; } - - /// - /// 中间件创建者 - /// - /// - public PipelineBuilder(IServiceProvider appServices) - : this(appServices, context => Task.CompletedTask) - { - } - - /// - /// 中间件创建者 - /// - /// - /// 完成执行内容处理者 - public PipelineBuilder(IServiceProvider appServices, InvokeDelegate completedHandler) - { - this.AppServices = appServices; - this.completedHandler = completedHandler; - } - - - /// - /// 使用中间件 - /// - /// - /// - /// - /// - public IPipelineBuilder Use() where TMiddleware : class, IMiddleware - { - var middleware = this.AppServices.GetRequiredService(); - return this.Use(middleware.InvokeAsync); - } - - /// - /// 使用中间件 - /// - /// - /// - /// - /// - public IPipelineBuilder Use(Func, Task> middleware) - { - return this.Use(next => context => middleware(context, () => next(context))); - } - - /// - /// 使用中间件 - /// - /// - /// - public IPipelineBuilder Use(Func, InvokeDelegate> middleware) - { - this.middlewares.Add(middleware); - return this; - } - - - /// - /// 创建所有中间件执行处理者 - /// - /// - public InvokeDelegate Build() - { - var handler = this.completedHandler; - for (var i = this.middlewares.Count - 1; i >= 0; i--) - { - handler = this.middlewares[i](handler); - } - return handler; - } - - - /// - /// 使用默认配制创建新的PipelineBuilder - /// - /// - public IPipelineBuilder New() - { - return new PipelineBuilder(this.AppServices, this.completedHandler); - } - } -} \ No newline at end of file diff --git a/FastGithub.Core/PipelineBuilderExtensions.cs b/FastGithub.Core/PipelineBuilderExtensions.cs deleted file mode 100644 index 67a585b0..00000000 --- a/FastGithub.Core/PipelineBuilderExtensions.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace FastGithub -{ - /// - /// 中间件创建者扩展 - /// - public static class PipelineBuilderExtensions - { - /// - /// 中断执行中间件 - /// - /// - /// - /// 处理者 - /// - public static IPipelineBuilder Run(this IPipelineBuilder builder, InvokeDelegate handler) - { - return builder.Use(_ => handler); - } - - /// - /// 条件中间件 - /// - /// - /// - /// - /// - /// - public static IPipelineBuilder When(this IPipelineBuilder builder, Func predicate, InvokeDelegate handler) - { - return builder.Use(next => async context => - { - if (predicate.Invoke(context) == true) - { - await handler.Invoke(context); - } - else - { - await next(context); - } - }); - } - - - /// - /// 条件中间件 - /// - /// - /// - /// - /// - /// - public static IPipelineBuilder When(this IPipelineBuilder builder, Func predicate, Action> configureAction) - { - return builder.Use(next => async context => - { - if (predicate.Invoke(context) == true) - { - var branchBuilder = builder.New(); - configureAction(branchBuilder); - await branchBuilder.Build().Invoke(context); - } - else - { - await next(context); - } - }); - } - } -} diff --git a/FastGithub.Core/ServiceAttribute.cs b/FastGithub.Core/ServiceAttribute.cs deleted file mode 100644 index be8f59dd..00000000 --- a/FastGithub.Core/ServiceAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; - -namespace FastGithub -{ - /// - /// 表示服务特性 - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class ServiceAttribute : Attribute - { - /// - /// 获取服务的生命周期 - /// - public ServiceLifetime Lifetime { get; } - - /// - /// 获取或设置注册的服务类型 - /// 为null直接使得当前类型 - /// - public Type? ServiceType { get; set; } - - /// - /// 将当前实现类型注册为服务的特性 - /// - /// 生命周期 - public ServiceAttribute(ServiceLifetime lifetime) - { - Lifetime = lifetime; - } - } -} diff --git a/FastGithub.Core/ServiceCollectionExtensions.cs b/FastGithub.Core/ServiceCollectionExtensions.cs deleted file mode 100644 index 7b0e7443..00000000 --- a/FastGithub.Core/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Linq; -using System.Reflection; - -namespace FastGithub -{ - /// - /// 服务注册扩展 - /// - public static class ServiceCollectionExtensions - { - /// - /// 注册程序集下所有服务下选项 - /// - /// - /// 配置 - /// - public static IServiceCollection AddServiceAndOptions(this IServiceCollection services, Assembly assembly, IConfiguration configuration) - { - services.AddAttributeServices(assembly); - services.AddAttributeOptions(assembly, configuration); - - return services; - } - - /// - /// 添加程序集下ServiceAttribute标记的服务 - /// - /// - /// - /// - private static IServiceCollection AddAttributeServices(this IServiceCollection services, Assembly assembly) - { - var implTypes = assembly - .GetTypes() - .Where(item => item.IsClass && item.IsAbstract == false) - .ToArray(); - - foreach (var implType in implTypes) - { - var attributes = implType.GetCustomAttributes(false); - foreach (var attr in attributes) - { - var serviceType = attr.ServiceType ?? implType; - if (services.Any(item => item.ServiceType == serviceType && item.ImplementationType == implType) == false) - { - var descriptor = ServiceDescriptor.Describe(serviceType, implType, attr.Lifetime); - services.Add(descriptor); - } - } - } - return services; - } - - - /// - /// 添加程序集下OptionsAttribute标记的服务 - /// - /// - /// - /// - private static IServiceCollection AddAttributeOptions(this IServiceCollection services, Assembly assembly, IConfiguration configuration) - { - foreach (var optionsType in assembly.GetTypes()) - { - var optionsAttribute = optionsType.GetCustomAttribute(); - if (optionsAttribute != null) - { - var key = optionsAttribute.SessionKey ?? optionsType.Name; - var section = configuration.GetSection(key); - OptionsBinder.Create(services, optionsType).Bind(section); - } - } - return services; - } - - /// - /// options绑定器 - /// - private abstract class OptionsBinder - { - public abstract void Bind(IConfiguration configuration); - - /// - /// 创建OptionsBinder实例 - /// - /// - /// - /// - public static OptionsBinder Create(IServiceCollection services, Type optionsType) - { - var binderType = typeof(OptionsBinderImpl<>).MakeGenericType(optionsType); - var binder = Activator.CreateInstance(binderType, new object[] { services }); - - return binder is OptionsBinder optionsBinder - ? optionsBinder - : throw new TypeInitializationException(binderType.FullName, null); - } - - private class OptionsBinderImpl : OptionsBinder where TOptions : class - { - private readonly IServiceCollection services; - - public OptionsBinderImpl(IServiceCollection services) - { - this.services = services; - } - - public override void Bind(IConfiguration configuration) - { - this.services.AddOptions().Bind(configuration); - } - } - } - } -} diff --git a/FastGithub.Dns/DnsHostedService.cs b/FastGithub.Dns/DnsHostedService.cs deleted file mode 100644 index 1a952fbb..00000000 --- a/FastGithub.Dns/DnsHostedService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using DNS.Server; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Dns -{ - sealed class DnsHostedService : IHostedService - { - private readonly DnsServer dnsServer; - private readonly ILogger logger; - - public DnsHostedService( - GithubRequestResolver githubRequestResolver, - IOptions options, - ILogger logger) - { - this.dnsServer = new DnsServer(githubRequestResolver, options.Value.UpStream); - this.logger = logger; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - this.dnsServer.Listen(); - this.logger.LogInformation("dns服务启用成功"); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - this.dnsServer.Dispose(); - this.logger.LogInformation("dns服务已终止"); - return Task.CompletedTask; - } - } -} diff --git a/FastGithub.Dns/DnsOptions.cs b/FastGithub.Dns/DnsOptions.cs deleted file mode 100644 index 39ef3731..00000000 --- a/FastGithub.Dns/DnsOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Net; - -namespace FastGithub.Dns -{ - [Options("Dns")] - sealed class DnsOptions - { - public IPAddress UpStream { get; set; } = IPAddress.Parse("114.114.114.114"); - } -} diff --git a/FastGithub.Dns/DnsServiceCollectionExtensions.cs b/FastGithub.Dns/DnsServiceCollectionExtensions.cs deleted file mode 100644 index f8353a8f..00000000 --- a/FastGithub.Dns/DnsServiceCollectionExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FastGithub.Dns; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace FastGithub -{ - /// - /// 服务注册扩展 - /// - public static class DnsServiceCollectionExtensions - { - /// - /// 注册github的dns服务 - /// - /// - /// 配置 - /// - public static IServiceCollection AddGithubDns(this IServiceCollection services, IConfiguration configuration) - { - var assembly = typeof(DnsServiceCollectionExtensions).Assembly; - return services - .AddServiceAndOptions(assembly, configuration) - .AddHostedService() - .AddGithubScanner(configuration); - } - } -} diff --git a/FastGithub.Dns/FastGithub.Dns.csproj b/FastGithub.Dns/FastGithub.Dns.csproj deleted file mode 100644 index 33f532d6..00000000 --- a/FastGithub.Dns/FastGithub.Dns.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net5.0 - enable - - - - - - - - - - diff --git a/FastGithub.Dns/GithubRequestResolver.cs b/FastGithub.Dns/GithubRequestResolver.cs deleted file mode 100644 index 98e8c660..00000000 --- a/FastGithub.Dns/GithubRequestResolver.cs +++ /dev/null @@ -1,48 +0,0 @@ -using DNS.Client.RequestResolver; -using DNS.Protocol; -using DNS.Protocol.ResourceRecords; -using FastGithub.Scanner; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Dns -{ - [Service(ServiceLifetime.Singleton)] - sealed class GithubRequestResolver : IRequestResolver - { - private readonly IGithubScanService githubScanService; - private readonly ILogger logger; - - public GithubRequestResolver( - IGithubScanService githubScanService, - ILogger logger) - { - this.githubScanService = githubScanService; - this.logger = logger; - } - - public Task Resolve(IRequest request, CancellationToken cancellationToken = default) - { - var response = Response.FromRequest(request); - var question = request.Questions.FirstOrDefault(); - - if (question != null && question.Type == RecordType.A) - { - var domain = question.Name.ToString(); - var fastAddress = this.githubScanService.FindFastAddress(domain); - - if (fastAddress != null) - { - var record = new IPAddressResourceRecord(question.Name, fastAddress); - response.AnswerRecords.Add(record); - this.logger.LogInformation(record.ToString()); - } - } - - return Task.FromResult(response); - } - } -} diff --git a/FastGithub.DomainResolve/DnsClient.cs b/FastGithub.DomainResolve/DnsClient.cs new file mode 100644 index 00000000..e96bc886 --- /dev/null +++ b/FastGithub.DomainResolve/DnsClient.cs @@ -0,0 +1,359 @@ +using DNS.Client; +using DNS.Client.RequestResolver; +using DNS.Protocol; +using DNS.Protocol.ResourceRecords; +using FastGithub.Configuration; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// DNS客户端 + /// + 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 logger; + + private readonly ConcurrentDictionary 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 Addresses, TimeSpan TimeToLive); + + /// + /// DNS客户端 + /// + /// + /// + /// + public DnsClient( + DnscryptProxy dnscryptProxy, + FastGithubConfig fastGithubConfig, + ILogger logger) + { + this.dnscryptProxy = dnscryptProxy; + this.fastGithubConfig = fastGithubConfig; + this.logger = logger; + } + + /// + /// 解析域名 + /// + /// 远程结节 + /// 是否使用快速排序 + /// + /// + public async IAsyncEnumerable ResolveAsync(DnsEndPoint endPoint, bool fastSort, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var hashSet = new HashSet(); + 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; + } + } + } + } + + /// + /// 获取dns服务 + /// + /// + private async IAsyncEnumerable 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; + } + } + } + + /// + /// 获取dns是否可用 + /// + /// + /// + /// + private async ValueTask IsDnsAvailableAsync(IPEndPoint dns, CancellationToken cancellationToken) + { + if (dns.Port != DNS_PORT) + { + return true; + } + + if (this.dnsStateCache.TryGetValue(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(); + } + } + + /// + /// 解析域名 + /// + /// + /// + /// + /// + /// + private async Task> 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>(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(); + } + 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(), expiration); + } + finally + { + semaphore.Release(); + } + } + + /// + /// 是否为Socket异常 + /// + /// + /// + private static bool IsSocketException(Exception ex) + { + if (ex is SocketException) + { + return true; + } + + var inner = ex.InnerException; + return inner != null && IsSocketException(inner); + } + + + /// + /// 解析域名 + /// + /// + /// + /// + /// + /// + private async Task LookupCoreAsync(IPEndPoint dns, DnsEndPoint endPoint, bool fastSort, CancellationToken cancellationToken = default) + { + if (endPoint.Host == LOCALHOST) + { + var loopbacks = new List(); + 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)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); + } + + /// + /// 获取IP记录 + /// + /// + /// + /// + /// + private static async Task> GetAddressRecordsAsync(IRequestResolver resolver, string domain, CancellationToken cancellationToken) + { + var addressRecords = new List(); + 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> 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(); + } + } + + + /// + /// 连接速度排序 + /// + /// + /// + /// + /// + private static async Task> OrderByConnectAnyAsync(IList 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 { fastestAddress }; + foreach (var address in addresses) + { + if (address.Equals(fastestAddress) == false) + { + list.Add(address); + } + } + return list; + } + + /// + /// 连接指定ip和端口 + /// + /// + /// + /// + /// + private static async Task 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; + } + } + } +} diff --git a/FastGithub.DomainResolve/DnscryptProxy.cs b/FastGithub.DomainResolve/DnscryptProxy.cs new file mode 100644 index 00000000..6fd62078 --- /dev/null +++ b/FastGithub.DomainResolve/DnscryptProxy.cs @@ -0,0 +1,154 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using static PInvoke.AdvApi32; + +namespace FastGithub.DomainResolve +{ + /// + /// DnscryptProxy服务 + /// + sealed class DnscryptProxy + { + private readonly ILogger logger; + private readonly string processName; + private readonly string serviceName; + private readonly string exeFilePath; + private readonly string tomlFilePath; + + /// + /// 相关进程 + /// + private Process? process; + + /// + /// 获取监听的节点 + /// + public IPEndPoint? LocalEndPoint { get; private set; } + + /// + /// DnscryptProxy服务 + /// + /// + public DnscryptProxy(ILogger 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"); + } + + /// + /// 启动dnscrypt-proxy + /// + /// + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + await this.StartCoreAsync(cancellationToken); + } + catch (Exception ex) + { + this.logger.LogWarning($"{this.processName}启动失败:{ex.Message}"); + } + } + + /// + /// 启动dnscrypt-proxy + /// + /// + /// + 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; + } + } + + /// + /// 停止服务 + /// + 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; + } + } + + /// + /// 启动DnscryptProxy进程 + /// + /// + private Process? StartDnscryptProxy() + { + return Process.Start(new ProcessStartInfo + { + FileName = this.exeFilePath, + WorkingDirectory = Path.GetDirectoryName(this.exeFilePath), + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + } + + /// + /// 转换为字符串 + /// + /// + public override string ToString() + { + return this.processName; + } + } +} diff --git a/FastGithub.DomainResolve/DomainResolveHostedService.cs b/FastGithub.DomainResolve/DomainResolveHostedService.cs new file mode 100644 index 00000000..9054fbd4 --- /dev/null +++ b/FastGithub.DomainResolve/DomainResolveHostedService.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名解析后台服务 + /// + sealed class DomainResolveHostedService : BackgroundService + { + private readonly DnscryptProxy dnscryptProxy; + private readonly IDomainResolver domainResolver; + private readonly ILogger logger; + private readonly TimeSpan dnscryptProxyInitDelay = TimeSpan.FromSeconds(5d); + private readonly TimeSpan testPeriodTimeSpan = TimeSpan.FromSeconds(1d); + + /// + /// 域名解析后台服务 + /// + /// + /// + public DomainResolveHostedService( + DnscryptProxy dnscryptProxy, + IDomainResolver domainResolver, + ILogger logger) + { + this.dnscryptProxy = dnscryptProxy; + this.domainResolver = domainResolver; + this.logger = logger; + } + + /// + /// 后台任务 + /// + /// + /// + 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, "域名解析异常"); + } + } + + /// + /// 停止服务 + /// + /// + /// + public override Task StopAsync(CancellationToken cancellationToken) + { + this.dnscryptProxy.Stop(); + return base.StopAsync(cancellationToken); + } + } +} diff --git a/FastGithub.DomainResolve/DomainResolver.cs b/FastGithub.DomainResolve/DomainResolver.cs new file mode 100644 index 00000000..b64b550d --- /dev/null +++ b/FastGithub.DomainResolve/DomainResolver.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名解析器 + /// + 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 logger; + private readonly ConcurrentDictionary dnsEndPointAddress = new(); + + /// + /// 域名解析器 + /// + /// + /// + /// + /// + public DomainResolver( + DnsClient dnsClient, + PersistenceService persistence, + IPAddressService addressService, + ILogger 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()); + } + } + + /// + /// 解析域名 + /// + /// 节点 + /// + /// + public async IAsyncEnumerable 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())) + { + await this.persistence.WriteDnsEndPointsAsync(this.dnsEndPointAddress.Keys, cancellationToken); + } + + await foreach (var adddress in this.dnsClient.ResolveAsync(endPoint, fastSort: true, cancellationToken)) + { + yield return adddress; + } + } + } + + /// + /// 对所有节点进行测速 + /// + /// + /// + 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}]"); + } + } + } + } +} diff --git a/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj b/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj new file mode 100644 index 00000000..7ae4c7a0 --- /dev/null +++ b/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/FastGithub.DomainResolve/IDomainResolver.cs b/FastGithub.DomainResolve/IDomainResolver.cs new file mode 100644 index 00000000..35007b8d --- /dev/null +++ b/FastGithub.DomainResolve/IDomainResolver.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名解析器 + /// + public interface IDomainResolver + { + /// + /// 解析所有ip + /// + /// 节点 + /// + /// + IAsyncEnumerable ResolveAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default); + + /// + /// 对所有节点进行测速 + /// + /// + /// + Task TestSpeedAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/FastGithub.DomainResolve/IPAddressService.cs b/FastGithub.DomainResolve/IPAddressService.cs new file mode 100644 index 00000000..95ad36fb --- /dev/null +++ b/FastGithub.DomainResolve/IPAddressService.cs @@ -0,0 +1,142 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// IP服务 + /// 域名IP关系缓存10分钟 + /// IPEndPoint时延缓存5分钟 + /// IPEndPoint连接超时5秒 + /// + 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; + + /// + /// IP服务 + /// + /// + public IPAddressService(DnsClient dnsClient) + { + this.dnsClient = dnsClient; + } + + /// + /// 并行获取可连接的IP + /// + /// + /// + /// + /// + public async Task GetAddressesAsync(DnsEndPoint dnsEndPoint, IEnumerable oldAddresses, CancellationToken cancellationToken) + { + var ipEndPoints = new HashSet(); + + // 历史未过期的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(); + } + + 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(); + } + + + /// + /// 获取IP节点的时延 + /// + /// + /// + /// + private async Task GetAddressElapsedAsync(IPEndPoint endPoint, CancellationToken cancellationToken) + { + if (this.addressElapsedCache.TryGetValue(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(); + } + } + + /// + /// 是否为本机网络问题 + /// + /// + /// + 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; + } + } +} diff --git a/FastGithub.DomainResolve/PersistenceService.cs b/FastGithub.DomainResolve/PersistenceService.cs new file mode 100644 index 00000000..7f844fc4 --- /dev/null +++ b/FastGithub.DomainResolve/PersistenceService.cs @@ -0,0 +1,121 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名持久化 + /// + 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 logger; + + + private record EndPointItem(string Host, int Port); + + [JsonSerializable(typeof(EndPointItem[]))] + [JsonSourceGenerationOptions( + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] + private partial class EndPointItemsContext : JsonSerializerContext + { + } + + + /// + /// 域名持久化 + /// + /// + /// + public PersistenceService( + FastGithubConfig fastGithubConfig, + ILogger logger) + { + this.fastGithubConfig = fastGithubConfig; + this.logger = logger; + } + + + /// + /// 读取保存的节点 + /// + /// + public IList ReadDnsEndPoints() + { + if (File.Exists(dataFile) == false) + { + return Array.Empty(); + } + + try + { + dataLocker.Wait(); + + var utf8Json = File.ReadAllBytes(dataFile); + var endPointItems = JsonSerializer.Deserialize(utf8Json, EndPointItemsContext.Default.EndPointItemArray); + if (endPointItems == null) + { + return Array.Empty(); + } + + var dnsEndPoints = new List(); + 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(); + } + finally + { + dataLocker.Release(); + } + } + + /// + /// 保存节点到文件 + /// + /// + /// + /// + public async Task WriteDnsEndPointsAsync(IEnumerable 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(); + } + } + } +} diff --git a/FastGithub.DomainResolve/ServiceCollectionExtensions.cs b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..525acfe1 --- /dev/null +++ b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +using FastGithub.DomainResolve; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace FastGithub +{ + /// + /// 服务注册扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 注册域名解析相关服务 + /// + /// + /// + public static IServiceCollection AddDomainResolve(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddHostedService(); + return services; + } + } +} diff --git a/FastGithub.DomainResolve/ServiceInstallUtil.cs b/FastGithub.DomainResolve/ServiceInstallUtil.cs new file mode 100644 index 00000000..417fb9ba --- /dev/null +++ b/FastGithub.DomainResolve/ServiceInstallUtil.cs @@ -0,0 +1,88 @@ +using System.IO; +using System.Runtime.Versioning; +using static PInvoke.AdvApi32; + +namespace FastGithub.DomainResolve +{ + public static class ServiceInstallUtil + { + /// + /// 安装并启动服务 + /// + /// + /// + /// + /// + [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); + } + } + + /// + /// 停止并删除服务 + /// + /// + /// + [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); + } + } +} diff --git a/FastGithub.DomainResolve/TomlUtil.cs b/FastGithub.DomainResolve/TomlUtil.cs new file mode 100644 index 00000000..5228f7aa --- /dev/null +++ b/FastGithub.DomainResolve/TomlUtil.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tommy; + +namespace FastGithub.DomainResolve +{ + /// + /// doml配置工具 + /// + static class TomlUtil + { + /// + /// 设置监听地址 + /// + /// + /// + /// + /// + public static Task SetListensAsync(string tomlPath, IPEndPoint endpoint, CancellationToken cancellationToken) + { + var value = new TomlArray + { + endpoint.ToString() + }; + return SetAsync(tomlPath, "listen_addresses", value, cancellationToken); + } + + /// + /// 设置日志等级 + /// + /// + /// + /// + /// + public static Task SetLogLevelAsync(string tomlPath, int logLevel, CancellationToken cancellationToken) + { + return SetAsync(tomlPath, "log_level", new TomlInteger { Value = logLevel }, cancellationToken); + } + + /// + /// 设置负载均衡模式 + /// + /// + /// + /// + /// + public static Task SetLBStrategyAsync(string tomlPath, string value, CancellationToken cancellationToken) + { + return SetAsync(tomlPath, "lb_strategy", new TomlString { Value = value }, cancellationToken); + } + + /// + /// 设置TTL + /// + /// + /// + /// + /// + /// + 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); + } + + /// + /// 设置指定键的值 + /// + /// + /// + /// + /// + /// + 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); + } + } +} diff --git a/FastGithub.FlowAnalyze/DelegatingDuplexPipe.cs b/FastGithub.FlowAnalyze/DelegatingDuplexPipe.cs new file mode 100644 index 00000000..8489b2d1 --- /dev/null +++ b/FastGithub.FlowAnalyze/DelegatingDuplexPipe.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace FastGithub.FlowAnalyze +{ + class DelegatingDuplexPipe : 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 delegatingStreamFactory) : + this(duplexPipe, new StreamPipeReaderOptions(leaveOpen: true), new StreamPipeWriterOptions(leaveOpen: true), delegatingStreamFactory) + { + } + + public DelegatingDuplexPipe(IDuplexPipe duplexPipe, StreamPipeReaderOptions readerOptions, StreamPipeWriterOptions writerOptions, Func 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(); + } + } +} \ No newline at end of file diff --git a/FastGithub.FlowAnalyze/DelegatingStream.cs b/FastGithub.FlowAnalyze/DelegatingStream.cs new file mode 100644 index 00000000..603ce07e --- /dev/null +++ b/FastGithub.FlowAnalyze/DelegatingStream.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.FlowAnalyze +{ + abstract class DelegatingStream : Stream + { + protected Stream Inner { get; } + + public DelegatingStream(Stream inner) + { + this.Inner = inner; + } + + public override bool CanRead + { + get + { + return this.Inner.CanRead; + } + } + + public override bool CanSeek + { + get + { + return this.Inner.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return this.Inner.CanWrite; + } + } + + public override long Length + { + get + { + return this.Inner.Length; + } + } + + public override long Position + { + get + { + return this.Inner.Position; + } + + set + { + this.Inner.Position = value; + } + } + + public override void Flush() + { + this.Inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return this.Inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return this.Inner.Read(buffer, offset, count); + } + + public override int Read(Span destination) + { + return this.Inner.Read(destination); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return this.Inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask ReadAsync(Memory 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 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 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(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); + } + } +} diff --git a/FastGithub.FlowAnalyze/DuplexPipeStreamExtensions.cs b/FastGithub.FlowAnalyze/DuplexPipeStreamExtensions.cs new file mode 100644 index 00000000..adebfa8a --- /dev/null +++ b/FastGithub.FlowAnalyze/DuplexPipeStreamExtensions.cs @@ -0,0 +1,175 @@ +using System; +using System.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.FlowAnalyze +{ + static class DuplexPipeStreamExtensions + { + public static Stream AsStream(this IDuplexPipe duplexPipe, bool throwOnCancelled = false) + { + return new DuplexPipeStream(duplexPipe, throwOnCancelled); + } + + private class DuplexPipeStream : Stream + { + private readonly PipeReader input; + private readonly PipeWriter output; + private readonly bool throwOnCancelled; + private volatile bool cancelCalled; + + public DuplexPipeStream(IDuplexPipe duplexPipe, bool throwOnCancelled = false) + { + this.input = duplexPipe.Input; + this.output = duplexPipe.Output; + this.throwOnCancelled = throwOnCancelled; + } + + public void CancelPendingRead() + { + this.cancelCalled = true; + this.input.CancelPendingRead(); + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValueTask vt = ReadAsyncInternal(new Memory(buffer, offset, count), default); + return vt.IsCompleted ? + vt.Result : + vt.AsTask().GetAwaiter().GetResult(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + return ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask ReadAsync(Memory 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 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 ReadAsyncInternal(Memory 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(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); + } + } + } +} diff --git a/FastGithub.FlowAnalyze/FastGithub.FlowAnalyze.csproj b/FastGithub.FlowAnalyze/FastGithub.FlowAnalyze.csproj new file mode 100644 index 00000000..65cbe964 --- /dev/null +++ b/FastGithub.FlowAnalyze/FastGithub.FlowAnalyze.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/FastGithub.FlowAnalyze/FlowAnalyzeDuplexPipe.cs b/FastGithub.FlowAnalyze/FlowAnalyzeDuplexPipe.cs new file mode 100644 index 00000000..15e38616 --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowAnalyzeDuplexPipe.cs @@ -0,0 +1,12 @@ +using System.IO.Pipelines; + +namespace FastGithub.FlowAnalyze +{ + sealed class FlowAnalyzeDuplexPipe : DelegatingDuplexPipe + { + public FlowAnalyzeDuplexPipe(IDuplexPipe duplexPipe, IFlowAnalyzer flowAnalyzer) : + base(duplexPipe, stream => new FlowAnalyzeStream(stream, flowAnalyzer)) + { + } + } +} diff --git a/FastGithub.FlowAnalyze/FlowAnalyzeStream.cs b/FastGithub.FlowAnalyze/FlowAnalyzeStream.cs new file mode 100644 index 00000000..b0a6fc5b --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowAnalyzeStream.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.FlowAnalyze +{ + sealed class FlowAnalyzeStream : DelegatingStream + { + private readonly IFlowAnalyzer flowAnalyzer; + + public FlowAnalyzeStream(Stream inner, IFlowAnalyzer flowAnalyzer) + : base(inner) + { + this.flowAnalyzer = flowAnalyzer; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = base.Read(buffer, offset, count); + this.flowAnalyzer.OnFlow(FlowType.Read, read); + return read; + } + + public override int Read(Span destination) + { + int read = base.Read(destination); + this.flowAnalyzer.OnFlow(FlowType.Read, read); + return read; + } + + public override async Task 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 ReadAsync(Memory 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 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 source, CancellationToken cancellationToken = default) + { + this.flowAnalyzer.OnFlow(FlowType.Wirte, source.Length); + return base.WriteAsync(source, cancellationToken); + } + } +} diff --git a/FastGithub.FlowAnalyze/FlowAnalyzer.cs b/FastGithub.FlowAnalyze/FlowAnalyzer.cs new file mode 100644 index 00000000..fd2b8d04 --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowAnalyzer.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; + +namespace FastGithub.FlowAnalyze +{ + sealed class FlowAnalyzer : IFlowAnalyzer + { + private const int INTERVAL_SECONDS = 5; + private readonly FlowQueues readQueues = new(INTERVAL_SECONDS); + private readonly FlowQueues writeQueues = new(INTERVAL_SECONDS); + + /// + /// 收到数据 + /// + /// + /// + public void OnFlow(FlowType flowType, int length) + { + if (flowType == FlowType.Read) + { + this.readQueues.OnFlow(length); + } + else + { + this.writeQueues.OnFlow(length); + } + } + + /// + /// 获取流量分析 + /// + /// + 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 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; + } + + /// + /// 清除无效记录 + /// + /// + 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; + } + } + } +} diff --git a/FastGithub.FlowAnalyze/FlowStatistics.cs b/FastGithub.FlowAnalyze/FlowStatistics.cs new file mode 100644 index 00000000..cab0a165 --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowStatistics.cs @@ -0,0 +1,28 @@ +namespace FastGithub.FlowAnalyze +{ + /// + /// 流量统计 + /// + public record FlowStatistics + { + /// + /// 获取总读上行 + /// + public long TotalRead { get; init; } + + /// + /// 获取总下行 + /// + public long TotalWrite { get; init; } + + /// + /// 获取读取速率 + /// + public double ReadRate { get; init; } + + /// + /// 获取写入速率 + /// + public double WriteRate { get; init; } + } +} diff --git a/FastGithub.FlowAnalyze/FlowStatisticsContext.cs b/FastGithub.FlowAnalyze/FlowStatisticsContext.cs new file mode 100644 index 00000000..18926e00 --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowStatisticsContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace FastGithub.FlowAnalyze +{ + [JsonSerializable(typeof(FlowStatistics))] + public partial class FlowStatisticsContext : JsonSerializerContext + { + } +} diff --git a/FastGithub.FlowAnalyze/FlowType.cs b/FastGithub.FlowAnalyze/FlowType.cs new file mode 100644 index 00000000..4ad7475a --- /dev/null +++ b/FastGithub.FlowAnalyze/FlowType.cs @@ -0,0 +1,18 @@ +namespace FastGithub.FlowAnalyze +{ + /// + /// 流量类型 + /// + public enum FlowType + { + /// + /// 读取 + /// + Read, + + /// + /// 写入 + /// + Wirte + } +} diff --git a/FastGithub.FlowAnalyze/IFlowAnalyzer.cs b/FastGithub.FlowAnalyze/IFlowAnalyzer.cs new file mode 100644 index 00000000..809969af --- /dev/null +++ b/FastGithub.FlowAnalyze/IFlowAnalyzer.cs @@ -0,0 +1,21 @@ +namespace FastGithub.FlowAnalyze +{ + /// + /// 流量分析器 + /// + public interface IFlowAnalyzer + { + /// + /// 收到数据 + /// + /// + /// + void OnFlow(FlowType flowType, int length); + + /// + /// 获取速率 + /// + /// + FlowStatistics GetFlowStatistics(); + } +} diff --git a/FastGithub.FlowAnalyze/ListenOptionsExtensions.cs b/FastGithub.FlowAnalyze/ListenOptionsExtensions.cs new file mode 100644 index 00000000..616e13d0 --- /dev/null +++ b/FastGithub.FlowAnalyze/ListenOptionsExtensions.cs @@ -0,0 +1,37 @@ +using FastGithub.FlowAnalyze; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; + +namespace FastGithub +{ + /// + /// ListenOptions扩展 + /// + public static class ListenOptionsExtensions + { + /// + /// 使用流量分析中间件 + /// + /// + /// + public static ListenOptions UseFlowAnalyze(this ListenOptions listen) + { + var flowAnalyzer = listen.ApplicationServices.GetRequiredService(); + 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; + } + } +} diff --git a/FastGithub.FlowAnalyze/ServiceCollectionExtensions.cs b/FastGithub.FlowAnalyze/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..9b04450e --- /dev/null +++ b/FastGithub.FlowAnalyze/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using FastGithub.FlowAnalyze; +using Microsoft.Extensions.DependencyInjection; + +namespace FastGithub +{ + /// + /// ServiceCollection扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 添加流量分析 + /// + /// + /// + public static IServiceCollection AddFlowAnalyze(this IServiceCollection services) + { + return services.AddSingleton(); + } + } +} diff --git a/FastGithub.FlowAnalyze/TaskToApm.cs b/FastGithub.FlowAnalyze/TaskToApm.cs new file mode 100644 index 00000000..99097912 --- /dev/null +++ b/FastGithub.FlowAnalyze/TaskToApm.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.FlowAnalyze +{ + static class TaskToApm + { + /// + /// Marshals the Task as an IAsyncResult, using the supplied callback and state + /// to implement the APM pattern. + /// + /// The Task to be marshaled. + /// The callback to be invoked upon completion. + /// The state to be stored in the IAsyncResult. + /// An IAsyncResult to represent the task's asynchronous operation. + public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) => + new TaskAsyncResult(task, state, callback); + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static void End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar) + { + twar._task.GetAwaiter().GetResult(); + return; + } + + throw new ArgumentNullException(); + } + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static TResult End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar && twar._task is Task task) + { + return task.GetAwaiter().GetResult(); + } + + throw new ArgumentNullException(); + } + + /// Provides a simple IAsyncResult that wraps a Task. + /// + /// 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. + /// + internal sealed class TaskAsyncResult : IAsyncResult + { + /// The wrapped Task. + internal readonly Task _task; + /// Callback to invoke when the wrapped task completes. + private readonly AsyncCallback? _callback; + + /// Initializes the IAsyncResult with the Task to wrap and the associated object state. + /// The Task to wrap. + /// The new AsyncState value. + /// Callback to invoke when the wrapped task completes. + 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 + } + } + + /// Invokes the callback. + private void InvokeCallback() + { + Debug.Assert(!CompletedSynchronously); + Debug.Assert(_callback != null); + _callback.Invoke(this); + } + + /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. + public object? AsyncState { get; } + /// Gets a value that indicates whether the asynchronous operation completed synchronously. + /// This is set lazily based on whether the has completed by the time this object is created. + public bool CompletedSynchronously { get; } + /// Gets a value that indicates whether the asynchronous operation has completed. + public bool IsCompleted => _task.IsCompleted; + /// Gets a that is used to wait for an asynchronous operation to complete. + public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; + } + } +} \ No newline at end of file diff --git a/FastGithub.Http/FastGithub.Http.csproj b/FastGithub.Http/FastGithub.Http.csproj new file mode 100644 index 00000000..e621b9ac --- /dev/null +++ b/FastGithub.Http/FastGithub.Http.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/FastGithub.Http/HttpClient.cs b/FastGithub.Http/HttpClient.cs new file mode 100644 index 00000000..7eb5232f --- /dev/null +++ b/FastGithub.Http/HttpClient.cs @@ -0,0 +1,58 @@ +using FastGithub.Configuration; +using FastGithub.DomainResolve; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.Http +{ + /// + /// 表示http客户端 + /// + public class HttpClient : HttpMessageInvoker + { + /// + /// 插入的UserAgent标记 + /// + private readonly static ProductInfoHeaderValue userAgent = new(new ProductHeaderValue(nameof(FastGithub), "1.0")); + + /// + /// http客户端 + /// + /// + /// + public HttpClient(DomainConfig domainConfig, IDomainResolver domainResolver) + : this(new HttpClientHandler(domainConfig, domainResolver), disposeHandler: true) + { + } + + /// + /// http客户端 + /// + /// + /// + public HttpClient(HttpMessageHandler handler, bool disposeHandler) + : base(handler, disposeHandler) + { + } + + /// + /// 发送请求 + /// + /// + /// + /// + public override async Task 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; + } + } +} \ No newline at end of file diff --git a/FastGithub.Http/HttpClientFactory.cs b/FastGithub.Http/HttpClientFactory.cs new file mode 100644 index 00000000..76be907a --- /dev/null +++ b/FastGithub.Http/HttpClientFactory.cs @@ -0,0 +1,90 @@ +using FastGithub.Configuration; +using FastGithub.DomainResolve; +using System; +using System.Collections.Concurrent; + +namespace FastGithub.Http +{ + /// + /// HttpClient工厂 + /// + sealed class HttpClientFactory : IHttpClientFactory + { + private readonly IDomainResolver domainResolver; + + /// + /// 首次生命周期 + /// + private readonly TimeSpan firstLiftTime = TimeSpan.FromSeconds(10d); + + /// + /// 非首次生命周期 + /// + private readonly TimeSpan nextLifeTime = TimeSpan.FromSeconds(100d); + + /// + /// LifetimeHttpHandler清理器 + /// + private readonly LifetimeHttpHandlerCleaner httpHandlerCleaner = new(); + + /// + /// LazyOf(LifetimeHttpHandler)缓存 + /// + private readonly ConcurrentDictionary> httpHandlerLazyCache = new(); + + + /// + /// HttpClient工厂 + /// + /// + public HttpClientFactory(IDomainResolver domainResolver) + { + this.domainResolver = domainResolver; + } + + /// + /// 创建httpClient + /// + /// + /// + /// + 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 CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey) + { + return new Lazy(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.firstLiftTime), true); + } + } + + /// + /// 创建LifetimeHttpHandler + /// + /// + /// + /// + private LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime) + { + return new LifetimeHttpHandler(this.domainResolver, lifeTimeKey, lifeTime, this.OnLifetimeHttpHandlerDeactivate); + } + + /// + /// 当有httpHandler失效时 + /// + /// httpHandler + private void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler) + { + var lifeTimeKey = lifetimeHttpHandler.LifeTimeKey; + this.httpHandlerLazyCache[lifeTimeKey] = CreateLifetimeHttpHandlerLazy(lifeTimeKey); + this.httpHandlerCleaner.Add(lifetimeHttpHandler); + + Lazy CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey) + { + return new Lazy(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.nextLifeTime), true); + } + } + } +} \ No newline at end of file diff --git a/FastGithub.Http/HttpClientHandler.cs b/FastGithub.Http/HttpClientHandler.cs new file mode 100644 index 00000000..61310bb5 --- /dev/null +++ b/FastGithub.Http/HttpClientHandler.cs @@ -0,0 +1,236 @@ +using FastGithub.Configuration; +using FastGithub.DomainResolve; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.Http +{ + /// + /// HttpClientHandler + /// + class HttpClientHandler : DelegatingHandler + { + private readonly DomainConfig domainConfig; + private readonly IDomainResolver domainResolver; + private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d); + + /// + /// HttpClientHandler + /// + /// + /// + public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver) + { + this.domainConfig = domainConfig; + this.domainResolver = domainResolver; + this.InnerHandler = this.CreateSocketsHttpHandler(); + } + + /// + /// 发送请求 + /// + /// + /// + /// + protected override async Task 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); + } + + /// + /// 创建转发代理的httpHandler + /// + /// + private SocketsHttpHandler CreateSocketsHttpHandler() + { + return new SocketsHttpHandler + { + Proxy = null, + UseProxy = false, + UseCookies = false, + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.None, + ConnectCallback = this.ConnectCallback + }; + } + + /// + /// 连接回调 + /// + /// + /// + /// + private async ValueTask ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + var innerExceptions = new List(); + 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); + } + + /// + /// 建立连接 + /// + /// + /// + /// + /// + private async ValueTask 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; + } + } + + /// + /// 解析为IPEndPoint + /// + /// + /// + /// + private async IAsyncEnumerable 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); + } + } + } + + /// + /// 读取使用的DNS名称 + /// + /// + /// + private static IEnumerable ReadDnsNames(X509Certificate? cert) + { + if (cert is X509Certificate2 x509) + { + var extension = x509.Extensions.OfType().FirstOrDefault(); + if (extension != null) + { + return extension.EnumerateDnsNames(); + } + } + return Array.Empty(); + } + + /// + /// 比较域名 + /// + /// + /// + /// + 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; + } + } +} diff --git a/FastGithub.Http/HttpConnectTimeoutException.cs b/FastGithub.Http/HttpConnectTimeoutException.cs new file mode 100644 index 00000000..8c695286 --- /dev/null +++ b/FastGithub.Http/HttpConnectTimeoutException.cs @@ -0,0 +1,21 @@ +using System; +using System.Net; + +namespace FastGithub.Http +{ + /// + /// http连接超时异常 + /// + sealed class HttpConnectTimeoutException : Exception + { + /// + /// http连接超时异常 + /// + /// 连接的ip + public HttpConnectTimeoutException(IPAddress address) + : base(address.ToString()) + { + + } + } +} diff --git a/FastGithub.Http/IHttpClientFactory.cs b/FastGithub.Http/IHttpClientFactory.cs new file mode 100644 index 00000000..77b6ef6f --- /dev/null +++ b/FastGithub.Http/IHttpClientFactory.cs @@ -0,0 +1,18 @@ +using FastGithub.Configuration; + +namespace FastGithub.Http +{ + /// + /// httpClient工厂 + /// + public interface IHttpClientFactory + { + /// + /// 创建httpClient + /// + /// + /// + /// + HttpClient CreateHttpClient(string domain, DomainConfig domainConfig); + } +} diff --git a/FastGithub.Http/LifeTimeKey.cs b/FastGithub.Http/LifeTimeKey.cs new file mode 100644 index 00000000..11ad5f40 --- /dev/null +++ b/FastGithub.Http/LifeTimeKey.cs @@ -0,0 +1,31 @@ +using FastGithub.Configuration; + +namespace FastGithub.Http +{ + /// + /// 生命周期的Key + /// + record LifeTimeKey + { + /// + /// 域名 + /// + public string Domain { get; } + + /// + /// 域名配置 + /// + public DomainConfig DomainConfig { get; } + + /// + /// 生命周期的Key + /// + /// + /// + public LifeTimeKey(string domain, DomainConfig domainConfig) + { + this.Domain = domain; + this.DomainConfig = domainConfig; + } + } +} diff --git a/FastGithub.Http/LifetimeHttpHandler.cs b/FastGithub.Http/LifetimeHttpHandler.cs new file mode 100644 index 00000000..a3fb0c6d --- /dev/null +++ b/FastGithub.Http/LifetimeHttpHandler.cs @@ -0,0 +1,49 @@ +using FastGithub.DomainResolve; +using System; +using System.Net.Http; +using System.Threading; + +namespace FastGithub.Http +{ + /// + /// 表示自主管理生命周期的的HttpMessageHandler + /// + sealed class LifetimeHttpHandler : DelegatingHandler + { + private readonly Timer timer; + + public LifeTimeKey LifeTimeKey { get; } + + /// + /// 具有生命周期的HttpHandler + /// + /// + /// + /// + /// + public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action deactivateAction) + { + this.LifeTimeKey = lifeTimeKey; + this.InnerHandler = new HttpClientHandler(lifeTimeKey.DomainConfig, domainResolver); + this.timer = new Timer(this.OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan); + } + + /// + /// timer触发时 + /// + /// + private void OnTimerCallback(object? state) + { + this.timer.Dispose(); + ((Action)(state!))(this); + } + + /// + /// 这里不释放资源 + /// + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/FastGithub.Http/LifetimeHttpHandlerCleaner.cs b/FastGithub.Http/LifetimeHttpHandlerCleaner.cs new file mode 100644 index 00000000..94e72f2a --- /dev/null +++ b/FastGithub.Http/LifetimeHttpHandlerCleaner.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.Http +{ + /// + /// 表示LifetimeHttpHandler清理器 + /// + sealed class LifetimeHttpHandlerCleaner + { + /// + /// 当前监视生命周期的记录的数量 + /// + private int trackingEntryCount = 0; + + /// + /// 监视生命周期的记录队列 + /// + private readonly ConcurrentQueue trackingEntries = new(); + + /// + /// 获取或设置清理的时间间隔 + /// 默认10s + /// + public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromSeconds(10d); + + /// + /// 添加要清除的httpHandler + /// + /// httpHandler + 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(); + } + } + + /// + /// 启动清理作业 + /// + private async void StartCleanup() + { + await Task.Yield(); + while (this.Cleanup() == false) + { + await Task.Delay(this.CleanupInterval); + } + } + + /// + /// 清理失效的拦截器 + /// 返回是否完全清理 + /// + /// + 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; + } + + + /// + /// 表示监视生命周期的记录 + /// + private class TrackingEntry : IDisposable + { + /// + /// 用于释放资源的对象 + /// + private readonly IDisposable disposable; + + /// + /// 监视对象的弱引用 + /// + private readonly WeakReference weakReference; + + /// + /// 获取是否可以释放资源 + /// + /// + public bool CanDispose => this.weakReference.IsAlive == false; + + /// + /// 监视生命周期的记录 + /// + /// 激活状态的httpHandler + public TrackingEntry(LifetimeHttpHandler handler) + { + this.disposable = handler.InnerHandler!; + this.weakReference = new WeakReference(handler); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + try + { + this.disposable.Dispose(); + } + catch (Exception) + { + } + } + } + } +} diff --git a/FastGithub.Http/RequestContext.cs b/FastGithub.Http/RequestContext.cs new file mode 100644 index 00000000..d4a89c2a --- /dev/null +++ b/FastGithub.Http/RequestContext.cs @@ -0,0 +1,31 @@ +using FastGithub.Configuration; + +namespace FastGithub.Http +{ + /// + /// 表示请求上下文 + /// + sealed class RequestContext + { + /// + /// 获取或设置是否为https请求 + /// + public bool IsHttps { get; } + + /// + /// 获取或设置Sni值 + /// + public TlsSniPattern TlsSniValue { get; } + + /// + /// 请求上下文 + /// + /// + /// + public RequestContext(bool isHttps, TlsSniPattern tlsSniValue) + { + IsHttps = isHttps; + TlsSniValue = tlsSniValue; + } + } +} diff --git a/FastGithub.Http/RequestContextExtensions.cs b/FastGithub.Http/RequestContextExtensions.cs new file mode 100644 index 00000000..5388ca65 --- /dev/null +++ b/FastGithub.Http/RequestContextExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; + +namespace FastGithub.Http +{ + /// + /// 请求上下文扩展 + /// + static class RequestContextExtensions + { + private static readonly HttpRequestOptionsKey key = new(nameof(RequestContext)); + + /// + /// 设置RequestContext + /// + /// + /// + public static void SetRequestContext(this HttpRequestMessage httpRequestMessage, RequestContext requestContext) + { + httpRequestMessage.Options.Set(key, requestContext); + } + + /// + /// 获取RequestContext + /// + /// + /// + public static RequestContext GetRequestContext(this HttpRequestMessage httpRequestMessage) + { + return httpRequestMessage.Options.TryGetValue(key, out var requestContext) + ? requestContext + : throw new InvalidOperationException($"请先调用{nameof(SetRequestContext)}"); + } + } +} diff --git a/FastGithub.Http/ServiceCollectionExtensions.cs b/FastGithub.Http/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b0981787 --- /dev/null +++ b/FastGithub.Http/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using FastGithub.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace FastGithub +{ + /// + /// 服务注册扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 添加HttpClient相关服务 + /// + /// + /// + public static IServiceCollection AddHttpClient(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } + } +} diff --git a/FastGithub.HttpServer/ApplicationBuilderExtensions.cs b/FastGithub.HttpServer/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..ada408fe --- /dev/null +++ b/FastGithub.HttpServer/ApplicationBuilderExtensions.cs @@ -0,0 +1,63 @@ +using FastGithub.HttpServer.HttpMiddlewares; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace FastGithub +{ + /// + /// ApplicationBuilder扩展 + /// + public static class ApplicationBuilderExtensions + { + /// + /// 使用http代理策略中间件 + /// + /// + /// + public static IApplicationBuilder UseHttpProxyPac(this IApplicationBuilder app) + { + var middleware = app.ApplicationServices.GetRequiredService(); + return app.Use(next => context => middleware.InvokeAsync(context, next)); + } + + /// + /// 使用请求日志中间件 + /// + /// + /// + public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app) + { + var middleware = app.ApplicationServices.GetRequiredService(); + return app.Use(next => context => middleware.InvokeAsync(context, next)); + } + + /// + /// 禁用请求日志中间件 + /// + /// + /// + public static IApplicationBuilder DisableRequestLogging(this IApplicationBuilder app) + { + return app.Use(next => context => + { + var loggingFeature = context.Features.Get(); + if (loggingFeature != null) + { + loggingFeature.Enable = false; + } + return next(context); + }); + } + + /// + /// 使用反向代理中间件 + /// + /// + /// + public static IApplicationBuilder UseHttpReverseProxy(this IApplicationBuilder app) + { + var middleware = app.ApplicationServices.GetRequiredService(); + return app.Use(next => context => middleware.InvokeAsync(context, next)); + } + } +} diff --git a/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinux.cs b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinux.cs new file mode 100644 index 00000000..d12017cc --- /dev/null +++ b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinux.cs @@ -0,0 +1,78 @@ +using FastGithub; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace FastGithub.HttpServer.Certs.CaCertInstallers +{ + abstract class CaCertInstallerOfLinux : ICaCertInstaller + { + private readonly ILogger logger; + + /// + /// 更新工具文件名 + /// + protected abstract string CaCertUpdatePath { get; } + + /// + /// 证书根目录 + /// + protected abstract string CaCertStorePath { get; } + + [DllImport("libc", SetLastError = true)] + private static extern uint geteuid(); + + public CaCertInstallerOfLinux(ILogger logger) + { + this.logger = logger; + } + + /// + /// 是否支持 + /// + /// + public bool IsSupported() + { + return OperatingSystem.IsLinux() && File.Exists(CaCertUpdatePath); + } + + /// + /// 安装ca证书 + /// + /// 证书文件路径 + 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证书异常"); + } + } + } +} \ No newline at end of file diff --git a/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxDebian.cs b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxDebian.cs new file mode 100644 index 00000000..5e74ae6d --- /dev/null +++ b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxDebian.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Logging; + +namespace FastGithub.HttpServer.Certs.CaCertInstallers +{ + sealed class CaCertInstallerOfLinuxDebian : CaCertInstallerOfLinux + { + protected override string CaCertUpdatePath => "/usr/sbin/update-ca-certificates"; + + protected override string CaCertStorePath => "/usr/local/share/ca-certificates"; + + public CaCertInstallerOfLinuxDebian(ILogger logger) + : base(logger) + { + } + } +} \ No newline at end of file diff --git a/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxRedHat.cs b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxRedHat.cs new file mode 100644 index 00000000..4fad5c43 --- /dev/null +++ b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfLinuxRedHat.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Logging; + +namespace FastGithub.HttpServer.Certs.CaCertInstallers +{ + sealed class CaCertInstallerOfLinuxRedHat : CaCertInstallerOfLinux + { + protected override string CaCertUpdatePath => "/usr/bin/update-ca-trust"; + + protected override string CaCertStorePath => "/etc/pki/ca-trust/source/anchors"; + + public CaCertInstallerOfLinuxRedHat(ILogger logger) + : base(logger) + { + } + } +} \ No newline at end of file diff --git a/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfMacOS.cs b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfMacOS.cs new file mode 100644 index 00000000..23dcad83 --- /dev/null +++ b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfMacOS.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace FastGithub.HttpServer.Certs.CaCertInstallers +{ + sealed class CaCertInstallerOfMacOS : ICaCertInstaller + { + private readonly ILogger logger; + + public CaCertInstallerOfMacOS(ILogger logger) + { + this.logger = logger; + } + + /// + /// 是否支持 + /// + /// + public bool IsSupported() + { + return OperatingSystem.IsMacOS(); + } + + /// + /// 安装ca证书 + /// + /// 证书文件路径 + public void Install(string caCertFilePath) + { + logger.LogWarning($"请手动安装CA证书然后设置信任CA证书{caCertFilePath}"); + } + } +} diff --git a/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfWindows.cs b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfWindows.cs new file mode 100644 index 00000000..72eb9567 --- /dev/null +++ b/FastGithub.HttpServer/Certs/CaCertInstallers/CaCertInstallerOfWindows.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Security.Cryptography.X509Certificates; + +namespace FastGithub.HttpServer.Certs.CaCertInstallers +{ + sealed class CaCertInstallerOfWindows : ICaCertInstaller + { + private readonly ILogger logger; + + public CaCertInstallerOfWindows(ILogger logger) + { + this.logger = logger; + } + + /// + /// 是否支持 + /// + /// + public bool IsSupported() + { + return OperatingSystem.IsWindows(); + } + + /// + /// 安装ca证书 + /// + /// 证书文件路径 + 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}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”"); + } + } + } +} diff --git a/FastGithub.HttpServer/Certs/CertGenerator.cs b/FastGithub.HttpServer/Certs/CertGenerator.cs new file mode 100644 index 00000000..8bb00ff9 --- /dev/null +++ b/FastGithub.HttpServer/Certs/CertGenerator.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace FastGithub.HttpServer.Certs +{ + /// + /// 证书生成器 + /// + 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"); + + /// + /// 生成ca证书 + /// + /// + /// + /// + /// + /// + /// + 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); + } + + /// + /// 生成服务器证书 + /// + /// + /// + /// + /// + /// + /// + /// + public static X509Certificate2 CreateEndCertificate( + X509Certificate2 issuerCertificate, + X500DistinguishedName subjectName, + IEnumerable? 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 + } + } +} diff --git a/FastGithub.HttpServer/Certs/CertService.cs b/FastGithub.HttpServer/Certs/CertService.cs new file mode 100644 index 00000000..9ae1abe9 --- /dev/null +++ b/FastGithub.HttpServer/Certs/CertService.cs @@ -0,0 +1,172 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace FastGithub.HttpServer.Certs +{ + /// + /// 证书服务 + /// + sealed class CertService + { + private const string CACERT_PATH = "cacert"; + private readonly IMemoryCache serverCertCache; + private readonly IEnumerable certInstallers; + private readonly ILogger logger; + private X509Certificate2? caCert; + + + /// + /// 获取证书文件路径 + /// + public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/fastgithub.crt" : $"{CACERT_PATH}/fastgithub.cer"; + + /// + /// 获取私钥文件路径 + /// + public string CaKeyFilePath { get; } = $"{CACERT_PATH}/fastgithub.key"; + + /// + /// 证书服务 + /// + /// + /// + /// + public CertService( + IMemoryCache serverCertCache, + IEnumerable certInstallers, + ILogger logger) + { + this.serverCertCache = serverCertCache; + this.certInstallers = certInstallers; + this.logger = logger; + Directory.CreateDirectory(CACERT_PATH); + } + + /// + /// 生成CA证书 + /// + 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; + } + + /// + /// 安装和信任CA证书 + /// + 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); + } + + /// + /// 设置ssl验证 + /// + /// 是否验证 + /// + 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; + } + } + + /// + /// 获取颁发给指定域名的证书 + /// + /// + /// + 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)); + } + } + + /// + /// 获取域名 + /// + /// + /// + private static IEnumerable GetExtraDomains() + { + yield return Environment.MachineName; + yield return IPAddress.Loopback.ToString(); + yield return IPAddress.IPv6Loopback.ToString(); + } + } +} diff --git a/FastGithub.HttpServer/Certs/ICaCertInstaller.cs b/FastGithub.HttpServer/Certs/ICaCertInstaller.cs new file mode 100644 index 00000000..16f8fd63 --- /dev/null +++ b/FastGithub.HttpServer/Certs/ICaCertInstaller.cs @@ -0,0 +1,20 @@ +namespace FastGithub.HttpServer.Certs +{ + /// + /// CA证书安装器 + /// + interface ICaCertInstaller + { + /// + /// 是否支持 + /// + /// + bool IsSupported(); + + /// + /// 安装ca证书 + /// + /// 证书文件路径 + void Install(string caCertFilePath); + } +} diff --git a/FastGithub.HttpServer/FastGithub.HttpServer.csproj b/FastGithub.HttpServer/FastGithub.HttpServer.csproj new file mode 100644 index 00000000..339d9569 --- /dev/null +++ b/FastGithub.HttpServer/FastGithub.HttpServer.csproj @@ -0,0 +1,17 @@ + + + + true + + + + + + + + + + + + + diff --git a/FastGithub.HttpServer/HttpMiddlewares/HttpProxyPacMiddleware.cs b/FastGithub.HttpServer/HttpMiddlewares/HttpProxyPacMiddleware.cs new file mode 100644 index 00000000..3bf01b42 --- /dev/null +++ b/FastGithub.HttpServer/HttpMiddlewares/HttpProxyPacMiddleware.cs @@ -0,0 +1,68 @@ +using FastGithub.Configuration; +using FastGithub.HttpServer.TcpMiddlewares; +using Microsoft.AspNetCore.Http; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.HttpMiddlewares +{ + /// + /// http代理策略中间件 + /// + sealed class HttpProxyPacMiddleware + { + private readonly FastGithubConfig fastGithubConfig; + + /// + /// http代理策略中间件 + /// + /// + public HttpProxyPacMiddleware(FastGithubConfig fastGithubConfig) + { + this.fastGithubConfig = fastGithubConfig; + } + + /// + /// 处理请求 + /// + /// + /// + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + // http请求经过了httpProxy中间件 + var proxyFeature = context.Features.Get(); + 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); + } + } + + /// + /// 创建proxypac脚本 + /// + /// + /// + 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(); + } + } +} \ No newline at end of file diff --git a/FastGithub.HttpServer/HttpMiddlewares/HttpReverseProxyMiddleware.cs b/FastGithub.HttpServer/HttpMiddlewares/HttpReverseProxyMiddleware.cs new file mode 100644 index 00000000..d39ada95 --- /dev/null +++ b/FastGithub.HttpServer/HttpMiddlewares/HttpReverseProxyMiddleware.cs @@ -0,0 +1,140 @@ +using FastGithub.Configuration; +using FastGithub.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Threading.Tasks; +using Yarp.ReverseProxy.Forwarder; + +namespace FastGithub.HttpServer.HttpMiddlewares +{ + /// + /// 反向代理中间件 + /// + 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 logger; + + public HttpReverseProxyMiddleware( + IHttpForwarder httpForwarder, + IHttpClientFactory httpClientFactory, + FastGithubConfig fastGithubConfig, + ILogger logger) + { + this.httpForwarder = httpForwarder; + this.httpClientFactory = httpClientFactory; + this.fastGithubConfig = fastGithubConfig; + this.logger = logger; + } + + /// + /// 处理请求 + /// + /// + /// + 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); + } + } + } + + /// + /// 获取域名的DomainConfig + /// + /// + /// + /// + 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('.'); + } + } + + /// + /// 获取目标前缀 + /// + /// + /// + /// + /// + 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; + } + + /// + /// 处理错误信息 + /// + /// + /// + /// + 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 + }); + } + } +} diff --git a/FastGithub.HttpServer/HttpMiddlewares/IRequestLoggingFeature.cs b/FastGithub.HttpServer/HttpMiddlewares/IRequestLoggingFeature.cs new file mode 100644 index 00000000..c23e6fec --- /dev/null +++ b/FastGithub.HttpServer/HttpMiddlewares/IRequestLoggingFeature.cs @@ -0,0 +1,13 @@ +namespace FastGithub.HttpServer.HttpMiddlewares +{ + /// + /// 请求日志特性 + /// + public interface IRequestLoggingFeature + { + /// + /// 是否启用 + /// + bool Enable { get; set; } + } +} diff --git a/FastGithub.HttpServer/HttpMiddlewares/RequestLoggingMilldeware.cs b/FastGithub.HttpServer/HttpMiddlewares/RequestLoggingMilldeware.cs new file mode 100644 index 00000000..9959d8ac --- /dev/null +++ b/FastGithub.HttpServer/HttpMiddlewares/RequestLoggingMilldeware.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.HttpMiddlewares +{ + /// + /// 请求日志中间件 + /// + sealed class RequestLoggingMiddleware + { + private readonly ILogger logger; + + /// + /// 请求日志中间件 + /// + /// + public RequestLoggingMiddleware(ILogger logger) + { + this.logger = logger; + } + + /// + /// 执行请求 + /// + /// + /// + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var feature = new RequestLoggingFeature(); + context.Features.Set(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)}"); + } + } + + /// + /// 是否为错误 + /// + /// + /// + private static bool IsError(Exception exception) + { + if (exception is OperationCanceledException) + { + return false; + } + + if (HasInnerException(exception)) + { + return false; + } + + return true; + } + + /// + /// 是否有内部异常异常 + /// + /// + /// + /// + private static bool HasInnerException(Exception exception) where TInnerException : Exception + { + var inner = exception.InnerException; + while (inner != null) + { + if (inner is TInnerException) + { + return true; + } + inner = inner.InnerException; + } + return false; + } + + /// + /// 获取异常信息 + /// + /// + /// + 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; + } + } +} diff --git a/FastGithub.HttpServer/KestrelServerExtensions.cs b/FastGithub.HttpServer/KestrelServerExtensions.cs new file mode 100644 index 00000000..b8ba8540 --- /dev/null +++ b/FastGithub.HttpServer/KestrelServerExtensions.cs @@ -0,0 +1,185 @@ +using FastGithub.Configuration; +using FastGithub.HttpServer.Certs; +using FastGithub.HttpServer.TcpMiddlewares; +using FastGithub.HttpServer.TlsMiddlewares; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace FastGithub +{ + /// + /// Kestrel扩展 + /// + public static class KestrelServerExtensions + { + /// + /// 无限制 + /// + /// + public static void NoLimit(this KestrelServerOptions kestrel) + { + kestrel.Limits.MaxRequestBodySize = null; + kestrel.Limits.MinResponseDataRate = null; + kestrel.Limits.MinRequestBodyDataRate = null; + } + + /// + /// 监听http代理 + /// + /// + public static void ListenHttpProxy(this KestrelServerOptions kestrel) + { + var options = kestrel.ApplicationServices.GetRequiredService>().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(); + var tunnelMiddleware = kestrel.ApplicationServices.GetRequiredService(); + + 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代理服务启动完成"); + } + + /// + /// 监听ssh协议代理 + /// + /// + public static void ListenSshReverseProxy(this KestrelServerOptions kestrel) + { + var sshPort = GlobalListener.SshPort; + kestrel.ListenLocalhost(sshPort, listen => + { + listen.UseFlowAnalyze(); + listen.UseConnectionHandler(); + }); + + kestrel.GetLogger().LogInformation($"已监听ssh://localhost:{sshPort},github的ssh反向代理服务启动完成"); + } + + /// + /// 监听git协议代理代理 + /// + /// + public static void ListenGitReverseProxy(this KestrelServerOptions kestrel) + { + var gitPort = GlobalListener.GitPort; + kestrel.ListenLocalhost(gitPort, listen => + { + listen.UseFlowAnalyze(); + listen.UseConnectionHandler(); + }); + + kestrel.GetLogger().LogInformation($"已监听git://localhost:{gitPort},github的git反向代理服务启动完成"); + } + + /// + /// 监听http反向代理 + /// + /// + public static void ListenHttpReverseProxy(this KestrelServerOptions kestrel) + { + var httpPort = GlobalListener.HttpPort; + kestrel.ListenLocalhost(httpPort); + + if (OperatingSystem.IsWindows()) + { + kestrel.GetLogger().LogInformation($"已监听http://localhost:{httpPort},http反向代理服务启动完成"); + } + } + + /// + /// 监听https反向代理 + /// + /// + /// + 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反向代理服务启动完成"); + } + } + + /// + /// 获取日志 + /// + /// + /// + private static ILogger GetLogger(this KestrelServerOptions kestrel) + { + var loggerFactory = kestrel.ApplicationServices.GetRequiredService(); + return loggerFactory.CreateLogger($"{nameof(FastGithub)}.{nameof(HttpServer)}"); + } + + /// + /// 使用Tls中间件 + /// + /// + /// https配置 + /// + public static ListenOptions UseTls(this ListenOptions listen) + { + var certService = listen.ApplicationServices.GetRequiredService(); + certService.CreateCaCertIfNotExists(); + certService.InstallAndTrustCaCert(); + return listen.UseTls(domain => certService.GetOrCreateServerCert(domain)); + } + + /// + /// 使用Tls中间件 + /// + /// + /// https配置 + /// + private static ListenOptions UseTls(this ListenOptions listen, Func certFactory) + { + var invadeMiddleware = listen.ApplicationServices.GetRequiredService(); + var restoreMiddleware = listen.ApplicationServices.GetRequiredService(); + + 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; + } + } +} diff --git a/FastGithub.HttpServer/ServiceCollectionExtensions.cs b/FastGithub.HttpServer/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..162786e2 --- /dev/null +++ b/FastGithub.HttpServer/ServiceCollectionExtensions.cs @@ -0,0 +1,44 @@ +using FastGithub.HttpServer.Certs; +using FastGithub.HttpServer.Certs.CaCertInstallers; +using FastGithub.HttpServer.HttpMiddlewares; +using FastGithub.HttpServer.TcpMiddlewares; +using FastGithub.HttpServer.TlsMiddlewares; +using Microsoft.Extensions.DependencyInjection; +namespace FastGithub +{ + /// + /// http反向代理的服务注册扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 添加http反向代理 + /// + /// + /// + public static IServiceCollection AddReverseProxy(this IServiceCollection services) + { + return services + .AddMemoryCache() + .AddHttpForwarder() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + + // tcp + .AddSingleton() + .AddSingleton() + + // tls + .AddSingleton() + .AddSingleton() + + // http + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/GithubGitReverseProxyHandler.cs b/FastGithub.HttpServer/TcpMiddlewares/GithubGitReverseProxyHandler.cs new file mode 100644 index 00000000..f725a554 --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/GithubGitReverseProxyHandler.cs @@ -0,0 +1,19 @@ +using FastGithub.DomainResolve; + +namespace FastGithub.HttpServer.TcpMiddlewares +{ + /// + /// github的git代理处理者 + /// + sealed class GithubGitReverseProxyHandler : TcpReverseProxyHandler + { + /// + /// github的git代理处理者 + /// + /// + public GithubGitReverseProxyHandler(IDomainResolver domainResolver) + : base(domainResolver, new("github.com", 9418)) + { + } + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/GithubSshReverseProxyHandler.cs b/FastGithub.HttpServer/TcpMiddlewares/GithubSshReverseProxyHandler.cs new file mode 100644 index 00000000..beb80cdc --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/GithubSshReverseProxyHandler.cs @@ -0,0 +1,19 @@ +using FastGithub.DomainResolve; + +namespace FastGithub.HttpServer.TcpMiddlewares +{ + /// + /// github的ssh代理处理者 + /// + sealed class GithubSshReverseProxyHandler : TcpReverseProxyHandler + { + /// + /// github的ssh代理处理者 + /// + /// + public GithubSshReverseProxyHandler(IDomainResolver domainResolver) + : base(domainResolver, new("github.com", 22)) + { + } + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/HttpProxyMiddleware.cs b/FastGithub.HttpServer/TcpMiddlewares/HttpProxyMiddleware.cs new file mode 100644 index 00000000..98df5c3f --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/HttpProxyMiddleware.cs @@ -0,0 +1,156 @@ +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 +{ + /// + /// 正向代理中间件 + /// + sealed class HttpProxyMiddleware + { + private readonly HttpParser 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"); + + /// + /// 执行中间件 + /// + /// + /// + /// + public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context) + { + var input = context.Transport.Input; + var output = context.Transport.Output; + var request = new HttpRequestHandler(); + + while (context.ConnectionClosed.IsCancellationRequested == false) + { + var result = await input.ReadAsync(); + if (result.IsCanceled) + { + break; + } + + try + { + if (this.ParseRequest(result, request, out var consumed)) + { + if (request.ProxyProtocol == ProxyProtocol.TunnelProxy) + { + input.AdvanceTo(consumed); + await output.WriteAsync(this.http200, context.ConnectionClosed); + } + else + { + input.AdvanceTo(result.Buffer.Start); + } + + context.Features.Set(request); + await next(context); + + break; + } + else + { + input.AdvanceTo(result.Buffer.Start, result.Buffer.End); + } + + if (result.IsCompleted) + { + break; + } + } + catch (Exception) + { + await output.WriteAsync(this.http400, context.ConnectionClosed); + break; + } + } + } + + + /// + /// 解析http请求 + /// + /// + /// + /// + /// + private bool ParseRequest(ReadResult result, HttpRequestHandler request, out SequencePosition consumed) + { + var reader = new SequenceReader(result.Buffer); + if (this.httpParser.ParseRequestLine(request, ref reader) && + this.httpParser.ParseHeaders(request, ref reader)) + { + consumed = reader.Position; + return true; + } + else + { + consumed = default; + return false; + } + } + + + /// + /// 代理请求处理器 + /// + 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 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 name, ReadOnlySpan value) + { + } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) + { + } + void IHttpHeadersHandler.OnStaticIndexedHeader(int index) + { + } + void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + } + } + } +} \ No newline at end of file diff --git a/FastGithub.HttpServer/TcpMiddlewares/IHttpProxyFeature.cs b/FastGithub.HttpServer/TcpMiddlewares/IHttpProxyFeature.cs new file mode 100644 index 00000000..1b4e9c6e --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/IHttpProxyFeature.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; + +namespace FastGithub.HttpServer.TcpMiddlewares +{ + interface IHttpProxyFeature + { + HostString ProxyHost { get; } + + ProxyProtocol ProxyProtocol { get; } + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/ProxyProtocol.cs b/FastGithub.HttpServer/TcpMiddlewares/ProxyProtocol.cs new file mode 100644 index 00000000..f16268ca --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/ProxyProtocol.cs @@ -0,0 +1,23 @@ +namespace FastGithub.HttpServer.TcpMiddlewares +{ + /// + /// 代理协议 + /// + enum ProxyProtocol + { + /// + /// 无代理 + /// + None, + + /// + /// http代理 + /// + HttpProxy, + + /// + /// 隧道代理 + /// + TunnelProxy + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/TcpReverseProxyHandler.cs b/FastGithub.HttpServer/TcpMiddlewares/TcpReverseProxyHandler.cs new file mode 100644 index 00000000..a3c8d2ed --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/TcpReverseProxyHandler.cs @@ -0,0 +1,77 @@ +using FastGithub.DomainResolve; +using Microsoft.AspNetCore.Connections; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.TcpMiddlewares +{ + /// + /// tcp协议代理处理者 + /// + abstract class TcpReverseProxyHandler : ConnectionHandler + { + private readonly IDomainResolver domainResolver; + private readonly DnsEndPoint endPoint; + private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d); + + /// + /// tcp协议代理处理者 + /// + /// + /// + public TcpReverseProxyHandler(IDomainResolver domainResolver, DnsEndPoint endPoint) + { + this.domainResolver = domainResolver; + this.endPoint = endPoint; + } + + /// + /// tcp连接后 + /// + /// + /// + 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); + } + + /// + /// 创建连接 + /// + /// + /// + /// + private async Task CreateConnectionAsync(CancellationToken cancellationToken) + { + var innerExceptions = new List(); + 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); + } + } +} diff --git a/FastGithub.HttpServer/TcpMiddlewares/TunnelMiddleware.cs b/FastGithub.HttpServer/TcpMiddlewares/TunnelMiddleware.cs new file mode 100644 index 00000000..7f9af9fc --- /dev/null +++ b/FastGithub.HttpServer/TcpMiddlewares/TunnelMiddleware.cs @@ -0,0 +1,132 @@ +using FastGithub.Configuration; +using FastGithub.DomainResolve; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.TcpMiddlewares +{ + /// + /// 隧道中间件 + /// + sealed class TunnelMiddleware + { + private readonly FastGithubConfig fastGithubConfig; + private readonly IDomainResolver domainResolver; + private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d); + + /// + /// 隧道中间件 + /// + /// + /// + public TunnelMiddleware( + FastGithubConfig fastGithubConfig, + IDomainResolver domainResolver) + { + this.fastGithubConfig = fastGithubConfig; + this.domainResolver = domainResolver; + } + + /// + /// 执行中间件 + /// + /// + /// + /// + public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context) + { + var proxyFeature = context.Features.Get(); + if (proxyFeature == null || // 非代理 + proxyFeature.ProxyProtocol != ProxyProtocol.TunnelProxy || //非隧道代理 + context.Features.Get() != null) // 经过隧道的https + { + await next(context); + } + else + { + var transport = context.Features.Get()?.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); + } + } + } + + + /// + /// 创建连接 + /// + /// + /// + /// + /// + private async Task CreateConnectionAsync(HostString host, CancellationToken cancellationToken) + { + var innerExceptions = new List(); + 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); + } + + /// + /// 获取目标终节点 + /// + /// + /// + /// + private async IAsyncEnumerable 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); + } + } + } + } +} diff --git a/FastGithub.HttpServer/TlsMiddlewares/FakeTlsConnectionFeature.cs b/FastGithub.HttpServer/TlsMiddlewares/FakeTlsConnectionFeature.cs new file mode 100644 index 00000000..9dcbc894 --- /dev/null +++ b/FastGithub.HttpServer/TlsMiddlewares/FakeTlsConnectionFeature.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.TlsMiddlewares +{ + /// + /// 假冒的TlsConnectionFeature + /// + sealed class FakeTlsConnectionFeature : ITlsConnectionFeature + { + public static FakeTlsConnectionFeature Instance { get; } = new FakeTlsConnectionFeature(); + + public X509Certificate2? ClientCertificate + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/FastGithub.HttpServer/TlsMiddlewares/TlsInvadeMiddleware.cs b/FastGithub.HttpServer/TlsMiddlewares/TlsInvadeMiddleware.cs new file mode 100644 index 00000000..548da170 --- /dev/null +++ b/FastGithub.HttpServer/TlsMiddlewares/TlsInvadeMiddleware.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using System.Buffers; +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.TlsMiddlewares +{ + /// + /// https入侵中间件 + /// + sealed class TlsInvadeMiddleware + { + /// + /// 执行中间件 + /// + /// + /// + public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context) + { + // 连接不是tls + if (await IsTlsConnectionAsync(context) == false) + { + // 没有任何tls中间件执行过 + if (context.Features.Get() == null) + { + // 设置假的ITlsConnectionFeature,迫使https中间件跳过自身的工作 + context.Features.Set(FakeTlsConnectionFeature.Instance); + } + } + await next(context); + } + + + /// + /// 是否为tls协议 + /// + /// + /// + private static async Task 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(result.Buffer); + return reader.TryRead(out var firstByte) && + reader.TryRead(out var nextByte) && + firstByte == 0x16 && + nextByte == 0x3; + } + } + } +} diff --git a/FastGithub.HttpServer/TlsMiddlewares/TlsRestoreMiddleware.cs b/FastGithub.HttpServer/TlsMiddlewares/TlsRestoreMiddleware.cs new file mode 100644 index 00000000..c9df0ff6 --- /dev/null +++ b/FastGithub.HttpServer/TlsMiddlewares/TlsRestoreMiddleware.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using System.Threading.Tasks; + +namespace FastGithub.HttpServer.TlsMiddlewares +{ + /// + /// https恢复中间件 + /// + sealed class TlsRestoreMiddleware + { + /// + /// 执行中间件 + /// + /// + /// + public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context) + { + if (context.Features.Get() == FakeTlsConnectionFeature.Instance) + { + // 擦除入侵 + context.Features.Set(null); + } + await next(context); + } + } +} diff --git a/FastGithub.PacketIntercept/Dns/DnsInterceptor.cs b/FastGithub.PacketIntercept/Dns/DnsInterceptor.cs new file mode 100644 index 00000000..118902fd --- /dev/null +++ b/FastGithub.PacketIntercept/Dns/DnsInterceptor.cs @@ -0,0 +1,157 @@ +using DNS.Protocol; +using DNS.Protocol.ResourceRecords; +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using WindivertDotnet; + +namespace FastGithub.PacketIntercept.Dns +{ + /// + /// dns拦截器 + /// + [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 logger; + + private readonly TimeSpan ttl = TimeSpan.FromMinutes(5d); + + /// + /// 刷新DNS缓存 + /// + [DllImport("dnsapi.dll", EntryPoint = "DnsFlushResolverCache", SetLastError = true)] + private static extern void DnsFlushResolverCache(); + + /// + /// dns拦截器 + /// + /// + /// + /// + public DnsInterceptor( + FastGithubConfig fastGithubConfig, + ILogger logger, + IOptionsMonitor options) + { + this.fastGithubConfig = fastGithubConfig; + this.logger = logger; + + options.OnChange(_ => DnsFlushResolverCache()); + } + + /// + /// DNS拦截 + /// + /// + /// + /// + 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); + } + } + } + + /// + /// 修改DNS数据包 + /// + /// + /// + 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}"); + } + + + /// + /// 尝试解析请求 + /// + /// + /// + /// + static bool TryParseRequest(byte[] payload, [MaybeNullWhen(false)] out Request request) + { + try + { + request = Request.FromArray(payload); + return true; + } + catch (Exception) + { + request = null; + return false; + } + } + } +} diff --git a/FastGithub.PacketIntercept/Dns/HostsConflictSolver.cs b/FastGithub.PacketIntercept/Dns/HostsConflictSolver.cs new file mode 100644 index 00000000..0282290b --- /dev/null +++ b/FastGithub.PacketIntercept/Dns/HostsConflictSolver.cs @@ -0,0 +1,115 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept.Dns +{ + /// + /// host文件冲解决者 + /// + [SupportedOSPlatform("windows")] + sealed class HostsConflictSolver : IDnsConflictSolver + { + private readonly FastGithubConfig fastGithubConfig; + private readonly ILogger logger; + + /// + /// host文件冲解决者 + /// + /// + /// + public HostsConflictSolver( + FastGithubConfig fastGithubConfig, + ILogger logger) + { + this.fastGithubConfig = fastGithubConfig; + this.logger = logger; + } + + /// + /// 解决冲突 + /// + /// + /// + 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}"); + } + } + } + + /// + /// 恢复冲突 + /// + /// + /// + public Task RestoreAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// 是否为冲突的行 + /// + /// + /// + 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); + } + } +} diff --git a/FastGithub.PacketIntercept/Dns/ProxyConflictSolver.cs b/FastGithub.PacketIntercept/Dns/ProxyConflictSolver.cs new file mode 100644 index 00000000..2ad54d79 --- /dev/null +++ b/FastGithub.PacketIntercept/Dns/ProxyConflictSolver.cs @@ -0,0 +1,161 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept.Dns +{ + /// + /// 代理冲突解决者 + /// + [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 options; + private readonly ILogger logger; + + [DllImport("wininet.dll")] + private static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength); + + + /// + /// 代理冲突解决者 + /// + /// + /// + public ProxyConflictSolver( + IOptions options, + ILogger logger) + { + this.options = options; + this.logger = logger; + } + + /// + /// 解决冲突 + /// + /// + /// + public Task SolveAsync(CancellationToken cancellationToken) + { + this.SetToProxyOvride(); + this.CheckProxyConflict(); + return Task.CompletedTask; + } + + /// + /// 恢复冲突 + /// + /// + /// + public Task RestoreAsync(CancellationToken cancellationToken) + { + this.RemoveFromProxyOvride(); + return Task.CompletedTask; + } + + /// + /// 添加到ProxyOvride + /// + 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); + } + + /// + /// 从ProxyOvride移除 + /// + 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); + } + + /// + /// 检测代理冲突 + /// + 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}"); + } + } + } + + /// + /// 获取ProxyOverride + /// + /// + /// + private static string[] GetProxyOvride(RegistryKey registryKey) + { + var value = registryKey.GetValue(PROXYOVERRIDE_KEY, null)?.ToString(); + if (value == null) + { + return Array.Empty(); + } + + return value + .Split(PROXYOVERRIDE_SEPARATOR, StringSplitOptions.RemoveEmptyEntries) + .Select(item => item.Trim()) + .ToArray(); + } + + /// + /// 设置ProxyOverride + /// + /// + /// + private static void SetProxyOvride(RegistryKey registryKey, IEnumerable 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); + } + } +} diff --git a/FastGithub.PacketIntercept/DnsInterceptHostedService.cs b/FastGithub.PacketIntercept/DnsInterceptHostedService.cs new file mode 100644 index 00000000..cab53598 --- /dev/null +++ b/FastGithub.PacketIntercept/DnsInterceptHostedService.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept +{ + /// + /// dns拦截后台服务 + /// + [SupportedOSPlatform("windows")] + sealed class DnsInterceptHostedService : BackgroundService + { + private readonly IDnsInterceptor dnsInterceptor; + private readonly IEnumerable conflictSolvers; + private readonly ILogger logger; + private readonly IHost host; + + /// + /// dns拦截后台服务 + /// + /// + /// + /// + /// + public DnsInterceptHostedService( + IDnsInterceptor dnsInterceptor, + IEnumerable conflictSolvers, + ILogger logger, + IHost host) + { + this.dnsInterceptor = dnsInterceptor; + this.conflictSolvers = conflictSolvers; + this.logger = logger; + this.host = host; + } + + /// + /// 启动时处理冲突 + /// + /// + /// + public override async Task StartAsync(CancellationToken cancellationToken) + { + foreach (var solver in this.conflictSolvers) + { + await solver.SolveAsync(cancellationToken); + } + await base.StartAsync(cancellationToken); + } + + /// + /// 停止时恢复冲突 + /// + /// + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + foreach (var solver in this.conflictSolvers) + { + await solver.RestoreAsync(cancellationToken); + } + await base.StopAsync(cancellationToken); + } + + /// + /// dns后台 + /// + /// + /// + 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); + } + } + } +} diff --git a/FastGithub.PacketIntercept/FastGithub.PacketIntercept.csproj b/FastGithub.PacketIntercept/FastGithub.PacketIntercept.csproj new file mode 100644 index 00000000..d4128bc6 --- /dev/null +++ b/FastGithub.PacketIntercept/FastGithub.PacketIntercept.csproj @@ -0,0 +1,16 @@ + + + + true + + + + + + + + + + + + diff --git a/FastGithub.PacketIntercept/IDnsConflictSolver.cs b/FastGithub.PacketIntercept/IDnsConflictSolver.cs new file mode 100644 index 00000000..d41525be --- /dev/null +++ b/FastGithub.PacketIntercept/IDnsConflictSolver.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept +{ + /// + /// Dns冲突解决者 + /// + interface IDnsConflictSolver + { + /// + /// 解决冲突 + /// + /// + /// + Task SolveAsync(CancellationToken cancellationToken); + + /// + /// 恢复冲突 + /// + /// + /// + Task RestoreAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/FastGithub.PacketIntercept/IDnsInterceptor.cs b/FastGithub.PacketIntercept/IDnsInterceptor.cs new file mode 100644 index 00000000..7244af3c --- /dev/null +++ b/FastGithub.PacketIntercept/IDnsInterceptor.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept +{ + /// + /// dns拦截器接口 + /// + interface IDnsInterceptor + { + /// + /// 拦截数据包 + /// + /// + /// + Task InterceptAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/FastGithub.PacketIntercept/ITcpInterceptor.cs b/FastGithub.PacketIntercept/ITcpInterceptor.cs new file mode 100644 index 00000000..d1ee0cf5 --- /dev/null +++ b/FastGithub.PacketIntercept/ITcpInterceptor.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept +{ + /// + /// tcp拦截器接口 + /// + interface ITcpInterceptor + { + /// + /// 拦截数据包 + /// + /// + /// + Task InterceptAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/FastGithub.PacketIntercept/ServiceCollectionExtensions.cs b/FastGithub.PacketIntercept/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..dfe1a7ce --- /dev/null +++ b/FastGithub.PacketIntercept/ServiceCollectionExtensions.cs @@ -0,0 +1,37 @@ +using FastGithub.PacketIntercept; +using FastGithub.PacketIntercept.Dns; +using FastGithub.PacketIntercept.Tcp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Runtime.Versioning; + +namespace FastGithub +{ + /// + /// 服务注册扩展 + /// + public static class ServiceCollectionExtensions + { + /// + /// 注册数据包拦截器 + /// + /// + /// + [SupportedOSPlatform("windows")] + public static IServiceCollection AddPacketIntercept(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.TryAddSingleton(); + services.AddHostedService(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); + + return services; + } + } +} diff --git a/FastGithub.PacketIntercept/Tcp/GitInterceptor.cs b/FastGithub.PacketIntercept/Tcp/GitInterceptor.cs new file mode 100644 index 00000000..d9a3cda6 --- /dev/null +++ b/FastGithub.PacketIntercept/Tcp/GitInterceptor.cs @@ -0,0 +1,22 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System.Runtime.Versioning; + +namespace FastGithub.PacketIntercept.Tcp +{ + /// + /// git拦截器 + /// + [SupportedOSPlatform("windows")] + sealed class GitInterceptor : TcpInterceptor + { + /// + /// git拦截器 + /// + /// + public GitInterceptor(ILogger logger) + : base(9418, GlobalListener.GitPort, logger) + { + } + } +} diff --git a/FastGithub.PacketIntercept/Tcp/HttpInterceptor.cs b/FastGithub.PacketIntercept/Tcp/HttpInterceptor.cs new file mode 100644 index 00000000..6da13b2d --- /dev/null +++ b/FastGithub.PacketIntercept/Tcp/HttpInterceptor.cs @@ -0,0 +1,22 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System.Runtime.Versioning; + +namespace FastGithub.PacketIntercept.Tcp +{ + /// + /// http拦截器 + /// + [SupportedOSPlatform("windows")] + sealed class HttpInterceptor : TcpInterceptor + { + /// + /// http拦截器 + /// + /// + public HttpInterceptor(ILogger logger) + : base(80, GlobalListener.HttpPort, logger) + { + } + } +} diff --git a/FastGithub.PacketIntercept/Tcp/HttpsInterceptor.cs b/FastGithub.PacketIntercept/Tcp/HttpsInterceptor.cs new file mode 100644 index 00000000..c300a435 --- /dev/null +++ b/FastGithub.PacketIntercept/Tcp/HttpsInterceptor.cs @@ -0,0 +1,22 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System.Runtime.Versioning; + +namespace FastGithub.PacketIntercept.Tcp +{ + /// + /// https拦截器 + /// + [SupportedOSPlatform("windows")] + sealed class HttpsInterceptor : TcpInterceptor + { + /// + /// https拦截器 + /// + /// + public HttpsInterceptor(ILogger logger) + : base(443, GlobalListener.HttpsPort, logger) + { + } + } +} diff --git a/FastGithub.PacketIntercept/Tcp/SshInterceptor.cs b/FastGithub.PacketIntercept/Tcp/SshInterceptor.cs new file mode 100644 index 00000000..9a395978 --- /dev/null +++ b/FastGithub.PacketIntercept/Tcp/SshInterceptor.cs @@ -0,0 +1,22 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System.Runtime.Versioning; + +namespace FastGithub.PacketIntercept.Tcp +{ + /// + /// ssh拦截器 + /// + [SupportedOSPlatform("windows")] + sealed class SshInterceptor : TcpInterceptor + { + /// + /// ssh拦截器 + /// + /// + public SshInterceptor(ILogger logger) + : base(22, GlobalListener.SshPort, logger) + { + } + } +} diff --git a/FastGithub.PacketIntercept/Tcp/TcpInterceptor.cs b/FastGithub.PacketIntercept/Tcp/TcpInterceptor.cs new file mode 100644 index 00000000..2d38bb10 --- /dev/null +++ b/FastGithub.PacketIntercept/Tcp/TcpInterceptor.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Logging; +using System; +using System.ComponentModel; +using System.Net; +using System.Net.Sockets; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using WindivertDotnet; + +namespace FastGithub.PacketIntercept.Tcp +{ + /// + /// tcp拦截器 + /// + [SupportedOSPlatform("windows")] + abstract class TcpInterceptor : ITcpInterceptor + { + private readonly Filter filter; + private readonly ushort oldServerPort; + private readonly ushort newServerPort; + private readonly ILogger logger; + + /// + /// tcp拦截器 + /// + /// 修改前的服务器端口 + /// 修改后的服务器端口 + /// + 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; + } + + /// + /// 拦截指定端口的数据包 + /// + /// + /// + 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); + } + } + } + + /// + /// 修改tcp数据端口的端口 + /// + /// + /// + 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); + } + } +} diff --git a/FastGithub.PacketIntercept/TcpInterceptHostedService.cs b/FastGithub.PacketIntercept/TcpInterceptHostedService.cs new file mode 100644 index 00000000..28a4f63c --- /dev/null +++ b/FastGithub.PacketIntercept/TcpInterceptHostedService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.PacketIntercept +{ + /// + /// tcp拦截后台服务 + /// + [SupportedOSPlatform("windows")] + sealed class TcpInterceptHostedService : BackgroundService + { + private readonly IEnumerable tcpInterceptors; + private readonly ILogger logger; + private readonly IHost host; + + /// + /// tcp拦截后台服务 + /// + /// + /// + /// + public TcpInterceptHostedService( + IEnumerable tcpInterceptors, + ILogger logger, + IHost host) + { + this.tcpInterceptors = tcpInterceptors; + this.logger = logger; + this.host = host; + } + + /// + /// https后台 + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + var tasks = this.tcpInterceptors.Select(item => item.InterceptAsync(stoppingToken)); + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) + { + } + catch (Win32Exception ex) when (ex.NativeErrorCode == 995) + { + } + catch (Exception ex) + { + this.logger.LogError(ex, "tcp拦截器异常"); + await this.host.StopAsync(stoppingToken); + } + } + } +} diff --git a/FastGithub.Scanner/FastGithub.Scanner.csproj b/FastGithub.Scanner/FastGithub.Scanner.csproj deleted file mode 100644 index 493d088b..00000000 --- a/FastGithub.Scanner/FastGithub.Scanner.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net5.0 - enable - - - - - - - - - - - - - diff --git a/FastGithub.Scanner/GithubContext.cs b/FastGithub.Scanner/GithubContext.cs deleted file mode 100644 index dffefcaf..00000000 --- a/FastGithub.Scanner/GithubContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Net; - -namespace FastGithub.Scanner -{ - sealed class GithubContext : IEquatable - { - /// - /// 获取域名 - /// - public string Domain { get; } - - /// - /// 获取ip - /// - public IPAddress Address { get; } - - /// - /// 获取或设置是否可用 - /// - public bool Available { get; set; } = false; - - /// - /// 获取或设置扫描总耗时 - /// - public TimeSpan Elapsed { get; set; } = TimeSpan.MaxValue; - - - public GithubContext(string domain, IPAddress address) - { - this.Domain = domain; - this.Address = address; - } - - public override string ToString() - { - return $"{Address}\t{Domain}\t# {Elapsed}"; - } - - public override bool Equals(object? obj) - { - return obj is GithubContext other && this.Equals(other); - } - - public bool Equals(GithubContext? other) - { - return other != null && other.Address.Equals(this.Address) && other.Domain == this.Domain; - } - - public override int GetHashCode() - { - return HashCode.Combine(this.Domain, this.Address); - } - } -} diff --git a/FastGithub.Scanner/GithubContextCollection.cs b/FastGithub.Scanner/GithubContextCollection.cs deleted file mode 100644 index 3abf00d7..00000000 --- a/FastGithub.Scanner/GithubContextCollection.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; - -namespace FastGithub.Scanner -{ - sealed class GithubContextCollection - { - private readonly object syncRoot = new(); - private readonly HashSet contextHashSet = new(); - private readonly Dictionary domainAdressCache = new(); - - public void AddOrUpdate(GithubContext context) - { - lock (this.syncRoot) - { - if (this.contextHashSet.TryGetValue(context, out var value)) - { - value.Elapsed = context.Elapsed; - value.Available = context.Available; - } - else - { - this.contextHashSet.Add(context); - } - } - } - - public GithubContext[] ToArray() - { - lock (this.syncRoot) - { - return this.contextHashSet.ToArray(); - } - } - - /// - /// 查找又稳又快的ip - /// - /// - /// - public IPAddress? FindFastAddress(string domain) - { - lock (this.syncRoot) - { - // 如果上一次的ip可以使用,就返回上一次的ip - if (this.domainAdressCache.TryGetValue(domain, out var address)) - { - var key = new GithubContext(domain, address); - if (this.contextHashSet.TryGetValue(key, out var context) && context.Available) - { - return address; - } - } - - var fastAddress = this.contextHashSet - .Where(item => item.Available && item.Domain == domain) - .OrderBy(item => item.Elapsed) - .Select(item => item.Address) - .FirstOrDefault(); - - if (fastAddress != null) - { - this.domainAdressCache[domain] = fastAddress; - } - else - { - this.domainAdressCache.Remove(domain); - } - return fastAddress; - } - } - } -} diff --git a/FastGithub.Scanner/GithubFullScanHostedService.cs b/FastGithub.Scanner/GithubFullScanHostedService.cs deleted file mode 100644 index 6fc20618..00000000 --- a/FastGithub.Scanner/GithubFullScanHostedService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using FastGithub.Scanner; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub -{ - sealed class GithubFullScanHostedService : BackgroundService - { - private readonly IGithubScanService githubScanService; - private readonly IOptionsMonitor options; - - public GithubFullScanHostedService( - IGithubScanService githubScanService, - IOptionsMonitor options) - { - this.githubScanService = githubScanService; - this.options = options; - } - - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (stoppingToken.IsCancellationRequested == false) - { - await githubScanService.ScanAllAsync(stoppingToken); - await Task.Delay(this.options.CurrentValue.ScanAllInterval, stoppingToken); - } - } - } -} diff --git a/FastGithub.Scanner/GithubMetaService.cs b/FastGithub.Scanner/GithubMetaService.cs deleted file mode 100644 index 265d5564..00000000 --- a/FastGithub.Scanner/GithubMetaService.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Net.Http; -using System.Net.Http.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner -{ - [Service(ServiceLifetime.Singleton)] - sealed class GithubMetaService - { - private readonly IHttpClientFactory httpClientFactory; - private readonly IOptionsMonitor options; - private readonly ILogger logger; - - public GithubMetaService( - IHttpClientFactory httpClientFactory, - IOptionsMonitor options, - ILogger logger) - { - this.httpClientFactory = httpClientFactory; - this.options = options; - this.logger = logger; - } - - public async Task GetMetaAsync(CancellationToken cancellationToken = default) - { - try - { - var httpClient = this.httpClientFactory.CreateClient(); - return await httpClient.GetFromJsonAsync(this.options.CurrentValue.MetaUri, cancellationToken); - } - catch (Exception ex) - { - this.logger.LogError(ex, "获取meta.json文件失败"); - return default; - } - } - } -} diff --git a/FastGithub.Scanner/GithubOptions.cs b/FastGithub.Scanner/GithubOptions.cs deleted file mode 100644 index 0a4a88ee..00000000 --- a/FastGithub.Scanner/GithubOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace FastGithub.Scanner -{ - [Options("Github")] - sealed class GithubOptions - { - public TimeSpan ScanAllInterval { get; set; } = TimeSpan.FromHours(2d); - - public TimeSpan ScanResultInterval { get; set; } = TimeSpan.FromMinutes(1d); - - public Uri MetaUri { get; set; } = new Uri("https://gitee.com/jiulang/fast-github/raw/master/FastGithub/meta.json"); - - public TimeSpan PortScanTimeout { get; set; } = TimeSpan.FromSeconds(1d); - - public TimeSpan HttpsScanTimeout { get; set; } = TimeSpan.FromSeconds(5d); - } -} diff --git a/FastGithub.Scanner/GithubResultScanHostedService.cs b/FastGithub.Scanner/GithubResultScanHostedService.cs deleted file mode 100644 index 11ab4a0c..00000000 --- a/FastGithub.Scanner/GithubResultScanHostedService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FastGithub.Scanner; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub -{ - sealed class GithubResultScanHostedService : BackgroundService - { - private readonly IGithubScanService githubScanService; - private readonly IOptionsMonitor options; - - public GithubResultScanHostedService( - IGithubScanService githubScanService, - IOptionsMonitor options) - { - this.githubScanService = githubScanService; - this.options = options; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (stoppingToken.IsCancellationRequested == false) - { - await Task.Delay(this.options.CurrentValue.ScanResultInterval, stoppingToken); - await githubScanService.ScanResultAsync(); - } - } - } -} diff --git a/FastGithub.Scanner/GithubScanService.cs b/FastGithub.Scanner/GithubScanService.cs deleted file mode 100644 index 480b5737..00000000 --- a/FastGithub.Scanner/GithubScanService.cs +++ /dev/null @@ -1,90 +0,0 @@ -using FastGithub.Scanner.Middlewares; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner -{ - [Service(ServiceLifetime.Singleton, ServiceType = typeof(IGithubScanService))] - sealed class GithubScanService : IGithubScanService - { - private readonly GithubMetaService metaService; - private readonly ILogger logger; - private readonly GithubContextCollection results = new(); - - private readonly InvokeDelegate fullScanDelegate; - private readonly InvokeDelegate resultScanDelegate; - - public GithubScanService( - GithubMetaService metaService, - ILogger logger, - IPipelineBuilder pipelineBuilder) - { - this.metaService = metaService; - this.logger = logger; - - this.fullScanDelegate = pipelineBuilder - .New() - .Use() - .Use() - .Use() - .Use() - .Use() - .Build(); - - this.resultScanDelegate = pipelineBuilder - .New() - .Use() - .Use() - .Use() - .Use() - .Build(); - } - - public async Task ScanAllAsync(CancellationToken cancellationToken = default) - { - this.logger.LogInformation("完整扫描开始"); - var meta = await this.metaService.GetMetaAsync(cancellationToken); - if (meta != null) - { - var scanTasks = meta.ToGithubContexts().Select(ctx => ScanAsync(ctx)); - await Task.WhenAll(scanTasks); - } - - this.logger.LogInformation("完整扫描结束"); - - async Task ScanAsync(GithubContext context) - { - await this.fullScanDelegate(context); - if (context.Available == true) - { - this.results.AddOrUpdate(context); - } - } - } - - public async Task ScanResultAsync() - { - this.logger.LogInformation("结果扫描开始"); - - var contexts = this.results.ToArray(); - foreach (var context in contexts) - { - await this.resultScanDelegate(context); - } - - this.logger.LogInformation("结果扫描结束"); - } - - public IPAddress? FindFastAddress(string domain) - { - return domain.Contains("github", StringComparison.OrdinalIgnoreCase) - ? this.results.FindFastAddress(domain) - : default; - } - } -} diff --git a/FastGithub.Scanner/IGithubScanService.cs b/FastGithub.Scanner/IGithubScanService.cs deleted file mode 100644 index 44553a37..00000000 --- a/FastGithub.Scanner/IGithubScanService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner -{ - public interface IGithubScanService - { - Task ScanAllAsync(CancellationToken cancellationToken = default); - Task ScanResultAsync(); - IPAddress? FindFastAddress(string domain); - } -} \ No newline at end of file diff --git a/FastGithub.Scanner/IPRange.cs b/FastGithub.Scanner/IPRange.cs deleted file mode 100644 index f59c08ae..00000000 --- a/FastGithub.Scanner/IPRange.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Sockets; - -namespace FastGithub.Scanner -{ - sealed class IPRange : IEnumerable - { - private readonly IPNetwork network; - - public AddressFamily AddressFamily => this.network.AddressFamily; - - public int Size => (int)this.network.Total; - - private IPRange(IPNetwork network) - { - this.network = network; - } - - public IEnumerator GetEnumerator() - { - return new Enumerator(this.network); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - private class Enumerator : IEnumerator - { - private IPAddress? currrent; - private readonly IPNetwork network; - private readonly IPAddress maxAddress; - - public Enumerator(IPNetwork network) - { - this.network = network; - this.maxAddress = Add(network.LastUsable, 1); - } - - public IPAddress Current => this.currrent ?? throw new NotImplementedException(); - - object IEnumerator.Current => this.Current; - - public void Dispose() - { - } - - public bool MoveNext() - { - var value = this.currrent == null - ? this.network.FirstUsable - : Add(this.currrent, 1); - - if (value.Equals(maxAddress)) - { - return false; - } - - this.currrent = value; - return true; - } - - public void Reset() - { - this.currrent = null; - } - } - - /// - /// 添加值 - /// - /// - /// - /// - private static IPAddress Add(IPAddress ip, int value) - { - var span = ip.GetAddressBytes().AsSpan(); - var hostValue = BinaryPrimitives.ReadInt32BigEndian(span); - BinaryPrimitives.WriteInt32BigEndian(span, hostValue + value); - return new IPAddress(span); - } - - public static IEnumerable From(IEnumerable networks) - { - foreach (var item in networks) - { - if (TryParse(item, out var value)) - { - yield return value; - } - } - } - - public static bool TryParse(ReadOnlySpan network, [MaybeNullWhen(false)] out IPRange value) - { - if (network.IsEmpty == false && IPNetwork.TryParse(network.ToString(), out var ipNetwork)) - { - value = new IPRange(ipNetwork); - return true; - } - - value = null; - return false; - } - - public override string ToString() - { - return this.network.ToString(); - } - - } -} diff --git a/FastGithub.Scanner/Meta.cs b/FastGithub.Scanner/Meta.cs deleted file mode 100644 index 94ceed12..00000000 --- a/FastGithub.Scanner/Meta.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Text.Json.Serialization; - -namespace FastGithub.Scanner -{ - sealed class Meta - { - [JsonPropertyName("hooks")] - public string[] Hooks { get; set; } = Array.Empty(); - - [JsonPropertyName("web")] - public string[] Web { get; set; } = Array.Empty(); - - [JsonPropertyName("api")] - public string[] Api { get; set; } = Array.Empty(); - - [JsonPropertyName("git")] - public string[] Git { get; set; } = Array.Empty(); - - [JsonPropertyName("packages")] - public string[] Packages { get; set; } = Array.Empty(); - - [JsonPropertyName("pages")] - public string[] Pages { get; set; } = Array.Empty(); - - [JsonPropertyName("importer")] - public string[] Importer { get; set; } = Array.Empty(); - - [JsonPropertyName("actions")] - public string[] Actions { get; set; } = Array.Empty(); - - [JsonPropertyName("dependabot")] - public string[] Dependabot { get; set; } = Array.Empty(); - - - public IEnumerable ToGithubContexts() - { - foreach (var range in IPRange.From(this.Web).OrderBy(item => item.Size)) - { - if (range.AddressFamily == AddressFamily.InterNetwork) - { - foreach (var address in range) - { - yield return new GithubContext("github.com", address); - } - } - } - - foreach (var range in IPRange.From(this.Api).OrderBy(item => item.Size)) - { - if (range.AddressFamily == AddressFamily.InterNetwork) - { - foreach (var address in range) - { - yield return new GithubContext("api.github.com", address); - } - } - } - } - } -} diff --git a/FastGithub.Scanner/Middlewares/ConcurrentMiddleware.cs b/FastGithub.Scanner/Middlewares/ConcurrentMiddleware.cs deleted file mode 100644 index 0a18c722..00000000 --- a/FastGithub.Scanner/Middlewares/ConcurrentMiddleware.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner.Middlewares -{ - [Service(ServiceLifetime.Singleton)] - sealed class ConcurrentMiddleware : IMiddleware - { - private readonly SemaphoreSlim semaphoreSlim = new(Environment.ProcessorCount * 4); - - public async Task InvokeAsync(GithubContext context, Func next) - { - try - { - await this.semaphoreSlim.WaitAsync(); - await next(); - } - finally - { - this.semaphoreSlim.Release(); - } - } - } -} diff --git a/FastGithub.Scanner/Middlewares/HttpsScanMiddleware.cs b/FastGithub.Scanner/Middlewares/HttpsScanMiddleware.cs deleted file mode 100644 index 6ceb89a9..00000000 --- a/FastGithub.Scanner/Middlewares/HttpsScanMiddleware.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner.Middlewares -{ - [Service(ServiceLifetime.Singleton)] - sealed class HttpsScanMiddleware : IMiddleware - { - private readonly IOptionsMonitor options; - private readonly ILogger logger; - - public HttpsScanMiddleware( - IOptionsMonitor options, - ILogger logger) - { - this.options = options; - this.logger = logger; - } - - public async Task InvokeAsync(GithubContext context, Func next) - { - try - { - context.Available = false; - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"https://{context.Address}"), - }; - request.Headers.Host = context.Domain; - - using var httpClient = new HttpClient(new HttpClientHandler - { - Proxy = null, - UseProxy = false, - }); - - using var cancellationTokenSource = new CancellationTokenSource(this.options.CurrentValue.HttpsScanTimeout); - var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token); - var server = response.EnsureSuccessStatusCode().Headers.Server; - if (server.Any(s => string.Equals("GitHub.com", s.Product?.Name, StringComparison.OrdinalIgnoreCase))) - { - context.Available = true; - await next(); - } - } - catch (TaskCanceledException) - { - this.logger.LogTrace($"{context.Domain} {context.Address}连接超时"); - } - catch (Exception ex) - { - this.logger.LogTrace($"{context.Domain} {context.Address} {ex.Message}"); - } - } - } -} diff --git a/FastGithub.Scanner/Middlewares/PortScanMiddleware.cs b/FastGithub.Scanner/Middlewares/PortScanMiddleware.cs deleted file mode 100644 index 5f6f5553..00000000 --- a/FastGithub.Scanner/Middlewares/PortScanMiddleware.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -namespace FastGithub.Scanner.Middlewares -{ - [Service(ServiceLifetime.Singleton)] - sealed class PortScanMiddleware : IMiddleware - { - private const int PORT = 443; - private readonly IOptionsMonitor options; - private readonly ILogger logger; - - public PortScanMiddleware( - IOptionsMonitor options, - ILogger logger) - { - this.options = options; - this.logger = logger; - } - - public async Task InvokeAsync(GithubContext context, Func next) - { - try - { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - using var cancellationTokenSource = new CancellationTokenSource(this.options.CurrentValue.PortScanTimeout); - await socket.ConnectAsync(context.Address, PORT, cancellationTokenSource.Token); - - await next(); - } - catch (Exception) - { - this.logger.LogTrace($"{context.Domain} {context.Address}的{PORT}端口未开放"); - } - } - } -} diff --git a/FastGithub.Scanner/Middlewares/ScanElapsedMiddleware.cs b/FastGithub.Scanner/Middlewares/ScanElapsedMiddleware.cs deleted file mode 100644 index 382c37b8..00000000 --- a/FastGithub.Scanner/Middlewares/ScanElapsedMiddleware.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Diagnostics; -using System.Threading.Tasks; - -namespace FastGithub.Scanner.Middlewares -{ - [Service(ServiceLifetime.Singleton)] - sealed class ScanElapsedMiddleware : IMiddleware - { - public async Task InvokeAsync(GithubContext context, Func next) - { - var stopwatch = new Stopwatch(); - try - { - stopwatch.Start(); - await next(); - } - finally - { - stopwatch.Stop(); - context.Elapsed = stopwatch.Elapsed; - } - } - } -} diff --git a/FastGithub.Scanner/Middlewares/ScanOkLogMiddleware.cs b/FastGithub.Scanner/Middlewares/ScanOkLogMiddleware.cs deleted file mode 100644 index 18dfd16a..00000000 --- a/FastGithub.Scanner/Middlewares/ScanOkLogMiddleware.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; - -namespace FastGithub.Scanner.Middlewares -{ - [Service(ServiceLifetime.Singleton)] - sealed class ScanOkLogMiddleware : IMiddleware - { - private readonly ILogger logger; - - public ScanOkLogMiddleware(ILogger logger) - { - this.logger = logger; - } - - public Task InvokeAsync(GithubContext context, Func next) - { - if (context.Available) - { - this.logger.LogInformation(context.ToString()); - } - - return next(); - } - } -} diff --git a/FastGithub.Scanner/ScannerServiceCollectionExtensions.cs b/FastGithub.Scanner/ScannerServiceCollectionExtensions.cs deleted file mode 100644 index a3d95721..00000000 --- a/FastGithub.Scanner/ScannerServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using FastGithub.Scanner; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System.Threading.Tasks; - -namespace FastGithub -{ - /// - /// 服务注册扩展 - /// - public static class ScannerServiceCollectionExtensions - { - /// - /// 注册程序集下所有服务下选项 - /// - /// - /// 配置 - /// - public static IServiceCollection AddGithubScanner(this IServiceCollection services, IConfiguration configuration) - { - var assembly = typeof(ScannerServiceCollectionExtensions).Assembly; - return services - .AddHttpClient() - .AddServiceAndOptions(assembly, configuration) - .AddHostedService() - .AddHostedService() - .AddSingleton>(appService => - { - return new PipelineBuilder(appService, ctx => Task.CompletedTask); - }) - ; - } - } -} diff --git a/FastGithub.UI/AssemblyInfo.cs b/FastGithub.UI/AssemblyInfo.cs new file mode 100644 index 00000000..8b5504ec --- /dev/null +++ b/FastGithub.UI/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/FastGithub.UI/FastGithub.UI.csproj b/FastGithub.UI/FastGithub.UI.csproj new file mode 100644 index 00000000..3fb90e7f --- /dev/null +++ b/FastGithub.UI/FastGithub.UI.csproj @@ -0,0 +1,34 @@ + + + + true + true + WinExe + 8.0 + net45 + app.ico + app.manifest + MIT + False + + + + + + + + + + + + + + + all + + + all + + + + diff --git a/FastGithub.UI/FlowChart.xaml b/FastGithub.UI/FlowChart.xaml new file mode 100644 index 00000000..c96a499f --- /dev/null +++ b/FastGithub.UI/FlowChart.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 上行流量 + + + 下行流量 + + + + diff --git a/FastGithub.UI/FlowChart.xaml.cs b/FastGithub.UI/FlowChart.xaml.cs new file mode 100644 index 00000000..86327107 --- /dev/null +++ b/FastGithub.UI/FlowChart.xaml.cs @@ -0,0 +1,112 @@ +using LiveCharts; +using LiveCharts.Configurations; +using LiveCharts.Wpf; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace FastGithub.UI +{ + /// + /// FlowChart.xaml 的交互逻辑 + /// + public partial class FlowChart : UserControl + { + private readonly LineSeries readSeries = new LineSeries + { + Title = "上行速率", + PointGeometry = null, + LineSmoothness = 1D, + Values = new ChartValues() + }; + + private readonly LineSeries writeSeries = new LineSeries() + { + Title = "下行速率", + PointGeometry = null, + LineSmoothness = 1D, + Values = new ChartValues() + }; + + private static DateTime GetDateTime(double timestamp) => new DateTime(1970, 1, 1).Add(TimeSpan.FromMilliseconds(timestamp)).ToLocalTime(); + + private static double GetTimestamp(DateTime dateTime) => dateTime.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds; + + + public SeriesCollection Series { get; } = new SeriesCollection(Mappers.Xy().X(item => item.Timestamp).Y(item => item.Rate)); + + public Func XFormatter { get; } = timestamp => GetDateTime(timestamp).ToString("HH:mm:ss"); + + public Func YFormatter { get; } = value => $"{FlowStatistics.ToNetworkSizeString((long)value)}/s"; + + public FlowChart() + { + InitializeComponent(); + + this.Series.Add(this.readSeries); + this.Series.Add(this.writeSeries); + + this.DataContext = this; + this.InitFlowChartAsync(); + } + + private async void InitFlowChartAsync() + { + using var httpClient = new HttpClient(); + while (this.Dispatcher.HasShutdownStarted == false) + { + try + { + await this.FlushFlowStatisticsAsync(httpClient); + } + catch (Exception) + { + } + finally + { + await Task.Delay(TimeSpan.FromSeconds(1d)); + } + } + } + + private async Task FlushFlowStatisticsAsync(HttpClient httpClient) + { + var response = await httpClient.GetAsync("http://localhost/flowStatistics"); + var json = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var flowStatistics = JsonConvert.DeserializeObject(json); + if (flowStatistics == null) + { + return; + } + + this.textBlockRead.Text = FlowStatistics.ToNetworkSizeString(flowStatistics.TotalRead); + this.textBlockWrite.Text = FlowStatistics.ToNetworkSizeString(flowStatistics.TotalWrite); + + var timestamp = GetTimestamp(DateTime.Now); + this.readSeries.Values.Add(new RateTick(flowStatistics.ReadRate, timestamp)); + this.writeSeries.Values.Add(new RateTick(flowStatistics.WriteRate, timestamp)); + + if (this.readSeries.Values.Count > 60) + { + this.readSeries.Values.RemoveAt(0); + this.writeSeries.Values.RemoveAt(0); + } + } + + private class RateTick + { + public double Rate { get; } + + public double Timestamp { get; } + + public RateTick(double rate, double timestamp) + { + this.Rate = rate; + this.Timestamp = timestamp; + } + } + + } +} diff --git a/FastGithub.UI/FlowStatistics.cs b/FastGithub.UI/FlowStatistics.cs new file mode 100644 index 00000000..97f20ee9 --- /dev/null +++ b/FastGithub.UI/FlowStatistics.cs @@ -0,0 +1,42 @@ +namespace FastGithub.UI +{ + /// + /// 流量统计 + /// + public class FlowStatistics + { + /// + /// 获取总读上行 + /// + public long TotalRead { get; set; } + + /// + /// 获取总下行 + /// + public long TotalWrite { get; set; } + + /// + /// 获取上行速率 + /// + public double ReadRate { get; set; } + + /// + /// 获取下行速率 + /// + public double WriteRate { get; set; } + + + public static string ToNetworkSizeString(long value) + { + if (value < 1024) + { + return $"{value}B"; + } + if (value < 1024 * 1024) + { + return $"{value / 1024d:0.00}KB"; + } + return $"{value / 1024d / 1024d:0.00}MB"; + } + } +} diff --git a/FastGithub.UI/IssuesWebbrowser.xaml b/FastGithub.UI/IssuesWebbrowser.xaml new file mode 100644 index 00000000..ffc0ed77 --- /dev/null +++ b/FastGithub.UI/IssuesWebbrowser.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/FastGithub.UI/IssuesWebbrowser.xaml.cs b/FastGithub.UI/IssuesWebbrowser.xaml.cs new file mode 100644 index 00000000..448fdc41 --- /dev/null +++ b/FastGithub.UI/IssuesWebbrowser.xaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace FastGithub.UI +{ + /// + /// IssuesWebbrowser.xaml 的交互逻辑 + /// + public partial class IssuesWebbrowser : UserControl + { + public IssuesWebbrowser() + { + InitializeComponent(); + + this.NavigateIssueHtml(); + this.webBrowser.AddHandler(KeyDownEvent, new RoutedEventHandler(WebBrowser_KeyDown), true); + } + + /// + /// 拦截F5 + /// + /// + /// + private void WebBrowser_KeyDown(object sender, RoutedEventArgs e) + { + var @event = (KeyEventArgs)e; + if (@event.Key == Key.F5) + { + this.NavigateIssueHtml(); + } + } + + private void NavigateIssueHtml() + { + try + { + var resource = Application.GetResourceStream(new Uri("Resource/issue.html", UriKind.Relative)); + this.webBrowser.NavigateToStream(resource.Stream); + } + catch (Exception) + { + } + } + } +} diff --git a/FastGithub.UI/LogLevel.cs b/FastGithub.UI/LogLevel.cs new file mode 100644 index 00000000..2b1e07d3 --- /dev/null +++ b/FastGithub.UI/LogLevel.cs @@ -0,0 +1,15 @@ +namespace FastGithub.UI +{ + /// + /// 日志等级 + /// + public enum LogLevel + { + Verbose, + Debug, + Information, + Warning, + Error, + Fatal + } +} diff --git a/FastGithub.UI/MainWindow.xaml b/FastGithub.UI/MainWindow.xaml new file mode 100644 index 00000000..b3ada218 --- /dev/null +++ b/FastGithub.UI/MainWindow.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FastGithub.UI/MainWindow.xaml.cs b/FastGithub.UI/MainWindow.xaml.cs new file mode 100644 index 00000000..1af7b478 --- /dev/null +++ b/FastGithub.UI/MainWindow.xaml.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Windows; +using System.Windows.Interop; + +namespace FastGithub.UI +{ + /// + /// MainWindow.xaml 的交互逻辑 + /// + public partial class MainWindow : Window + { + private readonly System.Windows.Forms.NotifyIcon notifyIcon; + private const string FASTGITHUB_UI = "FastGithub.UI"; + private const string RELEASES_URI = "https://github.com/dotnetcore/FastGithub/releases"; + + public MainWindow() + { + InitializeComponent(); + + var upgrade = new System.Windows.Forms.MenuItem("检测更新(&U)"); + upgrade.Click += (s, e) => Process.Start(RELEASES_URI); + + var exit = new System.Windows.Forms.MenuItem("关闭应用(&C)"); + exit.Click += (s, e) => this.Close(); + + var version = this.GetType().Assembly.GetCustomAttribute()?.InformationalVersion; + this.Title = $"{FASTGITHUB_UI} v{version}"; + this.notifyIcon = new System.Windows.Forms.NotifyIcon + { + Visible = true, + Text = FASTGITHUB_UI, + ContextMenu = new System.Windows.Forms.ContextMenu(new[] { upgrade, exit }), + Icon = System.Drawing.Icon.ExtractAssociatedIcon(System.Windows.Forms.Application.ExecutablePath) + }; + + this.notifyIcon.MouseClick += (s, e) => + { + if (e.Button == System.Windows.Forms.MouseButtons.Left) + { + this.Show(); + this.Activate(); + this.WindowState = WindowState.Normal; + } + }; + } + + + /// + /// 拦截最小化事件 + /// + /// + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + var hwndSource = (HwndSource)PresentationSource.FromVisual(this); + hwndSource.AddHook(WndProc); + + IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + const int WM_SYSCOMMAND = 0x112; + const int SC_MINIMIZE = 0xf020; + const int SC_CLOSE = 0xf060; + + if (msg == WM_SYSCOMMAND) + { + if (wParam.ToInt32() == SC_MINIMIZE || wParam.ToInt32() == SC_CLOSE) + { + this.Hide(); + handled = true; + } + } + return IntPtr.Zero; + } + } + + /// + /// 关闭时 + /// + /// + protected override void OnClosed(EventArgs e) + { + this.notifyIcon.Icon = null; + this.notifyIcon.Dispose(); + base.OnClosed(e); + } + } +} diff --git a/FastGithub.UI/Program.cs b/FastGithub.UI/Program.cs new file mode 100644 index 00000000..3a8702b5 --- /dev/null +++ b/FastGithub.UI/Program.cs @@ -0,0 +1,109 @@ +using Microsoft.Win32; +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Windows; + +namespace FastGithub.UI +{ + class Program + { + private const string MUTEX_NAME = "Global\\FastGithub.UI"; + private const string MAIN_WINDOWS = "MainWindow.xaml"; + private const string FASTGITHUB_PATH = "fastgithub.exe"; + + [STAThread] + static void Main(string[] args) + { + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + using var mutex = new Mutex(true, MUTEX_NAME, out var isFirstInstance); + if (isFirstInstance == false) + { + return; + } + + StartFastGithub(); + SetWebBrowserDPI(); + SetWebBrowserVersion(); + + var app = new Application(); + app.StartupUri = new Uri(MAIN_WINDOWS, UriKind.Relative); + app.Run(); + } + + /// + /// 程序集加载失败时 + /// + /// + /// + /// + private static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs args) + { + var name = new AssemblyName(args.Name).Name; + if (name.EndsWith(".resources")) + { + return default; + } + + var stream = Application.GetResourceStream(new Uri($"Resource/{name}.dll", UriKind.Relative)).Stream; + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return Assembly.Load(buffer); + } + + /// + /// 设置浏览器版本 + /// + private static void SetWebBrowserVersion() + { + const string subKey = @"Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION"; + var registryKey = Registry.CurrentUser.OpenSubKey(subKey, true); + if (registryKey == null) + { + registryKey = Registry.CurrentUser.CreateSubKey(subKey); + } + var name = $"{Process.GetCurrentProcess().ProcessName}.exe"; + using var webBrowser = new System.Windows.Forms.WebBrowser(); + var value = int.Parse($"{webBrowser.Version.Major}000"); + registryKey.SetValue(name, value, RegistryValueKind.DWord); + } + + /// + /// 设置浏览器DPI + /// + private static void SetWebBrowserDPI() + { + const string subKey = @"Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_96DPI_PIXEL"; + var registryKey = Registry.CurrentUser.OpenSubKey(subKey, true); + if (registryKey == null) + { + registryKey = Registry.CurrentUser.CreateSubKey(subKey); + } + var name = $"{Process.GetCurrentProcess().ProcessName}.exe"; + registryKey.SetValue(name, 1, RegistryValueKind.DWord); + } + + /// + /// 启动fastgithub + /// + /// + private static void StartFastGithub() + { + if (File.Exists(FASTGITHUB_PATH) == false) + { + return; + } + + var startInfo = new ProcessStartInfo + { + FileName = FASTGITHUB_PATH, + Arguments = $"ParentProcessId={Process.GetCurrentProcess().Id} UdpLoggerPort={UdpLogger.Port}", + UseShellExecute = false, + CreateNoWindow = true + }; + Process.Start(startInfo); + } + } +} diff --git a/FastGithub.UI/Resource/LiveCharts.Wpf.dll b/FastGithub.UI/Resource/LiveCharts.Wpf.dll new file mode 100644 index 00000000..86d44193 Binary files /dev/null and b/FastGithub.UI/Resource/LiveCharts.Wpf.dll differ diff --git a/FastGithub.UI/Resource/LiveCharts.dll b/FastGithub.UI/Resource/LiveCharts.dll new file mode 100644 index 00000000..66f46618 Binary files /dev/null and b/FastGithub.UI/Resource/LiveCharts.dll differ diff --git a/FastGithub.UI/Resource/Newtonsoft.Json.dll b/FastGithub.UI/Resource/Newtonsoft.Json.dll new file mode 100644 index 00000000..7af125a2 Binary files /dev/null and b/FastGithub.UI/Resource/Newtonsoft.Json.dll differ diff --git a/FastGithub.UI/Resource/issue.html b/FastGithub.UI/Resource/issue.html new file mode 100644 index 00000000..fc5a97e2 --- /dev/null +++ b/FastGithub.UI/Resource/issue.html @@ -0,0 +1,88 @@ + + + + + + + + 证书验证 + + + + +
+

Firefox

+
+

建立安全连接失败

+

连接到 github.com 时发生错误。对等端的证书有一个无效的签名。

+

错误代码:SEC_ERROR_BAD_SIGNATURE

+
+ +

解决办法

+

+ 1 + 地址栏输入:about:config +

+

+ 2 + 输入首选项名称:security.enterprise_roots.enabled +

+

+ 3 + 修改值为:true +

+
+ +
+ +
+

git.exe

+
+

clone、pull或push等证书异常

+

fatal: unable to access 'https://github.com/xxx.git/'

+

SSL certificate problem: unable to get local issuer certificate

+
+ +

解决办法

+

+ 1 + 管理员身份运行:cmd +

+

+ 2 + 在cmd输入:git config --global http.sslverify false +

+
+ + + + + \ No newline at end of file diff --git a/FastGithub.UI/Resource/reward.png b/FastGithub.UI/Resource/reward.png new file mode 100644 index 00000000..e65413f5 Binary files /dev/null and b/FastGithub.UI/Resource/reward.png differ diff --git a/FastGithub.UI/UdpLog.cs b/FastGithub.UI/UdpLog.cs new file mode 100644 index 00000000..99c3f906 --- /dev/null +++ b/FastGithub.UI/UdpLog.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Windows; + +namespace FastGithub.UI +{ + public class UdpLog + { + public DateTime Timestamp { get; set; } + + public LogLevel Level { get; set; } + + public string Message { get; set; } = string.Empty; + + public string SourceContext { get; set; } = string.Empty; + + public string Color => this.Level <= LogLevel.Information ? "#333" : "IndianRed"; + + /// + /// 复制到剪贴板 + /// + public void SetToClipboard() + { + Clipboard.SetText($"{this.Timestamp:yyyy-MM-dd HH:mm:ss.fff}\r\n{this.Message}"); + } + + } + +} diff --git a/FastGithub.UI/UdpLogListBox.xaml b/FastGithub.UI/UdpLogListBox.xaml new file mode 100644 index 00000000..79e529c5 --- /dev/null +++ b/FastGithub.UI/UdpLogListBox.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FastGithub.UI/UdpLogListBox.xaml.cs b/FastGithub.UI/UdpLogListBox.xaml.cs new file mode 100644 index 00000000..554e44f4 --- /dev/null +++ b/FastGithub.UI/UdpLogListBox.xaml.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows.Controls; + +namespace FastGithub.UI +{ + /// + /// UdpLogListBox.xaml 的交互逻辑 + /// + public partial class UdpLogListBox : UserControl + { + private readonly int maxLogCount = 100; + public ObservableCollection LogList { get; } = new ObservableCollection(); + + public UdpLogListBox() + { + InitializeComponent(); + + this.DataContext = this; + this.InitUdpLoggerAsync(); + } + + private async void InitUdpLoggerAsync() + { + while (this.Dispatcher.HasShutdownStarted == false) + { + try + { + var log = await UdpLogger.GetUdpLogAsync(); + if (log != null) + { + this.LogList.Insert(0, log); + if (this.LogList.Count > this.maxLogCount) + { + this.LogList.RemoveAt(this.maxLogCount); + } + } + } + catch (Exception) + { + } + } + } + + private void MenuItem_Copy_Click(object sender, System.Windows.RoutedEventArgs e) + { + if (this.listBox.SelectedValue is UdpLog udpLog) + { + udpLog.SetToClipboard(); + } + } + + private void MenuItem_Clear_Click(object sender, System.Windows.RoutedEventArgs e) + { + this.LogList.Clear(); + } + } +} diff --git a/FastGithub.UI/UdpLogger.cs b/FastGithub.UI/UdpLogger.cs new file mode 100644 index 00000000..5eeff40e --- /dev/null +++ b/FastGithub.UI/UdpLogger.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace FastGithub.UI +{ + static class UdpLogger + { + private static readonly byte[] buffer = new byte[ushort.MaxValue]; + private static readonly Socket socket = new Socket(SocketType.Dgram, ProtocolType.Udp); + + /// + /// 获取日志端口 + /// + public static int Port { get; } = GetAvailableUdpPort(38457); + + + static UdpLogger() + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, Port)); + } + + /// + /// 获取可用的随机Udp端口 + /// + /// + /// + /// + private static int GetAvailableUdpPort(int minValue, AddressFamily addressFamily = AddressFamily.InterNetwork) + { + var hashSet = new HashSet(); + var tcpListeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + + foreach (var endpoint in tcpListeners) + { + if (endpoint.AddressFamily == addressFamily) + { + hashSet.Add(endpoint.Port); + } + } + + for (var port = minValue; port < IPEndPoint.MaxPort; port++) + { + if (hashSet.Contains(port) == false) + { + return port; + } + } + + throw new ArgumentException("当前无可用的端口"); + } + + public static async Task GetUdpLogAsync() + { + EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); + var taskCompletionSource = new TaskCompletionSource(); + socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEP, EndReceiveFrom, taskCompletionSource); + var length = await taskCompletionSource.Task; + + var json = Encoding.UTF8.GetString(buffer, 0, length); + var log = JsonConvert.DeserializeObject(json); + if (log != null) + { + log.Message = log.Message.Replace("\"", null); + } + return log; + } + + private static void EndReceiveFrom(IAsyncResult ar) + { + EndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); + var length = socket.EndReceiveFrom(ar, ref remoteEP); + var taskCompletionSource = (TaskCompletionSource)ar.AsyncState; + taskCompletionSource.TrySetResult(length); + } + } +} diff --git a/FastGithub.UI/app.ico b/FastGithub.UI/app.ico new file mode 100644 index 00000000..8f5f6aec Binary files /dev/null and b/FastGithub.UI/app.ico differ diff --git a/FastGithub.UI/app.manifest b/FastGithub.UI/app.manifest new file mode 100644 index 00000000..603d39fb --- /dev/null +++ b/FastGithub.UI/app.manifest @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FastGithub.sln b/FastGithub.sln index 2b0900c7..2be28f97 100644 --- a/FastGithub.sln +++ b/FastGithub.sln @@ -1,15 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31320.298 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32203.90 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub", "FastGithub\FastGithub.csproj", "{C1099390-6103-4917-A740-A3002B542FE0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Core", "FastGithub.Core\FastGithub.Core.csproj", "{4E4841D2-F743-40BB-BE28-729DB53775CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Http", "FastGithub.Http\FastGithub.Http.csproj", "{B5DCB3E4-5094-4170-B844-6F395002CA42}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Dns", "FastGithub.Dns\FastGithub.Dns.csproj", "{43FF9C79-51D5-4037-AA0B-CA3006E2A7E6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.DomainResolve", "FastGithub.DomainResolve\FastGithub.DomainResolve.csproj", "{5D26ABDD-F341-4EB7-9D08-FCB80F79B4B4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Scanner", "FastGithub.Scanner\FastGithub.Scanner.csproj", "{7F24CD2F-07C0-4002-A534-80688DE95ECF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Configuration", "FastGithub.Configuration\FastGithub.Configuration.csproj", "{C63CEBB1-56DA-4AC3-BDC9-52424EC292A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.HttpServer", "FastGithub.HttpServer\FastGithub.HttpServer.csproj", "{C9807DA0-4620-445E-ABBF-57A617B8E773}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.PacketIntercept", "FastGithub.PacketIntercept\FastGithub.PacketIntercept.csproj", "{701FF90C-E651-4E0B-AE7F-84D1F17DD178}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.UI", "FastGithub.UI\FastGithub.UI.csproj", "{5082061F-38D5-4F50-945E-791C85B9BDB5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.FlowAnalyze", "FastGithub.FlowAnalyze\FastGithub.FlowAnalyze.csproj", "{93478EAF-739C-47DA-B8FE-AEBA78A75E11}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,18 +29,34 @@ Global {C1099390-6103-4917-A740-A3002B542FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C1099390-6103-4917-A740-A3002B542FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C1099390-6103-4917-A740-A3002B542FE0}.Release|Any CPU.Build.0 = Release|Any CPU - {4E4841D2-F743-40BB-BE28-729DB53775CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E4841D2-F743-40BB-BE28-729DB53775CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E4841D2-F743-40BB-BE28-729DB53775CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E4841D2-F743-40BB-BE28-729DB53775CC}.Release|Any CPU.Build.0 = Release|Any CPU - {43FF9C79-51D5-4037-AA0B-CA3006E2A7E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43FF9C79-51D5-4037-AA0B-CA3006E2A7E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43FF9C79-51D5-4037-AA0B-CA3006E2A7E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43FF9C79-51D5-4037-AA0B-CA3006E2A7E6}.Release|Any CPU.Build.0 = Release|Any CPU - {7F24CD2F-07C0-4002-A534-80688DE95ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F24CD2F-07C0-4002-A534-80688DE95ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F24CD2F-07C0-4002-A534-80688DE95ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F24CD2F-07C0-4002-A534-80688DE95ECF}.Release|Any CPU.Build.0 = Release|Any CPU + {B5DCB3E4-5094-4170-B844-6F395002CA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5DCB3E4-5094-4170-B844-6F395002CA42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5DCB3E4-5094-4170-B844-6F395002CA42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5DCB3E4-5094-4170-B844-6F395002CA42}.Release|Any CPU.Build.0 = Release|Any CPU + {5D26ABDD-F341-4EB7-9D08-FCB80F79B4B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D26ABDD-F341-4EB7-9D08-FCB80F79B4B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D26ABDD-F341-4EB7-9D08-FCB80F79B4B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D26ABDD-F341-4EB7-9D08-FCB80F79B4B4}.Release|Any CPU.Build.0 = Release|Any CPU + {C63CEBB1-56DA-4AC3-BDC9-52424EC292A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C63CEBB1-56DA-4AC3-BDC9-52424EC292A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C63CEBB1-56DA-4AC3-BDC9-52424EC292A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C63CEBB1-56DA-4AC3-BDC9-52424EC292A0}.Release|Any CPU.Build.0 = Release|Any CPU + {C9807DA0-4620-445E-ABBF-57A617B8E773}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9807DA0-4620-445E-ABBF-57A617B8E773}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9807DA0-4620-445E-ABBF-57A617B8E773}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9807DA0-4620-445E-ABBF-57A617B8E773}.Release|Any CPU.Build.0 = Release|Any CPU + {701FF90C-E651-4E0B-AE7F-84D1F17DD178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {701FF90C-E651-4E0B-AE7F-84D1F17DD178}.Debug|Any CPU.Build.0 = Debug|Any CPU + {701FF90C-E651-4E0B-AE7F-84D1F17DD178}.Release|Any CPU.ActiveCfg = Release|Any CPU + {701FF90C-E651-4E0B-AE7F-84D1F17DD178}.Release|Any CPU.Build.0 = Release|Any CPU + {5082061F-38D5-4F50-945E-791C85B9BDB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5082061F-38D5-4F50-945E-791C85B9BDB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5082061F-38D5-4F50-945E-791C85B9BDB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5082061F-38D5-4F50-945E-791C85B9BDB5}.Release|Any CPU.Build.0 = Release|Any CPU + {93478EAF-739C-47DA-B8FE-AEBA78A75E11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93478EAF-739C-47DA-B8FE-AEBA78A75E11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93478EAF-739C-47DA-B8FE-AEBA78A75E11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93478EAF-739C-47DA-B8FE-AEBA78A75E11}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FastGithub/.config/dotnet-tools.json b/FastGithub/.config/dotnet-tools.json new file mode 100644 index 00000000..2ae59961 --- /dev/null +++ b/FastGithub/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "5.0.8", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/FastGithub/AppHostedService.cs b/FastGithub/AppHostedService.cs new file mode 100644 index 00000000..39a9ee61 --- /dev/null +++ b/FastGithub/AppHostedService.cs @@ -0,0 +1,165 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub +{ + /// + /// app后台服务 + /// + sealed class AppHostedService : BackgroundService + { + private readonly IHost host; + private readonly IOptions appOptions; + private readonly IOptions fastGithubOptions; + private readonly ILogger logger; + + public AppHostedService( + IHost host, + IOptions appOptions, + IOptions fastGithubOptions, + ILogger logger) + { + this.host = host; + this.appOptions = appOptions; + this.fastGithubOptions = fastGithubOptions; + this.logger = logger; + } + + /// + /// 启动完成 + /// + /// + /// + public override Task StartAsync(CancellationToken cancellationToken) + { + var version = ProductionVersion.Current; + this.logger.LogInformation($"{nameof(FastGithub)}启动完成,当前版本为v{version},访问 https://github.com/dotnetcore/fastgithub 关注新版本"); + return base.StartAsync(cancellationToken); + } + + /// + /// 后台任务 + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Delay(TimeSpan.FromSeconds(1d), stoppingToken); + await this.CheckFastGithubProxyAsync(stoppingToken); + await this.WaitForParentProcessExitAsync(stoppingToken); + } + + + /// + /// 检测fastgithub代理设置 + /// + /// + /// + private async Task CheckFastGithubProxyAsync(CancellationToken cancellationToken) + { + if (OperatingSystem.IsWindows() == false) + { + try + { + if (await this.UseFastGithubProxyAsync() == false) + { + var httpProxyPort = this.fastGithubOptions.Value.HttpProxyPort; + this.logger.LogWarning($"请设置系统自动代理为http://{IPAddress.Loopback}:{httpProxyPort},或手动代理http/https为{IPAddress.Loopback}:{httpProxyPort}"); + } + } + catch (Exception) + { + this.logger.LogWarning("尝试获取代理信息失败"); + } + } + } + + + /// + /// 应用fastgithub代理 + /// + /// + /// + /// + private async Task UseFastGithubProxyAsync() + { + var systemProxy = HttpClient.DefaultProxy; + if (systemProxy == null) + { + return false; + } + + var domain = this.fastGithubOptions.Value.DomainConfigs.Keys.FirstOrDefault(); + if (domain == null) + { + return true; + } + + var destination = new Uri($"https://{domain.Replace('*', 'a')}"); + var proxyServer = systemProxy.GetProxy(destination); + if (proxyServer == null) + { + return false; + } + + var httpProxyPort = this.fastGithubOptions.Value.HttpProxyPort; + if (proxyServer.Port != httpProxyPort) + { + return false; + } + + if (IPAddress.TryParse(proxyServer.Host, out var address)) + { + return IPAddress.IsLoopback(address); + } + + try + { + var addresses = await Dns.GetHostAddressesAsync(proxyServer.Host); + return addresses.Any(item => IPAddress.IsLoopback(item)); + } + catch (Exception) + { + return false; + } + } + + /// + /// 等待父进程退出 + /// + /// + /// + private async Task WaitForParentProcessExitAsync(CancellationToken cancellationToken) + { + var parentId = this.appOptions.Value.ParentProcessId; + if (parentId <= 0) + { + return; + } + + try + { + Process.GetProcessById(parentId).WaitForExit(); + } + catch (Exception ex) + { + this.logger.LogError(ex, $"获取进程{parentId}异常"); + } + finally + { + this.logger.LogInformation($"正在主动关闭,因为父进程已退出"); + await this.host.StopAsync(cancellationToken); + } + } + + } +} diff --git a/FastGithub/AppOptions.cs b/FastGithub/AppOptions.cs new file mode 100644 index 00000000..41c75b0f --- /dev/null +++ b/FastGithub/AppOptions.cs @@ -0,0 +1,18 @@ +namespace FastGithub +{ + /// + /// app选项 + /// + public record AppOptions + { + /// + /// 父进程id + /// + public int ParentProcessId { get; init; } + + /// + /// udp日志服务器端口 + /// + public int UdpLoggerPort { get; init; } + } +} diff --git a/FastGithub/ConsoleUtil.cs b/FastGithub/ConsoleUtil.cs new file mode 100644 index 00000000..d24f8d51 --- /dev/null +++ b/FastGithub/ConsoleUtil.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace FastGithub +{ + static class ConsoleUtil + { + private const uint ENABLE_QUICK_EDIT = 0x0040; + + private const int STD_INPUT_HANDLE = -10; + + [DllImport("kernel32.dll", SetLastError = true)] + [SupportedOSPlatform("windows")] + private static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + [SupportedOSPlatform("windows")] + private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll", SetLastError = true)] + [SupportedOSPlatform("windows")] + private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); + + /// + /// 禁用快速编辑模式 + /// + /// + public static bool DisableQuickEdit() + { + if (OperatingSystem.IsWindows()) + { + var hwnd = GetStdHandle(STD_INPUT_HANDLE); + if (GetConsoleMode(hwnd, out uint mode)) + { + mode &= ~ENABLE_QUICK_EDIT; + return SetConsoleMode(hwnd, mode); + } + } + + return false; + } + } +} diff --git a/FastGithub/FastGithub.csproj b/FastGithub/FastGithub.csproj index ab4f45da..57ea9e41 100644 --- a/FastGithub/FastGithub.csproj +++ b/FastGithub/FastGithub.csproj @@ -1,25 +1,52 @@ - + - - Exe - enable - net5.0;net6.0 - true - 1.0.2 - + + fastgithub + Exe + MIT + app.manifest + true + - - true - + + + + + + + + - - - + + + + + + - - - PreserveNewest - - + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + PreserveNewest + + + PreserveNewest + + + + + + diff --git a/FastGithub/ProductionVersion.cs b/FastGithub/ProductionVersion.cs new file mode 100644 index 00000000..2b8fb70d --- /dev/null +++ b/FastGithub/ProductionVersion.cs @@ -0,0 +1,103 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace FastGithub +{ + /// + /// 表示产品版本 + /// + public class ProductionVersion : IComparable + { + private static readonly string? productionVersion = Assembly + .GetEntryAssembly()? + .GetCustomAttribute()? + .InformationalVersion; + + /// + /// 获取当前应用程序的产品版本 + /// + public static ProductionVersion? Current { get; } = productionVersion == null ? null : Parse(productionVersion); + + + /// + /// 版本 + /// + public Version Version { get; } + + /// + /// 子版本 + /// + public string SubVersion { get; } + + /// + /// 产品版本 + /// + /// + /// + public ProductionVersion(Version version, string subVersion) + { + this.Version = version; + this.SubVersion = subVersion; + } + + /// + /// 比较版本 + /// + /// + /// + public int CompareTo(ProductionVersion? other) + { + var x = this; + var y = other; + + if (y == null) + { + return 1; + } + + var value = x.Version.CompareTo(y.Version); + if (value == 0) + { + value = CompareSubVerson(x.SubVersion, y.SubVersion); + } + return value; + + static int CompareSubVerson(string subX, string subY) + { + if (subX.Length == 0 && subY.Length == 0) + { + return 0; + } + if (subX.Length == 0) + { + return 1; + } + if (subY.Length == 0) + { + return -1; + } + + return StringComparer.OrdinalIgnoreCase.Compare(subX, subY); + } + } + + public override string ToString() + { + return $"{Version}{SubVersion}"; + } + + /// + /// 解析 + /// + /// + /// + public static ProductionVersion Parse(string productionVersion) + { + const string VERSION = @"^\d+\.(\d+.){0,2}\d+"; + var verion = Regex.Match(productionVersion, VERSION).Value; + var subVersion = productionVersion[verion.Length..]; + return new ProductionVersion(Version.Parse(verion), subVersion); + } + } +} diff --git a/FastGithub/Program.cs b/FastGithub/Program.cs index 916f2bdc..0256fdca 100644 --- a/FastGithub/Program.cs +++ b/FastGithub/Program.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +using System; +using System.IO; namespace FastGithub { + class Program { /// @@ -10,22 +13,37 @@ class Program /// public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + ConsoleUtil.DisableQuickEdit(); + var contentRoot = Path.GetDirectoryName(Environment.ProcessPath); + if (string.IsNullOrEmpty(contentRoot) == false) + { + Environment.CurrentDirectory = contentRoot; + } + var options = new WebApplicationOptions + { + Args = args, + ContentRootPath = contentRoot + }; + CreateWebApplication(options).Run(singleton: true); } /// /// 创建host /// - /// + /// /// - public static IHostBuilder CreateHostBuilder(string[] args) + private static WebApplication CreateWebApplication(WebApplicationOptions options) { - return Host - .CreateDefaultBuilder(args) - .ConfigureServices((ctx, services) => - { - services.AddGithubDns(ctx.Configuration); - }); + var builder = WebApplication.CreateBuilder(options); + builder.ConfigureHost(); + builder.ConfigureWebHost(); + builder.ConfigureConfiguration(); + builder.ConfigureServices(); + + var app = builder.Build(); + app.ConfigureApp(); + return app; } + } } diff --git a/FastGithub/Properties/launchSettings.json b/FastGithub/Properties/launchSettings.json index b100c0a1..0a87a1b0 100644 --- a/FastGithub/Properties/launchSettings.json +++ b/FastGithub/Properties/launchSettings.json @@ -1,14 +1,7 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", +{ "profiles": { "FastGithub": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development", - "Logging__LogLevel__Default": "Trace" - } + "commandName": "Project" } } -} +} \ No newline at end of file diff --git a/FastGithub/ServiceExtensions.cs b/FastGithub/ServiceExtensions.cs new file mode 100644 index 00000000..669cb6f8 --- /dev/null +++ b/FastGithub/ServiceExtensions.cs @@ -0,0 +1,180 @@ +using FastGithub.DomainResolve; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PInvoke; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; + +namespace FastGithub +{ + /// + /// IHostBuilder扩展 + /// + static class ServiceExtensions + { + /// + /// 控制命令 + /// + private enum Command + { + Start, + Stop, + } + + [SupportedOSPlatform("linux")] + [DllImport("libc", SetLastError = true)] + private static extern uint geteuid(); + + /// + /// 使用windows服务 + /// + /// + /// + public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder) + { + return WindowsServiceLifetimeHostBuilderExtensions.UseWindowsService(hostBuilder); + } + + /// + /// 运行主机 + /// + /// + /// + public static void Run(this WebApplication app, bool singleton) + { + var logger = app.Services.GetRequiredService().CreateLogger(nameof(FastGithub)); + if (UseCommand(logger) == false) + { + using var mutex = new Mutex(true, "Global\\FastGithub", out var firstInstance); + if (singleton == false || firstInstance) + { + app.Run(); + } + else + { + logger.LogWarning($"程序将自动关闭:系统已运行其它实例"); + } + } + } + + /// + /// 使用命令 + /// + /// + /// + private static bool UseCommand(ILogger logger) + { + var args = Environment.GetCommandLineArgs(); + if (Enum.TryParse(args.Skip(1).FirstOrDefault(), true, out var cmd) == false) + { + return false; + } + + var action = cmd == Command.Start ? "启动" : "停止"; + try + { + if (OperatingSystem.IsWindows()) + { + UseCommandAtWindows(cmd); + } + else if (OperatingSystem.IsLinux()) + { + UseCommandAtLinux(cmd); + } + else + { + return false; + } + logger.LogInformation($"服务{action}成功"); + } + catch (Exception ex) + { + logger.LogError(ex.Message, $"服务{action}异常"); + } + return true; + } + + /// + /// 应用控制指令 + /// + /// + [SupportedOSPlatform("windows")] + private static void UseCommandAtWindows(Command cmd) + { + var binaryPath = Environment.GetCommandLineArgs().First(); + var serviceName = Path.GetFileNameWithoutExtension(binaryPath); + var state = true; + if (cmd == Command.Start) + { + state = ServiceInstallUtil.InstallAndStartService(serviceName, binaryPath); + } + else if (cmd == Command.Stop) + { + state = ServiceInstallUtil.StopAndDeleteService(serviceName); + } + + if (state == false) + { + throw new Win32Exception(); + } + } + + /// + /// 应用控制指令 + /// + /// + [SupportedOSPlatform("linux")] + private static void UseCommandAtLinux(Command cmd) + { + if (geteuid() != 0) + { + throw new UnauthorizedAccessException("无法操作服务:没有root权限"); + } + + var binaryPath = Path.GetFullPath(Environment.GetCommandLineArgs().First()); + var serviceName = Path.GetFileNameWithoutExtension(binaryPath); + var serviceFilePath = $"/etc/systemd/system/{serviceName}.service"; + + if (cmd == Command.Start) + { + var serviceBuilder = new StringBuilder() + .AppendLine("[Unit]") + .AppendLine($"Description={serviceName}") + .AppendLine() + .AppendLine("[Service]") + .AppendLine("Type=notify") + .AppendLine($"User={Environment.UserName}") + .AppendLine($"ExecStart={binaryPath}") + .AppendLine($"WorkingDirectory={Path.GetDirectoryName(binaryPath)}") + .AppendLine() + .AppendLine("[Install]") + .AppendLine("WantedBy=multi-user.target"); + File.WriteAllText(serviceFilePath, serviceBuilder.ToString()); + + Process.Start("chcon", $"--type=bin_t {binaryPath}").WaitForExit(); // SELinux + Process.Start("systemctl", "daemon-reload").WaitForExit(); + Process.Start("systemctl", $"start {serviceName}.service").WaitForExit(); + Process.Start("systemctl", $"enable {serviceName}.service").WaitForExit(); + } + else if (cmd == Command.Stop) + { + Process.Start("systemctl", $"stop {serviceName}.service").WaitForExit(); + Process.Start("systemctl", $"disable {serviceName}.service").WaitForExit(); + + if (File.Exists(serviceFilePath)) + { + File.Delete(serviceFilePath); + } + Process.Start("systemctl", "daemon-reload").WaitForExit(); + } + } + } +} diff --git a/FastGithub/Startup.cs b/FastGithub/Startup.cs new file mode 100644 index 00000000..16cc53fe --- /dev/null +++ b/FastGithub/Startup.cs @@ -0,0 +1,136 @@ +using FastGithub.Configuration; +using FastGithub.FlowAnalyze; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Sinks.Network; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Text.Json; + +namespace FastGithub +{ + /// + /// + /// + static class Startup + { + /// + /// ͨ + /// + /// + public static void ConfigureHost(this WebApplicationBuilder builder) + { + builder.Host.UseSystemd().UseWindowsService(); + builder.Host.UseSerilog((hosting, logger) => + { + var template = "{Timestamp:O} [{Level:u3}]{NewLine}{SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}"; + logger + .ReadFrom.Configuration(hosting.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: template) + .WriteTo.File(Path.Combine("logs", @"log.txt"), rollingInterval: RollingInterval.Day, outputTemplate: template); + + var udpLoggerPort = hosting.Configuration.GetValue(nameof(AppOptions.UdpLoggerPort), 38457); + logger.WriteTo.UDPSink(IPAddress.Loopback, udpLoggerPort); + }); + } + + /// + /// web + /// + /// + public static void ConfigureWebHost(this WebApplicationBuilder builder) + { + builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(1d)); + builder.WebHost.UseKestrel(kestrel => + { + kestrel.NoLimit(); + if (OperatingSystem.IsWindows()) + { + kestrel.ListenHttpsReverseProxy(); + kestrel.ListenHttpReverseProxy(); + kestrel.ListenSshReverseProxy(); + kestrel.ListenGitReverseProxy(); + } + else + { + kestrel.ListenHttpProxy(); + } + }); + } + + + /// + /// + /// + /// + public static void ConfigureConfiguration(this WebApplicationBuilder builder) + { + const string APPSETTINGS = "appsettings"; + if (Directory.Exists(APPSETTINGS) == true) + { + foreach (var file in Directory.GetFiles(APPSETTINGS, "appsettings.*.json")) + { + var jsonFile = Path.Combine(APPSETTINGS, Path.GetFileName(file)); + builder.Configuration.AddJsonFile(jsonFile, true, true); + } + } + } + + + /// + /// ÷ + /// + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary))] + public static void ConfigureServices(this WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + services.Configure(configuration); + services.Configure(configuration.GetSection(nameof(FastGithub))); + + services.AddConfiguration(); + services.AddDomainResolve(); + services.AddHttpClient(); + services.AddReverseProxy(); + services.AddFlowAnalyze(); + services.AddHostedService(); + + if (OperatingSystem.IsWindows()) + { + services.AddPacketIntercept(); + } + } + + /// + /// Ӧ + /// + /// + public static void ConfigureApp(this WebApplication app) + { + app.UseHttpProxyPac(); + app.UseRequestLogging(); + app.UseHttpReverseProxy(); + + app.UseRouting(); + app.DisableRequestLogging(); + + app.MapGet("/flowStatistics", context => + { + var flowStatistics = context.RequestServices.GetRequiredService().GetFlowStatistics(); + var json = JsonSerializer.Serialize(flowStatistics, FlowStatisticsContext.Default.FlowStatistics); + return context.Response.WriteAsync(json); + }); + } + } +} diff --git a/FastGithub/app.manifest b/FastGithub/app.manifest new file mode 100644 index 00000000..60a45a21 --- /dev/null +++ b/FastGithub/app.manifest @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FastGithub/appsettings.json b/FastGithub/appsettings.json index 13581ab3..e1ee1436 100644 --- a/FastGithub/appsettings.json +++ b/FastGithub/appsettings.json @@ -1,19 +1,37 @@ -{ - "Dns": { - "UpStream": "114.114.114.114" - }, - "Github": { - "ScanAllInterval": "02:00:00", // ɨʱ - "ScanResultInterval": "00:01:00", // ɨʱ - "MetaUri": "https://gitee.com/jiulang/fast-github/raw/master/FastGithub/meta.json", // ipԴļuri - "PortScanTimeout": "00:00:01", // ˿ɨ賬ʱʱ - "HttpsScanTimeout": "00:00:05" // httpsɨ賬ʱʱ +{ + // 新增的子配置文件appsettings.*.json,重启应用程序才生效 + "FastGithub": { + "HttpProxyPort": 38457, // http代理端口,linux/osx平台使用 + "FallbackDns": [ // 以下dns必须要支持tcp + "8.8.8.8:53", + "119.29.29.29:53", + "114.114.114.114:53" + ], + "DomainConfigs": { + "*.fastgithub.com": { // 域名的*表示除.之外0到多个任意字符 + "TlsSni": false, // 指示tls握手时是否发送SNI + "TlsSniPattern": null, // SNI表达式,@domain变量表示取域名值 @ipaddress变量表示取ip @random变量表示取随机值,其它字符保留不替换 + "TlsIgnoreNameMismatch": false, // 是否忽略服务器证书域名不匹配,当不发送SNI时服务器可能发回域名不匹配的证书,默认为false + "Timeout": null, // 请求超时时长,格式为"00:02:00",默认为null + "IPAddress": null, // 请求的ip,默认为null + "Destination": null, // 请求目的地,格式为绝对或相对Uri,默认null + "Response": { // 阻断请求直接响应,设置了Response其它配置都不起作用了 + "StatusCode": 404, // 响应的状态码 + "ContentType": "text/plain;charset=utf-8", // 如果有ContentValue,就要指示ContentType + "ContentValue": "这是一个用于示范配置的域名" // 自定义返回的内容,这是可选的 + } + } + } }, - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "System": "Warning", - "Microsoft": "Warning" + "Override": { + "Yarp": "Warning", + "System": "Warning", + "Microsoft": "Warning", + "Microsoft.AspNetCore.Server.Kestrel": "Error" + } } } } diff --git a/FastGithub/appsettings/appsettings.amazonaws.json b/FastGithub/appsettings/appsettings.amazonaws.json new file mode 100644 index 00000000..686af53a --- /dev/null +++ b/FastGithub/appsettings/appsettings.amazonaws.json @@ -0,0 +1,12 @@ +{ + "FastGithub": { + "DomainConfigs": { + "s3.amazonaws.com": { + "TlsIgnoreNameMismatch": true + }, + "*.s3.amazonaws.com": { + "TlsIgnoreNameMismatch": true + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.bootcss.json b/FastGithub/appsettings/appsettings.bootcss.json new file mode 100644 index 00000000..166c19fc --- /dev/null +++ b/FastGithub/appsettings/appsettings.bootcss.json @@ -0,0 +1,13 @@ +{ + "FastGithub": { + "DomainConfigs": { + "*.cloudflare.com": { + "TlsSni": true + }, + "cdn.bootcss.com": { + "TlsSni": true, + "Destination": "https://cdnjs.cloudflare.com/ajax/libs/" + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.fastly.json b/FastGithub/appsettings/appsettings.fastly.json new file mode 100644 index 00000000..2c51353a --- /dev/null +++ b/FastGithub/appsettings/appsettings.fastly.json @@ -0,0 +1,15 @@ +{ + "FastGithub": { + "DomainConfigs": { + "*.fastly.net": { + "TlsIgnoreNameMismatch": true + }, + "*.*.fastly.net": { + "TlsIgnoreNameMismatch": true + }, + "*.*.*.fastly.net": { + "TlsIgnoreNameMismatch": true + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.github.json b/FastGithub/appsettings/appsettings.github.json new file mode 100644 index 00000000..1285c4e0 --- /dev/null +++ b/FastGithub/appsettings/appsettings.github.json @@ -0,0 +1,36 @@ +{ + "FastGithub": { + "DomainConfigs": { + "github.com": { + "TlsSni": false + }, + "api.github.com": { + "TlsSni": false + }, + "githubstatus.com": { + "TlsSni": false + }, + "gist.github.com": { + "TlsIgnoreNameMismatch": true + }, + "vscode-auth.github.com": { + "TlsSni": true + }, + "*.github.com": { + "TlsIgnoreNameMismatch": true + }, + "*.github.io": { + "TlsIgnoreNameMismatch": true + }, + "*.githubapp.com": { + "TlsIgnoreNameMismatch": true + }, + "*.githubassets.com": { + "TlsIgnoreNameMismatch": true + }, + "*.githubusercontent.com": { + "TlsIgnoreNameMismatch": true + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.google.json b/FastGithub/appsettings/appsettings.google.json new file mode 100644 index 00000000..a61b140b --- /dev/null +++ b/FastGithub/appsettings/appsettings.google.json @@ -0,0 +1,25 @@ +{ + "FastGithub": { + "DomainConfigs": { + "ajax.googleapis.com": { + "TlsSni": true, + "Destination": "https://gapis.geekzu.org/ajax/" + }, + "fonts.googleapis.com": { + "TlsSni": true, + "Destination": "https://fonts.geekzu.org/" + }, + "themes.googleusercontent.com": { + "TlsSni": true, + "Destination": "https://gapis.geekzu.org/g-themes/" + }, + "fonts.gstatic.com": { + "TlsSni": true, + "Destination": "https://gapis.geekzu.org/g-fonts/" + }, + "*.gravatar.com": { + "TlsIgnoreNameMismatch": true + } + } + } +} diff --git a/FastGithub/appsettings/appsettings.imgur.json b/FastGithub/appsettings/appsettings.imgur.json new file mode 100644 index 00000000..f5c364a2 --- /dev/null +++ b/FastGithub/appsettings/appsettings.imgur.json @@ -0,0 +1,15 @@ +{ + "FastGithub": { + "DomainConfigs": { + "imgur.com": { + "TlsIgnoreNameMismatch": true + }, + "*.imgur.com": { + "TlsIgnoreNameMismatch": true + }, + "*.*.imgur.com": { + "TlsIgnoreNameMismatch": true + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.microsoft.json b/FastGithub/appsettings/appsettings.microsoft.json new file mode 100644 index 00000000..7222bf15 --- /dev/null +++ b/FastGithub/appsettings/appsettings.microsoft.json @@ -0,0 +1,85 @@ +{ + "FastGithub": { + "DomainConfigs": { + "azure.com": { + "TlsSni": true + }, + "*.azure.com": { + "TlsSni": true + }, + "*.*.azure.com": { + "TlsSni": true + }, + "*.*.*.azure.com": { + "TlsSni": true + }, + "*.azureedge.net": { + "TlsSni": true + }, + "*.visualstudio.com": { + "TlsSni": true + }, + "*.*.visualstudio.com": { + "TlsSni": true + }, + "*.live.com": { + "TlsSni": true + }, + "*.*.live.com": { + "TlsSni": true + }, + "microsoftonline.com": { + "TlsSni": true + }, + "*.microsoftonline.com": { + "TlsSni": true + }, + "windows.net": { + "TlsSni": true + }, + "*.windows.net": { + "TlsSni": true + }, + "*.*.windows.net": { + "TlsSni": true + }, + "azurewebsites.net": { + "TlsSni": true + }, + "*.azurewebsites.net": { + "TlsSni": true + }, + "*.*.azurewebsites.net": { + "TlsSni": true + }, + "*.vsassets.io": { + "TlsSni": true + }, + "aadcdn.msauth.net": { + "TlsSni": true + }, + "aadcdn.msftauth.net": { + "TlsSni": true + }, + "static2.sharepointonline.com": { + "TlsSni": true + }, + "az764295.vo.msecnd.net": { + "TlsSni": true, + "Destination": "https://vscode.cdn.azure.cn/" + }, + "*.*.msecnd.net": { + "TlsSni": true + }, + "*.aspnetcdn.com": { + "TlsSni": true + }, + "onedrive.live.com": { + "TlsSni": false + }, + "*.onedrive.live.com": { + "TlsSni": false + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.packages.json b/FastGithub/appsettings/appsettings.packages.json new file mode 100644 index 00000000..2471588d --- /dev/null +++ b/FastGithub/appsettings/appsettings.packages.json @@ -0,0 +1,12 @@ +{ + "FastGithub": { + "DomainConfigs": { + "*.nuget.org": { + "TlsSni": true + }, + "*.maven.org": { + "TlsSni": true + } + } + } +} \ No newline at end of file diff --git a/FastGithub/appsettings/appsettings.v2ex.json b/FastGithub/appsettings/appsettings.v2ex.json new file mode 100644 index 00000000..fb5326b2 --- /dev/null +++ b/FastGithub/appsettings/appsettings.v2ex.json @@ -0,0 +1,12 @@ +{ + "FastGithub": { + "DomainConfigs": { + "v2ex.com": { + "TlsSni": false + }, + "*.v2ex.com": { + "TlsSni": false + } + } + } +} \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..5a33b82b --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,3 @@ +major-version-bump-message: "(breaking|major):" +minor-version-bump-message: "(feature|minor):" +patch-version-bump-message: "(fix|patch):" diff --git a/MacOSXConfig.md b/MacOSXConfig.md new file mode 100644 index 00000000..8735afaa --- /dev/null +++ b/MacOSXConfig.md @@ -0,0 +1,27 @@ +# MacOSX运行FastGithub配置教程 + +### 1 解决 "XXX" cannot be opened because the developer cannot be verified +打开终端进入FastGithub执行文件所在路径执行命令: +`sudo xattr -d com.apple.quarantine *.*` + +### 2 安装证书 +打开FastGithub后,目录内会生成cacert目录,双击打开fastgithub.cer,系统弹出Keychain Access窗口,列表中双击FastGitHub,弹出证书详情窗口,展开Trust并选择Always Trust。 + + + + + +### 3 配置代理 +#### 3.1 自动代理 +打开mac设置,网络,点击高级,选择代理,勾选网自动代理配置,填写FastGithub窗口提示的地址 + + + + + +#### 3.2 手动代理 +打开mac设置,网络,点击高级,选择代理,勾选网页代理(HTTP)及安全网页代理(HTTPS),填写FastGithub窗口提示的地址 + + + + diff --git a/README.html b/README.html new file mode 100644 index 00000000..dd262738 --- /dev/null +++ b/README.html @@ -0,0 +1,431 @@ + + + +README.md + + + + + + + + + + + + +

FastGithub

+

github加速神器,解决github打不开、用户头像无法加载、releases无法上传下载、git-clone、git-pull、git-push失败等问题。

+

1 程序下载

+ +

2 部署方式

+

2.1 windows-x64桌面

+
    +
  • 双击运行FastGithub.UI.exe
  • +
+

2.2 windows-x64服务

+
    +
  • fastgithub.exe start // 以windows服务安装并启动
  • +
  • fastgithub.exe stop // 以windows服务卸载并删除
  • +
+

2.3 linux-x64终端

+
    +
  • sudo ./fastgithub
  • +
  • 设置系统自动代理为http://127.0.0.1:38457,或手动代理http/https为127.0.0.1:38457
  • +
+

2.4 linux-x64服务

+
    +
  • sudo ./fastgithub start // 以systemd服务安装并启动
  • +
  • sudo ./fastgithub stop // 以systemd服务卸载并删除
  • +
  • 设置系统自动代理为http://127.0.0.1:38457,或手动代理http/https为127.0.0.1:38457
  • +
+

2.5 macOS-x64

+
    +
  • 双击运行fastgithub
  • +
  • 安装cacert/fastgithub.cer并设置信任
  • +
  • 设置系统自动代理为http://127.0.0.1:38457,或手动代理http/https为127.0.0.1:38457
  • +
  • 具体配置详情
  • +
+

2.6 docker-compose一键部署

+
    +
  • 准备好docker 18.09, docker-compose.
  • +
  • 在源码目录下,有一个docker-compose.yaml 文件,专用于在实际项目中,临时使用github.com源码,而做的demo配置。
  • +
  • 根据自己的需要更新docker-compose.yaml中的sample和build镜像即可完成拉github.com源码加速,并基于源码做后续的操作。
  • +
+

3 软件功能

+
    +
  • 提供域名的纯净IP解析;
  • +
  • 提供IP测速并选择最快的IP;
  • +
  • 提供域名的tls连接自定义配置;
  • +
  • google的CDN资源替换,解决大量国外网站无法加载js和css的问题;
  • +
+

4 证书验证

+

4.1 git

+

git操作提示SSL certificate problem
+需要关闭git的证书验证:git config --global http.sslverify false

+

4.2 firefox

+

firefox提示连接有潜在的安全问题
+设置->隐私与安全->证书->查看证书->证书颁发机构,导入cacert/fastgithub.cer,勾选“信任由此证书颁发机构来标识网站”

+

5 安全性说明

+

FastGithub为每台不同的主机生成自颁发CA证书,保存在cacert文件夹下。客户端设备需要安装和无条件信任自颁发的CA证书,请不要将证书私钥泄露给他人,以免造成损失。

+

6 合法性说明

+

《国际联网暂行规定》第六条规定:“计算机信息网络直接进行国际联网,必须使用邮电部国家公用电信网提供的国际出入口信道。任何单位和个人不得自行建立或者使用其他信道进行国际联网。” +FastGithub本地代理使用的都是“公用电信网提供的国际出入口信道”,从国外Github服务器到国内用户电脑上FastGithub程序的流量,使用的是正常流量通道,其间未对流量进行任何额外加密(仅有网页原有的TLS加密,区别于VPN的流量加密),而FastGithub获取到网页数据之后发生的整个代理过程完全在国内,不再适用国际互联网相关之规定。

+ + + diff --git a/README.md b/README.md index ad7f9377..3e7e595e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,59 @@ # FastGithub -github定制版的dns服务,解析访问github最快的ip +github加速神器,解决github打不开、用户头像无法加载、releases无法上传下载、git-clone、git-pull、git-push失败等问题。 -### 加速原理 -* 使用github公开的ip范围,扫描所有可用的ip; -* 间隔指定时间(5min)检测与记录扫描到的ip的访问耗时; -* 拦截dns,访问github时,返回最快的ip; +### 1 写在前面 +* **fastgithub不具备“翻墙”功能,也没有相关的计划** +* **fastgithub不支持Windows7等已被发行方停止支持的操作系统,并且也不会主动提供支持** +* **fastgithub不能为您的游戏加速** +* **fastgithub没有主动在github之外的任何渠道发布** + +### 2 部署方式 +#### 2.1 windows-x64桌面 +* 双击运行FastGithub.UI.exe -### 使用说明 -在局域网服务器(没有就使用本机)运行本程序,将网络连接的dns设置为程序运行的机器的ip。 +#### 2.2 windows-x64服务 +* `fastgithub.exe start` // 以windows服务安装并启动 +* `fastgithub.exe stop` // 以windows服务卸载并删除 + +#### 2.3 linux-x64终端 +* `sudo ./fastgithub` +* 设置系统自动代理为`http://127.0.0.1:38457`,或手动代理http/https为`127.0.0.1:38457` + +#### 2.4 linux-x64服务 +* `sudo ./fastgithub start` // 以systemd服务安装并启动 +* `sudo ./fastgithub stop` // 以systemd服务卸载并删除 +* 设置系统自动代理为`http://127.0.0.1:38457`,或手动代理http/https为`127.0.0.1:38457` + +#### 2.5 macOS-x64 +* 双击运行fastgithub +* 安装cacert/fastgithub.cer并设置信任 +* 设置系统自动代理为`http://127.0.0.1:38457`,或手动代理http/https为`127.0.0.1:38457` +* [具体配置详情](https://github.com/dotnetcore/FastGithub/blob/master/MacOSXConfig.md) + +#### 2.6 docker-compose一键部署 +* 准备好docker 18.09, docker-compose. +* 在源码目录下,有一个docker-compose.yaml 文件,专用于在实际项目中,临时使用github.com源码,而做的demo配置。 +* 根据自己的需要更新docker-compose.yaml中的sample和build镜像即可完成拉github.com源码加速,并基于源码做后续的操作。 + +### 3 软件功能 +* 提供域名的纯净IP解析; +* 提供IP测速并选择最快的IP; +* 提供域名的tls连接自定义配置; +* google的CDN资源替换,解决大量国外网站无法加载js和css的问题; + +### 4 证书验证 +#### 4.1 git +git操作提示`SSL certificate problem`
+需要关闭git的证书验证:`git config --global http.sslverify false` + +#### 4.2 firefox +firefox提示`连接有潜在的安全问题`
+设置->隐私与安全->证书->查看证书->证书颁发机构,导入cacert/fastgithub.cer,勾选“信任由此证书颁发机构来标识网站” + + +### 5 安全性说明 +FastGithub为每台不同的主机生成自颁发CA证书,保存在cacert文件夹下。客户端设备需要安装和无条件信任自颁发的CA证书,请不要将证书私钥泄露给他人,以免造成损失。 + +### 6 合法性说明 +《国际联网暂行规定》第六条规定:“计算机信息网络直接进行国际联网,必须使用邮电部国家公用电信网提供的国际出入口信道。任何单位和个人不得自行建立或者使用其他信道进行国际联网。” +FastGithub本地代理使用的都是“公用电信网提供的国际出入口信道”,从国外Github服务器到国内用户电脑上FastGithub程序的流量,使用的是正常流量通道,其间未对流量进行任何额外加密(仅有网页原有的TLS加密,区别于VPN的流量加密),而FastGithub获取到网页数据之后发生的整个代理过程完全在国内,不再适用国际互联网相关之规定。 diff --git a/Resources/MacOSXConfig/KeychainAccess.png b/Resources/MacOSXConfig/KeychainAccess.png new file mode 100644 index 00000000..87a422f0 Binary files /dev/null and b/Resources/MacOSXConfig/KeychainAccess.png differ diff --git a/Resources/MacOSXConfig/autoproxy.png b/Resources/MacOSXConfig/autoproxy.png new file mode 100644 index 00000000..b8e6d10d Binary files /dev/null and b/Resources/MacOSXConfig/autoproxy.png differ diff --git a/Resources/MacOSXConfig/cmdwin.png b/Resources/MacOSXConfig/cmdwin.png new file mode 100644 index 00000000..f40d8fca Binary files /dev/null and b/Resources/MacOSXConfig/cmdwin.png differ diff --git a/Resources/MacOSXConfig/proxy.png b/Resources/MacOSXConfig/proxy.png new file mode 100644 index 00000000..644e4237 Binary files /dev/null and b/Resources/MacOSXConfig/proxy.png differ diff --git a/Resources/MacOSXConfig/trust.png b/Resources/MacOSXConfig/trust.png new file mode 100644 index 00000000..ce323e64 Binary files /dev/null and b/Resources/MacOSXConfig/trust.png differ diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..f86dff3e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,34 @@ +version: "3.7" +services: + fastgithub: + image: slcnx/fastgithub + network_mode: host + restart: always + volumes: + - cacert:/fastgithub/cacert/ + sample: + depends_on: + - fastgithub + image: slcnx/ubuntu:18.04 + volumes: + - cacert:/tmp/cacert + - build_data:/build_data + working_dir: /build_data + restart: on-failure + tty: true + entrypoint: sh -c 'cp /tmp/cacert/fastgithub.cer /usr/local/share/ca-certificates/fastgithub.crt && update-ca-certificates && git clone https://github.com/kubernetes/kubernetes.git' + command: "" + environment: + https_proxy: http://127.0.0.1:38457 + http_proxy: http://127.0.0.1:38457 + network_mode: host + build: + working_dir: /build_data + depends_on: + - sample + image: nginx + volumes: + - build_data:/build_data +volumes: + cacert: {} + build_data: {} diff --git a/pack.sh b/pack.sh new file mode 100644 index 00000000..694b7449 --- /dev/null +++ b/pack.sh @@ -0,0 +1,25 @@ +#! /bin/bash +cd ./publish + +# win-x64 +zip -r fastgithub_win-x64.zip fastgithub_win-x64 + +# linux-x64 +chmod 777 ./fastgithub_linux-x64/fastgithub +chmod 777 ./fastgithub_linux-x64/dnscrypt-proxy/dnscrypt-proxy +zip -r fastgithub_linux-x64.zip fastgithub_linux-x64 + +# linux-arm64 +chmod 777 ./fastgithub_linux-arm64/fastgithub +chmod 777 ./fastgithub_linux-arm64/dnscrypt-proxy/dnscrypt-proxy +zip -r fastgithub_linux-arm64.zip fastgithub_linux-arm64 + +# osx-x64 +chmod 777 ./fastgithub_osx-x64/fastgithub +chmod 777 ./fastgithub_osx-x64/dnscrypt-proxy/dnscrypt-proxy +zip -r fastgithub_osx-x64.zip fastgithub_osx-x64 + +# osx-arm64 +chmod 777 ./fastgithub_osx-arm64/fastgithub +chmod 777 ./fastgithub_osx-arm64/dnscrypt-proxy/dnscrypt-proxy +zip -r fastgithub_osx-arm64.zip fastgithub_osx-arm64 diff --git a/publish.cmd b/publish.cmd new file mode 100644 index 00000000..d43fde9a --- /dev/null +++ b/publish.cmd @@ -0,0 +1,8 @@ +set output=./publish +if exist "%output%" rd /S /Q "%output%" +dotnet publish -c Release -o "%output%/fastgithub_win-x64" ./FastGithub.UI/FastGithub.UI.csproj +dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r win-x64 -o "%output%/fastgithub_win-x64" ./FastGithub/FastGithub.csproj +dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r linux-x64 -o "%output%/fastgithub_linux-x64" ./FastGithub/FastGithub.csproj +dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r linux-arm64 -o "%output%/fastgithub_linux-arm64" ./FastGithub/FastGithub.csproj +dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r osx-x64 -o "%output%/fastgithub_osx-x64" ./FastGithub/FastGithub.csproj +dotnet publish -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true --self-contained -r osx-arm64 -o "%output%/fastgithub_osx-arm64" ./FastGithub/FastGithub.csproj \ No newline at end of file