Compare commits

...

74 Commits

Author SHA1 Message Date
xream
c059296224 feat: 文件支持脚本操作 2024-01-12 06:16:39 +08:00
xream
9ae70eca09 feat: 同步配置支持文件 2024-01-12 03:52:41 +08:00
xream
d0acf49b83 feat: 文件接口 2024-01-12 02:23:57 +08:00
xream
c51f3511dd fix: 兼容部分不带节点名的 URI 2024-01-08 09:44:53 +08:00
xream
ee2fcc7ee3 fix: 兼容部分不带参数的 URI 输入 2024-01-08 09:28:33 +08:00
xream
95615d1877 feat: 支持全局请求超时(前端 > 2.14.29) 2024-01-08 07:22:03 +08:00
xream
962bcda9dd chore: 同步远程配置输出更多日志 2024-01-07 17:44:03 +08:00
xream
9bb4739d56 Node.js 版的通知支持第三方推送服务. 环境变量名 SUB_STORE_PUSH_SERVICE. 支持 Bark/PushPlus 等服务. 形如: https://api.day.app/XXXXXXXXX/[推送标题]/[推送内容]?group=SubStore&autoCopy=1&isArchive=1&sound=shake&level=timeSensitivehttp://www.pushplus.plus/send?token=XXXXXXXXX&title=[推送标题]&content=[推送内容]&channel=wechat 的 URL, [推送标题][推送内容] 会被自动替换 2024-01-02 22:52:33 +08:00
xream
de1d40f41a feat: Wireguard 结构跟进 Clash.Meta, allowed_ips 改为 allowed-ips 2024-01-02 16:38:48 +08:00
xream
c0ab301160 feat: Trojan URI 支持 gRPC 2023-12-29 16:08:02 +08:00
xream
a22df97a51 release: backend version 2.14.135 2023-12-29 15:42:31 +08:00
xream
45772ade4d Merge pull request #263 from Ariesly/ipv6-uri
fix: Handles node-info IPv6 address URIs
2023-12-29 15:39:03 +08:00
Ariesly
e8dab545f5 fix: Handles node-info IPv6 address URIs 2023-12-29 07:10:47 +00:00
xream
c2bd80207a doc: 补充文档 2023-12-27 02:55:04 +08:00
xream
bc5ae9a2ef feat: 支持 Surfboard(前端 > 2.14.27) 2023-12-27 00:28:15 +08:00
xream
36db057e32 feat: 当节点端口号为合法端口号时, 将类型转为整数(便于脚本判断) 2023-12-23 21:02:39 +08:00
xream
5ac73b863a feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:42:33 +08:00
xream
23042c33d6 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:41:37 +08:00
xream
4ca5f5e355 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:24:48 +08:00
xream
f10e5913fb feat: 兼容部分不规范的机场 Hysteria/Hysteria2 端口跳跃字段为空时 删除此字段 2023-12-17 18:31:12 +08:00
xream
8b75c11587 feat: Hysteria2 URI 输入支持 hy2:// 2023-12-17 16:13:34 +08:00
xream
c287dcad3b fix: 过滤 Stash/Clash Shadowsocks cipher 2023-12-13 20:11:36 +08:00
xream
ce6cd794c8 feat: 环境变量 SUB_STORE_DATA_URL 启动时自动从此地址拉取并恢复数据 2023-12-13 09:54:57 +08:00
xream
e05475aa5e feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 02:04:24 +08:00
xream
c35e9d37ae feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 01:26:16 +08:00
xream
8f2dbfe3df feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 00:34:08 +08:00
xream
a0a998dfdd feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:11 +08:00
xream
12491ac7c0 feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:03 +08:00
xream
78e3024cec feat: Node.js 前端代理后端路由 2023-12-12 22:52:50 +08:00
xream
5e21a20e37 fix: 修复 Loon Trojan WS 传输层 2023-12-12 21:13:17 +08:00
xream
76b5dc5809 feat: 脚本筛选支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
const port = Number($server.port)

