Compare commits

..

84 Commits

Author SHA1 Message Date
xream
c2499f6779 fix: 修复 Base64 内容的判断 2023-12-02 16:14:11 +08:00
xream
bf9210fc5a fix: 修复多行订阅流量(仅传递首个订阅的流量信息) 2023-12-01 17:09:56 +08:00
xream
53dd1fd4c5 feat: 支持不规范的 Loon ss+simple obfs 协议格式 2023-11-30 16:01:13 +08:00
xream
c541b83037 feat: 支持按顺序合并本地和远程订阅(前端版本 > 2.14.14 可输入) 2023-11-29 03:57:20 +08:00
xream
3054d5cd5d feat: 远程订阅支持换行符连接的多个订阅链接(前端版本 > 2.14.13 可输入) 2023-11-29 02:24:03 +08:00
xream
5a645081d1 fix: SS URI 端口取整数部分 2023-11-28 23:14:45 +08:00
xream
1fc5b764fe feat: 支持设置默认 User-Agent 2023-11-25 04:31:17 +08:00
xream
5f1415d9d4 feat: 后端支持自定义 hostport. 环境变量 SUB_STORE_BACKEND_API_HOST 默认 ::, SUB_STORE_BACKEND_API_PORT 默认 3000 2023-11-24 18:31:13 +08:00
xream
1e3b4a147a feat: 增加了节点字段 1. no-resolve, 可用于跳过域名解析 2. resolved 用来标记域名解析是否成功 2023-11-21 20:10:05 +08:00
xream
905a50c0b9 fix: Hysteria/Hysteria2 输出到 Stash 时 down-speed 和 up-speed 字段截取数字部分 2023-11-20 11:22:01 +08:00
xream
89e8a99729 Merge pull request #250 from YES-Lee/patch-1
feat: add sync task for qx
2023-11-19 11:44:04 +08:00
xream
ff8573cae7 fix: 修复 app 版参数 2023-11-16 12:49:06 +08:00
xream
1ae1ec40ca feat: 补全 Surge 全协议的 no-error-alert 和 ip-version 字段 2023-11-15 15:16:34 +08:00
xream
53925518b4 feat: Sub-Store 生成的订阅地址支持传入 订阅链接/User-Agent/节点内容 可以复用此订阅的其他设置
例如: 建一个 name 为 sub 的订阅, 配置好节点操作

以后可以自由传入参数 无需在 Sub-Store 前端创建新的配置

`/download/sub?target=Surge&content=encodeURIComponent编码过的本地节点`

