Compare commits

...

41 Commits

Author SHA1 Message Date
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
32 changed files with 2333 additions and 1351 deletions

View File

@@ -45,18 +45,14 @@ jobs:
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 "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT 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, Hysteria2) - [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

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.111", "version": "2.14.142",
"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';
@@ -139,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!`);
@@ -157,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__')
@@ -182,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);
} }
} }
@@ -214,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;
@@ -270,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

@@ -332,8 +332,8 @@ 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}`; name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
@@ -409,13 +409,13 @@ 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;
@@ -742,6 +742,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,
@@ -768,7 +769,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,
}, },
], ],

View File

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

View File

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

View File

@@ -605,6 +605,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;
},
}; };
} }
@@ -635,7 +651,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]);
} }

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 (
![ ![
@@ -20,6 +21,23 @@ export default function Clash_Producer() {
'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,15 +2,10 @@ 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' +
proxies
.filter((proxy) => { .filter((proxy) => {
if ( if (proxy.type === 'snell' && String(proxy.version) === '4') {
proxy.type === 'snell' &&
String(proxy.version) === '4'
) {
return false; return false;
} }
return true; return true;
@@ -135,10 +130,15 @@ export default function ClashMeta_Producer() {
) { ) {
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

@@ -99,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(
@@ -285,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);

View File

@@ -81,6 +81,25 @@ export default function ShadowRocket_Producer() {
) { ) {
proxy['fast-open'] = proxy.tfo; 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') { } else if (proxy.type === 'wireguard') {
proxy.keepalive = proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive']; proxy.keepalive ?? proxy['persistent-keepalive'];

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
@@ -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'])

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

@@ -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,7 +20,7 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`); $.info(`正在下载订阅:${name}`);
let { url, ua, content, mergeSources } = req.query; let { url, ua, content, mergeSources, ignoreFailedRemoteSub } = req.query;
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`); $.info(`指定远程订阅 URL: ${url}`);
@@ -37,6 +37,10 @@ async function downloadSubscription(req, res) {
mergeSources = decodeURIComponent(mergeSources); mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${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);
@@ -50,6 +54,7 @@ async function downloadSubscription(req, res) {
ua, ua,
content, content,
mergeSources, mergeSources,
ignoreFailedRemoteSub,
}); });
if (sub.source !== 'local' || url) { if (sub.source !== 'local' || url) {
@@ -116,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

@@ -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';
@@ -18,8 +20,8 @@ export default function serve() {
let port; let port;
let host; let host;
if ($.env.isNode) { if ($.env.isNode) {
port = eval('process.env.SUB_STORE_BACKEND_API_PORT'); port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
host = eval('process.env.SUB_STORE_BACKEND_API_HOST'); host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
} }
const $app = express({ substore: $, port, host }); const $app = express({ substore: $, port, host });
// register routes // register routes
@@ -37,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,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';
@@ -22,24 +22,31 @@ async function compareSub(req, res) {
) { ) {
content = sub.content; content = sub.content;
} else { } else {
try { const errors = {};
content = await Promise.all( content = await Promise.all(
sub.url sub.url
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length) .filter((i) => i.length)
.map((url) => download(url, sub.ua)), .map(async (url) => {
); try {
return await download(url, sub.ua);
} catch (err) { } catch (err) {
failed( errors[url] = err;
res, $.error(
new NetworkError( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
'FAILED_TO_DOWNLOAD_RESOURCE', );
'无法下载远程资源', return '';
`Reason: ${err}`, }
), }),
);
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
); );
return;
} }
if (sub.mergeSources === 'localFirst') { if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content); content.unshift(sub.content);
@@ -87,7 +94,7 @@ 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);
@@ -101,13 +108,34 @@ async function compareCollection(req, res) {
) { ) {
raw = sub.content; raw = sub.content;
} else { } else {
const errors = {};
raw = await Promise.all( raw = await Promise.all(
sub.url sub.url
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length) .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') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') { } else if (sub.mergeSources === 'remoteFirst') {
@@ -133,18 +161,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

@@ -30,6 +30,7 @@ async function produceArtifact({
ua, ua,
content, content,
mergeSources, mergeSources,
ignoreFailedRemoteSub,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
@@ -40,13 +41,35 @@ async function produceArtifact({
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content; raw = content;
} else if (url) { } else if (url) {
const errors = {};
raw = await Promise.all( raw = await Promise.all(
url url
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length) .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') { if (mergeSources === 'localFirst') {
raw.unshift(content); raw.unshift(content);
} else if (mergeSources === 'remoteFirst') { } else if (mergeSources === 'remoteFirst') {
@@ -58,13 +81,35 @@ async function produceArtifact({
) { ) {
raw = sub.content; raw = sub.content;
} else { } else {
const errors = {};
raw = await Promise.all( raw = await Promise.all(
sub.url sub.url
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length) .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') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') { } else if (sub.mergeSources === 'remoteFirst') {
@@ -131,13 +176,34 @@ async function produceArtifact({
) { ) {
raw = sub.content; raw = sub.content;
} else { } else {
const errors = {};
raw = await await Promise.all( raw = await await Promise.all(
sub.url sub.url
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length) .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') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') { } else if (sub.mergeSources === 'remoteFirst') {
@@ -174,15 +240,21 @@ async function produceArtifact({
$.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(
', ', ', ',
@@ -314,10 +386,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(
@@ -356,6 +430,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

@@ -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,17 +45,19 @@ export default async function download(url, ua) {
} }
const { isNode } = ENV(); const { isNode } = ENV();
const { defaultUserAgent } = $.read(SETTINGS_KEY); const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
ua = ua || defaultUserAgent || 'clash.meta'; const userAgent = ua || defaultUserAgent || 'clash.meta';
const id = hex_md5(ua + url); 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) => {
@@ -64,7 +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: ${ua}\nURL: ${url}`); $.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,6 +1,14 @@
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: url
@@ -8,8 +16,9 @@ export async function getFlowHeaders(url) {
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length)[0], .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),

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

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

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