return [80, 443].includes(port)
```
2023-12-11 11:57:12 +08:00
xream
a1776644a0 feat: Node 版后端支持挂载前端文件夹, 环境变量 SUB_STORE_FRONTEND_PATH, SUB_STORE_FRONTEND_HOST, SUB_STORE_FRONTEND_PORT 2023-12-10 13:13:39 +08:00
xream
7aaa03d4ca chore: workflow 2023-12-10 09:32:56 +08:00
xream
d0cba285ab fix: 处理 Hysteria2 URI 中的密码部分 2023-12-09 02:08:59 +08:00
xream
d636e1b94c fix: 处理预览时子订阅出错的情况 2023-12-08 18:16:50 +08:00
xream
69726cd5c4 fix: 处理 IPv6 地址 URI 2023-12-08 17:53:07 +08:00
xream
8918479b9e release: backend version 2.14.114 2023-12-08 11:49:11 +08:00
xream
17504ab5aa Merge pull request #261 from Ariesly/master 2023-12-08 11:45:58 +08:00
Ariesly
0d8fa91cd5 fix(hysteria2): For shadowrocket obfs 2023-12-08 01:51:54 +00:00
Ariesly
e7dfa1ce38 chore(hysteria2): Uri support with tfo 2023-12-08 01:34:53 +00:00
Ariesly
fe937d6ebf fix(hysteria2): Change to TLS Fingerprint 2023-12-08 01:30:09 +00:00
xream
b7b734f529 release: backend version 2.14.113 2023-12-07 18:15:21 +08:00
xream
f5ef6010bc Merge pull request #260 from Ariesly/master
feat: Hysteria2 URI
2023-12-07 18:03:26 +08:00
Ariesly
0e82a7669d feat: Hysteria2 URI 2023-12-07 06:25:33 +00:00
xream
6d11ea0fcc feat: ProxyUtils.produce 增加第二个参数 type, 暂时仅支持目标为 ClashMetainternal 输出节点数组供开发者使用 2023-12-05 21:53:22 +08:00
xream
75f802f607 fix: 默认 User-Agent 改为 clash.meta 后, 调整订阅预处理器的逻辑, 减少 Base64 误判 2023-12-05 12:43:13 +08:00
xream
000e90d114 feat: 手动下载备份文件和使用备份上传恢复(前端版本 > 2.14.15) 2023-12-04 16:07:10 +08:00
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
Johnson
6216217286 feat: add qx sync task 2023-10-25 10:44:22 -05:00
48 changed files with 3055 additions and 1435 deletions

View File

@@ -44,19 +44,15 @@ jobs:
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
if: ${{ success() }}
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 }}
generate_release_notes: true
files: | files: |
./backend/sub-store.min.js ./backend/sub-store.min.js
./backend/dist/sub-store-0.min.js ./backend/dist/sub-store-0.min.js

View File

@@ -7,7 +7,7 @@
</div> </div>
<p align="center" color="#6a737d"> <p align="center" color="#6a737d">
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/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) [![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)
@@ -31,23 +31,25 @@ Core functionalities:
- [x] SSD URI - [x] SSD URI
- [x] V2RayN URI - [x] V2RayN URI
- [x] Hysteria2 URI - [x] Hysteria2 URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge)) - [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2) - [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2) - [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria) - [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
- [x] QX - [x] QX
- [x] Loon - [x] Loon
- [x] Surge - [x] Surge
- [x] Surfboard
- [x] Stash - [x] Stash
- [x] Clash.Meta - [x] Clash.Meta
- [x] Clash - [x] Clash
- [x] ShadowRocket - [x] Shadowrocket
- [x] V2Ray - [x] V2Ray
- [x] V2Ray URI - [x] V2Ray URI
- [x] Plain JSON - [x] Plain JSON
@@ -76,24 +78,20 @@ Core functionalities:
### 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.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { build } = require('esbuild');
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), { let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8', encoding: 'utf8',
@@ -14,10 +14,12 @@ fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
encoding: 'utf8', encoding: 'utf8',
}); });
const { build } = require('estrella');
build({ build({
entry: 'dist/sub-store.no-bundle.js', entryPoints: ['dist/sub-store.no-bundle.js'],
outfile: 'dist/sub-store.bundle.js',
bundle: true, bundle: true,
minify: true,
sourcemap: true,
platform: 'node', platform: 'node',
format: 'cjs',
outfile: 'dist/sub-store.bundle.js',
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.79", "version": "2.14.146",
"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": {
@@ -9,15 +9,16 @@
"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", "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",
"dependencies": { "dependencies": {
"automerge": "1.0.1-preview.7", "automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"request": "^2.88.2", "request": "^2.88.2",
@@ -38,6 +39,7 @@
"browser-pack-flat": "^3.4.2", "browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"esbuild": "^0.19.8",
"eslint": "^8.16.0", "eslint": "^8.16.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",

2496
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import download from '@/utils/download'; import download from '@/utils/download';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers'; import PROXY_PRODUCERS from './producers';
@@ -71,7 +71,12 @@ async function process(proxies, operators = [], targetPlatform, source) {
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) {
@@ -93,7 +98,9 @@ async function process(proxies, operators = [], targetPlatform, source) {
// 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(
@@ -132,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
return proxies; return proxies;
} }
function produce(proxies, targetPlatform) { function produce(proxies, targetPlatform, type) {
const producer = PROXY_PRODUCERS[targetPlatform]; const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) { if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`); throw new Error(`Target platform: ${targetPlatform} is not supported!`);
@@ -150,7 +157,7 @@ function produce(proxies, targetPlatform) {
return proxies return proxies
.map((proxy) => { .map((proxy) => {
try { try {
let line = producer.produce(proxy); let line = producer.produce(proxy, type);
if ( if (
line.length > 0 && line.length > 0 &&
line.includes('__SubStoreLocalPort__') line.includes('__SubStoreLocalPort__')
@@ -175,7 +182,7 @@ function produce(proxies, targetPlatform) {
.filter((line) => line.length > 0) .filter((line) => line.length > 0)
.join('\n'); .join('\n');
} else if (producer.type === 'ALL') { } else if (producer.type === 'ALL') {
return producer.produce(proxies); return producer.produce(proxies, type);
} }
} }
@@ -183,6 +190,9 @@ export const ProxyUtils = {
parse, parse,
process, process,
produce, produce,
isIPv4,
isIPv6,
isIP,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -204,6 +214,15 @@ function safeMatch(parser, line) {
} }
function lastParse(proxy) { function lastParse(proxy) {
if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10);
}
if (proxy.server) {
proxy.server = proxy.server
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '');
}
if (proxy.type === 'trojan') { if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') { if (proxy.network === 'tcp') {
delete proxy.network; delete proxy.network;
@@ -212,6 +231,14 @@ function lastParse(proxy) {
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].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;
@@ -252,6 +279,9 @@ function lastParse(proxy) {
proxy[`${proxy.network}-opts`].path = [transportPath]; proxy[`${proxy.network}-opts`].path = [transportPath];
} }
} }
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
delete proxy.ports;
}
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];
@@ -330,11 +332,14 @@ function URI_VLESS() {
const parse = (line) => { const parse = (line) => {
line = line.split('vless://')[1]; line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, addons, name] = let [__, uuid, server, port, ___, addons = '', name] =
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line); /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10); port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid); uuid = decodeURIComponent(uuid);
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`; if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `VLESS ${server}:${port}`;
const proxy = { const proxy = {
type: 'vless', type: 'vless',
name, name,
@@ -407,19 +412,22 @@ function URI_VLESS() {
function URI_Hysteria2() { function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser'; const name = 'URI Hysteria2 Parser';
const test = (line) => { const test = (line) => {
return /^hysteria2:\/\//.test(line); return /^(hysteria2|hy2):\/\//.test(line);
}; };
const parse = (line) => { const parse = (line) => {
line = line.split('hysteria2://')[1]; line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, addons, name] = let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line); /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10); port = parseInt(`${port}`, 10);
if (isNaN(port)) { if (isNaN(port)) {
port = 443; port = 443;
} }
password = decodeURIComponent(password); password = decodeURIComponent(password);
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`; if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria2 ${server}:${port}`;
const proxy = { const proxy = {
type: 'hysteria2', type: 'hysteria2',
@@ -463,8 +471,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 };
@@ -487,7 +503,7 @@ function Clash_All() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
@@ -519,6 +535,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 };
@@ -642,6 +662,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';
@@ -719,6 +748,7 @@ function Loon_WireGuard() {
let publicKey = peers.match( let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, /(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2]; )?.[2];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
const proxy = { const proxy = {
type: 'wireguard', type: 'wireguard',
name, name,
@@ -745,7 +775,7 @@ function Loon_WireGuard() {
ipv6, ipv6,
'public-key': publicKey, 'public-key': publicKey,
'pre-shared-key': preSharedKey, 'pre-shared-key': preSharedKey,
allowed_ips: allowedIps, 'allowed-ips': allowedIps,
reserved, reserved,
}, },
], ],
@@ -864,6 +894,7 @@ export default [
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

@@ -36,7 +36,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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,10 +76,10 @@ 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/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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;
} }
@@ -89,10 +89,10 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
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)* { 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"; proxy.type = "hysteria2";
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { 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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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;
} }
@@ -163,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; }

View File

@@ -34,7 +34,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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,10 +74,10 @@ 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/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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;
} }
@@ -87,10 +87,10 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
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)* { 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"; proxy.type = "hysteria2";
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { 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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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;
} }
@@ -161,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; }

View File

@@ -90,11 +90,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
if (params["type"]) { if (params["type"]) {
proxy.network = params["type"] proxy.network = params["type"]
if (params["path"]) { if (['grpc'].includes(proxy.network)) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"])); proxy[proxy.network + '-opts'] = {
} 'grpc-service-name': params["serviceName"],
if (params["host"]) { '_grpc-type': params["mode"],
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); };
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
} }
} }

View File

@@ -88,11 +88,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
if (params["type"]) { if (params["type"]) {
proxy.network = params["type"] proxy.network = params["type"]
if (params["path"]) { if (['grpc'].includes(proxy.network)) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"])); proxy[proxy.network + '-opts'] = {
} 'grpc-service-name': params["serviceName"],
if (params["host"]) { '_grpc-type': params["mode"],
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); };
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
} }
} }

View File

@@ -25,7 +25,10 @@ function Base64Encoded() {
]; ];
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);
@@ -37,7 +40,9 @@ function Base64Encoded() {
function Clash() { function Clash() {
const name = 'Clash Pre-processor'; const name = 'Clash Pre-processor';
const test = function (raw) { const test = function (raw) {
return /proxies/.test(raw); if (!/proxies/.test(raw)) return false;
const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies);
}; };
const parse = function (raw) { const parse = function (raw) {
// Clash YAML format // Clash YAML format
@@ -105,4 +110,4 @@ function FullConfig() {
return { name, test, parse }; return { name, test, parse };
} }
export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()]; export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];

View File

@@ -8,6 +8,7 @@ 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 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:
@@ -310,6 +311,32 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
})(); })();
return output; return output;
}, },
nodeFunc: async (proxies) => {
let output = proxies;
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(input = []) {
let proxies
if (Array.isArray(input)) {
proxies = input
return proxies.map(($server = {}) => {
${script}
return $server
})
} else {
let $content = input
${script}
return $content
}
}`,
$arguments,
);
output = operator(proxies, targetPlatform, { source, ...env });
})();
return output;
},
}; };
} }
@@ -433,7 +460,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);
@@ -457,8 +486,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;
@@ -578,6 +614,22 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
})(); })();
return output; return output;
}, },
nodeFunc: async (proxies) => {
let output = FULL(proxies.length, true);
await (async function () {
const filter = createDynamicFunction(
'filter',
`async function filter(proxies = []) {
return proxies.filter(($server = {}) => {
${script}
})
}`,
$arguments,
);
output = filter(proxies, targetPlatform, { source, ...env });
})();
return output;
},
}; };
} }
@@ -608,7 +660,29 @@ async function ApplyFilter(filter, objs) {
} catch (err) { } catch (err) {
// print log and skip this filter // print log and skip this filter
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`); $.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
throw new Error(`脚本过滤失败 ${err.message ?? err}`); let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
}
try {
selected = await filter.nodeFunc(objs);
} catch (err) {
$.error(
`Cannot apply filter ${filter.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 objs.filter((_, i) => selected[i]); return objs.filter((_, i) => selected[i]);
} }
@@ -619,9 +693,36 @@ 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(
$.error(`Cannot apply operator ${operator.name}! Reason: ${err}`); `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
throw new Error(`脚本操作失败 ${err.message ?? err}`); );
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (
funcErrMsg.includes('$server is not defined') ||
funcErrMsg.includes('$content 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;
} }
@@ -668,6 +769,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',
@@ -678,6 +780,7 @@ function createDynamicFunction(name, script, $arguments) {
'$notification', '$notification',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
@@ -691,6 +794,7 @@ function createDynamicFunction(name, script, $arguments) {
$notification, $notification,
ProxyUtils, ProxyUtils,
scriptResourceCache, scriptResourceCache,
flowUtils,
); );
} else { } else {
return new Function( return new Function(
@@ -699,7 +803,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

@@ -7,6 +7,7 @@ export default function Clash_Producer() {
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532 // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files // github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies // filter unsupported proxies
// https://clash.wiki/configuration/outbound.html#shadowsocks
proxies = proxies.filter((proxy) => { proxies = proxies.filter((proxy) => {
if ( if (
![ ![
@@ -14,12 +15,29 @@ export default function Clash_Producer() {
'ssr', 'ssr',
'vmess', 'vmess',
'vless', 'vless',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'wireguard', 'wireguard',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') || (proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' && (proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' || (typeof proxy.flow !== 'undefined' ||

View File

@@ -2,136 +2,143 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() { export default function ClashMeta_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies, type) => {
return ( const list = proxies
'proxies:\n' + .filter((proxy) => {
proxies if (proxy.type === 'snell' && String(proxy.version) === '4') {
.filter((proxy) => { 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 ( if (
proxy.type === 'snell' && isPresent(proxy, 'cipher') &&
String(proxy.version) === '4' ![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) { ) {
return false; proxy.cipher = 'auto';
} }
return true; } else if (proxy.type === 'tuic') {
}) if (isPresent(proxy, 'alpn')) {
.map((proxy) => { proxy.alpn = Array.isArray(proxy.alpn)
if (proxy.type === 'vmess') { ? proxy.alpn
// handle vmess aead : [proxy.alpn];
if (isPresent(proxy, 'aead')) { } else {
if (proxy.aead) { proxy.alpn = ['h3'];
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') {
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 (
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 ( if (
['vmess', 'vless'].includes(proxy.type) && isPresent(proxy, 'http-opts.path') &&
proxy.network === 'http' !Array.isArray(httpPath)
) { ) {
let httpPath = proxy['http-opts']?.path; proxy['http-opts'].path = [httpPath];
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];
}
} }
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type, proxy.type,
) )
) { ) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy['tls-fingerprint']) { if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint']; proxy.fingerprint = proxy['tls-fingerprint'];
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
if ( if (
['grpc'].includes(proxy.network) && ['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`] proxy[`${proxy.network}-opts`]
) { ) {
delete proxy[`${proxy.network}-opts`]['_grpc-type']; delete proxy[`${proxy.network}-opts`]['_grpc-type'];
} }
return ' - ' + JSON.stringify(proxy) + '\n'; return proxy;
}) });
.join('')
); return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
}; };
return { type, produce }; return { type, produce };
} }