`/download/sub?target=Surge&url=encodeURIComponent编码过的订阅链接&ua=encodeURIComponent编码过的User-Agent`
2023-11-14 21:46:56 +08:00
xream
f3de132d70 feat: 脚本链接的末尾加上 #noCache 关闭缓存 2023-11-14 21:14:47 +08:00
xream
3e30a35bc4 feat: 脚本操作支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
$server.name = '前缀-' + $server.name
$server.ecn = true
$server['test-url'] = 'http://1.0.0.1/generate_204'
```
2023-11-14 17:07:01 +08:00
xream
3e5f3eafdd feat: 脚本操作 ProxyUtils 增加了 isIPv4, isIPv6, isIP 方法 2023-11-14 00:57:52 +08:00
xream
9c78b87834 feat: 兼容某些格式的 Trojan URI(首个 # 之后的字符串均视为节点名称) 2023-11-13 18:49:50 +08:00
xream
ea88cc1794 feat: 支持 QX tls-pubkey-sha256 tls-alpn tls-no-session-ticket tls-no-session-reuse 字段 2023-11-13 14:34:36 +08:00
xream
c8b197c0a1 feat: 支持 QX server_check_url 和 Stash benchmark-url 字段 2023-11-13 14:06:44 +08:00
xream
69fab11344 feat: 兼容传输层 headers 中小写的 host 字段 2023-11-08 09:54:53 +08:00
xream
955c74a77d feat: 兼容某些机场订阅 hysteria 节点中的 auth_str 字段(将会在未来某个时候删除 但是有的机场不规范) 2023-11-08 07:44:12 +08:00
xream
6d51774d36 feat: 为脚本操作增加流量信息操作 flowUtils 2023-11-07 16:42:28 +08:00
xream
a91f9d7728 feat: 兼容另一种 username password 格式 2023-10-31 21:59:34 +08:00
xream
df366cf8eb doc: pnpm 2023-10-30 01:44:18 +08:00
xream
c547f34f57 feat: 支持 Loon Hysteria2(ecn, 流量控制参数未知) 2023-10-29 23:04:56 +08:00
xream
a4ff32331a fix: 简单限制一下订阅/组合订阅的名称(不可包含 "/" ) 2023-10-29 22:38:20 +08:00
xream
14648d6401 feat: 订阅链接支持参数(例: https://foo.com#noCache 关闭缓存) 2023-10-26 11:26:31 +08:00
Johnson
6216217286 feat: add qx sync task 2023-10-25 10:44:22 -05:00
xream
6a66475154 feat: Surge 支持 block-quic 参数 2023-10-24 09:31:48 +08:00
xream
adc95bba60 feat: Surge 全协议支持 Shadow TLS, 部分协议增加 TLS Fingerprint 支持 2023-10-24 07:26:34 +08:00
xream
fab3644b86 feat: 支持 Shadowrocket Hysteria2 URI 格式输入 2023-10-18 23:48:45 +08:00
xream
c21ce0be16 fix: Surge Hysteria2 输出重复添加 tfo 的 bug 2023-10-18 05:09:10 +08:00
xream
fa65eb1850 feat: Base64 订阅关键词增加 VLESS 和 Hysteria2 2023-10-16 22:11:26 +08:00
xream
79c9b89c5f feat: Stash Hysteria2 2023-10-15 15:55:19 +08:00
xream
fca508ba8a feat: Surge Hysteria2 输入/输出增加 ecn 参数 2023-10-12 22:15:10 +08:00
xream
21b531a44d feat: Surge TUIC 输入/输出增加 ecn 参数 2023-10-12 22:09:58 +08:00
xream
4e5b46a43d feat: Surge Hysteria2 输出增加 download-bandwidth(若有值但解析失败则为 0) 2023-10-12 00:39:10 +08:00
xream
bf81ca4acf feat: 输入增加 Hysteria2 URI 支持; Surge Hysteria2 输出增加 fingerprint 2023-10-11 23:35:42 +08:00
xream
e7c0b23222 feat: Surge 输入输出增加 Hysteria2 2023-10-09 23:42:22 +08:00
xream
40fb0fd7f3 feat: 兼容更多 VMess URI 格式 2023-10-09 17:36:11 +08:00
xream
b061fca356 feat: Surge Snell 输入支持解析 reuse 字段 2023-10-08 16:42:35 +08:00
xream
d3c6c99b0a feat: proxy 增加 subName(订阅名), collectionName(组合订阅名); 脚本增加第三个参数 env(包含订阅/组合订阅/环境/版本等信息) 2023-10-08 13:21:22 +08:00
xream
3fbc280e28 [+] 重复节点通知中增加订阅名称和重复节点名称 2023-10-02 16:21:08 +08:00
xream
9e3e4c6e46 [+] Surge 输出支持 underlying-proxy; VMess/Vless URI 支持 gRPC mode(默认为 gun) 2023-10-01 22:05:51 +08:00
xream
bc0dd4b175 feat: 支持 hysteria2 2023-09-22 14:43:43 +08:00
xream
7603fac036 fix: 修复部分环境无 clearTimeout 的问题 2023-09-18 20:09:03 +08:00
K
9acc161684 fix @ 2023-09-15 18:52:21 +08:00
xream
024582a99d fix: 修复 sub-store-0 路由 2023-09-15 18:42:53 +08:00
xream
1d31a80b9f fix: 修复文件和模块命名/重复添加的逻辑 2023-09-15 10:08:36 +08:00
xream
b2d0276836 feat: 文件和模块接口获取原始内容; 文件列表不返回原始内容 2023-09-14 18:51:23 +08:00
xream
3211fbf357 feat: 模块接口; 脚本参数支持 JSON 和 URL编码 2023-09-14 17:34:24 +08:00
xream
33a17c2d66 feat: 实验性支持本地脚本复用 2023-09-14 08:56:33 +08:00
xream
2c89a0ddbd feat: 支持 Clash VLESS 输出(与 Clash.Meta 的区别为: 无 XTLS 2023-09-11 02:35:36 +08:00
xream
939022e5a3 fix: 修复了 Clash.Meta 输出 VLESS 时 内部字段 sni 未作用到 servername 的问题 2023-09-09 14:03:40 +08:00
xream
59bca5670d fix: 预览时脚本下载报错导致的崩溃 2023-09-07 23:17:36 +08:00
Peng-YM
07b38cf971 release: backend version 2.14.49 2023-09-04 23:16:52 +08:00
Peng-YM
28186f596f feat: added the ability to change the base path for the data files
before starting node, use the command `export SUB_STORE_DATA_BASE_PATH="<YOUR_PATH>"`
2023-09-04 23:16:13 +08:00
xream
ea31b1d0ec fix: 排序接口修正为使用 name 排序 2023-09-04 21:31:55 +08:00
xream
77191f9caa feat: 为 Gist 备份还原增加基础校验逻辑 2023-09-04 17:06:37 +08:00
xream
07a270963e feat: 支持 Surge WireGuard 的输入和输出(由于 Surge 配置的特殊性, 仅支持 同进同出) 支持的字段格式: HK WARP = wireguard, section-name=Cloudflare, no-error-alert=true, underlying-proxy=HK, test-url=http://1.0.0.1/generate_204, ip-version=v4-only 2023-09-01 02:44:43 +08:00
xream
f1e1d50a2c fix: 暂时将后端上传限制放宽到 1mb 2023-09-01 02:07:24 +08:00
Peng-YM
a65cd1f1c9 Update README.md 2023-08-31 16:41:50 +08:00
xream
5b0e2e1ef2 docs: config 2023-08-30 22:52:05 +08:00
xream
b29266ac57 chore: sync to GitLab 2023-08-30 16:19:17 +08:00
xream
336ddd6706 chore: 调整部分日志 2023-08-29 13:52:02 +08:00
xream
25ec219659 docs: 更新 Surge SSR 协议说明; 模块说明页增加更新说明的链接 2023-08-29 01:59:01 +08:00
xream
41d24b131a feat: 根据 UA 识别 macOS 版 Surge(也可指定参数 target=SurgeMac) 并支持 SSR 协议(节点字段 exec 为 ssr-local 路径, 默认 /usr/local/bin/ssr-local; 端口从 10000 开始递增, 暂不支持配置) 2023-08-29 01:46:49 +08:00
xream
ba78982f41 feat: 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组 2023-08-28 23:47:10 +08:00
xream
26193301b3 fix: 仅在 VMess/VLESS 且传输层为 http 时设置 Host 为数组 2023-08-28 23:38:03 +08:00
xream
0141e48200 feat: 增加还原备份完成的日志输出 2023-08-28 23:29:53 +08:00
xream
5ae6687b1f chore: changelog 2023-08-28 23:15:48 +08:00
xream
ad6d1ab441 fix: build dist 2023-08-28 20:41:40 +08:00
xream
f5aea14904 fix: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host) 2023-08-28 20:34:22 +08:00
Peng-YM
4f2c95f6ab chore: remove unnecessary files 2023-08-28 20:07:32 +08:00
Peng-YM
be1e2c9979 chore: removed tracking dist files from git 2023-08-28 20:06:50 +08:00
Peng-YM
347b19e30d remove: deprecated artifact 2023-08-28 20:01:05 +08:00
xream
f94a12bf6e feat: bundle 2023-08-28 19:01:34 +08:00
xream
bd510a9aa9 fix: sync 2023-08-28 18:48:33 +08:00
xream
f02af9d643 fix: vless servername 2023-08-28 15:32:08 +08:00
xream
af8e965866 feat: new target platform "Clash.Meta" 2023-08-28 13:10:48 +08:00
xream
4bebcff1d3 feat: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名 2023-08-28 00:09:24 +08:00
xream
7b8f6f7e52 feat: 域名解析新增 Tencent, Ali; 脚本下载失败, 脚本操作失败, 脚本过滤失败时都会报错了 2023-08-27 23:17:57 +08:00
xream
49c7107d20 fix: transport headers may have no Host 2023-08-27 18:17:30 +08:00
57 changed files with 2142 additions and 374 deletions

View File

@@ -1,15 +1,15 @@
name: build name: build
on: on:
push: push:
branches: branches:
- master - master
paths: paths:
- 'backend/package.json' - "backend/package.json"
pull_request: pull_request:
branches: branches:
- master - master
paths: paths:
- 'backend/package.json' - "backend/package.json"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -17,7 +17,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: 'master' ref: "master"
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
@@ -34,17 +34,28 @@ jobs:
run: | run: |
cd backend cd backend
pnpm run build pnpm run build
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag - id: tag
name: Generate release tag name: Generate release tag
run: | run: |
cd backend cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"` SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "::set-output name=release_tag::$SUBSTORE_RELEASE" echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }} tag_name: ${{ steps.tag.outputs.release_tag }}
files: | files: |
./backend/sub-store.min.js ./backend/sub-store.min.js
@@ -52,3 +63,9 @@ jobs:
./backend/dist/sub-store-1.min.js ./backend/dist/sub-store-1.min.js
./backend/dist/sub-store-parser.loon.min.js ./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js ./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

9
.gitignore vendored
View File

@@ -127,4 +127,11 @@ out
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Dist files
backend/dist/*
!backend/dist/.gitkeep
backend/sub-store.min.js
CHANGELOG.md

View File

@@ -10,8 +10,8 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket. Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
</p> </p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/Sub-Store) [![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities: Core functionalities:
@@ -19,6 +19,8 @@ Core functionalities:
1. Conversion among various formats. 1. Conversion among various formats.
2. Subscription formatting. 2. Subscription formatting.
3. Collect multiple subscriptions in one URL. 3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion ## 1. Subscription Conversion
@@ -28,18 +30,27 @@ Core functionalities:
- [x] SSR URI - [x] SSR URI
- [x] SSD URI - [x] SSD URI
- [x] V2RayN URI - [x] V2RayN URI
- [x] Hysteria2 URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP) - [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
- [x] Surge (SS, VMess, Trojan, HTTP) - [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP) - [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
- [x] QX - [x] QX
- [x] Loon - [x] Loon
- [x] Surge - [x] Surge
- [x] Stash & Clash - [x] Stash
- [x] Clash.Meta
- [x] Clash
- [x] ShadowRocket - [x] ShadowRocket
- [x] V2Ray
- [x] V2Ray URI
- [x] Plain JSON
## 2. Subscription Formatting ## 2. Subscription Formatting
@@ -61,33 +72,35 @@ Core functionalities:
- [x] **Regex rename operator**: replace by regex in proxy names. - [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names. - [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script. - [x] **Script operator**: modify proxy by script.
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
### Development ### Development
Go to `backend` and `web` directories, install node dependencies: Install `pnpm`
Go to `backend` directories, install node dependencies:
``` ```
npm install pnpm install
``` ```
1. In `backend`, run the backend server on http://localhost:3000 1. In `backend`, run the backend server on http://localhost:3000
``` ```
npm run serve pnpm start
``` ```
2. In`web`, start the vue-cli server
```
npm start
```
## LICENSE ## LICENSE
This project is under the GPL V3 LICENSE. This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements ## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work! - Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!

View File

@@ -9,7 +9,7 @@
* @updated: <%= updated %> * @updated: <%= updated %>
* @version: <%= pkg.version %> * @version: <%= pkg.version %>
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */

