Compare commits

...

54 Commits

Author SHA1 Message Date
xream
27a14bb255 revert: 回滚文件模板功能 2024-01-14 12:33:36 +08:00
xream
5ecce27f4e feat: 脚本内部 produceArtifact 支持指定 produceType: 'internal', produceOpts: { 'include-unsupported-proxy': true } 来获得内部的数据结构; 订阅链接参数支持 type=internal&includeUnsupportedProxy=true; 文件支持 nunjucks 模板, 为 sing-box 增加的 Filter 用法 sub/col 为订阅/组合订阅中的节点名 {{ '订阅的name' | sub('美国|🇺🇸|us', 'i') }}, subNode/colNode 为订阅/组合订阅中的节点 {{ '订阅的name' | subNode('美国|🇺🇸|us', 'i') }}, 底层 produceArtifact('subscription', 'sing-box', 'internal', '美国|🇺🇸|us', 'i') 2024-01-14 12:13:29 +08:00
xream
12903d77f7 fix: 修复无脚本操作时 文件数据结构错误 2024-01-13 22:01:02 +08:00
xream
0c6ec7f82a revert: 同步接口支持 POST 2024-01-13 20:40:45 +08:00
xream
ba251ced34 fix: 同步接口支持 POST 2024-01-13 19:56:30 +08:00
xream
d96a0421f7 fix: Surge Snell TFO 2024-01-13 16:51:48 +08:00
xream
aff7ddf41e feat: 脚本筛选的快捷操作支持 await 2024-01-13 13:55:07 +08:00
xream
164ae9a7a8 feat: 快捷脚本支持 await; 脚本操作支持 produceArtifact 2024-01-13 13:40:34 +08:00
xream
3aacd26b79 feat: 支持输出到 sing-box; 文件脚本支持 ` ; 脚本支持 ProxyUtils.yaml` 2024-01-13 10:28:07 +08:00
xream
5915416232 feat: 文件支持远程/合并, /api/file/name 接口支持参数覆盖 2024-01-12 07:22:25 +08:00
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
37 changed files with 3523 additions and 1484 deletions

View File

@@ -37,7 +37,6 @@ jobs:
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag
name: Generate release tag
@@ -45,18 +44,14 @@ jobs:
cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release
uses: softprops/action-gh-release@v1
if: ${{ success() }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }}
generate_release_notes: true
files: |
./backend/sub-store.min.js
./backend/dist/sub-store-0.min.js

View File

