├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── constants.ts ├── core │ ├── app.ts │ ├── proxy-utils │ │ ├── index.ts │ │ ├── parsers │ │ │ ├── index.ts │ │ │ └── peggy │ │ │ │ ├── loon.peg │ │ │ │ ├── loon.ts │ │ │ │ ├── qx.peg │ │ │ │ ├── qx.ts │ │ │ │ ├── surge.peg │ │ │ │ ├── surge.ts │ │ │ │ ├── trojan-uri.peg │ │ │ │ └── trojan-uri.ts │ │ ├── preprocessors │ │ │ └── index.ts │ │ ├── processors │ │ │ └── index.ts │ │ ├── producers │ │ │ ├── clash.ts │ │ │ ├── index.ts │ │ │ ├── loon.ts │ │ │ ├── qx.ts │ │ │ ├── stash.ts │ │ │ ├── surge.ts │ │ │ ├── uri.ts │ │ │ └── utils.ts │ │ └── validators │ │ │ └── index.js │ └── rule-utils │ │ ├── index.ts │ │ ├── parsers.ts │ │ ├── preprocessors.ts │ │ └── producers.ts ├── db.ts ├── env.ts ├── index.ts ├── restful │ ├── artifacts.ts │ ├── collections.ts │ ├── download.ts │ ├── errors │ │ └── index.ts │ ├── miscs.ts │ ├── node-info.ts │ ├── preview.ts │ ├── response.ts │ ├── settings.ts │ ├── sort.ts │ ├── subscriptions.ts │ └── sync.ts ├── test │ └── proxy-parsers │ │ ├── loon.spec.js │ │ ├── qx.spec.js │ │ ├── surge.spec.js │ │ └── testcases.js ├── utils │ ├── database.ts │ ├── download.ts │ ├── flow.ts │ ├── geo.ts │ ├── gist.ts │ ├── index.ts │ ├── logical.ts │ ├── migration.ts │ ├── platform.ts │ └── resource-cache.ts └── vendor │ ├── express.ts │ ├── md5.ts │ └── open-api.ts ├── tsconfig.json └── wrangler.toml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build sub-store-workers 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | paths-ignore: 9 | - 'README.md' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check Out 17 | uses: actions/checkout@v3 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v2 26 | id: pnpm-install 27 | with: 28 | version: latest 29 | run_install: false 30 | 31 | - name: Install and Deploy 32 | env: 33 | DATABASE_NAME: ${{ secrets.DATABASE_NAME }} 34 | DATABASE_ID: ${{ secrets.DATABASE_ID }} 35 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 36 | BEARER_TOKEN: ${{ secrets.BEARER_TOKEN }} 37 | D_TOKEN: ${{ secrets.D_TOKEN }} 38 | run: | 39 | sed -i "s|substore_workers_database_name|$DATABASE_NAME|g" wrangler.toml 40 | sed -i "s|substore_workers_database_id|$DATABASE_ID|g" wrangler.toml 41 | sed -i "s|substore_workers_bearer_token|$BEARER_TOKEN|g" wrangler.toml 42 | sed -i "s|substore_workers_d_token|$D_TOKEN|g" wrangler.toml 43 | pnpm install -g wrangler 44 | pnpm install 45 | CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" wrangler deploy > /dev/null 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | !.vscode/settings.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # Yarn Berry 32 | .yarn/* 33 | !.yarn/patches 34 | !.yarn/releases 35 | !.yarn/plugins 36 | !.yarn/sdks 37 | !.yarn/versions 38 | .pnp.* 39 | 40 | .wrangler 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sub-Store Workers 部署 2 | 3 | **版权声明:作者为 [Peng-YM](https://github.com/Peng-YM) 以及 [所有参与贡献的大佬们](https://github.com/sub-store-org/Sub-Store/graphs/contributors),由本人修改适配 Cloudflare Workers** 4 | 5 | 项目使用到 [Cloudflare Workers Serverless](https://workers.cloudflare.com/) 以及 [Cloudflare D1 Serverless SQL databases](https://developers.cloudflare.com/d1/),感谢 Cloudflare 6 | 7 | ## 部署 8 | 9 | 其中前序步骤由您自行完成,以下是关键步骤 10 | 11 | 1. 获取 Cloudflare API Token 12 | 2. 创建 Cloudflare D1 数据库 13 | 3. Fork 此仓库 14 | 4. 设置 Git Repository secrets 15 | 5. 启用 Actions 16 | 6. 完成部署 17 | 18 | ### 0x1 获取 Cloudflare API Token 19 | 20 | [点击打开](https://dash.cloudflare.com/profile/api-tokens) 此页面,-> 创建令牌 -> 编辑 Cloudflare Workers 使用模板 -> 帐户资源 以及 区域资源 根据您的需要进行设置 -> 继续以显示摘要 -> 创建令牌 21 | 22 | **将令牌保存以供后续步骤使用** 23 | 24 | ### 0x2 创建 Cloudflare D1 数据库 25 | 26 | [点击打开](https://dash.cloudflare.com/) 此页面,进入 Workers 和 Pages -> D1 -> 创建数据库 -> 仪表盘 27 | 28 | 设置一个数据库名称例如:`sub_store` 29 | 30 | 地理位置根据您的需要选择 31 | 32 | 创建完成后保存 **数据库名称** 以及 **数据库 ID** 以供后续步骤使用** 33 | 34 | ### 0x3 Fork 此仓库 35 | 36 | 如题 37 | 38 | ### 0x4 设置 Git Repository secrets 39 | 40 | 在您 Fork 后的仓库 -> Settings -> Secrets and variables -> Actions -> new repository secrets 41 | 42 | 重复上述步骤将以下内容依次添加 43 | 44 | - name 填入【 `CLOUDFLARE_API_TOKEN` 】secrets 填入第一步获取的令牌 45 | - name 填入【 `DATABASE_ID` 】secrets 填入第二步获取的数据库 ID 46 | - name 填入【 `DATABASE_NAME` 】secrets 填入第二步获取的数据库名称 47 | - name 填入【 `BEARER_TOKEN` 】secrets 您随机生成一个大小写字母+数字的组合填入,为 HTTP Bearer Token,前后端通信认证使用 48 | - name 填入【 `D_TOKEN` 】secrets 您随机生成一个大小写字母+数字的组合填入,为您从 sub-store-workers 后端获取和预览节点的认证 49 | 50 | ### 0x5 启用 Actions 51 | 52 | 在您 Fork 后的仓库启用 Actions,随后手动运行工作流 53 | 54 | ### 0x5 完成部署 55 | 56 | 如不出意外的话,回到 [此页面](https://dash.cloudflare.com/) 进入 Workers 和 Pages 即可看到已创建的应用 57 | 58 | 您可以在此处获取到您的 sub-store-workers 地址也就是后端地址,workers.dev 的地址在大陆地区可能访问异常,推荐您自行绑定域名 59 | 60 | ## 前端 61 | 62 | - **Vercel**: 63 | - **Cloudflare Pages**: 64 | 65 | 上述 2 个由我构建,但在大陆地区可能访问异常,推荐您审查代码然后使用 [SaintWe/Sub-Store-Front-End](https://github.com/SaintWe/Sub-Store-Front-End) 自行构建绑定自己的域名 66 | 67 | ### 前后端连接 68 | 69 | **您可在前端 [我的] 页面填入后端地址以及其他参数**,或使用 url 传入,例: 70 | 71 | - https://sub-store-workers.vercel.app?api_url=https%3A%2F%2Fgithub.com&bearer_token=111222333444&d_token=555666777888 72 | - https://sub-store-workers.vercel.app?api=https%3A%2F%2Fgithub.com&token=111222333444&d_token=555666777888 73 | 74 | *您设置后端地址、Bearer Token 以及 D Token 均保存在浏览器中,清理浏览器或导致您需重新填写* 75 | 76 | *将 URL 保存为书签方便多设备使用* 77 | 78 | ### 参数解析 79 | 80 | - `api_url` 或 `api` 为后端地址 81 | - `bearer_token` 或 `token` 为 HTTP Bearer Token,前后端通信认证使用 82 | - `d_token` 为您从 sub-store-workers 后端获取和预览节点的认证 83 | 84 | **请注意进行 Url 编码** 85 | 86 | ## 结束语 87 | 88 | - 感谢 [淮城一只猫 @JaxsonWang](https://github.com/JaxsonWang) 在移植过程中提供的帮助 89 | - 感谢 [@Peng-YM](https://github.com/Peng-YM/Sub-Store) 以及所有参与的大佬的无私的奉献 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sub-store-workers", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "wrangler dev --log-level debug", 7 | "deploy": "wrangler deploy", 8 | "deploy_dry": "wrangler deploy --dry-run" 9 | }, 10 | "dependencies": { 11 | "hono": "^3.3.1", 12 | "js-base64": "^3.7.5", 13 | "kysely": "^0.26.1", 14 | "kysely-d1": "^0.3.0", 15 | "lodash": "^4.17.21", 16 | "peggy": "^3.0.2", 17 | "static-js-yaml": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "@cloudflare/workers-types": "^4.20230628.0", 21 | "wrangler": "^3.1.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SCHEMA_VERSION_KEY = 'schemaVersion'; 2 | export const SETTINGS_KEY = 'settings'; 3 | export const SUBS_KEY = 'subs'; 4 | export const COLLECTIONS_KEY = 'collections'; 5 | export const ARTIFACTS_KEY = 'artifacts'; 6 | export const RULES_KEY = 'rules'; 7 | export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; 8 | export const GIST_BACKUP_FILE_NAME = 'Sub-Store'; 9 | export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository'; 10 | export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource'; 11 | export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour 12 | -------------------------------------------------------------------------------- /src/core/app.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPI } from '../vendor/open-api'; 2 | 3 | const $ = new OpenAPI('sub-store'); 4 | export default $; 5 | -------------------------------------------------------------------------------- /src/core/proxy-utils/index.ts: -------------------------------------------------------------------------------- 1 | import download from '../../utils/download'; 2 | 3 | import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; 4 | import PROXY_PREPROCESSORS from './preprocessors'; 5 | import PROXY_PRODUCERS from './producers'; 6 | import PROXY_PARSERS from './parsers'; 7 | import $ from '../../core/app'; 8 | 9 | function preprocess(raw) { 10 | for (const processor of PROXY_PREPROCESSORS) { 11 | try { 12 | if (processor.test(raw)) { 13 | $.info(`Pre-processor [${processor.name}] activated`); 14 | return processor.parse(raw); 15 | } 16 | } catch (e) { 17 | $.error(`Parser [${processor.name}] failed\n Reason: ${e}`); 18 | } 19 | } 20 | return raw; 21 | } 22 | 23 | function parse(raw) { 24 | raw = preprocess(raw); 25 | // parse 26 | const lines = raw.split('\n'); 27 | const proxies = []; 28 | let lastParser; 29 | 30 | for (let line of lines) { 31 | line = line.trim(); 32 | if (line.length === 0) continue; // skip empty line 33 | let success = false; 34 | 35 | // try to parse with last used parser 36 | if (lastParser) { 37 | const [proxy, error] = tryParse(lastParser, line); 38 | if (!error) { 39 | proxies.push(proxy); 40 | success = true; 41 | } 42 | } 43 | 44 | if (!success) { 45 | // search for a new parser 46 | for (const parser of PROXY_PARSERS) { 47 | const [proxy, error] = tryParse(parser, line); 48 | if (!error) { 49 | proxies.push(proxy); 50 | lastParser = parser; 51 | success = true; 52 | $.info(`${parser.name} is activated`); 53 | break; 54 | } 55 | } 56 | } 57 | 58 | if (!success) { 59 | $.error(`Failed to parse line: ${line}`); 60 | } 61 | } 62 | 63 | return proxies; 64 | } 65 | 66 | async function process(proxies, operators = [], targetPlatform) { 67 | for (const item of operators) { 68 | // process script 69 | let script; 70 | const $arguments = {}; 71 | if (item.type.indexOf('Script') !== -1) { 72 | const { mode, content } = item.args; 73 | if (mode === 'link') { 74 | const url = content; 75 | // extract link arguments 76 | const rawArgs = url.split('#'); 77 | if (rawArgs.length > 1) { 78 | for (const pair of rawArgs[1].split('&')) { 79 | const key = pair.split('=')[0]; 80 | const value = pair.split('=')[1] || true; 81 | $arguments[key] = value; 82 | } 83 | } 84 | 85 | // if this is a remote script, download it 86 | try { 87 | script = await download(url.split('#')[0]); 88 | // $.info(`Script loaded: >>>\n ${script}`); 89 | } catch (err) { 90 | $.error( 91 | `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, 92 | ); 93 | // skip the script if download failed. 94 | continue; 95 | } 96 | } else { 97 | script = content; 98 | } 99 | } 100 | 101 | if (!PROXY_PROCESSORS[item.type]) { 102 | $.error(`Unknown operator: "${item.type}"`); 103 | continue; 104 | } 105 | 106 | $.info( 107 | `Applying "${item.type}" with arguments:\n >>> ${ 108 | JSON.stringify(item.args, null, 2) || 'None' 109 | }`, 110 | ); 111 | let processor; 112 | if (item.type.indexOf('Script') !== -1) { 113 | processor = PROXY_PROCESSORS[item.type]( 114 | script, 115 | targetPlatform, 116 | $arguments, 117 | ); 118 | } else { 119 | processor = PROXY_PROCESSORS[item.type](item.args || {}); 120 | } 121 | proxies = await ApplyProcessor(processor, proxies); 122 | } 123 | return proxies; 124 | } 125 | 126 | function produce(proxies, targetPlatform) { 127 | const producer = PROXY_PRODUCERS[targetPlatform]; 128 | if (!producer) { 129 | throw new Error(`Target platform: ${targetPlatform} is not supported!`); 130 | } 131 | 132 | // filter unsupported proxies 133 | proxies = proxies.filter( 134 | (proxy) => 135 | !(proxy.supported && proxy.supported[targetPlatform] === false), 136 | ); 137 | 138 | $.info(`Producing proxies for target: ${targetPlatform}`); 139 | if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { 140 | return proxies 141 | .map((proxy) => { 142 | try { 143 | return producer.produce(proxy); 144 | } catch (err) { 145 | $.error( 146 | `Cannot produce proxy: ${JSON.stringify( 147 | proxy, 148 | null, 149 | 2, 150 | )}\nReason: ${err}`, 151 | ); 152 | return ''; 153 | } 154 | }) 155 | .filter((line) => line.length > 0) 156 | .join('\n'); 157 | } else if (producer.type === 'ALL') { 158 | return producer.produce(proxies); 159 | } 160 | } 161 | 162 | export const ProxyUtils = { 163 | parse, 164 | process, 165 | produce, 166 | }; 167 | 168 | function tryParse(parser, line) { 169 | if (!safeMatch(parser, line)) return [null, new Error('Parser mismatch')]; 170 | try { 171 | const proxy = parser.parse(line); 172 | return [proxy, null]; 173 | } catch (err) { 174 | return [null, err]; 175 | } 176 | } 177 | 178 | function safeMatch(parser, line) { 179 | try { 180 | return parser.test(line); 181 | } catch (err) { 182 | return false; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/loon.peg: -------------------------------------------------------------------------------- 1 | // global initializer 2 | {{ 3 | function $set(obj, path, value) { 4 | if (Object(obj) !== obj) return obj; 5 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 6 | path 7 | .slice(0, -1) 8 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 9 | path[path.length - 1] 10 | ] = value; 11 | return obj; 12 | } 13 | }} 14 | 15 | // per-parser initializer 16 | { 17 | const proxy = {}; 18 | const obfs = {}; 19 | const transport = {}; 20 | const $ = {}; 21 | 22 | function handleTransport() { 23 | if (transport.type === "tcp") { /* do nothing */ } 24 | else if (transport.type === "ws") { 25 | proxy.network = "ws"; 26 | $set(proxy, "ws-opts.path", transport.path); 27 | $set(proxy, "ws-opts.headers.Host", transport.host); 28 | } else if (transport.type === "http") { 29 | proxy.network = "http"; 30 | $set(proxy, "http-opts.path", transport.path); 31 | $set(proxy, "http-opts.headers.Host", transport.host); 32 | } 33 | } 34 | } 35 | 36 | start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { 37 | return proxy; 38 | } 39 | 40 | shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{ 41 | proxy.type = "ssr"; 42 | // handle ssr obfs 43 | proxy.obfs = obfs.type; 44 | } 45 | shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 46 | proxy.type = "ss"; 47 | // handle ss obfs 48 | if (obfs.type == "http" || obfs.type === "tls") { 49 | proxy.plugin = "obfs"; 50 | $set(proxy, "plugin-opts.mode", obfs.type); 51 | $set(proxy, "plugin-opts.host", obfs.host); 52 | $set(proxy, "plugin-opts.path", obfs.path); 53 | } 54 | } 55 | vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* { 56 | proxy.type = "vmess"; 57 | proxy.cipher = proxy.cipher || "none"; 58 | proxy.alterId = proxy.alterId || 0; 59 | handleTransport(); 60 | } 61 | vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { 62 | proxy.type = "vless"; 63 | handleTransport(); 64 | } 65 | trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { 66 | proxy.type = "trojan"; 67 | handleTransport(); 68 | } 69 | https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { 70 | proxy.type = "http"; 71 | proxy.tls = true; 72 | } 73 | http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { 74 | proxy.type = "http"; 75 | } 76 | 77 | address = comma server:server comma port:port { 78 | proxy.server = server; 79 | proxy.port = port; 80 | } 81 | 82 | server = ip/domain 83 | 84 | ip = & { 85 | const start = peg$currPos; 86 | let j = start; 87 | while (j < input.length) { 88 | if (input[j] === ",") break; 89 | j++; 90 | } 91 | peg$currPos = j; 92 | $.ip = input.substring(start, j).trim(); 93 | return true; 94 | } { return $.ip; } 95 | 96 | domain = match:[0-9a-zA-z-_.]+ { 97 | const domain = match.join(""); 98 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 99 | return domain; 100 | } 101 | throw new Error("Invalid domain: " + domain); 102 | } 103 | 104 | port = digits:[0-9]+ { 105 | const port = parseInt(digits.join(""), 10); 106 | if (port >= 0 && port <= 65535) { 107 | return port; 108 | } 109 | throw new Error("Invalid port number: " + port); 110 | } 111 | 112 | method = comma cipher:cipher { 113 | proxy.cipher = cipher; 114 | } 115 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto"); 116 | 117 | username = & { 118 | let j = peg$currPos; 119 | let start, end; 120 | let first = true; 121 | while (j < input.length) { 122 | if (input[j] === ',') { 123 | if (first) { 124 | start = j + 1; 125 | first = false; 126 | } else { 127 | end = j; 128 | break; 129 | } 130 | } 131 | j++; 132 | } 133 | const match = input.substring(start, end); 134 | if (match.indexOf("=") === -1) { 135 | $.username = match; 136 | peg$currPos = end; 137 | return true; 138 | } 139 | } { proxy.username = $.username; } 140 | password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } 141 | uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } 142 | 143 | obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } 144 | 145 | obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } 146 | obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; } 147 | 148 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; } 149 | obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } 150 | uri = $[^,]+ 151 | 152 | transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; } 153 | transport_host = comma "host" equals host:domain { transport.host = host; } 154 | transport_path = comma "path" equals path:uri { transport.path = path; } 155 | 156 | ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; } 157 | ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } 158 | 159 | vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } 160 | 161 | over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } 162 | tls_host = comma "tls-name" equals host:domain { proxy.sni = host; } 163 | tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } 164 | 165 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 166 | udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } 167 | 168 | tag = match:[^=,]* { proxy.name = match.join("").trim(); } 169 | comma = _ "," _ 170 | equals = _ "=" _ 171 | _ = [ \r\t]* 172 | bool = b:("true"/"false") { return b === "true" } 173 | others = comma [^=,]+ equals [^=,]+ -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/loon.ts: -------------------------------------------------------------------------------- 1 | import * as peggy from 'peggy'; 2 | const grammars = String.raw` 3 | // global initializer 4 | {{ 5 | function $set(obj, path, value) { 6 | if (Object(obj) !== obj) return obj; 7 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 8 | path 9 | .slice(0, -1) 10 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 11 | path[path.length - 1] 12 | ] = value; 13 | return obj; 14 | } 15 | }} 16 | 17 | // per-parser initializer 18 | { 19 | const proxy = {}; 20 | const obfs = {}; 21 | const transport = {}; 22 | const $ = {}; 23 | 24 | function handleTransport() { 25 | if (transport.type === "tcp") { /* do nothing */ } 26 | else if (transport.type === "ws") { 27 | proxy.network = "ws"; 28 | $set(proxy, "ws-opts.path", transport.path); 29 | $set(proxy, "ws-opts.headers.Host", transport.host); 30 | } else if (transport.type === "http") { 31 | proxy.network = "http"; 32 | $set(proxy, "http-opts.path", transport.path); 33 | $set(proxy, "http-opts.headers.Host", transport.host); 34 | } 35 | } 36 | } 37 | 38 | start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { 39 | return proxy; 40 | } 41 | 42 | shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{ 43 | proxy.type = "ssr"; 44 | // handle ssr obfs 45 | proxy.obfs = obfs.type; 46 | } 47 | shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 48 | proxy.type = "ss"; 49 | // handle ss obfs 50 | if (obfs.type == "http" || obfs.type === "tls") { 51 | proxy.plugin = "obfs"; 52 | $set(proxy, "plugin-opts.mode", obfs.type); 53 | $set(proxy, "plugin-opts.host", obfs.host); 54 | $set(proxy, "plugin-opts.path", obfs.path); 55 | } 56 | } 57 | vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* { 58 | proxy.type = "vmess"; 59 | proxy.cipher = proxy.cipher || "none"; 60 | proxy.alterId = proxy.alterId || 0; 61 | handleTransport(); 62 | } 63 | vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { 64 | proxy.type = "vless"; 65 | handleTransport(); 66 | } 67 | trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { 68 | proxy.type = "trojan"; 69 | handleTransport(); 70 | } 71 | https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { 72 | proxy.type = "http"; 73 | proxy.tls = true; 74 | } 75 | http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { 76 | proxy.type = "http"; 77 | } 78 | 79 | address = comma server:server comma port:port { 80 | proxy.server = server; 81 | proxy.port = port; 82 | } 83 | 84 | server = ip/domain 85 | 86 | ip = & { 87 | const start = peg$currPos; 88 | let j = start; 89 | while (j < input.length) { 90 | if (input[j] === ",") break; 91 | j++; 92 | } 93 | peg$currPos = j; 94 | $.ip = input.substring(start, j).trim(); 95 | return true; 96 | } { return $.ip; } 97 | 98 | domain = match:[0-9a-zA-z-_.]+ { 99 | const domain = match.join(""); 100 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 101 | return domain; 102 | } 103 | throw new Error("Invalid domain: " + domain); 104 | } 105 | 106 | port = digits:[0-9]+ { 107 | const port = parseInt(digits.join(""), 10); 108 | if (port >= 0 && port <= 65535) { 109 | return port; 110 | } 111 | throw new Error("Invalid port number: " + port); 112 | } 113 | 114 | method = comma cipher:cipher { 115 | proxy.cipher = cipher; 116 | } 117 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto"); 118 | 119 | username = & { 120 | let j = peg$currPos; 121 | let start, end; 122 | let first = true; 123 | while (j < input.length) { 124 | if (input[j] === ',') { 125 | if (first) { 126 | start = j + 1; 127 | first = false; 128 | } else { 129 | end = j; 130 | break; 131 | } 132 | } 133 | j++; 134 | } 135 | const match = input.substring(start, end); 136 | if (match.indexOf("=") === -1) { 137 | $.username = match; 138 | peg$currPos = end; 139 | return true; 140 | } 141 | } { proxy.username = $.username; } 142 | password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } 143 | uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } 144 | 145 | obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } 146 | 147 | obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } 148 | obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; } 149 | 150 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; } 151 | obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } 152 | uri = $[^,]+ 153 | 154 | transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; } 155 | transport_host = comma "host" equals host:domain { transport.host = host; } 156 | transport_path = comma "path" equals path:uri { transport.path = path; } 157 | 158 | ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; } 159 | ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } 160 | 161 | vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } 162 | 163 | over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } 164 | tls_host = comma "tls-name" equals host:domain { proxy.sni = host; } 165 | tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } 166 | 167 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 168 | udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } 169 | 170 | tag = match:[^=,]* { proxy.name = match.join("").trim(); } 171 | comma = _ "," _ 172 | equals = _ "=" _ 173 | _ = [ \r\t]* 174 | bool = b:("true"/"false") { return b === "true" } 175 | others = comma [^=,]+ equals [^=,]+ 176 | `; 177 | let parser; 178 | export default function getParser() { 179 | if (!parser) { 180 | parser = peggy.generate(grammars); 181 | } 182 | return parser; 183 | } 184 | -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/qx.peg: -------------------------------------------------------------------------------- 1 | // global initializer 2 | {{ 3 | function $set(obj, path, value) { 4 | if (Object(obj) !== obj) return obj; 5 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 6 | path 7 | .slice(0, -1) 8 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 9 | path[path.length - 1] 10 | ] = value; 11 | return obj; 12 | } 13 | }} 14 | 15 | // per-parse initializer 16 | { 17 | const proxy = {}; 18 | const obfs = {}; 19 | const $ = {}; 20 | 21 | function handleObfs() { 22 | if (obfs.type === "ws" || obfs.type === "wss") { 23 | proxy.network = "ws"; 24 | if (obfs.type === 'wss') { 25 | proxy.tls = true; 26 | } 27 | $set(proxy, "ws-opts.path", obfs.path); 28 | $set(proxy, "ws-opts.headers.Host", obfs.host); 29 | } else if (obfs.type === "over-tls") { 30 | proxy.tls = true; 31 | } else if (obfs.type === "http") { 32 | proxy.network = "http"; 33 | $set(proxy, "http-opts.path", obfs.path); 34 | $set(proxy, "http-opts.headers.Host", obfs.host); 35 | } 36 | } 37 | } 38 | 39 | start = (trojan/shadowsocks/vmess/http/socks5) { 40 | return proxy 41 | } 42 | 43 | trojan = "trojan" equals address 44 | (password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { 45 | proxy.type = "trojan"; 46 | handleObfs(); 47 | } 48 | 49 | shadowsocks = "shadowsocks" equals address 50 | (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* { 51 | if (proxy.protocol) { 52 | proxy.type = "ssr"; 53 | // handle ssr obfs 54 | if (obfs.host) proxy["obfs-param"] = obfs.host; 55 | if (obfs.type) proxy.obfs = obfs.type; 56 | } else { 57 | proxy.type = "ss"; 58 | // handle ss obfs 59 | if (obfs.type == "http" || obfs.type === "tls") { 60 | proxy.plugin = "obfs"; 61 | $set(proxy, "plugin-opts", { 62 | mode: obfs.type 63 | }); 64 | } else if (obfs.type === "ws" || obfs.type === "wss") { 65 | proxy.plugin = "v2ray-plugin"; 66 | $set(proxy, "plugin-opts.mode", "websocket"); 67 | if (obfs.type === "wss") { 68 | $set(proxy, "plugin-opts.tls", true); 69 | } 70 | } else if (obfs.type === 'over-tls') { 71 | throw new Error('ss over-tls is not supported'); 72 | } 73 | if (obfs.type) { 74 | $set(proxy, "plugin-opts.host", obfs.host); 75 | $set(proxy, "plugin-opts.path", obfs.path); 76 | } 77 | } 78 | } 79 | 80 | vmess = "vmess" equals address 81 | (uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* { 82 | proxy.type = "vmess"; 83 | proxy.cipher = proxy.cipher || "none"; 84 | if (proxy.aead) { 85 | proxy.alterId = 0; 86 | } else { 87 | proxy.alterId = proxy.alterId || 0; 88 | } 89 | handleObfs(); 90 | } 91 | 92 | http = "http" equals address 93 | (username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ 94 | proxy.type = "http"; 95 | } 96 | 97 | socks5 = "socks5" equals address 98 | (username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { 99 | proxy.type = "socks5"; 100 | } 101 | 102 | address = server:server ":" port:port { 103 | proxy.server = server; 104 | proxy.port = port; 105 | } 106 | server = ip/domain 107 | 108 | domain = match:[0-9a-zA-z-_.]+ { 109 | const domain = match.join(""); 110 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 111 | return domain; 112 | } 113 | } 114 | 115 | ip = & { 116 | const start = peg$currPos; 117 | let end; 118 | let j = start; 119 | while (j < input.length) { 120 | if (input[j] === ",") break; 121 | if (input[j] === ":") end = j; 122 | j++; 123 | } 124 | peg$currPos = end || j; 125 | $.ip = input.substring(start, end).trim(); 126 | return true; 127 | } { return $.ip; } 128 | 129 | port = digits:[0-9]+ { 130 | const port = parseInt(digits.join(""), 10); 131 | if (port >= 0 && port <= 65535) { 132 | return port; 133 | } 134 | } 135 | 136 | username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); } 137 | password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); } 138 | uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); } 139 | 140 | method = comma "method" equals cipher:cipher { 141 | proxy.cipher = cipher; 142 | }; 143 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"); 144 | aead = comma "aead" equals flag:bool { proxy.aead = flag; } 145 | 146 | udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } 147 | udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } 148 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 149 | 150 | over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } 151 | tls_host = comma "tls-host" equals sni:domain { proxy.sni = sni; } 152 | tls_verification = comma "tls-verification" equals flag:bool { 153 | proxy["skip-cert-verify"] = !flag; 154 | } 155 | tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } 156 | 157 | obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } 158 | obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; } 159 | obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; }; 160 | 161 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; } 162 | obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } 163 | 164 | ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } 165 | ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } 166 | 167 | uri = $[^,]+ 168 | 169 | tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } 170 | others = comma [^=,]+ equals [^=,]+ 171 | comma = _ "," _ 172 | equals = _ "=" _ 173 | _ = [ \r\t]* 174 | bool = b:("true"/"false") { return b === "true" } -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/qx.ts: -------------------------------------------------------------------------------- 1 | import * as peggy from 'peggy'; 2 | const grammars = String.raw` 3 | // global initializer 4 | {{ 5 | function $set(obj, path, value) { 6 | if (Object(obj) !== obj) return obj; 7 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 8 | path 9 | .slice(0, -1) 10 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 11 | path[path.length - 1] 12 | ] = value; 13 | return obj; 14 | } 15 | }} 16 | 17 | // per-parse initializer 18 | { 19 | const proxy = {}; 20 | const obfs = {}; 21 | const $ = {}; 22 | 23 | function handleObfs() { 24 | if (obfs.type === "ws" || obfs.type === "wss") { 25 | proxy.network = "ws"; 26 | if (obfs.type === 'wss') { 27 | proxy.tls = true; 28 | } 29 | $set(proxy, "ws-opts.path", obfs.path); 30 | $set(proxy, "ws-opts.headers.Host", obfs.host); 31 | } else if (obfs.type === "over-tls") { 32 | proxy.tls = true; 33 | } else if (obfs.type === "http") { 34 | proxy.network = "http"; 35 | $set(proxy, "http-opts.path", obfs.path); 36 | $set(proxy, "http-opts.headers.Host", obfs.host); 37 | } 38 | } 39 | } 40 | 41 | start = (trojan/shadowsocks/vmess/http/socks5) { 42 | return proxy 43 | } 44 | 45 | trojan = "trojan" equals address 46 | (password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { 47 | proxy.type = "trojan"; 48 | handleObfs(); 49 | } 50 | 51 | shadowsocks = "shadowsocks" equals address 52 | (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* { 53 | if (proxy.protocol) { 54 | proxy.type = "ssr"; 55 | // handle ssr obfs 56 | if (obfs.host) proxy["obfs-param"] = obfs.host; 57 | if (obfs.type) proxy.obfs = obfs.type; 58 | } else { 59 | proxy.type = "ss"; 60 | // handle ss obfs 61 | if (obfs.type == "http" || obfs.type === "tls") { 62 | proxy.plugin = "obfs"; 63 | $set(proxy, "plugin-opts", { 64 | mode: obfs.type 65 | }); 66 | } else if (obfs.type === "ws" || obfs.type === "wss") { 67 | proxy.plugin = "v2ray-plugin"; 68 | $set(proxy, "plugin-opts.mode", "websocket"); 69 | if (obfs.type === "wss") { 70 | $set(proxy, "plugin-opts.tls", true); 71 | } 72 | } else if (obfs.type === 'over-tls') { 73 | throw new Error('ss over-tls is not supported'); 74 | } 75 | if (obfs.type) { 76 | $set(proxy, "plugin-opts.host", obfs.host); 77 | $set(proxy, "plugin-opts.path", obfs.path); 78 | } 79 | } 80 | } 81 | 82 | vmess = "vmess" equals address 83 | (uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* { 84 | proxy.type = "vmess"; 85 | proxy.cipher = proxy.cipher || "none"; 86 | if (proxy.aead) { 87 | proxy.alterId = 0; 88 | } else { 89 | proxy.alterId = proxy.alterId || 0; 90 | } 91 | handleObfs(); 92 | } 93 | 94 | http = "http" equals address 95 | (username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ 96 | proxy.type = "http"; 97 | } 98 | 99 | socks5 = "socks5" equals address 100 | (username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { 101 | proxy.type = "socks5"; 102 | } 103 | 104 | address = server:server ":" port:port { 105 | proxy.server = server; 106 | proxy.port = port; 107 | } 108 | server = ip/domain 109 | 110 | domain = match:[0-9a-zA-z-_.]+ { 111 | const domain = match.join(""); 112 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 113 | return domain; 114 | } 115 | } 116 | 117 | ip = & { 118 | const start = peg$currPos; 119 | let end; 120 | let j = start; 121 | while (j < input.length) { 122 | if (input[j] === ",") break; 123 | if (input[j] === ":") end = j; 124 | j++; 125 | } 126 | peg$currPos = end || j; 127 | $.ip = input.substring(start, end).trim(); 128 | return true; 129 | } { return $.ip; } 130 | 131 | port = digits:[0-9]+ { 132 | const port = parseInt(digits.join(""), 10); 133 | if (port >= 0 && port <= 65535) { 134 | return port; 135 | } 136 | } 137 | 138 | username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); } 139 | password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); } 140 | uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); } 141 | 142 | method = comma "method" equals cipher:cipher { 143 | proxy.cipher = cipher; 144 | }; 145 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"); 146 | aead = comma "aead" equals flag:bool { proxy.aead = flag; } 147 | 148 | udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } 149 | udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } 150 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 151 | 152 | over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } 153 | tls_host = comma "tls-host" equals sni:domain { proxy.sni = sni; } 154 | tls_verification = comma "tls-verification" equals flag:bool { 155 | proxy["skip-cert-verify"] = !flag; 156 | } 157 | tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } 158 | 159 | obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } 160 | obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; } 161 | obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; }; 162 | 163 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; } 164 | obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } 165 | 166 | ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } 167 | ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } 168 | 169 | uri = $[^,]+ 170 | 171 | tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } 172 | others = comma [^=,]+ equals [^=,]+ 173 | comma = _ "," _ 174 | equals = _ "=" _ 175 | _ = [ \r\t]* 176 | bool = b:("true"/"false") { return b === "true" } 177 | `; 178 | let parser; 179 | export default function getParser() { 180 | if (!parser) { 181 | parser = peggy.generate(grammars); 182 | } 183 | return parser; 184 | } 185 | -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/surge.peg: -------------------------------------------------------------------------------- 1 | // global initializer 2 | {{ 3 | function $set(obj, path, value) { 4 | if (Object(obj) !== obj) return obj; 5 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 6 | path 7 | .slice(0, -1) 8 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 9 | path[path.length - 1] 10 | ] = value; 11 | return obj; 12 | } 13 | }} 14 | 15 | // per-parser initializer 16 | { 17 | const proxy = {}; 18 | const obfs = {}; 19 | const $ = {}; 20 | 21 | function handleWebsocket() { 22 | if (obfs.type === "ws") { 23 | proxy.network = "ws"; 24 | $set(proxy, "ws-opts.path", obfs.path); 25 | $set(proxy, "ws-opts.headers", obfs['ws-headers']); 26 | } 27 | } 28 | } 29 | 30 | start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) { 31 | return proxy; 32 | } 33 | 34 | shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 35 | proxy.type = "ss"; 36 | // handle obfs 37 | if (obfs.type == "http" || obfs.type === "tls") { 38 | proxy.plugin = "obfs"; 39 | $set(proxy, "plugin-opts.mode", obfs.type); 40 | $set(proxy, "plugin-opts.host", obfs.host); 41 | $set(proxy, "plugin-opts.path", obfs.path); 42 | } 43 | } 44 | vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { 45 | proxy.type = "vmess"; 46 | proxy.cipher = proxy.cipher || "none"; 47 | if (proxy.aead) { 48 | proxy.alterId = 0; 49 | } else { 50 | proxy.alterId = proxy.alterId || 0; 51 | } 52 | handleWebsocket(); 53 | } 54 | trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { 55 | proxy.type = "trojan"; 56 | handleWebsocket(); 57 | } 58 | https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { 59 | proxy.type = "http"; 60 | proxy.tls = true; 61 | } 62 | http = tag equals "http" address (username password)? (fast_open/others)* { 63 | proxy.type = "http"; 64 | } 65 | snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 66 | proxy.type = "snell"; 67 | // handle obfs 68 | if (obfs.type == "http" || obfs.type === "tls") { 69 | $set(proxy, "obfs-opts.mode", obfs.type); 70 | $set(proxy, "obfs-opts.host", obfs.host); 71 | $set(proxy, "obfs-opts.path", obfs.path); 72 | } 73 | } 74 | socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { 75 | proxy.type = "socks5"; 76 | } 77 | socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { 78 | proxy.type = "socks5"; 79 | proxy.tls = true; 80 | } 81 | 82 | address = comma server:server comma port:port { 83 | proxy.server = server; 84 | proxy.port = port; 85 | } 86 | 87 | server = ip/domain 88 | 89 | ip = & { 90 | const start = peg$currPos; 91 | let j = start; 92 | while (j < input.length) { 93 | if (input[j] === ",") break; 94 | j++; 95 | } 96 | peg$currPos = j; 97 | $.ip = input.substring(start, j).trim(); 98 | return true; 99 | } { return $.ip; } 100 | 101 | domain = match:[0-9a-zA-z-_.]+ { 102 | const domain = match.join(""); 103 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 104 | return domain; 105 | } 106 | } 107 | 108 | port = digits:[0-9]+ { 109 | const port = parseInt(digits.join(""), 10); 110 | if (port >= 0 && port <= 65535) { 111 | return port; 112 | } 113 | } 114 | 115 | username = & { 116 | let j = peg$currPos; 117 | let start, end; 118 | let first = true; 119 | while (j < input.length) { 120 | if (input[j] === ',') { 121 | if (first) { 122 | start = j + 1; 123 | first = false; 124 | } else { 125 | end = j; 126 | break; 127 | } 128 | } 129 | j++; 130 | } 131 | const match = input.substring(start, end); 132 | if (match.indexOf("=") === -1) { 133 | $.username = match; 134 | peg$currPos = end; 135 | return true; 136 | } 137 | } { proxy.username = $.username; } 138 | password = comma match:[^,]+ { proxy.password = match.join(""); } 139 | 140 | tls = comma "tls" equals flag:bool { proxy.tls = flag; } 141 | sni = comma "sni" equals sni:domain { proxy.sni = sni; } 142 | tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } 143 | tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } 144 | 145 | snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } 146 | snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } 147 | 148 | passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } 149 | vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } 150 | vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } 151 | 152 | method = comma "encrypt-method" equals cipher:cipher { 153 | proxy.cipher = cipher; 154 | } 155 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"); 156 | 157 | ws = comma "ws" equals flag:bool { obfs.type = "ws"; } 158 | ws_headers = comma "ws-headers" equals headers:$[^,]+ { 159 | const pairs = headers.split("|"); 160 | const result = {}; 161 | pairs.forEach(pair => { 162 | const [key, value] = pair.trim().split(":"); 163 | result[key.trim()] = value.trim(); 164 | }) 165 | obfs["ws-headers"] = result; 166 | } 167 | ws_path = comma "ws-path" equals path:uri { obfs.path = path; } 168 | 169 | obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; } 170 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }; 171 | obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path } 172 | uri = $[^,]+ 173 | 174 | udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } 175 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 176 | 177 | tag = match:[^=,]* { proxy.name = match.join("").trim(); } 178 | comma = _ "," _ 179 | equals = _ "=" _ 180 | _ = [ \r\t]* 181 | bool = b:("true"/"false") { return b === "true" } 182 | others = comma [^=,]+ equals [^=,]+ -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/surge.ts: -------------------------------------------------------------------------------- 1 | import * as peggy from 'peggy'; 2 | const grammars = String.raw` 3 | // global initializer 4 | {{ 5 | function $set(obj, path, value) { 6 | if (Object(obj) !== obj) return obj; 7 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 8 | path 9 | .slice(0, -1) 10 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 11 | path[path.length - 1] 12 | ] = value; 13 | return obj; 14 | } 15 | }} 16 | 17 | // per-parser initializer 18 | { 19 | const proxy = {}; 20 | const obfs = {}; 21 | const $ = {}; 22 | 23 | function handleWebsocket() { 24 | if (obfs.type === "ws") { 25 | proxy.network = "ws"; 26 | $set(proxy, "ws-opts.path", obfs.path); 27 | $set(proxy, "ws-opts.headers", obfs['ws-headers']); 28 | } 29 | } 30 | } 31 | 32 | start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) { 33 | return proxy; 34 | } 35 | 36 | shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 37 | proxy.type = "ss"; 38 | // handle obfs 39 | if (obfs.type == "http" || obfs.type === "tls") { 40 | proxy.plugin = "obfs"; 41 | $set(proxy, "plugin-opts.mode", obfs.type); 42 | $set(proxy, "plugin-opts.host", obfs.host); 43 | $set(proxy, "plugin-opts.path", obfs.path); 44 | } 45 | } 46 | vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { 47 | proxy.type = "vmess"; 48 | proxy.cipher = proxy.cipher || "none"; 49 | if (proxy.aead) { 50 | proxy.alterId = 0; 51 | } else { 52 | proxy.alterId = proxy.alterId || 0; 53 | } 54 | handleWebsocket(); 55 | } 56 | trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { 57 | proxy.type = "trojan"; 58 | handleWebsocket(); 59 | } 60 | https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { 61 | proxy.type = "http"; 62 | proxy.tls = true; 63 | } 64 | http = tag equals "http" address (username password)? (fast_open/others)* { 65 | proxy.type = "http"; 66 | } 67 | snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { 68 | proxy.type = "snell"; 69 | // handle obfs 70 | if (obfs.type == "http" || obfs.type === "tls") { 71 | $set(proxy, "obfs-opts.mode", obfs.type); 72 | $set(proxy, "obfs-opts.host", obfs.host); 73 | $set(proxy, "obfs-opts.path", obfs.path); 74 | } 75 | } 76 | socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { 77 | proxy.type = "socks5"; 78 | } 79 | socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { 80 | proxy.type = "socks5"; 81 | proxy.tls = true; 82 | } 83 | 84 | address = comma server:server comma port:port { 85 | proxy.server = server; 86 | proxy.port = port; 87 | } 88 | 89 | server = ip/domain 90 | 91 | ip = & { 92 | const start = peg$currPos; 93 | let j = start; 94 | while (j < input.length) { 95 | if (input[j] === ",") break; 96 | j++; 97 | } 98 | peg$currPos = j; 99 | $.ip = input.substring(start, j).trim(); 100 | return true; 101 | } { return $.ip; } 102 | 103 | domain = match:[0-9a-zA-z-_.]+ { 104 | const domain = match.join(""); 105 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 106 | return domain; 107 | } 108 | } 109 | 110 | port = digits:[0-9]+ { 111 | const port = parseInt(digits.join(""), 10); 112 | if (port >= 0 && port <= 65535) { 113 | return port; 114 | } 115 | } 116 | 117 | username = & { 118 | let j = peg$currPos; 119 | let start, end; 120 | let first = true; 121 | while (j < input.length) { 122 | if (input[j] === ',') { 123 | if (first) { 124 | start = j + 1; 125 | first = false; 126 | } else { 127 | end = j; 128 | break; 129 | } 130 | } 131 | j++; 132 | } 133 | const match = input.substring(start, end); 134 | if (match.indexOf("=") === -1) { 135 | $.username = match; 136 | peg$currPos = end; 137 | return true; 138 | } 139 | } { proxy.username = $.username; } 140 | password = comma match:[^,]+ { proxy.password = match.join(""); } 141 | 142 | tls = comma "tls" equals flag:bool { proxy.tls = flag; } 143 | sni = comma "sni" equals sni:domain { proxy.sni = sni; } 144 | tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } 145 | tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } 146 | 147 | snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } 148 | snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } 149 | 150 | passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } 151 | vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } 152 | vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } 153 | 154 | method = comma "encrypt-method" equals cipher:cipher { 155 | proxy.cipher = cipher; 156 | } 157 | cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"); 158 | 159 | ws = comma "ws" equals flag:bool { obfs.type = "ws"; } 160 | ws_headers = comma "ws-headers" equals headers:$[^,]+ { 161 | const pairs = headers.split("|"); 162 | const result = {}; 163 | pairs.forEach(pair => { 164 | const [key, value] = pair.trim().split(":"); 165 | result[key.trim()] = value.trim(); 166 | }) 167 | obfs["ws-headers"] = result; 168 | } 169 | ws_path = comma "ws-path" equals path:uri { obfs.path = path; } 170 | 171 | obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; } 172 | obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }; 173 | obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path } 174 | uri = $[^,]+ 175 | 176 | udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } 177 | fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 178 | 179 | tag = match:[^=,]* { proxy.name = match.join("").trim(); } 180 | comma = _ "," _ 181 | equals = _ "=" _ 182 | _ = [ \r\t]* 183 | bool = b:("true"/"false") { return b === "true" } 184 | others = comma [^=,]+ equals [^=,]+ 185 | `; 186 | let parser; 187 | export default function getParser() { 188 | if (!parser) { 189 | parser = peggy.generate(grammars); 190 | } 191 | return parser; 192 | } 193 | -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/trojan-uri.peg: -------------------------------------------------------------------------------- 1 | // global initializer 2 | {{ 3 | function $set(obj, path, value) { 4 | if (Object(obj) !== obj) return obj; 5 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 6 | path 7 | .slice(0, -1) 8 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 9 | path[path.length - 1] 10 | ] = value; 11 | return obj; 12 | } 13 | 14 | function toBool(str) { 15 | if (typeof str === 'undefined' || str === null) return undefined; 16 | return /(TRUE)|1/i.test(str); 17 | } 18 | }} 19 | 20 | { 21 | const proxy = {}; 22 | const obfs = {}; 23 | const $ = {}; 24 | const params = {}; 25 | } 26 | 27 | start = (trojan) { 28 | return proxy 29 | } 30 | 31 | trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{ 32 | proxy.type = "trojan"; 33 | proxy.password = password; 34 | proxy.server = server; 35 | proxy.port = port; 36 | proxy.name = name; 37 | 38 | // name may be empty 39 | if (!proxy.name) { 40 | proxy.name = server + ":" + port; 41 | } 42 | }; 43 | 44 | password = match:$[^@]+ { 45 | return decodeURIComponent(match); 46 | }; 47 | 48 | server = ip/domain; 49 | 50 | domain = match:[0-9a-zA-z-_.]+ { 51 | const domain = match.join(""); 52 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 53 | return domain; 54 | } 55 | } 56 | 57 | ip = & { 58 | const start = peg$currPos; 59 | let end; 60 | let j = start; 61 | while (j < input.length) { 62 | if (input[j] === ",") break; 63 | if (input[j] === ":") end = j; 64 | j++; 65 | } 66 | peg$currPos = end || j; 67 | $.ip = input.substring(start, end).trim(); 68 | return true; 69 | } { return $.ip; } 70 | 71 | port = digits:[0-9]+ { 72 | const port = parseInt(digits.join(""), 10); 73 | if (port >= 0 && port <= 65535) { 74 | return port; 75 | } else { 76 | throw new Error("Invalid port: " + port); 77 | } 78 | } 79 | 80 | params = "?" head:param tail:("&"@param)* { 81 | proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); 82 | proxy.sni = params["sni"] || params["peer"]; 83 | 84 | if (toBool(params["ws"])) { 85 | proxy.network = "ws"; 86 | $set(proxy, "ws-opts.path", params["wspath"]); 87 | } 88 | 89 | proxy.udp = toBool(params["udp"]); 90 | proxy.tfo = toBool(params["tfo"]); 91 | } 92 | 93 | param = kv/single; 94 | 95 | kv = key:$[a-z]i+ "=" value:$[^&#]i+ { 96 | params[key] = value; 97 | } 98 | 99 | single = key:$[a-z]i+ { 100 | params[key] = true; 101 | }; 102 | 103 | name = "#" + match:$.* { 104 | return decodeURIComponent(match); 105 | } -------------------------------------------------------------------------------- /src/core/proxy-utils/parsers/peggy/trojan-uri.ts: -------------------------------------------------------------------------------- 1 | import * as peggy from 'peggy'; 2 | const grammars = String.raw` 3 | // global initializer 4 | {{ 5 | function $set(obj, path, value) { 6 | if (Object(obj) !== obj) return obj; 7 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 8 | path 9 | .slice(0, -1) 10 | .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ 11 | path[path.length - 1] 12 | ] = value; 13 | return obj; 14 | } 15 | 16 | function toBool(str) { 17 | if (typeof str === 'undefined' || str === null) return undefined; 18 | return /(TRUE)|1/i.test(str); 19 | } 20 | }} 21 | 22 | { 23 | const proxy = {}; 24 | const obfs = {}; 25 | const $ = {}; 26 | const params = {}; 27 | } 28 | 29 | start = (trojan) { 30 | return proxy 31 | } 32 | 33 | trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{ 34 | proxy.type = "trojan"; 35 | proxy.password = password; 36 | proxy.server = server; 37 | proxy.port = port; 38 | proxy.name = name; 39 | 40 | // name may be empty 41 | if (!proxy.name) { 42 | proxy.name = server + ":" + port; 43 | } 44 | }; 45 | 46 | password = match:$[^@]+ { 47 | return decodeURIComponent(match); 48 | }; 49 | 50 | server = ip/domain; 51 | 52 | domain = match:[0-9a-zA-z-_.]+ { 53 | const domain = match.join(""); 54 | if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { 55 | return domain; 56 | } 57 | } 58 | 59 | ip = & { 60 | const start = peg$currPos; 61 | let end; 62 | let j = start; 63 | while (j < input.length) { 64 | if (input[j] === ",") break; 65 | if (input[j] === ":") end = j; 66 | j++; 67 | } 68 | peg$currPos = end || j; 69 | $.ip = input.substring(start, end).trim(); 70 | return true; 71 | } { return $.ip; } 72 | 73 | port = digits:[0-9]+ { 74 | const port = parseInt(digits.join(""), 10); 75 | if (port >= 0 && port <= 65535) { 76 | return port; 77 | } else { 78 | throw new Error("Invalid port: " + port); 79 | } 80 | } 81 | 82 | params = "?" head:param tail:("&"@param)* { 83 | proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); 84 | proxy.sni = params["sni"] || params["peer"]; 85 | 86 | if (toBool(params["ws"])) { 87 | proxy.network = "ws"; 88 | $set(proxy, "ws-opts.path", params["wspath"]); 89 | } 90 | 91 | proxy.udp = toBool(params["udp"]); 92 | proxy.tfo = toBool(params["tfo"]); 93 | } 94 | 95 | param = kv/single; 96 | 97 | kv = key:$[a-z]i+ "=" value:$[^&#]i+ { 98 | params[key] = value; 99 | } 100 | 101 | single = key:$[a-z]i+ { 102 | params[key] = true; 103 | }; 104 | 105 | name = "#" + match:$.* { 106 | return decodeURIComponent(match); 107 | } 108 | `; 109 | let parser; 110 | export default function getParser() { 111 | if (!parser) { 112 | parser = peggy.generate(grammars); 113 | } 114 | return parser; 115 | } 116 | -------------------------------------------------------------------------------- /src/core/proxy-utils/preprocessors/index.ts: -------------------------------------------------------------------------------- 1 | import { safeLoad } from 'static-js-yaml'; 2 | import { Base64 } from 'js-base64'; 3 | 4 | function HTML() { 5 | const name = 'HTML'; 6 | const test = (raw) => /^/.test(raw); 7 | // simply discard HTML 8 | const parse = () => ''; 9 | return { name, test, parse }; 10 | } 11 | 12 | function Base64Encoded() { 13 | const name = 'Base64 Pre-processor'; 14 | 15 | const keys = [ 16 | 'dm1lc3M', 17 | 'c3NyOi8v', 18 | 'dHJvamFu', 19 | 'c3M6Ly', 20 | 'c3NkOi8v', 21 | 'c2hhZG93', 22 | 'aHR0c', 23 | ]; 24 | 25 | const test = function (raw) { 26 | return keys.some((k) => raw.indexOf(k) !== -1); 27 | }; 28 | const parse = function (raw) { 29 | raw = Base64.decode(raw); 30 | return raw; 31 | }; 32 | return { name, test, parse }; 33 | } 34 | 35 | function Clash() { 36 | const name = 'Clash Pre-processor'; 37 | const test = function (raw) { 38 | return /proxies/.test(raw); 39 | }; 40 | const parse = function (raw) { 41 | // Clash YAML format 42 | const proxies = safeLoad(raw).proxies; 43 | return proxies.map((p) => JSON.stringify(p)).join('\n'); 44 | }; 45 | return { name, test, parse }; 46 | } 47 | 48 | function SSD() { 49 | const name = 'SSD Pre-processor'; 50 | const test = function (raw) { 51 | return raw.indexOf('ssd://') === 0; 52 | }; 53 | const parse = function (raw) { 54 | // preprocessing for SSD subscription format 55 | const output = []; 56 | let ssdinfo = JSON.parse(Base64.decode(raw.split('ssd://')[1])); 57 | let port = ssdinfo.port; 58 | let method = ssdinfo.encryption; 59 | let password = ssdinfo.password; 60 | // servers config 61 | let servers = ssdinfo.servers; 62 | for (let i = 0; i < servers.length; i++) { 63 | let server = servers[i]; 64 | method = server.encryption ? server.encryption : method; 65 | password = server.password ? server.password : password; 66 | let userinfo = Base64.encode(method + ':' + password); 67 | let hostname = server.server; 68 | port = server.port ? server.port : port; 69 | let tag = server.remarks ? server.remarks : i; 70 | let plugin = server.plugin_options 71 | ? '/?plugin=' + 72 | encodeURIComponent( 73 | server.plugin + ';' + server.plugin_options, 74 | ) 75 | : ''; 76 | output[i] = 77 | 'ss://' + 78 | userinfo + 79 | '@' + 80 | hostname + 81 | ':' + 82 | port + 83 | plugin + 84 | '#' + 85 | tag; 86 | } 87 | return output.join('\n'); 88 | }; 89 | return { name, test, parse }; 90 | } 91 | 92 | function FullConfig() { 93 | const name = 'Full Config Preprocessor'; 94 | const test = function (raw) { 95 | return /^(\[server_local\]|\[Proxy\])/gm.test(raw); 96 | }; 97 | const parse = function (raw) { 98 | const regex = /^\[server_local]|\[Proxy]/gm; 99 | const match = regex.exec(raw); 100 | const results = []; 101 | 102 | let first = true; 103 | if (match) { 104 | raw = raw.substring(match.index); 105 | for (const line of raw.split('\n')) { 106 | if (!first && !line.test(/^\s*\[/)) { 107 | results.push(line); 108 | } 109 | // skip the first line 110 | first = false; 111 | } 112 | return results.join('\n'); 113 | } 114 | }; 115 | return { name, test, parse }; 116 | } 117 | 118 | export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()]; 119 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/clash.ts: -------------------------------------------------------------------------------- 1 | import { isPresent } from '../../../core/proxy-utils/producers/utils'; 2 | 3 | export default function Clash_Producer() { 4 | const type = 'ALL'; 5 | const produce = (proxies) => { 6 | // filter unsupported proxies 7 | proxies = proxies.filter((proxy) => 8 | ['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes( 9 | proxy.type, 10 | ), 11 | ); 12 | return ( 13 | 'proxies:\n' + 14 | proxies 15 | .map((proxy) => { 16 | if (proxy.type === 'vmess') { 17 | // handle vmess aead 18 | if (isPresent(proxy, 'aead')) { 19 | if (proxy.aead) { 20 | proxy.alterId = 0; 21 | } 22 | delete proxy.aead; 23 | } 24 | if (isPresent(proxy, 'sni')) { 25 | proxy.servername = proxy.sni; 26 | delete proxy.sni; 27 | } 28 | } 29 | 30 | delete proxy['tls-fingerprint']; 31 | return ' - ' + JSON.stringify(proxy) + '\n'; 32 | }) 33 | .join('') 34 | ); 35 | }; 36 | return { type, produce }; 37 | } 38 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/index.ts: -------------------------------------------------------------------------------- 1 | import Surge_Producer from './surge'; 2 | import Clash_Producer from './clash'; 3 | import Stash_Producer from './stash'; 4 | import Loon_Producer from './loon'; 5 | import URI_Producer from './uri'; 6 | import QX_Producer from './qx'; 7 | 8 | function JSON_Producer() { 9 | const type = 'ALL'; 10 | const produce = (proxies) => JSON.stringify(proxies, null, 2); 11 | return { type, produce }; 12 | } 13 | 14 | export default { 15 | QX: QX_Producer(), 16 | Surge: Surge_Producer(), 17 | Loon: Loon_Producer(), 18 | Clash: Clash_Producer(), 19 | URI: URI_Producer(), 20 | JSON: JSON_Producer(), 21 | Stash: Stash_Producer(), 22 | }; 23 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/loon.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | const targetPlatform = 'Loon'; 3 | import { isPresent, Result } from './utils'; 4 | 5 | export default function Loon_Producer() { 6 | const produce = (proxy) => { 7 | switch (proxy.type) { 8 | case 'ss': 9 | return shadowsocks(proxy); 10 | case 'ssr': 11 | return shadowsocksr(proxy); 12 | case 'trojan': 13 | return trojan(proxy); 14 | case 'vmess': 15 | return vmess(proxy); 16 | case 'vless': 17 | return vless(proxy); 18 | case 'http': 19 | return http(proxy); 20 | } 21 | throw new Error( 22 | `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, 23 | ); 24 | }; 25 | return { produce }; 26 | } 27 | 28 | function shadowsocks(proxy) { 29 | const result = new Result(proxy); 30 | result.append( 31 | `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`, 32 | ); 33 | 34 | // obfs 35 | if (isPresent(proxy, 'plugin')) { 36 | if (proxy.plugin === 'obfs') { 37 | result.append(`,obfs-name=${proxy['plugin-opts'].mode}`); 38 | result.appendIfPresent( 39 | `,obfs-host=${proxy['plugin-opts'].host}`, 40 | 'plugin-opts.host', 41 | ); 42 | result.appendIfPresent( 43 | `,obfs-uri=${proxy['plugin-opts'].path}`, 44 | 'plugin-opts.path', 45 | ); 46 | } else { 47 | throw new Error(`plugin ${proxy.plugin} is not supported`); 48 | } 49 | } 50 | 51 | // tfo 52 | result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 53 | 54 | // udp 55 | result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); 56 | 57 | return result.toString(); 58 | } 59 | 60 | function shadowsocksr(proxy) { 61 | const result = new Result(proxy); 62 | result.append( 63 | `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`, 64 | ); 65 | 66 | // ssr protocol 67 | result.append(`,protocol=${proxy.protocol}`); 68 | result.appendIfPresent( 69 | `,protocol-param=${proxy['protocol-param']}`, 70 | 'protocol-param', 71 | ); 72 | 73 | // obfs 74 | result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs'); 75 | result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param'); 76 | 77 | // tfo 78 | result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 79 | 80 | // udp 81 | result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); 82 | 83 | return result.toString(); 84 | } 85 | 86 | function trojan(proxy) { 87 | const result = new Result(proxy); 88 | result.append( 89 | `${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`, 90 | ); 91 | 92 | // transport 93 | if (isPresent(proxy, 'network')) { 94 | if (proxy.network === 'ws') { 95 | result.append(`,transport=ws`); 96 | result.appendIfPresent( 97 | `,path=${proxy['ws-opts'].path}`, 98 | 'ws-opts.path', 99 | ); 100 | result.appendIfPresent( 101 | `,host=${proxy['ws-opts'].headers.Host}`, 102 | 'ws-opts.headers.Host', 103 | ); 104 | } else { 105 | throw new Error(`network ${proxy.network} is unsupported`); 106 | } 107 | } 108 | 109 | // tls verification 110 | result.appendIfPresent( 111 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 112 | 'skip-cert-verify', 113 | ); 114 | 115 | // sni 116 | result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); 117 | 118 | // tfo 119 | result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 120 | 121 | // udp 122 | result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); 123 | 124 | return result.toString(); 125 | } 126 | 127 | function vmess(proxy) { 128 | const result = new Result(proxy); 129 | result.append( 130 | `${proxy.name}=vmess,${proxy.server},${proxy.port},${ 131 | proxy.cipher === 'auto' ? 'none' : proxy.cipher 132 | },"${proxy.uuid}"`, 133 | ); 134 | 135 | // transport 136 | if (isPresent(proxy, 'network')) { 137 | if (proxy.network === 'ws') { 138 | result.append(`,transport=ws`); 139 | result.appendIfPresent( 140 | `,path=${proxy['ws-opts'].path}`, 141 | 'ws-opts.path', 142 | ); 143 | result.appendIfPresent( 144 | `,host=${proxy['ws-opts'].headers.Host}`, 145 | 'ws-opts.headers.Host', 146 | ); 147 | } else if (proxy.network === 'http') { 148 | result.append(`,transport=http`); 149 | result.appendIfPresent( 150 | `,path=${proxy['http-opts'].path}`, 151 | 'http-opts.path', 152 | ); 153 | result.appendIfPresent( 154 | `,host=${proxy['http-opts'].headers.Host}`, 155 | 'http-opts.headers.Host', 156 | ); 157 | } else { 158 | throw new Error(`network ${proxy.network} is unsupported`); 159 | } 160 | } else { 161 | result.append(`,transport=tcp`); 162 | } 163 | 164 | // tls 165 | result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); 166 | 167 | // tls verification 168 | result.appendIfPresent( 169 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 170 | 'skip-cert-verify', 171 | ); 172 | 173 | // sni 174 | result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); 175 | 176 | // AEAD 177 | if (isPresent(proxy, 'aead')) { 178 | result.append(`,alterId=0`); 179 | } else { 180 | result.append(`,alterId=${proxy.alterId}`); 181 | } 182 | 183 | // tfo 184 | result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 185 | 186 | // udp 187 | result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); 188 | return result.toString(); 189 | } 190 | 191 | function vless(proxy) { 192 | const result = new Result(proxy); 193 | result.append( 194 | `${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`, 195 | ); 196 | 197 | // transport 198 | if (isPresent(proxy, 'network')) { 199 | if (proxy.network === 'ws') { 200 | result.append(`,transport=ws`); 201 | result.appendIfPresent( 202 | `,path=${proxy['ws-opts'].path}`, 203 | 'ws-opts.path', 204 | ); 205 | result.appendIfPresent( 206 | `,host=${proxy['ws-opts'].headers.Host}`, 207 | 'ws-opts.headers.Host', 208 | ); 209 | } else if (proxy.network === 'http') { 210 | result.append(`,transport=http`); 211 | result.appendIfPresent( 212 | `,path=${proxy['http-opts'].path}`, 213 | 'http-opts.path', 214 | ); 215 | result.appendIfPresent( 216 | `,host=${proxy['http-opts'].headers.Host}`, 217 | 'http-opts.headers.Host', 218 | ); 219 | } else { 220 | throw new Error(`network ${proxy.network} is unsupported`); 221 | } 222 | } else { 223 | result.append(`,transport=tcp`); 224 | } 225 | 226 | // tls 227 | result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); 228 | 229 | // tls verification 230 | result.appendIfPresent( 231 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 232 | 'skip-cert-verify', 233 | ); 234 | 235 | // sni 236 | result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); 237 | 238 | // tfo 239 | result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 240 | 241 | // udp 242 | result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); 243 | return result.toString(); 244 | } 245 | 246 | function http(proxy) { 247 | const result = new Result(proxy); 248 | const type = proxy.tls ? 'https' : 'http'; 249 | result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); 250 | result.appendIfPresent(`,${proxy.username}`, 'username'); 251 | result.appendIfPresent(`,"${proxy.password}"`, 'password'); 252 | 253 | // sni 254 | result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); 255 | 256 | // tls verification 257 | result.appendIfPresent( 258 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 259 | 'skip-cert-verify', 260 | ); 261 | 262 | // tfo 263 | result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); 264 | 265 | // udp 266 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 267 | return result.toString(); 268 | } 269 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/qx.ts: -------------------------------------------------------------------------------- 1 | import { isPresent, Result } from './utils'; 2 | 3 | const targetPlatform = 'QX'; 4 | 5 | export default function QX_Producer() { 6 | const produce = (proxy) => { 7 | switch (proxy.type) { 8 | case 'ss': 9 | return shadowsocks(proxy); 10 | case 'ssr': 11 | return shadowsocksr(proxy); 12 | case 'trojan': 13 | return trojan(proxy); 14 | case 'vmess': 15 | return vmess(proxy); 16 | case 'http': 17 | return http(proxy); 18 | case 'socks5': 19 | return socks5(proxy); 20 | } 21 | throw new Error( 22 | `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, 23 | ); 24 | }; 25 | return { produce }; 26 | } 27 | 28 | function shadowsocks(proxy) { 29 | const result = new Result(proxy); 30 | const append = result.append.bind(result); 31 | const appendIfPresent = result.appendIfPresent.bind(result); 32 | 33 | append(`shadowsocks=${proxy.server}:${proxy.port}`); 34 | append(`,method=${proxy.cipher}`); 35 | append(`,password=${proxy.password}`); 36 | 37 | // obfs 38 | if (needTls(proxy)) { 39 | proxy.tls = true; 40 | } 41 | if (isPresent(proxy, 'plugin')) { 42 | if (proxy.plugin === 'obfs') { 43 | const opts = proxy['plugin-opts']; 44 | append(`,obfs=${opts.mode}`); 45 | } else if ( 46 | proxy.plugin === 'v2ray-plugin' && 47 | proxy['plugin-opts'].mode === 'websocket' 48 | ) { 49 | const opts = proxy['plugin-opts']; 50 | if (opts.tls) append(`,obfs=wss`); 51 | else append(`,obfs=ws`); 52 | } else { 53 | throw new Error(`plugin is not supported`); 54 | } 55 | appendIfPresent( 56 | `,obfs-host=${proxy['plugin-opts'].host}`, 57 | 'plugin-opts.host', 58 | ); 59 | appendIfPresent( 60 | `,obfs-uri=${proxy['plugin-opts'].path}`, 61 | 'plugin-opts.path', 62 | ); 63 | } 64 | 65 | // tls fingerprint 66 | appendIfPresent( 67 | `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 68 | 'tls-fingerprint', 69 | ); 70 | 71 | // tls verification 72 | appendIfPresent( 73 | `,tls-verification=${!proxy['skip-cert-verify']}`, 74 | 'skip-cert-verify', 75 | ); 76 | appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); 77 | 78 | // tfo 79 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 80 | 81 | // udp 82 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 83 | 84 | // tag 85 | append(`,tag=${proxy.name}`); 86 | 87 | return result.toString(); 88 | } 89 | 90 | function shadowsocksr(proxy) { 91 | const result = new Result(proxy); 92 | const append = result.append.bind(result); 93 | const appendIfPresent = result.appendIfPresent.bind(result); 94 | 95 | append(`shadowsocks=${proxy.server}:${proxy.port}`); 96 | append(`,method=${proxy.cipher}`); 97 | append(`,password=${proxy.password}`); 98 | 99 | // ssr protocol 100 | append(`,ssr-protocol=${proxy.protocol}`); 101 | appendIfPresent( 102 | `,ssr-protocol-param=${proxy['protocol-param']}`, 103 | 'protocol-param', 104 | ); 105 | 106 | // obfs 107 | appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs'); 108 | appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param'); 109 | 110 | // tfo 111 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 112 | 113 | // udp 114 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 115 | 116 | // tag 117 | append(`,tag=${proxy.name}`); 118 | 119 | return result.toString(); 120 | } 121 | 122 | function trojan(proxy) { 123 | const result = new Result(proxy); 124 | const append = result.append.bind(result); 125 | const appendIfPresent = result.appendIfPresent.bind(result); 126 | 127 | append(`trojan=${proxy.server}:${proxy.port}`); 128 | append(`,password=${proxy.password}`); 129 | 130 | // obfs ws 131 | if (isPresent(proxy, 'network')) { 132 | if (proxy.network === 'ws') { 133 | if (needTls(proxy)) append(`,obfs=wss`); 134 | else append(`,obfs=ws`); 135 | appendIfPresent( 136 | `,obfs-uri=${proxy['ws-opts'].path}`, 137 | 'ws-opts.path', 138 | ); 139 | appendIfPresent( 140 | `,obfs-host=${proxy['ws-opts'].headers.Host}`, 141 | 'ws-opts.headers.Host', 142 | ); 143 | } else { 144 | throw new Error(`network ${proxy.network} is unsupported`); 145 | } 146 | } 147 | 148 | // over tls 149 | if (proxy.network !== 'ws' && needTls(proxy)) { 150 | append(`,over-tls=true`); 151 | } 152 | 153 | // tls fingerprint 154 | appendIfPresent( 155 | `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 156 | 'tls-fingerprint', 157 | ); 158 | 159 | // tls verification 160 | appendIfPresent( 161 | `,tls-verification=${!proxy['skip-cert-verify']}`, 162 | 'skip-cert-verify', 163 | ); 164 | appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); 165 | 166 | // tfo 167 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 168 | 169 | // udp 170 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 171 | 172 | // tag 173 | append(`,tag=${proxy.name}`); 174 | 175 | return result.toString(); 176 | } 177 | 178 | function vmess(proxy) { 179 | const result = new Result(proxy); 180 | const append = result.append.bind(result); 181 | const appendIfPresent = result.appendIfPresent.bind(result); 182 | 183 | append(`vmess=${proxy.server}:${proxy.port}`); 184 | 185 | // cipher 186 | let cipher; 187 | if (proxy.cipher === 'auto') { 188 | cipher = 'chacha20-ietf-poly1305'; 189 | } else { 190 | cipher = proxy.cipher; 191 | } 192 | append(`,method=${cipher}`); 193 | 194 | append(`,password=${proxy.uuid}`); 195 | 196 | // obfs 197 | if (needTls(proxy)) { 198 | proxy.tls = true; 199 | } 200 | if (isPresent(proxy, 'network')) { 201 | if (proxy.network === 'ws') { 202 | if (proxy.tls) append(`,obfs=wss`); 203 | else append(`,obfs=ws`); 204 | } else if (proxy.network === 'http') { 205 | append(`,obfs=http`); 206 | } else { 207 | throw new Error(`network ${proxy.network} is unsupported`); 208 | } 209 | appendIfPresent( 210 | `,obfs-uri=${proxy[`${proxy.network}-opts`].path}`, 211 | `${proxy.network}-opts.path`, 212 | ); 213 | appendIfPresent( 214 | `,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`, 215 | `${proxy.network}-opts.headers.Host`, 216 | ); 217 | } else { 218 | // over-tls 219 | if (proxy.tls) append(`,obfs=over-tls`); 220 | } 221 | 222 | // tls fingerprint 223 | appendIfPresent( 224 | `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 225 | 'tls-fingerprint', 226 | ); 227 | 228 | // tls verification 229 | appendIfPresent( 230 | `,tls-verification=${!proxy['skip-cert-verify']}`, 231 | 'skip-cert-verify', 232 | ); 233 | appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); 234 | 235 | // AEAD 236 | if (isPresent(proxy, 'aead')) { 237 | append(`,aead=${proxy.aead}`); 238 | } else { 239 | append(`,aead=${proxy.alterId === 0}`); 240 | } 241 | 242 | // tfo 243 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 244 | 245 | // udp 246 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 247 | 248 | // tag 249 | append(`,tag=${proxy.name}`); 250 | 251 | return result.toString(); 252 | } 253 | 254 | function http(proxy) { 255 | const result = new Result(proxy); 256 | const append = result.append.bind(result); 257 | const appendIfPresent = result.appendIfPresent.bind(result); 258 | 259 | append(`http=${proxy.server}:${proxy.port}`); 260 | appendIfPresent(`,username=${proxy.username}`, 'username'); 261 | appendIfPresent(`,password=${proxy.password}`, 'password'); 262 | 263 | // tls 264 | if (needTls(proxy)) { 265 | proxy.tls = true; 266 | } 267 | appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); 268 | 269 | // tls fingerprint 270 | appendIfPresent( 271 | `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 272 | 'tls-fingerprint', 273 | ); 274 | 275 | // tls verification 276 | appendIfPresent( 277 | `,tls-verification=${!proxy['skip-cert-verify']}`, 278 | 'skip-cert-verify', 279 | ); 280 | appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); 281 | 282 | // tfo 283 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 284 | 285 | // udp 286 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 287 | 288 | // tag 289 | append(`,tag=${proxy.name}`); 290 | 291 | return result.toString(); 292 | } 293 | 294 | function socks5(proxy) { 295 | const result = new Result(proxy); 296 | const append = result.append.bind(result); 297 | const appendIfPresent = result.appendIfPresent.bind(result); 298 | 299 | append(`socks5=${proxy.server}:${proxy.port}`); 300 | appendIfPresent(`,username=${proxy.username}`, 'username'); 301 | appendIfPresent(`,password=${proxy.password}`, 'password'); 302 | 303 | // tls 304 | if (needTls(proxy)) { 305 | proxy.tls = true; 306 | } 307 | appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); 308 | 309 | // tls fingerprint 310 | appendIfPresent( 311 | `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 312 | 'tls-fingerprint', 313 | ); 314 | 315 | // tls verification 316 | appendIfPresent( 317 | `,tls-verification=${!proxy['skip-cert-verify']}`, 318 | 'skip-cert-verify', 319 | ); 320 | appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); 321 | 322 | // tfo 323 | appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); 324 | 325 | // udp 326 | appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 327 | 328 | // tag 329 | append(`,tag=${proxy.name}`); 330 | 331 | return result.toString(); 332 | } 333 | 334 | function needTls(proxy) { 335 | return ( 336 | proxy.tls || 337 | proxy.sni || 338 | typeof proxy['skip-cert-verify'] !== 'undefined' || 339 | typeof proxy['tls-fingerprint'] !== 'undefined' || 340 | typeof proxy['tls-host'] !== 'undefined' 341 | ); 342 | } 343 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/stash.ts: -------------------------------------------------------------------------------- 1 | import { isPresent } from '../../../core/proxy-utils/producers/utils'; 2 | 3 | export default function Stash_Producer() { 4 | const type = 'ALL'; 5 | const produce = (proxies) => { 6 | return ( 7 | 'proxies:\n' + 8 | proxies 9 | .map((proxy) => { 10 | if (proxy.type === 'vmess') { 11 | // handle vmess aead 12 | if (isPresent(proxy, 'aead')) { 13 | if (proxy.aead) { 14 | proxy.alterId = 0; 15 | } 16 | delete proxy.aead; 17 | } 18 | if (isPresent(proxy, 'sni')) { 19 | proxy.servername = proxy.sni; 20 | delete proxy.sni; 21 | } 22 | } 23 | 24 | delete proxy['tls-fingerprint']; 25 | return ' - ' + JSON.stringify(proxy) + '\n'; 26 | }) 27 | .join('') 28 | ); 29 | }; 30 | return { type, produce }; 31 | } 32 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/surge.ts: -------------------------------------------------------------------------------- 1 | import { Result, isPresent } from './utils'; 2 | import { isNotBlank } from '../../../utils'; 3 | import $ from '../../../core/app'; 4 | 5 | const targetPlatform = 'Surge'; 6 | 7 | export default function Surge_Producer() { 8 | const produce = (proxy) => { 9 | switch (proxy.type) { 10 | case 'ss': 11 | return shadowsocks(proxy); 12 | case 'trojan': 13 | return trojan(proxy); 14 | case 'vmess': 15 | return vmess(proxy); 16 | case 'http': 17 | return http(proxy); 18 | case 'socks5': 19 | return socks5(proxy); 20 | case 'snell': 21 | return snell(proxy); 22 | } 23 | throw new Error( 24 | `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, 25 | ); 26 | }; 27 | return { produce }; 28 | } 29 | 30 | function shadowsocks(proxy) { 31 | const result = new Result(proxy); 32 | result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); 33 | result.append(`,encrypt-method=${proxy.cipher}`); 34 | result.appendIfPresent(`,password=${proxy.password}`, 'password'); 35 | 36 | // obfs 37 | if (isPresent(proxy, 'plugin')) { 38 | if (proxy.plugin === 'obfs') { 39 | result.append(`,obfs=${proxy['plugin-opts'].mode}`); 40 | result.appendIfPresent( 41 | `,obfs-host=${proxy['plugin-opts'].host}`, 42 | 'plugin-opts.host', 43 | ); 44 | result.appendIfPresent( 45 | `,obfs-uri=${proxy['plugin-opts'].path}`, 46 | 'plugin-opts.path', 47 | ); 48 | } else { 49 | throw new Error(`plugin ${proxy.plugin} is not supported`); 50 | } 51 | } 52 | 53 | // tfo 54 | result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); 55 | 56 | // udp 57 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 58 | 59 | // test-url 60 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 61 | 62 | return result.toString(); 63 | } 64 | 65 | function trojan(proxy) { 66 | const result = new Result(proxy); 67 | result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); 68 | result.appendIfPresent(`,password=${proxy.password}`, 'password'); 69 | 70 | // transport 71 | handleTransport(result, proxy); 72 | 73 | // tls 74 | result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); 75 | 76 | // tls fingerprint 77 | result.appendIfPresent( 78 | `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 79 | 'tls-fingerprint', 80 | ); 81 | 82 | // tls verification 83 | result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); 84 | result.appendIfPresent( 85 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 86 | 'skip-cert-verify', 87 | ); 88 | 89 | // tfo 90 | result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); 91 | 92 | // udp 93 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 94 | 95 | // test-url 96 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 97 | 98 | return result.toString(); 99 | } 100 | 101 | function vmess(proxy) { 102 | const result = new Result(proxy); 103 | result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); 104 | result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); 105 | 106 | // transport 107 | handleTransport(result, proxy); 108 | 109 | // AEAD 110 | if (isPresent(proxy, 'aead')) { 111 | result.append(`,vmess-aead=${proxy.aead}`); 112 | } else { 113 | result.append(`,vmess-aead=${proxy.alterId === 0}`); 114 | } 115 | 116 | // tls fingerprint 117 | result.appendIfPresent( 118 | `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 119 | 'tls-fingerprint', 120 | ); 121 | 122 | // tls 123 | result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); 124 | 125 | // tls verification 126 | result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); 127 | result.appendIfPresent( 128 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 129 | 'skip-cert-verify', 130 | ); 131 | 132 | // tfo 133 | result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); 134 | 135 | // udp 136 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 137 | 138 | // test-url 139 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 140 | 141 | return result.toString(); 142 | } 143 | 144 | function http(proxy) { 145 | const result = new Result(proxy); 146 | const type = proxy.tls ? 'https' : 'http'; 147 | result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); 148 | result.appendIfPresent(`,${proxy.username}`, 'username'); 149 | result.appendIfPresent(`,${proxy.password}`, 'password'); 150 | 151 | // tls fingerprint 152 | result.appendIfPresent( 153 | `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 154 | 'tls-fingerprint', 155 | ); 156 | 157 | // tls verification 158 | result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); 159 | result.appendIfPresent( 160 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 161 | 'skip-cert-verify', 162 | ); 163 | 164 | // tfo 165 | result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); 166 | 167 | // udp 168 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 169 | 170 | // test-url 171 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 172 | 173 | return result.toString(); 174 | } 175 | 176 | function socks5(proxy) { 177 | const result = new Result(proxy); 178 | const type = proxy.tls ? 'socks5-tls' : 'socks5'; 179 | result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); 180 | result.appendIfPresent(`,${proxy.username}`, 'username'); 181 | result.appendIfPresent(`,${proxy.password}`, 'password'); 182 | 183 | // tls fingerprint 184 | result.appendIfPresent( 185 | `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 186 | 'tls-fingerprint', 187 | ); 188 | 189 | // tls verification 190 | result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); 191 | result.appendIfPresent( 192 | `,skip-cert-verify=${proxy['skip-cert-verify']}`, 193 | 'skip-cert-verify', 194 | ); 195 | 196 | // tfo 197 | if (proxy.tfo) { 198 | $.info(`Option tfo is not supported by Surge, thus omitted`); 199 | } 200 | 201 | // udp 202 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 203 | 204 | // test-url 205 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 206 | 207 | return result.toString(); 208 | } 209 | 210 | function snell(proxy) { 211 | const result = new Result(proxy); 212 | result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); 213 | result.appendIfPresent(`,version=${proxy.version}`, 'version'); 214 | result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); 215 | 216 | // obfs 217 | result.appendIfPresent( 218 | `,obfs=${proxy['obfs-opts'].mode}`, 219 | 'obfs-opts.mode', 220 | ); 221 | result.appendIfPresent( 222 | `,obfs-host=${proxy['obfs-opts'].host}`, 223 | 'obfs-opts.host', 224 | ); 225 | result.appendIfPresent( 226 | `,obfs-uri=${proxy['obfs-opts'].path}`, 227 | 'obfs-opts.path', 228 | ); 229 | 230 | // udp 231 | result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); 232 | 233 | // test-url 234 | result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); 235 | 236 | return result.toString(); 237 | } 238 | 239 | function handleTransport(result, proxy) { 240 | if (isPresent(proxy, 'network')) { 241 | if (proxy.network === 'ws') { 242 | result.append(`,ws=true`); 243 | if (isPresent(proxy, 'ws-opts')) { 244 | result.appendIfPresent( 245 | `,ws-path=${proxy['ws-opts'].path}`, 246 | 'ws-opts.path', 247 | ); 248 | if (isPresent(proxy, 'ws-opts.headers')) { 249 | const headers = proxy['ws-opts'].headers; 250 | const value = Object.keys(headers) 251 | .map((k) => `${k}:${headers[k]}`) 252 | .join('|'); 253 | if (isNotBlank(value)) { 254 | result.append(`,ws-headers=${value}`); 255 | } 256 | } 257 | } 258 | } else { 259 | throw new Error(`network ${proxy.network} is unsupported`); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/uri.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import { Base64 } from 'js-base64'; 3 | 4 | export default function URI_Producer() { 5 | const type = 'SINGLE'; 6 | const produce = (proxy) => { 7 | let result = ''; 8 | switch (proxy.type) { 9 | case 'ss': 10 | const userinfo = `${proxy.cipher}:${proxy.password}`; 11 | result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${ 12 | proxy.port 13 | }/`; 14 | if (proxy.plugin) { 15 | result += '?plugin='; 16 | const opts = proxy['plugin-opts']; 17 | switch (proxy.plugin) { 18 | case 'obfs': 19 | result += encodeURIComponent( 20 | `simple-obfs;obfs=${opts.mode}${ 21 | opts.host ? ';obfs-host=' + opts.host : '' 22 | }`, 23 | ); 24 | break; 25 | case 'v2ray-plugin': 26 | result += encodeURIComponent( 27 | `v2ray-plugin;obfs=${opts.mode}${ 28 | opts.host ? ';obfs-host' + opts.host : '' 29 | }${opts.tls ? ';tls' : ''}`, 30 | ); 31 | break; 32 | default: 33 | throw new Error( 34 | `Unsupported plugin option: ${proxy.plugin}`, 35 | ); 36 | } 37 | } 38 | result += `#${encodeURIComponent(proxy.name)}`; 39 | break; 40 | case 'ssr': 41 | result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${ 42 | proxy.cipher 43 | }:${proxy.obfs}:${Base64.encode(proxy.password)}/`; 44 | result += `?remarks=${Base64.encode(proxy.name)}${ 45 | proxy['obfs-param'] 46 | ? '&obfsparam=' + Base64.encode(proxy['obfs-param']) 47 | : '' 48 | }${ 49 | proxy['protocol-param'] 50 | ? '&protocolparam=' + 51 | Base64.encode(proxy['protocol-param']) 52 | : '' 53 | }`; 54 | result = 'ssr://' + Base64.encode(result); 55 | break; 56 | case 'vmess': 57 | // V2RayN URI format 58 | result = { 59 | ps: proxy.name, 60 | add: proxy.server, 61 | port: proxy.port, 62 | id: proxy.uuid, 63 | type: '', 64 | aid: 0, 65 | net: proxy.network || 'tcp', 66 | tls: proxy.tls ? 'tls' : '', 67 | }; 68 | // obfs 69 | if (proxy.network === 'ws') { 70 | result.path = proxy['ws-opts'].path || '/'; 71 | result.host = proxy['ws-opts'].headers.Host || proxy.server; 72 | } 73 | result = 'vmess://' + Base64.encode(JSON.stringify(result)); 74 | break; 75 | case 'trojan': 76 | result = `trojan://${proxy.password}@${proxy.server}:${ 77 | proxy.port 78 | }#${encodeURIComponent(proxy.name)}`; 79 | break; 80 | } 81 | return result; 82 | }; 83 | return { type, produce }; 84 | } 85 | -------------------------------------------------------------------------------- /src/core/proxy-utils/producers/utils.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export class Result { 4 | constructor(proxy) { 5 | this.proxy = proxy; 6 | this.output = []; 7 | } 8 | 9 | append(data) { 10 | if (typeof data === 'undefined') { 11 | throw new Error('required field is missing'); 12 | } 13 | this.output.push(data); 14 | } 15 | 16 | appendIfPresent(data, attr) { 17 | if (isPresent(this.proxy, attr)) { 18 | this.append(data); 19 | } 20 | } 21 | 22 | toString() { 23 | return this.output.join(''); 24 | } 25 | } 26 | 27 | export function isPresent(obj, attr) { 28 | const data = _.get(obj, attr); 29 | return typeof data !== 'undefined' && data !== null; 30 | } 31 | -------------------------------------------------------------------------------- /src/core/proxy-utils/validators/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaintWe/Sub-Store-Workers/241ce04a27abd17db6217d8aae50a46c6103c9b9/src/core/proxy-utils/validators/index.js -------------------------------------------------------------------------------- /src/core/rule-utils/index.ts: -------------------------------------------------------------------------------- 1 | import RULE_PREPROCESSORS from './preprocessors'; 2 | import RULE_PRODUCERS from './producers'; 3 | import RULE_PARSERS from './parsers'; 4 | import $ from '../../core/app'; 5 | 6 | export const RuleUtils = (function () { 7 | function preprocess(raw) { 8 | for (const processor of RULE_PREPROCESSORS) { 9 | try { 10 | if (processor.test(raw)) { 11 | $.info(`Pre-processor [${processor.name}] activated`); 12 | return processor.parse(raw); 13 | } 14 | } catch (e) { 15 | $.error(`Parser [${processor.name}] failed\n Reason: ${e}`); 16 | } 17 | } 18 | return raw; 19 | } 20 | 21 | function parse(raw) { 22 | raw = preprocess(raw); 23 | for (const parser of RULE_PARSERS) { 24 | let matched; 25 | try { 26 | matched = parser.test(raw); 27 | } catch (err) { 28 | matched = false; 29 | } 30 | if (matched) { 31 | $.info(`Rule parser [${parser.name}] is activated!`); 32 | return parser.parse(raw); 33 | } 34 | } 35 | } 36 | 37 | function produce(rules, targetPlatform) { 38 | const producer = RULE_PRODUCERS[targetPlatform]; 39 | if (!producer) { 40 | throw new Error( 41 | `Target platform: ${targetPlatform} is not supported!`, 42 | ); 43 | } 44 | if ( 45 | typeof producer.type === 'undefined' || 46 | producer.type === 'SINGLE' 47 | ) { 48 | return rules 49 | .map((rule) => { 50 | try { 51 | return producer.func(rule); 52 | } catch (err) { 53 | console.log( 54 | `ERROR: cannot produce rule: ${JSON.stringify( 55 | rule, 56 | )}\nReason: ${err}`, 57 | ); 58 | return ''; 59 | } 60 | }) 61 | .filter((line) => line.length > 0) 62 | .join('\n'); 63 | } else if (producer.type === 'ALL') { 64 | return producer.func(rules); 65 | } 66 | } 67 | 68 | return { parse, produce }; 69 | })(); 70 | -------------------------------------------------------------------------------- /src/core/rule-utils/parsers.ts: -------------------------------------------------------------------------------- 1 | const RULE_TYPES_MAPPING = [ 2 | [/^(DOMAIN|host|HOST)$/, 'DOMAIN'], 3 | [/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'], 4 | [/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'], 5 | [/^USER-AGENT$/i, 'USER-AGENT'], 6 | [/^PROCESS-NAME$/, 'PROCESS-NAME'], 7 | [/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'], 8 | [/^SRC-IP(-CIDR)?$/, 'SRC-IP'], 9 | [/^(IN|SRC)-PORT$/, 'IN-PORT'], 10 | [/^PROTOCOL$/, 'PROTOCOL'], 11 | [/^IP-CIDR$/i, 'IP-CIDR'], 12 | [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/], 13 | ]; 14 | 15 | function AllRuleParser() { 16 | const name = 'Universal Rule Parser'; 17 | const test = () => true; 18 | const parse = (raw) => { 19 | const lines = raw.split('\n'); 20 | const result = []; 21 | for (let line of lines) { 22 | line = line.trim(); 23 | // skip empty line 24 | if (line.length === 0) continue; 25 | // skip comments 26 | if (/\s*#/.test(line)) continue; 27 | try { 28 | const params = line.split(',').map((w) => w.trim()); 29 | let rawType = params[0]; 30 | let matched = false; 31 | for (const item of RULE_TYPES_MAPPING) { 32 | const regex = item[0]; 33 | if (regex.test(rawType)) { 34 | matched = true; 35 | const rule = { 36 | type: item[1], 37 | content: params[1], 38 | }; 39 | if ( 40 | rule.type === 'IP-CIDR' || 41 | rule.type === 'IP-CIDR6' 42 | ) { 43 | rule.options = params.slice(2); 44 | } 45 | result.push(rule); 46 | } 47 | } 48 | if (!matched) throw new Error('Invalid rule type: ' + rawType); 49 | } catch (e) { 50 | console.error(`Failed to parse line: ${line}\n Reason: ${e}`); 51 | } 52 | } 53 | return result; 54 | }; 55 | return { name, test, parse }; 56 | } 57 | 58 | export default [AllRuleParser()]; 59 | -------------------------------------------------------------------------------- /src/core/rule-utils/preprocessors.ts: -------------------------------------------------------------------------------- 1 | function HTML() { 2 | const name = 'HTML'; 3 | const test = (raw) => /^/.test(raw); 4 | // simply discard HTML 5 | const parse = () => ''; 6 | return { name, test, parse }; 7 | } 8 | 9 | function ClashProvider() { 10 | const name = 'Clash Provider'; 11 | const test = (raw) => raw.indexOf('payload:') === 0; 12 | const parse = (raw) => { 13 | return raw.replace('payload:', '').replace(/^\s*-\s*/gm, ''); 14 | }; 15 | return { name, test, parse }; 16 | } 17 | 18 | export default [HTML(), ClashProvider()]; 19 | -------------------------------------------------------------------------------- /src/core/rule-utils/producers.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'static-js-yaml'; 2 | 3 | function QXFilter() { 4 | const type = 'SINGLE'; 5 | const func = (rule) => { 6 | // skip unsupported rules 7 | const UNSUPPORTED = [ 8 | 'URL-REGEX', 9 | 'DEST-PORT', 10 | 'SRC-IP', 11 | 'IN-PORT', 12 | 'PROTOCOL', 13 | ]; 14 | if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; 15 | 16 | const TRANSFORM = { 17 | 'DOMAIN-KEYWORD': 'HOST-KEYWORD', 18 | 'DOMAIN-SUFFIX': 'HOST-SUFFIX', 19 | DOMAIN: 'HOST', 20 | 'IP-CIDR6': 'IP6-CIDR', 21 | }; 22 | 23 | // QX does not support the no-resolve option 24 | return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`; 25 | }; 26 | return { type, func }; 27 | } 28 | 29 | function SurgeRuleSet() { 30 | const type = 'SINGLE'; 31 | const func = (rule) => { 32 | let output = `${rule.type},${rule.content}`; 33 | if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') { 34 | output += rule.options ? `,${rule.options[0]}` : ''; 35 | } 36 | return output; 37 | }; 38 | return { type, func }; 39 | } 40 | 41 | function LoonRules() { 42 | const type = 'SINGLE'; 43 | const func = (rule) => { 44 | // skip unsupported rules 45 | const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL']; 46 | if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; 47 | return SurgeRuleSet().func(rule); 48 | }; 49 | return { type, func }; 50 | } 51 | 52 | function ClashRuleProvider() { 53 | const type = 'ALL'; 54 | const func = (rules) => { 55 | const TRANSFORM = { 56 | 'DEST-PORT': 'DST-PORT', 57 | 'SRC-IP': 'SRC-IP-CIDR', 58 | 'IN-PORT': 'SRC-PORT', 59 | }; 60 | const conf = { 61 | payload: rules.map((rule) => { 62 | let output = `${TRANSFORM[rule.type] || rule.type},${ 63 | rule.content 64 | }`; 65 | if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') { 66 | output += rule.options ? `,${rule.options[0]}` : ''; 67 | } 68 | return output; 69 | }), 70 | }; 71 | return YAML.dump(conf); 72 | }; 73 | return { type, func }; 74 | } 75 | 76 | export default { 77 | QX: QXFilter(), 78 | Surge: SurgeRuleSet(), 79 | Loon: LoonRules(), 80 | Clash: ClashRuleProvider(), 81 | }; 82 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Kysely } from 'kysely'; 2 | import { D1Dialect } from 'kysely-d1'; 3 | import { Env } from '.'; 4 | 5 | interface SubStoreTable { 6 | key: string; 7 | data: string; 8 | }; 9 | 10 | interface ResourceCacheTable { 11 | key: string; 12 | data: string; 13 | time: number; 14 | }; 15 | 16 | interface Database { 17 | sub_store: SubStoreTable; 18 | resource_cache: ResourceCacheTable; 19 | }; 20 | 21 | let DB: Kysely; 22 | 23 | export function setDBClient(env: Env) { 24 | DB = new Kysely({ 25 | dialect: new D1Dialect({ database: env.DB }), 26 | }); 27 | } 28 | 29 | export { DB }; 30 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '.'; 2 | 3 | export function setEnvironment(e: Env) { 4 | env = e; 5 | } 6 | 7 | export let env: Env; 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, Hono } from 'hono'; 2 | import { cors } from 'hono/cors'; 3 | import { bearerAuth } from 'hono/bearer-auth' 4 | import registerCollectionRoutes from './restful/collections'; 5 | import registerSubscriptionRoutes from './restful/subscriptions'; 6 | import registerArtifactRoutes from './restful/artifacts'; 7 | import registerSyncRoutes from './restful/sync'; 8 | import registerDownloadRoutes from './restful/download'; 9 | import registerSettingRoutes from './restful/settings'; 10 | import registerPreviewRoutes from './restful/preview'; 11 | import registerSortingRoutes from './restful/sort'; 12 | import registerMiscRoutes from './restful/miscs'; 13 | import registerNodeInfoRoutes from './restful/node-info'; 14 | import { setDBClient } from './db'; 15 | import { setEnvironment } from './env'; 16 | 17 | type Bindings = { 18 | DB: D1Database 19 | } 20 | 21 | export interface Env { 22 | __STATIC_CONTENT: KVNamespace; 23 | DB: D1Database; 24 | BEARER_TOKEN: string; 25 | D_TOKEN: string; 26 | } 27 | 28 | export default { 29 | async fetch(request: Request, env: Env, ctx: ExecutionContext) { 30 | 31 | setEnvironment(env); 32 | setDBClient(env); 33 | 34 | await env.DB.prepare("CREATE TABLE IF NOT EXISTS sub_store (key TEXT NOT NULL PRIMARY KEY, data TEXT)").run() 35 | await env.DB.prepare("CREATE TABLE IF NOT EXISTS resource_cache (key TEXT NOT NULL PRIMARY KEY, data TEXT, time INTEGER)").run() 36 | 37 | const $app = new Hono<{ Bindings: Bindings }>(); 38 | 39 | $app.use('/*', cors()); 40 | 41 | if (env.BEARER_TOKEN) { 42 | $app.use('/api/*', bearerAuth({ token: env.BEARER_TOKEN })); 43 | } 44 | 45 | if (env.D_TOKEN) { 46 | $app.use('/download/*', async (c, next) => { 47 | if (c.req.query('d_token') !== env.D_TOKEN) { 48 | return c.text('由于开启了 d_token 验证,当前 d_token 不正确或未提供', 401); 49 | } 50 | await next(); 51 | }); 52 | } 53 | 54 | $app.get('/subs', (c: Context) => c.redirect('/', 302)); 55 | $app.get('/sync', (c: Context) => c.redirect('/', 302)); 56 | $app.get('/my', (c: Context) => c.redirect('/', 302)); 57 | 58 | await registerCollectionRoutes($app); 59 | await registerSubscriptionRoutes($app); 60 | await registerDownloadRoutes($app); 61 | await registerPreviewRoutes($app); 62 | await registerSortingRoutes($app); 63 | await registerSettingRoutes($app); 64 | await registerArtifactRoutes($app); 65 | await registerSyncRoutes($app); 66 | await registerNodeInfoRoutes($app); 67 | await registerMiscRoutes($app); 68 | 69 | return $app.fetch(request, env, ctx); 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /src/restful/artifacts.ts: -------------------------------------------------------------------------------- 1 | import $ from '../core/app'; 2 | import { 3 | ARTIFACT_REPOSITORY_KEY, 4 | ARTIFACTS_KEY, 5 | SETTINGS_KEY, 6 | } from '../constants'; 7 | import { deleteByName, findByName, updateByName } from '../utils/database'; 8 | import { failed, success } from './response'; 9 | import { 10 | InternalServerError, 11 | RequestInvalidError, 12 | ResourceNotFoundError, 13 | } from './errors'; 14 | import Gist from '../utils/gist'; 15 | import { Context, Hono } from 'hono'; 16 | 17 | export default async function register($app: Hono) { 18 | // Initialization 19 | if (! await $.read(ARTIFACTS_KEY)) await $.write([], ARTIFACTS_KEY); 20 | 21 | // RESTful APIs 22 | $app.get('/api/artifacts', getAllArtifacts).post(createArtifact); 23 | $app.get('/api/artifact/:name', getArtifact).patch(updateArtifact).delete(deleteArtifact); 24 | } 25 | 26 | const getAllArtifacts = async (c: Context) => { 27 | const allArtifacts = await $.read(ARTIFACTS_KEY); 28 | return success(c, allArtifacts); 29 | } 30 | 31 | const getArtifact = async (c: Context) => { 32 | let { name } = c.req.param(); 33 | name = decodeURIComponent(name); 34 | const allArtifacts = await $.read(ARTIFACTS_KEY); 35 | const artifact = findByName(allArtifacts, name); 36 | 37 | if (artifact) { 38 | return success(c, artifact); 39 | } else { 40 | return failed( 41 | c, 42 | new ResourceNotFoundError( 43 | 'RESOURCE_NOT_FOUND', 44 | `Artifact ${name} does not exist!`, 45 | ), 46 | 404, 47 | ); 48 | } 49 | } 50 | 51 | const createArtifact = async (c: Context) => { 52 | const artifact = await c.req.json(); 53 | if (!validateArtifactName(artifact.name)) { 54 | return failed( 55 | c, 56 | new RequestInvalidError( 57 | 'INVALID_ARTIFACT_NAME', 58 | `Artifact name ${artifact.name} is invalid.`, 59 | ), 60 | ); 61 | } 62 | 63 | $.info(`正在创建远程配置:${artifact.name}`); 64 | const allArtifacts = await $.read(ARTIFACTS_KEY); 65 | if (findByName(allArtifacts, artifact.name)) { 66 | return failed( 67 | c, 68 | new RequestInvalidError( 69 | 'DUPLICATE_KEY', 70 | `Artifact ${artifact.name} already exists.`, 71 | ), 72 | ); 73 | } else { 74 | allArtifacts.push(artifact); 75 | await $.write(allArtifacts, ARTIFACTS_KEY); 76 | return success(c, artifact, 201); 77 | } 78 | } 79 | 80 | const updateArtifact = async (c: Context) => { 81 | const allArtifacts = await $.read(ARTIFACTS_KEY); 82 | let oldName = c.req.param('name'); 83 | oldName = decodeURIComponent(oldName); 84 | const artifact = findByName(allArtifacts, oldName); 85 | if (artifact) { 86 | $.info(`正在更新远程配置:${artifact.name}`); 87 | const newArtifact = { 88 | ...artifact, 89 | ...await c.req.json(), 90 | }; 91 | if (!validateArtifactName(newArtifact.name)) { 92 | return failed( 93 | c, 94 | new RequestInvalidError( 95 | 'INVALID_ARTIFACT_NAME', 96 | `Artifact name ${newArtifact.name} is invalid.`, 97 | ), 98 | ); 99 | } 100 | updateByName(allArtifacts, oldName, newArtifact); 101 | await $.write(allArtifacts, ARTIFACTS_KEY); 102 | return success(c, newArtifact); 103 | } else { 104 | return failed( 105 | c, 106 | new RequestInvalidError( 107 | 'DUPLICATE_KEY', 108 | `Artifact ${oldName} already exists.`, 109 | ), 110 | ); 111 | } 112 | } 113 | 114 | const deleteArtifact = async (c: Context) => { 115 | let { name } = c.req.param(); 116 | name = decodeURIComponent(name); 117 | $.info(`正在删除远程配置:${name}`); 118 | const allArtifacts = await $.read(ARTIFACTS_KEY); 119 | try { 120 | const artifact = findByName(allArtifacts, name); 121 | if (!artifact) throw new Error(`远程配置:${name}不存在!`); 122 | if (artifact.updated) { 123 | // delete gist 124 | const files = {}; 125 | files[encodeURIComponent(artifact.name)] = { 126 | content: '', 127 | }; 128 | await syncToGist(files); 129 | } 130 | // delete local cache 131 | deleteByName(allArtifacts, name); 132 | await $.write(allArtifacts, ARTIFACTS_KEY); 133 | return success(c); 134 | } catch (err) { 135 | $.error(`无法删除远程配置:${name},原因:${err}`); 136 | return failed( 137 | c, 138 | new InternalServerError( 139 | `FAILED_TO_DELETE_ARTIFACT`, 140 | `Failed to delete artifact ${name}`, 141 | `Reason: ${err}`, 142 | ), 143 | ); 144 | } 145 | } 146 | 147 | const validateArtifactName = (name: string) => { 148 | return /^[a-zA-Z0-9._-]*$/.test(name); 149 | } 150 | 151 | const syncToGist = async (files) => { 152 | const { gistToken } = await $.read(SETTINGS_KEY); 153 | if (!gistToken) { 154 | return Promise.reject('未设置Gist Token!'); 155 | } 156 | const manager = new Gist({ 157 | token: gistToken, 158 | key: ARTIFACT_REPOSITORY_KEY, 159 | }); 160 | return manager.upload(files); 161 | } 162 | 163 | export { syncToGist }; 164 | -------------------------------------------------------------------------------- /src/restful/collections.ts: -------------------------------------------------------------------------------- 1 | import { deleteByName, findByName, updateByName } from '../utils/database'; 2 | import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '../constants'; 3 | import { failed, success } from './response'; 4 | import $ from '../core/app'; 5 | import { RequestInvalidError, ResourceNotFoundError } from './errors'; 6 | import { Context, Hono } from 'hono'; 7 | 8 | export default async function register($app: Hono) { 9 | if (! await $.read(COLLECTIONS_KEY)) await $.write([], COLLECTIONS_KEY); 10 | 11 | $app.get('/api/collections', getAllCollections).post(createCollection); 12 | $app.get('/api/collection/:name', getCollection).patch(updateCollection).delete(deleteCollection); 13 | } 14 | 15 | // collection API 16 | const createCollection = async (c: Context) => { 17 | // req, res 18 | const collection = await c.req.json(); 19 | $.info(`正在创建组合订阅:${collection.name}`); 20 | const allCols = await $.read(COLLECTIONS_KEY); 21 | if (findByName(allCols, collection.name)) { 22 | return failed( 23 | c, 24 | new RequestInvalidError( 25 | 'DUPLICATE_KEY', 26 | `Collection ${collection.name} already exists.`, 27 | ), 28 | ); 29 | } 30 | allCols.push(collection); 31 | await $.write(allCols, COLLECTIONS_KEY); 32 | return success(c, collection, 201); 33 | } 34 | 35 | const getCollection = async (c: Context) => { 36 | let { name } = c.req.param(); 37 | name = decodeURIComponent(name); 38 | const allCols = await $.read(COLLECTIONS_KEY); 39 | const collection = findByName(allCols, name); 40 | if (collection) { 41 | return success(c, collection); 42 | } else { 43 | return failed( 44 | c, 45 | new ResourceNotFoundError( 46 | `SUBSCRIPTION_NOT_FOUND`, 47 | `Collection ${name} does not exist`, 48 | 404, 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | const updateCollection = async (c: Context) => { 55 | let { name } = c.req.param(); 56 | name = decodeURIComponent(name); 57 | let collection = await c.req.json(); 58 | const allCols = await $.read(COLLECTIONS_KEY); 59 | const oldCol = findByName(allCols, name); 60 | if (oldCol) { 61 | const newCol = { 62 | ...oldCol, 63 | ...collection, 64 | }; 65 | $.info(`正在更新组合订阅:${name}...`); 66 | 67 | if (name !== newCol.name) { 68 | // update all artifacts referring this collection 69 | const allArtifacts = await $.read(ARTIFACTS_KEY) || []; 70 | for (const artifact of allArtifacts) { 71 | if ( 72 | artifact.type === 'collection' && 73 | artifact.source === oldCol.name 74 | ) { 75 | artifact.source = newCol.name; 76 | } 77 | } 78 | await $.write(allArtifacts, ARTIFACTS_KEY); 79 | } 80 | 81 | updateByName(allCols, name, newCol); 82 | await $.write(allCols, COLLECTIONS_KEY); 83 | return success(c, newCol); 84 | } else { 85 | return failed( 86 | c, 87 | new ResourceNotFoundError( 88 | 'RESOURCE_NOT_FOUND', 89 | `Collection ${name} does not exist!`, 90 | ), 91 | 404, 92 | ); 93 | } 94 | } 95 | 96 | const deleteCollection = async (c: Context) => { 97 | let { name } = c.req.param(); 98 | name = decodeURIComponent(name); 99 | $.info(`正在删除组合订阅:${name}`); 100 | let allCols = await $.read(COLLECTIONS_KEY); 101 | allCols = deleteByName(allCols, name); 102 | await $.write(allCols, COLLECTIONS_KEY); 103 | return success(c); 104 | } 105 | 106 | const getAllCollections = async (c: Context) => { 107 | const allCols = await $.read(COLLECTIONS_KEY); 108 | return success(c, allCols); 109 | } 110 | -------------------------------------------------------------------------------- /src/restful/download.ts: -------------------------------------------------------------------------------- 1 | import { getPlatformFromHeaders } from '../utils/platform'; 2 | import { COLLECTIONS_KEY, SUBS_KEY } from '../constants'; 3 | import { findByName } from '../utils/database'; 4 | import { getFlowHeaders } from '../utils/flow'; 5 | import $ from '../core/app'; 6 | import { failed } from './response'; 7 | import { InternalServerError, ResourceNotFoundError } from './errors'; 8 | import { produceArtifact } from './sync'; 9 | import { Context, Hono } from 'hono'; 10 | 11 | export default async function register($app: Hono) { 12 | const route = new Hono(); 13 | route.get('/collection/:collection_name', downloadCollection); 14 | route.get('/:name', downloadSubscription); 15 | 16 | $app.route('/download', route); 17 | } 18 | 19 | const downloadSubscription = async (c: Context) => { 20 | let { name } = c.req.param(); 21 | name = decodeURIComponent(name); 22 | const platform = c.req.query('target') || getPlatformFromHeaders(c.req.headers) || 'JSON'; 23 | $.info(`正在下载订阅:${name}`); 24 | 25 | const allSubs = await $.read(SUBS_KEY); 26 | const sub = findByName(allSubs, name); 27 | if (sub) { 28 | try { 29 | const output = await produceArtifact({ 30 | type: 'subscription', 31 | name, 32 | platform, 33 | }); 34 | if (sub.source !== 'local') { 35 | // forward flow headers 36 | const flowInfo = await getFlowHeaders(sub.url); 37 | if (flowInfo) { 38 | c.header('subscription-userinfo', flowInfo); 39 | } 40 | } 41 | if (platform === 'JSON') { 42 | c.header('Content-Type', 'application/json;charset=utf-8'); 43 | return c.body( 44 | output, 45 | ); 46 | } else { 47 | return c.body(output); 48 | } 49 | } catch (err) { 50 | $.notify( 51 | `🌍 Sub-Store 下载订阅失败`, 52 | `❌ 无法下载订阅:${name}!`, 53 | `🤔 原因:${JSON.stringify(err)}`, 54 | ); 55 | $.error(JSON.stringify(err)); 56 | return failed( 57 | c, 58 | new InternalServerError( 59 | 'INTERNAL_SERVER_ERROR', 60 | `Failed to download subscription: ${name}`, 61 | `Reason: ${JSON.stringify(err)}`, 62 | ), 63 | ); 64 | } 65 | } else { 66 | $.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}!`); 67 | return failed( 68 | c, 69 | new ResourceNotFoundError( 70 | 'RESOURCE_NOT_FOUND', 71 | `Subscription ${name} does not exist!`, 72 | ), 73 | 404, 74 | ); 75 | } 76 | } 77 | 78 | const downloadCollection = async (c: Context) => { 79 | let name = c.req.param('collection_name'); 80 | name = decodeURIComponent(name); 81 | 82 | const platform = c.req.query('target') || getPlatformFromHeaders(c.req.headers) || 'JSON'; 83 | 84 | const allCols = await $.read(COLLECTIONS_KEY); 85 | const collection = findByName(allCols, name); 86 | 87 | $.info(`正在下载组合订阅:${name}`); 88 | 89 | if (collection) { 90 | try { 91 | const output = await produceArtifact({ 92 | type: 'collection', 93 | name, 94 | platform, 95 | }); 96 | 97 | // forward flow header from the first subscription in this collection 98 | const allSubs = await $.read(SUBS_KEY); 99 | const subnames = collection.subscriptions; 100 | if (subnames.length > 0) { 101 | const sub = findByName(allSubs, subnames[0]); 102 | if (sub.source !== 'local') { 103 | const flowInfo = await getFlowHeaders(sub.url); 104 | if (flowInfo) { 105 | c.header('subscription-userinfo', flowInfo); 106 | } 107 | } 108 | } 109 | if (platform === 'JSON') { 110 | c.header('Content-Type', 'application/json;charset=utf-8'); 111 | } 112 | return c.body(output); 113 | } catch (err) { 114 | $.notify( 115 | `🌍 Sub-Store 下载组合订阅失败`, 116 | `❌ 下载组合订阅错误:${name}!`, 117 | `🤔 原因:${err}`, 118 | ); 119 | return failed( 120 | c, 121 | new InternalServerError( 122 | 'INTERNAL_SERVER_ERROR', 123 | `Failed to download collection: ${name}`, 124 | `Reason: ${JSON.stringify(err)}`, 125 | ), 126 | ); 127 | } 128 | } else { 129 | $.notify( 130 | `🌍 Sub-Store 下载组合订阅失败`, 131 | `❌ 未找到组合订阅:${name}!`, 132 | ); 133 | return failed( 134 | c, 135 | new ResourceNotFoundError( 136 | 'RESOURCE_NOT_FOUND', 137 | `Collection ${name} does not exist!`, 138 | ), 139 | 404, 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/restful/errors/index.ts: -------------------------------------------------------------------------------- 1 | class BaseError { 2 | code; message; details; type: any; 3 | constructor(code: any, message: any, details?: any) { 4 | this.code = code; 5 | this.message = message; 6 | this.details = details; 7 | } 8 | } 9 | 10 | export class InternalServerError extends BaseError { 11 | constructor(code: any, message: any, details?: any) { 12 | super(code, message, details); 13 | this.type = 'InternalServerError'; 14 | } 15 | } 16 | 17 | export class RequestInvalidError extends BaseError { 18 | constructor(code: any, message: any, details?: any) { 19 | super(code, message, details); 20 | this.type = 'RequestInvalidError'; 21 | } 22 | } 23 | 24 | export class ResourceNotFoundError extends BaseError { 25 | constructor(code: any, message: any, details?: any) { 26 | super(code, message, details); 27 | this.type = 'ResourceNotFoundError'; 28 | } 29 | } 30 | 31 | export class NetworkError extends BaseError { 32 | constructor(code: any, message: any, details?: any) { 33 | super(code, message, details); 34 | this.type = 'NetworkError'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/restful/miscs.ts: -------------------------------------------------------------------------------- 1 | import $ from '../core/app'; 2 | import { failed, success } from './response'; 3 | import { version as substoreVersion } from '../../package.json'; 4 | import { updateArtifactStore, updateGitHubAvatar } from './settings'; 5 | import resourceCache from '../utils/resource-cache'; 6 | import { 7 | GIST_BACKUP_FILE_NAME, 8 | GIST_BACKUP_KEY, 9 | SETTINGS_KEY, 10 | } from '../constants'; 11 | import { InternalServerError, RequestInvalidError } from './errors'; 12 | import Gist from '../utils/gist'; 13 | import migrate from '../utils/migration'; 14 | import { Context, Hono } from 'hono'; 15 | 16 | export default async function register($app: Hono) { 17 | // utils 18 | $app.get('/api/utils/env', getEnv); // get runtime environment 19 | $app.get('/api/utils/backup', gistBackup); // gist backup actions 20 | $app.get('/api/utils/refresh', refresh); 21 | 22 | // Storage management 23 | $app.get('/api/storage', async (c: Context) => { 24 | return c.json(await $.read('#sub-store')); 25 | }).post(async (c: Context) => { 26 | const data = await c.req.json(); 27 | await $.write(data, '#sub-store'); 28 | return c.json({}); 29 | }); 30 | 31 | // Redirect sub.store to vercel webpage 32 | $app.get('/', async (c: Context) => c.redirect('https://sub-store-workers.vercel.app/', 302)); 33 | 34 | $app.all('/', (c: Context) => { 35 | return c.text('Hello from sub-store, made with ❤️ by Peng-YM'); 36 | }); 37 | } 38 | 39 | const getEnv = async (c: Context) => { 40 | let backend = 'CloudFlareWorkers'; 41 | 42 | return success(c, { 43 | backend, 44 | version: substoreVersion, 45 | }); 46 | } 47 | 48 | const refresh = async (c: Context) => { 49 | // 1. get GitHub avatar and artifact store 50 | await updateGitHubAvatar(); 51 | await updateArtifactStore(); 52 | 53 | // 2. clear resource cache 54 | await resourceCache.revokeAll(); 55 | return success(c); 56 | } 57 | 58 | const gistBackup = async (c: Context) => { 59 | const { action } = c.req.query(); 60 | // read token 61 | const { gistToken } = await $.read(SETTINGS_KEY); 62 | if (!gistToken) { 63 | return failed( 64 | c, 65 | new RequestInvalidError( 66 | 'GIST_TOKEN_NOT_FOUND', 67 | `GitHub Token is required for backup!`, 68 | ), 69 | ); 70 | } else { 71 | const gist = new Gist({ 72 | token: gistToken, 73 | key: GIST_BACKUP_KEY, 74 | }); 75 | try { 76 | let content; 77 | const settings = await $.read(SETTINGS_KEY); 78 | const updated = settings.syncTime; 79 | switch (action) { 80 | case 'upload': 81 | // update syncTime 82 | settings.syncTime = new Date().getTime(); 83 | await $.write(settings, SETTINGS_KEY); 84 | content = await $.read('#sub-store'); 85 | $.info(`上传备份中...`); 86 | try { 87 | await gist.upload({ 88 | [GIST_BACKUP_FILE_NAME]: { content }, 89 | }); 90 | } catch (err) { 91 | // restore syncTime if upload failed 92 | settings.syncTime = updated; 93 | await $.write(settings, SETTINGS_KEY); 94 | throw err; 95 | } 96 | break; 97 | case 'download': 98 | $.info(`还原备份中...`); 99 | content = await gist.download(GIST_BACKUP_FILE_NAME); 100 | // restore settings 101 | await $.write(content, '#sub-store'); 102 | // perform migration after restoring from gist 103 | migrate(); 104 | break; 105 | } 106 | return success(c); 107 | } catch (err) { 108 | return failed( 109 | c, 110 | new InternalServerError( 111 | 'BACKUP_FAILED', 112 | `Failed to ${action} data to gist!`, 113 | `Reason: ${JSON.stringify(err)}`, 114 | ), 115 | ); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/restful/node-info.ts: -------------------------------------------------------------------------------- 1 | import producer from '../core/proxy-utils/producers'; 2 | import { HTTP } from '../vendor/open-api'; 3 | import { failed, success } from './response'; 4 | import { NetworkError } from './errors'; 5 | import { Context, Hono } from 'hono'; 6 | 7 | export default async function register($app: Hono) { 8 | $app.post('/api/utils/node-info', getNodeInfo); 9 | } 10 | 11 | const getNodeInfo = async (c: Context) => { 12 | const proxy = await c.req.json(); 13 | const lang = c.req.query('lang') || 'zh-CN'; 14 | let shareUrl; 15 | try { 16 | shareUrl = producer.URI.produce(proxy); 17 | } catch (err) { 18 | // do nothing 19 | } 20 | 21 | try { 22 | const $http = HTTP(); 23 | const info = await $http 24 | .get({ 25 | url: `http://ip-api.com/json/${encodeURIComponent( 26 | proxy.server, 27 | )}?lang=${lang}`, 28 | headers: { 29 | 'User-Agent': 30 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15', 31 | }, 32 | }) 33 | .then( 34 | (resp: Response) => resp.json() 35 | ) 36 | .then((data) => { 37 | if (data.status !== 'success') { 38 | throw new Error(data.message); 39 | } 40 | // remove unnecessary fields 41 | delete data.status; 42 | return data; 43 | }); 44 | return success(c, { 45 | shareUrl, 46 | info, 47 | }); 48 | } catch (err) { 49 | return failed( 50 | c, 51 | new NetworkError( 52 | 'FAILED_TO_GET_NODE_INFO', 53 | `Failed to get node info`, 54 | `Reason: ${err}`, 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/restful/preview.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError, NetworkError } from './errors'; 2 | import { ProxyUtils } from '../core/proxy-utils'; 3 | import { findByName } from '../utils/database'; 4 | import { success, failed } from './response'; 5 | import download from '../utils/download'; 6 | import { SUBS_KEY } from '../constants'; 7 | import $ from '../core/app'; 8 | import { Context, Hono } from 'hono'; 9 | 10 | export default async function register($app: Hono) { 11 | const route = new Hono(); 12 | route.post('/sub', compareSub); 13 | route.post('/collection', compareCollection); 14 | 15 | $app.route('/api/preview', route); 16 | } 17 | 18 | const compareSub = async (c: Context) => { 19 | const sub = await c.req.json(); 20 | const target = c.req.query('target') || 'JSON'; 21 | let content; 22 | if (sub.source === 'local') { 23 | content = sub.content; 24 | } else { 25 | try { 26 | content = await download(sub.url, sub.ua); 27 | } catch (err) { 28 | return failed( 29 | c, 30 | new NetworkError( 31 | 'FAILED_TO_DOWNLOAD_RESOURCE', 32 | '无法下载远程资源', 33 | `Reason: ${err}`, 34 | ), 35 | ); 36 | } 37 | } 38 | // parse proxies 39 | const original = ProxyUtils.parse(content); 40 | 41 | // add id 42 | original.forEach((proxy: any, i: any) => { 43 | proxy.id = i; 44 | }); 45 | 46 | // apply processors 47 | const processed = await ProxyUtils.process( 48 | original, 49 | sub.process || [], 50 | target, 51 | ); 52 | 53 | // produce 54 | return success(c, { original, processed }); 55 | } 56 | 57 | const compareCollection = async (c: Context) => { 58 | const allSubs = await $.read(SUBS_KEY); 59 | const collection = await c.req.json(); 60 | const subnames = collection.subscriptions; 61 | const results = {}; 62 | 63 | await Promise.all( 64 | subnames.map(async (name: string) => { 65 | const sub = findByName(allSubs, name); 66 | try { 67 | let raw; 68 | if (sub.source === 'local') { 69 | raw = sub.content; 70 | } else { 71 | raw = await download(sub.url, sub.ua); 72 | } 73 | // parse proxies 74 | let currentProxies = ProxyUtils.parse(raw); 75 | // apply processors 76 | currentProxies = await ProxyUtils.process( 77 | currentProxies, 78 | sub.process || [], 79 | 'JSON', 80 | ); 81 | results[name] = currentProxies; 82 | } catch (err) { 83 | return failed( 84 | c, 85 | new InternalServerError( 86 | 'PROCESS_FAILED', 87 | `处理子订阅 ${name} 失败`, 88 | `Reason: ${err}`, 89 | ), 90 | ); 91 | } 92 | }), 93 | ); 94 | 95 | // merge proxies with the original order 96 | const original = Array.prototype.concat.apply( 97 | [], 98 | subnames.map((name: any) => results[name] || []), 99 | ); 100 | 101 | original.forEach((proxy, i) => { 102 | proxy.id = i; 103 | }); 104 | 105 | const processed = await ProxyUtils.process( 106 | original, 107 | collection.process || [], 108 | 'JSON', 109 | ); 110 | 111 | return success(c, { original, processed }); 112 | } 113 | -------------------------------------------------------------------------------- /src/restful/response.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | 3 | export function success(c: Context, data: any = '', statusCode: number = 200) { 4 | return c.json( 5 | { 6 | status: 'success', 7 | data, 8 | }, 9 | statusCode 10 | ); 11 | } 12 | 13 | export function failed(c: Context, error: any, statusCode: number = 500) { 14 | return c.json( 15 | { 16 | status: 'failed', 17 | error: { 18 | code: error.code, 19 | type: error.type, 20 | message: error.message, 21 | details: error.details, 22 | }, 23 | }, 24 | statusCode 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/restful/settings.ts: -------------------------------------------------------------------------------- 1 | import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '../constants'; 2 | import { success } from './response'; 3 | import $ from '../core/app'; 4 | import Gist from '../utils/gist'; 5 | import { Context, Hono } from 'hono'; 6 | 7 | export default async function register($app: Hono) { 8 | const settings = await $.read(SETTINGS_KEY); 9 | if (!settings) { 10 | await $.write( 11 | { 12 | username: '', 13 | }, 14 | SETTINGS_KEY 15 | ) 16 | }; 17 | $app.get('/api/settings', getSettings).patch(updateSettings); 18 | } 19 | 20 | const getSettings = async (c: Context) => { 21 | await updateGitHubAvatar(); 22 | const settings = await $.read(SETTINGS_KEY); 23 | if (!settings.avatarUrl) await updateGitHubAvatar(); 24 | if (!settings.artifactStore) await updateArtifactStore(); 25 | return success(c, settings); 26 | } 27 | 28 | const updateSettings = async (c: Context) => { 29 | const settings = await $.read(SETTINGS_KEY); 30 | const newSettings = { 31 | ...settings, 32 | ...await c.req.json(), 33 | }; 34 | await $.write(newSettings, SETTINGS_KEY); 35 | await updateGitHubAvatar(); 36 | await updateArtifactStore(); 37 | return success(c, newSettings); 38 | } 39 | 40 | export const updateGitHubAvatar = async () => { 41 | const settings = await $.read(SETTINGS_KEY); 42 | const username = settings.githubUser; 43 | if (username) { 44 | try { 45 | const data = await $.http 46 | .get({ 47 | url: `https://api.github.com/users/${username}`, 48 | headers: { 49 | 'User-Agent': 50 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', 51 | }, 52 | }) 53 | .then((resp: Response) => resp.json()); 54 | settings.avatarUrl = data['avatar_url']; 55 | await $.write(settings, SETTINGS_KEY); 56 | } catch (e) { 57 | $.error('Failed to fetch GitHub avatar for User: ' + username); 58 | } 59 | } 60 | } 61 | 62 | export const updateArtifactStore = async () => { 63 | $.log('Updating artifact store'); 64 | const settings = await $.read(SETTINGS_KEY); 65 | const { githubUser, gistToken } = settings; 66 | if (githubUser && gistToken) { 67 | const manager = new Gist({ 68 | token: gistToken, 69 | key: ARTIFACT_REPOSITORY_KEY, 70 | }); 71 | try { 72 | const gistId = await manager.locate(); 73 | if (gistId !== -1) { 74 | settings.artifactStore = `https://gist.github.com/${githubUser}/${gistId}`; 75 | await $.write(settings, SETTINGS_KEY); 76 | } 77 | } catch (err) { 78 | $.error('Failed to fetch artifact store for User: ' + githubUser); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/restful/sort.ts: -------------------------------------------------------------------------------- 1 | import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '../constants'; 2 | import $ from '../core/app'; 3 | import { success } from './response'; 4 | import { Context, Hono } from 'hono'; 5 | 6 | export default async function register($app: Hono) { 7 | const route = new Hono(); 8 | route.post('/subs', sortSubs); 9 | route.post('/collections', sortCollections); 10 | route.post('/artifacts', sortArtifacts); 11 | 12 | $app.route('/api/sort', route); 13 | } 14 | 15 | const sortSubs = async (c: Context) => { 16 | const orders = await c.req.json(); 17 | const allSubs = await $.read(SUBS_KEY); 18 | allSubs.sort((a: any, b: any) => orders.indexOf(a) - orders.indexOf(b)); 19 | await $.write(allSubs, SUBS_KEY); 20 | return success(c, allSubs); 21 | } 22 | 23 | const sortCollections = async (c: Context) => { 24 | const orders = await c.req.json(); 25 | const allCols = await $.read(COLLECTIONS_KEY); 26 | allCols.sort((a: any, b: any) => orders.indexOf(a) - orders.indexOf(b)); 27 | await $.write(allCols, COLLECTIONS_KEY); 28 | return success(c, allCols); 29 | } 30 | 31 | const sortArtifacts = async (c: Context) => { 32 | const orders = await c.req.json(); 33 | const allArtifacts = await $.read(ARTIFACTS_KEY); 34 | allArtifacts.sort((a: any, b: any) => orders.indexOf(a) - orders.indexOf(b)); 35 | await $.write(allArtifacts, ARTIFACTS_KEY); 36 | return success(c, allArtifacts); 37 | } 38 | -------------------------------------------------------------------------------- /src/restful/subscriptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NetworkError, 3 | InternalServerError, 4 | ResourceNotFoundError, 5 | RequestInvalidError, 6 | } from './errors'; 7 | import { deleteByName, findByName, updateByName } from '../utils/database'; 8 | import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '../constants'; 9 | import { getFlowHeaders } from '../utils/flow'; 10 | import { success, failed } from './response'; 11 | import { Context, Hono } from 'hono'; 12 | import $ from '../core/app'; 13 | 14 | export default async function register($app: Hono) { 15 | if (!await $.read(SUBS_KEY)) await $.write([], SUBS_KEY); 16 | 17 | const route = new Hono(); 18 | route.get('/flow/:flow_name', getFlowInfo); 19 | route.get('/:name', getSubscription).patch(updateSubscription).delete(deleteSubscription); 20 | 21 | $app.route('/api/sub', route); 22 | $app.get('/api/subs', getAllSubscriptions).post(createSubscription); 23 | } 24 | 25 | // subscriptions API 26 | const getFlowInfo = async (c: Context) => { 27 | let name = c.req.param('flow_name'); 28 | name = decodeURIComponent(name); 29 | const allSubs = await $.read(SUBS_KEY); 30 | const sub = findByName(allSubs, name); 31 | if (!sub) { 32 | return failed( 33 | c, 34 | new ResourceNotFoundError( 35 | 'RESOURCE_NOT_FOUND', 36 | `Subscription ${name} does not exist!`, 37 | ), 38 | 404, 39 | ); 40 | } 41 | if (sub.source === 'local') { 42 | return failed( 43 | c, 44 | new RequestInvalidError( 45 | 'NO_FLOW_INFO', 46 | 'N/A', 47 | `Local subscription ${name} has no flow information!`, 48 | ), 49 | ); 50 | } 51 | try { 52 | const flowHeaders = await getFlowHeaders(sub.url); 53 | if (!flowHeaders) { 54 | return failed( 55 | c, 56 | new InternalServerError( 57 | 'NO_FLOW_INFO', 58 | 'No flow info', 59 | `Failed to fetch flow headers`, 60 | ), 61 | ); 62 | } 63 | 64 | // unit is KB 65 | const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/) 66 | const upload = Number(uploadMatch[1] + uploadMatch[2]); 67 | 68 | const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/) 69 | const download = Number(downloadMatch[1] + downloadMatch[2]); 70 | 71 | const total = Number(flowHeaders.match(/total=(\d+)/)[1]); 72 | 73 | // optional expire timestamp 74 | const match = flowHeaders.match(/expire=(\d+)/); 75 | const expires = match ? Number(match[1]) : undefined; 76 | 77 | return success(c, { expires, total, usage: { upload, download } }); 78 | } catch (err) { 79 | return failed( 80 | c, 81 | new NetworkError( 82 | `URL_NOT_ACCESSIBLE`, 83 | `The URL for subscription ${name} is inaccessible.`, 84 | ), 85 | ); 86 | } 87 | } 88 | 89 | const createSubscription = async (c: Context) => { 90 | const sub = await c.req.json(); 91 | $.info(`正在创建订阅: ${sub.name}`); 92 | let allSubs = await $.read(SUBS_KEY); 93 | if (findByName(allSubs, sub.name)) { 94 | return failed( 95 | c, 96 | new RequestInvalidError( 97 | 'DUPLICATE_KEY', 98 | `Subscription ${sub.name} already exists.`, 99 | ), 100 | ); 101 | } 102 | allSubs.push(sub); 103 | await $.write(allSubs, SUBS_KEY); 104 | return success(c, sub, 201); 105 | } 106 | 107 | const getSubscription = async (c: Context) => { 108 | let { name } = c.req.param(); 109 | name = decodeURIComponent(name); 110 | const allSubs = await $.read(SUBS_KEY); 111 | const sub = findByName(allSubs, name); 112 | if (sub) { 113 | return success(c, sub); 114 | } else { 115 | return failed( 116 | c, 117 | new ResourceNotFoundError( 118 | `SUBSCRIPTION_NOT_FOUND`, 119 | `Subscription ${name} does not exist`, 120 | 404, 121 | ), 122 | ); 123 | } 124 | } 125 | 126 | const updateSubscription = async (c: Context) => { 127 | let { name } = c.req.param(); 128 | name = decodeURIComponent(name); // the original name 129 | let sub = await c.req.json(); 130 | const allSubs = await $.read(SUBS_KEY); 131 | const oldSub = findByName(allSubs, name); 132 | if (oldSub) { 133 | const newSub = { 134 | ...oldSub, 135 | ...sub, 136 | }; 137 | $.info(`正在更新订阅: ${name}`); 138 | // allow users to update the subscription name 139 | if (name !== sub.name) { 140 | // update all collections refer to this name 141 | const allCols = await $.read(COLLECTIONS_KEY) || []; 142 | for (const collection of allCols) { 143 | const idx = collection.subscriptions.indexOf(name); 144 | if (idx !== -1) { 145 | collection.subscriptions[idx] = sub.name; 146 | } 147 | } 148 | 149 | // update all artifacts referring this subscription 150 | const allArtifacts = await $.read(ARTIFACTS_KEY) || []; 151 | for (const artifact of allArtifacts) { 152 | if ( 153 | artifact.type === 'subscription' && 154 | artifact.source == name 155 | ) { 156 | artifact.source = sub.name; 157 | } 158 | } 159 | 160 | await $.write(allCols, COLLECTIONS_KEY); 161 | await $.write(allArtifacts, ARTIFACTS_KEY); 162 | } 163 | updateByName(allSubs, name, newSub); 164 | await $.write(allSubs, SUBS_KEY); 165 | return success(c, newSub); 166 | } else { 167 | return failed( 168 | c, 169 | new ResourceNotFoundError( 170 | 'RESOURCE_NOT_FOUND', 171 | `Subscription ${name} does not exist!`, 172 | ), 173 | 404, 174 | ); 175 | } 176 | } 177 | 178 | const deleteSubscription = async (c: Context) => { 179 | let { name } = c.req.param(); 180 | name = decodeURIComponent(name); 181 | $.info(`删除订阅:${name}...`); 182 | // delete from subscriptions 183 | let allSubs = await $.read(SUBS_KEY); 184 | allSubs = deleteByName(allSubs, name) 185 | await $.write(allSubs, SUBS_KEY); 186 | // delete from collections 187 | const allCols = await $.read(COLLECTIONS_KEY); 188 | if (allCols) { 189 | for (const collection of allCols) { 190 | collection.subscriptions = collection.subscriptions.filter( 191 | (s: any) => s !== name, 192 | ); 193 | } 194 | await $.write(allCols, COLLECTIONS_KEY); 195 | } 196 | return success(c); 197 | } 198 | 199 | const getAllSubscriptions = async (c: Context) => { 200 | const allSubs = await $.read(SUBS_KEY); 201 | return success(c, allSubs); 202 | } 203 | -------------------------------------------------------------------------------- /src/restful/sync.ts: -------------------------------------------------------------------------------- 1 | import $ from '../core/app'; 2 | import { 3 | ARTIFACTS_KEY, 4 | COLLECTIONS_KEY, 5 | RULES_KEY, 6 | SUBS_KEY, 7 | } from '../constants'; 8 | import { failed, success } from './response'; 9 | import { InternalServerError, ResourceNotFoundError } from './errors'; 10 | import { findByName } from '../utils/database'; 11 | import download from '../utils/download'; 12 | import { ProxyUtils } from '../core/proxy-utils'; 13 | import { RuleUtils } from '../core/rule-utils'; 14 | import { syncToGist } from './artifacts'; 15 | import { Context, Hono } from 'hono'; 16 | 17 | export default async function register($app: Hono) { 18 | // Initialization 19 | if (! await $.read(ARTIFACTS_KEY)) await $.write([], ARTIFACTS_KEY); 20 | 21 | // sync all artifacts 22 | const route = new Hono(); 23 | route.get('/artifacts', syncAllArtifacts); 24 | route.get('/artifact/:name', syncArtifact); 25 | $app.route('/api/sync', route); 26 | } 27 | 28 | type ArtifactType = { 29 | type: string, 30 | name: string, 31 | platform: string, 32 | } 33 | 34 | async function produceArtifact({ type, name, platform }: ArtifactType) { 35 | platform = platform || 'JSON'; 36 | 37 | // produce Clash node format for ShadowRocket 38 | if (platform === 'ShadowRocket') platform = 'Clash'; 39 | 40 | if (type === 'subscription') { 41 | const allSubs = await $.read(SUBS_KEY); 42 | const sub = findByName(allSubs, name); 43 | let raw; 44 | if (sub.source === 'local') { 45 | raw = sub.content; 46 | } else { 47 | raw = await download(sub.url, sub.ua); 48 | } 49 | $.info('down done') 50 | 51 | // parse proxies 52 | let proxies = ProxyUtils.parse(raw); 53 | // apply processors 54 | proxies = await ProxyUtils.process( 55 | proxies, 56 | sub.process || [], 57 | platform, 58 | ); 59 | // check duplicate 60 | const exist = {}; 61 | for (const proxy of proxies) { 62 | if (exist[proxy.name]) { 63 | $.notify( 64 | '🌍 Sub-Store', 65 | '⚠️ 订阅包含重复节点!', 66 | '请仔细检测配置!', 67 | { 68 | 'media-url': 69 | 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png', 70 | }, 71 | ); 72 | break; 73 | } 74 | exist[proxy.name] = true; 75 | } 76 | // produce 77 | return ProxyUtils.produce(proxies, platform); 78 | } else if (type === 'collection') { 79 | const allSubs = await $.read(SUBS_KEY); 80 | const allCols = await $.read(COLLECTIONS_KEY); 81 | const collection = findByName(allCols, name); 82 | const subnames = collection.subscriptions; 83 | const results = {}; 84 | let processed = 0; 85 | 86 | await Promise.all( 87 | subnames.map(async (name) => { 88 | const sub = findByName(allSubs, name); 89 | try { 90 | $.info(`正在处理子订阅:${sub.name}...`); 91 | let raw; 92 | if (sub.source === 'local') { 93 | raw = sub.content; 94 | } else { 95 | raw = await download(sub.url, sub.ua); 96 | } 97 | // parse proxies 98 | let currentProxies = ProxyUtils.parse(raw); 99 | // apply processors 100 | currentProxies = await ProxyUtils.process( 101 | currentProxies, 102 | sub.process || [], 103 | platform, 104 | ); 105 | results[name] = currentProxies; 106 | processed++; 107 | $.info( 108 | `✅ 子订阅:${sub.name}加载成功,进度--${ 109 | 100 * (processed / subnames.length).toFixed(1) 110 | }% `, 111 | ); 112 | } catch (err) { 113 | processed++; 114 | $.error( 115 | `❌ 处理组合订阅中的子订阅: ${ 116 | sub.name 117 | }时出现错误:${err},该订阅已被跳过!进度--${ 118 | 100 * (processed / subnames.length).toFixed(1) 119 | }%`, 120 | ); 121 | } 122 | }), 123 | ); 124 | 125 | // merge proxies with the original order 126 | let proxies = Array.prototype.concat.apply( 127 | [], 128 | subnames.map((name) => results[name]), 129 | ); 130 | 131 | // apply own processors 132 | proxies = await ProxyUtils.process( 133 | proxies, 134 | collection.process || [], 135 | platform, 136 | ); 137 | if (proxies.length === 0) { 138 | throw new Error(`组合订阅中不含有效节点!`); 139 | } 140 | // check duplicate 141 | const exist = {}; 142 | for (const proxy of proxies) { 143 | if (exist[proxy.name]) { 144 | $.notify( 145 | '🌍 Sub-Store', 146 | '⚠️ 订阅包含重复节点!', 147 | '请仔细检测配置!', 148 | { 149 | 'media-url': 150 | 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png', 151 | }, 152 | ); 153 | break; 154 | } 155 | exist[proxy.name] = true; 156 | } 157 | return ProxyUtils.produce(proxies, platform); 158 | } else if (type === 'rule') { 159 | const allRules = await $.read(RULES_KEY); 160 | const rule = findByName(allRules, name); 161 | let rules = []; 162 | for (let i = 0; i < rule.urls.length; i++) { 163 | const url = rule.urls[i]; 164 | $.info( 165 | `正在处理URL:${url},进度--${ 166 | 100 * ((i + 1) / rule.urls.length).toFixed(1) 167 | }% `, 168 | ); 169 | try { 170 | const { body } = await download(url); 171 | const currentRules = RuleUtils.parse(body); 172 | rules = rules.concat(currentRules); 173 | } catch (err) { 174 | $.error( 175 | `处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`, 176 | ); 177 | } 178 | } 179 | // remove duplicates 180 | rules = await RuleUtils.process(rules, [ 181 | { type: 'Remove Duplicate Filter' }, 182 | ]); 183 | // produce output 184 | return RuleUtils.produce(rules, platform); 185 | } 186 | } 187 | 188 | const syncAllArtifacts = async (c: Context) => { 189 | $.info('开始同步所有远程配置...'); 190 | const allArtifacts = await $.read(ARTIFACTS_KEY); 191 | const files = {}; 192 | 193 | try { 194 | await Promise.all( 195 | allArtifacts.map(async (artifact: any) => { 196 | if (artifact.sync) { 197 | $.info(`正在同步云配置:${artifact.name}...`); 198 | const output = await produceArtifact({ 199 | type: artifact.type, 200 | name: artifact.source, 201 | platform: artifact.platform, 202 | }); 203 | 204 | files[artifact.name] = { 205 | content: output, 206 | }; 207 | } 208 | }), 209 | ); 210 | 211 | const resp = await syncToGist(files); 212 | const body = JSON.parse(resp.body); 213 | 214 | for (const artifact of allArtifacts) { 215 | if (artifact.sync) { 216 | artifact.updated = new Date().getTime(); 217 | // extract real url from gist 218 | artifact.url = body.files[artifact.name].raw_url.replace( 219 | /\/raw\/[^/]*\/(.*)/, 220 | '/raw/$1', 221 | ); 222 | } 223 | } 224 | 225 | await $.write(allArtifacts, ARTIFACTS_KEY); 226 | $.info('全部订阅同步成功!'); 227 | return success(c); 228 | } catch (err) { 229 | $.info(`同步订阅失败,原因:${err}`); 230 | return failed( 231 | c, 232 | new InternalServerError( 233 | `FAILED_TO_SYNC_ARTIFACTS`, 234 | `Failed to sync all artifacts`, 235 | `Reason: ${err}`, 236 | ), 237 | ); 238 | } 239 | } 240 | 241 | const syncArtifact = async (c: Context) => { 242 | let { name } = c.req.param(); 243 | name = decodeURIComponent(name); 244 | const allArtifacts = await $.read(ARTIFACTS_KEY); 245 | const artifact = findByName(allArtifacts, name); 246 | 247 | if (!artifact) { 248 | return failed( 249 | c, 250 | new ResourceNotFoundError( 251 | 'RESOURCE_NOT_FOUND', 252 | `Artifact ${name} does not exist!`, 253 | ), 254 | 404, 255 | ); 256 | } 257 | 258 | const output = await produceArtifact({ 259 | type: artifact.type, 260 | name: artifact.source, 261 | platform: artifact.platform, 262 | }); 263 | 264 | $.info( 265 | `正在上传配置:${artifact.name}\n>>>${JSON.stringify( 266 | artifact, 267 | null, 268 | 2, 269 | )}`, 270 | ); 271 | try { 272 | const resp = await syncToGist({ 273 | [encodeURIComponent(artifact.name)]: { 274 | content: output, 275 | }, 276 | }); 277 | artifact.updated = new Date().getTime(); 278 | const body = JSON.parse(resp.body); 279 | artifact.url = body.files[ 280 | encodeURIComponent(artifact.name) 281 | ].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); 282 | await $.write(allArtifacts, ARTIFACTS_KEY); 283 | return success(c, artifact); 284 | } catch (err) { 285 | return failed( 286 | c, 287 | new InternalServerError( 288 | `FAILED_TO_SYNC_ARTIFACT`, 289 | `Failed to sync artifact ${name}`, 290 | `Reason: ${err}`, 291 | ), 292 | ); 293 | } 294 | } 295 | 296 | export { produceArtifact }; 297 | -------------------------------------------------------------------------------- /src/test/proxy-parsers/loon.spec.js: -------------------------------------------------------------------------------- 1 | import getLoonParser from '@/core/proxy-utils/parsers/peggy/loon'; 2 | import { describe, it } from 'mocha'; 3 | import testcases from './testcases'; 4 | import { expect } from 'chai'; 5 | 6 | const parser = getLoonParser(); 7 | 8 | describe('Loon', function () { 9 | describe('shadowsocks', function () { 10 | it('test shadowsocks simple', function () { 11 | const { input, expected } = testcases.SS.SIMPLE; 12 | const proxy = parser.parse(input.Loon); 13 | expect(proxy).eql(expected); 14 | }); 15 | it('test shadowsocks obfs + tls', function () { 16 | const { input, expected } = testcases.SS.OBFS_TLS; 17 | const proxy = parser.parse(input.Loon); 18 | expect(proxy).eql(expected); 19 | }); 20 | it('test shadowsocks obfs + http', function () { 21 | const { input, expected } = testcases.SS.OBFS_HTTP; 22 | const proxy = parser.parse(input.Loon); 23 | expect(proxy).eql(expected); 24 | }); 25 | }); 26 | 27 | describe('shadowsocksr', function () { 28 | it('test shadowsocksr simple', function () { 29 | const { input, expected } = testcases.SSR.SIMPLE; 30 | const proxy = parser.parse(input.Loon); 31 | expect(proxy).eql(expected); 32 | }); 33 | }); 34 | 35 | describe('trojan', function () { 36 | it('test trojan simple', function () { 37 | const { input, expected } = testcases.TROJAN.SIMPLE; 38 | const proxy = parser.parse(input.Loon); 39 | expect(proxy).eql(expected); 40 | }); 41 | 42 | it('test trojan + ws', function () { 43 | const { input, expected } = testcases.TROJAN.WS; 44 | const proxy = parser.parse(input.Loon); 45 | expect(proxy).eql(expected); 46 | }); 47 | 48 | it('test trojan + wss', function () { 49 | const { input, expected } = testcases.TROJAN.WSS; 50 | const proxy = parser.parse(input.Loon); 51 | expect(proxy).eql(expected); 52 | }); 53 | }); 54 | 55 | describe('vmess', function () { 56 | it('test vmess simple', function () { 57 | const { input, expected } = testcases.VMESS.SIMPLE; 58 | const proxy = parser.parse(input.Loon); 59 | expect(proxy).eql(expected.Loon); 60 | }); 61 | 62 | it('test vmess + aead', function () { 63 | const { input, expected } = testcases.VMESS.AEAD; 64 | const proxy = parser.parse(input.Loon); 65 | expect(proxy).eql(expected.Loon); 66 | }); 67 | 68 | it('test vmess + ws', function () { 69 | const { input, expected } = testcases.VMESS.WS; 70 | const proxy = parser.parse(input.Loon); 71 | expect(proxy).eql(expected.Loon); 72 | }); 73 | 74 | it('test vmess + wss', function () { 75 | const { input, expected } = testcases.VMESS.WSS; 76 | const proxy = parser.parse(input.Loon); 77 | expect(proxy).eql(expected.Loon); 78 | }); 79 | 80 | it('test vmess + http', function () { 81 | const { input, expected } = testcases.VMESS.HTTP; 82 | const proxy = parser.parse(input.Loon); 83 | expect(proxy).eql(expected.Loon); 84 | }); 85 | 86 | it('test vmess + http + tls', function () { 87 | const { input, expected } = testcases.VMESS.HTTP_TLS; 88 | const proxy = parser.parse(input.Loon); 89 | expect(proxy).eql(expected.Loon); 90 | }); 91 | }); 92 | 93 | describe('vless', function () { 94 | it('test vless simple', function () { 95 | const { input, expected } = testcases.VLESS.SIMPLE; 96 | const proxy = parser.parse(input.Loon); 97 | expect(proxy).eql(expected.Loon); 98 | }); 99 | 100 | it('test vless + ws', function () { 101 | const { input, expected } = testcases.VLESS.WS; 102 | const proxy = parser.parse(input.Loon); 103 | expect(proxy).eql(expected.Loon); 104 | }); 105 | 106 | it('test vless + wss', function () { 107 | const { input, expected } = testcases.VLESS.WSS; 108 | const proxy = parser.parse(input.Loon); 109 | expect(proxy).eql(expected.Loon); 110 | }); 111 | 112 | it('test vless + http', function () { 113 | const { input, expected } = testcases.VLESS.HTTP; 114 | const proxy = parser.parse(input.Loon); 115 | expect(proxy).eql(expected.Loon); 116 | }); 117 | 118 | it('test vless + http + tls', function () { 119 | const { input, expected } = testcases.VLESS.HTTP_TLS; 120 | const proxy = parser.parse(input.Loon); 121 | expect(proxy).eql(expected.Loon); 122 | }); 123 | }); 124 | 125 | describe('http(s)', function () { 126 | it('test http simple', function () { 127 | const { input, expected } = testcases.HTTP.SIMPLE; 128 | const proxy = parser.parse(input.Loon); 129 | expect(proxy).eql(expected); 130 | }); 131 | 132 | it('test http with authentication', function () { 133 | const { input, expected } = testcases.HTTP.AUTH; 134 | const proxy = parser.parse(input.Loon); 135 | expect(proxy).eql(expected); 136 | }); 137 | 138 | it('test https', function () { 139 | const { input, expected } = testcases.HTTP.TLS; 140 | const proxy = parser.parse(input.Loon); 141 | expect(proxy).eql(expected); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/test/proxy-parsers/qx.spec.js: -------------------------------------------------------------------------------- 1 | import getQXParser from '@/core/proxy-utils/parsers/peggy/qx'; 2 | import { describe, it } from 'mocha'; 3 | import testcases from './testcases'; 4 | import { expect } from 'chai'; 5 | 6 | const parser = getQXParser(); 7 | 8 | describe('QX', function () { 9 | describe('shadowsocks', function () { 10 | it('test shadowsocks simple', function () { 11 | const { input, expected } = testcases.SS.SIMPLE; 12 | const proxy = parser.parse(input.QX); 13 | expect(proxy).eql(expected); 14 | }); 15 | it('test shadowsocks obfs + tls', function () { 16 | const { input, expected } = testcases.SS.OBFS_TLS; 17 | const proxy = parser.parse(input.QX); 18 | expect(proxy).eql(expected); 19 | }); 20 | it('test shadowsocks obfs + http', function () { 21 | const { input, expected } = testcases.SS.OBFS_HTTP; 22 | const proxy = parser.parse(input.QX); 23 | expect(proxy).eql(expected); 24 | }); 25 | it('test shadowsocks v2ray-plugin + ws', function () { 26 | const { input, expected } = testcases.SS.V2RAY_PLUGIN_WS; 27 | const proxy = parser.parse(input.QX); 28 | expect(proxy).eql(expected); 29 | }); 30 | it('test shadowsocks v2ray-plugin + wss', function () { 31 | const { input, expected } = testcases.SS.V2RAY_PLUGIN_WSS; 32 | const proxy = parser.parse(input.QX); 33 | expect(proxy).eql(expected); 34 | }); 35 | }); 36 | 37 | describe('shadowsocksr', function () { 38 | it('test shadowsocksr simple', function () { 39 | const { input, expected } = testcases.SSR.SIMPLE; 40 | const proxy = parser.parse(input.QX); 41 | expect(proxy).eql(expected); 42 | }); 43 | }); 44 | 45 | describe('trojan', function () { 46 | it('test trojan simple', function () { 47 | const { input, expected } = testcases.TROJAN.SIMPLE; 48 | const proxy = parser.parse(input.QX); 49 | expect(proxy).eql(expected); 50 | }); 51 | 52 | it('test trojan + ws', function () { 53 | const { input, expected } = testcases.TROJAN.WS; 54 | const proxy = parser.parse(input.QX); 55 | expect(proxy).eql(expected); 56 | }); 57 | 58 | it('test trojan + wss', function () { 59 | const { input, expected } = testcases.TROJAN.WSS; 60 | const proxy = parser.parse(input.QX); 61 | expect(proxy).eql(expected); 62 | }); 63 | 64 | it('test trojan + tls fingerprint', function () { 65 | const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT; 66 | const proxy = parser.parse(input.QX); 67 | expect(proxy).eql(expected); 68 | }); 69 | }); 70 | 71 | describe('vmess', function () { 72 | it('test vmess simple', function () { 73 | const { input, expected } = testcases.VMESS.SIMPLE; 74 | const proxy = parser.parse(input.QX); 75 | expect(proxy).eql(expected.QX); 76 | }); 77 | 78 | it('test vmess aead', function () { 79 | const { input, expected } = testcases.VMESS.AEAD; 80 | const proxy = parser.parse(input.QX); 81 | expect(proxy).eql(expected.QX); 82 | }); 83 | 84 | it('test vmess + ws', function () { 85 | const { input, expected } = testcases.VMESS.WS; 86 | const proxy = parser.parse(input.QX); 87 | expect(proxy).eql(expected.QX); 88 | }); 89 | 90 | it('test vmess + wss', function () { 91 | const { input, expected } = testcases.VMESS.WSS; 92 | const proxy = parser.parse(input.QX); 93 | expect(proxy).eql(expected.QX); 94 | }); 95 | 96 | it('test vmess + http', function () { 97 | const { input, expected } = testcases.VMESS.HTTP; 98 | const proxy = parser.parse(input.QX); 99 | expect(proxy).eql(expected.QX); 100 | }); 101 | }); 102 | 103 | describe('http', function () { 104 | it('test http simple', function () { 105 | const { input, expected } = testcases.HTTP.SIMPLE; 106 | const proxy = parser.parse(input.QX); 107 | expect(proxy).eql(expected); 108 | }); 109 | 110 | it('test http with authentication', function () { 111 | const { input, expected } = testcases.HTTP.AUTH; 112 | const proxy = parser.parse(input.QX); 113 | expect(proxy).eql(expected); 114 | }); 115 | 116 | it('test https', function () { 117 | const { input, expected } = testcases.HTTP.TLS; 118 | const proxy = parser.parse(input.QX); 119 | expect(proxy).eql(expected); 120 | }); 121 | }); 122 | 123 | describe('socks5', function () { 124 | it('test socks5 simple', function () { 125 | const { input, expected } = testcases.SOCKS5.SIMPLE; 126 | const proxy = parser.parse(input.QX); 127 | expect(proxy).eql(expected); 128 | }); 129 | 130 | it('test socks5 with authentication', function () { 131 | const { input, expected } = testcases.SOCKS5.AUTH; 132 | const proxy = parser.parse(input.QX); 133 | expect(proxy).eql(expected); 134 | }); 135 | 136 | it('test socks5 + tls', function () { 137 | const { input, expected } = testcases.SOCKS5.TLS; 138 | const proxy = parser.parse(input.QX); 139 | expect(proxy).eql(expected); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/test/proxy-parsers/surge.spec.js: -------------------------------------------------------------------------------- 1 | import getSurgeParser from '@/core/proxy-utils/parsers/peggy/surge'; 2 | import { describe, it } from 'mocha'; 3 | import testcases from './testcases'; 4 | import { expect } from 'chai'; 5 | 6 | const parser = getSurgeParser(); 7 | 8 | describe('Surge', function () { 9 | describe('shadowsocks', function () { 10 | it('test shadowsocks simple', function () { 11 | const { input, expected } = testcases.SS.SIMPLE; 12 | const proxy = parser.parse(input.Surge); 13 | expect(proxy).eql(expected); 14 | }); 15 | it('test shadowsocks obfs + tls', function () { 16 | const { input, expected } = testcases.SS.OBFS_TLS; 17 | const proxy = parser.parse(input.Surge); 18 | expect(proxy).eql(expected); 19 | }); 20 | it('test shadowsocks obfs + http', function () { 21 | const { input, expected } = testcases.SS.OBFS_HTTP; 22 | const proxy = parser.parse(input.Surge); 23 | expect(proxy).eql(expected); 24 | }); 25 | }); 26 | 27 | describe('trojan', function () { 28 | it('test trojan simple', function () { 29 | const { input, expected } = testcases.TROJAN.SIMPLE; 30 | const proxy = parser.parse(input.Surge); 31 | expect(proxy).eql(expected); 32 | }); 33 | 34 | it('test trojan + ws', function () { 35 | const { input, expected } = testcases.TROJAN.WS; 36 | const proxy = parser.parse(input.Surge); 37 | expect(proxy).eql(expected); 38 | }); 39 | 40 | it('test trojan + wss', function () { 41 | const { input, expected } = testcases.TROJAN.WSS; 42 | const proxy = parser.parse(input.Surge); 43 | expect(proxy).eql(expected); 44 | }); 45 | 46 | it('test trojan + tls fingerprint', function () { 47 | const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT; 48 | const proxy = parser.parse(input.Surge); 49 | expect(proxy).eql(expected); 50 | }); 51 | }); 52 | 53 | describe('vmess', function () { 54 | it('test vmess simple', function () { 55 | const { input, expected } = testcases.VMESS.SIMPLE; 56 | const proxy = parser.parse(input.Surge); 57 | expect(proxy).eql(expected.Surge); 58 | }); 59 | 60 | it('test vmess aead', function () { 61 | const { input, expected } = testcases.VMESS.AEAD; 62 | const proxy = parser.parse(input.Surge); 63 | expect(proxy).eql(expected.Surge); 64 | }); 65 | 66 | it('test vmess + ws', function () { 67 | const { input, expected } = testcases.VMESS.WS; 68 | const proxy = parser.parse(input.Surge); 69 | expect(proxy).eql(expected.Surge); 70 | }); 71 | 72 | it('test vmess + wss', function () { 73 | const { input, expected } = testcases.VMESS.WSS; 74 | const proxy = parser.parse(input.Surge); 75 | expect(proxy).eql(expected.Surge); 76 | }); 77 | }); 78 | 79 | describe('http', function () { 80 | it('test http simple', function () { 81 | const { input, expected } = testcases.HTTP.SIMPLE; 82 | const proxy = parser.parse(input.Surge); 83 | expect(proxy).eql(expected); 84 | }); 85 | 86 | it('test http with authentication', function () { 87 | const { input, expected } = testcases.HTTP.AUTH; 88 | const proxy = parser.parse(input.Surge); 89 | expect(proxy).eql(expected); 90 | }); 91 | 92 | it('test https', function () { 93 | const { input, expected } = testcases.HTTP.TLS; 94 | const proxy = parser.parse(input.Surge); 95 | expect(proxy).eql(expected); 96 | }); 97 | }); 98 | 99 | describe('socks5', function () { 100 | it('test socks5 simple', function () { 101 | const { input, expected } = testcases.SOCKS5.SIMPLE; 102 | const proxy = parser.parse(input.Surge); 103 | expect(proxy).eql(expected); 104 | }); 105 | 106 | it('test socks5 with authentication', function () { 107 | const { input, expected } = testcases.SOCKS5.AUTH; 108 | const proxy = parser.parse(input.Surge); 109 | expect(proxy).eql(expected); 110 | }); 111 | 112 | it('test socks5 + tls', function () { 113 | const { input, expected } = testcases.SOCKS5.TLS; 114 | const proxy = parser.parse(input.Surge); 115 | expect(proxy).eql(expected); 116 | }); 117 | }); 118 | 119 | describe('snell', function () { 120 | it('test snell simple', function () { 121 | const { input, expected } = testcases.SNELL.SIMPLE; 122 | const proxy = parser.parse(input.Surge); 123 | expect(proxy).eql(expected); 124 | }); 125 | 126 | it('test snell obfs + http', function () { 127 | const { input, expected } = testcases.SNELL.OBFS_HTTP; 128 | const proxy = parser.parse(input.Surge); 129 | expect(proxy).eql(expected); 130 | }); 131 | 132 | it('test snell obfs + tls', function () { 133 | const { input, expected } = testcases.SNELL.OBFS_TLS; 134 | const proxy = parser.parse(input.Surge); 135 | expect(proxy).eql(expected); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/utils/database.ts: -------------------------------------------------------------------------------- 1 | export function findByName(list: Array, name: any) { 2 | return [...list].find((item) => item.name === name); 3 | } 4 | 5 | export function findIndexByName(list: Array, name: any) { 6 | return [...list].findIndex((item) => item.name === name); 7 | } 8 | 9 | export function deleteByName(list: Array, name: any) { 10 | list = [...list]; 11 | const idx = findIndexByName(list, name); 12 | list.splice(idx, 1); 13 | return list; 14 | } 15 | 16 | export function updateByName(list: Array, name: any, newItem: any) { 17 | const idx = findIndexByName(list, name); 18 | return [...list][idx] = newItem; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | import { HTTP } from '../vendor/open-api'; 2 | import { hex_md5 } from '../vendor/md5'; 3 | import resourceCache from '../utils/resource-cache'; 4 | 5 | export default async function download(url: string, ua: string) { 6 | 7 | ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; 8 | const id = hex_md5(ua + url); 9 | const http = HTTP({ 10 | headers: { 11 | 'User-Agent': ua, 12 | }, 13 | }); 14 | 15 | const result = new Promise(async (resolve, reject) => { 16 | // try to find in app cache 17 | const cached = await resourceCache.get(id); 18 | if (cached) { 19 | resolve(cached); 20 | } else { 21 | await http.get(url) 22 | .then(async (resp: Response) => { 23 | const body = await resp.text(); 24 | if (body.replace(/\s/g, '').length === 0) 25 | reject(new Error('远程资源内容为空!')); 26 | else { 27 | await resourceCache.set(id, body); 28 | resolve(body); 29 | } 30 | }) 31 | .catch(() => { 32 | reject(new Error(`无法下载 URL:${url}`)); 33 | }); 34 | } 35 | }); 36 | return await result; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/flow.ts: -------------------------------------------------------------------------------- 1 | import { HTTP } from '../vendor/open-api'; 2 | 3 | export async function getFlowHeaders(url: string) { 4 | const http = HTTP({ 5 | headers: { 6 | 'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)', 7 | } 8 | }); 9 | const { headers } = await http.get(url); 10 | return headers.get('SUBSCRIPTION-USERINFO'); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/geo.ts: -------------------------------------------------------------------------------- 1 | // get proxy flag according to its name 2 | export function getFlag(name) { 3 | // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js 4 | // flags from @surgioproject: https://github.com/surgioproject/surgio/blob/master/lib/misc/flag_cn.ts 5 | 6 | // refer: https://zh.wikipedia.org/wiki/ISO_3166-1二位字母代码 7 | // refer: https://zh.wikipedia.org/wiki/ISO_3166-1三位字母代码 8 | const Flags = { 9 | '🏳️‍🌈': ['流量', '时间', '过期', 'Bandwidth', 'Expire'], 10 | '🇸🇱': ['应急', '测试节点'], 11 | '🇦🇩': ['Andorra', '安道尔'], 12 | '🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜'], 13 | '🇦🇫': ['Afghanistan', '阿富汗'], 14 | '🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'], 15 | '🇦🇲': ['Armenia', '亚美尼亚'], 16 | '🇦🇷': ['Argentina', '阿根廷'], 17 | '🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'], 18 | '🇦🇺': [ 19 | 'Australia', 20 | '澳大利亚', 21 | '澳洲', 22 | '墨尔本', 23 | '悉尼', 24 | '土澳', 25 | '京澳', 26 | '廣澳', 27 | '滬澳', 28 | '沪澳', 29 | '广澳', 30 | 'Sydney', 31 | ], 32 | '🇦🇿': ['Azerbaijan', '阿塞拜疆'], 33 | '🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'], 34 | '🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'], 35 | '🇧🇪': ['Belgium', '比利时', '比利時'], 36 | '🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'], 37 | '🇧🇭': ['Bahrain', '巴林'], 38 | '🇧🇷': ['Brazil', '巴西', '圣保罗'], 39 | '🇧🇾': ['Belarus', '白俄罗斯', '白俄'], 40 | '🇨🇦': [ 41 | 'Canada', 42 | '加拿大', 43 | '蒙特利尔', 44 | '温哥华', 45 | '楓葉', 46 | '枫叶', 47 | '滑铁卢', 48 | '多伦多', 49 | 'Waterloo', 50 | ], 51 | '🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'], 52 | '🇨🇱': ['Chile', '智利'], 53 | '🇨🇴': ['Colombia', '哥伦比亚'], 54 | '🇨🇷': ['Costa Rica', '哥斯达黎加'], 55 | '🇨🇾': ['Cyprus', '塞浦路斯'], 56 | '🇨🇿': ['Czechia', '捷克'], 57 | '🇩🇪': [ 58 | 'German', 59 | '德国', 60 | '德國', 61 | '京德', 62 | '滬德', 63 | '廣德', 64 | '沪德', 65 | '广德', 66 | '法兰克福', 67 | 'Frankfurt', 68 | ], 69 | '🇩🇰': ['Denmark', '丹麦', '丹麥'], 70 | '🇪🇨': ['Ecuador', '厄瓜多尔'], 71 | '🇪🇪': ['Estonia', '爱沙尼亚'], 72 | '🇪🇬': ['Egypt', '埃及'], 73 | '🇪🇸': ['Spain', '西班牙'], 74 | '🇪🇺': ['European Union', '欧盟', '欧罗巴'], 75 | '🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'], 76 | '🇫🇷': ['France', '法国', '法國', '巴黎'], 77 | '🇬🇧': [ 78 | 'Great Britain', 79 | '英国', 80 | 'England', 81 | 'United Kingdom', 82 | '伦敦', 83 | '英', 84 | 'London', 85 | ], 86 | '🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'], 87 | '🇬🇷': ['Greece', '希腊', '希臘'], 88 | '🇭🇰': [ 89 | 'Hongkong', 90 | '香港', 91 | 'Hong Kong', 92 | 'HongKong', 93 | 'HONG KONG', 94 | '深港', 95 | '沪港', 96 | '呼港', 97 | '穗港', 98 | '京港', 99 | '港', 100 | ], 101 | '🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'], 102 | '🇭🇺': ['Hungary', '匈牙利'], 103 | '🇯🇴': ['Jordan', '约旦'], 104 | '🇯🇵': [ 105 | 'Japan', 106 | '日本', 107 | '东京', 108 | '大阪', 109 | '埼玉', 110 | '沪日', 111 | '穗日', 112 | '川日', 113 | '中日', 114 | '泉日', 115 | '杭日', 116 | '深日', 117 | '辽日', 118 | '广日', 119 | '大坂', 120 | 'Osaka', 121 | 'Tokyo', 122 | ], 123 | '🇰🇪': ['Kenya', '肯尼亚'], 124 | '🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'], 125 | '🇰🇭': ['Cambodia', '柬埔寨'], 126 | '🇰🇵': ['North Korea', '朝鲜'], 127 | '🇰🇷': [ 128 | 'Korea', 129 | '韩国', 130 | '韓國', 131 | '韩', 132 | '韓', 133 | '首尔', 134 | '春川', 135 | 'Chuncheon', 136 | 'Seoul', 137 | ], 138 | '🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'], 139 | '🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'], 140 | '🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'], 141 | '🇮🇱': ['Israel', '以色列'], 142 | '🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'], 143 | '🇮🇳': ['India', '印度', '孟买', 'MFumbai'], 144 | '🇮🇷': ['Iran', '伊朗'], 145 | '🇮🇸': ['Iceland', '冰岛', '冰島'], 146 | '🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'], 147 | '🇱🇹': ['Lithuania', '立陶宛'], 148 | '🇱🇺': ['Luxembourg', '卢森堡'], 149 | '🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'], 150 | '🇲🇦': ['Morocco', '摩洛哥'], 151 | '🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'], 152 | '🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'], 153 | '🇲🇰': ['Macedonia', '马其顿', '馬其頓'], 154 | '🇲🇳': ['Mongolia', '蒙古'], 155 | '🇲🇴': ['Macao', '澳门', '澳門', 'CTM'], 156 | '🇲🇹': ['Malta', '马耳他'], 157 | '🇲🇽': ['Mexico', '墨西哥'], 158 | '🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'], 159 | '🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'], 160 | '🇳🇴': ['Norway', '挪威'], 161 | '🇳🇵': ['Nepal', '尼泊尔'], 162 | '🇳🇿': ['New Zealand', '新西兰', '新西蘭'], 163 | '🇵🇦': ['Panama', '巴拿马'], 164 | '🇵🇪': ['Peru', '秘鲁', '祕魯'], 165 | '🇵🇭': ['Philippines', '菲律宾', '菲律賓'], 166 | '🇵🇰': ['Pakistan', '巴基斯坦'], 167 | '🇵🇱': ['Poland', '波兰', '波蘭'], 168 | '🇵🇷': ['Puerto Rico', '波多黎各'], 169 | '🇵🇹': ['Portugal', '葡萄牙'], 170 | '🇵🇾': ['Paraguay', '巴拉圭'], 171 | '🇷🇴': ['Romania', '罗马尼亚'], 172 | '🇷🇸': ['Serbia', '塞尔维亚'], 173 | '🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'], 174 | '🇷🇺': [ 175 | 'Russia', 176 | '俄罗斯', 177 | '俄国', 178 | '俄羅斯', 179 | '伯力', 180 | '莫斯科', 181 | '圣彼得堡', 182 | '西伯利亚', 183 | '京俄', 184 | '杭俄', 185 | '廣俄', 186 | '滬俄', 187 | '广俄', 188 | '沪俄', 189 | 'Moscow', 190 | ], 191 | '🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'], 192 | '🇸🇪': ['Sweden', '瑞典'], 193 | '🇸🇬': [ 194 | 'Singapore', 195 | '新加坡', 196 | '狮城', 197 | '沪新', 198 | '京新', 199 | '中新', 200 | '泉新', 201 | '穗新', 202 | '深新', 203 | '杭新', 204 | '广新', 205 | '廣新', 206 | '滬新', 207 | ], 208 | '🇸🇮': ['Slovenia', '斯洛文尼亚'], 209 | '🇸🇰': ['Slovakia', '斯洛伐克'], 210 | '🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'], 211 | '🇹🇳': ['Tunisia', '突尼斯'], 212 | '🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'], 213 | '🇹🇼': [ 214 | 'Taiwan', 215 | '台湾', 216 | '台北', 217 | '台中', 218 | '新北', 219 | '彰化', 220 | '台', 221 | 'Taipei', 222 | ], 223 | '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'], 224 | '🇺🇸': [ 225 | 'United States', 226 | '美国', 227 | 'America', 228 | '美', 229 | '京美', 230 | '波特兰', 231 | '达拉斯', 232 | '俄勒冈', 233 | '凤凰城', 234 | '费利蒙', 235 | '硅谷', 236 | '矽谷', 237 | '拉斯维加斯', 238 | '洛杉矶', 239 | '圣何塞', 240 | '圣克拉拉', 241 | '西雅图', 242 | '芝加哥', 243 | '沪美', 244 | '哥伦布', 245 | '纽约', 246 | 'Los Angeles', 247 | 'San Jose', 248 | 'Sillicon Valley', 249 | 'Michigan', 250 | ], 251 | '🇺🇾': ['Uruguay', '乌拉圭'], 252 | '🇻🇪': ['Venezuela', '委内瑞拉'], 253 | '🇻🇳': ['Vietnam', '越南', '胡志明'], 254 | '🇿🇦': ['South Africa', '南非'], 255 | '🇨🇳': [ 256 | 'China', 257 | '中国', 258 | '中國', 259 | '回国', 260 | '回國', 261 | '国内', 262 | '國內', 263 | '华东', 264 | '华西', 265 | '华南', 266 | '华北', 267 | '华中', 268 | '江苏', 269 | '北京', 270 | '上海', 271 | '广州', 272 | '深圳', 273 | '杭州', 274 | '徐州', 275 | '青岛', 276 | '宁波', 277 | '镇江', 278 | ], 279 | }; 280 | 281 | const ISOFlags = { 282 | '🏳️‍🌈': ['EXP', 'BAND'], 283 | '🇸🇱': ['TEST', 'SOS'], 284 | '🇦🇩': ['AD', 'AND'], 285 | '🇦🇪': ['AE', 'ARE'], 286 | '🇦🇫': ['AF', 'AFG'], 287 | '🇦🇱': ['AL', 'ALB'], 288 | '🇦🇲': ['AM', 'ARM'], 289 | '🇦🇷': ['AR', 'ARG'], 290 | '🇦🇹': ['AT', 'AUT'], 291 | '🇦🇺': ['AU', 'AUS'], 292 | '🇦🇿': ['AZ', 'AZE'], 293 | '🇧🇦': ['BA', 'BIH'], 294 | '🇧🇩': ['BD', 'BGD'], 295 | '🇧🇪': ['BE', 'BEL'], 296 | '🇧🇬': ['BG', 'BGR'], 297 | '🇧🇭': ['BH', 'BHR'], 298 | '🇧🇷': ['BR', 'BRA'], 299 | '🇧🇾': ['BY', 'BLR'], 300 | '🇨🇦': ['CA', 'CAN'], 301 | '🇨🇭': ['CH', 'CHE'], 302 | '🇨🇱': ['CL', 'CHL'], 303 | '🇨🇴': ['CO', 'COL'], 304 | '🇨🇷': ['CR', 'CRI'], 305 | '🇨🇾': ['CY', 'CYP'], 306 | '🇨🇿': ['CZ', 'CZE'], 307 | '🇩🇪': ['DE', 'DEU'], 308 | '🇩🇰': ['DK', 'DNK'], 309 | '🇪🇨': ['EC', 'ECU'], 310 | '🇪🇪': ['EE', 'EST'], 311 | '🇪🇬': ['EG', 'EGY'], 312 | '🇪🇸': ['ES', 'ESP'], 313 | '🇪🇺': ['EU'], 314 | '🇫🇮': ['FI', 'FIN'], 315 | '🇫🇷': ['FR', 'FRA'], 316 | '🇬🇧': ['GB', 'GBR', 'UK'], 317 | '🇬🇪': ['GE', 'GEO'], 318 | '🇬🇷': ['GR', 'GRC'], 319 | '🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'], 320 | '🇭🇷': ['HR', 'HRV'], 321 | '🇭🇺': ['HU', 'HUN'], 322 | '🇯🇴': ['JO', 'JOR'], 323 | '🇯🇵': ['JP', 'JPN'], 324 | '🇰🇪': ['KE', 'KEN'], 325 | '🇰🇬': ['KG', 'KGZ'], 326 | '🇰🇭': ['KH', 'KGZ'], 327 | '🇰🇵': ['KP', 'PRK'], 328 | '🇰🇷': ['KR', 'KOR'], 329 | '🇰🇿': ['KZ', 'KAZ'], 330 | '🇮🇩': ['ID', 'IDN'], 331 | '🇮🇪': ['IE', 'IRL'], 332 | '🇮🇱': ['IL', 'ISR'], 333 | '🇮🇲': ['IM', 'IMN'], 334 | '🇮🇳': ['IN', 'IND'], 335 | '🇮🇷': ['IR', 'IRN'], 336 | '🇮🇸': ['IS', 'ISL'], 337 | '🇮🇹': ['IT', 'ITA'], 338 | '🇱🇹': ['LT', 'LTU'], 339 | '🇱🇺': ['LU', 'LUX'], 340 | '🇱🇻': ['LV', 'LVA'], 341 | '🇲🇦': ['MA', 'MAR'], 342 | '🇲🇩': ['MD', 'MDA'], 343 | '🇳🇬': ['NG', 'NGA'], 344 | '🇲🇰': ['MK', 'MKD'], 345 | '🇲🇳': ['MN', 'MNG'], 346 | '🇲🇴': ['MO', 'MAC', 'CTM'], 347 | '🇲🇹': ['MT', 'MLT'], 348 | '🇲🇽': ['MX', 'MEX'], 349 | '🇲🇾': ['MY', 'MYS'], 350 | '🇳🇱': ['NL', 'NLD'], 351 | '🇳🇴': ['NO', 'NOR'], 352 | '🇳🇵': ['NP', 'NPL'], 353 | '🇳🇿': ['NZ', 'NZL'], 354 | '🇵🇦': ['PA', 'PAN'], 355 | '🇵🇪': ['PE', 'PER'], 356 | '🇵🇭': ['PH', 'PHL'], 357 | '🇵🇰': ['PK', 'PAK'], 358 | '🇵🇱': ['PL', 'POL'], 359 | '🇵🇷': ['PR', 'PRI'], 360 | '🇵🇹': ['PT', 'PRT'], 361 | '🇵🇾': ['PY', 'PRY'], 362 | '🇷🇴': ['RO', 'ROU'], 363 | '🇷🇸': ['RS', 'SRB'], 364 | '🇷🇪': ['RE', 'REU'], 365 | '🇷🇺': ['RU', 'RUS'], 366 | '🇸🇦': ['SA', 'SAU'], 367 | '🇸🇪': ['SE', 'SWE'], 368 | '🇸🇬': ['SG', 'SGP'], 369 | '🇸🇮': ['SI', 'SVN'], 370 | '🇸🇰': ['SK', 'SVK'], 371 | '🇹🇭': ['TH', 'THA'], 372 | '🇹🇳': ['TN', 'TUN'], 373 | '🇹🇷': ['TR', 'TUR'], 374 | '🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'], 375 | '🇺🇦': ['UA', 'UKR'], 376 | '🇺🇸': ['US', 'USA', 'LAX', 'SFO'], 377 | '🇺🇾': ['UY', 'URY'], 378 | '🇻🇪': ['VE', 'VEN'], 379 | '🇻🇳': ['VN', 'VNM'], 380 | '🇿🇦': ['ZA', 'ZAF'], 381 | '🇨🇳': ['CN', 'CHN', 'BACK'], 382 | }; 383 | // 原旗帜或空 384 | let Flag = 385 | name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] || 386 | '🏴‍☠️'; 387 | //console.log(`oldFlag = ${Flag}`) 388 | // 旗帜匹配 389 | for (let flag of Object.keys(Flags)) { 390 | const keywords = Flags[flag]; 391 | //console.log(`keywords = ${keywords}`) 392 | if ( 393 | // 不精确匹配(只要包含就算,忽略大小写) 394 | keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name)) 395 | ) { 396 | //console.log(`newFlag = ${flag}`) 397 | return (Flag = flag); 398 | } 399 | } 400 | // ISO旗帜匹配 401 | for (let flag of Object.keys(ISOFlags)) { 402 | const keywords = ISOFlags[flag]; 403 | //console.log(`keywords = ${keywords}`) 404 | if ( 405 | // 精确匹配(两侧均有分割) 406 | keywords.some((keyword) => 407 | RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name), 408 | ) 409 | ) { 410 | //console.log(`ISOFlag = ${flag}`) 411 | return (Flag = flag); 412 | } 413 | } 414 | //console.log(`Final Flag = ${Flag}`) 415 | return Flag; 416 | } 417 | -------------------------------------------------------------------------------- /src/utils/gist.ts: -------------------------------------------------------------------------------- 1 | import { HTTP } from '../vendor/open-api'; 2 | 3 | /** 4 | * Gist backup 5 | */ 6 | export default class Gist { 7 | constructor({ token, key }) { 8 | this.http = HTTP({ 9 | baseURL: 'https://api.github.com', 10 | headers: { 11 | Authorization: `token ${token}`, 12 | 'User-Agent': 13 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', 14 | }, 15 | events: { 16 | onResponse: (resp) => { 17 | if (/^[45]/.test(String(resp.statusCode))) { 18 | return Promise.reject( 19 | `ERROR: ${JSON.parse(resp.body).message}`, 20 | ); 21 | } else { 22 | return resp; 23 | } 24 | }, 25 | }, 26 | }); 27 | this.key = key; 28 | } 29 | 30 | async locate() { 31 | return this.http.get('/gists').then((response: Response) => response.json()).then((gists: any) => { 32 | for (let g of gists) { 33 | if (g.description === this.key) { 34 | return g.id; 35 | } 36 | } 37 | return -1; 38 | }); 39 | } 40 | 41 | async upload(files) { 42 | const id = await this.locate(); 43 | 44 | if (id === -1) { 45 | // create a new gist for backup 46 | return this.http.post({ 47 | url: '/gists', 48 | body: JSON.stringify({ 49 | description: this.key, 50 | public: false, 51 | files, 52 | }), 53 | }); 54 | } else { 55 | // update an existing gist 56 | return this.http.patch({ 57 | url: `/gists/${id}`, 58 | body: JSON.stringify({ files }), 59 | }); 60 | } 61 | } 62 | 63 | async download(filename) { 64 | const id = await this.locate(); 65 | if (id === -1) { 66 | return Promise.reject('未找到Gist备份!'); 67 | } else { 68 | try { 69 | const { files } = await this.http 70 | .get(`/gists/${id}`) 71 | .then((resp: Response) => resp.json()); 72 | const url = files[filename].raw_url; 73 | return await this.http.get(url).then((resp: Response) => resp.json()); 74 | } catch (err) { 75 | return Promise.reject(err); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // source: https://stackoverflow.com/a/36760050 2 | const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/; 3 | 4 | // source: https://ihateregex.io/expr/ipv6/ 5 | const IPV6_REGEX = 6 | /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; 7 | 8 | function isIPv4(ip) { 9 | return IPV4_REGEX.test(ip); 10 | } 11 | 12 | function isIPv6(ip) { 13 | return IPV6_REGEX.test(ip); 14 | } 15 | 16 | function isNotBlank(str) { 17 | return typeof str === 'string' && str.trim().length > 0; 18 | } 19 | 20 | function getIfNotBlank(str, defaultValue) { 21 | return isNotBlank(str) ? str : defaultValue; 22 | } 23 | 24 | function isPresent(obj) { 25 | return typeof obj !== 'undefined' && obj !== null; 26 | } 27 | 28 | function getIfPresent(obj, defaultValue) { 29 | return isPresent(obj) ? obj : defaultValue; 30 | } 31 | 32 | export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent }; 33 | -------------------------------------------------------------------------------- /src/utils/logical.ts: -------------------------------------------------------------------------------- 1 | function AND(...args) { 2 | return args.reduce((a, b) => a.map((c, i) => b[i] && c)); 3 | } 4 | 5 | function OR(...args) { 6 | return args.reduce((a, b) => a.map((c, i) => b[i] || c)); 7 | } 8 | 9 | function NOT(array) { 10 | return array.map((c) => !c); 11 | } 12 | 13 | function FULL(length, bool) { 14 | return [...Array(length).keys()].map(() => bool); 15 | } 16 | 17 | export { AND, OR, NOT, FULL }; 18 | -------------------------------------------------------------------------------- /src/utils/migration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SUBS_KEY, 3 | COLLECTIONS_KEY, 4 | SCHEMA_VERSION_KEY, 5 | ARTIFACTS_KEY, 6 | RULES_KEY, 7 | } from '../constants'; 8 | import $ from '../core/app'; 9 | 10 | export default function migrate() { 11 | migrateV2(); 12 | } 13 | 14 | function migrateV2() { 15 | const version = $.read(SCHEMA_VERSION_KEY); 16 | if (!version) doMigrationV2(); 17 | 18 | // write the current version 19 | if (version !== '2.0') { 20 | $.write('2.0', SCHEMA_VERSION_KEY); 21 | } 22 | } 23 | 24 | function doMigrationV2() { 25 | $.info('Start migrating...'); 26 | // 1. migrate subscriptions 27 | const subs = {}; 28 | // const subs = $.read(SUBS_KEY) || {}; 29 | const newSubs = Object.values(subs).map((s) => { 30 | // set default source to remote 31 | s.source = s.source || 'remote'; 32 | 33 | migrateDisplayName(s); 34 | migrateProcesses(s); 35 | return s; 36 | }); 37 | $.write(newSubs, SUBS_KEY); 38 | 39 | // 2. migrate collections 40 | const collections = $.read(COLLECTIONS_KEY) || {}; 41 | const newCollections = Object.values(collections).map((collection) => { 42 | delete collection.ua; 43 | migrateDisplayName(collection); 44 | migrateProcesses(collection); 45 | return collection; 46 | }); 47 | $.write(newCollections, COLLECTIONS_KEY); 48 | 49 | // 3. migrate artifacts 50 | const artifacts = $.read(ARTIFACTS_KEY) || {}; 51 | const newArtifacts = Object.values(artifacts); 52 | $.write(newArtifacts, ARTIFACTS_KEY); 53 | 54 | // 4. migrate rules 55 | const rules = $.read(RULES_KEY) || {}; 56 | const newRules = Object.values(rules); 57 | $.write(newRules, RULES_KEY); 58 | 59 | // 5. delete builtin rules 60 | delete $.cache.builtin; 61 | $.info('Migration complete!'); 62 | 63 | function migrateDisplayName(item) { 64 | const displayName = item['display-name']; 65 | if (displayName) { 66 | item.displayName = displayName; 67 | delete item['display-name']; 68 | } 69 | } 70 | 71 | function migrateProcesses(item) { 72 | const processes = item.process; 73 | if (!processes || processes.length === 0) return; 74 | const newProcesses = []; 75 | const quickSettingOperator = { 76 | type: 'Quick Setting Operator', 77 | args: { 78 | udp: 'DEFAULT', 79 | tfo: 'DEFAULT', 80 | scert: 'DEFAULT', 81 | 'vmess aead': 'DEFAULT', 82 | useless: 'DEFAULT', 83 | }, 84 | }; 85 | for (const p of processes) { 86 | if (!p.type) continue; 87 | if (p.type === 'Useless Filter') { 88 | quickSettingOperator.args.useless = 'ENABLED'; 89 | } else if (p.type === 'Set Property Operator') { 90 | const { key, value } = p.args; 91 | switch (key) { 92 | case 'udp': 93 | quickSettingOperator.args.udp = value 94 | ? 'ENABLED' 95 | : 'DISABLED'; 96 | break; 97 | case 'tfo': 98 | quickSettingOperator.args.tfo = value 99 | ? 'ENABLED' 100 | : 'DISABLED'; 101 | break; 102 | case 'skip-cert-verify': 103 | quickSettingOperator.args.scert = value 104 | ? 'ENABLED' 105 | : 'DISABLED'; 106 | break; 107 | case 'aead': 108 | quickSettingOperator.args['vmess aead'] = value 109 | ? 'ENABLED' 110 | : 'DISABLED'; 111 | break; 112 | } 113 | } else if (p.type.indexOf('Keyword') !== -1) { 114 | // drop keyword operators and keyword filters 115 | } else if (p.type === 'Flag Operator') { 116 | // set default args 117 | const add = typeof p.args === 'undefined' ? true : p.args; 118 | p.args = { 119 | mode: add ? 'add' : 'remove', 120 | }; 121 | newProcesses.push(p); 122 | } else { 123 | newProcesses.push(p); 124 | } 125 | } 126 | newProcesses.unshift(quickSettingOperator); 127 | item.process = newProcesses; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/utils/platform.ts: -------------------------------------------------------------------------------- 1 | export function getPlatformFromHeaders(headers:Headers) { 2 | const UA = headers.get('USER-AGENT') ?? ''; 3 | if (UA.indexOf('Quantumult%20X') !== -1) { 4 | return 'QX'; 5 | } else if (UA.indexOf('Surge') !== -1) { 6 | return 'Surge'; 7 | } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { 8 | return 'Loon'; 9 | } else if (UA.indexOf('Shadowrocket') !== -1) { 10 | return 'ShadowRocket'; 11 | } else if (UA.indexOf('Stash') !== -1) { 12 | return 'Stash'; 13 | } else { 14 | return 'JSON'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/resource-cache.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_EXPIRATION_TIME_MS, RESOURCE_CACHE_KEY } from '../constants'; 2 | import { DB } from '../db'; 3 | 4 | class ResourceCache { 5 | expires: number; 6 | constructor(expires: number) { 7 | this.expires = expires; 8 | } 9 | 10 | async revokeAll() { 11 | await DB.deleteFrom('resource_cache').execute(); 12 | } 13 | 14 | async get(key: string): Promise { 15 | const time = new Date().getTime() - this.expires; 16 | const result = await DB.selectFrom('resource_cache').selectAll().where('key', '=', key).where('time', '>', time).executeTakeFirst(); 17 | return !result ? null : JSON.parse(result.data); 18 | } 19 | 20 | async set(key: string, data: any): Promise { 21 | data = JSON.stringify(data); 22 | const time = new Date().getTime(); 23 | try { 24 | await DB 25 | .insertInto('resource_cache') 26 | .values([{ key, data, time }]) 27 | .onConflict((oc) => oc.column('key').doUpdateSet({ data, time })) 28 | .execute(); 29 | } catch (err) { 30 | throw err; 31 | } 32 | } 33 | } 34 | 35 | export default new ResourceCache(CACHE_EXPIRATION_TIME_MS); 36 | -------------------------------------------------------------------------------- /src/vendor/express.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | export default function express({ substore: $, port }) { 4 | port = port || 3000; 5 | const DEFAULT_HEADERS = { 6 | 'Content-Type': 'text/plain;charset=UTF-8', 7 | 'Access-Control-Allow-Origin': '*', 8 | 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE', 9 | 'Access-Control-Allow-Headers': 10 | 'Origin, X-Requested-With, Content-Type, Accept', 11 | }; 12 | 13 | // route handlers 14 | const handlers = []; 15 | 16 | // http methods 17 | const METHODS_NAMES = [ 18 | 'GET', 19 | 'POST', 20 | 'PUT', 21 | 'DELETE', 22 | 'PATCH', 23 | 'OPTIONS', 24 | "HEAD'", 25 | 'ALL', 26 | ]; 27 | 28 | // dispatch url to route 29 | const dispatch = (request, start = 0) => { 30 | let { method, url, headers, body } = request; 31 | headers = formatHeaders(headers); 32 | if (/json/i.test(headers['content-type'])) { 33 | body = JSON.parse(body); 34 | } 35 | 36 | method = method.toUpperCase(); 37 | const { path, query } = extractURL(url); 38 | 39 | // pattern match 40 | let handler = null; 41 | let i; 42 | let longestMatchedPattern = 0; 43 | for (i = start; i < handlers.length; i++) { 44 | if (handlers[i].method === 'ALL' || method === handlers[i].method) { 45 | const { pattern } = handlers[i]; 46 | if (patternMatched(pattern, path)) { 47 | if (pattern.split('/').length > longestMatchedPattern) { 48 | handler = handlers[i]; 49 | longestMatchedPattern = pattern.split('/').length; 50 | } 51 | } 52 | } 53 | } 54 | if (handler) { 55 | // dispatch to next handler 56 | const next = () => { 57 | dispatch(method, url, i); 58 | }; 59 | const req = { 60 | method, 61 | url, 62 | path, 63 | query, 64 | params: extractPathParams(handler.pattern, path), 65 | headers, 66 | body, 67 | }; 68 | const res = Response(); 69 | const cb = handler.callback; 70 | 71 | const errFunc = (err) => { 72 | res.status(500).json({ 73 | status: 'failed', 74 | message: `Internal Server Error: ${err}`, 75 | }); 76 | }; 77 | 78 | if (cb.constructor.name === 'AsyncFunction') { 79 | cb(req, res, next).catch(errFunc); 80 | } else { 81 | try { 82 | cb(req, res, next); 83 | } catch (err) { 84 | errFunc(err); 85 | } 86 | } 87 | } else { 88 | // no route, return 404 89 | const res = Response(); 90 | res.status(404).json({ 91 | status: 'failed', 92 | message: 'ERROR: 404 not found', 93 | }); 94 | } 95 | }; 96 | 97 | const app = {}; 98 | 99 | // attach http methods 100 | METHODS_NAMES.forEach((method) => { 101 | app[method.toLowerCase()] = (pattern, callback) => { 102 | // add handler 103 | handlers.push({ method, pattern, callback }); 104 | }; 105 | }); 106 | 107 | // chainable route 108 | app.route = (pattern) => { 109 | const chainApp = {}; 110 | METHODS_NAMES.forEach((method) => { 111 | chainApp[method.toLowerCase()] = (callback) => { 112 | // add handler 113 | handlers.push({ method, pattern, callback }); 114 | return chainApp; 115 | }; 116 | }); 117 | return chainApp; 118 | }; 119 | 120 | // start service 121 | app.start = () => { 122 | dispatch($request); 123 | }; 124 | 125 | return app; 126 | 127 | /************************************************ 128 | Utility Functions 129 | *************************************************/ 130 | function rawBodySaver(req, res, buf, encoding) { 131 | if (buf && buf.length) { 132 | req.rawBody = buf.toString(encoding || 'utf8'); 133 | } 134 | } 135 | 136 | function Response() { 137 | let statusCode = 200; 138 | const headers = DEFAULT_HEADERS; 139 | const STATUS_CODE_MAP = { 140 | 200: 'HTTP/1.1 200 OK', 141 | 201: 'HTTP/1.1 201 Created', 142 | 302: 'HTTP/1.1 302 Found', 143 | 307: 'HTTP/1.1 307 Temporary Redirect', 144 | 308: 'HTTP/1.1 308 Permanent Redirect', 145 | 404: 'HTTP/1.1 404 Not Found', 146 | 500: 'HTTP/1.1 500 Internal Server Error', 147 | }; 148 | return new (class { 149 | status(code) { 150 | statusCode = code; 151 | return this; 152 | } 153 | 154 | send(body = '') { 155 | const response = { 156 | status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode, 157 | body, 158 | headers, 159 | }; 160 | if (isQX) { 161 | $done(response); 162 | } else if (isLoon || isSurge) { 163 | $done({ 164 | response, 165 | }); 166 | } 167 | } 168 | 169 | end() { 170 | this.send(); 171 | } 172 | 173 | html(data) { 174 | this.set('Content-Type', 'text/html;charset=UTF-8'); 175 | this.send(data); 176 | } 177 | 178 | json(data) { 179 | this.set('Content-Type', 'application/json;charset=UTF-8'); 180 | this.send(JSON.stringify(data)); 181 | } 182 | 183 | set(key, val) { 184 | headers[key] = val; 185 | return this; 186 | } 187 | })(); 188 | } 189 | } 190 | 191 | function formatHeaders(headers) { 192 | const result = {}; 193 | for (const k of Object.keys(headers)) { 194 | result[k.toLowerCase()] = headers[k]; 195 | } 196 | return result; 197 | } 198 | 199 | function patternMatched(pattern, path) { 200 | if (pattern instanceof RegExp && pattern.test(path)) { 201 | return true; 202 | } else { 203 | // root pattern, match all 204 | if (pattern === '/') return true; 205 | // normal string pattern 206 | if (pattern.indexOf(':') === -1) { 207 | const spath = path.split('/'); 208 | const spattern = pattern.split('/'); 209 | for (let i = 0; i < spattern.length; i++) { 210 | if (spath[i] !== spattern[i]) { 211 | return false; 212 | } 213 | } 214 | return true; 215 | } else if (extractPathParams(pattern, path)) { 216 | // string pattern with path parameters 217 | return true; 218 | } 219 | } 220 | return false; 221 | } 222 | 223 | function extractURL(url) { 224 | // extract path 225 | const match = url.match(/https?:\/\/[^/]+(\/[^?]*)/) || []; 226 | const path = match[1] || '/'; 227 | 228 | // extract query string 229 | const split = url.indexOf('?'); 230 | const query = {}; 231 | if (split !== -1) { 232 | let hashes = url.slice(url.indexOf('?') + 1).split('&'); 233 | for (let i = 0; i < hashes.length; i++) { 234 | const hash = hashes[i].split('='); 235 | query[hash[0]] = hash[1]; 236 | } 237 | } 238 | return { 239 | path, 240 | query, 241 | }; 242 | } 243 | 244 | function extractPathParams(pattern, path) { 245 | if (pattern.indexOf(':') === -1) { 246 | return null; 247 | } else { 248 | const params = {}; 249 | for (let i = 0, j = 0; i < pattern.length; i++, j++) { 250 | if (pattern[i] === ':') { 251 | let key = []; 252 | let val = []; 253 | while (pattern[++i] !== '/' && i < pattern.length) { 254 | key.push(pattern[i]); 255 | } 256 | while (path[j] !== '/' && j < path.length) { 257 | val.push(path[j++]); 258 | } 259 | params[key.join('')] = val.join(''); 260 | } else { 261 | if (pattern[i] !== path[j]) { 262 | return null; 263 | } 264 | } 265 | } 266 | return params; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/vendor/open-api.ts: -------------------------------------------------------------------------------- 1 | import { RequestInit, Response } from '@cloudflare/workers-types'; 2 | import { DB } from '../db'; 3 | 4 | export class OpenAPI { 5 | name: string; 6 | debug: boolean; 7 | http; 8 | constructor(name = 'untitled', debug = false) { 9 | this.name = name; 10 | this.debug = debug; 11 | 12 | this.http = HTTP(); 13 | 14 | const delay = (t, v) => 15 | new Promise(function (resolve) { 16 | setTimeout(resolve.bind(null, v), t); 17 | }); 18 | 19 | Promise.prototype.delay = async function (t) { 20 | const v = await this; 21 | return await delay(t, v); 22 | }; 23 | } 24 | 25 | async write(data: any, key: string): Promise { 26 | this.log(`SET ${key}`); 27 | if (key.indexOf('#') !== -1) { 28 | key = key.substring(1); 29 | } 30 | data = JSON.stringify(data); 31 | try { 32 | await DB 33 | .insertInto('sub_store') 34 | .values([{ key, data }]) 35 | .onConflict((oc) => oc.column('key').doUpdateSet({ data })) 36 | .execute(); 37 | } catch (err) { 38 | console.log(err); 39 | console.log((err as any).cause); 40 | throw err; 41 | } 42 | } 43 | 44 | async read(key: string): Promise { 45 | this.log(`READ ${key}`); 46 | if (key.indexOf('#') !== -1) { 47 | key = key.substring(1); 48 | } 49 | const result = await DB.selectFrom('sub_store').selectAll().where('key', '=', key).executeTakeFirst(); 50 | return !result ? null : JSON.parse(result.data); 51 | } 52 | 53 | async delete(key: string) { 54 | this.log(`DELETE ${key}`); 55 | if (key.indexOf('#') !== -1) { 56 | key = key.substring(1); 57 | } 58 | await DB.deleteFrom('sub_store').where('key', '=', key).execute(); 59 | } 60 | 61 | // notification 62 | notify(title: string, subtitle = '', content = '', options = {}) { 63 | const openURL = options['open-url']; 64 | const mediaURL = options['media-url']; 65 | 66 | const content_ = 67 | content + 68 | (openURL ? `\n点击跳转: ${openURL}` : '') + 69 | (mediaURL ? `\n多媒体: ${mediaURL}` : ''); 70 | console.log(`${title}\n${subtitle}\n${content_}\n\n`); 71 | } 72 | 73 | // other helper functions 74 | log(msg: any) { 75 | if (this.debug) console.log(`[${this.name}] LOG: ${msg}`); 76 | } 77 | 78 | info(msg: any) { 79 | console.log(`[${this.name}] INFO: ${msg}`); 80 | } 81 | 82 | error(msg: any) { 83 | console.log(`[${this.name}] ERROR: ${msg}`); 84 | } 85 | 86 | wait(millisec: number) { 87 | return new Promise((resolve) => setTimeout(resolve, millisec)); 88 | } 89 | } 90 | 91 | export function HTTP(defaultOptions = {}): { get: (any), post: (any), put: (any), delete: (any), head: (any), options: (any), patch: (any)} { 92 | const methods = [ 93 | 'GET', 94 | 'POST', 95 | 'PUT', 96 | 'DELETE', 97 | 'HEAD', 98 | 'OPTIONS', 99 | 'PATCH', 100 | ]; 101 | async function send(method: string, options: RequestInit|string): Promise { 102 | const url = typeof options === 'string' ? options : options.url; 103 | if (typeof options === 'string') { 104 | options = { method: method }; 105 | } else { 106 | options.method = method; 107 | } 108 | options = { ...defaultOptions, ...options }; 109 | return await fetch(url, options).then( 110 | (res: Response) => { 111 | if (res.ok) { 112 | return res; 113 | } 114 | } 115 | ).catch( 116 | (e) => { 117 | new Error(e.message); 118 | return null; 119 | } 120 | ) 121 | } 122 | const http = {}; 123 | methods.forEach( 124 | (method) => 125 | (http[method.toLowerCase()] = async (options: any) => await send(method, options)), 126 | ); 127 | return http; 128 | } 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "lib": [ 10 | "esnext" 11 | ], 12 | "types": [ 13 | "@cloudflare/workers-types" 14 | ], 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "hono/jsx" 17 | }, 18 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "sub-store-workers" 2 | compatibility_date = "2023-07-19" 3 | main = "src/index.ts" 4 | compatibility_flags = [ "nodejs_compat" ] 5 | minify = true 6 | 7 | [[d1_databases]] 8 | binding = "DB" 9 | database_name = "substore_workers_database_name" 10 | database_id = "substore_workers_database_id" 11 | 12 | [vars] 13 | BEARER_TOKEN = "substore_workers_bearer_token" 14 | D_TOKEN = "substore_workers_d_token" 15 | --------------------------------------------------------------------------------