23
backend/bundle.js Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8',
});
content = content.replace(
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
'$2',
);
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
encoding: 'utf8',
});
const { build } = require('estrella');
build({
entry: 'dist/sub-store.no-bundle.js',
outfile: 'dist/sub-store.bundle.js',
bundle: true,
platform: 'node',
});

0
backend/dist/.gitkeep vendored Normal file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.31", "version": "2.14.109",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
@@ -8,7 +8,9 @@
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive", "test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
"serve": "node sub-store.min.js", "serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js", "start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp" "build": "gulp",
"bundle": "node bundle.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
}, },
"author": "Peng-YM", "author": "Peng-YM",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings'; export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs'; export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections'; export const COLLECTIONS_KEY = 'collections';
export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts'; export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules'; export const RULES_KEY = 'rules';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';

View File

@@ -63,35 +63,50 @@ function parse(raw) {
return proxies; return proxies;
} }
async function process(proxies, operators = [], targetPlatform) { async function process(proxies, operators = [], targetPlatform, source) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
const $arguments = {}; let $arguments = {};
if (item.type.indexOf('Script') !== -1) { if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
const url = content; let noCache;
let url = content;
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
}
// extract link arguments // extract link arguments
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
for (const pair of rawArgs[1].split('&')) { try {
const key = pair.split('=')[0]; // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
const value = pair.split('=')[1] || true; $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
$arguments[key] = value; } catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
} }
} }
// if this is a remote script, download it // if this is a remote script, download it
try { try {
script = await download(url.split('#')[0]); script = await download(
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`,
);
// $.info(`Script loaded: >>>\n ${script}`); // $.info(`Script loaded: >>>\n ${script}`);
} catch (err) { } catch (err) {
$.error( $.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
); );
// skip the script if download failed. throw new Error(`无法下载脚本: ${url}`);
continue;
} }
} else { } else {
script = content; script = content;
@@ -114,6 +129,7 @@ async function process(proxies, operators = [], targetPlatform) {
script, script,
targetPlatform, targetPlatform,
$arguments, $arguments,
source,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -137,10 +153,21 @@ function produce(proxies, targetPlatform) {
$.info(`Producing proxies for target: ${targetPlatform}`); $.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
return proxies return proxies
.map((proxy) => { .map((proxy) => {
try { try {
return producer.produce(proxy); let line = producer.produce(proxy);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) { } catch (err) {
$.error( $.error(
`Cannot produce proxy: ${JSON.stringify( `Cannot produce proxy: ${JSON.stringify(
@@ -163,6 +190,9 @@ export const ProxyUtils = {
parse, parse,
process, process,
produce, produce,
isIPv4,
isIPv6,
isIP,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -189,9 +219,17 @@ function lastParse(proxy) {
delete proxy.network; delete proxy.network;
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
proxy.tls = true; proxy.tls = true;
} }
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
if (transporthost && !transportHost) {
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
delete proxy[`${proxy.network}-opts`].headers.host;
}
}
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {
if (proxy.network) { if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
@@ -206,6 +244,32 @@ function lastParse(proxy) {
proxy.sni = proxy.server; proxy.sni = proxy.server;
} }
} }
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
['ws', 'http'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
!isIP(proxy.server)
) {
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].headers =
proxy[`${proxy.network}-opts`].headers || {};
proxy[`${proxy.network}-opts`].headers.Host =
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
? [proxy.server]
: proxy.server;
}
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
if (transportHost && !Array.isArray(transportHost)) {
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
}
if (transportPath && !Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
return proxy; return proxy;
} }

View File

@@ -33,7 +33,9 @@ function URI_SS() {
const serverAndPort = serverAndPortArray[1]; const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':'); const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx); proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1); proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
/\d+/,
)?.[0];
const userInfo = userInfoStr.split(':'); const userInfo = userInfoStr.split(':');
proxy.cipher = userInfo[0]; proxy.cipher = userInfo[0];
@@ -215,7 +217,6 @@ function URI_VMess() {
// V2rayN URI format // V2rayN URI format
params = JSON.parse(content); params = JSON.parse(content);
} catch (e) { } catch (e) {
// console.error(e);
// Shadowrocket URI format // Shadowrocket URI format
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line); let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
@@ -241,7 +242,7 @@ function URI_VMess() {
params.add = server; params.add = server;
} }
const proxy = { const proxy = {
name: params.ps ?? params.remark, name: params.ps ?? params.remarks,
type: 'vmess', type: 'vmess',
server: params.add, server: params.add,
port: parseInt(getIfPresent(params.port), 10), port: parseInt(getIfPresent(params.port), 10),
@@ -268,9 +269,19 @@ function URI_VMess() {
params.obfs === 'http' params.obfs === 'http'
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} }
if (proxy.network) { if (proxy.network) {
let transportHost = params.host ?? params.obfsParam; let transportHost = params.host ?? params.obfsParam;
try {
const parsedObfs = JSON.parse(transportHost);
const parsedHost = parsedObfs?.Host;
if (parsedHost) {
transportHost = parsedHost;
}
// eslint-disable-next-line no-empty
} catch (e) {}
let transportPath = params.path; let transportPath = params.path;
if (proxy.network === 'http') { if (proxy.network === 'http') {
@@ -286,10 +297,17 @@ function URI_VMess() {
} }
} }
if (transportPath || transportHost) { if (transportPath || transportHost) {
proxy[`${proxy.network}-opts`] = { if (['grpc'].includes(proxy.network)) {
path: getIfNotBlank(transportPath), proxy[`${proxy.network}-opts`] = {
headers: { Host: getIfNotBlank(transportHost) }, 'grpc-service-name': getIfNotBlank(transportPath),
}; '_grpc-type': getIfNotBlank(params.type),
};
} else {
proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
}
} else { } else {
delete proxy.network; delete proxy.network;
} }
@@ -366,6 +384,10 @@ function URI_VLESS() {
if (params.serviceName) { if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName; opts[`${proxy.network}-service-name`] = params.serviceName;
} }
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (Object.keys(opts).length > 0) { if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts; proxy[`${proxy.network}-opts`] = opts;
} }
@@ -384,6 +406,56 @@ function URI_VLESS() {
}; };
return { name, test, parse }; return { name, test, parse };
} }
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^hysteria2:\/\//.test(line);
};
const parse = (line) => {
line = line.split('hysteria2://')[1];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, addons, name] =
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
const proxy = {
type: 'hysteria2',
name,
server,
port,
password,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.sni = params.sni;
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
return proxy;
};
return { name, test, parse };
}
// Trojan URI format // Trojan URI format
function URI_Trojan() { function URI_Trojan() {
@@ -393,8 +465,16 @@ function URI_Trojan() {
}; };
const parse = (line) => { const parse = (line) => {
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser(); const parser = getTrojanURIParser();
const proxy = parser.parse(line); const proxy = parser.parse(newLine);
if (isNotBlank(name)) {
try {
proxy.name = decodeURIComponent(name);
} catch (e) {
console.log(e);
}
}
return proxy; return proxy;
}; };
return { name, test, parse }; return { name, test, parse };
@@ -417,13 +497,14 @@ function Clash_All() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'tuic', 'tuic',
'vless', 'vless',
'hysteria', 'hysteria',
'hysteria2',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
@@ -448,6 +529,10 @@ function Clash_All() {
} }
} }
if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url'];
}
return proxy; return proxy;
}; };
return { name, test, parse }; return { name, test, parse };
@@ -571,6 +656,15 @@ function Loon_Trojan() {
const parse = (line) => getLoonParser().parse(line); const parse = (line) => getLoonParser().parse(line);
return { name, test, parse }; return { name, test, parse };
} }
function Loon_Hysteria2() {
const name = 'Loon Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*Hysteria2/i.test(line.split(',')[0]);
};
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_Http() { function Loon_Http() {
const name = 'Loon HTTP Parser'; const name = 'Loon HTTP Parser';
@@ -740,7 +834,7 @@ function Surge_Socks5() {
function Surge_Snell() { function Surge_Snell() {
const name = 'Surge Snell Parser'; const name = 'Surge Snell Parser';
const test = (line) => { const test = (line) => {
return /^.*=\s*snell?/.test(line.split(',')[0]); return /^.*=\s*snell/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse }; return { name, test, parse };
@@ -749,7 +843,24 @@ function Surge_Snell() {
function Surge_Tuic() { function Surge_Tuic() {
const name = 'Surge Tuic Parser'; const name = 'Surge Tuic Parser';
const test = (line) => { const test = (line) => {
return /^.*=\s*tuic(-v5)??/.test(line.split(',')[0]); return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_WireGuard() {
const name = 'Surge WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Hysteria2() {
const name = 'Surge Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse }; return { name, test, parse };
@@ -760,6 +871,7 @@ export default [
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),
URI_VLESS(), URI_VLESS(),
URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
Clash_All(), Clash_All(),
Surge_SS(), Surge_SS(),
@@ -768,11 +880,14 @@ export default [
Surge_Http(), Surge_Http(),
Surge_Snell(), Surge_Snell(),
Surge_Tuic(), Surge_Tuic(),
Surge_WireGuard(),
Surge_Hysteria2(),
Surge_Socks5(), Surge_Socks5(),
Loon_SS(), Loon_SS(),
Loon_SSR(), Loon_SSR(),
Loon_VMess(), Loon_VMess(),
Loon_Vless(), Loon_Vless(),
Loon_Hysteria2(),
Loon_Trojan(), Loon_Trojan(),
Loon_Http(), Loon_Http(),
Loon_WireGuard(), Loon_WireGuard(),

View File

@@ -35,7 +35,7 @@ const grammars = String.raw`
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
return proxy; return proxy;
} }
@@ -44,7 +44,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -68,6 +68,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
@@ -142,6 +145,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -167,6 +173,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@@ -33,7 +33,7 @@
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
return proxy; return proxy;
} }
@@ -42,7 +42,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -66,6 +66,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
@@ -140,6 +143,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -165,6 +171,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@@ -43,13 +43,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
} }
trojan = "trojan" equals address trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleObfs(); handleObfs();
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(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)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
if (proxy.protocol) { if (proxy.protocol) {
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
@@ -80,7 +80,7 @@ shadowsocks = "shadowsocks" equals address
} }
vmess = "vmess" equals address vmess = "vmess" equals address
(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)* { (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -92,12 +92,12 @@ vmess = "vmess" equals address
} }
http = "http" equals address http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http"; proxy.type = "http";
} }
socks5 = "socks5" equals address socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -155,6 +155,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag; proxy["skip-cert-verify"] = !flag;
} }
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
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; } 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; }
@@ -166,6 +174,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
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; } 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; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+ uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -41,13 +41,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
} }
trojan = "trojan" equals address trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleObfs(); handleObfs();
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(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)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
if (proxy.protocol) { if (proxy.protocol) {
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
@@ -78,7 +78,7 @@ shadowsocks = "shadowsocks" equals address
} }
vmess = "vmess" equals address vmess = "vmess" equals address
(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)* { (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -90,12 +90,12 @@ vmess = "vmess" equals address
} }
http = "http" equals address http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http"; proxy.type = "http";
} }
socks5 = "socks5" equals address socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -153,6 +153,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag; proxy["skip-cert-verify"] = !flag;
} }
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
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; } 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; }
@@ -164,6 +172,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
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; } 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; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+ uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -32,11 +32,11 @@ const grammars = String.raw`
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -46,7 +46,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
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)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -56,18 +56,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -76,17 +76,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -157,6 +163,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -185,8 +192,19 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -30,11 +30,11 @@
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -44,7 +44,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
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)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -54,18 +54,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -74,17 +74,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -155,6 +161,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -183,8 +190,19 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -13,17 +13,22 @@ function Base64Encoded() {
const name = 'Base64 Pre-processor'; const name = 'Base64 Pre-processor';
const keys = [ const keys = [
'dm1lc3M', 'dm1lc3M', // vmess
'c3NyOi8v', 'c3NyOi8v', // ssr://
'dHJvamFu', 'dHJvamFu', // trojan
'c3M6Ly', 'c3M6Ly', // ss:/
'c3NkOi8v', 'c3NkOi8v', // ssd://
'c2hhZG93', 'c2hhZG93', // shadow
'aHR0c', 'aHR0c', // htt
'dmxlc3M=', // vless
'aHlzdGVyaWEy', // hysteria2
]; ];
const test = function (raw) { const test = function (raw) {
return keys.some((k) => raw.indexOf(k) !== -1); return (
!/^\w+:\/\/\w+/im.test(raw) &&
keys.some((k) => raw.indexOf(k) !== -1)
);
}; };
const parse = function (raw) { const parse = function (raw) {
raw = Base64.decode(raw); raw = Base64.decode(raw);

View File

@@ -7,6 +7,8 @@ import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import env from '@/utils/env';
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
/** /**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -294,7 +296,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`! 1. This function name should be `operator`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptOperator(script, targetPlatform, $arguments) { function ScriptOperator(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -305,7 +307,24 @@ function ScriptOperator(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = operator(proxies, targetPlatform); output = operator(proxies, targetPlatform, { source, ...env });
})();
return output;
},
nodeFunc: async (proxies) => {
let output = proxies;
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(proxies = []) {
return proxies.map(($server = {}) => {
${script}
return $server
})
}`,
$arguments,
);
output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -378,6 +397,46 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
)}&type=A&short=1`,
headers: {
accept: 'application/dns-json',
},
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
domain,
)}`,
headers: {
accept: 'application/dns-json',
},
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
}; };
function ResolveDomainOperator({ provider }) { function ResolveDomainOperator({ provider }) {
@@ -392,7 +451,9 @@ function ResolveDomainOperator({ provider }) {
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage. const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
const totalDomain = [ const totalDomain = [
...new Set( ...new Set(
proxies.filter((p) => !isIP(p.server)).map((c) => c.server), proxies
.filter((p) => !isIP(p.server) && !p['no-resolve'])
.map((c) => c.server),
), ),
]; ];
const totalBatch = Math.ceil(totalDomain.length / limit); const totalBatch = Math.ceil(totalDomain.length / limit);
@@ -416,8 +477,15 @@ function ResolveDomainOperator({ provider }) {
} }
await Promise.all(currentBatch); await Promise.all(currentBatch);
} }
proxies.forEach((proxy) => { proxies.forEach((p) => {
proxy.server = results[proxy.server] || proxy.server; if (!p['no-resolve']) {
if (results[p.server]) {
p.server = results[p.server];
p.resolved = true;
} else {
p.resolved = false;
}
}
}); });
return proxies; return proxies;
@@ -522,7 +590,7 @@ function TypeFilter(types) {
1. This function name should be `filter`! 1. This function name should be `filter`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptFilter(script, targetPlatform, $arguments) { function ScriptFilter(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -533,7 +601,7 @@ function ScriptFilter(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = filter(proxies, targetPlatform); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -566,7 +634,8 @@ async function ApplyFilter(filter, objs) {
selected = await filter.func(objs); selected = await filter.func(objs);
} catch (err) { } catch (err) {
// print log and skip this filter // print log and skip this filter
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`); $.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
throw new Error(`脚本过滤失败 ${err.message ?? err}`);
} }
return objs.filter((_, i) => selected[i]); return objs.filter((_, i) => selected[i]);
} }
@@ -577,8 +646,33 @@ async function ApplyOperator(operator, objs) {
const output_ = await operator.func(output); const output_ = await operator.func(output);
if (output_) output = output_; if (output_) output = output_;
} catch (err) { } catch (err) {
// print log and skip this operator $.error(
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`); `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
}
try {
const output_ = await operator.nodeFunc(output);
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
if (funcErr && nodeErrMsg === funcErrMsg) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
}
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
}
} }
return output; return output;
} }
@@ -625,6 +719,7 @@ function removeFlag(str) {
} }
function createDynamicFunction(name, script, $arguments) { function createDynamicFunction(name, script, $arguments) {
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(
'$arguments', '$arguments',
@@ -635,6 +730,7 @@ function createDynamicFunction(name, script, $arguments) {
'$notification', '$notification',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
@@ -648,6 +744,7 @@ function createDynamicFunction(name, script, $arguments) {
$notification, $notification,
ProxyUtils, ProxyUtils,
scriptResourceCache, scriptResourceCache,
flowUtils,
); );
} else { } else {
return new Function( return new Function(
@@ -656,7 +753,9 @@ function createDynamicFunction(name, script, $arguments) {
'lodash', 'lodash',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils, scriptResourceCache); )($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
} }
} }

View File

@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() { export default function Clash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter((proxy) => { proxies = proxies.filter((proxy) => {
if ( if (
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'vless',
'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type) ||
) { (proxy.type === 'snell' && String(proxy.version) === '4') ||
return false; (proxy.type === 'vless' &&
} else if ( (typeof proxy.flow !== 'undefined' ||
proxy.type === 'snell' && proxy['reality-opts']))
String(proxy.version) === '4'
) { ) {
return false; return false;
} }
@@ -61,6 +64,11 @@ export default function Clash_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if ( if (
@@ -82,10 +90,26 @@ export default function Clash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -0,0 +1,144 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
};
return { type, produce };
}

View File

@@ -1,5 +1,7 @@
import Surge_Producer from './surge'; import Surge_Producer from './surge';
import SurgeMac_Producer from './surgemac';
import Clash_Producer from './clash'; import Clash_Producer from './clash';
import ClashMeta_Producer from './clashmeta';
import Stash_Producer from './stash'; import Stash_Producer from './stash';
import Loon_Producer from './loon'; import Loon_Producer from './loon';
import URI_Producer from './uri'; import URI_Producer from './uri';
@@ -16,8 +18,10 @@ function JSON_Producer() {
export default { export default {
QX: QX_Producer(), QX: QX_Producer(),
Surge: Surge_Producer(), Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(), Loon: Loon_Producer(),
Clash: Clash_Producer(), Clash: Clash_Producer(),
ClashMeta: ClashMeta_Producer(),
URI: URI_Producer(), URI: URI_Producer(),
V2Ray: V2Ray_Producer(), V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(), JSON: JSON_Producer(),

View File

@@ -20,6 +20,8 @@ export default function Loon_Producer() {
return http(proxy); return http(proxy);
case 'wireguard': case 'wireguard':
return wireguard(proxy); return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -101,7 +103,7 @@ function trojan(proxy) {
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else { } else {
@@ -138,11 +140,11 @@ function vmess(proxy) {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
result.append(`,transport=ws`); result.append(`,transport=ws`);
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`, `,path=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
@@ -205,11 +207,11 @@ function vless(proxy) {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
result.append(`,transport=ws`); result.append(`,transport=ws`);
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`, `,path=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
@@ -334,3 +336,33 @@ function wireguard(proxy) {
return result.toString(); return result.toString();
} }
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}

View File

@@ -63,6 +63,19 @@ function shadowsocks(proxy) {
} }
if (needTls(proxy)) { if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -83,6 +96,12 @@ function shadowsocks(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -115,6 +134,12 @@ function shadowsocksr(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -135,11 +160,11 @@ function trojan(proxy) {
if (needTls(proxy)) append(`,obfs=wss`); if (needTls(proxy)) append(`,obfs=wss`);
else append(`,obfs=ws`); else append(`,obfs=ws`);
appendIfPresent( appendIfPresent(
`,obfs-uri=${proxy['ws-opts'].path}`, `,obfs-uri=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
appendIfPresent( appendIfPresent(
`,obfs-host=${proxy['ws-opts'].headers.Host}`, `,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else { } else {
@@ -153,6 +178,19 @@ function trojan(proxy) {
} }
if (needTls(proxy)) { if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -173,6 +211,12 @@ function trojan(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -230,6 +274,19 @@ function vmess(proxy) {
} }
if (needTls(proxy)) { if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -257,6 +314,12 @@ function vmess(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -279,6 +342,19 @@ function http(proxy) {
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
if (needTls(proxy)) { if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -299,6 +375,12 @@ function http(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -321,6 +403,19 @@ function socks5(proxy) {
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
if (needTls(proxy)) { if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -341,6 +436,12 @@ function socks5(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);

View File

@@ -1,6 +1,6 @@
import { isPresent } from '@/core/proxy-utils/producers/utils'; import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() { export default function ShadowRocket_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
return ( return (
@@ -63,6 +63,13 @@ export default function Stash_Producer() {
proxy.version = 5; proxy.version = 5;
} }
} else if (proxy.type === 'hysteria') { } else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) { if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn) proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn ? proxy.alpn
@@ -81,6 +88,11 @@ export default function Stash_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if ( if (
@@ -103,11 +115,26 @@ export default function Stash_Producer() {
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -12,13 +12,15 @@ export default function Stash_Producer() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'tuic', 'tuic',
'vless', 'vless',
'wireguard', 'wireguard',
'hysteria',
'hysteria2',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'snell' && (proxy.type === 'snell' &&
String(proxy.version) === '4') || String(proxy.version) === '4') ||
@@ -67,6 +69,7 @@ export default function Stash_Producer() {
!isPresent(proxy, 'fast-open') !isPresent(proxy, 'fast-open')
) { ) {
proxy['fast-open'] = proxy.tfo; proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
} }
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if ( if (
@@ -76,6 +79,13 @@ export default function Stash_Producer() {
proxy.version = 5; proxy.version = 5;
} }
} else if (proxy.type === 'hysteria') { } else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) { if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn) proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn ? proxy.alpn
@@ -86,6 +96,66 @@ export default function Stash_Producer() {
!isPresent(proxy, 'fast-open') !isPresent(proxy, 'fast-open')
) { ) {
proxy['fast-open'] = proxy.tfo; proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
}
if (isPresent(proxy, 'down-speed')) {
proxy['down-speed'] =
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
}
if (isPresent(proxy, 'up-speed')) {
proxy['up-speed'] =
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
}
} else if (proxy.type === 'hysteria2') {
if (
isPresent(proxy, 'password') &&
!isPresent(proxy, 'auth')
) {
proxy.auth = proxy.password;
delete proxy.password;
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
}
if (isPresent(proxy, 'down-speed')) {
proxy['down-speed'] =
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
}
if (isPresent(proxy, 'up-speed')) {
proxy['up-speed'] =
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
} }
} else if (proxy.type === 'wireguard') { } else if (proxy.type === 'wireguard') {
proxy.keepalive = proxy.keepalive =
@@ -94,6 +164,11 @@ export default function Stash_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if ( if (
@@ -115,10 +190,31 @@ export default function Stash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['test-url']) {
proxy['benchmark-url'] = proxy['test-url'];
delete proxy['test-url'];
}
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -29,6 +29,10 @@ export default function Surge_Producer() {
return snell(proxy); return snell(proxy);
case 'tuic': case 'tuic':
return tuic(proxy); return tuic(proxy);
case 'wireguard-surge':
return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -43,6 +47,16 @@ function shadowsocks(proxy) {
result.append(`,encrypt-method=${proxy.cipher}`); result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// obfs // obfs
if (isPresent(proxy, 'plugin')) { if (isPresent(proxy, 'plugin')) {
if (proxy.plugin === 'obfs') { if (proxy.plugin === 'obfs') {
@@ -69,6 +83,29 @@ function shadowsocks(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -77,6 +114,16 @@ function trojan(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// transport // transport
handleTransport(result, proxy); handleTransport(result, proxy);
@@ -105,6 +152,29 @@ function trojan(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -113,6 +183,16 @@ function vmess(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// transport // transport
handleTransport(result, proxy); handleTransport(result, proxy);
@@ -148,6 +228,29 @@ function vmess(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -158,6 +261,16 @@ function http(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls fingerprint // tls fingerprint
result.appendIfPresent( result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -180,6 +293,29 @@ function http(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -190,6 +326,16 @@ function socks5(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls fingerprint // tls fingerprint
result.appendIfPresent( result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -214,6 +360,29 @@ function socks5(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -223,6 +392,16 @@ function snell(proxy) {
result.appendIfPresent(`,version=${proxy.version}`, 'version'); result.appendIfPresent(`,version=${proxy.version}`, 'version');
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// obfs // obfs
result.appendIfPresent( result.appendIfPresent(
`,obfs=${proxy['obfs-opts']?.mode}`, `,obfs=${proxy['obfs-opts']?.mode}`,
@@ -243,6 +422,29 @@ function snell(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// reuse // reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
@@ -272,6 +474,11 @@ function tuic(proxy) {
'ip-version', 'ip-version',
); );
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification // tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent( result.appendIfPresent(
@@ -279,13 +486,169 @@ function tuic(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy['fast-open']}`, 'fast-open'); if (isPresent(proxy, 'tfo')) {
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}
function wireguard(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString(); return result.toString();
} }

View File

@@ -0,0 +1,65 @@
import { Result } from './utils';
import Surge_Producer from './surge';
const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
case 'ssr':
return shadowsocksr(proxy);
case 'ss':
return surge_Producer.produce(proxy);
case 'trojan':
return surge_Producer.produce(proxy);
case 'vmess':
return surge_Producer.produce(proxy);
case 'http':
return surge_Producer.produce(proxy);
case 'socks5':
return surge_Producer.produce(proxy);
case 'snell':
return surge_Producer.produce(proxy);
case 'tuic':
return surge_Producer.produce(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
);
};
return { produce };
}
function shadowsocksr(proxy) {
const result = new Result(proxy);
proxy.local_port = '__SubStoreLocalPort__';
proxy.local_address = proxy.local_address ?? '127.0.0.1';
result.append(
`${proxy.name} = external, exec = "${
proxy.exec || '/usr/local/bin/ssr-local'
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
);
for (const [key, value] of Object.entries({
cipher: '-m',
obfs: '-o',
password: '-k',
port: '-p',
protocol: '-O',
'protocol-param': '-G',
server: '-s',
local_port: '-l',
local_address: '-b',
})) {
result.appendIfPresent(
`, args = "${value}", args = "${proxy[key]}"`,
key,
);
}
return result.toString();
}

View File

@@ -91,6 +91,16 @@ export default function URI_Producer() {
? vmessTransportHost[0] ? vmessTransportHost[0]
: vmessTransportHost; : vmessTransportHost;
} }
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
'grpc-service-name'
];
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
}
} }
result = 'vmess://' + Base64.encode(JSON.stringify(result)); result = 'vmess://' + Base64.encode(JSON.stringify(result));
break; break;
@@ -141,6 +151,12 @@ export default function URI_Producer() {
let vlessTransport = `&type=${encodeURIComponent( let vlessTransport = `&type=${encodeURIComponent(
proxy.network, proxy.network,
)}`; )}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
}
let vlessTransportServiceName = let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[ proxy[`${proxy.network}-opts`]?.[

View File

@@ -47,7 +47,7 @@ function AllRuleParser() {
} }
if (!matched) throw new Error('Invalid rule type: ' + rawType); if (!matched) throw new Error('Invalid rule type: ' + rawType);
} catch (e) { } catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`); console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
} }
} }
return result; return result;

View File

@@ -7,7 +7,7 @@
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash. * Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */
import { version } from '../package.json'; import { version } from '../package.json';

View File

@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
import registerSettingRoutes from '@/restful/settings'; import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs'; import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort'; import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerModuleRoutes from '@/restful/module';
migrate(); migrate();
serve(); serve();
@@ -30,6 +32,8 @@ function serve() {
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerSortRoutes($app); registerSortRoutes($app);

View File

@@ -22,6 +22,16 @@ export default function register($app) {
function createCollection(req, res) { function createCollection(req, res) {
const collection = req.body; const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`); $.info(`正在创建组合订阅:${collection.name}`);
if (/\//.test(collection.name)) {
failed(
res,
new RequestInvalidError(
'INVALID_NAME',
`Collection ${collection.name} is invalid`,
),
);
return;
}
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
if (findByName(allCols, collection.name)) { if (findByName(allCols, collection.name)) {
failed( failed(
@@ -31,6 +41,7 @@ function createCollection(req, res) {
`Collection ${collection.name} already exists.`, `Collection ${collection.name} already exists.`,
), ),
); );
return;
} }
allCols.push(collection); allCols.push(collection);
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);

View File

@@ -20,6 +20,23 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`); $.info(`正在下载订阅:${name}`);
let { url, ua, content, mergeSources } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
@@ -29,12 +46,16 @@ async function downloadSubscription(req, res) {
type: 'subscription', type: 'subscription',
name, name,
platform, platform,
url,
ua,
content,
mergeSources,
}); });
if (sub.source !== 'local') { if (sub.source !== 'local' || url) {
try { try {
// forward flow headers // forward flow headers
const flowInfo = await getFlowHeaders(sub.url); const flowInfo = await getFlowHeaders(url || sub.url);
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
} }

109
backend/src/restful/file.js Normal file
View File

@@ -0,0 +1,109 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
}
// file API
function createFile(req, res) {
const file = req.body;
file.name = `${file.name ?? Date.now()}`;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${file.name} 的文件`
: `无法同时创建相同的文件 可稍后重试`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
res.status(200).json(file.content);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(
res, // eslint-disable-next-line no-unused-vars
allFiles.map(({ content, ...rest }) => rest),
);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -4,6 +4,8 @@ import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts'; import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync'; import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings'; import registerSettingRoutes from './settings';
@@ -13,8 +15,13 @@ import registerMiscRoutes from './miscs';
import registerNodeInfoRoutes from './node-info'; import registerNodeInfoRoutes from './node-info';
export default function serve() { export default function serve() {
const $app = express({ substore: $ }); let port;
let host;
if ($.env.isNode) {
port = eval('process.env.SUB_STORE_BACKEND_API_PORT');
host = eval('process.env.SUB_STORE_BACKEND_API_HOST');
}
const $app = express({ substore: $, port, host });
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
@@ -23,6 +30,8 @@ export default function serve() {
registerSortingRoutes($app); registerSortingRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);
registerMiscRoutes($app); registerMiscRoutes($app);

View File

@@ -1,7 +1,6 @@
import $ from '@/core/app'; import $ from '@/core/app';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import { import {
@@ -12,6 +11,7 @@ import {
import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import env from '@/utils/env';
export default function register($app) { export default function register($app) {
// utils // utils
@@ -49,19 +49,7 @@ export default function register($app) {
} }
function getEnv(req, res) { function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV(); success(res, env);
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
} }
async function refresh(_, res) { async function refresh(_, res) {
@@ -118,6 +106,23 @@ async function gistBackup(req, res) {
case 'download': case 'download':
$.info(`还原备份中...`); $.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME); content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (
Object.keys(JSON.parse(content).settings).length ===
0
) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings // restore settings
$.write(content, '#sub-store'); $.write(content, '#sub-store');
if ($.env.isNode) { if ($.env.isNode) {
@@ -125,8 +130,10 @@ async function gistBackup(req, res) {
$.cache = content; $.cache = content;
$.persistCache(); $.persistCache();
} }
// perform migration after restoring from gist $.info(`perform migration after restoring from gist...`);
migrate(); migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break; break;
} }
success(res); success(res);

View File

@@ -0,0 +1,114 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { MODULES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { hex_md5 } from '@/vendor/md5';
export default function register($app) {
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
$app.route('/api/module/:name')
.get(getModule)
.patch(updateModule)
.delete(deleteModule);
$app.route('/api/modules')
.get(getAllModules)
.post(createModule)
.put(replaceModule);
}
// module API
function createModule(req, res) {
const module = req.body;
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
$.info(`正在创建模块:${module.name}`);
const allModules = $.read(MODULES_KEY);
if (findByName(allModules, module.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${module.name} 的模块`
: `已存在相同的模块 请勿重复添加`,
),
);
}
allModules.push(module);
$.write(allModules, MODULES_KEY);
success(res, module, 201);
}
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
res.status(200).json(module.content);
} else {
failed(
res,
new ResourceNotFoundError(
`MODULE_NOT_FOUND`,
`Module ${name} does not exist`,
404,
),
);
}
}
function updateModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let module = req.body;
const allModules = $.read(MODULES_KEY);
const oldModule = findByName(allModules, name);
if (oldModule) {
const newModule = {
...oldModule,
...module,
};
$.info(`正在更新模块:${name}...`);
updateByName(allModules, name, newModule);
$.write(allModules, MODULES_KEY);
success(res, newModule);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Module ${name} does not exist!`,
),
404,
);
}
}
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);
$.write(allModules, MODULES_KEY);
success(res);
}
function getAllModules(req, res) {
const allModules = $.read(MODULES_KEY);
success(
res,
// eslint-disable-next-line no-unused-vars
allModules.map(({ content, ...rest }) => rest),
);
}
function replaceModule(req, res) {
const allModules = req.body;
$.write(allModules, MODULES_KEY);
success(res);
}

View File

@@ -12,98 +12,167 @@ export default function register($app) {
} }
async function compareSub(req, res) { async function compareSub(req, res) {
const sub = req.body; try {
const target = req.query.target || 'JSON'; const sub = req.body;
let content; const target = req.query.target || 'JSON';
if (sub.source === 'local') { let content;
content = sub.content; if (
} else { sub.source === 'local' &&
try { !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
content = await download(sub.url, sub.ua); ) {
} catch (err) { content = sub.content;
failed( } else {
res,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
}
// parse proxies
const original = ProxyUtils.parse(content);
// add id
original.forEach((proxy, i) => {
proxy.id = i;
});
// apply processors
const processed = await ProxyUtils.process(
original,
sub.process || [],
target,
);
// produce
success(res, { original, processed });
}
async function compareCollection(req, res) {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try { try {
let raw; content = await Promise.all(
if (sub.source === 'local') { sub.url
raw = sub.content; .split(/[\r\n]+/)
} else { .map((i) => i.trim())
raw = await download(sub.url, sub.ua); .filter((i) => i.length)
} .map((url) => download(url, sub.ua)),
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
); );
results[name] = currentProxies;
} catch (err) { } catch (err) {
failed( failed(
res, res,
new InternalServerError( new NetworkError(
'PROCESS_FAILED', 'FAILED_TO_DOWNLOAD_RESOURCE',
`处理子订阅 ${name} 失败`, '无法下载远程资源',
`Reason: ${err}`, `Reason: ${err}`,
), ),
); );
return;
} }
}), if (sub.mergeSources === 'localFirst') {
); content.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
content.push(sub.content);
}
}
// parse proxies
const original = (Array.isArray(content) ? content : [content])
.map((i) => ProxyUtils.parse(i))
.flat();
// merge proxies with the original order // add id
const original = Array.prototype.concat.apply( original.forEach((proxy, i) => {
[], proxy.id = i;
subnames.map((name) => results[name] || []), proxy.subName = sub.name;
); });
original.forEach((proxy, i) => { // apply processors
proxy.id = i; const processed = await ProxyUtils.process(
}); original,
sub.process || [],
target,
{ [sub.name]: sub },
);
const processed = await ProxyUtils.process( // produce
original, success(res, { original, processed });
collection.process || [], } catch (err) {
'JSON', $.error(err.message ?? err);
); failed(
res,
success(res, { original, processed }); new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview subscription`,
`Reason: ${err.message ?? err}`,
),
);
}
}
async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
let raw;
if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(
sub.mergeSources,
)
) {
raw = sub.content;
} else {
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
);
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
}
// parse proxies
let currentProxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
{ [sub.name]: sub, _collection: collection },
);
results[name] = currentProxies;
} catch (err) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
`Reason: ${err}`,
),
);
}
}),
);
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name] || []),
);
original.forEach((proxy, i) => {
proxy.id = i;
proxy.collectionName = collection.name;
});
const processed = await ProxyUtils.process(
original,
collection.process || [],
'JSON',
{ _collection: collection },
);
success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview collection`,
`Reason: ${err.message ?? err}`,
),
);
}
} }

View File

@@ -44,8 +44,12 @@ export async function updateGitHubAvatar() {
.then((resp) => JSON.parse(resp.body)); .then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url']; settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} catch (e) { } catch (err) {
$.error('Failed to fetch GitHub avatar for User: ' + username); $.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }
@@ -67,7 +71,11 @@ export async function updateArtifactStore() {
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} }
} catch (err) { } catch (err) {
$.error('Failed to fetch artifact store for User: ' + githubUser); $.error(
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }

View File

@@ -11,7 +11,7 @@ export default function register($app) {
function sortSubs(req, res) { function sortSubs(req, res) {
const orders = req.body; const orders = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
success(res, allSubs); success(res, allSubs);
} }
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) { function sortCollections(req, res) {
const orders = req.body; const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
success(res, allCols); success(res, allCols);
} }
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) { function sortArtifacts(req, res) {
const orders = req.body; const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts); success(res, allArtifacts);
} }

View File

@@ -6,7 +6,7 @@ import {
} from './errors'; } from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -68,20 +68,7 @@ async function getFlowInfo(req, res) {
return; return;
} }
// unit is KB success(res, parseFlowHeaders(flowHeaders));
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
success(res, { expires, total, usage: { upload, download } });
} catch (err) { } catch (err) {
failed( failed(
res, res,
@@ -96,6 +83,16 @@ async function getFlowInfo(req, res) {
function createSubscription(req, res) { function createSubscription(req, res) {
const sub = req.body; const sub = req.body;
$.info(`正在创建订阅: ${sub.name}`); $.info(`正在创建订阅: ${sub.name}`);
if (/\//.test(sub.name)) {
failed(
res,
new RequestInvalidError(
'INVALID_NAME',
`Subscription ${sub.name} is invalid`,
),
);
return;
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
if (findByName(allSubs, sub.name)) { if (findByName(allSubs, sub.name)) {
failed( failed(
@@ -105,6 +102,7 @@ function createSubscription(req, res) {
`Subscription ${sub.name} already exists.`, `Subscription ${sub.name} already exists.`,
), ),
); );
return;
} }
allSubs.push(sub); allSubs.push(sub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);

View File

@@ -22,25 +22,69 @@ export default function register($app) {
$app.get('/api/sync/artifact/:name', syncArtifact); $app.get('/api/sync/artifact/:name', syncArtifact);
} }
async function produceArtifact({ type, name, platform }) { async function produceArtifact({
type,
name,
platform,
url,
ua,
content,
mergeSources,
}) {
platform = platform || 'JSON'; platform = platform || 'JSON';
if (type === 'subscription') { if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
let raw; let raw;
if (sub.source === 'local') { if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, ua)),
);
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
raw = sub.content; raw = sub.content;
} else { } else {
raw = await download(sub.url, sub.ua); raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
);
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
} }
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
proxies.forEach((proxy) => {
proxy.subName = sub.name;
});
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`); throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -51,7 +95,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -79,18 +123,43 @@ async function produceArtifact({ type, name, platform }) {
try { try {
$.info(`正在处理子订阅:${sub.name}...`); $.info(`正在处理子订阅:${sub.name}...`);
let raw; let raw;
if (sub.source === 'local') { if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(
sub.mergeSources,
)
) {
raw = sub.content; raw = sub.content;
} else { } else {
raw = await download(sub.url, sub.ua); raw = await await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
);
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors // apply processors
currentProxies = await ProxyUtils.process( currentProxies = await ProxyUtils.process(
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection },
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -127,11 +196,16 @@ async function produceArtifact({ type, name, platform }) {
subnames.map((name) => results[name] || []), subnames.map((name) => results[name] || []),
); );
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
});
// apply own processors // apply own processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -142,7 +216,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -255,20 +329,20 @@ async function syncArtifact(req, res) {
return; return;
} }
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try { try {
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
const resp = await syncToGist({ const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: { [encodeURIComponent(artifact.name)]: {
content: output, content: output,

View File

@@ -1,12 +1,52 @@
import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(url, ua) { export default async function download(url, ua) {
let $arguments = {};
const rawArgs = url.split('#');
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到本地 ${type}: ${name}`);
}
return item.content;
}
const { isNode } = ENV(); const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; const { defaultUserAgent } = $.read(SETTINGS_KEY);
ua = ua || defaultUserAgent || 'clash.meta';
const id = hex_md5(ua + url); const id = hex_md5(ua + url);
if (!isNode && tasks.has(id)) { if (!isNode && tasks.has(id)) {
return tasks.get(id); return tasks.get(id);
@@ -21,9 +61,10 @@ export default async function download(url, ua) {
const result = new Promise((resolve, reject) => { const result = new Promise((resolve, reject) => {
// try to find in app cache // try to find in app cache
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) { if (!$arguments?.noCache && cached) {
resolve(cached); resolve(cached);
} else { } else {
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
http.get(url) http.get(url)
.then((resp) => { .then((resp) => {
const body = resp.body; const body = resp.body;

16
backend/src/utils/env.js Normal file
View File

@@ -0,0 +1,16 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
export default {
backend,
version: substoreVersion,
};

View File

@@ -3,7 +3,10 @@ import { HTTP } from '@/vendor/open-api';
export async function getFlowHeaders(url) { export async function getFlowHeaders(url) {
const http = HTTP(); const http = HTTP();
const { headers } = await http.get({ const { headers } = await http.get({
url, url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: { headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)', 'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
}, },
@@ -13,3 +16,28 @@ export async function getFlowHeaders(url) {
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export function parseFlowHeaders(flowHeaders) {
if (!flowHeaders) return;
// unit is KB
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
return { expires, total, usage: { upload, download } };
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}

View File

@@ -1,14 +1,18 @@
export function getPlatformFromHeaders(headers) { export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let UA = ''; let UA = '';
let ua = '';
for (let k of keys) { for (let k of keys) {
if (/USER-AGENT/i.test(k)) { if (/USER-AGENT/i.test(k)) {
UA = headers[k]; UA = headers[k];
ua = UA.toLowerCase();
break; break;
} }
} }
if (UA.indexOf('Quantumult%20X') !== -1) { if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX'; return 'QX';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) { } else if (UA.indexOf('Surge') !== -1) {
return 'Surge'; return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
@@ -17,6 +21,15 @@ export function getPlatformFromHeaders(headers) {
return 'ShadowRocket'; return 'ShadowRocket';
} else if (UA.indexOf('Stash') !== -1) { } else if (UA.indexOf('Stash') !== -1) {
return 'Stash'; return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else { } else {
return 'JSON'; return 'JSON';
} }

View File

@@ -48,7 +48,7 @@ class ResourceCache {
} }
set(id, value) { set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value } this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist(); this._persist();
} }
} }

View File

@@ -1,8 +1,9 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { ENV } from './open-api'; import { ENV } from './open-api';
export default function express({ substore: $, port }) { export default function express({ substore: $, port, host }) {
port = port || 3000; port = port || 3000;
host = host || '::';
const { isNode } = ENV(); const { isNode } = ENV();
const DEFAULT_HEADERS = { const DEFAULT_HEADERS = {
'Content-Type': 'text/plain;charset=UTF-8', 'Content-Type': 'text/plain;charset=UTF-8',
@@ -17,7 +18,7 @@ export default function express({ substore: $, port }) {
const express_ = eval(`require("express")`); const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`); const bodyParser = eval(`require("body-parser")`);
const app = express_(); const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver })); app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use( app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }), bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
); );
@@ -29,8 +30,9 @@ export default function express({ substore: $, port }) {
// adapter // adapter
app.start = () => { app.start = () => {
app.listen(port, () => { const listener = app.listen(port, host, () => {
$.info(`Express started on port: ${port}`); const { address, port } = listener.address();
$.info(`Express started on ${address}:${port}`);
}); });
}; };
return app; return app;

View File

@@ -49,27 +49,32 @@ export class OpenAPI {
if (isNode) { if (isNode) {
// create a json for root cache // create a json for root cache
let fpath = 'root.json'; const basePath =
if (!this.node.fs.existsSync(fpath)) { eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.root = {}; this.root = {};
} else { } else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`)); this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} }
// create a json file with the given name if not exists // create a json file with the given name if not exists
fpath = `${this.name}.json`; let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.cache = {}; this.cache = {};
} else { } else {
this.cache = JSON.parse( this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.node.fs.readFileSync(`${this.name}.json`),
);
} }
} }
} }
@@ -80,14 +85,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name); if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) { if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
`${this.name}.json`, `${basePath}/${this.name}.json`,
data, data,
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
); );
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
'root.json', `${basePath}/root.json`,
JSON.stringify(this.root, null, 2), JSON.stringify(this.root, null, 2),
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
@@ -308,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
return ( return (
timer timer
? Promise.race([timer, worker]).then((res) => { ? Promise.race([timer, worker]).then((res) => {
clearTimeout(timeoutid); if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutid);
}
return res; return res;
}) })
: worker : worker

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
#!desc=高级订阅管理工具 #!desc=高级订阅管理工具
#!openUrl=https://sub.store #!openUrl=https://sub.store
#!author=Peng-YM #!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store #!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png #!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入 #!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入

7
config/QX-Task.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name":"Sub-Store",
"description":"",
"task":[
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
]
}

View File

@@ -1,20 +1,32 @@
# Sub-Store 配置指南 # Sub-Store 配置指南
## 查看更新说明:
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置: ## 脚本配置:
### 1. Loon ### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。 安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge ### 2. Surge
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX ### 3. QX
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可 订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
### 4. Stash ### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。 安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket ### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
## 使用 Sub-Store ## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。 1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。