View File

@@ -8,6 +8,7 @@ import URI_Producer from './uri';
import V2Ray_Producer from './v2ray'; import V2Ray_Producer from './v2ray';
import QX_Producer from './qx'; import QX_Producer from './qx';
import ShadowRocket_Producer from './shadowrocket'; import ShadowRocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
function JSON_Producer() { function JSON_Producer() {
const type = 'ALL'; const type = 'ALL';
@@ -27,4 +28,5 @@ export default {
JSON: JSON_Producer(), JSON: JSON_Producer(),
Stash: Stash_Producer(), Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(), ShadowRocket: ShadowRocket_Producer(),
Surfboard: Surfboard_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}`,
@@ -97,7 +99,7 @@ function trojan(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(
@@ -283,7 +285,8 @@ function wireguard(proxy) {
proxy.ipv6 = proxy.peers[0].ipv6; proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key']; proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key']; proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips']; // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
proxy.reserved = proxy.peers[0].reserved; proxy.reserved = proxy.peers[0].reserved;
} }
const result = new Result(proxy); const result = new Result(proxy);
@@ -334,3 +337,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}`);
@@ -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

@@ -63,6 +63,32 @@ export default function ShadowRocket_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')) {
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 === 'hysteria2') {
if (
proxy['obfs-password'] &&
proxy.obfs == 'salamander'
) {
proxy.obfs = proxy['obfs-password'];
delete proxy['obfs-password'];
}
if (isPresent(proxy, 'alpn')) { if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn) proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn ? proxy.alpn

View File

@@ -3,6 +3,7 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() { export default function Stash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
return ( return (
'proxies:\n' + 'proxies:\n' +
proxies proxies
@@ -12,7 +13,7 @@ export default function Stash_Producer() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
@@ -22,6 +23,23 @@ export default function Stash_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && (proxy.type === 'snell' &&
String(proxy.version) === '4') || String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts']) (proxy.type === 'vless' && proxy['reality-opts'])
@@ -79,6 +97,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
@@ -105,6 +130,14 @@ export default function Stash_Producer() {
proxy['up-speed'] = proxy.up; proxy['up-speed'] = proxy.up;
delete 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') { } else if (proxy.type === 'hysteria2') {
if ( if (
isPresent(proxy, 'password') && isPresent(proxy, 'password') &&
@@ -134,6 +167,14 @@ export default function Stash_Producer() {
proxy['up-speed'] = proxy.up; proxy['up-speed'] = proxy.up;
delete 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 =
proxy.keepalive ?? proxy['persistent-keepalive']; proxy.keepalive ?? proxy['persistent-keepalive'];
@@ -178,6 +219,12 @@ export default function Stash_Producer() {
proxy.fingerprint = 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.subName;
delete proxy.collectionName; delete proxy.collectionName;
if ( if (

View File

@@ -0,0 +1,199 @@
import { Result, isPresent } from './utils';
import { isNotBlank } from '@/utils';
// import $ from '@/core/app';
const targetPlatform = 'Surfboard';
export default function Surfboard_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
case 'http':
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'wireguard-surge':
return wireguard(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
);
};
return { produce };
}
function shadowsocks(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
// obfs
if (isPresent(proxy, 'plugin')) {
if (proxy.plugin === 'obfs') {
result.append(`,obfs=${proxy['plugin-opts'].mode}`);
result.appendIfPresent(
`,obfs-host=${proxy['plugin-opts'].host}`,
'plugin-opts.host',
);
result.appendIfPresent(
`,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path',
);
} else {
throw new Error(`plugin ${proxy.plugin} is not supported`);
}
}
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
// transport
handleTransport(result, proxy);
// tls
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function vmess(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
// transport
handleTransport(result, proxy);
// AEAD
if (isPresent(proxy, 'aead')) {
result.append(`,vmess-aead=${proxy.aead}`);
} else {
result.append(`,vmess-aead=${proxy.alterId === 0}`);
}
// tls
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function http(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
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',
);
return result.toString();
}
function handleTransport(result, proxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,ws=true`);
if (isPresent(proxy, 'ws-opts')) {
result.appendIfPresent(
`,ws-path=${proxy['ws-opts'].path}`,
'ws-opts.path',
);
if (isPresent(proxy, 'ws-opts.headers')) {
const headers = proxy['ws-opts'].headers;
const value = Object.keys(headers)
.map((k) => {
let v = headers[k];
if (['Host'].includes(k)) {
v = `"${v}"`;
}
return `${k}:${v}`;
})
.join('|');
if (isNotBlank(value)) {
result.append(`,ws-headers=${value}`);
}
}
}
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
}
}

View File

@@ -47,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') {
@@ -104,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);
@@ -163,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);
@@ -231,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']}`,
@@ -286,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']}`,
@@ -342,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}`,
@@ -414,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(

View File

@@ -1,7 +1,7 @@
import { Result } from './utils'; import { Result } from './utils';
import Surge_Producer from './surge'; import Surge_Producer from './surge';
const targetPlatform = 'SurgeMac'; // const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer(); const surge_Producer = Surge_Producer();
@@ -10,24 +10,9 @@ export default function SurgeMac_Producer() {
switch (proxy.type) { switch (proxy.type) {
case 'ssr': case 'ssr':
return shadowsocksr(proxy); return shadowsocksr(proxy);
case 'ss': default:
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); return surge_Producer.produce(proxy);
} }
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
);
}; };
return { produce }; return { produce };
} }

View File

@@ -1,10 +1,14 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { isIPv6 } from '@/utils';
export default function URI_Producer() { export default function URI_Producer() {
const type = 'SINGLE'; const type = 'SINGLE';
const produce = (proxy) => { const produce = (proxy) => {
let result = ''; let result = '';
if (proxy.server && isIPv6(proxy.server)) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`; const userinfo = `${proxy.cipher}:${proxy.password}`;
@@ -197,6 +201,21 @@ export default function URI_Producer() {
let trojanTransport = ''; let trojanTransport = '';
if (proxy.network) { if (proxy.network) {
trojanTransport = `&type=${proxy.network}`; trojanTransport = `&type=${proxy.network}`;
if (['grpc'].includes(proxy.network)) {
let trojanTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
)}`;
}
let trojanTransportPath = let trojanTransportPath =
proxy[`${proxy.network}-opts`]?.path; proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost = let trojanTransportHost =
@@ -222,6 +241,44 @@ export default function URI_Producer() {
proxy['skip-cert-verify'] ? '&allowInsecure=1' : '' proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`; }${trojanTransport}#${encodeURIComponent(proxy.name)}`;
break; break;
case 'hysteria2':
let hysteria2params = [];
if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`);
}
if (proxy.obfs) {
hysteria2params.push(
`obfs=${encodeURIComponent(proxy.obfs)}`,
);
if (proxy['obfs-password']) {
hysteria2params.push(
`obfs-password=${encodeURIComponent(
proxy['obfs-password'],
)}`,
);
}
}
if (proxy.sni) {
hysteria2params.push(
`sni=${encodeURIComponent(proxy.sni)}`,
);
}
if (proxy['tls-fingerprint']) {
hysteria2params.push(
`pinSHA256=${encodeURIComponent(
proxy['tls-fingerprint'],
)}`,
);
}
if (proxy.tfo) {
hysteria2params.push(`fastopen=1`);
}
result = `hysteria2://${encodeURIComponent(proxy.password)}@${
proxy.server
}:${proxy.port}?${hysteria2params.join(
'&',
)}#${encodeURIComponent(proxy.name)}`;
break;
} }
return result; return result;
}; };

View File

@@ -20,6 +20,27 @@ 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, ignoreFailedRemoteSub } = 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}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
@@ -29,12 +50,17 @@ async function downloadSubscription(req, res) {
type: 'subscription', type: 'subscription',
name, name,
platform, platform,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
}); });
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);
} }
@@ -95,12 +121,20 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`); $.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub } = req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (collection) { if (collection) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'collection', type: 'collection',
name, name,
platform, platform,
ignoreFailedRemoteSub,
}); });
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection

View File

@@ -3,6 +3,7 @@ import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors'; import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { ProxyUtils } from '@/core/proxy-utils';
export default function register($app) { export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY); if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
@@ -12,7 +13,10 @@ export default function register($app) {
.patch(updateFile) .patch(updateFile)
.delete(deleteFile); .delete(deleteFile);
$app.route('/api/wholeFile/:name').get(getWholeFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile); $app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
$app.route('/api/wholeFiles').get(getAllWholeFiles);
} }
// file API // file API
@@ -37,13 +41,35 @@ function createFile(req, res) {
success(res, file, 201); success(res, file, 201);
} }
function getFile(req, res) { async function getFile(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY); const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name); const file = findByName(allFiles, name);
if (file) { if (file) {
res.status(200).json(file.content); let content = file.content ?? '';
content = await ProxyUtils.process(content, file.process || []);
res.set('Content-Type', 'text/plain; charset=utf-8').send(
content ?? '',
);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function getWholeFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
} else { } else {
failed( failed(
res, res,
@@ -102,6 +128,11 @@ function getAllFiles(req, res) {
); );
} }
function getAllWholeFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(res, allFiles);
}
function replaceFile(req, res) { function replaceFile(req, res) {
const allFiles = req.body; const allFiles = req.body;
$.write(allFiles, FILES_KEY); $.write(allFiles, FILES_KEY);

View File

@@ -1,5 +1,7 @@
import express from '@/vendor/express'; import express from '@/vendor/express';
import $ from '@/core/app'; import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
@@ -15,8 +17,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') || 3000;
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);
@@ -32,4 +39,120 @@ export default function serve() {
registerMiscRoutes($app); registerMiscRoutes($app);
$app.start(); $app.start();
if ($.env.isNode) {
const path = eval(`require("path")`);
const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
const fe_host =
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
const fe_abs_path = path.resolve(
fe_path || path.join(__dirname, 'frontend'),
);
if (fe_path) {
try {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
const express_ = eval(`require("express")`);
const history = eval(`require("connect-history-api-fallback")`);
const { createProxyMiddleware } = eval(
`require("http-proxy-middleware")`,
);
const app = express_();
const staticFileMiddleware = express_.static(fe_path);
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
let be_download = '/download/';
if (fe_be_path) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
be_api_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_api}`;
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
},
}),
);
app.use(
be_download_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_download_rewrite)
? path.replace(be_download_rewrite, be_download)
: path;
},
}),
);
}
app.use(staticFileMiddleware);
app.use(
history({
disableDotRule: true,
verbose: false,
}),
);
app.use(staticFileMiddleware);
const listener = app.listen(fe_port, fe_host, () => {
const { address: fe_address, port: fe_port } =
listener.address();
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
);
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
);
}
});
}
if (data_url) {
$.info(`[BACKEND] downloading data from ${data_url}`);
download(data_url)
.then((content) => {
$.write(content, '#sub-store');
$.cache = JSON.parse(content);
$.persistCache();
migrate();
$.info(`[BACKEND] restored data from ${data_url}`);
})
.catch((e) => {
$.error(`[BACKEND] restore data failed`);
console.error(e);
throw e;
});
}
}
} }