@@ -7,7 +7,7 @@
</div>
<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>
[![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] V2RayN URI
- [x] Hysteria2 URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [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
- [x] QX
- [x] Loon
- [x] Surge
- [x] Surfboard
- [x] Stash
- [x] Clash.Meta
- [x] Clash
- [x] ShadowRocket
- [x] Shadowrocket
- [x] V2Ray
- [x] V2Ray URI
- [x] Plain JSON

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.112",
"version": "2.14.157",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -9,15 +9,16 @@
"serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp",
"bundle": "node bundle.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
"bundle": "node bundle.js"
},
"author": "Peng-YM",
"license": "GPL-3.0",
"dependencies": {
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"request": "^2.88.2",
@@ -38,6 +39,7 @@
"browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0",
"chai": "^4.3.6",
"esbuild": "^0.19.8",
"eslint": "^8.16.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",

2501
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import YAML from 'static-js-yaml';
import download from '@/utils/download';
import { isIPv4, isIPv6 } from '@/utils';
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
@@ -59,7 +60,6 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`);
}
}
return proxies;
}
@@ -139,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
return proxies;
}
function produce(proxies, targetPlatform, type) {
function produce(proxies, targetPlatform, type, opts = {}) {
const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
@@ -154,10 +154,10 @@ function produce(proxies, targetPlatform, type) {
$.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
return proxies
const list = proxies
.map((proxy) => {
try {
let line = producer.produce(proxy, type);
let line = producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
@@ -179,10 +179,10 @@ function produce(proxies, targetPlatform, type) {
return '';
}
})
.filter((line) => line.length > 0)
.join('\n');
.filter((line) => line.length > 0);
return type === 'internal' ? list : list.join('\n');
} else if (producer.type === 'ALL') {
return producer.produce(proxies, type);
return producer.produce(proxies, type, opts);
}
}
@@ -193,6 +193,7 @@ export const ProxyUtils = {
isIPv4,
isIPv6,
isIP,
yaml: YAML,
};
function tryParse(parser, line) {
@@ -214,6 +215,15 @@ function safeMatch(parser, line) {
}
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.network === 'tcp') {
delete proxy.network;
@@ -270,6 +280,9 @@ function lastParse(proxy) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
delete proxy.ports;
}
return proxy;
}

View File

@@ -332,11 +332,14 @@ function URI_VLESS() {
const parse = (line) => {
line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, addons, name] =
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
let [__, uuid, server, port, ___, addons = '', name] =
/^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
@@ -409,19 +412,22 @@ function URI_VLESS() {
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^hysteria2:\/\//.test(line);
return /^(hysteria2|hy2):\/\//.test(line);
};
const parse = (line) => {
line = line.split('hysteria2://')[1];
line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, addons, name] =
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria2 ${server}:${port}`;
const proxy = {
type: 'hysteria2',
@@ -742,6 +748,7 @@ function Loon_WireGuard() {
let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
const proxy = {
type: 'wireguard',
name,
@@ -768,7 +775,7 @@ function Loon_WireGuard() {
ipv6,
'public-key': publicKey,
'pre-shared-key': preSharedKey,
allowed_ips: allowedIps,
'allowed-ips': allowedIps,
reserved,
},
],

View File

@@ -90,11 +90,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
if (params["type"]) {
proxy.network = params["type"]
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"]));
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} 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"]) {
proxy.network = params["type"]
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"]));
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} 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

@@ -7,6 +7,8 @@ import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import env from '@/utils/env';
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
@@ -316,11 +318,21 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(proxies = []) {
return proxies.map(($server = {}) => {
${script}
return $server
})
`async function operator(input = []) {
if (Array.isArray(input)) {
let proxies = input
let list = []
for await (let $server of proxies) {
${script}
list.push($server)
}
return list
} else {
let { $content, $files } = input
${script}
return { $content, $files }
}
}`,
$arguments,
);
@@ -605,6 +617,28 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
})();
return output;
},
nodeFunc: async (proxies) => {
let output = FULL(proxies.length, true);
await (async function () {
const filter = createDynamicFunction(
'filter',
`async function filter(input = []) {
let proxies = input
let list = []
const fn = async ($server) => {
${script}
}
for await (let $server of proxies) {
list.push(await fn($server))
}
return list
}`,
$arguments,
);
output = filter(proxies, targetPlatform, { source, ...env });
})();
return output;
},
};
}
@@ -633,9 +667,32 @@ async function ApplyFilter(filter, objs) {
try {
selected = await filter.func(objs);
} catch (err) {
// print log and skip this filter
$.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 {
$.error(
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
);
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]);
}
@@ -646,14 +703,18 @@ async function ApplyOperator(operator, objs) {
const output_ = await operator.func(output);
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
if (
funcErrMsg.includes('$server is not defined') ||
funcErrMsg.includes('$content is not defined') ||
funcErrMsg.includes('$files is not defined')
) {
funcErr = '';
} else {
$.error(
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
}
try {
@@ -731,6 +792,7 @@ function createDynamicFunction(name, script, $arguments) {
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)(
$arguments,
@@ -745,6 +807,7 @@ function createDynamicFunction(name, script, $arguments) {
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
} else {
return new Function(
@@ -754,8 +817,17 @@ function createDynamicFunction(name, script, $arguments) {
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
)(
$arguments,
$,
lodash,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
}
}

View File

@@ -2,118 +2,139 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
const produce = (proxies, type, opts = {}) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies
proxies = proxies.filter((proxy) => {
if (
![
'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}
return true;
});
return (
'proxies:\n' +
proxies
.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://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} 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;
// https://clash.wiki/configuration/outbound.html#shadowsocks
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].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 === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
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://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} 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 (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
delete proxy.tls;
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -2,9 +2,10 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
const produce = (proxies, type, opts = {}) => {
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
}

View File

@@ -7,7 +7,9 @@ import Loon_Producer from './loon';
import URI_Producer from './uri';
import V2Ray_Producer from './v2ray';
import QX_Producer from './qx';
import ShadowRocket_Producer from './shadowrocket';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
function JSON_Producer() {
const type = 'ALL';
@@ -17,6 +19,7 @@ function JSON_Producer() {
export default {
QX: QX_Producer(),
QuantumultX: QX_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
@@ -26,5 +29,8 @@ export default {
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(),
Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(),
Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(),
Surfboard: Surfboard_Producer(),
'sing-box': singbox_Producer(),
};

View File

@@ -99,7 +99,7 @@ function trojan(proxy) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
@@ -285,7 +285,8 @@ function wireguard(proxy) {
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-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;
}
const result = new Result(proxy);

View File

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

View File

@@ -0,0 +1,688 @@
import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
};
const smuxParser = (smux, proxy) => {
if (!smux || !smux.enabled) return;
proxy.multiplex = { enabled: true };
proxy.multiplex.protocol = smux.protocol;
if (smux['max-connections'])
proxy.multiplex.max_connections = parseInt(
`${smux['max-connections']}`,
10,
);
if (smux['max-streams'])
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true;
};
const wsParser = (proxy, parsedProxy) => {
const transport = { type: 'ws', headers: {} };
if (proxy['ws-opts']) {
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
if (wsPath !== '') transport.path = `${wsPath}`;
if (Object.keys(wsHeaders).length > 0) {
const headers = {};
for (const key of Object.keys(wsHeaders)) {
let value = wsHeaders[key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
transport.headers = headers;
}
}
if (proxy['ws-headers']) {
const headers = {};
for (const key of Object.keys(proxy['ws-headers'])) {
let value = proxy['ws-headers'][key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
for (const key of Object.keys(headers))
transport.headers[key] = headers[key];
}
if (proxy['ws-path'] && proxy['ws-path'] !== '')
transport.path = `${proxy['ws-path']}`;
if (transport.path) {
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(transport.path);
transport.path = path;
if (ed !== '') {
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
transport.max_early_data = parseInt(ed, 10);
}
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.headers.Host[0];
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
transport.type = 'httpupgrade';
if (transport.headers.Host) {
transport.host = transport.headers.Host[0];
delete transport.headers.Host;
}
if (transport.max_early_data) delete transport.max_early_data;
if (transport.early_data_header_name)
delete transport.early_data_header_name;
}
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h1Parser = (proxy, parsedProxy) => {
const transport = { type: 'http', headers: {} };
if (proxy['http-opts']) {
const {
method = '',
path: h1Path = '',
headers: h1Headers = {},
} = proxy['http-opts'];
if (method !== '') transport.method = method;
if (Array.isArray(h1Path)) {
transport.path = `${h1Path[0]}`;
} else if (h1Path !== '') transport.path = `${h1Path}`;
for (const key of Object.keys(h1Headers)) {
let value = h1Headers[key];
if (value === '') continue;
if (key.toLowerCase() === 'host') {
let host = value;
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
continue;
}
if (!Array.isArray(value))
value = `${value}`.split(',').map((i) => i.trim());
if (value.length > 0) transport.headers[key] = value;
}
}
if (proxy['http-host'] && proxy['http-host'] !== '') {
let host = proxy['http-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (!transport.host) return;
if (proxy['http-path'] && proxy['http-path'] !== '') {
const path = proxy['http-path'];
if (Array.isArray(path)) {
transport.path = `${path[0]}`;
} else if (path !== '') transport.path = `${path}`;
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h2Parser = (proxy, parsedProxy) => {
const transport = { type: 'http' };
if (proxy['h2-opts']) {
let { host = '', path = '' } = proxy['h2-opts'];
if (path !== '') transport.path = `${path}`;
if (host !== '') {
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
}
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
let host = proxy['h2-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (proxy['h2-path'] && proxy['h2-path'] !== '')
transport.path = `${proxy['h2-path']}`;
parsedProxy.tls.enabled = true;
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
parsedProxy.transport = transport;
};
const grpcParser = (proxy, parsedProxy) => {
const transport = { type: 'grpc' };
if (proxy['grpc-opts']) {
const serviceName = proxy['grpc-opts']['grpc-service-name'];
if (serviceName && serviceName !== '')
transport.service_name = serviceName;
}
parsedProxy.transport = transport;
};
const tlsParser = (proxy, parsedProxy) => {
if (proxy.tls) parsedProxy.tls.enabled = true;
if (proxy.servername && proxy.servername !== '')
parsedProxy.tls.server_name = proxy.servername;
if (proxy.peer && proxy.peer !== '')
parsedProxy.tls.server_name = proxy.peer;
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
if (proxy.insecure) parsedProxy.tls.insecure = true;
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
if (typeof proxy.alpn === 'string') {
parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (proxy['reality-opts']) {
parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key'])
parsedProxy.tls.reality.public_key =
proxy['reality-opts']['public-key'];
if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id'];
}
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
const httpParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'http',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.headers) {
parsedProxy.headers = {};
for (const k of Object.keys(proxy.headers)) {
parsedProxy.headers[k] = `${proxy.headers[k]}`;
}
if (Object.keys(parsedProxy.headers).length === 0)
delete parsedProxy.headers;
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const socks5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'socks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
version: '5',
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
return parsedProxy;
};
const ssParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
parsedProxy.plugin = 'obfs-local';
parsedProxy.plugin_opts = '';
if (proxy['obfs-host'])
proxy['plugin-opts'].host = proxy['obfs-host'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'mode':
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
break;
case 'host':
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
break;
}
});
}
if (proxy.plugin === 'v2ray-plugin') {
parsedProxy.plugin = 'v2ray-plugin';
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'tls':
if (proxy['plugin-opts'].tls) optArr.push('tls');
break;
case 'host':
optArr.push(`host=${proxy['plugin-opts'].host}`);
break;
case 'path':
optArr.push(`path=${proxy['plugin-opts'].path}`);
break;
case 'headers':
optArr.push(
`headers=${JSON.stringify(
proxy['plugin-opts'].headers,
)}`,
);
break;
case 'mux':
if (proxy['plugin-opts'].mux)
parsedProxy.multiplex = { enabled: true };
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
}
});
}
parsedProxy.plugin_opts = optArr.join(';');
}
return parsedProxy;
};
// eslint-disable-next-line no-unused-vars
const ssrParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocksr',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
obfs: proxy.obfs,
protocol: proxy.protocol,
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
parsedProxy.protocol_param = proxy['protocol-param'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vmessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vmess',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
security: proxy.cipher,
alter_id: parseInt(`${proxy.alterId}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (
[
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'aes-128-ctr',
].indexOf(parsedProxy.security) === -1
)
parsedProxy.security = 'auto';
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vlessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vless',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'trojan',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
disable_mtu_discovery: false,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
if (reg.test(`${proxy.up}`)) {
parsedProxy.up = `${proxy.up}`;
} else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
}
if (reg.test(`${proxy.down}`)) {
parsedProxy.down = `${proxy.down}`;
} else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
}
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
if (proxy.recv_window_conn)
parsedProxy.recv_window_conn = proxy.recv_window_conn;
if (proxy['recv-window-conn'])
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
if (proxy.disable_mtu_discovery) {
if (typeof proxy.disable_mtu_discovery === 'boolean') {
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
} else {
if (proxy.disable_mtu_discovery === 1)
parsedProxy.disable_mtu_discovery = true;
}
}
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
obfs: {},
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'tuic',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (
proxy['congestion-controller'] &&
proxy['congestion-controller'] !== 'cubic'
)
parsedProxy.congestion_control = proxy['congestion-controller'];
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
tfoParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'wireguard',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
local_address: [proxy.ip, proxy.ipv6],
private_key: proxy['private-key'],
peer_public_key: proxy['public-key'],
pre_shared_key: proxy['pre-shared-key'],
reserved: [],
};
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved);
} else {
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
}
if (proxy.peers && proxy.peers.length > 0) {
parsedProxy.peers = [];
for (const p of proxy.peers) {
const peer = {
server: p.server,
server_port: parseInt(`${p.port}`, 10),
public_key: p['public-key'],
allowed_ips: p.allowed_ips,
reserved: [],
};
if (typeof p.reserved === 'string') {
peer.reserved.push(p.reserved);
} else {
for (const r of p.reserved) peer.reserved.push(r);
}
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
parsedProxy.peers.push(peer);
}
}
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
export default function singbox_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
const list = [];
ClashMeta_Producer()
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
.map((proxy) => {
try {
switch (proxy.type) {
case 'http':
list.push(httpParser(proxy));
break;
case 'socks5':
if (proxy.tls) {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
);
} else {
list.push(socks5Parser(proxy));
}
break;
case 'ss':
if (proxy.plugin === 'shadow-tls') {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
);
} else {
list.push(ssParser(proxy));
}
break;
case 'ssr':
if (opts['include-unsupported-proxy']) {
list.push(ssrParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
break;
case 'vmess':
if (
!proxy.network ||
['ws', 'grpc', 'h2', 'http'].includes(
proxy.network,
)
) {
list.push(vmessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
);
}
break;
case 'vless':
if (
!proxy.flow ||
['xtls-rprx-vision'].includes(proxy.flow)
) {
list.push(vlessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'trojan':
if (!proxy.flow) {
list.push(trojanParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'hysteria':
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
list.push(tuic5Parser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: TUIC v4`,
);
}
break;
case 'wireguard':
list.push(wireguardParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
} catch (e) {
// console.log(e);
$.error(e.message ?? e);
}
});
return type === 'internal' ? list : JSON.stringify(list, null, 2);
};
return { type, produce };
}

View File

@@ -3,6 +3,7 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
return (
'proxies:\n' +
proxies
@@ -22,6 +23,23 @@ export default function Stash_Producer() {
'hysteria',
'hysteria2',
].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 === 'vless' && proxy['reality-opts'])

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

@@ -416,6 +416,9 @@ function snell(proxy) {
'obfs-opts.path',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');

View File

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

View File

@@ -1,10 +1,14 @@
/* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64';
import { isIPv6 } from '@/utils';
export default function URI_Producer() {
const type = 'SINGLE';
const produce = (proxy) => {
let result = '';
if (proxy.server && isIPv6(proxy.server)) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) {
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
@@ -197,6 +201,21 @@ export default function URI_Producer() {
let trojanTransport = '';
if (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 =
proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost =
@@ -222,6 +241,44 @@ export default function URI_Producer() {
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
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;
};

View File

@@ -20,7 +20,15 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
let { url, ua, content, mergeSources } = req.query;
let {
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
} = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
@@ -37,6 +45,18 @@ async function downloadSubscription(req, res) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
@@ -50,6 +70,11 @@ async function downloadSubscription(req, res) {
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
if (sub.source !== 'local' || url) {
@@ -116,12 +141,34 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
if (collection) {
try {
const output = await produceArtifact({
type: 'collection',
name,
platform,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
// forward flow header from the first subscription in this collection

View File

@@ -2,7 +2,12 @@ import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import {
RequestInvalidError,
ResourceNotFoundError,
InternalServerError,
} from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
@@ -12,7 +17,10 @@ export default function register($app) {
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/wholeFile/:name').get(getWholeFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
$app.route('/api/wholeFiles').get(getAllWholeFiles);
}
// file API
@@ -37,13 +45,85 @@ function createFile(req, res) {
success(res, file, 201);
}
function getFile(req, res) {
async function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = 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 (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
}
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
try {
const output = await produceArtifact({
type: 'file',
name,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
});
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);
} catch (err) {
$.notify(
`🌍 Sub-Store 下载文件失败`,
`❌ 无法下载文件:${name}`,
`🤔 原因:${err.message ?? err}`,
);
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download file: ${name}`,
`Reason: ${err.message ?? err}`,
),
);
}
} else {
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}`);
failed(
res,
new ResourceNotFoundError(
'RESOURCE_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) {
res.status(200).json(file.content);
success(res, file);
} else {
failed(
res,
@@ -102,6 +182,11 @@ function getAllFiles(req, res) {
);
}
function getAllWholeFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(res, allFiles);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);

View File

@@ -1,5 +1,7 @@
import express from '@/vendor/express';
import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
@@ -18,8 +20,8 @@ export default function serve() {
let port;
let host;
if ($.env.isNode) {
port = eval('process.env.SUB_STORE_BACKEND_API_PORT');
host = eval('process.env.SUB_STORE_BACKEND_API_HOST');
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
@@ -37,4 +39,120 @@ export default function serve() {
registerMiscRoutes($app);
$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

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

View File

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

View File

@@ -1,4 +1,4 @@
import { InternalServerError, NetworkError } from './errors';
import { InternalServerError } from './errors';
import { ProxyUtils } from '@/core/proxy-utils';
import { findByName } from '@/utils/database';
import { success, failed } from './response';
@@ -9,6 +9,85 @@ import $ from '@/core/app';
export default function register($app) {
$app.post('/api/preview/sub', compareSub);
$app.post('/api/preview/collection', compareCollection);
$app.post('/api/preview/file', previewFile);
}
async function previewFile(req, res) {
try {
const file = req.body;
let content;
if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!file.ignoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
}
}
// parse proxies
const files = (Array.isArray(content) ? content : [content]).flat();
let filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
// apply processors
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
file.process,
)
: { $content: filesContent, $files: files };
// produce
success(res, {
original: filesContent,
processed: processed?.$content ?? '',
});
} 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) {
@@ -22,24 +101,31 @@ async function compareSub(req, res) {
) {
content = sub.content;
} else {
try {
content = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
const errors = {};
content = 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(
', ',
)} 发生错误, 请查看日志`,
);
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content);
@@ -87,7 +173,7 @@ async function compareCollection(req, res) {
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
const errors = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
@@ -101,13 +187,34 @@ async function compareCollection(req, res) {
) {
raw = sub.content;
} else {
const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
.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') {
@@ -133,18 +240,28 @@ async function compareCollection(req, res) {
);
results[name] = currentProxies;
} catch (err) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
`Reason: ${err}`,
),
errors[name] = err;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${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
const original = Array.prototype.concat.apply(
[],

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 { success } from '@/restful/response';
@@ -6,6 +11,7 @@ export default function register($app) {
$app.post('/api/sort/subs', sortSubs);
$app.post('/api/sort/collections', sortCollections);
$app.post('/api/sort/artifacts', sortArtifacts);
$app.post('/api/sort/files', sortFiles);
}
function sortSubs(req, res) {
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
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

@@ -4,6 +4,7 @@ import {
COLLECTIONS_KEY,
RULES_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import { failed, success } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -30,6 +31,10 @@ async function produceArtifact({
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
ignoreFailedRemoteFile,
produceType,
produceOpts = {},
}) {
platform = platform || 'JSON';
@@ -40,13 +45,35 @@ async function produceArtifact({
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((url) => download(url, ua)),
.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') {
@@ -58,13 +85,35 @@ async function produceArtifact({
) {
raw = sub.content;
} else {
const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
.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') {
@@ -107,7 +156,7 @@ async function produceArtifact({
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
@@ -131,13 +180,34 @@ async function produceArtifact({
) {
raw = sub.content;
} else {
const errors = {};
raw = await await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map((url) => download(url, sub.ua)),
.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') {
@@ -174,15 +244,21 @@ async function produceArtifact({
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err},该订阅已被跳过!进度--${
}时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
}),
);
if (Object.keys(errors).length > 0) {
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
@@ -227,7 +303,7 @@ async function produceArtifact({
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
@@ -255,6 +331,110 @@ async function produceArtifact({
]);
// produce output
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 raw;
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 || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
let filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
// apply processors
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
file.process,
)
: { $content: filesContent, $files: files };
return processed?.$content ?? '';
}
}
@@ -314,10 +494,12 @@ async function syncAllArtifacts(_, res) {
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
if (!artifact) {
$.error(`找不到远程配置 ${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -356,6 +538,7 @@ async function syncArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
failed(
res,
new InternalServerError(

View File

@@ -7,7 +7,7 @@ import $ from '@/core/app';
const tasks = new Map();
export default async function download(url, ua) {
export default async function download(url, ua, timeout) {
let $arguments = {};
const rawArgs = url.split('#');
if (rawArgs.length > 1) {
@@ -45,17 +45,19 @@ export default async function download(url, ua) {
}
const { isNode } = ENV();
const { defaultUserAgent } = $.read(SETTINGS_KEY);
ua = ua || defaultUserAgent || 'clash.meta';
const id = hex_md5(ua + url);
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
const http = HTTP({
headers: {
'User-Agent': ua,
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
const result = new Promise((resolve, reject) => {
@@ -64,7 +66,9 @@ export default async function download(url, ua) {
if (!$arguments?.noCache && cached) {
resolve(cached);
} else {
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
);
http.get(url)
.then((resp) => {
const body = resp.body;

View File

@@ -1,6 +1,14 @@
import { SETTINGS_KEY } from '@/constants';
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 { headers } = await http.get({
url: url
@@ -8,8 +16,9 @@ export async function getFlowHeaders(url) {
.map((i) => i.trim())
.filter((i) => i.length)[0],
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) =>
/SUBSCRIPTION-USERINFO/i.test(k),

View File

@@ -13,6 +13,12 @@ function isIPv6(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) {
return typeof str === 'string' && str.trim().length > 0;
}
@@ -29,4 +35,12 @@ function getIfPresent(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) {
return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) {
@@ -18,7 +20,7 @@ export function getPlatformFromHeaders(headers) {
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'ShadowRocket';
return 'Shadowrocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else if (
@@ -30,6 +32,8 @@ export function getPlatformFromHeaders(headers) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) {
return 'sing-box';
} else {
return 'JSON';
}

View File

@@ -2,8 +2,6 @@
import { ENV } from './open-api';
export default function express({ substore: $, port, host }) {
port = port || 3000;
host = host || '::';
const { isNode } = ENV();
const DEFAULT_HEADERS = {
'Content-Type': 'text/plain;charset=UTF-8',
@@ -32,7 +30,7 @@ export default function express({ substore: $, port, host }) {
app.start = () => {
const listener = app.listen(port, host, () => {
const { address, port } = listener.address();
$.info(`Express started on ${address}:${port}`);
$.info(`[BACKEND] ${address}:${port}`);
});
};
return app;

View File

@@ -191,6 +191,32 @@ export class OpenAPI {
(openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
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}`);
});
}
}
}

View File

@@ -22,6 +22,8 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
### 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-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
### 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) 即可。

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="
}
}
}