├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── pkg.yml │ └── update.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── hosts-cli.js ├── fcm-hosts ├── hosts ├── next-hosts ├── package-lock.json ├── package.json ├── src ├── IpManage │ ├── SpeedTest.ts │ └── index.ts ├── cli.ts ├── constants.ts ├── dns │ ├── base.ts │ ├── choice.ts │ ├── https.ts │ ├── index.ts │ └── ipaddress.ts ├── hosts.ts ├── index.ts ├── log.ts └── utils.ts ├── template.md ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | bin/hosts-cli.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'magic', 4 | 'magic/typescript', 5 | ], 6 | rules: {} 7 | }; 8 | -------------------------------------------------------------------------------- /.github/workflows/pkg.yml: -------------------------------------------------------------------------------- 1 | name: pkg 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build_x64_executables: 10 | runs-on: ubuntu-18.04 11 | strategy: 12 | matrix: 13 | pkg_target_without_node: 14 | - mac-x64 15 | - win-x64 16 | - linuxstatic-x64 17 | - linux-x64 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3.1.1 21 | with: 22 | node-version: '14' 23 | - run: npm ci 24 | - run: | 25 | set -xeu 26 | npm run build 27 | ./node_modules/.bin/pkg --out-path=hosts-server-pkg-${{ matrix.pkg_target_without_node }} --targets=node16-${{ matrix.pkg_target_without_node }} . 28 | - name: tar.gz or zip 29 | run: | 30 | set -xeu 31 | if [ "${{ matrix.pkg_target_without_node }}" = "win-x64" ]; then 32 | zip -r hosts-server-pkg-${{ matrix.pkg_target_without_node }}.zip ./hosts-server-pkg-${{ matrix.pkg_target_without_node }} 33 | else 34 | tar czvf hosts-server-pkg-${{ matrix.pkg_target_without_node }}.tar.gz ./hosts-server-pkg-${{ matrix.pkg_target_without_node }} 35 | fi 36 | - uses: actions/upload-artifact@v3 37 | with: 38 | name: build_x64 39 | path: | 40 | hosts-server-pkg-*.tar.gz 41 | hosts-server-pkg-*.zip 42 | 43 | build_arm_executables: 44 | runs-on: ubuntu-18.04 45 | strategy: 46 | matrix: 47 | pkg_target_without_node: 48 | - mac-arm64 49 | - win-arm64 50 | - linuxstatic-arm64 51 | - linuxstatic-armv7 52 | - linux-arm64 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: Set up QEMU 56 | uses: docker/setup-qemu-action@v1 57 | - run: | 58 | set -xeu 59 | # NOTE: node:16 image causes an error "glob error [Error: EACCES: permission denied, scandir '/root/.npm/_logs'] { ..." 60 | docker run --rm -i -v $PWD:/app --platform=linux/arm64/v8 node:14 bash << 'EOF' 61 | set -xeu 62 | # Install ldid for macos-arm64 signing 63 | curl -LO https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus2/ldid_linux_aarch64 64 | chmod +x ldid_linux_aarch64 65 | mv ./ldid_linux_aarch64 /usr/local/bin/ldid 66 | cd /app 67 | npm ci 68 | npm run build 69 | ./node_modules/.bin/pkg --out-path=hosts-server-pkg-${{ matrix.pkg_target_without_node }} --targets=node16-${{ matrix.pkg_target_without_node }} . 70 | EOF 71 | - name: tar.gz or zip 72 | run: | 73 | set -xeu 74 | if [ "${{ matrix.pkg_target_without_node }}" = "win-arm64" ]; then 75 | zip -r hosts-server-pkg-${{ matrix.pkg_target_without_node }}.zip ./hosts-server-pkg-${{ matrix.pkg_target_without_node }} 76 | else 77 | tar czvf hosts-server-pkg-${{ matrix.pkg_target_without_node }}.tar.gz ./hosts-server-pkg-${{ matrix.pkg_target_without_node }} 78 | fi 79 | - uses: actions/upload-artifact@v3 80 | with: 81 | name: build_arm 82 | path: | 83 | hosts-server-pkg-*.tar.gz 84 | hosts-server-pkg-*.zip 85 | 86 | macos_operational_test: 87 | runs-on: macos-10.15 88 | needs: build_x64_executables 89 | steps: 90 | - uses: actions/download-artifact@v3 91 | with: 92 | name: build_x64 93 | path: . 94 | - name: Unarchive tar.gz 95 | run: tar xvf hosts-server-pkg-mac-x64.tar.gz 96 | - name: Operational test 97 | run: | 98 | set -xeu 99 | # Run a server in background 100 | ./hosts-server-pkg-mac-x64/hosts-server --port=8080 &> ./hosts-server.log & 101 | # Get server PID 102 | server_pid=$! 103 | # Wait for server running 104 | sleep 10 105 | # Get data as a file 106 | curl localhost:8080 > /tmp/hosts.txt 107 | # Print downloaded file 108 | cat /tmp/hosts.txt 109 | # Print server's log 110 | cat ./hosts-server.log 111 | # Stop the server 112 | kill $server_pid 113 | 114 | windows_operational_test: 115 | runs-on: windows-2019 116 | needs: build_x64_executables 117 | steps: 118 | - uses: actions/download-artifact@v3 119 | with: 120 | name: build_x64 121 | path: . 122 | - name: Unarchive zip 123 | run: unzip hosts-server-pkg-win-x64.zip 124 | - name: Operational test 125 | run: | 126 | # Run a server in background 127 | $args = @("--port=8080") 128 | $server_pid = Start-Process -PassThru -FilePath .\hosts-server-pkg-win-x64\hosts-server.exe -ArgumentList $args | foreach { $_.Id } 129 | # Wait for server running 130 | sleep 10 131 | # Get data as a file 132 | curl localhost:8080 > C:\Temp\hosts.txt 133 | # Print downloaded file 134 | cat C:\Temp\hosts.txt 135 | # Stop the server 136 | kill $server_pid 137 | 138 | linux_operational_test: 139 | runs-on: ubuntu-18.04 140 | needs: build_x64_executables 141 | steps: 142 | - uses: actions/download-artifact@v3 143 | with: 144 | name: build_x64 145 | path: . 146 | - name: Unarchive tar.gz 147 | run: tar xvf hosts-server-pkg-linuxstatic-x64.tar.gz 148 | - name: Operational test 149 | run: | 150 | set -xeu 151 | # Run a server in background 152 | ./hosts-server-pkg-linuxstatic-x64/hosts-server --port=8080 &> ./hosts-server.log & 153 | # Get server PID 154 | server_pid=$! 155 | # Wait for server running 156 | sleep 10 157 | # Get data as a file 158 | curl localhost:8080 > /tmp/hosts.txt 159 | # Print downloaded file 160 | cat /tmp/hosts.txt 161 | # Print server's log 162 | cat ./hosts-server.log 163 | # Stop the server 164 | kill $server_pid 165 | 166 | release_executables: 167 | if: startsWith( github.ref, 'refs/tags/') 168 | needs: 169 | - macos_operational_test 170 | - windows_operational_test 171 | - linux_operational_test 172 | - build_arm_executables 173 | runs-on: ubuntu-18.04 174 | steps: 175 | - uses: actions/download-artifact@v3 176 | with: 177 | name: build_x64 178 | path: . 179 | - uses: actions/download-artifact@v3 180 | with: 181 | name: build_arm 182 | path: . 183 | - run: | 184 | set -xeu 185 | mkdir ./publish_dir 186 | mv hosts-server-pkg-* ./publish_dir 187 | # Show and create checksums 188 | (cd publish_dir && sha256sum * | tee /dev/stderr > sha256sums.txt) 189 | 190 | # create release 191 | - name: release 192 | uses: softprops/action-gh-release@v1 193 | if: startsWith(github.ref, 'refs/tags/') 194 | with: 195 | files: "publish_dir/**" 196 | env: 197 | GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }} 198 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: UpdateHosts 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - "v*.*.*" 7 | schedule: 8 | - cron: '0 */2 * * *' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [14] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Update hosts 29 | run: npm run make 30 | 31 | - name: Commit 32 | id: commit 33 | run: | 34 | git config --global user.email hostsBot@fake.com 35 | git config --global user.name hostsBot 36 | git add . 37 | git commit -m "Update hosts" 38 | continue-on-error: true 39 | 40 | - name: Check on failures 41 | if: steps.commit.outputs.status == 'failure' 42 | run: exit 1 43 | 44 | - name: Push changes 45 | uses: ad-m/github-push-action@master 46 | with: 47 | github_token: ${{ secrets.COMMIT_TOKEN }} 48 | 49 | - name: Move files 50 | run: | 51 | mkdir ./public 52 | mv ./README.md ./public/ 53 | 54 | - name: GitHub Pages 55 | uses: peaceiris/actions-gh-pages@v3 56 | with: 57 | github_token: ${{ secrets.COMMIT_TOKEN }} 58 | publish_dir: ./public 59 | enable_jekyll: true 60 | keep_files: true 61 | 62 | git-mirror: 63 | needs: build 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: wearerequired/git-mirror-action@v1 67 | env: 68 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 69 | with: 70 | source-repo: 'git@github.com:ineo6/hosts.git' 71 | destination-repo: 'git@gitlab.com:ineo6/hosts.git' 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .idea 3 | dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ineo6 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FCM Hosts 2 | 3 | Android 14+或某些时期可能需要这个 4 | 5 | ## 新方案 6 | 7 | hosts方案有时候也会断连。现在推荐dns分流方案: 8 | 9 | 0. 如果以前设置过hosts,请删掉 10 | 1. 添加`geosite:googlefcm`策略,DNS使用国内doh服务。注意,无论你设置了`cn`还是`!cn`策略,保证它是最后一条 11 | 2. `proxy_group`中添加谷歌FCM,并设为直连 12 | 3. 如果仍然连接失败且发现解析到IPv6,关闭IPv6 13 | 14 | ![image](https://github.com/user-attachments/assets/ca5e614e-9916-4193-938a-8da71b31962f) 15 | 16 | ## hosts方案 17 | 18 |
展开/收起 19 | 20 | ``` 21 | 142.250.157.188 mtalk.google.com 22 | 74.125.200.188 alt1-mtalk.google.com 23 | 142.250.141.188 alt2-mtalk.google.com 24 | 64.233.171.188 alt3-mtalk.google.com 25 | 173.194.202.188 alt4-mtalk.google.com 26 | 74.125.126.188 alt5-mtalk.google.com 27 | 142.250.115.188 alt6-mtalk.google.com 28 | 108.177.104.188 alt7-mtalk.google.com 29 | 142.250.152.188 alt8-mtalk.google.com 30 | 180.163.151.161 dl.google.com 31 | 180.163.150.33 dl.l.google.com 32 | ``` 33 | 34 | 如果你的手机上装了 Magisk,也可以考虑使用 [systemless-fcm-hosts](https://github.com/Goooler/systemless-fcm-hosts) 集成 35 | 36 | ## 规则订阅 37 | 38 | https://gcore.jsdelivr.net/gh/entr0pia/fcm-hosts@fcm/fcm-hosts 39 | 40 | https://github.com/entr0pia/fcm-hosts/raw/fcm/fcm-hosts 41 | 42 |
43 | 44 | 45 | 46 | ## 测试 47 | 48 | 可以使用 [FCM Toolbox](https://github.com/SimonMarquis/FCM-toolbox) 测试消息送达的情况 49 | 50 | -------------------------------------------------------------------------------- /bin/hosts-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cli'); 4 | -------------------------------------------------------------------------------- /fcm-hosts: -------------------------------------------------------------------------------- 1 | 142.250.157.188 mtalk.google.com 2 | 74.125.200.188 alt1-mtalk.google.com 3 | 142.250.141.188 alt2-mtalk.google.com 4 | 64.233.171.188 alt3-mtalk.google.com 5 | 173.194.202.188 alt4-mtalk.google.com 6 | 74.125.126.188 alt5-mtalk.google.com 7 | 142.250.115.188 alt6-mtalk.google.com 8 | 108.177.104.188 alt7-mtalk.google.com 9 | 142.250.152.188 alt8-mtalk.google.com 10 | 180.163.151.161 dl.google.com 11 | 180.163.150.33 dl.l.google.com 12 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | # 地址可能会变动,请务必关注GitHub、Gitlab获取最新消息 2 | # 也可以关注公众号:湖中剑,保证不迷路 3 | # GitHub Host Start 4 | 5 | 185.199.108.154 github.githubassets.com 6 | 140.82.114.21 central.github.com 7 | 185.199.110.133 desktop.githubusercontent.com 8 | 185.199.110.153 assets-cdn.github.com 9 | 185.199.109.133 camo.githubusercontent.com 10 | 185.199.108.133 github.map.fastly.net 11 | 151.101.193.194 github.global.ssl.fastly.net 12 | 140.82.112.4 gist.github.com 13 | 185.199.111.153 github.io 14 | 140.82.112.4 github.com 15 | 140.82.113.6 api.github.com 16 | 185.199.110.133 raw.githubusercontent.com 17 | 185.199.110.133 user-images.githubusercontent.com 18 | 185.199.109.133 favicons.githubusercontent.com 19 | 185.199.108.133 avatars5.githubusercontent.com 20 | 185.199.108.133 avatars4.githubusercontent.com 21 | 185.199.109.133 avatars3.githubusercontent.com 22 | 185.199.109.133 avatars2.githubusercontent.com 23 | 185.199.108.133 avatars1.githubusercontent.com 24 | 185.199.110.133 avatars0.githubusercontent.com 25 | 185.199.108.133 avatars.githubusercontent.com 26 | 140.82.112.9 codeload.github.com 27 | 54.231.162.225 github-cloud.s3.amazonaws.com 28 | 52.216.177.3 github-com.s3.amazonaws.com 29 | 54.231.198.33 github-production-release-asset-2e65be.s3.amazonaws.com 30 | 52.217.130.81 github-production-user-asset-6210df.s3.amazonaws.com 31 | 52.217.85.108 github-production-repository-file-5c1aeb.s3.amazonaws.com 32 | 185.199.111.153 githubstatus.com 33 | 140.82.112.18 github.community 34 | 185.199.111.133 media.githubusercontent.com 35 | 185.199.111.133 objects.githubusercontent.com 36 | 185.199.111.133 raw.github.com 37 | 20.221.80.166 copilot-proxy.githubusercontent.com 38 | 39 | # Please Star : https://github.com/ineo6/hosts 40 | # Mirror Repo : https://gitlab.com/ineo6/hosts 41 | 42 | # Update at: 2023-11-22 14:14:16 43 | 44 | # GitHub Host End -------------------------------------------------------------------------------- /next-hosts: -------------------------------------------------------------------------------- 1 | # 地址可能会变动,请务必关注GitHub、Gitlab获取最新消息 2 | # 也可以关注公众号:湖中剑,保证不迷路 3 | # GitHub Host Start 4 | 5 | 185.199.108.154 github.githubassets.com 6 | 140.82.114.21 central.github.com 7 | 185.199.110.133 desktop.githubusercontent.com 8 | 185.199.110.153 assets-cdn.github.com 9 | 185.199.109.133 camo.githubusercontent.com 10 | 185.199.108.133 github.map.fastly.net 11 | 151.101.193.194 github.global.ssl.fastly.net 12 | 140.82.112.4 gist.github.com 13 | 185.199.111.153 github.io 14 | 140.82.112.4 github.com 15 | 140.82.113.6 api.github.com 16 | 185.199.110.133 raw.githubusercontent.com 17 | 185.199.110.133 user-images.githubusercontent.com 18 | 185.199.109.133 favicons.githubusercontent.com 19 | 185.199.108.133 avatars5.githubusercontent.com 20 | 185.199.108.133 avatars4.githubusercontent.com 21 | 185.199.109.133 avatars3.githubusercontent.com 22 | 185.199.109.133 avatars2.githubusercontent.com 23 | 185.199.108.133 avatars1.githubusercontent.com 24 | 185.199.110.133 avatars0.githubusercontent.com 25 | 185.199.108.133 avatars.githubusercontent.com 26 | 140.82.112.9 codeload.github.com 27 | 54.231.162.225 github-cloud.s3.amazonaws.com 28 | 52.216.177.3 github-com.s3.amazonaws.com 29 | 54.231.198.33 github-production-release-asset-2e65be.s3.amazonaws.com 30 | 52.217.130.81 github-production-user-asset-6210df.s3.amazonaws.com 31 | 52.217.85.108 github-production-repository-file-5c1aeb.s3.amazonaws.com 32 | 185.199.111.153 githubstatus.com 33 | 140.82.112.18 github.community 34 | 185.199.111.133 media.githubusercontent.com 35 | 185.199.111.133 objects.githubusercontent.com 36 | 185.199.111.133 raw.github.com 37 | 20.221.80.166 copilot-proxy.githubusercontent.com 38 | 39 | # Please Star : https://github.com/ineo6/hosts 40 | # Mirror Repo : https://gitlab.com/ineo6/hosts 41 | 42 | # Update at: 2023-11-22 14:14:16 43 | 44 | # GitHub Host End -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hosts-server", 3 | "version": "1.0.1", 4 | "description": "GitHub最新hosts文件", 5 | "main": "src/index.ts", 6 | "bin": { 7 | "hosts-server": "bin/hosts-cli.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "make": "ts-node ./src/index.ts --type=default", 12 | "pkg:mac": "pkg . -t node14-mac-x64 -o app-mac", 13 | "pkg:win": "pkg . -t node14-win-x64 -o app-win" 14 | }, 15 | "author": "ineo6", 16 | "license": "MIT", 17 | "dependencies": { 18 | "chalk": "^4.1.2", 19 | "cheerio": "1.0.0-rc.10", 20 | "dayjs": "^1.10.4", 21 | "debug": "^4.3.4", 22 | "dns-over-http": "^0.2.0", 23 | "dns-over-tls": "^0.0.8", 24 | "is-browser": "^2.1.0", 25 | "lodash.unionby": "^4.8.0", 26 | "log": "^6.3.1", 27 | "lru-cache": "^7.8.1", 28 | "node-fetch": "^2.6.1", 29 | "portfinder": "^1.0.28", 30 | "yargs-parser": "^21.0.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/eslint-parser": "^7.17.0", 34 | "@types/debug": "^4.1.7", 35 | "@types/lodash.unionby": "^4.8.6", 36 | "@types/node-fetch": "^2.6.1", 37 | "@types/yargs-parser": "^21.0.0", 38 | "@typescript-eslint/eslint-plugin": "^5.19.0", 39 | "@typescript-eslint/parser": "^5.19.0", 40 | "eslint": "7.32.0", 41 | "eslint-config-magic": "^2.3.0", 42 | "pkg": "^5.6.0", 43 | "ts-node": "^10.7.0", 44 | "typescript": "^4.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/IpManage/SpeedTest.ts: -------------------------------------------------------------------------------- 1 | import net from "net"; 2 | import unionBy from 'lodash.unionby' 3 | import {IDnsMap, DnsType} from "../dns"; 4 | import {createLogger, disableLog} from "../log"; 5 | 6 | const log = createLogger() 7 | 8 | interface IAlive { 9 | time: number; 10 | host: string; 11 | status: string; 12 | } 13 | 14 | interface IpData { 15 | host: any; 16 | port: number; 17 | } 18 | 19 | interface ISpeedTester { 20 | hostname: string; 21 | interval: number; 22 | dnsMap: IDnsMap; 23 | cb?: Function; 24 | silent?: Boolean; 25 | } 26 | 27 | class SpeedTester { 28 | public hostname: string; 29 | public alive: IAlive[]; 30 | public backupList: any[]; 31 | 32 | private readonly dnsMap: IDnsMap; 33 | private lastReadTime: number; 34 | private ready: boolean; 35 | private testCount: number; 36 | private keepCheckId: any; 37 | private readonly interval: number; 38 | private readonly silent: Boolean | undefined; 39 | 40 | public constructor({hostname, dnsMap, interval, silent}: ISpeedTester) { 41 | this.dnsMap = dnsMap; 42 | this.hostname = hostname; 43 | this.lastReadTime = Date.now(); 44 | this.ready = false; 45 | this.alive = []; 46 | this.backupList = []; 47 | this.keepCheckId = false; 48 | 49 | this.testCount = 0; 50 | this.interval = interval; 51 | this.silent = silent; 52 | 53 | if (interval) { 54 | this.touch() 55 | } 56 | 57 | if (silent) { 58 | disableLog() 59 | } 60 | } 61 | 62 | public getFastIp() { 63 | if (this.alive.length === 0) { 64 | this.test(); 65 | return null; 66 | } 67 | return this.alive[0].host; 68 | } 69 | 70 | public pickFastAliveIp() { 71 | this.touch(); 72 | 73 | return this.getFastIp() 74 | } 75 | 76 | public touch() { 77 | this.lastReadTime = Date.now(); 78 | if (!this.keepCheckId) { 79 | this.startChecker(); 80 | } 81 | } 82 | 83 | public startChecker() { 84 | if (this.keepCheckId) { 85 | clearInterval(this.keepCheckId); 86 | } 87 | this.keepCheckId = setInterval(() => { 88 | if (this.alive.length > 0) { 89 | this.testBackups(); 90 | return; 91 | } 92 | this.test(); 93 | }, this.interval); 94 | } 95 | 96 | public async getIpListFromDns(dnsMap: IDnsMap) { 97 | const ips: any = {}; 98 | const promiseList = []; 99 | // eslint-disable-next-line guard-for-in 100 | for (const key in dnsMap) { 101 | const one = this.getFromOneDns(dnsMap[key]).then(ipList => { 102 | if (ipList) { 103 | for (const ip of ipList) { 104 | ips[ip] = 1; 105 | } 106 | } 107 | }); 108 | promiseList.push(one); 109 | } 110 | await Promise.all(promiseList); 111 | const items: IpData[] = []; 112 | // eslint-disable-next-line guard-for-in 113 | for (const ip in ips) { 114 | items.push({host: ip, port: 443}); 115 | } 116 | return items; 117 | } 118 | 119 | public async getFromOneDns(dns: DnsType) { 120 | return await dns._lookup(this.hostname); 121 | } 122 | 123 | public async test(cb?: Function) { 124 | if ( 125 | this.backupList.length === 0 || 126 | this.testCount < 10 || 127 | this.testCount % 5 === 0 128 | ) { 129 | const newList = await this.getIpListFromDns(this.dnsMap); 130 | const newBackupList = [...newList, ...this.backupList]; 131 | this.backupList = unionBy(newBackupList, "host"); 132 | } 133 | this.testCount++; 134 | 135 | log.info("结果:", this.hostname, " ips:", this.backupList); 136 | await this.testBackups(); 137 | 138 | cb?.() 139 | } 140 | 141 | public async testBackups() { 142 | const testAll = []; 143 | const aliveList: IAlive[] = []; 144 | for (const item of this.backupList) { 145 | testAll.push(this.doTest(item, aliveList)); 146 | } 147 | await Promise.all(testAll); 148 | this.alive = aliveList; 149 | this.ready = true; 150 | } 151 | 152 | public async doTest(item: { host: any }, aliveList: IAlive[]) { 153 | try { 154 | const ret = await this.testOne(item); 155 | 156 | aliveList.push({...ret, ...item}); 157 | aliveList.sort((a, b) => a.time - b.time); 158 | this.backupList.sort((a, b) => a.time - b.time); 159 | } catch (e) { 160 | log.info("Speed test error", this.hostname, item.host, e.message); 161 | } 162 | } 163 | 164 | public testOne(item: any): Promise> { 165 | const timeout = 5000; 166 | const {host, port} = item; 167 | const startTime = Date.now(); 168 | let isOver = false; 169 | return new Promise((resolve, reject) => { 170 | // eslint-disable-next-line no-undef 171 | let timeoutId: NodeJS.Timeout | null = null; 172 | const client = net.createConnection({host, port}, () => { 173 | // 'connect' 监听器 174 | const connectionTime = Date.now(); 175 | isOver = true; 176 | timeoutId && clearTimeout(timeoutId); 177 | resolve({status: "success", time: connectionTime - startTime}); 178 | client.end(); 179 | }); 180 | client.on("end", () => { 181 | }); 182 | client.on("error", error => { 183 | log.error("Speed test error", this.hostname, host, error.message); 184 | isOver = true; 185 | timeoutId && clearTimeout(timeoutId); 186 | reject(error); 187 | }); 188 | 189 | timeoutId = setTimeout(() => { 190 | if (isOver) { 191 | return; 192 | } 193 | log.error("Speed test timeout", this.hostname, host); 194 | reject(new Error("timeout")); 195 | client.end(); 196 | }, timeout); 197 | }); 198 | } 199 | } 200 | 201 | export default SpeedTester; 202 | -------------------------------------------------------------------------------- /src/IpManage/index.ts: -------------------------------------------------------------------------------- 1 | import SpeedTest from "./SpeedTest"; 2 | import {IDnsMap, IDnsOption, initDNS} from "../dns"; 3 | 4 | const SpeedTestPool: { [key: string]: SpeedTest } = {}; 5 | 6 | interface IIpManageOption { 7 | hostList: any; 8 | interval: number; 9 | dnsProviders: string[]; 10 | providers: IDnsOption; 11 | callback?: Function; 12 | silent?: Boolean; 13 | } 14 | 15 | interface IConfig { 16 | hostList: any; 17 | interval: number; 18 | dnsProviders: string[]; 19 | dnsMap: IDnsMap; 20 | callback?: Function; 21 | silent?: Boolean; 22 | } 23 | 24 | export default class IpManage { 25 | private config: IConfig 26 | 27 | public constructor(option: IIpManageOption) { 28 | this.config = { 29 | ...option, 30 | dnsMap: initDNS(option.providers) 31 | } 32 | 33 | this.initSpeedTest() 34 | } 35 | 36 | public initSpeedTest() { 37 | let countArr = [] 38 | const afterCb = () => { 39 | countArr.push(1) 40 | 41 | if (countArr.length === this.config.hostList.length) { 42 | this.config.callback?.() 43 | } 44 | } 45 | 46 | this.config.hostList.forEach((hostname: string) => { 47 | SpeedTestPool[hostname] = new SpeedTest({ 48 | hostname, 49 | dnsMap: this.config.dnsMap, 50 | interval: this.config.interval, 51 | silent: this.config.silent 52 | }); 53 | 54 | SpeedTestPool[hostname].test(afterCb) 55 | }) 56 | } 57 | 58 | public getAllSpeedTester() { 59 | const allSpeed = []; 60 | 61 | for (const key in SpeedTestPool) { 62 | if (Object.prototype.hasOwnProperty.call(SpeedTestPool, key)) { 63 | allSpeed.push({ 64 | hostname: SpeedTestPool[key].hostname, 65 | alive: SpeedTestPool[key].alive, 66 | backupList: SpeedTestPool[key].backupList 67 | }); 68 | } 69 | } 70 | 71 | return allSpeed; 72 | } 73 | 74 | public getSpeedTester(hostname: string) { 75 | let instance = SpeedTestPool[hostname]; 76 | 77 | if (!instance) { 78 | instance = new SpeedTest({ 79 | hostname, 80 | dnsMap: this.config.dnsMap, 81 | interval: this.config.interval, 82 | silent: this.config.silent 83 | }); 84 | SpeedTestPool[hostname] = instance; 85 | } 86 | 87 | return instance; 88 | } 89 | 90 | public reSpeedTest() { 91 | for (const key in SpeedTestPool) { 92 | if (Object.prototype.hasOwnProperty.call(SpeedTestPool, key)) { 93 | SpeedTestPool[key].test(); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import parser from "yargs-parser"; 3 | import chalk from "chalk"; 4 | import portfinder from 'portfinder'; 5 | import IpManage from "./IpManage"; 6 | import {githubUrls, providers} from "./constants"; 7 | import {HostData} from "./hosts"; 8 | import {buildHosts} from "./utils"; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-require-imports 11 | const pkg = require('../package.json'); 12 | 13 | const argv = parser(process.argv.slice(2)); 14 | 15 | const speedConfig = { 16 | interval: 10 * 60 * 1000, 17 | hostList: githubUrls, 18 | dnsProviders: ["usa", "quad9", "iqDNS-tls", 'iqDNS'], 19 | providers, 20 | }; 21 | 22 | const defaultPort = 8080; 23 | 24 | let ipManage: IpManage; 25 | 26 | (async () => { 27 | try { 28 | const {interval, debug, port = defaultPort} = argv; 29 | 30 | await createServer({port}); 31 | 32 | ipManage = new IpManage({ 33 | ...speedConfig, 34 | silent: !debug, 35 | interval: interval >= 0 ? interval * 1000 : speedConfig.interval 36 | }); 37 | } catch (e) { 38 | console.error(chalk.red(e.message)); 39 | console.error(e.stack); 40 | process.exit(1); 41 | } 42 | })(); 43 | 44 | function getHosts() { 45 | const result = ipManage.getAllSpeedTester(); 46 | 47 | const newHosts: HostData[] = []; 48 | 49 | result.forEach(item => { 50 | newHosts.push({ 51 | name: item.hostname, 52 | ip: item.alive 53 | }) 54 | }); 55 | 56 | return buildHosts(newHosts); 57 | } 58 | 59 | async function createServer({port}: { port: number }) { 60 | const foundPort = await portfinder.getPortPromise({port}); 61 | 62 | http.createServer((request, response) => { 63 | response.writeHead(200, {'Content-Type': 'text-plain;charset=utf-8'}); 64 | 65 | response.end(getHosts()); 66 | }).listen(foundPort); 67 | 68 | const localUrl = `http://localhost:${foundPort}`; 69 | 70 | console.log(); 71 | console.log( 72 | [ 73 | ` 当前版本:${pkg.version}`, 74 | ` Github Hosts 运行在:`, 75 | ` - Local: ${chalk.cyan(localUrl)}`, 76 | ].join('\n'), 77 | ); 78 | console.log(); 79 | } 80 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const providers = { 2 | aliyun: { 3 | type: "https", 4 | server: "https://dns.alidns.com/dns-query", 5 | cacheSize: 1e3 6 | }, 7 | usa: { 8 | type: "https", 9 | server: "https://1.1.1.1/dns-query", 10 | cacheSize: 1e3 11 | }, 12 | quad9: { 13 | type: "https", 14 | server: "https://9.9.9.9/dns-query", 15 | cacheSize: 1e3 16 | }, 17 | rubyfish: { 18 | type: "https", 19 | server: "https://rubyfish.cn/dns-query", 20 | cacheSize: 1e3 21 | }, 22 | iqDNS: { 23 | type: 'https', 24 | server: 'https://i.passcloud.xyz/dns-query', 25 | cacheSize: 1e3 26 | } 27 | } 28 | 29 | export const githubUrls = [ 30 | 'mtalk.google.com', 31 | 'alt1-mtalk.google.com', 32 | 'alt2-mtalk.google.com', 33 | 'alt3-mtalk.google.com', 34 | 'alt4-mtalk.google.com', 35 | 'alt5-mtalk.google.com', 36 | 'alt6-mtalk.google.com', 37 | 'alt7-mtalk.google.com', 38 | 'alt8-mtalk.google.com', 39 | 'dl.google.com', 40 | 'dl.l.google.com', 41 | ]; 42 | -------------------------------------------------------------------------------- /src/dns/base.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import LRU from 'lru-cache'; 3 | import {DynamicChoice} from './choice'; 4 | import {createLogger} from "../log"; 5 | 6 | const log = createLogger() 7 | 8 | const cacheSize = 1024; 9 | 10 | class IpCache extends DynamicChoice { 11 | private lookupCount: number; 12 | 13 | public constructor(hostName: any) { 14 | super(hostName); 15 | this.lookupCount = 0; 16 | } 17 | 18 | /** 19 | * 获取到新的ipList 20 | * @param ipList 21 | */ 22 | public setBackupList(ipList: any[]) { 23 | super.setBackupList(ipList); 24 | this.lookupCount++; 25 | } 26 | } 27 | 28 | export default abstract class BaseDNS { 29 | public dnsServer: string; 30 | 31 | protected log = log; 32 | 33 | private cache: any; 34 | 35 | public constructor(dnsServer: string) { 36 | this.cache = new LRU({ 37 | maxSize: cacheSize 38 | }); 39 | 40 | this.dnsServer = dnsServer; 41 | } 42 | 43 | public async lookup(hostName: any) { 44 | try { 45 | let ipCache = this.cache.get(hostName); 46 | if (ipCache) { 47 | if (ipCache.value) { 48 | ipCache.doCount(ipCache.value, false); 49 | return ipCache.value; 50 | } 51 | } else { 52 | ipCache = new IpCache(hostName); 53 | this.cache.set(hostName, ipCache); 54 | } 55 | 56 | let ipList = await this._lookup(hostName); 57 | if (!ipList) { 58 | // 没有获取到ipv4地址 59 | ipList = []; 60 | } 61 | ipList.push(hostName); // 把原域名加入到统计里去 62 | 63 | ipCache.setBackupList(ipList); 64 | this.log.info( 65 | `[dns counter]:${hostName}`, 66 | ipCache.value, 67 | ipList, 68 | JSON.stringify(ipCache) 69 | ); 70 | this.log.info(`[DNS] ${hostName} -> ${ipCache.value}`); 71 | 72 | return ipCache.value; 73 | } catch (error) { 74 | this.log.error(`[DNS] cannot resolve hostName ${hostName} (${error})`, error); 75 | return hostName; 76 | } 77 | } 78 | 79 | public abstract _lookup(hostName: string): any 80 | } 81 | -------------------------------------------------------------------------------- /src/dns/choice.ts: -------------------------------------------------------------------------------- 1 | export class DynamicChoice { 2 | private key: any; 3 | private count: {}; 4 | private createTime: Date; 5 | private backup: any[] | undefined; 6 | private value: any | undefined; 7 | 8 | public constructor(key: any) { 9 | this.key = key; 10 | this.count = {}; 11 | this.createTime = new Date(); 12 | } 13 | 14 | public setBackupList(backupList: any[]) { 15 | this.backup = backupList; 16 | this.value = backupList.shift(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dns/https.ts: -------------------------------------------------------------------------------- 1 | import {promisify} from 'util'; 2 | // @ts-ignore 3 | import doh from 'dns-over-http'; 4 | import BaseDNS from './base'; 5 | 6 | const dohQueryAsync = promisify(doh.query); 7 | 8 | export default class DNSOverHTTPS extends BaseDNS { 9 | public constructor(dnsServer: string) { 10 | super(dnsServer); 11 | } 12 | 13 | public async _lookup(hostName: string) { 14 | try { 15 | const result = await dohQueryAsync({url: this.dnsServer}, [ 16 | { 17 | type: 'A', 18 | name: hostName 19 | } 20 | ]); 21 | 22 | if (result.answers.length === 0) { 23 | // 说明没有获取到ip 24 | this.log.error('该域名没有ip地址解析', hostName); 25 | return []; 26 | } 27 | const ret = result.answers 28 | .filter((item: { type: string; }) => { 29 | return item.type === 'A'; 30 | }) 31 | .map((item: { data: any; }) => { 32 | return item.data; 33 | }); 34 | if (ret.length === 0) { 35 | this.log.error('该域名没有ipv4地址解析', hostName); 36 | } else { 37 | this.log.info('获取到域名地址:', hostName, JSON.stringify(ret)); 38 | } 39 | return ret; 40 | } catch (err) { 41 | this.log.error('Https dns query error', hostName, this.dnsServer, err.message); 42 | return []; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/dns/index.ts: -------------------------------------------------------------------------------- 1 | import DNSOverHTTPS from './https'; 2 | import DNSOverIpAddress from './ipaddress'; 3 | 4 | interface IProvider { 5 | type: string; 6 | server: string; 7 | cacheSize: number; 8 | } 9 | 10 | export interface IDnsOption { 11 | [key: string]: IProvider 12 | } 13 | 14 | export type DnsType = DNSOverHTTPS | DNSOverIpAddress 15 | 16 | export interface IDnsMap { 17 | [key: string]: DnsType 18 | } 19 | 20 | export function initDNS(dnsProviders: IDnsOption) { 21 | const dnsMap: IDnsMap = {}; 22 | 23 | for (const key in dnsProviders) { 24 | if (Object.prototype.hasOwnProperty.call(dnsProviders, key)) { 25 | const conf = dnsProviders[key]; 26 | 27 | if (conf.type === 'ipaddress') { 28 | dnsMap[key] = new DNSOverIpAddress(conf.server); 29 | } else if (conf.type === 'https') { 30 | dnsMap[key] = new DNSOverHTTPS(conf.server); 31 | } 32 | } 33 | } 34 | return dnsMap; 35 | } 36 | -------------------------------------------------------------------------------- /src/dns/ipaddress.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import BaseDNS from './base'; 3 | 4 | const headers = { 5 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', 6 | }; 7 | 8 | export default class DNSOverIpAddress extends BaseDNS { 9 | public async _lookup(hostName: string) { 10 | const url = `https://${hostName}.ipaddress.com`; 11 | 12 | const res = await fetch(url, { 13 | headers: headers 14 | }); 15 | if (res.status !== 200 && res.status !== 201) { 16 | this.log.info(`[dns] get ${hostName} ipaddress: error:${res}`); 17 | return; 18 | } 19 | const ret = await res.text(); 20 | 21 | const regexp = /IP Address<\/th>