├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.json ├── .node-version ├── .npmrc ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── dev ├── dev.cmd ├── run └── run.cmd ├── bump.config.ts ├── config.d.ts ├── config.js ├── constant.d.ts ├── constant.js ├── docs ├── .vuepress │ ├── client.ts │ ├── components │ │ └── Sponsor.vue │ ├── config.ts │ ├── public │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── api-gateway-preview.png │ │ │ ├── generate-result.png │ │ │ ├── heroku_1.png │ │ │ ├── heroku_2.png │ │ │ ├── heroku_3.png │ │ │ ├── netlify-connect-to-git-provider.png │ │ │ ├── netlify-import-config.png │ │ │ ├── netlify-redis-config.png │ │ │ ├── now-error.jpeg │ │ │ ├── now-logs.png │ │ │ ├── qx-device-id.png │ │ │ ├── railway-11.png │ │ │ ├── railway-12.png │ │ │ ├── railway-13.png │ │ │ ├── railway-21.png │ │ │ ├── railway-22.png │ │ │ ├── zeabur-config.png │ │ │ ├── zeabur-create-project.png │ │ │ ├── zeabur-domain.png │ │ │ └── zeabur-redis-create.mp4 │ │ ├── join-telegram.png │ │ ├── support.jpg │ │ ├── surgio-icon.png │ │ └── surgio-square.png │ └── theme │ │ ├── index.ts │ │ └── layouts │ │ └── Layout.vue ├── README.md ├── guide.md ├── guide │ ├── advance │ │ ├── advanced-provider.md │ │ ├── api-gateway.md │ │ ├── api-gateway │ │ │ ├── netlify.md │ │ │ ├── railway.md │ │ │ ├── vercel.md │ │ │ └── zeabur.md │ │ ├── automation.md │ │ ├── custom-filter.md │ │ ├── redis-cache.md │ │ └── surge-advance.md │ ├── api.md │ ├── cli.md │ ├── client │ │ ├── clash.md │ │ ├── examples.md │ │ └── sing-box.md │ ├── custom-artifact.md │ ├── custom-config.md │ ├── custom-provider.md │ ├── custom-template.md │ ├── env.md │ ├── faq.md │ ├── getting-started.md │ ├── install-ssr-local.md │ ├── learning-resources.md │ ├── upgrade-guide-v2.md │ └── upgrade-guide-v3.md └── support.md ├── examples ├── README.md ├── clash-remote-snippet │ ├── README.md │ ├── provider │ │ └── demo.js │ ├── surgio.conf.js │ └── template │ │ └── clash.tpl ├── hooks │ ├── README.md │ ├── provider │ │ ├── demo.js │ │ └── error.js │ ├── surgio.conf.js │ └── template │ │ └── clash.tpl └── quantumultx │ ├── README.md │ ├── provider │ └── demo.js │ ├── surgio.conf.js │ └── template │ ├── quantumult_subscribe.tpl │ ├── quantumultx.tpl │ └── quantumultx_rules.tpl ├── generator.d.ts ├── generator.js ├── hygen-template ├── artifact │ └── new │ │ ├── index.js │ │ └── template.ejs.t ├── provider │ └── new │ │ ├── index.js │ │ └── template.ejs.t └── template │ └── new │ ├── index.js │ └── template.ejs.t ├── index.d.ts ├── index.js ├── internal.d.ts ├── internal.js ├── package.json ├── patches └── vuepress-plugin-umami-analytics@1.8.1.patch ├── pnpm-lock.yaml ├── provider.d.ts ├── provider.js ├── scripts └── run-example.js ├── src ├── __tests__ │ ├── __snapshots__ │ │ ├── index.test.ts.md │ │ └── index.test.ts.snap │ └── index.test.ts ├── base-command.ts ├── commands │ ├── check.ts │ ├── clean-cache.ts │ ├── doctor.ts │ ├── generate.ts │ ├── lint.ts │ ├── new.ts │ ├── subscriptions.ts │ └── upload.ts ├── config.ts ├── configurables.ts ├── constant │ ├── __tests__ │ │ └── constant.test.ts │ ├── constant.ts │ ├── env.ts │ ├── error.ts │ └── index.ts ├── filters │ ├── __tests__ │ │ └── filter.test.ts │ ├── classes.ts │ ├── filters.ts │ ├── index.ts │ └── utils.ts ├── generator │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── artifact.test.ts.md │ │ │ ├── artifact.test.ts.snap │ │ │ ├── json-template.test.ts.md │ │ │ ├── json-template.test.ts.snap │ │ │ ├── template.test.ts.md │ │ │ └── template.test.ts.snap │ │ ├── artifact.test.ts │ │ ├── json-template.test.ts │ │ ├── snippet.tpl │ │ └── template.test.ts │ ├── artifact.ts │ ├── index.ts │ ├── json-template.ts │ └── template.ts ├── hooks │ └── init.ts ├── index.ts ├── internal.ts ├── misc │ ├── deprecation.ts │ └── flag_cn.ts ├── provider │ ├── BlackSSLProvider.ts │ ├── ClashProvider.ts │ ├── CustomProvider.ts │ ├── Provider.ts │ ├── ShadowsocksJsonSubscribeProvider.ts │ ├── ShadowsocksSubscribeProvider.ts │ ├── ShadowsocksrSubscribeProvider.ts │ ├── SsdProvider.ts │ ├── TrojanProvider.ts │ ├── V2rayNSubscribeProvider.ts │ ├── __tests__ │ │ ├── ClashProvider.test.ts │ │ ├── CustomProvider.test.ts │ │ ├── ShadowsocksJsonSubscribeProvider.test.ts │ │ ├── ShadowsocksSubscribeProvider.test.ts │ │ ├── ShadowsocksrSubscribeProvider.test.ts │ │ ├── SsdProvider.test.ts │ │ ├── V2rayNSubscribeProvider.test.ts │ │ └── __snapshots__ │ │ │ ├── ClashProvider.test.ts.md │ │ │ ├── ClashProvider.test.ts.snap │ │ │ ├── SsdProvider.test.ts.md │ │ │ ├── SsdProvider.test.ts.snap │ │ │ ├── V2rayNSubscribeProvider.test.ts.md │ │ │ └── V2rayNSubscribeProvider.test.ts.snap │ ├── index.ts │ └── types.ts ├── redis.ts ├── types.ts ├── utils │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── index.test.ts.md │ │ │ ├── index.test.ts.snap │ │ │ ├── remote-snippet.test.ts.md │ │ │ ├── remote-snippet.test.ts.snap │ │ │ ├── surfboard.test.ts.md │ │ │ ├── surfboard.test.ts.snap │ │ │ ├── surge.test.ts.md │ │ │ └── surge.test.ts.snap │ │ ├── cache.test.ts │ │ ├── clash.test.ts │ │ ├── dns.test.ts │ │ ├── flag.test.ts │ │ ├── http-client.test.ts │ │ ├── index.test.ts │ │ ├── loon.test.ts │ │ ├── quantumult.test.ts │ │ ├── relayable-url.test.ts │ │ ├── remote-snippet.test.ts │ │ ├── singbox.test.ts │ │ ├── ss.test.ts │ │ ├── ssr.test.ts │ │ ├── subscription.test.ts │ │ ├── surfboard.test.ts │ │ ├── surge.test.ts │ │ ├── tmp-helper.file.test.ts │ │ ├── tmp-helper.redis.test.ts │ │ ├── trojan.test.ts │ │ └── useragent.test.ts │ ├── cache.ts │ ├── clash.ts │ ├── constant.ts │ ├── dns.ts │ ├── doctor.ts │ ├── env-flag.ts │ ├── error-helper.ts │ ├── errors.ts │ ├── flag.ts │ ├── http-client.ts │ ├── index.ts │ ├── linter.ts │ ├── loon.ts │ ├── quantumult.ts │ ├── relayable-url.ts │ ├── remote-snippet.ts │ ├── singbox.ts │ ├── ss.ts │ ├── ssr.ts │ ├── subscription.ts │ ├── surfboard.ts │ ├── surge.ts │ ├── time.ts │ ├── tmp-helper.ts │ ├── trojan.ts │ └── useragent.ts └── validators │ ├── artifact.ts │ ├── common.ts │ ├── filter.ts │ ├── hooks.ts │ ├── http.ts │ ├── hysteria2.ts │ ├── index.ts │ ├── provider.ts │ ├── shadowsocks.ts │ ├── shadowsocksr.ts │ ├── snell.ts │ ├── socks5.ts │ ├── surgio-config.ts │ ├── trojan.ts │ ├── tuic.ts │ ├── vless.ts │ ├── vmess.ts │ └── wireguard.ts ├── test ├── asset │ ├── ForeignMedia.list │ ├── clash-sample.yaml │ ├── gui-config-1.json │ ├── netflix.list │ ├── ssd-sample-2.json │ ├── ssd-sample.json │ ├── surge-script-list.txt │ ├── surgio-snippet.tpl │ ├── telegram.list │ ├── test-ruleset-1.list │ ├── test-ss-sub.txt │ ├── test-ssr-sub.txt │ ├── test-v2rayn-sub-compatible.txt │ └── test-v2rayn-sub.txt ├── benchmark │ └── .gitkeep ├── cli.cli-test.ts ├── cli.cli-test.ts.snap ├── fixture │ ├── assign-local-port │ │ ├── provider │ │ │ ├── ssr.js │ │ │ └── v2rayn.js │ │ ├── surgio.conf.js │ │ └── template │ │ │ └── test.tpl │ ├── custom-filter │ │ ├── provider │ │ │ ├── custom.js │ │ │ ├── ss.js │ │ │ └── ss2.js │ │ ├── surgio.conf.js │ │ └── template │ │ │ ├── test.tpl │ │ │ └── test2.tpl │ ├── not-specify-binPath │ │ ├── provider │ │ │ └── ssr.js │ │ ├── surgio.conf.js │ │ └── template │ │ │ └── test.tpl │ ├── plain │ │ ├── provider │ │ │ ├── clash.js │ │ │ ├── clash_mod.js │ │ │ ├── custom.js │ │ │ ├── ss.js │ │ │ ├── ss_json.js │ │ │ ├── ss_with_up.js │ │ │ ├── ssd.js │ │ │ ├── ssr.js │ │ │ ├── ssr_with_udp.js │ │ │ └── v2rayn.js │ │ ├── surgio.conf.js │ │ └── template │ │ │ ├── extend-render-context.tpl │ │ │ ├── singbox.json │ │ │ ├── snippet │ │ │ └── snippet.tpl │ │ │ ├── template-functions.tpl │ │ │ └── test.tpl │ ├── template-error │ │ ├── provider │ │ │ └── test.js │ │ ├── surgio.conf.js │ │ └── template │ │ │ └── test.tpl │ └── template-variables-functions │ │ ├── provider │ │ └── ss.js │ │ ├── surgio.conf.js │ │ └── template │ │ └── test.tpl ├── helpers │ ├── oclif.js │ └── stub-axios.js └── tsconfig.json ├── tsconfig.build.json ├── tsconfig.eslint.json ├── tsconfig.json ├── types └── .gitkeep ├── utils.d.ts ├── utils.js └── zbpack.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "build", 9 | "chore", 10 | "ci", 11 | "docs", 12 | "feat", 13 | "fix", 14 | "perf", 15 | "refactor", 16 | "revert", 17 | "style", 18 | "test", 19 | "sample" 20 | ] 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /test/asset 3 | /docs/.vuepress/dist 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | module.exports = { 4 | env: { 5 | es6: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:import/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 'esnext', 16 | project: join(__dirname, 'tsconfig.eslint.json'), 17 | sourceType: 'module', 18 | }, 19 | plugins: ['@typescript-eslint'], 20 | settings: { 21 | 'import/resolver': { 22 | typescript: true, 23 | node: true, 24 | }, 25 | }, 26 | rules: { 27 | '@typescript-eslint/ban-ts-comment': 0, 28 | '@typescript-eslint/no-var-requires': 0, 29 | '@typescript-eslint/no-explicit-any': 0, 30 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 31 | 'import/order': [ 32 | 'error', 33 | { 34 | groups: [ 35 | ['builtin', 'external', 'internal'], 36 | 'parent', 37 | 'sibling', 38 | 'index', 39 | 'object', 40 | 'type', 41 | ], 42 | 'newlines-between': 'always', 43 | }, 44 | ], 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: geekdada 2 | custom: ["https://surgio.js.org/support.html"] 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macOS-latest] 17 | node-version: [18, 20, 22] 18 | env: 19 | COREPACK_INTEGRITY_KEYS: 0 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | # This has to be done before setting up Node.js, 25 | # more info found in https://github.com/actions/setup-node/issues/531#issuecomment-1872977503 26 | - name: Enable Corepack 27 | run: corepack enable 28 | 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: 'pnpm' 34 | 35 | - name: Install dependencies 36 | run: | 37 | pnpm install 38 | 39 | - name: test, report coverage 40 | run: | 41 | pnpm build 42 | pnpm test:lint 43 | pnpm coverage 44 | 45 | - uses: codecov/codecov-action@v1 46 | if: success() && matrix.os == 'ubuntu-latest' 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .vscode 4 | .ideas 5 | .env 6 | .idea 7 | .nyc_output 8 | 9 | node_modules/ 10 | coverage/ 11 | 12 | /.zeabur/output/ 13 | /dist/ 14 | /build/ 15 | /binary-build/ 16 | /docs/.vuepress/dist 17 | /docs/.vuepress/.cache 18 | /docs/.vuepress/.temp 19 | /test/fixture/**/dist 20 | /test/benchmark/playground.js 21 | /examples/**/dist 22 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register", 4 | "./test/helpers/oclif.js", 5 | "./test/helpers/stub-axios.js" 6 | ], 7 | "watch-extensions": [ 8 | "ts" 9 | ], 10 | "spec": [ 11 | "test/**/*.cli-test.ts" 12 | ], 13 | "recursive": true, 14 | "reporter": "spec", 15 | "timeout": 60000 16 | } 17 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | auto-install-peers=false 3 | hoist-pattern[]=!@vuepress/* 4 | hoist-pattern[]=!vuepress* -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | useTabs: false, 4 | tabWidth: 2, 5 | trailingComma: 'all', 6 | singleQuote: true, 7 | bracketSpacing: true, 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | ## Development 4 | 5 | - Run `pnpm run dev` to start the development toolchain 6 | - Run `pnpm link -g` to link the package globally 7 | - Run `pnpm link -g surgio` in your local surgio config repository to use the local version of surgio 8 | - Run generate command to check the result 9 | 10 | ## Testing 11 | 12 | - Run `pnpm run test` to run the tests 13 | - Tests will be automatically checked by GitHub Actions 14 | - A green pipeline is required to merge a PR 15 | 16 | ## Versioning 17 | 18 | TODO 19 | 20 | ## Documentation 21 | 22 | - Run `pnpm run docs:dev` to start the live preview of the documentation 23 | - Add version tag in Markdown if the feature is only available in/after a specific version 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - Present Roy Li 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 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | const path = require('path'); 5 | const dotenv = require('dotenv'); 6 | const fs = require('fs-extra'); 7 | 8 | const envPath = path.resolve(process.cwd(), './.env') 9 | const project = path.join(__dirname, '..', 'tsconfig.json'); 10 | 11 | if (fs.existsSync(envPath)) { 12 | dotenv.config({ path: envPath }) 13 | } 14 | 15 | // In dev mode -> use ts-node and dev plugins 16 | process.env.NODE_ENV = 'development'; 17 | 18 | require('ts-node').register({ project }); 19 | 20 | // In dev mode, always show stack traces 21 | oclif.settings.debug = true; 22 | 23 | // Start the CLI 24 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle); 25 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | const dotenv = require('dotenv'); 5 | const { resolve } = require('path'); 6 | const fs = require('fs-extra'); 7 | 8 | const envPath = resolve(process.cwd(), './.env') 9 | 10 | if (fs.existsSync(envPath)) { 11 | dotenv.config({ path: envPath }) 12 | } 13 | 14 | oclif 15 | .run() 16 | .then(require('@oclif/core/flush')) 17 | .catch(require('@oclif/core/handle')); 18 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | export default defineConfig({ 4 | execute: 'npm run changelog', 5 | all: true, 6 | }) 7 | -------------------------------------------------------------------------------- /config.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/config' 2 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/config') 2 | -------------------------------------------------------------------------------- /constant.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/constant' 2 | -------------------------------------------------------------------------------- /constant.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/constant') 2 | -------------------------------------------------------------------------------- /docs/.vuepress/client.ts: -------------------------------------------------------------------------------- 1 | import { defineClientConfig } from '@vuepress/client' 2 | import Layout from './theme/layouts/Layout.vue' 3 | 4 | export default defineClientConfig({ 5 | layouts: { 6 | Layout, 7 | }, 8 | }) -------------------------------------------------------------------------------- /docs/.vuepress/components/Sponsor.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineUserConfig, type HeadConfig, type PluginConfig, type UserConfig } from 'vuepress' 2 | import { path } from '@vuepress/utils' 3 | import { docsearchPlugin } from '@vuepress/plugin-docsearch' 4 | import { registerComponentsPlugin } from '@vuepress/plugin-register-components' 5 | import { sitemapPlugin } from '@vuepress/plugin-sitemap' 6 | import { umamiAnalyticsPlugin } from 'vuepress-plugin-umami-analytics' 7 | import { viteBundler } from '@vuepress/bundler-vite' 8 | import { shikiPlugin } from '@vuepress/plugin-shiki' 9 | 10 | import customTheme from './theme' 11 | 12 | const meta = { 13 | title: 'Surgio', 14 | description: '一站式各类代理规则生成器', 15 | url: 'https://surgio.js.org', 16 | icon: 'https://surgio.js.org/surgio-square.png', 17 | favicon: 'https://surgio.js.org/favicon-96x96.png', 18 | } 19 | 20 | const head: HeadConfig[] = [ 21 | [ 22 | 'link', 23 | { 24 | href: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600|Roboto Mono', 25 | rel: 'stylesheet', 26 | type: 'text/css', 27 | }, 28 | ], 29 | [ 30 | 'script', 31 | { 32 | src: 'https://buttons.github.io/buttons.js', 33 | async: true, 34 | defer: true, 35 | }, 36 | ], 37 | ['link', { rel: 'icon', href: '/favicon-96x96.png' }], 38 | ['link', { rel: 'icon', href: meta.favicon, type: 'image/png' }], 39 | ['meta', { property: 'og:image', content: meta.icon }], 40 | ['meta', { property: 'twitter:image', content: meta.icon }], 41 | ['meta', { property: 'og:description', content: meta.description }], 42 | ['meta', { property: 'twitter:description', content: meta.description }], 43 | ['meta', { property: 'twitter:title', content: meta.title }], 44 | ['meta', { property: 'og:title', content: meta.title }], 45 | ['meta', { property: 'og:site_name', content: meta.title }], 46 | ['meta', { property: 'og:url', content: meta.url }], 47 | ] 48 | 49 | const plugins: PluginConfig = [ 50 | registerComponentsPlugin({ 51 | componentsDir: path.resolve(__dirname, './components'), 52 | }), 53 | shikiPlugin({ 54 | // 配置项 55 | langs: ['ts', 'json', 'bash', 'shell', 'yaml', 'js', 'json5', 'html', 'ini', 'toml', 'md'], 56 | }), 57 | ] 58 | 59 | if (process.env.NODE_ENV === 'production') { 60 | plugins.push( 61 | docsearchPlugin({ 62 | appId: 'AXEPS6U765', 63 | apiKey: 'c7282707083d364aceb47ba33e14d5ab', 64 | indexName: 'surgio', 65 | }), 66 | sitemapPlugin({ 67 | hostname: 'https://surgio.js.org', 68 | }), 69 | umamiAnalyticsPlugin({ 70 | id: '444a5a25-af75-4c30-b7a4-6aaba520daf6', 71 | src: 'https://sashimi.royli.dev/sashimi.js', 72 | domains: ['surgio.js.org'], 73 | cache: true, 74 | }), 75 | ) 76 | } 77 | 78 | export default defineUserConfig({ 79 | bundler: viteBundler(), 80 | theme: customTheme, 81 | locales: { 82 | '/': { 83 | lang: 'zh-CN', 84 | title: meta.title, 85 | description: meta.description, 86 | }, 87 | }, 88 | title: meta.title, 89 | description: meta.description, 90 | head, 91 | plugins, 92 | }) as UserConfig 93 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/favicon-96x96.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/images/api-gateway-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/api-gateway-preview.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/generate-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/generate-result.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/heroku_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/heroku_1.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/heroku_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/heroku_2.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/heroku_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/heroku_3.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/netlify-connect-to-git-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/netlify-connect-to-git-provider.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/netlify-import-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/netlify-import-config.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/netlify-redis-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/netlify-redis-config.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/now-error.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/now-error.jpeg -------------------------------------------------------------------------------- /docs/.vuepress/public/images/now-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/now-logs.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/qx-device-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/qx-device-id.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/railway-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/railway-11.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/railway-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/railway-12.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/railway-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/railway-13.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/railway-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/railway-21.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/railway-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/railway-22.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/zeabur-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/zeabur-config.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/zeabur-create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/zeabur-create-project.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/zeabur-domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/zeabur-domain.png -------------------------------------------------------------------------------- /docs/.vuepress/public/images/zeabur-redis-create.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/images/zeabur-redis-create.mp4 -------------------------------------------------------------------------------- /docs/.vuepress/public/join-telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/join-telegram.png -------------------------------------------------------------------------------- /docs/.vuepress/public/support.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/support.jpg -------------------------------------------------------------------------------- /docs/.vuepress/public/surgio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/surgio-icon.png -------------------------------------------------------------------------------- /docs/.vuepress/public/surgio-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/docs/.vuepress/public/surgio-square.png -------------------------------------------------------------------------------- /docs/.vuepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultTheme } from '@vuepress/theme-default' 2 | import type { Theme } from 'vuepress' 3 | 4 | export default { 5 | name: 'custom-theme', 6 | extends: defaultTheme({ 7 | docsRepo: 'geekdada/surgio', 8 | docsBranch: 'master', 9 | repo: 'geekdada/surgio', 10 | repoLabel: 'GitHub', 11 | editLink: true, 12 | editLinkText: '帮助我们改善此页面!', 13 | docsDir: 'docs', 14 | navbar: [ 15 | { 16 | text: 'Changelog', 17 | link: 'https://github.com/surgioproject/surgio/releases', 18 | }, 19 | ], 20 | sidebar: [ 21 | { 22 | text: '指南', 23 | children: [ 24 | '/guide', 25 | '/guide/getting-started', 26 | { 27 | text: '自定义', 28 | children: [ 29 | '/guide/custom-config', 30 | '/guide/custom-provider', 31 | '/guide/custom-template', 32 | '/guide/custom-artifact', 33 | ], 34 | }, 35 | { 36 | text: '客户端规则维护指南', 37 | children: ['/guide/client/sing-box', '/guide/client/clash', '/guide/client/examples'], 38 | }, 39 | '/guide/api', 40 | '/guide/cli', 41 | '/guide/faq', 42 | '/guide/upgrade-guide-v2', 43 | '/guide/upgrade-guide-v3', 44 | '/guide/learning-resources', 45 | ], 46 | }, 47 | { 48 | text: '进阶', 49 | children: [ 50 | '/guide/advance/surge-advance', 51 | '/guide/advance/custom-filter', 52 | '/guide/advance/advanced-provider', 53 | '/guide/advance/automation', 54 | { 55 | text: '快速搭建托管 API', 56 | children: [ 57 | '/guide/advance/api-gateway', 58 | '/guide/advance/api-gateway/zeabur', 59 | '/guide/advance/api-gateway/netlify', 60 | '/guide/advance/api-gateway/railway', 61 | '/guide/advance/api-gateway/vercel', 62 | ], 63 | }, 64 | '/guide/advance/redis-cache', 65 | { 66 | link: 'https://royli.dev/blog/2019/better-proxy-rules-for-apple-services', 67 | text: '苹果服务的连接策略推荐' 68 | } 69 | ], 70 | }, 71 | ], 72 | }), 73 | } as Theme; 74 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: '一站式各类代理规则生成器' 4 | actions: 5 | - text: 快速上手 → 6 | link: /guide 7 | type: primary 8 | footer: MIT Licensed | Copyright © 2019-present 9 | --- 10 |

11 | Star 12 |

13 | 14 | ### 马上开始 15 | 16 | ```bash 17 | # 安装 18 | npm init surgio-store my-rule-store 19 | 20 | # 或 使用国内镜像安装 21 | npm init surgio-store my-rule-store --use-cnpm 22 | 23 | # 生成一批规则 24 | cd my-rule-store 25 | npx surgio generate 26 | ``` 27 | 28 | :::warning 注意 29 | 目前 Surgio 仅支持 Node.js 版本 >= 18.0.0。 30 | ::: 31 | 32 | ### 交流 33 | 34 | [](https://t.me/surgiotg) 35 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍 3 | --- 4 | 5 | # 介绍 6 | 7 | ## 背景 8 | 9 | 基本上,市面上所有的「机场」都会针对各个客户端提供一套标准的配置文件。这些配置文件能够让绝大部分人免去自己去学习写规则的痛苦。有一部分人因为不满足于通用的规则自己去编写规则或者从网络上东拼西凑别人的规则,然后使用 lhie1 和 Fndroid 编写的工具合成,最终加入到不同的客户端中。这个过程略微繁琐并且很难做到完全自动化更新。 10 | 11 | 目前常见的代理软件除了 Surge 做到了无缝地跨平台(almost),其他代理软件大多都有自己的配置文件,然后通过兼容的方式去读取另一个软件的配置。Clash 虽然跨平台,但是目前来看安卓和 iOS 上还没有非常好的 Clash 客户端。也就是说,大部分人其实在桌面端和移动端使用着不一样的客户端,并且很有可能使用着多份配置文件。 12 | 13 | 「机场」遍地开花让很多人手上持着多个机场订阅。有的「机场」技术完备为不同人群提供了多样化的配置,有的却只能提供一个订阅链接,给使用者造成了不小的麻烦。 14 | 15 | ## 它是如何工作的? 16 | 17 | Surgio 由两部分组成:一部分解析不同机场提供的订阅地址或者自己维护的节点列表,另一部分根据模板定义生成指定的规则。 18 | 19 | Surgio 还包括了一个实用工具 —— 上传到阿里云 OSS,能够快速实现订阅功能。 20 | 21 | ## Surgio 适合谁? 22 | 23 | 如果你符合以下几点,那 Surgio 就是适合你的。 24 | 25 | - 购买了两个以上的 「机场」 26 | - 使用了两个以上的代理软件 27 | - 对机场提供的规则不满意 28 | - 会写点 JavaScript 29 | 30 | ## 特性 31 | 32 | - 维护自己的私人小机场(支持 Shadowsocks, Vmess, Shadowsocksr 等类型的节点) 33 | - 读取机场的订阅地址 34 | - 同时生成针对不同客户端的配置 35 | - 同样的规则可写成「可复用片段」,减少重复劳作 36 | - 读取符合 Surge Ruleset 规范的远程片段 37 | - 让 Surge 能够订阅 SSR 38 | 39 | ## 劝退 40 | 41 | 目前很多核心功能对动手能力不强的小白和不了解 JavaScript 的朋友来说还有一些门槛。如果你在使用过程中感到吃力,推荐使用以下工具: 42 | 43 | - [Hackl0us/SS-Rule-Snippet](https://github.com/Hackl0us/SS-Rule-Snippet) 44 | 45 | ## Code of Conduct 46 | 47 | Surgio 现在不会将来也不会记录或上报你使用的节点或订阅信息。 48 | 49 | ## 支持开发 50 | 51 | 感谢 Surgio 的支持者们! 52 | 53 | 54 | 55 |

56 | 57 | 58 | 59 |

60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/guide/advance/api-gateway.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 相信很多人都用过网络上处理规则的 API 接口,也有人在使用过 Surgio 后觉得更新规则不太灵活。虽然我们已经能够通过自动化的方法每隔一段时间更新一次规则,但还是无法做到实时更新。这篇文章就是想教大家,利用现成的 SAAS(Software as a Service) 服务,来实现一个 Surgio 规则仓库的 API。 4 | 5 | 目前 Surgio 多个部署平台,推荐 Railway 和 Netlify。你也可以部署在自己的主机上,不过没有技术支持。 6 | 7 | 需要保证 Surgio 版本号大于 `v1.20.0`。 8 | -------------------------------------------------------------------------------- /docs/guide/advance/api-gateway/netlify.md: -------------------------------------------------------------------------------- 1 | # 部署 - Netlify Functions 2 | 3 | [[toc]] 4 | 5 | :::tip 提示 6 | 1. 该方法不要求代码托管平台,可为私有仓库(文章以 GitHub 为例) 7 | 2. 已经部署其它平台的仓库可以修改之后增加部署到 Netlify Functions,互不影响 8 | 3. 我们有一个运行的示例供你参考:[netlify-demo](https://github.com/surgioproject/netlify-demo) 9 | ::: 10 | 11 | ## 准备 12 | 13 | 确保 `surgio` 升级至 `v2.17.0` 或以上; `@surgio/gateway` 升级至 `v1.5.0` 或以上。 14 | 15 | ### 开启接口鉴权 16 | 17 | :::warning 注意 18 | 不建议关闭鉴权! 19 | ::: 20 | 21 | 请阅读 [这里](/guide/api.md#打开鉴权)。 22 | 23 | ### 增加平台配置 24 | 25 | 在代码库根目录新建文件 `netlify.toml`,内容如下: 26 | 27 | ```toml 28 | [build] 29 | command = "exit 0" 30 | functions = "netlify/functions" 31 | publish = "." 32 | 33 | [functions] 34 | included_files = [ 35 | "node_modules/surgio/**", 36 | "node_modules/@surgio/**", 37 | "provider/**", 38 | "template/**", 39 | "*.js", 40 | "*.json" 41 | ] 42 | 43 | [[redirects]] 44 | from = "/*" 45 | to = "/.netlify/functions/index" 46 | status = 200 47 | force = true 48 | ``` 49 | 50 | 在代码库根目录新建目录 `netlify/functions` 并新建文件 `netlify/functions/index.js`,内容如下: 51 | 52 | ```js 53 | 'use strict'; 54 | 55 | const gateway = require('@surgio/gateway'); 56 | 57 | module.exports.handler = gateway.createLambdaHandler(); 58 | ``` 59 | 60 | 将修改 push 到代码库。 61 | 62 | ## 部署 63 | 64 | 在 Netlify 中选择新建项目,并选择代码库平台。 65 | 66 | ![](/images/netlify-connect-to-git-provider.png) 67 | 68 | 授权成功之后即可选择代码库,然后会看到如下的页面: 69 | 70 | ![](/images/netlify-import-config.png) 71 | 72 | 点击 __Deploy site__ 按钮,即可部署。 73 | 74 | ## 配置 Redis 缓存 75 | 76 | :::tip 此步骤可选,推荐配置 77 | 78 | [教程](/guide/advance/redis-cache.md) 79 | ::: 80 | 81 | ## 查看用量 82 | 83 | 你可以在账户的 Billing 页面查询当月的用量。 84 | 85 | ## 使用 86 | 87 | 你可能还需要更新 _surgio.conf.js_ 内 `urlBase` 的值,它应该类似: 88 | 89 | ``` 90 | https://surgio-demo.netlify.app/get-artifact/ 91 | ``` 92 | 93 | :::tip 移步至 94 | [托管 API 的功能介绍](/guide/api.md) 95 | ::: 96 | -------------------------------------------------------------------------------- /docs/guide/advance/api-gateway/railway.md: -------------------------------------------------------------------------------- 1 | # 部署 - Railway 2 | 3 | [[toc]] 4 | 5 | :::tip 提示 6 | 1. 该方法要求代码仓库由 GitHub 托管,可为私有仓库 7 | 2. 已经部署 Vercel 的项目可以经过简单修改部署至 Railway 8 | 3. 已经部署 Heroku 的项目可以直接部署至 Railway 9 | 4. 我们有一个运行的示例供你参考:[railway-demo](https://github.com/surgioproject/railway-demo) 10 | ::: 11 | 12 | ## 准备 13 | 14 | 确保 `surgio` 升级至 `v2.17.0` 或以上; `@surgio/gateway` 升级至 `v1.5.0` 或以上。 15 | 16 | ### 开启接口鉴权 17 | 18 | :::warning 注意 19 | 不建议关闭鉴权! 20 | ::: 21 | 22 | 请阅读 [这里](/guide/api.md#打开鉴权)。 23 | 24 | ### 增加平台配置 25 | 26 | 在代码库的根目录新建文件 `Procfile`,内容如下: 27 | 28 | ``` 29 | web: npm start 30 | ``` 31 | 32 | 继续新建文件 `gateway.js`,内容如下: 33 | 34 | ```js 35 | const gateway = require('@surgio/gateway') 36 | const PORT = process.env.PORT || 3000 37 | 38 | ;(async () => { 39 | const app = await gateway.bootstrapServer() 40 | await app.listen(PORT, '0.0.0.0') 41 | console.log('> Your app is ready at http://0.0.0.0:' + PORT) 42 | })() 43 | ``` 44 | 45 | 参照 [这里](https://github.com/surgioproject/railway-demo/blob/master/package.json) 在 `scripts` 下补充 `start` 脚本。 46 | 47 | ```json 48 | { 49 | "start": "node gateway.js" 50 | } 51 | ``` 52 | 53 | 前往 [Railway.app](https://railway.app?referralCode=tN8cxr) 注册账号,可以不绑定信用卡。 54 | 55 | ## 新建项目 56 | 57 | 打开 [Railway.app](https://railway.app?referralCode=tN8cxr),在 Dashboard 中选择新建项目。 58 | 59 | ![](/images/railway-11.png) 60 | 61 | 选择从代码仓库部署。 62 | 63 | ![](/images/railway-12.png) 64 | 65 | 随后在项目列表中找到代码库,选择用于部署的分支,点击部署。部署成功后即可使用默认分配的域名访问 Surgio 面板。 66 | 67 | 今后代码库的分支有更新 Railway 会自动拉取并部署。和 Vercel 不同的是,Railway 属于容器化方案,因此打包编译的时间会比 Vercel 久很多。 68 | 69 | ![](/images/railway-13.png) 70 | 71 | :::tip 不要忘记! 72 | 请不要忘记将 `surgio.conf.js` 中 `urlBase` 改为 Railway 的域名路径。 73 | ::: 74 | 75 | ## 配置项目 76 | 77 | 下面的内容属于自定义范畴,可跳过。如果你没有一定基础建议跳过。 78 | 79 | ### 自定义域名 80 | 81 | ![](/images/railway-21.png) 82 | 83 | ### 修改环境变量 84 | 85 | 需要注意的是,每次增删环境变量都会触发打包编译,如果一次性要添加很多环境变量建议使用 **Bulk Import**。 86 | 87 | ![](/images/railway-22.png) 88 | 89 | ## 配置 Redis 缓存 90 | 91 | :::tip 此步骤可选,推荐配置 92 | [教程](/guide/advance/redis-cache.md) 93 | ::: 94 | 95 | ## 查看用量 96 | 97 | Railway 每月有 5 刀的免费用量,足够单个 Surgio 项目使用。你可以在 [这里](https://railway.app/account/billing) 查看本月的用量。 98 | 99 | ## 使用 100 | 101 | 你可能还需要更新 _surgio.conf.js_ 内 `urlBase` 的值,它应该类似: 102 | 103 | ``` 104 | https://surgio-demo.railway.app/get-artifact/ 105 | ``` 106 | 107 | :::tip 移步至 108 | [托管 API 的功能介绍](/guide/api.md) 109 | ::: -------------------------------------------------------------------------------- /docs/guide/advance/api-gateway/zeabur.md: -------------------------------------------------------------------------------- 1 | # 部署 - Zeabur 2 | 3 | [[toc]] 4 | 5 | ## 准备 6 | 7 | 确保 `surgio` 升级至 `v2.17.0` 或以上; `@surgio/gateway` 升级至 `v1.5.0` 或以上。 8 | 9 | ### 开启接口鉴权 10 | 11 | :::warning 注意 12 | 不建议关闭鉴权! 13 | ::: 14 | 15 | 请阅读 [这里](/guide/api.md#打开鉴权)。 16 | 17 | ### 增加平台配置 18 | 19 | 在代码库的根目录新建文件 `gateway.js`,内容如下: 20 | 21 | ```js 22 | const gateway = require('@surgio/gateway'); 23 | const os = require('os'); 24 | 25 | const PORT = process.env.PORT || 3000; 26 | 27 | (async () => { 28 | const app = await gateway.bootstrapServer(); 29 | 30 | await app.listen(PORT, '0.0.0.0'); 31 | console.log(`> Your app is ready at http://0.0.0.0:${PORT}`); 32 | })(); 33 | ``` 34 | 35 | 参照 [这里](https://github.com/surgioproject/railway-demo/blob/master/package.json) 在 `scripts` 下补充 `start` 脚本。 36 | 37 | ```json 38 | { 39 | "start": "node gateway.js" 40 | } 41 | ``` 42 | 43 | ### 注册 Zeabur 账号 44 | 45 | 前往 [Zeabur.com](https://zeabur.com?referralCode=geekdada&utm_source=geekdada&utm_campaign=oss) 注册账号。 46 | 47 | ## 新建项目 48 | 49 | ![](/images/zeabur-create-project.png) 50 | 51 | 选择 __GitHub__ 并且选择你的 Surgio 仓库。这一步可能会需要授权,请根据提示操作即可。 52 | 53 | 正常情况下 Zeabur 能够自动识别项目的类型和所需的配置,请检查是否如下所示: 54 | 55 | ![](/images/zeabur-config.png) 56 | 57 | :::tip 提示 58 | - `Build Command` 可不存在 59 | - `Node Version` 大于等于 18 即可 60 | - `Start Command` 必须为 `npm start` 或 `yarn start` 61 | ::: 62 | 63 | ## 部署 64 | 65 | 点击 __部署__ 按钮,即可部署。 66 | 67 | ## 配置域名 68 | 69 | ![](/images/zeabur-domain.png) 70 | 71 | 你可以在这里生成一个 Zeabur 的域名,也可以使用自己的域名。 72 | 73 | ## 配置 Redis 缓存 74 | 75 | :::tip 76 | 此步骤可选,推荐配置 77 | ::: 78 | 79 | 推荐直接使用 Zeabur 提供的 Redis 服务。 80 | 81 | ### 新建 Redis 实例 82 | 83 | 84 | 85 | 实例创建成功后,Zeabur 会自动将 Redis 的连接信息添加到项目的环境变量中。后面请参考 [Redis 缓存](/guide/advance/redis-cache.md) 进行配置。需要注意的是,Zeabur 注入的环境变量是 `REDIS_URI`。 86 | 87 | ## 使用 88 | 89 | 你可能还需要更新 _surgio.conf.js_ 内 `urlBase` 的值,它应该类似: 90 | 91 | ``` 92 | https://surgio-demo.zeabur.app/get-artifact/ 93 | ``` 94 | 95 | :::tip 移步至 96 | [托管 API 的功能介绍](/guide/api.md) 97 | ::: 98 | -------------------------------------------------------------------------------- /docs/guide/advance/redis-cache.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 1 3 | --- 4 | 5 | # 开启 Redis 缓存 6 | 7 | 如果你正在使用 API 网关,并且加入了多个订阅和远程片段,那么开启 Redis 缓存可以有效降低冷启动的时间。 8 | 9 | 原本的本地缓存实现方式是使用文件和内存缓存,但是这种方式存在一个问题,就是每次重新部署 Serverless 平台时,都会清空缓存。内存缓存甚至会在进程挂起时被清空。这就导致了冷启动时间变长。Redis 缓存因为运行在独立的进程中,所以不会受到这些影响。 10 | 11 | ## 新建一个免费的 Redis 实例 12 | 13 | - Railway 和 Zeabur 都提供 Redis 服务 14 | - [Redis Cloud](https://redis.com/try-free/) 15 | - [Upstash](https://upstash.com/redis/) 16 | 17 | 上面这两个地方服务提供了额免费的 Redis 实例,性能已经完全满足 Surgio 的需求。你可以使用你自己的 Redis 数据库,只要能够从外网访问即可。 18 | 19 | 需要注意,假如你的 Surgio 服务部署在美西,那 Redis 也最好在美西。Railway 默认的部署区域是 `us-west-1`,Vercel 的默认部署区域是 `us-east-1`,Netlify 的默认部署区域是 `us-east-2`。 20 | 21 | 新建成功之后,上面的平台应该会提供一个连接地址,格式类似: 22 | 23 | ``` 24 | redis://:xxx...@some-thing-like-35533.upstash.io:35533 25 | ``` 26 | 27 | 如果你开启了 TLS,则连接地址应该是: 28 | 29 | ``` 30 | rediss://:xxx...@some-thing-like-35533.upstash.io:35533 31 | ``` 32 | 33 | ## 配置 Redis 34 | 35 | ### 在所有环境下开启 36 | 37 | ```js 38 | // surgio.conf.js 39 | 40 | module.exports = { 41 | cache: { 42 | type: 'redis', 43 | redisUrl: 'redis://:xxx...@some-thing-like-35533.upstash.io:35533', 44 | }, 45 | } 46 | ``` 47 | 48 | ### 仅在部分环境下开启 49 | 50 | 请在需要开启 Redis 的环境下配置环境变量 `REDIS_URL`。不建议在本地生成配置时也连接 Redis,这样反而会变慢。 51 | 52 | :::tip 提示 53 | Zeabur 的 Redis 环境变量是 `REDIS_URI`。 54 | ::: 55 | 56 | ```js 57 | // surgio.conf.js 58 | 59 | module.exports = { 60 | cache: process.env.REDIS_URL || process.env.REDIS_URI 61 | ? { 62 | type: 'redis', 63 | redisUrl: process.env.REDIS_URL || process.env.REDIS_URI, 64 | } 65 | : undefined, 66 | } 67 | ``` 68 | 69 | 以 Netlify 为例,你可以在后台下图位置增加环境变量。 70 | 71 | ![](/images/netlify-redis-config.png) 72 | -------------------------------------------------------------------------------- /docs/guide/advance/surge-advance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Surge 进阶 - 生成 SSR 订阅 3 | sidebarDepth: 2 4 | --- 5 | 6 | # Surge 进阶 - 生成 SSR 订阅 7 | 8 | :::warning 注意 9 | - 本文仅针对 Surge for Mac 10 | - 如果你已经订阅了 Surge 4,推荐使用 [原生](/guide/custom-config.md#surgeconfig-v2ray) 的 Vmess 支持 11 | ::: 12 | 13 | Surge 没有原生提供对 SSR 的支持 ~~(将来也不太可能)~~ ,但是提供了一个叫做 [External Proxy Provider](https://medium.com/@Blankwonder/surge-mac-new-features-external-proxy-provider-375e0e9ea660) 的功能,能够满足我们连接 SSR 服务器。 14 | 15 | ## 开始之前 16 | 17 | 在一切开始之前,你需要确保本地已经安装了 V2Ray 和 SSR 的可执行文件。 18 | 19 | - [安装 SSR](/guide/install-ssr-local.md) 20 | 21 | ## 修改 Surgio 配置 22 | 23 | 找到 `surgio.conf.js`,补充如下字段: 24 | 25 | ```js {3-6} 26 | module.exports = { 27 | // ... 28 | binPath: { 29 | shadowsocksr: '/usr/local/bin/ssr-local', 30 | }, 31 | resolveHostname: true, 32 | } 33 | ``` 34 | 35 | :::tip 提示 36 | 关于 `resolveHostname` 的解释请看 [这里](/guide/custom-config.md#surgeconfig-resolvehostname)。 37 | ::: 38 | 39 | ## 生成 40 | 41 | 1. 确保模板中调用 `getSurgeNodes` 方法。 42 | 2. Provider 中包含 SSR 的订阅。 43 | 44 | ## 注意事项 45 | 46 | 1. 同样的一份 Surge 托管配置,其中的 SSR 节点能够在其它有二进制文件的电脑中启动。 47 | 2. 如果你能让 `~/.config/surgio` 同步起来,可以把二进制文件也放里面,那订阅对于这两种节点都是有意义的。注意 Surge 不能识别 `~/` 但是能识别 `$HOME/`。恕不提供更多支持。 48 | -------------------------------------------------------------------------------- /docs/guide/cli.md: -------------------------------------------------------------------------------- 1 | # 命令行功能 2 | 3 | [[toc]] 4 | 5 | ## `generate` 6 | 7 | > 生成所有 Artifact 8 | 9 | ```bash 10 | $ npx surgio generate 11 | ``` 12 | 13 | 从 1.23.0 版本开始,Surgio 会在生成过程前进行前置的代码检查和修复。这个过程会方便不了解 JS 的用户进行问题修正,熟悉 ESLint 的用户也也可以自己配置 `.eslintrc` 来覆盖 Surgio 内置的规则。Surgio 默认开启了 ESLint 的 Fix 功能且无法关闭。 14 | 15 | ### 可选参数 16 | 17 | #### `--cache-snippet` 18 | 19 | > 20 | 21 | 开启远程片段缓存。 22 | 23 | :::tip 提示 24 | 默认的缓存时间为 12 小时,你可以通过设置 [环境变量](/guide/env.md#surgio-remote-snippet-cache-maxage) 来修改缓存时间 25 | ::: 26 | 27 | #### `--skip-fail` 28 | 29 | 略过 Artifact 生成过程中的错误。 30 | 31 | :::tip 提示 32 | 推荐不要在 CI 环境使用这个参数 33 | ::: 34 | 35 | #### `--skip-lint` 36 | 37 | 略过前置代码检查(不建议)。 38 | 39 | ## `upload` 40 | 41 | > 上传 Artifact 42 | 43 | ```bash 44 | $ npx surgio upload 45 | ``` 46 | 47 | ## `subscriptions` 48 | 49 | > 查询 Provider 订阅流量 50 | 51 | ```bash 52 | $ npx surgio subscriptions 53 | ``` 54 | 55 | :::tip 提示 56 | 1. 目前支持查询从 Header 中返回的流量信息和返回流量信息节点的订阅; 57 | 2. 不论 Provider 有没有被使用都会查询; 58 | ::: 59 | 60 | ## `new` 61 | 62 | > 新建助手 63 | 64 | ```bash 65 | $ npx surgio new provider|artifact|template 66 | ``` 67 | 68 | :::tip 提示 69 | 1. 目前新建 Artifact 助手无法识别 `artifacts: require(...)` 这样的配置文件,可能会破坏文件结构; 70 | ::: 71 | 72 | ## `doctor` 73 | 74 | > 检查运行环境 75 | 76 | ```bash 77 | $ npx surgio doctor 78 | # @surgio/gateway: 0.12.2 79 | # surgio: 1.20.2 80 | # node: 12.16.2 (/usr/local/Cellar/node@12/12.16.2_1/bin/node) 81 | # npx: 6.14.4 82 | # yarn: 1.22.4 83 | # npm: 6.14.4 84 | ``` 85 | 86 | ## `lint` 87 | 88 | > 检查代码格式 89 | 90 | 假设代码格式检查不通过,则 JS 极有可能无法正常运行,请耐心检查,也可以使用下面的命令自动修复一部分错误。 91 | 92 | ```bash 93 | $ npx surgio lint --fix 94 | ``` 95 | 96 | ### 可选参数 97 | 98 | #### `--fix` 99 | 100 | 自动修复部分格式错误。 101 | 102 | ## `clean-cache` 103 | 104 | > 清除缓存 105 | 106 | ## 全局参数 107 | 108 | ### `-V --verbose` 调试模式 109 | 110 | > 开启调试日志 111 | -------------------------------------------------------------------------------- /docs/guide/client/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 代码示例 3 | sidebarDepth: 2 4 | --- 5 | 6 | # 代码示例 7 | 8 | 这些案例引用了我维护的代理规则,你也可以用相似的方法引用其它的远程片段。需要注意的是,我们只能引用 Surge 的远程片段。 9 | 10 | 对于 `Apple` 和 `Apple CDN` 这两个策略组,我强烈建议你使用内置的 `apple_rules.tpl`,它可以很好的解决苹果服务接口的访问和苹果全球 CDN 的访问分流。想了解更多细节可以阅读 [这篇文章](https://royli.dev/blog/2019/better-proxy-rules-for-apple-services)。 11 | 12 | ## Quantumult X + 远程规则 13 | 14 | 对于 QuantumultX 来说,根据国别来生成托管文件是一个比较好的管理方式。 15 | 16 | 在使用本仓库之前,你需要配置好阿里云 OSS 或者 Surgio 面板,这样才能让 QuantumultX 下载到托管文件。 17 | 18 | [仓库](https://github.com/surgioproject/surgio/tree/master/examples/quantumultx) 19 | 20 | ## Clash + 远程规则 21 | 22 | [仓库](https://github.com/surgioproject/surgio/tree/master/examples/clash-remote-snippet) 23 | 24 | ## 钩子函数 25 | 26 | [仓库](https://github.com/surgioproject/surgio/tree/master/examples/hooks) 27 | -------------------------------------------------------------------------------- /docs/guide/client/sing-box.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 1 3 | --- 4 | 5 | # sing-box 6 | 7 | > 8 | 9 | 因为 sing-box 的配置文件为 JSON 格式,所以我们引入了一种新的方式来维护 sing-box 规则,或是其它 JSON 格式的规则。 10 | 11 | ## 准备 12 | 13 | 首先我们找到一份基础的规则文件,它可能是这样的: 14 | 15 | ```json 16 | { 17 | "inbounds": [], 18 | "outbounds": [ 19 | { 20 | "type": "block", 21 | "tag": "block" 22 | }, 23 | { 24 | "type": "dns", 25 | "tag": "dns" 26 | } 27 | ], 28 | "route": {}, 29 | "experimental": { 30 | "cache_file": { 31 | "enabled": true 32 | }, 33 | "clash_api": { 34 | "external_controller": "127.0.0.1:9090" 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | 我们看到此时 `outbounds` 已经包含了一些内容,我们要做的就是把节点信息填充到 `outbounds` 中。 41 | 42 | 把这个文件保存在 `tempalte` 目录下,命名为 `singbox.json`。 43 | 44 | ## 编写 Artifact 45 | 46 | ```js {9-26} 47 | const { extendOutbounds } = require('surgio'); 48 | 49 | module.exports = { 50 | artifacts: [ 51 | { 52 | name: 'singbox.json', 53 | template: 'singbox', 54 | templateType: 'json', 55 | extendTemplate: extendOutbounds( 56 | ({ getSingboxNodes, getSingboxNodeNames, nodeList }) => [ 57 | { 58 | type: 'direct', 59 | tag: 'direct', 60 | tcp_fast_open: false, 61 | tcp_multi_path: true, 62 | }, 63 | { 64 | type: 'selector', 65 | tag: 'proxy', 66 | outbounds: ['auto', ...getSingboxNodeNames(nodeList)], 67 | // outbounds: getSingboxNodeNames(nodeList), // 如果你不需要 auto 节点 68 | interrupt_exist_connections: false, 69 | }, 70 | ...getSingboxNodes(nodeList), 71 | ], 72 | ), 73 | provider: 'ss', 74 | }, 75 | ] 76 | } 77 | ``` 78 | 79 | 这个配置的含义是: 80 | 81 | - `template` 为 `singbox`,即我们刚刚创建的模板文件 82 | - `extendTemplate` 为 `extendOutbounds`,这个函数会把节点信息填充到 `outbounds` 中 83 | 84 | 第 10 行的 `getSingboxNodes`, `getSingboxNodeNames` 属于「模板方法」,具体有哪些可用的模板方法可以看 [这里](/guide/custom-template.md#模板方法)。 85 | 86 | ## `extendOutbounds` 函数 87 | 88 | `extendOutbounds` 支持两种写法,一种是直接输入一个不可变的变量,另一种是输入一个函数。变量即确定的不会变化的内容,函数则是相对动态的内容。上面的例子中我们使用了函数的写法。 89 | 90 | ### 直接输入变量 91 | 92 | ```js 93 | const { extendOutbounds } = require('surgio'); 94 | 95 | module.exports = { 96 | artifacts: [ 97 | { 98 | name: 'singbox.json', 99 | template: 'singbox', 100 | templateType: 'json', 101 | extendTemplate: extendOutbounds([ 102 | { 103 | type: 'direct', 104 | tag: 'direct', 105 | tcp_fast_open: false, 106 | tcp_multi_path: true, 107 | }, 108 | ]), 109 | provider: 'ss', 110 | }, 111 | ] 112 | } 113 | ``` 114 | 115 | 你可以在 [这里](/guide/custom-template.md#模板方法) 查看这篇文章中提到的所有模板方法的文档。 116 | -------------------------------------------------------------------------------- /docs/guide/custom-artifact.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Artifact 产品 3 | sidebarDepth: 2 4 | --- 5 | 6 | # Artifact 产品 7 | 8 | Surgio 会根据 Artifact 的值来生成配置文件。你可以一次性配置多个 Artifact,一次性生成所有需要的配置文件。 9 | 10 | ```json5 11 | { 12 | name: 'SurgeV3.conf', 13 | template: 'surge_v3', 14 | provider: 'demo', 15 | } 16 | ``` 17 | 18 | ## 属性 19 | 20 | ### name 21 | 22 | - 类型: `string` 23 | - 默认值: `undefined` 24 | - 25 | 26 | 配置文件名 27 | 28 | ### template 29 | 30 | - 类型: `string` 31 | - 默认值: `undefined` 32 | - 33 | 34 | 模板名。会在 `./template` 目录内寻找同名文件(`.tpl` 后缀可省略)。 35 | 36 | ### templateType 37 | 38 | - 类型: `string` 39 | - 默认值: `default` 40 | - 有效值: `default`, `json` 41 | - 42 | 43 | 模板类型。默认为 `default`,即以传统方式解析模板文件。 44 | 45 | ### extendTemplate 46 | 47 | - 类型: `function` 48 | - 默认值: `undefined` 49 | - 50 | 51 | 拓展 JSON 类型的模板,在编写 sing-box 规则时会用到。 52 | 53 | ### provider 54 | 55 | - 类型: `string` 56 | - 默认值: `undefined` 57 | - 58 | 59 | 模板名。会在 `./provider` 目录内寻找同名文件(`.js` 后缀可省略)。 60 | 61 | ### combineProviders 62 | 63 | - 类型: `string[]` 64 | - 默认值: `undefined` 65 | 66 | 合并其它 Provider。 67 | 68 | :::warning 注意 69 | 由于我们可以在 Provider 中定义属于自己的 `customFilters` 和 `nodeFilter`,它们在合并时需要你注意以下几点: 70 | - 不论是主 Provider(即 `provider` 定义的 Provider),还是合并进来的 Provider,它们的 `nodeFilter` 只对自身的节点有效; 71 | - 对于 `customFilters` 来说,只有主 Provider 中定义的才会生效; 72 | ::: 73 | 74 | 例如: 75 | 76 | 最终生成的节点配置会包含 `my-provider`, `rixcloud`, `dlercloud` 三个 Provider 的节点。如果 `my-provider` 中有自定义过滤器 `customFilters`,那这些过滤器对 `rixcloud` 和 `dlercloud` 节点同样有效。 77 | 78 | ```js 79 | { 80 | provider: 'my-provider', 81 | combineProviders: ['rixcloud', 'dlercloud'], 82 | } 83 | ``` 84 | 85 | ### subscriptionUserInfoProvider 86 | 87 | - 类型: `string` 88 | - 默认值: `undefined` 89 | 90 | 默认情况下,仅当 `combineProviders` 为空(Provider只有一个)时才会生成 `subscriptionUserInfo`,即客户端能够识别的流量信息。定义此参数可以指定由哪个 Provider 提供 `subscriptionUserInfo`。 91 | 92 | ### customParams 93 | 94 | - 类型: `object` 95 | - 默认值: `{}` 96 | 97 | 自定义的模板变量。可以在模板中获取,方便定制化模板。 98 | 99 | 例如: 100 | 101 | ```json5 102 | { 103 | customParams: { 104 | beta: true, 105 | foo: 'bar', 106 | }, 107 | } 108 | ``` 109 | 110 | 此后即可在模板中使用 111 | 112 | ```md 113 | `{{ customParams.foo }}` 114 | ``` 115 | 116 | 来输出 `foo` 的内容。 117 | 118 | 你也可以定义布尔值以实现模板中的逻辑判断,比如: 119 | 120 | ```html 121 | 122 | {% if customParams.beta %} 123 | 124 | {% endif %} 125 | ``` 126 | 127 | :::tip 提示 128 | 1. 逻辑语句能够让你仅通过一个模板就能实现多种不同的配置。Nunjucks 的条件语法请参考其文档; 129 | 2. 你可以[定义全局的自定义模板变量了](/guide/custom-config.md#customparams); 130 | ::: 131 | 132 | ### destDir 133 | 134 | - 类型: `string` 135 | - 默认值: `undefined` 136 | 137 | 该 Artifact 的生成目录。对于本地管理规则仓库的朋友可能会非常有用,你不再需要人肉复制粘贴了。 138 | -------------------------------------------------------------------------------- /docs/guide/env.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 环境变量 3 | sidebarDepth: 2 4 | --- 5 | 6 | # 环境变量 7 | 8 | :::warning 注意 9 | 以下环境变量仅供调试使用 10 | ::: 11 | 12 | ### `SURGIO_NETWORK_TIMEOUT` 13 | 14 | - 默认值: `5000` 15 | - 单位: 秒 16 | 17 | ### `SURGIO_NETWORK_RETRY` 18 | 19 | - 默认值: `0` 20 | 21 | 举例,当最大重试次数为 2 时,加上原始的请求最多会请求 3 次。 22 | 23 | ### `SURGIO_NETWORK_CONCURRENCY` 24 | 25 | - 默认值: `5` 26 | 27 | ### `SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE` 28 | 29 | - 默认值: `43200000`(12 小时) 30 | 31 | ### `SURGIO_PROVIDER_CACHE_MAXAGE` 32 | 33 | - 默认值: `600000`(10 分钟) 34 | -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 0 3 | --- 4 | 5 | # 常见问题 6 | 7 | ## 本地生成规则时报错 8 | 9 | - `RequestError: connect ECONNREFUSED` 10 | - `RequestError: connect ECONNRESET` 11 | 12 | 命令行默认不会通过代理请求,请在命令行内运行如下代码重试。端口号请根据自己的情况进行更改。 13 | 14 | ```bash 15 | export https_proxy=http://127.0.0.1:6152;export http_proxy=http://127.0.0.1:6152;export all_proxy=socks5://127.0.0.1:6153 16 | ``` 17 | 18 | ## 安装依赖时出现 `[warn]` 日志 19 | 20 | 可以忽略。 21 | 22 | ## 访问 now.sh 报错 23 | 24 | ![](/images/now-error.jpeg) 25 | 26 | 点击 _check the logs_,可以看到错误日志,截图后反馈至交流群。 27 | 28 | Build logs 通常不会出错,如果没有看到错误请在 Functions 页面查看运行期错误。 29 | 30 | ![](/images/now-logs.png) 31 | 32 | ## 使用 `combineProviders` 后过滤器不生效 33 | 34 | 在使用合并 Provider 功能时,`combineProviders` 里 Provider 的过滤器 `customFilters`,`netflixFilter`,`youtubePremiumFilter` 不会生效,请在主 Provider 中定义它们。 35 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速上手 3 | --- 4 | 5 | # 快速上手 6 | 7 | :::warning 注意 8 | - 目前 Surgio 仅支持 Node.js 版本 >= 18.0.0 9 | - 文档中出现的命令如无特殊说明都只能运行在 macOS, Linux 或者 WSL 上 10 | ::: 11 | 12 | ## 安装 Node.js 13 | 14 | :::tip 提示 15 | 如果已安装可跳过 16 | ::: 17 | 18 | [前往下载 Node.js](https://nodejs.org/zh-cn/download/) 19 | 20 | ## 新建一个配置仓库 21 | 22 | ```bash 23 | # 安装 24 | npm init surgio-store my-rule-store 25 | 26 | # 或 使用国内镜像安装 27 | npm init surgio-store my-rule-store --use-cnpm 28 | 29 | # 来到仓库目录 30 | cd my-rule-store 31 | ``` 32 | 33 | ## 术语解释 34 | 35 | 在进入正题之前,我们先解释一下核心的几个术语: 36 | 37 | ### Provider -- 提供方 38 | 39 | 节点的提供方,可以是一个订阅地址也可以是一组节点的配置。 40 | 41 | ### Template -- 模板 42 | 43 | Surgio 根据模板来渲染指定的文件。 44 | 45 | ### Artifact -- 产品 46 | 47 | Surgio 生成出的规则就是「产品」。 48 | 49 | :::tip 提示 50 | 以上三者的关系简单来说就是:Surgio 根据 Artifact 的定义将 Provider 的节点用 Template 生成出来可用的配置。 51 | ::: 52 | 53 | ## 目录结构 54 | 55 | ```txt {5,6,7} 56 | ./my-rule-store 57 | ├── node_modules 58 | ├── package-lock.json 59 | ├── package.json 60 | ├── provider 61 | ├── surgio.conf.js 62 | └── template 63 | ``` 64 | 65 | 你只需要关心高亮的 *surgio.conf.js*, *provider* 和 *template* 三个东西。 66 | 67 | 仓库中已经包含了一些用于演示的代码。我们会在后面一节说明如何自定义它们。 68 | 69 | ## 生成规则 70 | 71 | ```bash 72 | npx surgio generate 73 | ``` 74 | 75 | 规则已经生成到 `dist` 目录了。 76 | 77 | 78 | 79 | ## 上传规则 80 | 81 | ```bash 82 | npx surgio upload 83 | ``` 84 | 85 | 你也可以使用预设好的组合命令,生成后上传规则: 86 | 87 | ```bash 88 | npm run update 89 | ``` 90 | 91 | :::warning 注意 92 | 请确保已配置阿里云 OSS。 93 | ::: 94 | 95 | ## 如何自定义 96 | 97 | 推荐想使用 Surgio 的朋友先熟悉一下三大件 Artifact, Provider 和 Template 分别包含什么功能再尝试自定义。 98 | 99 | Surgio 提供了一个新建组件的助手命令,你可以通过它来初始化想要的组件。 100 | 101 | ```bash 102 | npx surgio new [artifact|provider|template] 103 | ``` 104 | 105 | ## 样例 106 | 107 | 除了你使用 init 命令生成的初始仓库之外,你还可以在 [这里](https://github.com/geekdada/surgio/tree/master/examples) 找到其它使用样例。 108 | 109 | ## 升级 Surgio 110 | 111 | 确保你当前的版本和新版没有兼容性问题后,运行下面命令即可。 112 | 113 | ```bash 114 | $ npm install surgio@latest 115 | ``` 116 | 117 | 升级 API 网关 118 | 119 | ```bash 120 | $ npm install @surgio/gateway@latest 121 | ``` 122 | 123 | ## 配置文件 124 | 125 | Surgio 的配置文件位于目录内的 `surgio.conf.js`。 126 | -------------------------------------------------------------------------------- /docs/guide/install-ssr-local.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安装 SSR 3 | sidebarDepth: 2 4 | --- 5 | 6 | # 安装 SSR 7 | 8 | 由于 Python 版本的 SSR 的命令行工具不支持某些参数,所以 Surgio 仅提供 libev 版本的支持。 9 | 10 | ## 下载 11 | 12 | [点我下载](https://github.com/tindy2013/shadowsocks-static-binaries/raw/master/shadowsocksr-libev/macos/ssr-local) 13 | 14 | > MD5: `24184a0a50ce679ff8a6024ce72b2c16` 15 | 16 | :::tip 提示 17 | 建议放置在 `/usr/local/bin/ssr-local` 18 | ::: 19 | 20 | **你也可以直接运行命令** 21 | 22 | ```bash 23 | curl -L https://github.com/tindy2013/shadowsocks-static-binaries/raw/master/shadowsocksr-libev/macos/ssr-local -o /usr/local/bin/ssr-local && chmod +x /usr/local/bin/ssr-local 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/guide/learning-resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 0 3 | --- 4 | 5 | # 社区学习资源 6 | 7 | > 非常感谢社区中的朋友们为 Surgio 贡献学习资源! 8 | 9 | - [Surgio 简易教程](https://www.notion.so/Surgio-373256ca4ac547479a09cc561be576fd) 10 | - [🌊 Surgio Tutorial: Make Surge easier](https://wh0.is/p/surgio-tutorial-make-surge-easier) 11 | -------------------------------------------------------------------------------- /docs/guide/upgrade-guide-v2.md: -------------------------------------------------------------------------------- 1 | # v2 升级指南 2 | 3 | 在 v2.0.0 中对原有的一些接口和行为进行了修改,你可能要花一些时间来解决这些问题。相信我,会很快。 4 | 5 | **目录** 6 | 7 | [[toc]] 8 | 9 | ## Node 版本升级 10 | 11 | 新版 Surgio 不再支持 Node v10,推荐使用 v12。 12 | 13 | ## Gateway 面板 14 | 15 | 旧版的面板已经不再提供,请按照 [文档](/guide/advance/api-gateway.md) 部署新版面板。 16 | 17 | ## Surgio 配置修改 18 | 19 | #### surgeConfig.vmess 默认改为 `native` 20 | 21 | 由于 Surge 新版已经发布了一段时间,故不再默认使用 External Provider 的方式输出 Vmess 节点。 22 | 23 | #### surgeConfig.shadowsocksFormat 默认改为 `ss` 24 | 25 | 由于 Surge 新版已经发布了一段时间,故不再默认使用 `custom` 的方式输出 Shadowsocks 节点。 26 | 27 | ## 内置过滤器 28 | 29 | #### 协议过滤器名称修改 30 | 31 | 由于疏忽,有一些 [协议过滤器](/guide/custom-template.md#协议过滤器) 命名未符合规范,已修改。 32 | 33 | ## 自定义过滤器 34 | 35 | #### useProviders, discardProviders 默认开启严格模式 36 | 37 | 这个改变对于绝大部分用户没有影响,不过如果你原先使用这个过滤器来过滤一类包含了相同字段的 Provider,则需要手动关闭严格模式。 38 | 39 | ## Provider 修改 40 | 41 | #### `udp-relay` 全部为布尔类型 42 | 43 | 历史上 `udp-relay` 允许如 `"true"`, `"false"` 这样的字符串,新版中将严格验证这个值的类型。你可以全局搜索替换解决。 44 | 45 | #### 自定义 Vmess 节点新增 `udp-relay` 取代 `udp` 46 | 47 | Custom 类型的 Provider 允许自定义 Vmess 节点。原来定义开启 UDP 转发的键名为 `udp`,新版改为 `udp-relay`。你可以全局搜索替换解决。 48 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 |

支持开发

2 | 3 | 感谢 Surgio 的支持者们! 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | 这里的样例的目的是为了展示某个独立的功能点如何使用,而非呈现完整的规则仓库。如果你想得到一个完整的、开箱即用的规则仓库,请阅读 [这里](https://surgio.js.org/guide/getting-started.html#%E5%AE%89%E8%A3%85-node-js)。 4 | -------------------------------------------------------------------------------- /examples/clash-remote-snippet/README.md: -------------------------------------------------------------------------------- 1 | # Example - Clash 2 | -------------------------------------------------------------------------------- /examples/clash-remote-snippet/provider/demo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | type: 'custom', 5 | nodeList: [ 6 | { 7 | nodeName: '🇺🇸US', 8 | type: 'shadowsocks', 9 | hostname: 'us.example.com', 10 | port: '443', 11 | method: 'chacha20-ietf-poly1305', 12 | password: 'surgio', 13 | obfs: 'tls', 14 | obfsHost: 'world.taobao.com', 15 | udpRelay: true, 16 | }, 17 | { 18 | nodeName: '🇭🇰HK Netflix', 19 | type: 'shadowsocks', 20 | hostname: 'hk.example.com', 21 | port: '443', 22 | method: 'chacha20-ietf-poly1305', 23 | password: 'surgio', 24 | obfs: 'tls', 25 | obfsHost: 'world.taobao.com', 26 | udpRelay: true, 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /examples/clash-remote-snippet/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | remoteSnippets: [ 5 | { 6 | name: 'youtube', 7 | url: 'https://raw.githubusercontent.com/geekdada/surge-list/master/youtube.list', 8 | }, 9 | { 10 | name: 'global', 11 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Proxy.list', 12 | }, 13 | { 14 | name: 'netflix', 15 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Media/Netflix.list', 16 | }, 17 | ], 18 | artifacts: [ 19 | { 20 | name: 'Clash.yaml', 21 | template: 'clash', 22 | provider: 'demo', 23 | }, 24 | { 25 | name: 'Clash_enhanced_mode.yaml', 26 | template: 'clash', 27 | provider: 'demo', 28 | customParams: { 29 | enhancedMode: true, 30 | }, 31 | }, 32 | ], 33 | urlBase: 'https://config.example.com/', 34 | // https://surgio.js.org/guide/custom-config.html#upload 35 | // upload: {}, 36 | } 37 | -------------------------------------------------------------------------------- /examples/clash-remote-snippet/template/clash.tpl: -------------------------------------------------------------------------------- 1 | allow-lan: true 2 | mode: Rule 3 | external-controller: 127.0.0.1:7892 4 | port: 7890 5 | socks-port: 7891 6 | {% if customParams.enhancedMode %} 7 | dns: 8 | enable: true 9 | ipv6: false 10 | listen: 0.0.0.0:53 11 | enhanced-mode: fake-ip 12 | nameserver: 13 | - 119.29.29.29 14 | - 223.5.5.5 15 | {% endif %} 16 | 17 | Proxy: {{ getClashNodes(nodeList) | json }} 18 | 19 | Proxy Group: 20 | - type: select 21 | name: 🚀 Proxy 22 | proxies: {{ getClashNodeNames(nodeList) | json }} 23 | - type: select 24 | name: 🎬 Netflix 25 | proxies: {{ getClashNodeNames(nodeList, netflixFilter) | json }} 26 | - type: select 27 | name: 📺 Youtube 28 | proxies: {{ getClashNodeNames(nodeList) | json }} 29 | - type: url-test 30 | name: US 31 | proxies: {{ getClashNodeNames(nodeList, usFilter) | json }} 32 | url: {{ proxyTestUrl }} 33 | interval: 1200 34 | - type: url-test 35 | name: HK 36 | proxies: {{ getClashNodeNames(nodeList, hkFilter) | json }} 37 | url: {{ proxyTestUrl }} 38 | interval: 1200 39 | - type: select 40 | name: 🍎 Apple 41 | proxies: ['DIRECT', '🚀 Proxy', 'US', 'HK'] 42 | - type: select 43 | name: 🍎 Apple CDN 44 | proxies: ['DIRECT', '🍎 Apple'] 45 | 46 | Rule: 47 | {% filter clash %} 48 | {{ remoteSnippets.netflix.main('🎬 Netflix') }} 49 | {{ remoteSnippets.youtube.main('📺 Youtube') }} 50 | {{ remoteSnippets.global.main('🚀 Proxy') }} 51 | {% endfilter %} 52 | 53 | # LAN 54 | - DOMAIN-SUFFIX,local,DIRECT 55 | - IP-CIDR,127.0.0.0/8,DIRECT 56 | - IP-CIDR,172.16.0.0/12,DIRECT 57 | - IP-CIDR,192.168.0.0/16,DIRECT 58 | - IP-CIDR,10.0.0.0/8,DIRECT 59 | - IP-CIDR,17.0.0.0/8,DIRECT 60 | - IP-CIDR,100.64.0.0/10,DIRECT 61 | 62 | # Final 63 | - GEOIP,CN,DIRECT 64 | - MATCH,🚀 Proxy 65 | -------------------------------------------------------------------------------- /examples/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Example - Clash 2 | -------------------------------------------------------------------------------- /examples/hooks/provider/demo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { defineClashProvider } = require('surgio') 4 | 5 | /** 6 | * 这是一个能够成功的实例,它会从远程获取 Clash 配置 7 | */ 8 | module.exports = defineClashProvider({ 9 | url: 'https://raw.githubusercontent.com/surgioproject/surgio/master/test/asset/clash-sample.yaml', 10 | type: 'clash', 11 | udpRelay: true, 12 | addFlag: true, 13 | hooks: { 14 | afterNodeListResponse: async (nodeList, customParams) => { 15 | if (customParams.requestUserAgent?.toLowerCase().includes('surge')) { 16 | // 假如是 Surge 请求则在末尾插入一个我自己维护的节点 17 | nodeList.push({ 18 | type: 'shadowsocks', 19 | nodeName: 'US 自定义节点', 20 | hostname: 'example.com', 21 | port: 8388, 22 | method: 'chacha20-ietf-poly1305', 23 | password: 'password', 24 | }) 25 | } 26 | 27 | return nodeList 28 | }, 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /examples/hooks/provider/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { defineClashProvider } = require('surgio') 4 | 5 | /** 6 | * 这是一个一定会失败的示例 7 | */ 8 | module.exports = defineClashProvider({ 9 | url: 'https://raw.githubusercontent.com/surgioproject/surgio/master/test/asset/not-exist.yaml', 10 | type: 'clash', 11 | udpRelay: true, 12 | addFlag: true, 13 | hooks: { 14 | onError: async () => { 15 | return [ 16 | { 17 | nodeName: 'Fallback', 18 | type: 'shadowsocks', 19 | hostname: 'fallback.example.com', 20 | port: 443, 21 | method: 'chacha20-ietf-poly1305', 22 | password: 'password', 23 | }, 24 | ] 25 | }, 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /examples/hooks/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { defineSurgioConfig } = require('surgio') 4 | 5 | module.exports = defineSurgioConfig({ 6 | remoteSnippets: [ 7 | { 8 | name: 'youtube', 9 | url: 'https://raw.githubusercontent.com/geekdada/surge-list/master/youtube.list', 10 | }, 11 | { 12 | name: 'global', 13 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Proxy.list', 14 | }, 15 | { 16 | name: 'netflix', 17 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Media/Netflix.list', 18 | }, 19 | ], 20 | artifacts: [ 21 | { 22 | name: 'Clash.yaml', 23 | template: 'clash', 24 | provider: 'demo', 25 | combineProviders: ['error'], 26 | }, 27 | ], 28 | urlBase: 'https://config.example.com/', 29 | // https://surgio.js.org/guide/custom-config.html#upload 30 | // upload: {}, 31 | }) 32 | -------------------------------------------------------------------------------- /examples/hooks/template/clash.tpl: -------------------------------------------------------------------------------- 1 | allow-lan: true 2 | mode: Rule 3 | external-controller: 127.0.0.1:7892 4 | port: 7890 5 | socks-port: 7891 6 | {% if customParams.enhancedMode %} 7 | dns: 8 | enable: true 9 | ipv6: false 10 | listen: 0.0.0.0:53 11 | enhanced-mode: fake-ip 12 | nameserver: 13 | - 119.29.29.29 14 | - 223.5.5.5 15 | {% endif %} 16 | 17 | Proxy: {{ getClashNodes(nodeList) | json }} 18 | 19 | Proxy Group: 20 | - type: select 21 | name: 🚀 Proxy 22 | proxies: {{ getClashNodeNames(nodeList) | json }} 23 | - type: select 24 | name: 🎬 Netflix 25 | proxies: {{ getClashNodeNames(nodeList, netflixFilter) | json }} 26 | - type: select 27 | name: 📺 Youtube 28 | proxies: {{ getClashNodeNames(nodeList) | json }} 29 | - type: url-test 30 | name: US 31 | proxies: {{ getClashNodeNames(nodeList, usFilter) | json }} 32 | url: {{ proxyTestUrl }} 33 | interval: 1200 34 | - type: url-test 35 | name: HK 36 | proxies: {{ getClashNodeNames(nodeList, hkFilter) | json }} 37 | url: {{ proxyTestUrl }} 38 | interval: 1200 39 | - type: select 40 | name: 🍎 Apple 41 | proxies: ['DIRECT', '🚀 Proxy', 'US', 'HK'] 42 | - type: select 43 | name: 🍎 Apple CDN 44 | proxies: ['DIRECT', '🍎 Apple'] 45 | 46 | Rule: 47 | {% filter clash %} 48 | {{ remoteSnippets.netflix.main('🎬 Netflix') }} 49 | {{ remoteSnippets.youtube.main('📺 Youtube') }} 50 | {{ remoteSnippets.global.main('🚀 Proxy') }} 51 | {% endfilter %} 52 | 53 | # LAN 54 | - DOMAIN-SUFFIX,local,DIRECT 55 | - IP-CIDR,127.0.0.0/8,DIRECT 56 | - IP-CIDR,172.16.0.0/12,DIRECT 57 | - IP-CIDR,192.168.0.0/16,DIRECT 58 | - IP-CIDR,10.0.0.0/8,DIRECT 59 | - IP-CIDR,17.0.0.0/8,DIRECT 60 | - IP-CIDR,100.64.0.0/10,DIRECT 61 | 62 | # Final 63 | - GEOIP,CN,DIRECT 64 | - MATCH,🚀 Proxy 65 | -------------------------------------------------------------------------------- /examples/quantumultx/README.md: -------------------------------------------------------------------------------- 1 | # Example - QuantumultX 2 | -------------------------------------------------------------------------------- /examples/quantumultx/provider/demo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | type: 'custom', 5 | nodeList: [ 6 | { 7 | nodeName: '🇺🇸US', 8 | type: 'shadowsocks', 9 | hostname: 'us.example.com', 10 | port: '443', 11 | method: 'chacha20-ietf-poly1305', 12 | password: 'surgio', 13 | obfs: 'tls', 14 | obfsHost: 'world.taobao.com', 15 | udpRelay: true, 16 | }, 17 | { 18 | nodeName: '🇭🇰HK Netflix', 19 | type: 'shadowsocks', 20 | hostname: 'hk.example.com', 21 | port: '443', 22 | method: 'chacha20-ietf-poly1305', 23 | password: 'surgio', 24 | obfs: 'tls', 25 | obfsHost: 'world.taobao.com', 26 | udpRelay: true, 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /examples/quantumultx/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { utils } = require('surgio') 4 | 5 | module.exports = { 6 | remoteSnippets: [ 7 | { 8 | name: 'youtube', 9 | url: 'https://raw.githubusercontent.com/geekdada/surge-list/master/youtube.list', 10 | }, 11 | { 12 | name: 'global', 13 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Proxy.list', 14 | }, 15 | { 16 | name: 'netflix', 17 | url: 'https://git.royli.dev/me/lhie1_Rules/raw/branch/master/Surge/Surge%203/Provider/Media/Netflix.list', 18 | }, 19 | ], 20 | artifacts: [ 21 | { 22 | name: 'QuantumultX_rules.conf', 23 | template: 'quantumultx_rules', 24 | provider: 'demo', 25 | }, 26 | { 27 | name: 'QuantumultX.conf', 28 | template: 'quantumultx', 29 | provider: 'demo', 30 | }, 31 | { 32 | name: 'Quantumult_subscribe_us.conf', 33 | template: 'quantumult_subscribe', 34 | provider: 'demo', 35 | customParams: { 36 | magicVariable: utils.usFilter, 37 | }, 38 | }, 39 | { 40 | name: 'Quantumult_subscribe_hk.conf', 41 | template: 'quantumult_subscribe', 42 | provider: 'demo', 43 | customParams: { 44 | magicVariable: utils.hkFilter, 45 | }, 46 | }, 47 | ], 48 | urlBase: 'https://config.example.com/', 49 | // https://surgio.js.org/guide/custom-config.html#upload 50 | // upload: {}, 51 | } 52 | -------------------------------------------------------------------------------- /examples/quantumultx/template/quantumult_subscribe.tpl: -------------------------------------------------------------------------------- 1 | {{ getQuantumultXNodes(nodeList, customParams.magicVariable) }} 2 | -------------------------------------------------------------------------------- /examples/quantumultx/template/quantumultx.tpl: -------------------------------------------------------------------------------- 1 | # https://github.com/crossutility/Quantumult-X/blob/master/sample.conf 2 | 3 | [general] 4 | server_check_url=http://cp.cloudflare.com/generate_204 5 | 6 | [dns] 7 | server=223.5.5.5 8 | server=114.114.114.114 9 | server=119.29.29.29 10 | 11 | [server_remote] 12 | {{ getDownloadUrl('Quantumult_subscribe_us.conf') }}, tag=🇺🇸 US 13 | {{ getDownloadUrl('Quantumult_subscribe_hk.conf') }}, tag=🇭🇰 HK 14 | 15 | [policy] 16 | available=🇺🇸 Auto US, {{ getNodeNames(nodeList, usFilter) }} 17 | available=🇭🇰 Auto HK, {{ getNodeNames(nodeList, hkFilter) }} 18 | static=Netflix, PROXY, {{ getNodeNames(nodeList, netflixFilter) }}, img-url=https://raw.githubusercontent.com/zealson/Zure/master/IconSet/Netflix.png 19 | static=YouTube, PROXY, {{ getNodeNames(nodeList, youtubePremiumFilter) }}, img-url=https://raw.githubusercontent.com/zealson/Zure/master/IconSet/YouTube.png 20 | static=Apple, DIRECT, 🇺🇸 Auto US, 🇭🇰 Auto HK, img-url=https://raw.githubusercontent.com/zealson/Zure/master/IconSet/Apple.png 21 | static=Apple CDN, DIRECT, Apple, img-url=https://raw.githubusercontent.com/zealson/Zure/master/IconSet/Apple.png 22 | static=Paypal, DIRECT, 🇺🇸 Auto US, 🇭🇰 Auto HK, img-url=https://raw.githubusercontent.com/zealson/Zure/master/IconSet/PayPal.png 23 | 24 | [filter_remote] 25 | {{ getDownloadUrl('QuantumultX_rules.conf') }}, tag=分流规则 26 | 27 | [filter_local] 28 | ip-cidr, 10.0.0.0/8, direct 29 | ip-cidr, 127.0.0.0/8, direct 30 | ip-cidr, 172.16.0.0/12, direct 31 | ip-cidr, 192.168.0.0/16, direct 32 | ip-cidr, 224.0.0.0/24, direct 33 | geoip, cn, direct 34 | final, proxy 35 | -------------------------------------------------------------------------------- /examples/quantumultx/template/quantumultx_rules.tpl: -------------------------------------------------------------------------------- 1 | {{ remoteSnippets.youtube.main('YouTube') | quantumultx }} 2 | {{ remoteSnippets.netflix.main('Netflix') | quantumultx }} 3 | {{ remoteSnippets.global.main('PROXY') | quantumultx }} 4 | 5 | # LAN, debugging rules should place above this line 6 | DOMAIN-SUFFIX,local,DIRECT 7 | IP-CIDR,10.0.0.0/8,DIRECT 8 | IP-CIDR,100.64.0.0/10,DIRECT 9 | IP-CIDR,127.0.0.0/8,DIRECT 10 | IP-CIDR,172.16.0.0/12,DIRECT 11 | IP-CIDR,192.168.0.0/16,DIRECT 12 | 13 | GEOIP,CN,DIRECT 14 | FINAL,PROXY 15 | -------------------------------------------------------------------------------- /generator.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/generator' 2 | -------------------------------------------------------------------------------- /generator.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/generator') 2 | -------------------------------------------------------------------------------- /hygen-template/artifact/new/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { basename } = require('path') 4 | const { promises: fsp } = require('fs') 5 | const _ = require('lodash') 6 | 7 | const { loadConfig } = require('../../../build/config') 8 | 9 | module.exports = { 10 | prompt: ({ prompter: inquirer }) => { 11 | const config = loadConfig(process.cwd()) 12 | 13 | return inquirer.prompt([ 14 | { 15 | type: 'input', 16 | name: 'name', 17 | message: 'Artifact 名称(建议文件名包含后缀,如 .conf)', 18 | validate: (result) => !!result, 19 | }, 20 | { 21 | type: 'list', 22 | name: 'provider', 23 | message: 'Provider 名称', 24 | choices: async () => { 25 | const files = await listFolder(config.providerDir) 26 | 27 | return _.chain(files) 28 | .filter((item) => item.endsWith('js')) 29 | .map((item) => basename(item, '.js')) 30 | .value() 31 | }, 32 | }, 33 | { 34 | type: 'list', 35 | name: 'template', 36 | message: 'Template 名称', 37 | choices: async () => { 38 | const files = await listFolder(config.templateDir) 39 | 40 | return _.chain(files) 41 | .filter((item) => item.endsWith('tpl')) 42 | .map((item) => basename(item, '.tpl')) 43 | .value() 44 | }, 45 | }, 46 | { 47 | type: 'checkbox', 48 | name: 'combineProviders', 49 | message: '是否合并其它 Provider(不合并直接回车跳过)', 50 | choices: async (results) => { 51 | const files = await listFolder(config.providerDir) 52 | 53 | return _.chain(files) 54 | .filter((item) => item.endsWith('js')) 55 | .map((item) => basename(item, '.js')) 56 | .filter((item) => item !== results.provider) 57 | .value() 58 | }, 59 | }, 60 | ]) 61 | }, 62 | } 63 | 64 | function listFolder(f) { 65 | return fsp.readdir(f, { 66 | encoding: 'utf8', 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /hygen-template/artifact/new/template.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: surgio.conf.js 3 | inject: true 4 | skip_if: "name: '<%= name %>'" 5 | after: "artifacts\\:\\ \\[" 6 | --- 7 | { 8 | name: '<%= name %>', 9 | template: '<%= template %>', 10 | provider: '<%= provider %>', 11 | <% if (Array.isArray(combineProviders)) { -%> 12 | combineProviders: [ 13 | <% combineProviders.forEach(item => { -%> 14 | '<%= item %>', 15 | <% }) -%> 16 | ], 17 | <% } -%> 18 | }, 19 | -------------------------------------------------------------------------------- /hygen-template/provider/new/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { SupportProviderEnum } = require('../../../build/internal') 4 | 5 | module.exports = { 6 | prompt: ({ prompter: inquirer }) => { 7 | return inquirer.prompt([ 8 | { 9 | type: 'list', 10 | name: 'type', 11 | message: 'Provider 类型', 12 | choices: Object.keys(SupportProviderEnum).map( 13 | (key) => SupportProviderEnum[key], 14 | ), 15 | }, 16 | { 17 | type: 'input', 18 | name: 'name', 19 | message: 'Provider 名称', 20 | }, 21 | { 22 | type: 'input', 23 | name: 'url', 24 | message: '订阅 URL', 25 | when: (results) => { 26 | return [ 27 | SupportProviderEnum.Clash, 28 | SupportProviderEnum.V2rayNSubscribe, 29 | SupportProviderEnum.ShadowsocksJsonSubscribe, 30 | SupportProviderEnum.ShadowsocksrSubscribe, 31 | SupportProviderEnum.ShadowsocksSubscribe, 32 | SupportProviderEnum.Trojan, 33 | ].includes(results.type) 34 | }, 35 | validate: (str) => /^https?:\/{2}/.test(str), 36 | }, 37 | { 38 | type: 'confirm', 39 | name: 'addFlag', 40 | message: '是否自动为节点名添加国旗 Emoji(默认开启)', 41 | }, 42 | { 43 | type: 'confirm', 44 | name: 'udpRelay', 45 | message: '是否强制开启订阅 UDP 转发(默认关闭)', 46 | default: false, 47 | when: (results) => { 48 | return [ 49 | SupportProviderEnum.Clash, 50 | SupportProviderEnum.ShadowsocksJsonSubscribe, 51 | SupportProviderEnum.ShadowsocksrSubscribe, 52 | SupportProviderEnum.ShadowsocksSubscribe, 53 | SupportProviderEnum.Trojan, 54 | ].includes(results.type) 55 | }, 56 | }, 57 | { 58 | type: 'confirm', 59 | name: 'isRelayUrlEnabled', 60 | message: '是否开启订阅转发(默认关闭)', 61 | default: false, 62 | when: (results) => { 63 | return [ 64 | SupportProviderEnum.Clash, 65 | SupportProviderEnum.ShadowsocksJsonSubscribe, 66 | SupportProviderEnum.ShadowsocksrSubscribe, 67 | SupportProviderEnum.ShadowsocksSubscribe, 68 | SupportProviderEnum.Trojan, 69 | ].includes(results.type) 70 | }, 71 | }, 72 | { 73 | type: 'input', 74 | name: 'relayUrl', 75 | message: '输入订阅转发地址', 76 | when: (results) => results.isRelayUrlEnabled, 77 | validate: (str) => /^https?:\/{2}/.test(str), 78 | }, 79 | ]) 80 | }, 81 | } 82 | -------------------------------------------------------------------------------- /hygen-template/provider/new/template.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: provider/<%= name %>.js 3 | 4 | --- 5 | const { utils } = require('surgio'); 6 | 7 | module.exports = { 8 | type: '<%= type %>', 9 | <% if (typeof url !== 'undefined') { -%> 10 | url: '<%= url %>', 11 | <% } -%> 12 | <% if (type === 'custom') { -%> 13 | nodeList: [], 14 | <% } -%> 15 | <% if (addFlag === true) { -%> 16 | addFlag: true, 17 | <% } -%> 18 | <% if (typeof udpRelay !== 'undefined' && udpRelay === true) { -%> 19 | udpRelay: true, 20 | <% } -%> 21 | <% if (typeof relayUrl === 'string') { -%> 22 | relayUrl: '<%= relayUrl %>', 23 | <% } -%> 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /hygen-template/template/new/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | prompt: ({ prompter: inquirer }) => { 5 | return inquirer.prompt([ 6 | { 7 | type: 'input', 8 | name: 'name', 9 | message: 'Template 名称', 10 | }, 11 | ]) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /hygen-template/template/new/template.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: template/<%= name %>.tpl 3 | 4 | --- 5 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/index' 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/index.js') 2 | -------------------------------------------------------------------------------- /internal.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/internal' 2 | -------------------------------------------------------------------------------- /internal.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/internal.js') 2 | -------------------------------------------------------------------------------- /patches/vuepress-plugin-umami-analytics@1.8.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/server/index.js b/dist/server/index.js 2 | index d61f6716a7b49354722aa01f45465c1f2e912006..8568d24a144e09b1f23ebbd1adb652e7f97c5c2f 100644 3 | --- a/dist/server/index.js 4 | +++ b/dist/server/index.js 5 | @@ -1,22 +1,20 @@ 6 | import { getDirname, path } from '@vuepress/utils'; 7 | let __dirname = getDirname(import.meta.url); 8 | -export let umamiAnalyticsPlugin = ({ doNotTrack, autoTrack, hostUrl, domains, cache, src, id, }) => app => { 9 | - let plugin = { 10 | - name: 'vuepress-plugin-umami-analytics', 11 | - }; 12 | - if (app.env.isDev) { 13 | - return plugin; 14 | - } 15 | +export let umamiAnalyticsPlugin = ({ doNotTrack, autoTrack, hostUrl, domains, cache, src, id, }) => { 16 | return { 17 | - ...plugin, 18 | - define: { 19 | - __UMAMI_ANALYTICS_DO_NOT_TRACK__: doNotTrack, 20 | - __UMAMI_ANALYTICS_AUTO_TRACK__: autoTrack, 21 | - __UMAMI_ANALYTICS_HOST_URL__: hostUrl, 22 | - __UMAMI_ANALYTICS_DOMAINS__: domains, 23 | - __UMAMI_ANALYTICS_CACHE__: cache, 24 | - __UMAMI_ANALYTICS_SRC__: src, 25 | - __UMAMI_ANALYTICS_ID__: id, 26 | + name: 'vuepress-plugin-umami-analytics', 27 | + define: app => { 28 | + if (!app.env.isDev) { 29 | + return { 30 | + __UMAMI_ANALYTICS_DO_NOT_TRACK__: doNotTrack || false, 31 | + __UMAMI_ANALYTICS_AUTO_TRACK__: autoTrack || false, 32 | + __UMAMI_ANALYTICS_HOST_URL__: hostUrl || null, 33 | + __UMAMI_ANALYTICS_DOMAINS__: domains || [], 34 | + __UMAMI_ANALYTICS_CACHE__: cache || false, 35 | + __UMAMI_ANALYTICS_SRC__: src, 36 | + __UMAMI_ANALYTICS_ID__: id, 37 | + }; 38 | + } 39 | }, 40 | clientConfigFile: path.resolve(__dirname, '../client/index.js'), 41 | }; 42 | -------------------------------------------------------------------------------- /provider.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/provider' 2 | -------------------------------------------------------------------------------- /provider.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/provider') 2 | -------------------------------------------------------------------------------- /scripts/run-example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { resolve } = require('path') 4 | const execa = require('execa') 5 | const fs = require('fs-extra') 6 | 7 | const { argv } = process 8 | const [, , example] = argv 9 | 10 | if (!example) { 11 | console.error('Please provide an example name') 12 | process.exit(1) 13 | } 14 | 15 | const binPath = resolve(__dirname, '..', 'bin/run') 16 | const examplePath = resolve(__dirname, '..', 'examples', example) 17 | const nodeModulesPath = resolve(examplePath, 'node_modules') 18 | 19 | fs.ensureDirSync(nodeModulesPath) 20 | fs.ensureSymlinkSync(process.cwd(), resolve(nodeModulesPath, 'surgio')) 21 | 22 | execa(binPath, ['generate', '--project', examplePath], { stdio: 'inherit' }) 23 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/__tests__/__snapshots__/index.test.ts.snap -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import * as index from '../' 4 | 5 | test('exports.utils', (t) => { 6 | t.snapshot(index.utils) 7 | }) 8 | 9 | test('exports.categories', (t) => { 10 | t.snapshot(index.categories) 11 | }) 12 | 13 | test('exports.define*', (t) => { 14 | const keys = Object.keys(index).filter((key) => key.startsWith('define')) 15 | 16 | t.snapshot(keys) 17 | }) 18 | -------------------------------------------------------------------------------- /src/base-command.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | import { resolve } from 'path' 3 | import { Command, Flags, Interfaces, Config } from '@oclif/core' 4 | import { transports } from '@surgio/logger' 5 | import ora from 'ora' 6 | 7 | import redis from './redis' 8 | import { CommandConfig } from './types' 9 | import { loadConfig } from './config' 10 | import { errorHandler } from './utils/error-helper' 11 | 12 | export type Flags = Interfaces.InferredFlags< 13 | (typeof BaseCommand)['baseFlags'] & T['flags'] 14 | > 15 | export type Args = Interfaces.InferredArgs 16 | 17 | abstract class BaseCommand extends Command { 18 | protected flags!: Flags 19 | protected args!: Args 20 | protected surgioConfig!: CommandConfig 21 | public ora = ora({ 22 | stream: process.stdout, 23 | }) 24 | public projectDir!: string 25 | 26 | constructor(argv: string[], config: Config) { 27 | super(argv, config) 28 | } 29 | 30 | public async init(): Promise { 31 | await super.init() 32 | 33 | const { args, flags } = await this.parse({ 34 | flags: this.ctor.flags, 35 | baseFlags: (super.ctor as typeof BaseCommand).baseFlags, 36 | args: this.ctor.args, 37 | strict: this.ctor.strict, 38 | }) 39 | 40 | this.flags = flags as Flags 41 | this.args = args as Args 42 | 43 | // istanbul ignore next 44 | if (flags.verbose) { 45 | transports.console.level = 'debug' 46 | } 47 | 48 | if (flags.project.startsWith('.')) { 49 | flags.project = resolve(process.cwd(), flags.project) 50 | } 51 | 52 | this.projectDir = flags.project 53 | this.surgioConfig = loadConfig(this.projectDir) 54 | } 55 | 56 | protected async catch(err: Error & { exitCode?: number }): Promise { 57 | if (this.ora.isSpinning) { 58 | this.ora.fail() 59 | } 60 | await errorHandler.call(this, err) 61 | this.exit(err.exitCode || 1) 62 | } 63 | 64 | protected async cleanup(): Promise { 65 | await redis.destroyRedis() 66 | if (this.ora.isSpinning) { 67 | this.ora.succeed() 68 | } 69 | } 70 | } 71 | 72 | BaseCommand.enableJsonFlag = true 73 | BaseCommand.baseFlags = { 74 | project: Flags.string({ 75 | char: 'p', 76 | description: 'Surgio 配置目录', 77 | default: './', 78 | helpGroup: 'GLOBAL', 79 | }), 80 | verbose: Flags.boolean({ 81 | char: 'V', 82 | description: '打印调试日志', 83 | default: false, 84 | helpGroup: 'GLOBAL', 85 | }), 86 | } 87 | 88 | export default BaseCommand 89 | -------------------------------------------------------------------------------- /src/commands/check.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import path from 'path' 3 | import { Args, ux } from '@oclif/core' 4 | import fs from 'fs-extra' 5 | import inquirer from 'inquirer' 6 | 7 | import BaseCommand from '../base-command' 8 | import { getConfig } from '../config' 9 | import { getProvider } from '../provider' 10 | 11 | class CheckCommand extends BaseCommand { 12 | static description = '查询 Provider' 13 | 14 | static args = { 15 | provider: Args.string({ 16 | description: '发起检查的 Provider', 17 | required: true, 18 | }), 19 | } 20 | 21 | public async run(): Promise { 22 | const nodeList = await this.getNodeList(this.args.provider) 23 | 24 | if (!process.stdin.isTTY) { 25 | console.log(JSON.stringify(nodeList, null, 2)) 26 | return 27 | } 28 | 29 | const answers = await inquirer.prompt([ 30 | { 31 | type: 'list', 32 | name: 'node', 33 | message: '请选择节点', 34 | choices: nodeList.map((node) => { 35 | const name = 36 | 'hostname' in node && 'port' in node 37 | ? `${node.nodeName} - ${node.hostname}:${node.port}` 38 | : node.nodeName 39 | 40 | return { 41 | name, 42 | value: node, 43 | } 44 | }), 45 | }, 46 | ]) 47 | 48 | console.log(JSON.stringify(answers.node, null, 2)) 49 | 50 | await this.cleanup() 51 | } 52 | 53 | private async getNodeList(providerName: string) { 54 | ux.action.start('正在获取 Provider 信息') 55 | 56 | const config = getConfig() 57 | const filePath = path.resolve(config.providerDir, `./${providerName}.js`) 58 | const file: any | Error = fs.existsSync(filePath) 59 | ? await import(filePath) 60 | : new Error('找不到该 Provider') 61 | 62 | if (file instanceof Error) { 63 | throw file 64 | } 65 | 66 | const provider = await getProvider(providerName, file.default) 67 | const nodeList = await provider.getNodeList() 68 | 69 | ux.action.stop() 70 | 71 | return nodeList 72 | } 73 | } 74 | 75 | export default CheckCommand 76 | -------------------------------------------------------------------------------- /src/commands/clean-cache.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import os from 'os' 3 | import path from 'path' 4 | import { ux } from '@oclif/core' 5 | import fs from 'fs-extra' 6 | 7 | import BaseCommand from '../base-command' 8 | import { TMP_FOLDER_NAME } from '../constant' 9 | import { cleanCaches } from '../utils/cache' 10 | 11 | class CleanCacheCommand extends BaseCommand { 12 | static description = '清除缓存' 13 | 14 | public async run(): Promise { 15 | const tmpDir = path.join(os.tmpdir(), TMP_FOLDER_NAME) 16 | 17 | ux.action.start('正在清除缓存') 18 | 19 | if (fs.existsSync(tmpDir)) { 20 | await fs.remove(tmpDir) 21 | } 22 | 23 | await cleanCaches() 24 | 25 | ux.action.stop() 26 | 27 | await this.cleanup() 28 | } 29 | } 30 | 31 | export default CleanCacheCommand 32 | -------------------------------------------------------------------------------- /src/commands/doctor.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import BaseCommand from '../base-command' 3 | import { generateDoctorInfo } from '../utils/doctor' 4 | 5 | class DoctorCommand extends BaseCommand { 6 | static description = '检查运行环境' 7 | 8 | public async run(): Promise { 9 | const doctorInfo = await generateDoctorInfo( 10 | this.projectDir, 11 | this.config.pjson, 12 | ) 13 | 14 | doctorInfo.forEach((item) => { 15 | console.log(item) 16 | }) 17 | 18 | await this.cleanup() 19 | } 20 | } 21 | 22 | export default DoctorCommand 23 | -------------------------------------------------------------------------------- /src/commands/lint.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import { Flags } from '@oclif/core' 3 | 4 | import BaseCommand from '../base-command' 5 | import { check, checkAndFix } from '../utils/linter' 6 | 7 | class LintCommand extends BaseCommand { 8 | static description = '运行 JS 语法检查' 9 | 10 | public async run(): Promise { 11 | let result 12 | 13 | if (this.flags.fix) { 14 | result = await checkAndFix(this.projectDir) 15 | } else { 16 | result = await check(this.projectDir) 17 | } 18 | 19 | if (!result) { 20 | console.warn( 21 | '⚠️ JS 语法检查不通过,请根据提示修改文件(可添加参数 --fix 自动修复部分错误, 参考 https://url.royli.dev/SeB6m)', 22 | ) 23 | process.exit(1) 24 | } else { 25 | console.log('✅ JS 语法检查通过') 26 | } 27 | 28 | await this.cleanup() 29 | } 30 | } 31 | 32 | LintCommand.flags = { 33 | fix: Flags.boolean({ 34 | default: false, 35 | description: '自动修复部分 Lint 错误', 36 | }), 37 | } 38 | 39 | export default LintCommand 40 | -------------------------------------------------------------------------------- /src/commands/new.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import { join } from 'path' 3 | import { Args } from '@oclif/core' 4 | import { runner, Logger } from '@royli/hygen' 5 | 6 | import BaseCommand from '../base-command' 7 | const defaultTemplates = join(__dirname, '../../hygen-template') 8 | 9 | class NewCommand extends BaseCommand { 10 | static description = '新建文件助手' 11 | 12 | public async run(): Promise { 13 | const args: string[] = [...this.argv].concat('new') // [type] new ... 14 | 15 | await runner(args, { 16 | templates: defaultTemplates, 17 | cwd: this.projectDir, 18 | logger: new Logger(console.log.bind(console)), 19 | createPrompter: () => require('inquirer'), 20 | exec: async (action, body) => { 21 | const opts = body && body.length > 0 ? { input: body } : {} 22 | await require('execa')(action, opts) 23 | }, 24 | }) 25 | 26 | await this.cleanup() 27 | } 28 | } 29 | 30 | NewCommand.args = { 31 | type: Args.custom({ 32 | description: '文件类型', 33 | required: true, 34 | options: ['provider', 'template', 'artifact'], 35 | })(), 36 | } 37 | 38 | export default NewCommand 39 | -------------------------------------------------------------------------------- /src/commands/subscriptions.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | import { promises as fsp } from 'fs' 3 | import { basename, join } from 'path' 4 | import { createLogger } from '@surgio/logger' 5 | 6 | import BaseCommand from '../base-command' 7 | import { getProvider, PossibleProviderType } from '../provider' 8 | import { formatSubscriptionUserInfo } from '../utils' 9 | 10 | const logger = createLogger({ 11 | service: 'surgio:SubscriptionsCommand', 12 | }) 13 | 14 | class SubscriptionsCommand extends BaseCommand { 15 | static description = '查询订阅信息' 16 | 17 | public async run(): Promise { 18 | const providerList = await this.listProviders() 19 | 20 | for (const provider of providerList) { 21 | if (provider.supportGetSubscriptionUserInfo) { 22 | const userInfo = await provider.getSubscriptionUserInfo() 23 | 24 | if (userInfo) { 25 | const format = formatSubscriptionUserInfo(userInfo) 26 | console.log( 27 | '🤟 %s 已用流量:%s 剩余流量:%s 有效期至:%s', 28 | provider.name, 29 | format.used, 30 | format.left, 31 | format.expire, 32 | ) 33 | } else { 34 | console.log('⚠️ 无法查询 %s 的流量信息', provider.name) 35 | } 36 | } else { 37 | console.log('⚠️ 无法查询 %s 的流量信息', provider.name) 38 | } 39 | } 40 | 41 | await this.cleanup() 42 | } 43 | 44 | private async listProviders(): Promise> { 45 | const files = await fsp.readdir(this.surgioConfig.providerDir, { 46 | encoding: 'utf8', 47 | }) 48 | const providerList: PossibleProviderType[] = [] 49 | 50 | async function readProvider( 51 | path: string, 52 | ): Promise { 53 | let provider 54 | 55 | try { 56 | const providerName = basename(path, '.js') 57 | const module = await import(path) 58 | 59 | logger.debug('read %s %s', providerName, path) 60 | 61 | // eslint-disable-next-line prefer-const 62 | provider = await getProvider(providerName, module.default) 63 | } catch (err) { 64 | logger.debug(`${path} 不是一个合法的模块`) 65 | return undefined 66 | } 67 | 68 | if (!provider?.type) { 69 | logger.debug(`${path} 不是一个 Provider`) 70 | return undefined 71 | } 72 | 73 | logger.debug('got provider %j', provider) 74 | return provider 75 | } 76 | 77 | for (const file of files) { 78 | const result = await readProvider( 79 | join(this.surgioConfig.providerDir, file), 80 | ) 81 | if (result) { 82 | providerList.push(result) 83 | } 84 | } 85 | 86 | return providerList 87 | } 88 | } 89 | 90 | export default SubscriptionsCommand 91 | -------------------------------------------------------------------------------- /src/configurables.ts: -------------------------------------------------------------------------------- 1 | import { Promisable } from 'type-fest' 2 | 3 | import { 4 | BlackSSLProviderConfig, 5 | ClashProviderConfig, 6 | CommandConfigBeforeNormalize, 7 | CustomProviderConfig, 8 | PossibleProviderConfigType, 9 | ShadowsocksJsonSubscribeProviderConfig, 10 | ShadowsocksrSubscribeProviderConfig, 11 | ShadowsocksSubscribeProviderConfig, 12 | SsdProviderConfig, 13 | SupportProviderEnum, 14 | TrojanProviderConfig, 15 | V2rayNSubscribeProviderConfig, 16 | } from './types' 17 | 18 | export const defineSurgioConfig = (config: CommandConfigBeforeNormalize) => 19 | config 20 | 21 | export type ProviderDefineFunction< 22 | T extends PossibleProviderConfigType, 23 | U = Omit, 24 | > = (config: U) => T | Promisable 25 | 26 | export const defineBlackSSLProvider: ProviderDefineFunction< 27 | BlackSSLProviderConfig 28 | > = (config) => ({ 29 | ...config, 30 | type: SupportProviderEnum.BlackSSL, 31 | }) 32 | 33 | export const defineClashProvider: ProviderDefineFunction< 34 | ClashProviderConfig 35 | > = (config) => ({ 36 | ...config, 37 | type: SupportProviderEnum.Clash, 38 | }) 39 | 40 | export const defineCustomProvider: ProviderDefineFunction< 41 | CustomProviderConfig 42 | > = (config) => ({ 43 | ...config, 44 | type: SupportProviderEnum.Custom, 45 | }) 46 | 47 | export const defineShadowsocksJsonSubscribeProvider: ProviderDefineFunction< 48 | ShadowsocksJsonSubscribeProviderConfig 49 | > = (config) => ({ 50 | ...config, 51 | type: SupportProviderEnum.ShadowsocksJsonSubscribe, 52 | }) 53 | 54 | export const defineShadowsocksSubscribeProvider: ProviderDefineFunction< 55 | ShadowsocksSubscribeProviderConfig 56 | > = (config) => ({ 57 | ...config, 58 | type: SupportProviderEnum.ShadowsocksSubscribe, 59 | }) 60 | 61 | export const defineShadowsocksrSubscribeProvider: ProviderDefineFunction< 62 | ShadowsocksrSubscribeProviderConfig 63 | > = (config) => ({ 64 | ...config, 65 | type: SupportProviderEnum.ShadowsocksrSubscribe, 66 | }) 67 | 68 | export const defineSsdProvider: ProviderDefineFunction = ( 69 | config, 70 | ) => ({ 71 | ...config, 72 | type: SupportProviderEnum.Ssd, 73 | }) 74 | 75 | export const defineTrojanProvider: ProviderDefineFunction< 76 | TrojanProviderConfig 77 | > = (config) => ({ 78 | ...config, 79 | type: SupportProviderEnum.Trojan, 80 | }) 81 | 82 | export const defineV2rayNSubscribeProvider: ProviderDefineFunction< 83 | V2rayNSubscribeProviderConfig 84 | > = (config) => ({ 85 | ...config, 86 | type: SupportProviderEnum.V2rayNSubscribe, 87 | }) 88 | -------------------------------------------------------------------------------- /src/constant/__tests__/constant.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { expectType } from 'ts-expect' 3 | import { z } from 'zod' 4 | 5 | import { 6 | CLASH_META_SUPPORTED_VMESS_NETWORK, 7 | V2RAYN_SUPPORTED_VMESS_NETWORK, 8 | } from '../' 9 | import { VmessNetworkValidator } from '../../validators' 10 | 11 | test('constant', (t) => { 12 | for (const network of V2RAYN_SUPPORTED_VMESS_NETWORK) { 13 | expectType>(network) 14 | } 15 | 16 | for (const network of CLASH_META_SUPPORTED_VMESS_NETWORK) { 17 | expectType>(network) 18 | } 19 | 20 | t.pass() 21 | }) 22 | -------------------------------------------------------------------------------- /src/constant/env.ts: -------------------------------------------------------------------------------- 1 | export const ENV_NETWORK_TIMEOUT_KEY = 'SURGIO_NETWORK_TIMEOUT' 2 | 3 | export const ENV_NETWORK_RESOLVE_TIMEOUT = 'SURGIO_NETWORK_RESOLVE_TIMEOUT' 4 | 5 | export const ENV_SURGIO_NETWORK_CONCURRENCY = 'SURGIO_NETWORK_CONCURRENCY' 6 | 7 | export const ENV_SURGIO_NETWORK_RETRY = 'SURGIO_NETWORK_RETRY' 8 | 9 | export const ENV_SURGIO_NETWORK_CLASH_UA = 'SURGIO_NETWORK_CLASH_UA' 10 | 11 | export const ENV_SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE = 12 | 'SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE' 13 | 14 | export const ENV_SURGIO_PROVIDER_CACHE_MAXAGE = 'SURGIO_PROVIDER_CACHE_MAXAGE' 15 | 16 | export const ENV_SURGIO_GFW_FREE = 'SURGIO_GFW_FREE' 17 | 18 | export const SURGIO_RENDERED_ARTIFACT_CACHE_MAXAGE = 19 | 'SURGIO_RENDERED_ARTIFACT_CACHE_MAXAGE' 20 | -------------------------------------------------------------------------------- /src/constant/error.ts: -------------------------------------------------------------------------------- 1 | export const ERR_INVALID_FILTER = '传入的过滤器无效,请检查语法和变量名是否正确' 2 | -------------------------------------------------------------------------------- /src/constant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env' 2 | export * from './error' 3 | export * from './constant' 4 | -------------------------------------------------------------------------------- /src/filters/classes.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import { 4 | NodeFilterType, 5 | SortedNodeFilterType, 6 | PossibleNodeConfigType, 7 | } from '../types' 8 | 9 | // tslint:disable-next-line:max-classes-per-file 10 | export class SortFilterWithSortedFilters implements SortedNodeFilterType { 11 | public readonly supportSort = true 12 | 13 | constructor(public _filters: Array) { 14 | this.filter.bind(this) 15 | } 16 | 17 | public filter( 18 | nodeList: ReadonlyArray, 19 | ): ReadonlyArray { 20 | const result: T[] = [] 21 | 22 | this._filters.forEach((filter) => { 23 | result.push(...nodeList.filter(filter)) 24 | }) 25 | 26 | return _.uniqBy(result, (node) => node.nodeName) 27 | } 28 | } 29 | 30 | // tslint:disable-next-line:max-classes-per-file 31 | export class SortFilterWithSortedKeywords implements SortedNodeFilterType { 32 | public readonly supportSort = true 33 | 34 | constructor(public _keywords: Array) { 35 | this.filter.bind(this) 36 | } 37 | 38 | public filter( 39 | nodeList: ReadonlyArray, 40 | ): ReadonlyArray { 41 | const result: T[] = [] 42 | 43 | this._keywords.forEach((keyword) => { 44 | result.push(...nodeList.filter((node) => node.nodeName.includes(keyword))) 45 | }) 46 | 47 | return _.uniqBy(result, (node) => node.nodeName) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * as internalFilters from './filters' 2 | export * from './utils' 3 | -------------------------------------------------------------------------------- /src/generator/__tests__/__snapshots__/artifact.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/generator/__tests__/artifact.test.ts` 2 | 3 | The actual snapshot is saved in `artifact.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## render with extendRenderContext 8 | 9 | > Snapshot 1 10 | 11 | `␊ 12 | ` 13 | 14 | > Snapshot 2 15 | 16 | `bar␊ 17 | ` 18 | 19 | > Snapshot 3 20 | 21 | `foo␊ 22 | ` 23 | 24 | ## Artifact with underlyingProxy 25 | 26 | > Snapshot 1 27 | 28 | `#!MANAGED-CONFIG https://example.com/new_path.conf?access_token=abcd interval=43200 strict=false␊ 29 | ␊ 30 | [Proxy]␊ 31 | 🇺🇸US 1 = ss, us.example.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password, obfs=tls, obfs-host=gateway-carry.icloud.com, tfo=true, mptcp=true, ecn=true, underlying-proxy=underlying-proxy, block-quic=off␊ 32 | 🇺🇸US 2 = ss, us.example.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password, tfo=true, mptcp=true, ecn=true, underlying-proxy=underlying-proxy, block-quic=off␊ 33 | ␊ 34 | [Proxy Group]␊ 35 | Proxy = select, 🇺🇸US 1, 🇺🇸US 2, 🇺🇸US 3␊ 36 | ` 37 | -------------------------------------------------------------------------------- /src/generator/__tests__/__snapshots__/artifact.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/generator/__tests__/__snapshots__/artifact.test.ts.snap -------------------------------------------------------------------------------- /src/generator/__tests__/__snapshots__/json-template.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/generator/__tests__/json-template.test.ts` 2 | 3 | The actual snapshot is saved in `json-template.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## render 8 | 9 | > Snapshot 1 10 | 11 | `{␊ 12 | "foo": "foo",␊ 13 | "bar": "bar",␊ 14 | "outbounds": "new-value"␊ 15 | }` 16 | -------------------------------------------------------------------------------- /src/generator/__tests__/__snapshots__/json-template.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/generator/__tests__/__snapshots__/json-template.test.ts.snap -------------------------------------------------------------------------------- /src/generator/__tests__/__snapshots__/template.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/generator/__tests__/__snapshots__/template.test.ts.snap -------------------------------------------------------------------------------- /src/generator/__tests__/snippet.tpl: -------------------------------------------------------------------------------- 1 | USER-AGENT,com.google.ios.youtube* 2 | USER-AGENT,YouTube* 3 | DOMAIN-SUFFIX,googlevideo.com 4 | DOMAIN-SUFFIX,youtube.com 5 | DOMAIN,youtubei.googleapis.com 6 | PROCESS-NAME,YT Music 7 | -------------------------------------------------------------------------------- /src/generator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './artifact' 2 | export * from './template' 3 | export { 4 | extendOutbounds, 5 | createExtendFunction, 6 | combineExtendFunctions, 7 | } from './json-template' 8 | -------------------------------------------------------------------------------- /src/generator/json-template.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { JsonObject } from 'type-fest' 3 | import _ from 'lodash' 4 | import fs from 'fs-extra' 5 | 6 | type ExtendContext = Record 7 | 8 | type PremitiveValue = string | number | boolean 9 | type ExtendValue = 10 | | JsonObject[] 11 | | JsonObject 12 | | PremitiveValue[] 13 | | PremitiveValue 14 | | (( 15 | extendContext: ExtendContext, 16 | ) => JsonObject[] | JsonObject | PremitiveValue[] | PremitiveValue) 17 | 18 | type ExtendFunction = ( 19 | extendValue: ExtendValue, 20 | ) => (jsonInput: JsonObject, extendContext?: ExtendContext) => JsonObject 21 | 22 | export const createExtendFunction = (extendKey: string) => { 23 | const extendFunction: ExtendFunction = (extendValue) => { 24 | return (jsonInput, extendContext = {}) => { 25 | const jsonInputCopy = _.cloneDeep(jsonInput) 26 | const isExist = _.get(jsonInputCopy, extendKey) 27 | const extendValueIsFunction = _.isFunction(extendValue) 28 | const valueToExtend = extendValueIsFunction 29 | ? extendValue(extendContext) 30 | : extendValue 31 | 32 | if (isExist) { 33 | if (_.isArray(isExist)) { 34 | if (_.isArray(valueToExtend)) { 35 | _.set(jsonInputCopy, extendKey, [...isExist, ...valueToExtend]) 36 | } else { 37 | _.set(jsonInputCopy, extendKey, [...isExist, valueToExtend]) 38 | } 39 | } else { 40 | _.set(jsonInputCopy, extendKey, valueToExtend) 41 | } 42 | } else { 43 | _.set(jsonInputCopy, extendKey, valueToExtend) 44 | } 45 | 46 | return jsonInputCopy 47 | } 48 | } 49 | 50 | return extendFunction 51 | } 52 | 53 | export const extendOutbounds = createExtendFunction('outbounds') 54 | 55 | export const combineExtendFunctions = ( 56 | ...args: ReturnType[] 57 | ): ReturnType => { 58 | return (jsonInput, extendContext = {}) => { 59 | return args.reduce((acc, extend) => extend(acc, extendContext), jsonInput) 60 | } 61 | } 62 | 63 | export const render = ( 64 | templateDir: string, 65 | fileName: string, 66 | extend: (...args: any[]) => any, 67 | extendContext: ExtendContext, 68 | ): string => { 69 | const templatePath = join(templateDir, fileName) 70 | const jsonInput = fs.readJsonSync(templatePath) 71 | 72 | try { 73 | const jsonOutput = (extend as ReturnType)( 74 | jsonInput, 75 | extendContext, 76 | ) 77 | 78 | return JSON.stringify(jsonOutput, null, 2) 79 | } catch (error) { 80 | // istanbul ignore next 81 | if (error instanceof Error) { 82 | throw new Error( 83 | `Error when rendering JSON template ${fileName}: ${error.message}`, 84 | ) 85 | } 86 | 87 | // istanbul ignore next 88 | throw new Error(`Error when rendering JSON template ${fileName}`) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hooks/init.ts: -------------------------------------------------------------------------------- 1 | import { Hook } from '@oclif/core' 2 | 3 | const hook: Hook<'init'> = async function (opts) { 4 | // @ts-ignore 5 | import('update-notifier').then(({ default: updateNotifier }) => { 6 | updateNotifier({ pkg: opts.config.pjson }).notify() 7 | }) 8 | } 9 | 10 | export default hook 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAWS, 3 | isAWSLambda, 4 | isFlyIO, 5 | isGitHubActions, 6 | isGitLabCI, 7 | isHeroku, 8 | isNetlify, 9 | isNow, 10 | isRailway, 11 | isVercel, 12 | } from './utils' 13 | import * as useragentUtils from './utils/useragent' 14 | import * as filters from './filters' 15 | import { CATEGORIES } from './constant' 16 | 17 | export type { CommandConfigBeforeNormalize as SurgioConfig } from './types' 18 | export * from './configurables' 19 | export { default as httpClient } from './utils/http-client' 20 | export { unifiedCache as cache } from './utils/cache' 21 | export { 22 | extendOutbounds, 23 | createExtendFunction, 24 | combineExtendFunctions, 25 | } from './generator' 26 | 27 | const { internalFilters, ...filtersUtils } = filters 28 | 29 | export const utils = { 30 | ...internalFilters, 31 | ...filtersUtils, 32 | ...useragentUtils, 33 | isHeroku, 34 | isNow, 35 | isVercel, 36 | isGitHubActions, 37 | isGitLabCI, 38 | isRailway, 39 | isNetlify, 40 | isAWS, 41 | isFlyIO, 42 | isAWSLambda, 43 | } as const 44 | 45 | export const categories = CATEGORIES 46 | -------------------------------------------------------------------------------- /src/internal.ts: -------------------------------------------------------------------------------- 1 | import { PackageJson } from 'type-fest' 2 | 3 | export { isZodError, isSurgioError, SurgioError } from './utils' 4 | export * from './utils/cache' 5 | export * from './types' 6 | 7 | export const packageJson = require('../package.json') as PackageJson 8 | -------------------------------------------------------------------------------- /src/misc/deprecation.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/misc/deprecation.ts -------------------------------------------------------------------------------- /src/provider/__tests__/ShadowsocksJsonSubscribeProvider.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import { NodeTypeEnum } from '../../types' 5 | import * as config from '../../config' 6 | import { getShadowsocksJSONConfig } from '../ShadowsocksJsonSubscribeProvider' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.beforeEach(() => { 11 | sandbox.restore() 12 | sandbox.stub(config, 'getConfig').returns({} as any) 13 | }) 14 | 15 | test('getShadowsocksJSONConfig', async (t) => { 16 | const config = await getShadowsocksJSONConfig( 17 | 'http://example.com/gui-config.json?v=1', 18 | true, 19 | ) 20 | const config2 = await getShadowsocksJSONConfig( 21 | 'http://example.com/gui-config.json?v=2', 22 | false, 23 | ) 24 | 25 | t.deepEqual(config[0], { 26 | nodeName: '🇺🇸US 1', 27 | type: NodeTypeEnum.Shadowsocks, 28 | hostname: 'us.example.com', 29 | port: 443, 30 | method: 'chacha20-ietf-poly1305', 31 | password: 'password', 32 | udpRelay: true, 33 | obfs: 'tls', 34 | obfsHost: 'gateway-carry.icloud.com', 35 | }) 36 | t.deepEqual(config[1], { 37 | nodeName: '🇺🇸US 2', 38 | type: NodeTypeEnum.Shadowsocks, 39 | hostname: 'us.example.com', 40 | port: 444, 41 | method: 'chacha20-ietf-poly1305', 42 | password: 'password', 43 | udpRelay: true, 44 | }) 45 | t.deepEqual(config[2], { 46 | nodeName: '🇺🇸US 3', 47 | type: NodeTypeEnum.Shadowsocks, 48 | hostname: 'us.example.com', 49 | port: 445, 50 | method: 'chacha20-ietf-poly1305', 51 | password: 'password', 52 | udpRelay: true, 53 | obfs: 'tls', 54 | obfsHost: 'www.bing.com', 55 | }) 56 | t.deepEqual(config[3], { 57 | nodeName: '🇺🇸US 4', 58 | type: NodeTypeEnum.Shadowsocks, 59 | hostname: 'us.example.com', 60 | port: 80, 61 | method: 'chacha20-ietf-poly1305', 62 | password: 'password', 63 | udpRelay: true, 64 | obfs: 'http', 65 | obfsHost: 'www.bing.com', 66 | }) 67 | t.deepEqual(config2[0], { 68 | nodeName: '🇺🇸US 1', 69 | type: NodeTypeEnum.Shadowsocks, 70 | hostname: 'us.example.com', 71 | port: 443, 72 | method: 'chacha20-ietf-poly1305', 73 | password: 'password', 74 | udpRelay: false, 75 | obfs: 'tls', 76 | obfsHost: 'gateway-carry.icloud.com', 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/provider/__tests__/ShadowsocksSubscribeProvider.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import * as config from '../../config' 5 | import { getShadowsocksSubscription } from '../ShadowsocksSubscribeProvider' 6 | import { NodeTypeEnum } from '../../types' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.beforeEach(() => { 11 | sandbox.restore() 12 | sandbox.stub(config, 'getConfig').returns({} as any) 13 | }) 14 | 15 | test('getShadowsocksSubscription with udp', async (t) => { 16 | const { nodeList } = await getShadowsocksSubscription( 17 | 'http://example.com/test-ss-sub.txt', 18 | true, 19 | ) 20 | 21 | t.deepEqual(nodeList[0], { 22 | type: NodeTypeEnum.Shadowsocks, 23 | nodeName: '🇺🇸US 1', 24 | hostname: 'us.example.com', 25 | port: '443', 26 | method: 'chacha20-ietf-poly1305', 27 | password: 'password', 28 | udpRelay: true, 29 | obfs: 'tls', 30 | obfsHost: 'gateway-carry.icloud.com', 31 | }) 32 | t.deepEqual(nodeList[1], { 33 | nodeName: '🇺🇸US 2', 34 | type: NodeTypeEnum.Shadowsocks, 35 | hostname: 'us.example.com', 36 | port: '443', 37 | method: 'chacha20-ietf-poly1305', 38 | password: 'password', 39 | udpRelay: true, 40 | }) 41 | t.deepEqual(nodeList[2], { 42 | nodeName: '🇺🇸US 3', 43 | type: NodeTypeEnum.Shadowsocks, 44 | hostname: 'us.example.com', 45 | port: '443', 46 | method: 'chacha20-ietf-poly1305', 47 | password: 'password', 48 | udpRelay: true, 49 | obfs: 'wss', 50 | obfsHost: 'gateway-carry.icloud.com', 51 | }) 52 | }) 53 | 54 | test('getShadowsocksSubscription without udp', async (t) => { 55 | const { nodeList } = await getShadowsocksSubscription( 56 | 'http://example.com/test-ss-sub.txt', 57 | ) 58 | 59 | t.deepEqual(nodeList[0], { 60 | type: NodeTypeEnum.Shadowsocks, 61 | nodeName: '🇺🇸US 1', 62 | hostname: 'us.example.com', 63 | port: '443', 64 | method: 'chacha20-ietf-poly1305', 65 | password: 'password', 66 | obfs: 'tls', 67 | obfsHost: 'gateway-carry.icloud.com', 68 | }) 69 | t.deepEqual(nodeList[1], { 70 | nodeName: '🇺🇸US 2', 71 | type: NodeTypeEnum.Shadowsocks, 72 | hostname: 'us.example.com', 73 | port: '443', 74 | method: 'chacha20-ietf-poly1305', 75 | password: 'password', 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/provider/__tests__/ShadowsocksrSubscribeProvider.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import { NodeTypeEnum } from '../../types' 5 | import * as config from '../../config' 6 | import { getShadowsocksrSubscription } from '../ShadowsocksrSubscribeProvider' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.beforeEach(() => { 11 | sandbox.restore() 12 | sandbox.stub(config, 'getConfig').returns({} as any) 13 | }) 14 | 15 | test('getShadowsocksrSubscription', async (t) => { 16 | const { nodeList } = await getShadowsocksrSubscription( 17 | 'http://example.com/test-ssr-sub.txt?v=1', 18 | false, 19 | ) 20 | const { nodeList: nodeList2 } = await getShadowsocksrSubscription( 21 | 'http://example.com/test-ssr-sub.txt?v=2', 22 | true, 23 | ) 24 | 25 | t.deepEqual(nodeList[0], { 26 | nodeName: '测试中文', 27 | type: NodeTypeEnum.Shadowsocksr, 28 | hostname: '127.0.0.1', 29 | port: '1234', 30 | method: 'aes-128-cfb', 31 | password: 'aaabbb', 32 | obfs: 'tls1.2_ticket_auth', 33 | obfsparam: 'breakwa11.moe', 34 | protocol: 'auth_aes128_md5', 35 | protoparam: '', 36 | udpRelay: false, 37 | }) 38 | t.deepEqual(nodeList2[0], { 39 | nodeName: '测试中文', 40 | type: NodeTypeEnum.Shadowsocksr, 41 | hostname: '127.0.0.1', 42 | port: '1234', 43 | method: 'aes-128-cfb', 44 | password: 'aaabbb', 45 | obfs: 'tls1.2_ticket_auth', 46 | obfsparam: 'breakwa11.moe', 47 | protocol: 'auth_aes128_md5', 48 | protoparam: '', 49 | udpRelay: true, 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/provider/__tests__/SsdProvider.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import { SupportProviderEnum } from '../../types' 5 | import * as config from '../../config' 6 | import SsdProvider from '../SsdProvider' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.beforeEach(() => { 11 | sandbox.restore() 12 | sandbox.stub(config, 'getConfig').returns({} as any) 13 | }) 14 | 15 | test('SsdProvider 1', async (t) => { 16 | const provider = new SsdProvider('test', { 17 | type: SupportProviderEnum.Ssd, 18 | url: 'http://example.com/ssd-sample.txt', 19 | }) 20 | const nodeList = await provider.getNodeList() 21 | 22 | t.snapshot(nodeList) 23 | }) 24 | 25 | test('SsdProvider 2', async (t) => { 26 | const provider = new SsdProvider('test', { 27 | type: SupportProviderEnum.Ssd, 28 | url: 'http://example.com/ssd-sample-2.txt', 29 | }) 30 | const nodeList = await provider.getNodeList() 31 | 32 | t.snapshot(nodeList) 33 | }) 34 | 35 | test('SsdProvider udpRelay', async (t) => { 36 | const provider = new SsdProvider('test', { 37 | type: SupportProviderEnum.Ssd, 38 | url: 'http://example.com/ssd-sample.txt', 39 | udpRelay: true, 40 | }) 41 | const nodeList = await provider.getNodeList() 42 | 43 | t.snapshot(nodeList) 44 | }) 45 | 46 | test('SsdProvider.getSubscriptionUserInfo', async (t) => { 47 | const provider = new SsdProvider('test', { 48 | type: SupportProviderEnum.Ssd, 49 | url: 'http://example.com/ssd-sample.txt', 50 | }) 51 | const userInfo = await provider.getSubscriptionUserInfo() 52 | 53 | t.is(userInfo?.upload, 0) 54 | t.is(userInfo?.download, 32212254720) 55 | t.is(userInfo?.total, 429496729600) 56 | }) 57 | -------------------------------------------------------------------------------- /src/provider/__tests__/V2rayNSubscribeProvider.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import { SupportProviderEnum } from '../../types' 5 | import * as config from '../../config' 6 | import V2rayNSubscribeProvider, { 7 | getV2rayNSubscription, 8 | } from '../V2rayNSubscribeProvider' 9 | 10 | const sandbox = sinon.createSandbox() 11 | 12 | test.beforeEach(() => { 13 | sandbox.restore() 14 | sandbox.stub(config, 'getConfig').returns({} as any) 15 | }) 16 | 17 | test('V2rayNSubscribeProvider', async (t) => { 18 | const provider = new V2rayNSubscribeProvider('test', { 19 | type: SupportProviderEnum.V2rayNSubscribe, 20 | url: 'http://example.com/test-v2rayn-sub.txt', 21 | }) 22 | 23 | await t.notThrowsAsync(async () => { 24 | await provider.getNodeList() 25 | }) 26 | }) 27 | 28 | test('getV2rayNSubscription', async (t) => { 29 | const url = 'http://example.com/test-v2rayn-sub.txt' 30 | const configList = await getV2rayNSubscription({ 31 | url, 32 | isCompatibleMode: false, 33 | }) 34 | 35 | t.snapshot(configList) 36 | }) 37 | 38 | test('getV2rayNSubscription compatible mode', async (t) => { 39 | const url = 'http://example.com/test-v2rayn-sub-compatible.txt' 40 | const configList = await getV2rayNSubscription({ 41 | url, 42 | isCompatibleMode: true, 43 | }) 44 | 45 | t.snapshot(configList) 46 | }) 47 | 48 | test('getV2rayNSubscription udpRelay skipCertVerify', async (t) => { 49 | const url = 'http://example.com/test-v2rayn-sub-compatible.txt' 50 | const configList = await getV2rayNSubscription({ 51 | url, 52 | skipCertVerify: true, 53 | tls13: true, 54 | udpRelay: true, 55 | isCompatibleMode: true, 56 | }) 57 | 58 | t.snapshot(configList) 59 | }) 60 | -------------------------------------------------------------------------------- /src/provider/__tests__/__snapshots__/ClashProvider.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/provider/__tests__/__snapshots__/ClashProvider.test.ts.snap -------------------------------------------------------------------------------- /src/provider/__tests__/__snapshots__/SsdProvider.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/provider/__tests__/SsdProvider.test.ts` 2 | 3 | The actual snapshot is saved in `SsdProvider.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## SsdProvider 1 8 | 9 | > Snapshot 1 10 | 11 | [ 12 | { 13 | hostname: '45.45.45.45', 14 | method: 'aes-128-gcm', 15 | nodeName: 'US GIA', 16 | obfs: 'tls', 17 | obfsHost: 'www.taobao.com', 18 | password: 'password', 19 | port: 444, 20 | type: 'shadowsocks', 21 | udpRelay: false, 22 | }, 23 | { 24 | hostname: '55.55.55.55', 25 | method: 'aes-128-gcm', 26 | nodeName: 'HK GIA', 27 | obfs: 'http', 28 | obfsHost: 'www.taobao.com', 29 | password: 'password', 30 | port: 444, 31 | type: 'shadowsocks', 32 | udpRelay: false, 33 | }, 34 | ] 35 | 36 | ## SsdProvider 2 37 | 38 | > Snapshot 1 39 | 40 | [ 41 | { 42 | hostname: '45.45.45.45', 43 | method: 'aes-128-gcm', 44 | nodeName: 'Test Airport 45.45.45.45:555', 45 | obfs: 'tls', 46 | obfsHost: 'www.taobao.com', 47 | password: 'password', 48 | port: 555, 49 | type: 'shadowsocks', 50 | udpRelay: false, 51 | }, 52 | { 53 | hostname: '55.55.55.55', 54 | method: 'aes-128-gcm', 55 | nodeName: 'Test Airport 55.55.55.55:555', 56 | obfs: 'tls', 57 | obfsHost: 'www.taobao.com', 58 | password: 'password', 59 | port: 555, 60 | type: 'shadowsocks', 61 | udpRelay: false, 62 | }, 63 | ] 64 | 65 | ## SsdProvider udpRelay 66 | 67 | > Snapshot 1 68 | 69 | [ 70 | { 71 | hostname: '45.45.45.45', 72 | method: 'aes-128-gcm', 73 | nodeName: 'US GIA', 74 | obfs: 'tls', 75 | obfsHost: 'www.taobao.com', 76 | password: 'password', 77 | port: 444, 78 | type: 'shadowsocks', 79 | udpRelay: true, 80 | }, 81 | { 82 | hostname: '55.55.55.55', 83 | method: 'aes-128-gcm', 84 | nodeName: 'HK GIA', 85 | obfs: 'http', 86 | obfsHost: 'www.taobao.com', 87 | password: 'password', 88 | port: 444, 89 | type: 'shadowsocks', 90 | udpRelay: true, 91 | }, 92 | ] 93 | -------------------------------------------------------------------------------- /src/provider/__tests__/__snapshots__/SsdProvider.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/provider/__tests__/__snapshots__/SsdProvider.test.ts.snap -------------------------------------------------------------------------------- /src/provider/__tests__/__snapshots__/V2rayNSubscribeProvider.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/provider/__tests__/__snapshots__/V2rayNSubscribeProvider.test.ts.snap -------------------------------------------------------------------------------- /src/provider/index.ts: -------------------------------------------------------------------------------- 1 | import { PossibleProviderConfigType, SupportProviderEnum } from '../types' 2 | import { ProviderDefineFunction } from '../configurables' 3 | 4 | import BlackSSLProvider from './BlackSSLProvider' 5 | import ClashProvider from './ClashProvider' 6 | import CustomProvider from './CustomProvider' 7 | import ShadowsocksJsonSubscribeProvider from './ShadowsocksJsonSubscribeProvider' 8 | import ShadowsocksrSubscribeProvider from './ShadowsocksrSubscribeProvider' 9 | import ShadowsocksSubscribeProvider from './ShadowsocksSubscribeProvider' 10 | import SsdProvider from './SsdProvider' 11 | import TrojanProvider from './TrojanProvider' 12 | import V2rayNSubscribeProvider from './V2rayNSubscribeProvider' 13 | import { PossibleProviderType } from './types' 14 | import Provider from './Provider' 15 | 16 | export { 17 | BlackSSLProvider, 18 | ClashProvider, 19 | CustomProvider, 20 | ShadowsocksJsonSubscribeProvider, 21 | ShadowsocksrSubscribeProvider, 22 | ShadowsocksSubscribeProvider, 23 | SsdProvider, 24 | TrojanProvider, 25 | V2rayNSubscribeProvider, 26 | } 27 | 28 | export type { Provider } 29 | export type * from './types' 30 | 31 | export async function getProvider( 32 | name: string, 33 | config: ReturnType> | PossibleProviderConfigType, 34 | ): Promise { 35 | if (typeof config === 'function') { 36 | config = await config() 37 | } 38 | 39 | switch (config.type) { 40 | case SupportProviderEnum.BlackSSL: 41 | return new BlackSSLProvider(name, config) 42 | 43 | case SupportProviderEnum.ShadowsocksJsonSubscribe: 44 | return new ShadowsocksJsonSubscribeProvider(name, config) 45 | 46 | case SupportProviderEnum.ShadowsocksSubscribe: 47 | return new ShadowsocksSubscribeProvider(name, config) 48 | 49 | case SupportProviderEnum.ShadowsocksrSubscribe: 50 | return new ShadowsocksrSubscribeProvider(name, config) 51 | 52 | case SupportProviderEnum.Custom: 53 | return new CustomProvider(name, config) 54 | 55 | case SupportProviderEnum.V2rayNSubscribe: 56 | return new V2rayNSubscribeProvider(name, config) 57 | 58 | case SupportProviderEnum.Clash: 59 | return new ClashProvider(name, config) 60 | 61 | case SupportProviderEnum.Ssd: 62 | return new SsdProvider(name, config) 63 | 64 | case SupportProviderEnum.Trojan: 65 | return new TrojanProvider(name, config) 66 | 67 | default: 68 | throw new Error(`Unsupported provider type: ${(config as any).type}`) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/provider/types.ts: -------------------------------------------------------------------------------- 1 | import { PossibleNodeConfigType, SubscriptionUserinfo } from '../types' 2 | 3 | import BlackSSLProvider from './BlackSSLProvider' 4 | import ClashProvider from './ClashProvider' 5 | import CustomProvider from './CustomProvider' 6 | import ShadowsocksJsonSubscribeProvider from './ShadowsocksJsonSubscribeProvider' 7 | import ShadowsocksrSubscribeProvider from './ShadowsocksrSubscribeProvider' 8 | import ShadowsocksSubscribeProvider from './ShadowsocksSubscribeProvider' 9 | import SsdProvider from './SsdProvider' 10 | import TrojanProvider from './TrojanProvider' 11 | import V2rayNSubscribeProvider from './V2rayNSubscribeProvider' 12 | 13 | export type PossibleProviderType = 14 | | BlackSSLProvider 15 | | ShadowsocksJsonSubscribeProvider 16 | | ShadowsocksSubscribeProvider 17 | | CustomProvider 18 | | V2rayNSubscribeProvider 19 | | ShadowsocksrSubscribeProvider 20 | | ClashProvider 21 | | SsdProvider 22 | | TrojanProvider 23 | 24 | export type GetNodeListParams = Record & { 25 | requestUserAgent?: string 26 | } 27 | 28 | export type GetNodeListFunction = ( 29 | params?: GetNodeListParams, 30 | ) => Promise> 31 | 32 | export type GetSubscriptionUserInfoFunction = (params?: { 33 | requestUserAgent?: string 34 | }) => Promise 35 | -------------------------------------------------------------------------------- /src/redis.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@surgio/logger' 2 | import Redis from 'ioredis' 3 | 4 | import { CACHE_KEYS } from './constant' 5 | 6 | const logger = createLogger({ service: 'surgio:redis' }) 7 | 8 | const prepareRedis = () => { 9 | let client: Redis | null = null 10 | let redisURL: string | null = null 11 | 12 | return { 13 | hasRedis: () => !!client, 14 | createRedis(_redisURL: string, customRedis?: any): Redis { 15 | if (client && redisURL) { 16 | logger.debug('Redis client already created with URL: %s', redisURL) 17 | return client 18 | } 19 | redisURL = _redisURL 20 | 21 | if (customRedis) { 22 | client = new customRedis(_redisURL) 23 | } else { 24 | client = new Redis( 25 | _redisURL.includes('?') 26 | ? `${_redisURL}&family=0` 27 | : `${_redisURL}?family=0`, 28 | ) 29 | } 30 | 31 | return client as Redis 32 | }, 33 | getRedis(): Redis { 34 | if (!client) { 35 | throw new Error('Redis client is not initialized') 36 | } 37 | return client 38 | }, 39 | async destroyRedis(): Promise { 40 | if (client) { 41 | await client.quit() 42 | client = null 43 | redisURL = null 44 | } 45 | }, 46 | async cleanCache(): Promise { 47 | if (!client) { 48 | return 49 | } 50 | 51 | const keysToRemove = await Promise.all( 52 | Object.keys(CACHE_KEYS).map((key) => { 53 | if (!client) return 54 | 55 | return client.keys(`${CACHE_KEYS[key as keyof typeof CACHE_KEYS]}:*`) 56 | }), 57 | ) 58 | 59 | await Promise.all( 60 | keysToRemove.map((keys) => { 61 | if (!client || !keys || !keys.length) return 62 | return client.del(keys) 63 | }), 64 | ) 65 | }, 66 | } 67 | } 68 | const redis = prepareRedis() 69 | 70 | export default redis 71 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/index.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/utils/__tests__/index.test.ts` 2 | 3 | The actual snapshot is saved in `index.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## getV2rayNNodes 8 | 9 | > Snapshot 1 10 | 11 | '{"v":"2","ps":"测试 1","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"ws","type":"none","path":"/","host":"example.com"}' 12 | 13 | > Snapshot 2 14 | 15 | '{"v":"2","ps":"测试 2","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"tcp","type":"none","tls":"tls"}' 16 | 17 | > Snapshot 3 18 | 19 | '{"v":"2","ps":"测试 3","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"ws","type":"none","path":"/","host":"example.com"}' 20 | 21 | > Snapshot 4 22 | 23 | '{"v":"2","ps":"测试 4","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"tcp","type":"http","path":"/","host":"example.com"}' 24 | 25 | > Snapshot 5 26 | 27 | '{"v":"2","ps":"测试 5","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"h2","type":"none","path":"/","host":"example.com"}' 28 | 29 | > Snapshot 6 30 | 31 | '{"v":"2","ps":"测试 6","add":"1.1.1.1","port":"8080","id":"1386f85e-657b-4d6e-9d56-78badb75e1fd","aid":"64","scy":"auto","net":"grpc","type":"none","path":"example"}' 32 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/utils/__tests__/__snapshots__/index.test.ts.snap -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/remote-snippet.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/utils/__tests__/__snapshots__/remote-snippet.test.ts.snap -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/surfboard.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/utils/__tests__/surfboard.test.ts` 2 | 3 | The actual snapshot is saved in `surfboard.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## getSurfboardNodes 8 | 9 | > Snapshot 1 10 | 11 | `Test Node 1 = ss, example.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password, udp-relay=true, obfs=tls, obfs-host=example.com␊ 12 | Test Node 2 = ss, example2.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password␊ 13 | Test Node 4 = ss, example.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password, udp-relay=true, obfs=tls, obfs-host=example.com␊ 14 | Test Node 5 = ss, example2.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password␊ 15 | Test Node 6 = ss, example2.com, 443, encrypt-method=chacha20-ietf-poly1305, password=password␊ 16 | 测试 1 = vmess, 1.1.1.1, 8080, username=1386f85e-657b-4d6e-9d56-78badb75e1fd, ws=true, ws-path=/, ws-headers="user-agent:Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1", tls=true, vmess-aead=true␊ 17 | 测试 2 = vmess, 1.1.1.1, 8080, username=1386f85e-657b-4d6e-9d56-78badb75e1fd, encrypt-method=aes-128-gcm, vmess-aead=false␊ 18 | 测试 3 = vmess, 1.1.1.1, 8080, username=1386f85e-657b-4d6e-9d56-78badb75e1fd, ws=true, ws-path=/, ws-headers="user-agent:Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1", tls=true, skip-cert-verify=true, vmess-aead=false` 19 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/surfboard.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/utils/__tests__/__snapshots__/surfboard.test.ts.snap -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/surge.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/utils/__tests__/surge.test.ts` 2 | 3 | The actual snapshot is saved in `surge.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## getSurgeWireguardNodes 8 | 9 | > Snapshot 1 10 | 11 | `[WireGuard wg node]␊ 12 | self-ip=10.0.0.1␊ 13 | private-key=privateKey␊ 14 | mtu=1420␊ 15 | peer=(endpoint=wg.example.com:51820, public-key="publicKey")␊ 16 | ␊ 17 | [WireGuard wg node]␊ 18 | self-ip=10.0.0.1␊ 19 | private-key=privateKey␊ 20 | mtu=1420␊ 21 | prefer-ipv6=true␊ 22 | self-ip-v6=2001:db8:85a3::8a2e:370:7334␊ 23 | dns-server="1.1.1.1, 1.0.0.1"␊ 24 | peer=(endpoint=wg.example.com:51820, public-key="publicKey", preshared-key="presharedKey", allowed-ips="0.0.0.0/0", keepalive=25)␊ 25 | ␊ 26 | [WireGuard wg node]␊ 27 | self-ip=10.0.0.1␊ 28 | private-key=privateKey␊ 29 | mtu=1420␊ 30 | prefer-ipv6=true␊ 31 | self-ip-v6=2001:db8:85a3::8a2e:370:7334␊ 32 | dns-server="1.1.1.1, 1.0.0.1"␊ 33 | peer=(endpoint=wg.example.com:51821, public-key="publicKey", preshared-key="presharedKey", allowed-ips="0.0.0.0/0, ::/0", keepalive=25), (endpoint=wg.example.com:51821, public-key="publicKey", preshared-key="presharedKey", allowed-ips="0.0.0.0/0, ::/0", keepalive=25)` 34 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/surge.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/src/utils/__tests__/__snapshots__/surge.test.ts.snap -------------------------------------------------------------------------------- /src/utils/__tests__/cache.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import test from 'ava' 3 | import MockRedis from 'ioredis-mock' 4 | 5 | import * as config from '../../config' 6 | import redis from '../../redis' 7 | import { unifiedCache } from '../cache' 8 | 9 | const sandbox = sinon.createSandbox() 10 | 11 | test.beforeEach(() => { 12 | sandbox.restore() 13 | sandbox.stub(redis, 'getRedis').returns(new MockRedis()) 14 | sandbox.stub(config, 'getConfig').returns({ 15 | cache: { 16 | type: 'redis', 17 | }, 18 | } as any) 19 | }) 20 | 21 | test.after(() => { 22 | sandbox.restore() 23 | }) 24 | 25 | test('RedisCache should work', async (t) => { 26 | await unifiedCache.set('key', 'value') 27 | t.is(await unifiedCache.get('key'), 'value') 28 | t.is(await unifiedCache.has('key'), true) 29 | 30 | await unifiedCache.del('key') 31 | t.is(await unifiedCache.has('key'), false) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utils/__tests__/dns.test.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'dns' 2 | import test from 'ava' 3 | import Bluebird from 'bluebird' 4 | import sinon, { SinonStub } from 'sinon' 5 | 6 | import { resolveDomain } from '../dns' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.afterEach(() => { 11 | sandbox.restore() 12 | }) 13 | 14 | test.serial('resolveDomain ipv4', async (t) => { 15 | sandbox.stub(promises, 'resolve4').callsFake(async () => { 16 | return [{ address: '127.0.0.1', ttl: 100 }] 17 | }) 18 | sandbox.stub(promises, 'resolve6').callsFake(async () => { 19 | return [] 20 | }) 21 | 22 | const ips = await resolveDomain('ipv4.example.com') 23 | t.is(ips[0], '127.0.0.1') 24 | ;(promises.resolve4 as SinonStub).restore() 25 | ;(promises.resolve6 as SinonStub).restore() 26 | }) 27 | 28 | test.serial('resolveDomain ipv6', async (t) => { 29 | sandbox.stub(promises, 'resolve4').callsFake(async () => { 30 | return [] 31 | }) 32 | sandbox.stub(promises, 'resolve6').callsFake(async () => { 33 | return [{ address: '::1', ttl: 100 }] 34 | }) 35 | 36 | const ips = await resolveDomain('ipv6.example.com') 37 | t.is(ips[0], '::1') 38 | ;(promises.resolve4 as SinonStub).restore() 39 | ;(promises.resolve6 as SinonStub).restore() 40 | }) 41 | 42 | test.serial('resolveDomain timeout', async (t) => { 43 | sandbox.stub(promises, 'resolve4').callsFake(async () => { 44 | await Bluebird.delay(1000) 45 | return [{ address: '127.0.0.2', ttl: 1000 }] 46 | }) 47 | sandbox.stub(promises, 'resolve6').callsFake(async () => { 48 | await Bluebird.delay(1000) 49 | return [{ address: '::2', ttl: 1000 }] 50 | }) 51 | 52 | const ips = await resolveDomain('timeout.example.com', 0) 53 | t.is(ips.length, 0) 54 | ;(promises.resolve4 as SinonStub).restore() 55 | ;(promises.resolve6 as SinonStub).restore() 56 | }) 57 | -------------------------------------------------------------------------------- /src/utils/__tests__/flag.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { addFlagMap, prependFlag, removeFlag } from '../flag' 4 | 5 | test.before(() => { 6 | addFlagMap(/foobar/i, '🚀') 7 | addFlagMap('多伦多', '🇨🇦') 8 | addFlagMap(/sri\slanka/i, '🇱🇰') 9 | addFlagMap(/sri\slanka/i, '🇱🇰') 10 | addFlagMap('镇江', '🏁') 11 | }) 12 | 13 | test('prependFlag', (t) => { 14 | t.is(prependFlag('美国'), '🇺🇸 美国') 15 | t.is(prependFlag('上海美国'), '🇺🇸 上海美国') 16 | t.is(prependFlag('美国上海'), '🇺🇸 美国上海') 17 | t.is(prependFlag('阿联酋'), '🇦🇪 阿联酋') 18 | t.is(prependFlag('US'), '🇺🇸 US') 19 | t.is(prependFlag('us'), '🇺🇸 us') 20 | t.is(prependFlag('uk plus'), '🇬🇧 uk plus') 21 | t.is(prependFlag('英国 Plus'), '🇬🇧 英国 Plus') 22 | t.is(prependFlag('UsA-Node'), '🇺🇸 UsA-Node') 23 | t.is(prependFlag('香港_HK'), '🇭🇰 香港_HK') 24 | t.is(prependFlag('新加坡.sg'), '🇸🇬 新加坡.sg') 25 | t.is(prependFlag('日本|JP|'), '🇯🇵 日本|JP|') 26 | t.is(prependFlag('台湾.TWN'), '🇨🇳 台湾.TWN') 27 | t.is(prependFlag('德国Frankfurt'), '🇩🇪 德国Frankfurt') 28 | t.is(prependFlag('🇺🇸 jp'), '🇺🇸 jp') 29 | t.is(prependFlag('🇯🇵 US'), '🇯🇵 US') 30 | t.is(prependFlag('🇺🇸 jp', true), '🇯🇵 jp') 31 | t.is(prependFlag('🇯🇵 🇺🇸 jp', true), '🇯🇵 jp') 32 | t.is(prependFlag('🇺🇸 🇯🇵 US', true), '🇺🇸 US') 33 | t.is(prependFlag('foobar 节点'), '🚀 foobar 节点') 34 | t.is(prependFlag('上海 - 多伦多'), '🇨🇦 上海 - 多伦多') 35 | t.is(prependFlag('上海 - Sri Lanka'), '🇱🇰 上海 - Sri Lanka') 36 | t.is(prependFlag('镇江 - Sri Lanka'), '🇱🇰 镇江 - Sri Lanka') 37 | t.is(prependFlag('镇江'), '🏁 镇江') 38 | }) 39 | 40 | test('removeFlag', (t) => { 41 | t.is(removeFlag('🇺🇸 jp'), 'jp') 42 | t.is(removeFlag('🇺🇸 🇺🇸 jp'), 'jp') 43 | t.is(removeFlag('🇭🇰 香港节点'), '香港节点') 44 | t.is(removeFlag('🇯🇵 🇺🇸 东京'), '东京') 45 | t.is(removeFlag('🚀 测试节点'), '测试节点') 46 | t.is(removeFlag('节点 🇨🇳'), '节点') 47 | t.is(removeFlag('🇸🇬 新加坡 🇸🇬'), '新加坡') 48 | }) 49 | -------------------------------------------------------------------------------- /src/utils/__tests__/http-client.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import httpClient, { getUserAgent } from '../http-client' 4 | 5 | test('getUserAgent', (t) => { 6 | const pkg = require('../../../package.json') 7 | t.is(getUserAgent(), 'surgio/' + pkg.version) 8 | t.is(getUserAgent('foo'), 'foo surgio/' + pkg.version) 9 | }) 10 | 11 | test('httpClient', (t) => { 12 | const pkg = require('../../../package.json') 13 | t.is( 14 | httpClient.defaults.options.headers['user-agent'], 15 | 'surgio/' + pkg.version, 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/__tests__/relayable-url.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import relayableUrl from '../relayable-url' 4 | 5 | test('relayableUrl', (t) => { 6 | t.is( 7 | relayableUrl('http://example.com', 'http://proxy.example.com/%URL%'), 8 | 'http://proxy.example.com/http://example.com', 9 | ) 10 | t.is( 11 | relayableUrl('http://example.com', 'http://proxy.example.com/?url=%%URL%%'), 12 | 'http://proxy.example.com/?url=http%3A%2F%2Fexample.com', 13 | ) 14 | t.is(relayableUrl('http://example.com'), 'http://example.com') 15 | t.throws( 16 | () => { 17 | relayableUrl('http://example.com', 'http://proxy.example.com/') 18 | }, 19 | undefined, 20 | 'relayUrl 中必须包含 %URL% 或 %%URL%% 替换指示符', 21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /src/utils/__tests__/ss.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { NodeTypeEnum } from '../../types' 4 | import { parseSSUri, stringifySip003Options } from '../ss' 5 | 6 | test('parseSSUri', (t) => { 7 | t.deepEqual( 8 | parseSSUri( 9 | 'ss://Y2hhY2hhMjAtaWV0ZjpwYXNzd29yZA==@example.com:12345/?plugin=simple-obfs%3Bobfs%3Dhttp%3Bobfs-host%3Dexample.com#%E6%B5%8B%E8%AF%95%E8%8A%82%E7%82%B9', 10 | ), 11 | { 12 | hostname: 'example.com', 13 | method: 'chacha20-ietf', 14 | nodeName: '测试节点', 15 | password: 'password', 16 | port: '12345', 17 | type: NodeTypeEnum.Shadowsocks, 18 | obfs: 'http', 19 | obfsHost: 'example.com', 20 | }, 21 | ) 22 | }) 23 | 24 | test('stringifySip003Options', (t) => { 25 | t.is( 26 | stringifySip003Options({ 27 | a: 123, 28 | host: 'https://a.com/foo?bar=baz&q\\q=1&w;w=2', 29 | mode: 'quic', 30 | tls: true, 31 | }), 32 | 'a=123;host=https://a.com/foo?bar\\=baz&q\\\\q\\=1&w\\;w\\=2;mode=quic;tls=true', 33 | ) 34 | t.is(stringifySip003Options({}), '') 35 | t.is(stringifySip003Options(), '') 36 | }) 37 | -------------------------------------------------------------------------------- /src/utils/__tests__/subscription.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { 4 | formatSubscriptionUserInfo, 5 | parseSubscriptionNode, 6 | parseSubscriptionUserInfo, 7 | } from '../subscription' 8 | 9 | test('parseSubscriptionNode', (t) => { 10 | const result = parseSubscriptionNode( 11 | '剩余流量:57.37% 1.01TB', 12 | '过期时间:2020-04-21 22:27:38', 13 | ) 14 | if (!result) throw new Error() 15 | const reformat = formatSubscriptionUserInfo(result) 16 | 17 | t.is(result.upload, 0) 18 | t.is(result.download, 825185680652) 19 | t.is(result.total, 1935692424705) 20 | t.truthy(reformat.expire.includes('2020-04-21')) 21 | }) 22 | 23 | test('formatSubscriptionUserInfo', (t) => { 24 | t.deepEqual( 25 | parseSubscriptionUserInfo( 26 | 'upload=0; download=42211676245; total=216256217222; expire=1584563470;', 27 | ), 28 | { 29 | upload: 0, 30 | download: 42211676245, 31 | total: 216256217222, 32 | expire: 1584563470, 33 | }, 34 | ) 35 | 36 | t.deepEqual( 37 | parseSubscriptionUserInfo( 38 | 'upload=0; download=42211676245; total=216256217222; expire=1584563470', 39 | ), 40 | { 41 | upload: 0, 42 | download: 42211676245, 43 | total: 216256217222, 44 | expire: 1584563470, 45 | }, 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /src/utils/__tests__/tmp-helper.file.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import os from 'os' 3 | import test from 'ava' 4 | import fs from 'fs-extra' 5 | import Bluebird from 'bluebird' 6 | 7 | import { TMP_FOLDER_NAME } from '../../constant' 8 | import { createTmpFactory, TmpFile } from '../tmp-helper' 9 | 10 | test.afterEach.always(async () => { 11 | const dir = path.join( 12 | os.tmpdir(), 13 | TMP_FOLDER_NAME, 14 | 'tmp-helper-test-folder' + `_nodejs_${process.version}`, 15 | ) 16 | if (fs.existsSync(dir)) { 17 | await fs.remove(dir) 18 | } 19 | }) 20 | 21 | test.serial('no permission', (t) => { 22 | t.throws(() => { 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | const file = new TmpFile('/System') 25 | }) 26 | }) 27 | 28 | test.serial('should work', async (t) => { 29 | const factory = createTmpFactory( 30 | 'tmp-helper-test-folder' + `_nodejs_${process.version}`, 31 | ) 32 | const tmp = factory('tmp1.txt') 33 | 34 | t.is(await tmp.getContent(), void 0) 35 | await tmp.setContent('123456abcdef') 36 | t.is(await tmp.getContent(), '123456abcdef') 37 | t.is(await tmp.getContent(), '123456abcdef') 38 | }) 39 | 40 | test.serial('should work with maxAge 1', async (t) => { 41 | const factory = createTmpFactory( 42 | 'tmp-helper-test-folder' + `_nodejs_${process.version}`, 43 | ) 44 | const tmp = factory('tmp2.txt', 1000) 45 | 46 | t.is(await tmp.getContent(), void 0) 47 | await tmp.setContent('123456abcdef') 48 | await Bluebird.delay(100) 49 | t.is(await tmp.getContent(), '123456abcdef') 50 | await Bluebird.delay(1000) 51 | t.is(await tmp.getContent(), void 0) 52 | await tmp.setContent('123456abcdefg') 53 | await Bluebird.delay(100) 54 | t.is(await tmp.getContent(), '123456abcdefg') 55 | t.is(await tmp.getContent(), '123456abcdefg') 56 | }) 57 | 58 | test.serial('should work with maxAge 2', async (t) => { 59 | const factory = createTmpFactory( 60 | 'tmp-helper-test-folder' + `_nodejs_${process.version}`, 61 | ) 62 | const tmp = factory('tmp3.txt', 1000) 63 | 64 | t.is(await tmp.getContent(), void 0) 65 | await tmp.setContent('123456abcdef') 66 | await Bluebird.delay(100) 67 | t.is(await tmp.getContent(), '123456abcdef') 68 | }) 69 | -------------------------------------------------------------------------------- /src/utils/__tests__/tmp-helper.redis.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | import MockRedis from 'ioredis-mock' 4 | 5 | import redis from '../../redis' 6 | import { createTmpFactory } from '../tmp-helper' 7 | 8 | const sandbox = sinon.createSandbox() 9 | 10 | test.before(() => { 11 | redis.createRedis('', MockRedis) 12 | }) 13 | 14 | test.after(async () => { 15 | sandbox.restore() 16 | await redis.destroyRedis() 17 | }) 18 | 19 | test('should work', async (t) => { 20 | const factory = createTmpFactory('tmp-helper-test', 'redis') 21 | 22 | const tmp = factory('tmp1.txt') 23 | 24 | t.is(await tmp.getContent(), void 0) 25 | await tmp.setContent('123456abcdef') 26 | t.is(await tmp.getContent(), '123456abcdef') 27 | t.is(await tmp.getContent(), '123456abcdef') 28 | }) 29 | -------------------------------------------------------------------------------- /src/utils/__tests__/trojan.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { NodeTypeEnum } from '../../types' 4 | import { parseTrojanUri } from '../trojan' 5 | 6 | test('parseTrojanUri', (t) => { 7 | t.deepEqual( 8 | parseTrojanUri( 9 | 'trojan://password@example.com:443?allowInsecure=1&peer=sni.example.com#Example%20%E8%8A%82%E7%82%B9', 10 | ), 11 | { 12 | hostname: 'example.com', 13 | nodeName: 'Example 节点', 14 | password: 'password', 15 | port: '443', 16 | skipCertVerify: true, 17 | sni: 'sni.example.com', 18 | type: NodeTypeEnum.Trojan, 19 | }, 20 | ) 21 | 22 | t.deepEqual( 23 | parseTrojanUri( 24 | 'trojan://password@example.com:443#Example%20%E8%8A%82%E7%82%B9', 25 | ), 26 | { 27 | hostname: 'example.com', 28 | nodeName: 'Example 节点', 29 | password: 'password', 30 | port: '443', 31 | type: NodeTypeEnum.Trojan, 32 | }, 33 | ) 34 | 35 | t.deepEqual( 36 | parseTrojanUri( 37 | 'trojan://password@example.com:443?allowInsecure=true&peer=sni.example.com', 38 | ), 39 | { 40 | hostname: 'example.com', 41 | nodeName: 'example.com:443', 42 | password: 'password', 43 | port: '443', 44 | skipCertVerify: true, 45 | sni: 'sni.example.com', 46 | type: NodeTypeEnum.Trojan, 47 | }, 48 | ) 49 | 50 | t.throws( 51 | () => { 52 | parseTrojanUri('ss://') 53 | }, 54 | undefined, 55 | 'Invalid Trojan URI.', 56 | ) 57 | }) 58 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | // 兼容 Gateway,后续删除 2 | export * from '../constant' 3 | -------------------------------------------------------------------------------- /src/utils/dns.ts: -------------------------------------------------------------------------------- 1 | import { promises as dns, RecordWithTtl } from 'dns' 2 | import { createLogger } from '@surgio/logger' 3 | import Bluebird from 'bluebird' 4 | import { caching } from 'cache-manager' 5 | import ms from 'ms' 6 | 7 | import { getNetworkResolveTimeout } from './env-flag' 8 | 9 | const domainCache = caching('memory', { 10 | ttl: ms('1d'), 11 | max: 5000, 12 | }) 13 | const logger = createLogger({ service: 'surgio:utils:dns' }) 14 | 15 | export const resolveDomain = async ( 16 | domain: string, 17 | timeout: number = getNetworkResolveTimeout(), 18 | ): Promise> => { 19 | const cached = await (await domainCache).get(domain) 20 | 21 | if (cached) { 22 | return cached 23 | } 24 | 25 | logger.debug(`try to resolve domain ${domain}`) 26 | const now = Date.now() 27 | const records = await Bluebird.race>([ 28 | resolve4And6(domain), 29 | Bluebird.delay(timeout).then(() => []), 30 | ]) 31 | logger.debug( 32 | `resolved domain ${domain}: ${JSON.stringify(records)} ${ 33 | Date.now() - now 34 | }ms`, 35 | ) 36 | 37 | if (records.length) { 38 | const address = records.map((item) => item.address) 39 | await (await domainCache).set(domain, address, records[0].ttl) // ttl is in seconds 40 | return address 41 | } 42 | 43 | // istanbul ignore next 44 | return [] 45 | } 46 | 47 | export const resolve4And6 = async ( 48 | domain: string, 49 | ): Promise> => { 50 | // istanbul ignore next 51 | function onErr(): ReadonlyArray { 52 | return [] 53 | } 54 | 55 | const [ipv4, ipv6] = await Promise.all([ 56 | dns.resolve4(domain, { ttl: true }).catch(onErr), 57 | dns.resolve6(domain, { ttl: true }).catch(onErr), 58 | ]) 59 | 60 | return [...ipv4, ...ipv6] 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/doctor.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { promisify } from 'util' 3 | import check from 'check-node-version' 4 | import { readJSON } from 'fs-extra' 5 | import { PackageJson } from 'type-fest' 6 | 7 | type OnComplete = Parameters[1] 8 | type CheckInfo = Parameters[1] 9 | 10 | export const generateDoctorInfo = async ( 11 | cwd: string, 12 | pjson: PackageJson, 13 | ): Promise> => { 14 | const doctorInfo: string[] = [] 15 | const checkInfo = await promisify(check)().catch(() => null) 16 | 17 | try { 18 | const gatewayPkg: PackageJson = await readJSON( 19 | join(cwd, 'node_modules/@surgio/gateway/package.json'), 20 | ) 21 | doctorInfo.push(`@surgio/gateway: ${gatewayPkg.version}`) 22 | } catch (_) { 23 | // no catch 24 | } 25 | 26 | doctorInfo.push(`surgio: ${pjson.version} (${join(__dirname, '../..')})`) 27 | 28 | if (checkInfo) { 29 | Object.keys(checkInfo.versions).forEach((key) => { 30 | const version = checkInfo.versions[key].version 31 | if (version) { 32 | if (key === 'node') { 33 | doctorInfo.push(`${key}: ${version} (${process.execPath})`) 34 | } else { 35 | doctorInfo.push(`${key}: ${version}`) 36 | } 37 | } 38 | }) 39 | } 40 | 41 | return doctorInfo 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/env-flag.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms' 2 | 3 | import { 4 | ENV_NETWORK_RESOLVE_TIMEOUT, 5 | ENV_NETWORK_TIMEOUT_KEY, 6 | ENV_SURGIO_GFW_FREE, 7 | ENV_SURGIO_NETWORK_CLASH_UA, 8 | ENV_SURGIO_NETWORK_CONCURRENCY, 9 | ENV_SURGIO_NETWORK_RETRY, 10 | ENV_SURGIO_PROVIDER_CACHE_MAXAGE, 11 | ENV_SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE, 12 | SURGIO_RENDERED_ARTIFACT_CACHE_MAXAGE, 13 | } from '../constant' 14 | 15 | export const getNetworkTimeout = (): number => 16 | process.env[ENV_NETWORK_TIMEOUT_KEY] 17 | ? Number(process.env[ENV_NETWORK_TIMEOUT_KEY]) 18 | : ms('5s') 19 | 20 | export const getNetworkResolveTimeout = (): number => 21 | process.env[ENV_NETWORK_RESOLVE_TIMEOUT] 22 | ? Number(process.env[ENV_NETWORK_RESOLVE_TIMEOUT]) 23 | : ms('10s') 24 | 25 | export const getNetworkConcurrency = (): number => 26 | process.env[ENV_SURGIO_NETWORK_CONCURRENCY] 27 | ? Number(process.env[ENV_SURGIO_NETWORK_CONCURRENCY]) 28 | : 5 29 | 30 | export const getNetworkRetry = (): number => 31 | process.env[ENV_SURGIO_NETWORK_RETRY] 32 | ? Number(process.env[ENV_SURGIO_NETWORK_RETRY]) 33 | : 0 34 | 35 | export const getNetworkClashUA = (): string => 36 | process.env[ENV_SURGIO_NETWORK_CLASH_UA] ?? 'clash' 37 | 38 | export const getRemoteSnippetCacheMaxage = (): number => 39 | process.env[ENV_SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE] 40 | ? Number(process.env[ENV_SURGIO_REMOTE_SNIPPET_CACHE_MAXAGE]) 41 | : ms('12h') 42 | 43 | export const getProviderCacheMaxage = (): number => 44 | process.env[ENV_SURGIO_PROVIDER_CACHE_MAXAGE] 45 | ? Number(process.env[ENV_SURGIO_PROVIDER_CACHE_MAXAGE]) 46 | : ms('10m') 47 | 48 | export const getIsGFWFree = (): boolean => 49 | typeof process.env[ENV_SURGIO_GFW_FREE] !== 'undefined' 50 | 51 | export const getRenderedArtifactCacheMaxage = (): number => 52 | process.env[SURGIO_RENDERED_ARTIFACT_CACHE_MAXAGE] 53 | ? Number(process.env[SURGIO_RENDERED_ARTIFACT_CACHE_MAXAGE]) 54 | : ms('7d') 55 | -------------------------------------------------------------------------------- /src/utils/error-helper.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { fromZodError } from 'zod-validation-error' 3 | 4 | import BaseCommand from '../base-command' 5 | 6 | import { generateDoctorInfo } from './doctor' 7 | import { isError, isSurgioError, isZodError } from './errors' 8 | 9 | export const errorHandler = async function ( 10 | this: BaseCommand, 11 | err: Error, 12 | ): Promise { 13 | const doctorInfo = await generateDoctorInfo( 14 | this.projectDir, 15 | this.config.pjson, 16 | ) 17 | 18 | console.error() 19 | console.error(chalk.bgRed(' 发生错误 ')) 20 | 21 | if (isSurgioError(err)) { 22 | console.error(chalk.red(err.message)) 23 | 24 | if (err.providerName) { 25 | console.error(chalk.red(`Provider 名称: ${err.providerName}`)) 26 | } 27 | if (err.providerPath) { 28 | console.error(chalk.red(`文件地址: ${err.providerPath}`)) 29 | } 30 | if (typeof err.nodeIndex === 'number') { 31 | console.error(chalk.red(`错误发生在第 ${err.nodeIndex + 1} 个节点`)) 32 | } 33 | if (isZodError(err.cause)) { 34 | console.error( 35 | chalk.red( 36 | fromZodError(err.cause, { 37 | prefix: '原因', 38 | }).message, 39 | ), 40 | ) 41 | } else if (isError(err.cause)) { 42 | console.error() 43 | console.error(chalk.bgRed(' 原因 ')) 44 | console.error(chalk.red(err.cause.stack || err.cause)) 45 | } else { 46 | console.error() 47 | console.error(chalk.bgRed(' 错误堆栈 ')) 48 | console.error(chalk.yellow(err.stack)) 49 | } 50 | } else { 51 | console.error(chalk.red(err.message)) 52 | console.error() 53 | 54 | if (err.stack) { 55 | console.error(chalk.bgRed(' 错误堆栈 ')) 56 | console.error(chalk.yellow(err.stack)) 57 | } 58 | } 59 | 60 | console.error() 61 | console.error(chalk.bgRed(' 诊断信息 ')) 62 | console.error('版本号:', require('../../package.json').version) 63 | console.error('常见问题:', chalk.cyan('https://url.royli.dev/7EMxu')) 64 | console.error('加入交流群汇报问题 ', chalk.cyan('https://t.me/surgiotg')) 65 | console.error() 66 | doctorInfo.forEach((item) => { 67 | console.error(chalk.cyan(item)) 68 | }) 69 | console.error() 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { fromZodError } from 'zod-validation-error' 3 | 4 | type SurgioErrorOptions = { 5 | providerName?: string 6 | providerPath?: string 7 | nodeIndex?: number 8 | cause?: unknown 9 | } 10 | 11 | class SurgioError extends Error { 12 | public providerName?: string 13 | public providerPath?: string 14 | public nodeIndex?: number 15 | public cause?: unknown 16 | 17 | constructor(message: string, options: SurgioErrorOptions = {}) { 18 | super(message) 19 | 20 | this.name = 'SurgioError' 21 | this.providerName = options.providerName 22 | this.providerPath = options.providerPath 23 | this.nodeIndex = options.nodeIndex 24 | this.cause = options.cause 25 | } 26 | 27 | format(): string { 28 | const message: string[] = [] 29 | 30 | message.push(this.name + ': ' + this.message) 31 | if (this.providerName) { 32 | message.push(`Provider 名称: ${this.providerName}`) 33 | } 34 | if (this.providerPath) { 35 | message.push(`文件地址: ${this.providerPath}`) 36 | } 37 | if (typeof this.nodeIndex === 'number') { 38 | message.push(`错误发生在第 ${this.nodeIndex + 1} 个节点`) 39 | } 40 | if (isZodError(this.cause)) { 41 | message.push( 42 | fromZodError(this.cause, { 43 | prefix: '原因', 44 | }).message, 45 | ) 46 | } else if (isError(this.cause) && this.cause.stack) { 47 | message.push(' ') 48 | message.push(this.cause.stack) 49 | } else if (this.stack) { 50 | message.push(' ') 51 | message.push(this.stack) 52 | } 53 | 54 | return message.join('\n') 55 | } 56 | } 57 | 58 | export const isSurgioError = (val: unknown): val is SurgioError => { 59 | return val instanceof SurgioError 60 | } 61 | 62 | export const isZodError = (error: unknown): error is z.ZodError => { 63 | return error instanceof z.ZodError 64 | } 65 | 66 | export const isError = (val: unknown): val is Error => { 67 | return val instanceof Error 68 | } 69 | 70 | export { SurgioError } 71 | -------------------------------------------------------------------------------- /src/utils/flag.ts: -------------------------------------------------------------------------------- 1 | import EmojiRegex from 'emoji-regex' 2 | import _ from 'lodash' 3 | 4 | import { FLAGS } from '../misc/flag_cn' 5 | 6 | const flagMap: Map = new Map() 7 | const customFlagMap: Map = new Map() 8 | 9 | for (const [key, value] of Object.entries(FLAGS)) { 10 | value.forEach((name: string) => { 11 | flagMap.set(name, key) 12 | }) 13 | } 14 | 15 | export const addFlagMap = (name: string | RegExp, emoji: string): void => { 16 | if (flagMap.has(name)) { 17 | flagMap.delete(name) 18 | } 19 | customFlagMap.set(name, emoji) 20 | } 21 | 22 | export const prependFlag = ( 23 | str: string, 24 | removeExistingEmoji = false, 25 | ): string => { 26 | const emojiRegex = EmojiRegex() 27 | const existingEmoji = emojiRegex.exec(str) 28 | 29 | if (existingEmoji) { 30 | if (removeExistingEmoji) { 31 | // 去除已有的 emoji 32 | str = removeFlag(str) 33 | } else { 34 | // 不作处理 35 | return str 36 | } 37 | } 38 | 39 | for (const [key, value] of customFlagMap.entries()) { 40 | if (_.isRegExp(key)) { 41 | if (key.test(str)) { 42 | return `${value} ${str}` 43 | } 44 | } else { 45 | const isKeyChineseCharacters = /[\u4E00-\u9FA5]/.test(key) 46 | const regex = new RegExp(`(^|\\b)${key}(\\b|$)`, 'i') 47 | 48 | if (isKeyChineseCharacters && str.toUpperCase().includes(key)) { 49 | return `${value} ${str}` 50 | } else if (!isKeyChineseCharacters && regex.test(str)) { 51 | return `${value} ${str}` 52 | } 53 | } 54 | } 55 | 56 | for (const [key, value] of flagMap.entries()) { 57 | if (_.isRegExp(key)) { 58 | if (key.test(str)) { 59 | return `${value} ${str}` 60 | } 61 | } else { 62 | const isKeyChineseCharacters = /[\u4E00-\u9FA5]/.test(key) 63 | const regex = new RegExp(`(^|\\b)${key}(\\b|$)`, 'i') 64 | 65 | if (isKeyChineseCharacters && str.toUpperCase().includes(key)) { 66 | return `${value} ${str}` 67 | } else if (!isKeyChineseCharacters && regex.test(str)) { 68 | return `${value} ${str}` 69 | } 70 | } 71 | } 72 | 73 | return str 74 | } 75 | 76 | export const removeFlag = (str: string): string => { 77 | const emojiRegex = EmojiRegex() 78 | return str.replace(emojiRegex, '').trim() 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/http-client.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import HttpAgent, { HttpsAgent } from 'agentkeepalive' 3 | import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' 4 | 5 | import { NETWORK_SURGIO_UA } from '../constant' 6 | 7 | import { getNetworkRetry, getNetworkTimeout } from './env-flag' 8 | 9 | const httpProxy = hasHTTPProxy() 10 | const httpsProxy = hasHTTPSProxy() 11 | const pkg = require('../../package.json') 12 | const agent = 13 | !!httpProxy || !!httpsProxy 14 | ? { 15 | http: httpProxy 16 | ? new HttpProxyAgent({ 17 | keepAlive: true, 18 | keepAliveMsecs: 1000, 19 | maxSockets: 256, 20 | maxFreeSockets: 256, 21 | scheduling: 'lifo', 22 | proxy: httpProxy, 23 | }) 24 | : new HttpAgent(), 25 | https: httpsProxy 26 | ? new HttpsProxyAgent({ 27 | keepAlive: true, 28 | keepAliveMsecs: 1000, 29 | maxSockets: 256, 30 | maxFreeSockets: 256, 31 | scheduling: 'lifo', 32 | proxy: httpsProxy, 33 | }) 34 | : new HttpsAgent(), 35 | } 36 | : { 37 | http: new HttpAgent(), 38 | https: new HttpsAgent(), 39 | } 40 | 41 | export const getUserAgent = (str?: string): string => 42 | `${str ? str + ' ' : ''}${NETWORK_SURGIO_UA}/${pkg.version}` 43 | 44 | const httpClient = got.extend({ 45 | timeout: getNetworkTimeout(), 46 | retry: getNetworkRetry(), 47 | headers: { 48 | 'user-agent': getUserAgent(), 49 | }, 50 | agent, 51 | }) 52 | 53 | function hasHTTPProxy(): string | undefined { 54 | return process.env.HTTP_PROXY || process.env.http_proxy 55 | } 56 | 57 | function hasHTTPSProxy(): string | undefined { 58 | return process.env.HTTPS_PROXY || process.env.https_proxy 59 | } 60 | 61 | export default httpClient 62 | -------------------------------------------------------------------------------- /src/utils/linter.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | 3 | import { ESLint } from 'eslint' 4 | import _ from 'lodash' 5 | 6 | export const createCli = (cliConfig?: ESLint.Options): ESLint => { 7 | const linterConfig = { 8 | // 在测试情况下 fixture 目录不包含 eslintrc,避免 eslint 读取根目录的 eslintrc 9 | useEslintrc: process.env.NODE_ENV !== 'test', 10 | extensions: ['.js'], 11 | baseConfig: { 12 | extends: ['@surgio/eslint-config-surgio'].map( 13 | // @ts-ignore 14 | require.resolve, 15 | ), 16 | }, 17 | } 18 | 19 | return new ESLint({ 20 | ...linterConfig, 21 | ...cliConfig, 22 | }) 23 | } 24 | 25 | export const checkAndFix = async (cwd: string): Promise => { 26 | const cli = createCli({ fix: true, cwd }) 27 | const results = await cli.lintFiles(['.']) 28 | const errorCount = _.sumBy(results, (curr) => curr.errorCount) 29 | const fixableErrorCount = _.sumBy(results, (curr) => curr.fixableErrorCount) 30 | 31 | await ESLint.outputFixes(results) 32 | 33 | const formatter = await cli.loadFormatter('stylish') 34 | const resultText = formatter.format(results) 35 | 36 | console.log(resultText) 37 | 38 | return errorCount - fixableErrorCount === 0 39 | } 40 | 41 | export const check = async (cwd: string): Promise => { 42 | const cli = createCli({ cwd }) 43 | const results = await cli.lintFiles(['.']) 44 | const errorCount = _.sumBy(results, (curr) => curr.errorCount) 45 | const formatter = await cli.loadFormatter('stylish') 46 | const resultText = formatter.format(results) 47 | 48 | console.log(resultText) 49 | 50 | return errorCount === 0 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/relayable-url.ts: -------------------------------------------------------------------------------- 1 | export default function relayableUrl(url: string, relayUrl?: string): string { 2 | if (typeof relayUrl === 'string') { 3 | if (relayUrl.includes('%%URL%%')) { 4 | return relayUrl.replace('%%URL%%', encodeURIComponent(url)) 5 | } else if (relayUrl.includes('%URL%')) { 6 | return relayUrl.replace('%URL%', url) 7 | } else { 8 | throw new Error('relayUrl 中必须包含 %URL% 或 %%URL%% 替换指示符') 9 | } 10 | } 11 | return url 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/ss.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { createLogger } from '@surgio/logger' 3 | 4 | import { NodeTypeEnum, ShadowsocksNodeConfig } from '../types' 5 | 6 | import { decodeStringList, fromUrlSafeBase64 } from './index' 7 | 8 | const logger = createLogger({ service: 'surgio:utils:ss' }) 9 | 10 | export const parseSSUri = (str: string): ShadowsocksNodeConfig => { 11 | logger.debug('Shadowsocks URI', str) 12 | 13 | const scheme = new URL(str) 14 | const pluginString = scheme.searchParams.get('plugin') 15 | const userInfo = fromUrlSafeBase64(decodeURIComponent(scheme.username)).split( 16 | ':', 17 | ) 18 | const pluginInfo = 19 | typeof pluginString === 'string' 20 | ? decodeStringList(pluginString.split(';')) 21 | : {} 22 | 23 | return { 24 | type: NodeTypeEnum.Shadowsocks, 25 | nodeName: decodeURIComponent(scheme.hash.replace('#', '')), 26 | hostname: scheme.hostname, 27 | port: scheme.port, 28 | method: userInfo[0], 29 | password: userInfo[1], 30 | ...(pluginInfo['obfs-local'] 31 | ? { 32 | obfs: pluginInfo.obfs as 'http' | 'tls', 33 | obfsHost: pluginInfo['obfs-host'] + '', 34 | } 35 | : null), 36 | ...(pluginInfo['simple-obfs'] 37 | ? { 38 | obfs: pluginInfo.obfs as 'http' | 'tls', 39 | obfsHost: pluginInfo['obfs-host'] + '', 40 | } 41 | : null), 42 | ...(pluginInfo['v2ray-plugin'] 43 | ? { 44 | obfs: pluginInfo.tls ? 'wss' : 'ws', 45 | obfsHost: pluginInfo.host + '', 46 | } 47 | : null), 48 | } 49 | } 50 | 51 | // Marshal SIP003 plugin options in PossibleNodeConfigType to formatted string. 52 | // An example is 'a=123;host=https://a.com/foo?bar\=baz&q\\q\=1&w\;w\=2;mode=quic;tls=true', 53 | // where semicolons, equal signs and backslashes MUST be escaped with a backslash. 54 | export const stringifySip003Options = (args?: Record): string => { 55 | if (!args) { 56 | return '' 57 | } 58 | 59 | const keys = Object.keys(args).sort() 60 | const pairs: string[] = [] 61 | for (const key of keys) { 62 | pairs.push( 63 | `${key.replace(/([;=\\])/g, '\\$1')}=${args[key] 64 | .toString() 65 | .replace(/([;=\\])/g, '\\$1')}`, 66 | ) 67 | } 68 | return pairs.join(';') 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/ssr.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@surgio/logger' 2 | 3 | import { NodeTypeEnum, ShadowsocksrNodeConfig } from '../types' 4 | 5 | import { fromUrlSafeBase64 } from './index' 6 | 7 | const logger = createLogger({ service: 'surgio:utils:ssr' }) 8 | 9 | /** 10 | * 协议:https://github.com/shadowsocksr-backup/shadowsocks-rss/wiki/SSR-QRcode-scheme 11 | * ssr://xxx:xxx:xxx:xxx:xxx:xxx/?a=1&b=2 12 | * ssr://xxx:xxx:xxx:xxx:xxx:xxx 13 | */ 14 | export const parseSSRUri = (str: string): ShadowsocksrNodeConfig => { 15 | const scheme = fromUrlSafeBase64(str.replace('ssr://', '')) 16 | const configArray = scheme.split('/') 17 | const basicInfo = configArray[0].split(':') 18 | 19 | logger.debug('SSR URI', scheme) 20 | 21 | // 去除首部分 22 | configArray.shift() 23 | 24 | const extraString = configArray.join('/') 25 | const extras = extraString ? getUrlParameters(extraString) : {} 26 | const password = fromUrlSafeBase64(basicInfo.pop() as string) 27 | const obfs = basicInfo.pop() as string 28 | const method = basicInfo.pop() as string 29 | const protocol = basicInfo.pop() as string 30 | const port = basicInfo.pop() as string 31 | const hostname = basicInfo.join(':') 32 | const nodeName = extras.remarks 33 | ? fromUrlSafeBase64(extras.remarks) 34 | : `${hostname}:${port}` 35 | 36 | return { 37 | type: NodeTypeEnum.Shadowsocksr, 38 | nodeName, 39 | hostname, 40 | port, 41 | protocol, 42 | method, 43 | obfs, 44 | password, 45 | protoparam: fromUrlSafeBase64(extras.protoparam ?? '').replace(/\s/g, ''), 46 | obfsparam: fromUrlSafeBase64(extras.obfsparam ?? '').replace(/\s/g, ''), 47 | } 48 | } 49 | 50 | function getUrlParameters(url: string): Record { 51 | const result: Record = {} 52 | url.replace(/[?&]+([^=&]+)=([^&#]*)/gi, (origin, k, v) => { 53 | result[k] = v 54 | return origin 55 | }) 56 | return result 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/subscription.ts: -------------------------------------------------------------------------------- 1 | import { filesize } from 'filesize' 2 | import bytes from 'bytes' 3 | import { format, formatDistanceToNow } from 'date-fns' 4 | 5 | import { SubscriptionUserinfo } from '../types' 6 | 7 | export const parseSubscriptionUserInfo = ( 8 | str: string, 9 | ): SubscriptionUserinfo => { 10 | const res = { 11 | upload: 0, 12 | download: 0, 13 | total: 0, 14 | expire: 0, 15 | } 16 | 17 | str.split(';').forEach((item) => { 18 | if (!item) { 19 | return 20 | } 21 | const pair = item.split('=') 22 | const key = pair[0].trim() 23 | const value = Number(pair[1].trim()) 24 | 25 | if (key in res && !Number.isNaN(value)) { 26 | res[key as keyof typeof res] = value 27 | } 28 | }) 29 | 30 | return res 31 | } 32 | 33 | export const parseSubscriptionNode = ( 34 | dataString: string, 35 | expireString: string, 36 | ): SubscriptionUserinfo | undefined => { 37 | // dataString => 剩余流量:57.37% 1.01TB 38 | // expireString => 过期时间:2020-04-21 22:27:38 39 | 40 | const dataMatch = dataString.match(/剩余流量:(\d{0,2}(\.\d{1,4})?)%\s(.*)$/) 41 | const expireMatch = expireString.match(/过期时间:(.*)$/) 42 | 43 | if (dataMatch && expireMatch) { 44 | const percent = Number(dataMatch[1]) / 100 45 | const leftData = bytes.parse(dataMatch[3]) 46 | const total = Number((leftData / percent).toFixed(0)) 47 | const expire = Math.floor(new Date(expireMatch[1]).getTime() / 1000) 48 | 49 | return { 50 | upload: 0, 51 | download: total - leftData, 52 | total, 53 | expire, 54 | } 55 | } else { 56 | return undefined 57 | } 58 | } 59 | 60 | export const formatSubscriptionUserInfo = ( 61 | userInfo: SubscriptionUserinfo, 62 | ): { 63 | readonly upload: string 64 | readonly download: string 65 | readonly used: string 66 | readonly left: string 67 | readonly total: string 68 | readonly expire: string 69 | } => { 70 | return { 71 | upload: filesize(userInfo.upload, { base: 2 }) as string, 72 | download: filesize(userInfo.download, { base: 2 }) as string, 73 | used: filesize(userInfo.upload + userInfo.download, { base: 2 }) as string, 74 | left: filesize(userInfo.total - userInfo.upload - userInfo.download, { 75 | base: 2, 76 | }) as string, 77 | total: filesize(userInfo.total, { base: 2 }) as string, 78 | expire: userInfo.expire 79 | ? `${format( 80 | new Date(userInfo.expire * 1000), 81 | 'yyyy-MM-dd', 82 | )} (${formatDistanceToNow(new Date(userInfo.expire * 1000))})` 83 | : '无数据', 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | export const msToSeconds = (ms: number): number => Math.floor(ms / 1000) 2 | -------------------------------------------------------------------------------- /src/utils/trojan.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { createLogger } from '@surgio/logger' 3 | 4 | import { NodeTypeEnum, TrojanNodeConfig } from '../types' 5 | 6 | const logger = createLogger({ service: 'surgio:utils:trojan' }) 7 | 8 | export const parseTrojanUri = (str: string): TrojanNodeConfig => { 9 | logger.debug('Trojan URI', str) 10 | 11 | const scheme = new URL(str) 12 | 13 | if (scheme.protocol !== 'trojan:') { 14 | throw new Error('Invalid Trojan URI.') 15 | } 16 | 17 | const allowInsecure = 18 | scheme.searchParams.get('allowInsecure') === '1' || 19 | scheme.searchParams.get('allowInsecure') === 'true' 20 | const sni = scheme.searchParams.get('sni') || scheme.searchParams.get('peer') 21 | 22 | return { 23 | type: NodeTypeEnum.Trojan, 24 | hostname: scheme.hostname, 25 | port: scheme.port, 26 | password: scheme.username, 27 | nodeName: scheme.hash 28 | ? decodeURIComponent(scheme.hash.slice(1)) 29 | : `${scheme.hostname}:${scheme.port}`, 30 | ...(allowInsecure 31 | ? { 32 | skipCertVerify: true, 33 | } 34 | : null), 35 | ...(sni 36 | ? { 37 | sni, 38 | } 39 | : null), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/validators/artifact.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const ArtifactValidator = z.object({ 4 | name: z.string(), 5 | template: z.string(), 6 | templateType: z 7 | .union([z.literal('default'), z.literal('json')]) 8 | .default('default'), 9 | extendTemplate: z 10 | .function() 11 | .args(z.unknown()) 12 | .returns(z.unknown()) 13 | .optional(), 14 | provider: z.string(), 15 | categories: z.array(z.string()).optional(), 16 | combineProviders: z.array(z.string()).optional(), 17 | customParams: z.record(z.any()).optional(), 18 | customFilters: z.record(z.function()).optional(), 19 | destDir: z.ostring(), 20 | downloadUrl: z.ostring(), 21 | templateString: z.ostring(), 22 | subscriptionUserInfoProvider: z.ostring(), 23 | }) 24 | -------------------------------------------------------------------------------- /src/validators/filter.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import type { PossibleNodeConfigType } from '../types' 4 | 5 | type NodeFilterType = (nodeConfig: PossibleNodeConfigType) => boolean 6 | 7 | export const NodeFilterTypeValidator = z.custom((val) => { 8 | return typeof val === 'function' 9 | }) 10 | 11 | type SortedNodeFilterType = { 12 | readonly filter: ( 13 | nodeList: ReadonlyArray, 14 | ) => ReadonlyArray 15 | readonly supportSort: true 16 | } 17 | 18 | export const SortedNodeFilterTypeValidator = z.custom( 19 | (val) => { 20 | return ( 21 | typeof val === 'object' && 22 | val !== null && 23 | 'filter' in val && 24 | typeof val.filter === 'function' && 25 | ('supportSort' in val ? typeof val.supportSort === 'boolean' : true) 26 | ) 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/validators/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Promisable } from 'type-fest' 2 | import { z } from 'zod' 3 | 4 | import { GetNodeListParams } from '../provider' 5 | import { PossibleNodeConfigType } from '../types' 6 | 7 | type AfterNodeListResponse = ( 8 | nodeList: T[], 9 | customParams: GetNodeListParams, 10 | ) => Promisable 11 | 12 | export const AfterNodeListResponseHookValidator = 13 | z.custom((val) => { 14 | return typeof val === 'function' 15 | }) 16 | 17 | type OnError = (error: Error) => Promisable 18 | 19 | export const OnErrorHookValidator = z.custom((val) => { 20 | return typeof val === 'function' 21 | }) 22 | -------------------------------------------------------------------------------- /src/validators/http.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { 6 | PortValidator, 7 | SimpleNodeConfigValidator, 8 | TlsNodeConfigValidator, 9 | } from './common' 10 | 11 | export const HttpNodeConfigValidator = SimpleNodeConfigValidator.extend({ 12 | type: z.literal(NodeTypeEnum.HTTP), 13 | hostname: z.string(), 14 | port: PortValidator, 15 | username: z.string(), 16 | password: z.string(), 17 | path: z.string().optional(), 18 | headers: z.record(z.string()).optional(), 19 | }) 20 | 21 | export const HttpsNodeConfigValidator = TlsNodeConfigValidator.extend({ 22 | type: z.literal(NodeTypeEnum.HTTPS), 23 | username: z.string(), 24 | password: z.string(), 25 | path: z.string().optional(), 26 | headers: z.record(z.string()).optional(), 27 | }) 28 | -------------------------------------------------------------------------------- /src/validators/hysteria2.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { TlsNodeConfigValidator } from './common' 6 | 7 | export const Hysteria2NodeConfigValidator = TlsNodeConfigValidator.extend({ 8 | type: z.literal(NodeTypeEnum.Hysteria2), 9 | password: z.string(), 10 | downloadBandwidth: z.number().optional(), 11 | uploadBandwidth: z.number().optional(), 12 | obfs: z.literal('salamander').optional(), 13 | obfsPassword: z.string().optional(), 14 | }) 15 | -------------------------------------------------------------------------------- /src/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wireguard' 2 | export * from './shadowsocks' 3 | export * from './shadowsocksr' 4 | export * from './snell' 5 | export * from './vmess' 6 | export * from './trojan' 7 | export * from './tuic' 8 | export * from './socks5' 9 | export * from './http' 10 | export * from './provider' 11 | export * from './common' 12 | export * from './surgio-config' 13 | export * from './artifact' 14 | export * from './filter' 15 | export * from './hooks' 16 | export * from './hysteria2' 17 | export * from './vless' 18 | -------------------------------------------------------------------------------- /src/validators/provider.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { SupportProviderEnum } from '../types' 4 | 5 | import { 6 | NodeFilterTypeValidator, 7 | SortedNodeFilterTypeValidator, 8 | } from './filter' 9 | import { 10 | AfterNodeListResponseHookValidator, 11 | OnErrorHookValidator, 12 | } from './hooks' 13 | 14 | export const ProviderValidator = z.object({ 15 | type: z.nativeEnum(SupportProviderEnum), 16 | addFlag: z.oboolean(), 17 | removeExistingFlag: z.oboolean(), 18 | mptcp: z.oboolean(), 19 | tfo: z.oboolean(), 20 | ecn: z.oboolean(), 21 | blockQuic: z 22 | .union([z.literal('auto'), z.literal('on'), z.literal('off')]) 23 | .optional(), 24 | underlyingProxy: z.ostring(), 25 | startPort: z.number().min(1024).max(65535).optional(), 26 | relayUrl: z.string().url().optional(), 27 | requestUserAgent: z.ostring(), 28 | renameNode: z 29 | .function() 30 | .args(z.string()) 31 | .returns(z.union([z.string(), z.undefined(), z.void()])) 32 | .optional(), 33 | customFilters: z 34 | .record(z.union([NodeFilterTypeValidator, SortedNodeFilterTypeValidator])) 35 | .optional(), 36 | nodeFilter: z 37 | .union([NodeFilterTypeValidator, SortedNodeFilterTypeValidator]) 38 | .optional(), 39 | netflixFilter: z 40 | .union([NodeFilterTypeValidator, SortedNodeFilterTypeValidator]) 41 | .optional(), 42 | youtubePremiumFilter: z 43 | .union([NodeFilterTypeValidator, SortedNodeFilterTypeValidator]) 44 | .optional(), 45 | hooks: z 46 | .object({ 47 | afterNodeListResponse: AfterNodeListResponseHookValidator.optional(), 48 | onError: OnErrorHookValidator.optional(), 49 | }) 50 | .optional(), 51 | }) 52 | -------------------------------------------------------------------------------- /src/validators/shadowsocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { 6 | MultiplexValidator, 7 | PortValidator, 8 | SimpleNodeConfigValidator, 9 | } from './common' 10 | 11 | export const ShadowsocksNodeConfigValidator = SimpleNodeConfigValidator.extend({ 12 | type: z.literal(NodeTypeEnum.Shadowsocks), 13 | hostname: z.string(), 14 | port: PortValidator, 15 | method: z.string(), 16 | password: z.string(), 17 | udpRelay: z.oboolean(), 18 | obfs: z 19 | .union([ 20 | z.literal('tls'), 21 | z.literal('http'), 22 | z.literal('ws'), 23 | z.literal('wss'), 24 | z.literal('quic'), 25 | ]) 26 | .optional(), 27 | obfsHost: z.ostring(), 28 | obfsUri: z.ostring(), 29 | skipCertVerify: z.oboolean(), 30 | wsHeaders: z.record(z.string()).optional(), 31 | tls13: z.oboolean(), 32 | mux: z.oboolean(), 33 | multiplex: MultiplexValidator.optional(), 34 | }) 35 | -------------------------------------------------------------------------------- /src/validators/shadowsocksr.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { PortValidator, SimpleNodeConfigValidator } from './common' 6 | 7 | export const ShadowsocksrNodeConfigValidator = SimpleNodeConfigValidator.extend( 8 | { 9 | type: z.literal(NodeTypeEnum.Shadowsocksr), 10 | hostname: z.string(), 11 | port: PortValidator, 12 | method: z.string(), 13 | password: z.string(), 14 | obfs: z.string(), 15 | obfsparam: z.string(), 16 | protocol: z.string(), 17 | protoparam: z.string(), 18 | udpRelay: z.oboolean(), 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /src/validators/snell.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { 6 | IntegersVersionValidator, 7 | PortValidator, 8 | SimpleNodeConfigValidator, 9 | } from './common' 10 | 11 | export const SnellNodeConfigValidator = SimpleNodeConfigValidator.extend({ 12 | type: z.literal(NodeTypeEnum.Snell), 13 | hostname: z.string(), 14 | port: PortValidator, 15 | psk: z.string(), 16 | obfs: z.union([z.literal('http'), z.literal('tls')]).optional(), 17 | obfsHost: z.ostring(), 18 | reuse: z.oboolean(), 19 | version: IntegersVersionValidator.optional(), 20 | }) 21 | -------------------------------------------------------------------------------- /src/validators/socks5.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { TlsNodeConfigValidator } from './common' 6 | 7 | export const Socks5NodeConfigValidator = TlsNodeConfigValidator.extend({ 8 | type: z.literal(NodeTypeEnum.Socks5), 9 | username: z.ostring(), 10 | password: z.ostring(), 11 | udpRelay: z.oboolean(), 12 | tls: z.oboolean(), 13 | clientCert: z.ostring(), 14 | }) 15 | -------------------------------------------------------------------------------- /src/validators/trojan.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { MultiplexValidator, TlsNodeConfigValidator } from './common' 6 | 7 | export const TrojanNodeConfigValidator = TlsNodeConfigValidator.extend({ 8 | type: z.literal(NodeTypeEnum.Trojan), 9 | password: z.string(), 10 | udpRelay: z.oboolean(), 11 | network: z.union([z.literal('tcp'), z.literal('ws')]).optional(), 12 | wsPath: z.ostring(), 13 | wsHeaders: z.record(z.string()).optional(), 14 | multiplex: MultiplexValidator.optional(), 15 | }) 16 | -------------------------------------------------------------------------------- /src/validators/tuic.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { IntegersVersionValidator, TlsNodeConfigValidator } from './common' 6 | 7 | export const TuicNodeV5ConfigValidator = TlsNodeConfigValidator.extend({ 8 | type: z.literal(NodeTypeEnum.Tuic), 9 | password: z.string(), 10 | uuid: z.string(), 11 | version: IntegersVersionValidator, 12 | }) 13 | 14 | export const TuicNodeV4ConfigValidator = TlsNodeConfigValidator.extend({ 15 | type: z.literal(NodeTypeEnum.Tuic), 16 | token: z.string(), 17 | }) 18 | 19 | export const TuicNodeConfigValidator = z.union([ 20 | TuicNodeV4ConfigValidator, 21 | TuicNodeV5ConfigValidator, 22 | ]) 23 | -------------------------------------------------------------------------------- /src/validators/vless.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { 6 | MultiplexValidator, 7 | PortValidator, 8 | TlsNodeConfigValidator, 9 | } from './common' 10 | import { 11 | VmessNetworkValidator, 12 | VmessH2OptsValidator, 13 | VmessGRPCOptsValidator, 14 | VmessHttpOptsValidator, 15 | VmessWSOptsValidator, 16 | VmessQuicOptsValidator, 17 | VmessHttpUpgradeOptsValidator, 18 | } from './vmess' 19 | 20 | export const VlessRealityOptsValidator = z.object({ 21 | publicKey: z.string(), 22 | shortId: z.ostring(), 23 | spiderX: z.ostring(), 24 | }) 25 | 26 | export const VlessNodeConfigValidator = TlsNodeConfigValidator.extend({ 27 | type: z.literal(NodeTypeEnum.Vless), 28 | hostname: z.string(), 29 | port: PortValidator, 30 | method: z.literal('none'), 31 | uuid: z.string().uuid(), 32 | network: VmessNetworkValidator.default('tcp'), 33 | udpRelay: z.oboolean(), 34 | flow: z.ostring(), 35 | 36 | wsOpts: VmessWSOptsValidator.optional(), 37 | h2Opts: VmessH2OptsValidator.optional(), 38 | httpOpts: VmessHttpOptsValidator.optional(), 39 | grpcOpts: VmessGRPCOptsValidator.optional(), 40 | quicOpts: VmessQuicOptsValidator.optional(), 41 | httpUpgradeOpts: VmessHttpUpgradeOptsValidator.optional(), 42 | realityOpts: VlessRealityOptsValidator.optional(), 43 | multiplex: MultiplexValidator.optional(), 44 | }) 45 | -------------------------------------------------------------------------------- /src/validators/vmess.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { 6 | PortValidator, 7 | SimpleNodeConfigValidator, 8 | AlterIdValiator, 9 | MultiplexValidator, 10 | } from './common' 11 | 12 | export const VmessNetworkValidator = z.union([ 13 | z.literal('tcp'), 14 | z.literal('ws'), 15 | z.literal('h2'), 16 | z.literal('http'), 17 | z.literal('grpc'), 18 | z.literal('quic'), 19 | z.literal('httpupgrade'), 20 | ]) 21 | 22 | export const VmessMethodValidator = z.union([ 23 | z.literal('none'), 24 | z.literal('aes-128-gcm'), 25 | z.literal('chacha20-poly1305'), 26 | z.literal('auto'), 27 | ]) 28 | 29 | export const VmessWSOptsValidator = z.object({ 30 | path: z.string(), 31 | headers: z.record(z.string()).optional(), 32 | }) 33 | 34 | export const VmessH2OptsValidator = z.object({ 35 | path: z.string(), 36 | host: z.array(z.string()).nonempty(), 37 | }) 38 | 39 | export const VmessHttpOptsValidator = z.object({ 40 | path: z.array(z.string()), 41 | headers: z.record(z.string()).optional(), 42 | method: z.ostring().default('GET'), 43 | }) 44 | 45 | export const VmessGRPCOptsValidator = z.object({ 46 | serviceName: z.string(), 47 | }) 48 | 49 | export const VmessQuicOptsValidator = z.object({ 50 | // no field now 51 | }) 52 | 53 | export const VmessHttpUpgradeOptsValidator = z.object({ 54 | path: z.string(), 55 | host: z.string().optional(), 56 | headers: z.record(z.string()).optional(), 57 | }) 58 | 59 | /** 60 | * @see https://stash.wiki/proxy-protocols/proxy-types#vmess 61 | * @see https://wiki.metacubex.one/config/proxies/vmess/ 62 | */ 63 | export const VmessNodeConfigValidator = SimpleNodeConfigValidator.extend({ 64 | type: z.literal(NodeTypeEnum.Vmess), 65 | hostname: z.string(), 66 | port: PortValidator, 67 | method: VmessMethodValidator, 68 | uuid: z.string().uuid(), 69 | alterId: AlterIdValiator.optional(), 70 | network: VmessNetworkValidator.default('tcp'), 71 | udpRelay: z.oboolean(), 72 | 73 | wsOpts: VmessWSOptsValidator.optional(), 74 | h2Opts: VmessH2OptsValidator.optional(), 75 | httpOpts: VmessHttpOptsValidator.optional(), 76 | grpcOpts: VmessGRPCOptsValidator.optional(), 77 | quicOpts: VmessQuicOptsValidator.optional(), 78 | httpUpgradeOpts: VmessHttpUpgradeOptsValidator.optional(), 79 | 80 | tls: z.oboolean(), 81 | sni: z.ostring(), 82 | tls13: z.oboolean(), 83 | skipCertVerify: z.oboolean(), 84 | serverCertFingerprintSha256: z.ostring(), 85 | alpn: z.array(z.string()).nonempty().optional(), 86 | clientFingerprint: z.ostring(), 87 | multiplex: MultiplexValidator.optional(), 88 | 89 | /** 90 | * @deprecated 91 | */ 92 | host: z.ostring(), 93 | /** 94 | * @deprecated 95 | */ 96 | path: z.ostring(), 97 | /** 98 | * @deprecated 99 | */ 100 | wsHeaders: z.record(z.string()).optional(), 101 | }) 102 | -------------------------------------------------------------------------------- /src/validators/wireguard.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { NodeTypeEnum } from '../types' 4 | 5 | import { SimpleNodeConfigValidator } from './common' 6 | 7 | const WireguardPeerConfigValidator = z.object({ 8 | publicKey: z.string(), 9 | endpoint: z.string().includes(':'), 10 | allowedIps: z.string().optional(), 11 | keepalive: z.number().optional(), 12 | presharedKey: z.string().optional(), 13 | reservedBits: z.array(z.number()).optional(), 14 | }) 15 | 16 | export const WireguardNodeConfigValidator = SimpleNodeConfigValidator.extend({ 17 | type: z.literal(NodeTypeEnum.Wireguard), 18 | selfIp: z.string().ip({ version: 'v4' }), 19 | selfIpV6: z.string().ip({ version: 'v6' }).optional(), 20 | preferIpv6: z.boolean().optional(), 21 | privateKey: z.string(), 22 | mtu: z.number().optional(), 23 | dnsServers: z.array(z.string().ip()).nonempty().optional(), 24 | peers: z.array(WireguardPeerConfigValidator).nonempty(), 25 | reservedBits: z.array(z.number()).optional(), 26 | }) 27 | -------------------------------------------------------------------------------- /test/asset/gui-config-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoCheckUpdate": true, 3 | "availabilityStatistics": false, 4 | "checkPreRelease": false, 5 | "configs": [ 6 | { 7 | "method": "chacha20-ietf-poly1305", 8 | "password": "password", 9 | "plugin": "obfs-local", 10 | "plugin_opts": "obfs=tls;obfs-host=gateway-carry.icloud.com", 11 | "remarks": "🇺🇸US 1", 12 | "server": "us.example.com", 13 | "server_port": 443, 14 | "timeout": 5 15 | }, 16 | { 17 | "method": "chacha20-ietf-poly1305", 18 | "password": "password", 19 | "remarks": "🇺🇸US 2", 20 | "server": "us.example.com", 21 | "server_port": 444, 22 | "timeout": 5 23 | }, 24 | { 25 | "method": "chacha20-ietf-poly1305", 26 | "password": "password", 27 | "plugin": "obfs-local", 28 | "plugin_opts": "obfs=tls", 29 | "remarks": "🇺🇸US 3", 30 | "server": "us.example.com", 31 | "server_port": 445, 32 | "timeout": 5 33 | }, 34 | { 35 | "method": "chacha20-ietf-poly1305", 36 | "password": "password", 37 | "plugin": "obfs-local", 38 | "plugin_opts": "obfs=http", 39 | "remarks": "🇺🇸US 4", 40 | "server": "us.example.com", 41 | "server_port": 80, 42 | "timeout": 5 43 | } 44 | ], 45 | "enabled": true, 46 | "global": false, 47 | "hotkey": { 48 | "RegHotkeysAtStartup": false, 49 | "ServerMoveDown": "", 50 | "ServerMoveUp": "", 51 | "ShowLogs": "", 52 | "SwitchAllowLan": "", 53 | "SwitchSystemProxy": "", 54 | "SwitchSystemProxyMode": "" 55 | }, 56 | "index": 0, 57 | "isDefault": false, 58 | "isVerboseLogging": false, 59 | "localPort": 1080, 60 | "logViewer": { 61 | "BackgroundColor": "Black", 62 | "Font": "Consolas, 8pt", 63 | "TextColor": "White", 64 | "toolbarShown": false, 65 | "topMost": false, 66 | "wrapText": false 67 | }, 68 | "pacUrl": null, 69 | "portableMode": true, 70 | "proxy": { 71 | "proxyPort": 0, 72 | "proxyServer": "", 73 | "proxyTimeout": 3, 74 | "proxyType": 0, 75 | "useProxy": false 76 | }, 77 | "secureLocalPac": true, 78 | "shareOverLan": false, 79 | "strategy": null, 80 | "useOnlinePac": false 81 | } -------------------------------------------------------------------------------- /test/asset/netflix.list: -------------------------------------------------------------------------------- 1 | # Netflix 2 | USER-AGENT,Argo* 3 | DOMAIN-SUFFIX,fast.com 4 | DOMAIN-SUFFIX,netflix.com 5 | DOMAIN-SUFFIX,netflix.net 6 | DOMAIN-SUFFIX,nflxext.com 7 | DOMAIN-SUFFIX,nflximg.com 8 | DOMAIN-SUFFIX,nflximg.net 9 | DOMAIN-SUFFIX,nflxso.net 10 | DOMAIN-SUFFIX,nflxvideo.net -------------------------------------------------------------------------------- /test/asset/ssd-sample-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "airport": "Test Airport", 3 | "port": 555, 4 | "encryption": "aes-128-gcm", 5 | "password": "password", 6 | "traffic_used": 30, 7 | "traffic_total": 400, 8 | "expiry": "2099-10-06 15:28:07", 9 | "plugin": "simple-obfs", 10 | "plugin_options": "obfs=tls;obfs-host=www.taobao.com", 11 | "servers": [ 12 | { 13 | "id": 1, 14 | "server": "45.45.45.45" 15 | }, 16 | { 17 | "id": 2, 18 | "server": "55.55.55.55" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/asset/ssd-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "airport": "Test Airport", 3 | "port": 555, 4 | "encryption": "aes-128-gcm", 5 | "password": "password", 6 | "traffic_used": 30, 7 | "traffic_total": 400, 8 | "expiry": "2099-10-06 15:28:07", 9 | "servers": [ 10 | { 11 | "id": 1, 12 | "server": "45.45.45.45", 13 | "remarks": "US GIA", 14 | "ratio": 1, 15 | "port": 444, 16 | "encryption": "aes-128-gcm", 17 | "password": "password", 18 | "plugin": "simple-obfs", 19 | "plugin_options": "obfs=tls;obfs-host=www.taobao.com" 20 | }, 21 | { 22 | "id": 2, 23 | "server": "55.55.55.55", 24 | "remarks": "HK GIA", 25 | "ratio": 1, 26 | "port": 444, 27 | "encryption": "aes-128-gcm", 28 | "password": "password", 29 | "plugin": "simple-obfs", 30 | "plugin_options": "obfs=http;obfs-host=www.taobao.com" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/asset/surge-script-list.txt: -------------------------------------------------------------------------------- 1 | # RULE 1 2 | http-response ^https?://m?api\.weibo\.c(n|om)/2/(statuses/(unread|extend|positives/get|(friends|video)(/|_)timeline)|stories/(video_stream|home_list)|(groups|fangle)/timeline|profile/statuses|comments/build_comments|photo/recommend_list|service/picfeed|searchall|cardlist|page|\!/photos/pic_recommend_status) script-path=https://raw.githubusercontent.com/yichahucha/surge/master/wb_ad.js,requires-body=true 3 | http-response ^https?://(sdk|wb)app\.uve\.weibo\.com(/interface/sdk/sdkad.php|/wbapplua/wbpullad.lua) script-path=https://raw.githubusercontent.com/yichahucha/surge/master/wb_launch.js,requires-body=true 4 | 5 | # RULE 2 6 | http-response https://api.zhihu.com/moments\?(action|feed_type) requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20feed.js,script-update-interval=-1 7 | http-response https://api.zhihu.com/topstory/recommend requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20recommend.js,script-update-interval=-1 8 | http-response https://api.zhihu.com/.*/questions requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20answer.js,script-update-interval=-1 9 | http-response https://api.zhihu.com/market/header max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20market.js,script-update-interval=-1 10 | http-response https://api.zhihu.com/people/ requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20people.js,script-update-interval=-1 11 | http-request https://api.zhihu.com/.*/questions requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20answer.js,script-update-interval=-1 12 | http-request https://api.zhihu.com/market/header max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20market.js,script-update-interval=-1 13 | 14 | # RULE 3 15 | dns dnspod script-path=http://example.com/dnspod.js 16 | 17 | # RULE 4 18 | zhihu people = type=http-response,requires-body=1,max-size=0,pattern=https://api.zhihu.com/people/,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20people.js 19 | zhihu feed = type=http-response,max-size=0,pattern=https://api.zhihu.com/moments/recommend,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20feed.js 20 | zhihu recommend = type=http-request,requires-body=1,max-size=0,pattern=https://api.zhihu.com/topstory/recommend,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20recommend.js 21 | zhihu answer = type=http-request,max-size=0,pattern=https://api.zhihu.com/v4/questions,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20answer.js 22 | -------------------------------------------------------------------------------- /test/asset/surgio-snippet.tpl: -------------------------------------------------------------------------------- 1 | {% macro main(rule1, rule2) %} 2 | DOMAIN,example1.com,{{ rule1 }} 3 | DOMAIN,example2.com,{{ rule2 }} 4 | {% endmacro %} 5 | -------------------------------------------------------------------------------- /test/asset/telegram.list: -------------------------------------------------------------------------------- 1 | IP-CIDR,91.108.56.0/22,no-resolve 2 | IP-CIDR,91.108.4.0/22,no-resolve 3 | IP-CIDR,91.108.8.0/22,no-resolve 4 | IP-CIDR,109.239.140.0/24,no-resolve 5 | IP-CIDR,149.154.160.0/20,no-resolve 6 | IP-CIDR,149.154.164.0/22,no-resolve 7 | IP-CIDR,149.154.172.0/22,no-resolve 8 | IP-CIDR,91.108.12.0/22,no-resolve -------------------------------------------------------------------------------- /test/asset/test-ruleset-1.list: -------------------------------------------------------------------------------- 1 | # China Apps 2 | USER-AGENT,MicroMessenger Client 3 | USER-AGENT,WeChat* 4 | USER-AGENT,MApi* // Dianping 5 | IP-CIDR,149.154.164.0/22,no-resolve // Telegram -------------------------------------------------------------------------------- /test/asset/test-ss-sub.txt: -------------------------------------------------------------------------------- 1 | c3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRwd1lYTnpkMjl5WkFAdXMuZXhhbXBsZS5jb206NDQzLz9wbHVnaW49b2Jmcy1sb2NhbCUzQm9iZnMlM0R0bHMlM0JvYmZzLWhvc3QlM0RnYXRld2F5LWNhcnJ5LmljbG91ZC5jb20mZ3JvdXA9U3VyZ2lvI/Cfh7rwn4e4VVMgMQpzczovL1kyaGhZMmhoTWpBdGFXVjBaaTF3YjJ4NU1UTXdOVHB3WVhOemQyOXlaQUB1cy5leGFtcGxlLmNvbTo0NDMvP2dyb3VwPVN1cmdpbyPwn4e68J+HuFVTIDIKc3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRwd1lYTnpkMjl5WkFAdXMuZXhhbXBsZS5jb206NDQzLz9wbHVnaW49djJyYXktcGx1Z2luJTNCdGxzJTNCaG9zdCUzRGdhdGV3YXktY2FycnkuaWNsb3VkLmNvbSZncm91cD1TdXJnaW8j8J+HuvCfh7hVUyAzCnZtZXNzOi8vZXlKMklqb2lNaUlzSW5Ceklqb2k1cldMNksrVklERWlMQ0poWkdRaU9pSXhMakV1TVM0eElpd2ljRzl5ZENJNk9EQTRNQ3dpYVdRaU9pSXhNemcyWmpnMVpTMDJOVGRpTFRSa05tVXRPV1ExTmkwM09HSmhaR0kzTldVeFptUWlMQ0poYVdRaU9pSTJOQ0lzSW01bGRDSTZJbmR6SWl3aWRIbHdaU0k2SW01dmJtVWlMQ0pvYjNOMElqb2laWGhoYlhCc1pTNWpiMjBpTENKd1lYUm9Jam9pWEM4aUxDSnRkWGdpT25zaVpXNWhZbXhsWkNJNkltWmhiSE5sSW4xOQp2bWVzczovL2V5SjJJam9pTWlJc0luQnpJam9pNXJXTDZLK1ZJRElpTENKaFpHUWlPaUl4TGpFdU1TNHhJaXdpY0c5eWRDSTZPREE0TUN3aWFXUWlPaUl4TXpnMlpqZzFaUzAyTlRkaUxUUmtObVV0T1dRMU5pMDNPR0poWkdJM05XVXhabVFpTENKaGFXUWlPaUkyTkNJc0ltNWxkQ0k2SW5SamNDSXNJblI1Y0dVaU9pSnViMjVsSWl3aWFHOXpkQ0k2SW1WNFlXMXdiR1V1WTI5dElpd2ljR0YwYUNJNklsd3ZJaXdpYlhWNElqcDdJbVZ1WVdKc1pXUWlPaUptWVd4elpTSjlmUT09CnNzcjovL01USTNMakF1TUM0eE9qRXlNelE2WVhWMGFGOWhaWE14TWpoZmJXUTFPbUZsY3kweE1qZ3RZMlppT25Sc2N6RXVNbDkwYVdOclpYUmZZWFYwYURwWlYwWm9XVzFLYVM4X2IySm1jM0JoY21GdFBWbHVTbXhaVjNReldWUkZlRXh0TVhaYVVTWnlaVzFoY210elBUVnlWMHcyU3kxV05VeHBkRFZ3WVVnCg== 2 | -------------------------------------------------------------------------------- /test/asset/test-ssr-sub.txt: -------------------------------------------------------------------------------- 1 | c3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRwd1lYTnpkMjl5WkFAdXMuZXhhbXBsZS5jb206NDQzLz9wbHVnaW49b2Jmcy1sb2NhbCUzQm9iZnMlM0R0bHMlM0JvYmZzLWhvc3QlM0RnYXRld2F5LWNhcnJ5LmljbG91ZC5jb20mZ3JvdXA9U3VyZ2lvI/Cfh7rwn4e4VVMgMQpzczovL1kyaGhZMmhoTWpBdGFXVjBaaTF3YjJ4NU1UTXdOVHB3WVhOemQyOXlaQUB1cy5leGFtcGxlLmNvbTo0NDMvP2dyb3VwPVN1cmdpbyPwn4e68J+HuFVTIDIKdm1lc3M6Ly9leUoySWpvaU1pSXNJbkJ6SWpvaTVyV0w2SytWSURFaUxDSmhaR1FpT2lJeExqRXVNUzR4SWl3aWNHOXlkQ0k2T0RBNE1Dd2lhV1FpT2lJeE16ZzJaamcxWlMwMk5UZGlMVFJrTm1VdE9XUTFOaTAzT0dKaFpHSTNOV1V4Wm1RaUxDSmhhV1FpT2lJMk5DSXNJbTVsZENJNkluZHpJaXdpZEhsd1pTSTZJbTV2Ym1VaUxDSm9iM04wSWpvaVpYaGhiWEJzWlM1amIyMGlMQ0p3WVhSb0lqb2lYQzhpTENKdGRYZ2lPbnNpWlc1aFlteGxaQ0k2SW1aaGJITmxJbjE5CnZtZXNzOi8vZXlKMklqb2lNaUlzSW5Ceklqb2k1cldMNksrVklESWlMQ0poWkdRaU9pSXhMakV1TVM0eElpd2ljRzl5ZENJNk9EQTRNQ3dpYVdRaU9pSXhNemcyWmpnMVpTMDJOVGRpTFRSa05tVXRPV1ExTmkwM09HSmhaR0kzTldVeFptUWlMQ0poYVdRaU9pSTJOQ0lzSW01bGRDSTZJblJqY0NJc0luUjVjR1VpT2lKdWIyNWxJaXdpYUc5emRDSTZJbVY0WVcxd2JHVXVZMjl0SWl3aWNHRjBhQ0k2SWx3dklpd2liWFY0SWpwN0ltVnVZV0pzWldRaU9pSm1ZV3h6WlNKOWZRPT0Kc3NyOi8vTVRJM0xqQXVNQzR4T2pFeU16UTZZWFYwYUY5aFpYTXhNamhmYldRMU9tRmxjeTB4TWpndFkyWmlPblJzY3pFdU1sOTBhV05yWlhSZllYVjBhRHBaVjBab1dXMUthUzhfYjJKbWMzQmhjbUZ0UFZsdVNteFpWM1F6V1ZSRmVFeHRNWFphVVNaeVpXMWhjbXR6UFRWeVYwdzJTeTFXTlV4cGREVndZVWcK -------------------------------------------------------------------------------- /test/asset/test-v2rayn-sub-compatible.txt: -------------------------------------------------------------------------------- 1 | dm1lc3M6Ly9leUp3Y3lJNkl1YTFpK2l2bFNBeElpd2lZV1JrSWpvaU1TNHhMakV1TVNJc0luQnZjblFpT2pnd09EQXNJbWxrSWpvaU1UTTRObVk0TldVdE5qVTNZaTAwWkRabExUbGtOVFl0TnpoaVlXUmlOelZsTVdaa0lpd2lZV2xrSWpvaU5qUWlMQ0p1WlhRaU9pSjNjeUlzSW5SNWNHVWlPaUp1YjI1bElpd2lhRzl6ZENJNkltVjRZVzF3YkdVdVkyOXRJaXdpY0dGMGFDSTZJbHd2SWl3aWJYVjRJanA3SW1WdVlXSnNaV1FpT2lKbVlXeHpaU0o5ZlE9PQ== -------------------------------------------------------------------------------- /test/asset/test-v2rayn-sub.txt: -------------------------------------------------------------------------------- 1 | c3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRwd1lYTnpkMjl5WkFAdXMuZXhhbXBsZS5jb206NDQzLz9wbHVnaW49b2Jmcy1sb2NhbCUzQm9iZnMlM0R0bHMlM0JvYmZzLWhvc3QlM0RnYXRld2F5LWNhcnJ5LmljbG91ZC5jb20mZ3JvdXA9U3VyZ2lvI/Cfh7rwn4e4VVMgMQpzczovL1kyaGhZMmhoTWpBdGFXVjBaaTF3YjJ4NU1UTXdOVHB3WVhOemQyOXlaQUB1cy5leGFtcGxlLmNvbTo0NDMvP2dyb3VwPVN1cmdpbyPwn4e68J+HuFVTIDIKdm1lc3M6Ly9leUoySWpvaU1pSXNJbkJ6SWpvaTVyV0w2SytWSURFaUxDSmhaR1FpT2lJeExqRXVNUzR4SWl3aWNHOXlkQ0k2T0RBNE1Dd2lhV1FpT2lJeE16ZzJaamcxWlMwMk5UZGlMVFJrTm1VdE9XUTFOaTAzT0dKaFpHSTNOV1V4Wm1RaUxDSmhhV1FpT2lJMk5DSXNJbTVsZENJNkluZHpJaXdpZEhsd1pTSTZJbTV2Ym1VaUxDSm9iM04wSWpvaVpYaGhiWEJzWlM1amIyMGlMQ0p3WVhSb0lqb2lYQzhpTENKdGRYZ2lPbnNpWlc1aFlteGxaQ0k2SW1aaGJITmxJbjE5CnZtZXNzOi8vZXlKMklqb2lNaUlzSW5Ceklqb2k1cldMNksrVklESWlMQ0poWkdRaU9pSXhMakV1TVM0eElpd2ljRzl5ZENJNk9EQTRNQ3dpYVdRaU9pSXhNemcyWmpnMVpTMDJOVGRpTFRSa05tVXRPV1ExTmkwM09HSmhaR0kzTldVeFptUWlMQ0poYVdRaU9pSTJOQ0lzSW01bGRDSTZJblJqY0NJc0luUjVjR1VpT2lKdWIyNWxJaXdpYUc5emRDSTZJbVY0WVcxd2JHVXVZMjl0SWl3aWNHRjBhQ0k2SWx3dklpd2liWFY0SWpwN0ltVnVZV0pzWldRaU9pSm1ZV3h6WlNKOWZRPT0Kc3NyOi8vTVRJM0xqQXVNQzR4T2pFeU16UTZZWFYwYUY5aFpYTXhNamhmYldRMU9tRmxjeTB4TWpndFkyWmlPblJzY3pFdU1sOTBhV05yWlhSZllYVjBhRHBaVjBab1dXMUthUzhfYjJKbWMzQmhjbUZ0UFZsdVNteFpWM1F6V1ZSRmVFeHRNWFphVVNaeVpXMWhjbXR6UFRWeVYwdzJTeTFXTlV4cGREVndZVWcKdm1lc3M6Ly9leUoySWpvaU1pSXNJbkJ6SWpvaTVyV0w2SytWSUhSc2N5SXNJbUZrWkNJNkltVjRZVzF3YkdVdVkyOXRJaXdpY0c5eWRDSTZORFF6TENKcFpDSTZJakV6T0RabU9EVmxMVFkxTjJJdE5HUTJaUzA1WkRVMkxUYzRZbUZrWWpjMVpURm1aQ0lzSW1GcFpDSTZJalkwSWl3aWJtVjBJam9pZDNNaUxDSjBlWEJsSWpvaWJtOXVaU0lzSW1odmMzUWlPaUpsZUdGdGNHeGxMbU52YlNJc0luQmhkR2dpT2lKY0x5SXNJbTExZUNJNmV5SmxibUZpYkdWa0lqb2labUZzYzJVaWZTd2lkR3h6SWpvZ0luUnNjeUo5 2 | -------------------------------------------------------------------------------- /test/benchmark/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/test/benchmark/.gitkeep -------------------------------------------------------------------------------- /test/fixture/assign-local-port/provider/ssr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ssr-sub.txt', 5 | type: 'shadowsocksr_subscribe', 6 | startPort: 5000, 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/assign-local-port/provider/v2rayn.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-v2rayn-sub.txt', 5 | type: 'v2rayn_subscribe', 6 | startPort: 4000, 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/assign-local-port/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | artifacts: [ 5 | { 6 | name: 'v2rayn.conf', 7 | template: 'test', 8 | provider: 'v2rayn', 9 | }, 10 | { 11 | name: 'ssr.conf', 12 | template: 'test', 13 | provider: 'ssr', 14 | }, 15 | ], 16 | urlBase: 'http://example.com/', 17 | binPath: { 18 | shadowsocksr: '/usr/local/bin/ssr-local', 19 | v2ray: '/usr/local/bin/v2ray', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /test/fixture/assign-local-port/template/test.tpl: -------------------------------------------------------------------------------- 1 | #!MANAGED-CONFIG {{ downloadUrl }} interval=43200 strict=false 2 | 3 | [Proxy] 4 | {{ getSurgeNodes(nodeList) }} 5 | 6 | [Proxy Group] 7 | Proxy = select, {{ getNodeNames(nodeList) }} 8 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/provider/custom.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | type: 'custom', 5 | addFlag: true, 6 | nodeList: [ 7 | { 8 | type: 'shadowsocks', 9 | nodeName: 'V01 HK 1', 10 | hostname: 'us.example.com', 11 | port: '443', 12 | method: 'chacha20-ietf-poly1305', 13 | password: 'password', 14 | udpRelay: true, 15 | obfs: 'tls', 16 | obfsHost: 'gateway-carry.icloud.com', 17 | tfo: true, 18 | }, 19 | { 20 | type: 'shadowsocks', 21 | nodeName: 'V01 HK 2', 22 | hostname: 'us.example.com', 23 | port: '443', 24 | method: 'chacha20-ietf-poly1305', 25 | password: 'password', 26 | udpRelay: true, 27 | obfs: 'tls', 28 | obfsHost: 'gateway-carry.icloud.com', 29 | tfo: true, 30 | }, 31 | { 32 | type: 'shadowsocks', 33 | nodeName: 'V03 US', 34 | hostname: 'us.example.com', 35 | port: '443', 36 | method: 'chacha20-ietf-poly1305', 37 | password: 'password', 38 | udpRelay: true, 39 | obfs: 'tls', 40 | obfsHost: 'gateway-carry.icloud.com', 41 | tfo: true, 42 | }, 43 | ], 44 | } 45 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/provider/ss.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { utils } = require('../../../../') 4 | 5 | exports.keywordFilter = utils.useKeywords(['US 1', 'US 2']) 6 | exports.strictKeywordFilter = utils.useKeywords(['US', 'Netflix'], true) 7 | 8 | module.exports = { 9 | url: 'http://example.com/test-ss-sub.txt', 10 | type: 'shadowsocks_subscribe', 11 | customFilters: { 12 | keywordFilter: exports.keywordFilter, 13 | strictKeywordFilter: exports.strictKeywordFilter, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/provider/ss2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { utils } = require('../../../../') 4 | 5 | module.exports = { 6 | url: 'http://example.com/test-ss-sub.txt', 7 | type: 'shadowsocks_subscribe', 8 | nodeFilter: utils.useSortedKeywords(['US 2', 'US 1']), 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { utils } = require('../../../') 4 | 5 | module.exports = { 6 | artifacts: [ 7 | { 8 | name: 'ss.conf', 9 | template: 'test', 10 | provider: 'ss', 11 | }, 12 | { 13 | name: 'test_sorted_filter.conf', 14 | template: 'test2', 15 | provider: 'ss2', 16 | combineProviders: ['custom'], 17 | }, 18 | ], 19 | urlBase: 'http://example.com/', 20 | binPath: { 21 | shadowsocksr: '/usr/local/bin/ssr-local', 22 | v2ray: '/usr/local/bin/v2ray', 23 | }, 24 | customFilters: { 25 | globalKeywordFilter: utils.useKeywords(['US 1']), 26 | sortFilter: utils.useSortedKeywords(['🇺🇸US 2', '🇺🇸US 1']), 27 | hkFirstUsSecondFilter: utils.mergeSortedFilters([ 28 | utils.hkFilter, 29 | utils.usFilter, 30 | ]), 31 | providerFilter: utils.useProviders(['custom']), 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/template/test.tpl: -------------------------------------------------------------------------------- 1 | {{ getSurgeNodes(nodeList) }} 2 | ---- 3 | {{ getNodeNames(nodeList) }} 4 | ---- 5 | {{ getNodeNames(nodeList, customFilters.keywordFilter) }} 6 | ---- 7 | {{ getNodeNames(nodeList, customFilters.strictKeywordFilter) }} 8 | ---- 9 | {{ getNodeNames(nodeList, customFilters.globalKeywordFilter) }} 10 | ---- 11 | {{ getSurgeNodes(nodeList, customFilters.sortFilter) }} 12 | ---- 13 | {{ getNodeNames(nodeList, customFilters.sortFilter) }} 14 | ---- 15 | {{ getQuantumultXNodes(nodeList) }} 16 | ---- 17 | {{ getQuantumultXNodes(nodeList, customFilters.sortFilter) }} 18 | -------------------------------------------------------------------------------- /test/fixture/custom-filter/template/test2.tpl: -------------------------------------------------------------------------------- 1 | {{ getSurgeNodes(nodeList) }} 2 | ---- 3 | {{ getNodeNames(nodeList, customFilters.hkFirstUsSecondFilter) }} 4 | ---- 5 | {{ getQuantumultXNodes(nodeList, customFilters.hkFirstUsSecondFilter) }} 6 | ---- 7 | {{ getNodeNames(nodeList, customFilters.providerFilter) }} 8 | ---- 9 | {{ getClashNodeNames(nodeList, customFilters.providerFilter) | json }} 10 | -------------------------------------------------------------------------------- /test/fixture/not-specify-binPath/provider/ssr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ssr-sub.txt', 5 | type: 'shadowsocksr_subscribe', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/not-specify-binPath/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | artifacts: [ 5 | { 6 | name: 'ssr.conf', 7 | template: 'test', 8 | provider: 'ssr', 9 | }, 10 | ], 11 | urlBase: 'http://example.com/', 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/not-specify-binPath/template/test.tpl: -------------------------------------------------------------------------------- 1 | #!MANAGED-CONFIG {{ downloadUrl }} interval=43200 strict=false 2 | 3 | [Proxy] 4 | {{ getSurgeNodes(nodeList) }} 5 | 6 | [Proxy Group] 7 | Proxy = select, {{ getNodeNames(nodeList) }} 8 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/clash.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/clash-sample.yaml', 5 | type: 'clash', 6 | nodeFilter: (nodeConfig) => nodeConfig.type === 'shadowsocks', 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/clash_mod.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/clash-sample.yaml', 5 | type: 'clash', 6 | tls13: true, 7 | udpRelay: true, 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/custom.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | type: 'custom', 5 | addFlag: true, 6 | nodeList: [ 7 | { 8 | type: 'shadowsocks', 9 | nodeName: 'US', 10 | hostname: 'us.example.com', 11 | port: '443', 12 | method: 'chacha20-ietf-poly1305', 13 | password: 'password', 14 | udpRelay: true, 15 | obfs: 'tls', 16 | obfsHost: 'gateway-carry.icloud.com', 17 | tfo: true, 18 | mptcp: true, 19 | }, 20 | { 21 | type: 'snell', 22 | nodeName: 'Snell', 23 | hostname: 'us.example.com', 24 | port: '443', 25 | psk: 'password', 26 | obfs: 'tls', 27 | }, 28 | { 29 | type: 'https', 30 | nodeName: 'rename to HTTPS', 31 | hostname: 'us.example.com', 32 | port: '443', 33 | username: 'username', 34 | password: 'password', 35 | tfo: true, 36 | tls13: true, 37 | }, 38 | { 39 | type: 'trojan', 40 | nodeName: 'trojan node', 41 | hostname: 'trojan.example.com', 42 | port: '443', 43 | password: 'password', 44 | }, 45 | { 46 | type: 'trojan', 47 | nodeName: '火箭 trojan node', 48 | hostname: 'trojan.example.com', 49 | port: '443', 50 | password: 'password', 51 | }, 52 | { 53 | type: 'trojan', 54 | nodeName: 'foobar trojan node', 55 | hostname: 'trojan.example.com', 56 | port: '443', 57 | password: 'password', 58 | }, 59 | ], 60 | renameNode: (name) => { 61 | if (name === 'rename to HTTPS') { 62 | return 'HTTPS' 63 | } 64 | return name 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ss.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ss-sub.txt', 5 | type: 'shadowsocks_subscribe', 6 | tfo: true, 7 | mptcp: true, 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ss_json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/gui-config.json', 5 | type: 'shadowsocks_json_subscribe', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ss_with_up.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ss-sub.txt', 5 | type: 'shadowsocks_subscribe', 6 | tfo: true, 7 | mptcp: true, 8 | underlyingProxy: 'underlying-proxy', 9 | ecn: true, 10 | blockQuic: 'off', 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ssd.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/ssd-sample.txt', 5 | type: 'ssd', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ssr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ssr-sub.txt?v=1', 5 | type: 'shadowsocksr_subscribe', 6 | startPort: 61200, 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/ssr_with_udp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ssr-sub.txt?v=2', 5 | type: 'shadowsocksr_subscribe', 6 | udpRelay: true, 7 | tfo: true, 8 | startPort: 61100, 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/plain/provider/v2rayn.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-v2rayn-sub.txt', 5 | type: 'v2rayn_subscribe', 6 | tls13: process.env.TEST_TLS13_ENABLE === 'true', 7 | skipCertVerify: process.env.TEST_SKIP_CERT_VERIFY_ENABLE === 'true', 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/plain/template/extend-render-context.tpl: -------------------------------------------------------------------------------- 1 | {{ customParams.foo }} 2 | -------------------------------------------------------------------------------- /test/fixture/plain/template/snippet/snippet.tpl: -------------------------------------------------------------------------------- 1 | DOMAIN,example.com 2 | -------------------------------------------------------------------------------- /test/fixture/plain/template/template-functions.tpl: -------------------------------------------------------------------------------- 1 | getSurgeNodes 2 | {{ getSurgeNodes(nodeList) }} 3 | ---- 4 | getSurfboardNodes 5 | {{ getSurfboardNodes(nodeList) }} 6 | ---- 7 | getNodeNames 8 | {{ getNodeNames(nodeList) }} 9 | ---- 10 | getQuantumultXNodes 11 | {{ getQuantumultXNodes(nodeList) }} 12 | ---- 13 | getSurgeNodes 14 | {{ getSurgeNodes(nodeList, customFilters.globalFilter) }} 15 | ---- 16 | getLoonNodes 17 | {{ getLoonNodes(nodeList) }} 18 | ---- 19 | proxyTestUrl 20 | {{ proxyTestUrl }} 21 | ---- 22 | downloadUrl 23 | {{ downloadUrl }} 24 | --- 25 | {{ customParams.globalVariable }} 26 | --- 27 | {{ customParams.globalVariableWillBeRewritten }} 28 | --- 29 | {{ customParams.subLevel.anotherVariableWillBeRewritten }} 30 | --- 31 | {{ snippet("snippet/snippet.tpl").main("Proxy") }} 32 | --- 33 | {{ snippet("./snippet/snippet.tpl").main("Proxy") }} 34 | -------------------------------------------------------------------------------- /test/fixture/plain/template/test.tpl: -------------------------------------------------------------------------------- 1 | #!MANAGED-CONFIG {{ downloadUrl }} interval=43200 strict=false 2 | 3 | [Proxy] 4 | {{ getSurgeNodes(nodeList) }} 5 | 6 | [Proxy Group] 7 | Proxy = select, {{ getNodeNames(nodeList) }} 8 | -------------------------------------------------------------------------------- /test/fixture/template-error/provider/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/gui-config.json', 5 | type: 'shadowsocks_json_subscribe', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/template-error/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | artifacts: [ 5 | { 6 | name: 'test.conf', 7 | template: 'test', 8 | provider: 'test', 9 | }, 10 | ], 11 | urlBase: 'http://example.com/', 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/template-error/template/test.tpl: -------------------------------------------------------------------------------- 1 | #!MANAGED-CONFIG {{ downloadUrl }} interval=43200 strict=false 2 | 3 | [Proxy] 4 | {{ getSurgeNodes(nodeList) }} 5 | 6 | [Proxy Group] 7 | Proxy = select, {{ getNodeNames(nodeList }} 8 | -------------------------------------------------------------------------------- /test/fixture/template-variables-functions/provider/ss.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | url: 'http://example.com/test-ss-sub.txt', 5 | type: 'shadowsocks_subscribe', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/template-variables-functions/surgio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | artifacts: [ 5 | { 6 | name: 'ss.conf', 7 | template: 'test', 8 | provider: 'ss', 9 | }, 10 | ], 11 | remoteSnippets: [ 12 | { 13 | name: 'netflix', 14 | url: 'http://example.com/netflix.list', 15 | }, 16 | ], 17 | urlBase: 'http://example.com/', 18 | } 19 | -------------------------------------------------------------------------------- /test/fixture/template-variables-functions/template/test.tpl: -------------------------------------------------------------------------------- 1 | {{ remoteSnippets.netflix.main('Proxy') }} 2 | {{ getDownloadUrl('ss.conf') }} 3 | -------------------------------------------------------------------------------- /test/helpers/oclif.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const chai = require('chai') 3 | const chaiJestSnapshot = require('chai-jest-snapshot') 4 | 5 | chai.use(chaiJestSnapshot) 6 | 7 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 8 | process.env.NODE_ENV = 'development' 9 | 10 | global.oclif = global.oclif || {} 11 | global.oclif.columns = 80 12 | 13 | exports.mochaHooks = async () => { 14 | return { 15 | before() { 16 | chaiJestSnapshot.resetSnapshotRegistry() 17 | }, 18 | beforeEach() { 19 | chaiJestSnapshot.configureUsingMochaContext(this) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ], 9 | "types": [ 10 | "node", 11 | "mocha" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build/", 5 | "sourceMap": true, 6 | "noEmit": false, 7 | }, 8 | "include": [ 9 | "src/**/*.ts", 10 | ], 11 | "exclude": ["node_modules/**", "**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "**/*.ts", 5 | "**/*.js", 6 | "**/.*.js" 7 | ], 8 | "exclude": ["node_modules/**"], 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "transpileOnly": true, 4 | "files": true, 5 | }, 6 | "compilerOptions": { 7 | // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping 8 | "target": "ES2022", 9 | "module": "CommonJS", 10 | "moduleResolution": "Node", 11 | "declaration": true, 12 | "sourceMap": false, 13 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 14 | "allowSyntheticDefaultImports": true, // https://zhuanlan.zhihu.com/p/29022311 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "noEmit": true, 18 | 19 | /* Strict Type-Checking Options */ 20 | "strict": true /* Enable all strict type-checking options. */, 21 | "noUnusedLocals": false /* Report errors on unused locals. */, 22 | "noUnusedParameters": true /* Report errors on unused parameters. */, 23 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 24 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 25 | 26 | /* Debugging Options */ 27 | "traceResolution": false /* Report module resolution log messages. */, 28 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 29 | "listFiles": false /* Print names of files part of the compilation. */, 30 | "pretty": true /* Stylize errors and messages using color and context. */, 31 | 32 | "lib": [ 33 | "ES2022" 34 | ], 35 | "types": [ 36 | "node" 37 | ], 38 | "typeRoots": [ 39 | "./node_modules/@types", 40 | "./types" 41 | ] 42 | }, 43 | "include": [ 44 | "src/**/*.ts", 45 | "test/**/*.ts", 46 | ], 47 | "exclude": ["./node_modules/**"], 48 | "compileOnSave": false, 49 | } 50 | -------------------------------------------------------------------------------- /types/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surgioproject/surgio/736adb4186061704fce6288cc9c65f4ab223592e/types/.gitkeep -------------------------------------------------------------------------------- /utils.d.ts: -------------------------------------------------------------------------------- 1 | export * from './build/utils' 2 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/utils') 2 | -------------------------------------------------------------------------------- /zbpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache_dependencies": false, 3 | "build_command": "pnpm run docs:build", 4 | "serverless": true 5 | } --------------------------------------------------------------------------------