├── .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 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 | 
67 |
68 | 授权成功之后即可选择代码库,然后会看到如下的页面:
69 |
70 | 
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 | 
60 |
61 | 选择从代码仓库部署。
62 |
63 | 
64 |
65 | 随后在项目列表中找到代码库,选择用于部署的分支,点击部署。部署成功后即可使用默认分配的域名访问 Surgio 面板。
66 |
67 | 今后代码库的分支有更新 Railway 会自动拉取并部署。和 Vercel 不同的是,Railway 属于容器化方案,因此打包编译的时间会比 Vercel 久很多。
68 |
69 | 
70 |
71 | :::tip 不要忘记!
72 | 请不要忘记将 `surgio.conf.js` 中 `urlBase` 改为 Railway 的域名路径。
73 | :::
74 |
75 | ## 配置项目
76 |
77 | 下面的内容属于自定义范畴,可跳过。如果你没有一定基础建议跳过。
78 |
79 | ### 自定义域名
80 |
81 | 
82 |
83 | ### 修改环境变量
84 |
85 | 需要注意的是,每次增删环境变量都会触发打包编译,如果一次性要添加很多环境变量建议使用 **Bulk Import**。
86 |
87 | 
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 | 
50 |
51 | 选择 __GitHub__ 并且选择你的 Surgio 仓库。这一步可能会需要授权,请根据提示操作即可。
52 |
53 | 正常情况下 Zeabur 能够自动识别项目的类型和所需的配置,请检查是否如下所示:
54 |
55 | 
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 | 
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 | 
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 | 
25 |
26 | 点击 _check the logs_,可以看到错误日志,截图后反馈至交流群。
27 |
28 | Build logs 通常不会出错,如果没有看到错误请在 Functions 页面查看运行期错误。
29 |
30 | 
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 | }
--------------------------------------------------------------------------------