View File

@@ -22,12 +22,26 @@ export default function register($app) {
// Storage management // Storage management
$app.route('/api/storage') $app.route('/api/storage')
.get((req, res) => { .get((req, res) => {
res.json($.read('#sub-store')); res.set('content-type', 'application/json')
.set(
'content-disposition',
'attachment; filename="sub-store.json"',
)
.send(
$.env.isNode
? JSON.stringify($.cache)
: $.read('#sub-store'),
);
}) })
.post((req, res) => { .post((req, res) => {
const data = req.body; const { content } = req.body;
$.write(JSON.stringify(data), '#sub-store'); $.write(content, '#sub-store');
res.end(); if ($.env.isNode) {
$.cache = JSON.parse(content);
$.persistCache();
}
migrate();
success(res);
}); });
// Redirect sub.store to vercel webpage // Redirect sub.store to vercel webpage

View File

@@ -47,7 +47,9 @@ function getModule(req, res) {
const allModules = $.read(MODULES_KEY); const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name); const module = findByName(allModules, name);
if (module) { if (module) {
res.status(200).json(module.content); res.set('Content-Type', 'text/plain; charset=utf-8').send(
module.content,
);
} else { } else {
failed( failed(
res, res,

View File

@@ -22,7 +22,10 @@ async function getNodeInfo(req, res) {
const info = await $http const info = await $http
.get({ .get({
url: `http://ip-api.com/json/${encodeURIComponent( url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server, proxy.server
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '')
)}?lang=${lang}`, )}?lang=${lang}`,
headers: { headers: {
'User-Agent': 'User-Agent':

View File

@@ -1,4 +1,4 @@
import { InternalServerError, NetworkError } from './errors'; import { InternalServerError } from './errors';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { findByName } from '@/utils/database'; import { findByName } from '@/utils/database';
import { success, failed } from './response'; import { success, failed } from './response';
@@ -9,6 +9,28 @@ import $ from '@/core/app';
export default function register($app) { export default function register($app) {
$app.post('/api/preview/sub', compareSub); $app.post('/api/preview/sub', compareSub);
$app.post('/api/preview/collection', compareCollection); $app.post('/api/preview/collection', compareCollection);
$app.post('/api/preview/file', previewFile);
}
async function previewFile(req, res) {
try {
let { content = '', process = [] } = req.body;
const processed = await ProxyUtils.process(content, process || []);
// produce
success(res, { original: content, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview file`,
`Reason: ${err.message ?? err}`,
),
);
}
} }
async function compareSub(req, res) { async function compareSub(req, res) {
@@ -16,25 +38,48 @@ async function compareSub(req, res) {
const sub = req.body; const sub = req.body;
const target = req.query.target || 'JSON'; const target = req.query.target || 'JSON';
let content; let content;
if (sub.source === 'local') { if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
content = sub.content; content = sub.content;
} else { } else {
try { const errors = {};
content = await download(sub.url, sub.ua); content = await Promise.all(
} catch (err) { sub.url
failed( .split(/[\r\n]+/)
res, .map((i) => i.trim())
new NetworkError( .filter((i) => i.length)
'FAILED_TO_DOWNLOAD_RESOURCE', .map(async (url) => {
'无法下载远程资源', try {
`Reason: ${err}`, return await download(url, sub.ua);
), } catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
); );
return; }
if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
content.push(sub.content);
} }
} }
// parse proxies // parse proxies
const original = ProxyUtils.parse(content); const original = (Array.isArray(content) ? content : [content])
.map((i) => ProxyUtils.parse(i))
.flat();
// add id // add id
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
@@ -71,19 +116,58 @@ async function compareCollection(req, res) {
const collection = req.body; const collection = req.body;
const subnames = collection.subscriptions; const subnames = collection.subscriptions;
const results = {}; const results = {};
const errors = {};
await Promise.all( await Promise.all(
subnames.map(async (name) => { subnames.map(async (name) => {
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
try { try {
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); const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
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) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy.subName = sub.name;
@@ -99,18 +183,28 @@ async function compareCollection(req, res) {
); );
results[name] = currentProxies; results[name] = currentProxies;
} catch (err) { } catch (err) {
failed( errors[name] = err;
res,
new InternalServerError( $.error(
'PROCESS_FAILED', `❌ 处理组合订阅中的子订阅: ${
`处理子订阅 ${name} 失败`, sub.name
`Reason: ${err}`, }时出现错误:${err}!进度--${
), 100 * (processed / subnames.length).toFixed(1)
}%`,
); );
} }
}), }),
); );
if (
!collection.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order // merge proxies with the original order
const original = Array.prototype.concat.apply( const original = Array.prototype.concat.apply(
[], [],

View File

@@ -10,7 +10,12 @@ export default function register($app) {
} }
async function getSettings(req, res) { async function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY); let settings = $.read(SETTINGS_KEY);
if (!settings) {
settings = {};
$.write(settings, SETTINGS_KEY);
}
if (!settings.avatarUrl) await updateGitHubAvatar(); if (!settings.avatarUrl) await updateGitHubAvatar();
if (!settings.artifactStore) await updateArtifactStore(); if (!settings.artifactStore) await updateArtifactStore();
success(res, settings); success(res, settings);

View File

@@ -1,4 +1,9 @@
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants'; import {
ARTIFACTS_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
import { success } from '@/restful/response'; import { success } from '@/restful/response';
@@ -6,6 +11,7 @@ export default function register($app) {
$app.post('/api/sort/subs', sortSubs); $app.post('/api/sort/subs', sortSubs);
$app.post('/api/sort/collections', sortCollections); $app.post('/api/sort/collections', sortCollections);
$app.post('/api/sort/artifacts', sortArtifacts); $app.post('/api/sort/artifacts', sortArtifacts);
$app.post('/api/sort/files', sortFiles);
} }
function sortSubs(req, res) { function sortSubs(req, res) {
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts); success(res, allArtifacts);
} }
function sortFiles(req, res) {
const orders = req.body;
const allFiles = $.read(FILES_KEY);
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allFiles, FILES_KEY);
success(res, allFiles);
}

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,

View File

@@ -4,6 +4,7 @@ import {
COLLECTIONS_KEY, COLLECTIONS_KEY,
RULES_KEY, RULES_KEY,
SUBS_KEY, SUBS_KEY,
FILES_KEY,
} from '@/constants'; } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors'; import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -22,20 +23,105 @@ 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,
ignoreFailedRemoteSub,
}) {
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) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
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); const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
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) => { proxies.forEach((proxy) => {
proxy.subName = sub.name; proxy.subName = sub.name;
}); });
@@ -83,13 +169,52 @@ 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); const errors = {};
raw = await await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
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) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy.subName = sub.name;
@@ -116,15 +241,21 @@ async function produceArtifact({ type, name, platform }) {
$.error( $.error(
`❌ 处理组合订阅中的子订阅: ${ `❌ 处理组合订阅中的子订阅: ${
sub.name sub.name
}时出现错误:${err},该订阅已被跳过!进度--${ }时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1) 100 * (processed / subnames.length).toFixed(1)
}%`, }%`,
); );
} }
}), }),
); );
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
if (Object.keys(errors).length > 0) { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error( throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join( `组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ', ', ',
@@ -197,6 +328,13 @@ async function produceArtifact({ type, name, platform }) {
]); ]);
// produce output // produce output
return RuleUtils.produce(rules, platform); return RuleUtils.produce(rules, platform);
} else if (type === 'file') {
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let content = file.content ?? '';
content = await ProxyUtils.process(content, file.process || []);
return content ?? '';
} }
} }
@@ -256,10 +394,12 @@ async function syncAllArtifacts(_, res) {
async function syncArtifact(req, res) { async function syncArtifact(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name); const artifact = findByName(allArtifacts, name);
if (!artifact) { if (!artifact) {
$.error(`找不到远程配置 ${name}`);
failed( failed(
res, res,
new ResourceNotFoundError( new ResourceNotFoundError(
@@ -298,6 +438,7 @@ async function syncArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact); success(res, artifact);
} catch (err) { } catch (err) {
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
failed( failed(
res, res,
new InternalServerError( new InternalServerError(

View File

@@ -1,4 +1,4 @@
import { FILES_KEY, MODULES_KEY } from '@/constants'; import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
import { findByName } from '@/utils/database'; 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';
@@ -7,7 +7,7 @@ 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, timeout) {
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
@@ -45,16 +45,19 @@ export default async function download(url, ua) {
} }
const { isNode } = ENV(); const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const id = hex_md5(ua + url); const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) { if (!isNode && tasks.has(id)) {
return tasks.get(id); return tasks.get(id);
} }
const http = HTTP({ const http = HTTP({
headers: { headers: {
'User-Agent': ua, 'User-Agent': userAgent,
}, },
timeout: requestTimeout,
}); });
const result = new Promise((resolve, reject) => { const result = new Promise((resolve, reject) => {
@@ -63,6 +66,9 @@ export default async function download(url, ua) {
if (!$arguments?.noCache && cached) { if (!$arguments?.noCache && cached) {
resolve(cached); resolve(cached);
} else { } else {
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
);
http.get(url) http.get(url)
.then((resp) => { .then((resp) => {
const body = resp.body; const body = resp.body;

View File

@@ -1,15 +1,52 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api'; import { HTTP } from '@/vendor/open-api';
import $ from '@/core/app';
export async function getFlowHeaders(url) { export async function getFlowHeaders(url, ua, timeout) {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
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': userAgent,
}, },
timeout: requestTimeout,
}); });
const subkey = Object.keys(headers).filter((k) => const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k), /SUBSCRIPTION-USERINFO/i.test(k),
)[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

@@ -13,6 +13,12 @@ function isIPv6(ip) {
return IPV6_REGEX.test(ip); return IPV6_REGEX.test(ip);
} }
function isValidPortNumber(port) {
return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
port,
);
}
function isNotBlank(str) { function isNotBlank(str) {
return typeof str === 'string' && str.trim().length > 0; return typeof str === 'string' && str.trim().length > 0;
} }
@@ -29,4 +35,12 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue; return isPresent(obj) ? obj : defaultValue;
} }
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent }; export {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
getIfNotBlank,
isPresent,
getIfPresent,
};

View File

@@ -11,6 +11,8 @@ export function getPlatformFromHeaders(headers) {
} }
if (UA.indexOf('Quantumult%20X') !== -1) { if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX'; return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) { } else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac'; return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) { } else if (UA.indexOf('Surge') !== -1) {

View File

@@ -1,8 +1,7 @@
/* 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;
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',
@@ -29,8 +28,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(`[BACKEND] ${address}:${port}`);
}); });
}; };
return app; return app;

View File

@@ -191,6 +191,32 @@ export class OpenAPI {
(openURL ? `\n点击跳转: ${openURL}` : '') + (openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : ''); (mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`); console.log(`${title}\n${subtitle}\n${content_}\n\n`);
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
if (push) {
const url = push
.replace(
'[推送标题]',
encodeURIComponent(title || 'Sub-Store'),
)
.replace(
'[推送内容]',
encodeURIComponent(
[subtitle, content_].map((i) => i).join('\n'),
),
);
const $http = HTTP();
$http
.get({ url })
.then((resp) => {
console.log(
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
);
})
.catch((e) => {
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
});
}
} }
} }

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

@@ -22,6 +22,8 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
### 3. QX ### 3. QX
订阅 重写 [`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) 即可。 订阅 重写 [`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) 即可。
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
### 4. Stash ### 4. Stash
安装使用 覆写 [`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) 即可。 安装使用 覆写 [`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) 即可。

40
package-lock.json generated
View File

@@ -1,40 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
}