mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee2fcc7ee3 | ||
|
|
95615d1877 | ||
|
|
962bcda9dd | ||
|
|
9bb4739d56 | ||
|
|
de1d40f41a | ||
|
|
c0ab301160 | ||
|
|
a22df97a51 | ||
|
|
45772ade4d | ||
|
|
e8dab545f5 | ||
|
|
c2bd80207a | ||
|
|
bc5ae9a2ef | ||
|
|
36db057e32 | ||
|
|
5ac73b863a | ||
|
|
23042c33d6 | ||
|
|
4ca5f5e355 | ||
|
|
f10e5913fb | ||
|
|
8b75c11587 | ||
|
|
c287dcad3b | ||
|
|
ce6cd794c8 | ||
|
|
e05475aa5e | ||
|
|
c35e9d37ae | ||
|
|
8f2dbfe3df | ||
|
|
a0a998dfdd | ||
|
|
12491ac7c0 | ||
|
|
78e3024cec | ||
|
|
5e21a20e37 | ||
|
|
76b5dc5809 | ||
|
|
a1776644a0 | ||
|
|
7aaa03d4ca | ||
|
|
d0cba285ab | ||
|
|
d636e1b94c | ||
|
|
69726cd5c4 | ||
|
|
8918479b9e | ||
|
|
17504ab5aa | ||
|
|
0d8fa91cd5 | ||
|
|
e7dfa1ce38 | ||
|
|
fe937d6ebf | ||
|
|
b7b734f529 | ||
|
|
f5ef6010bc | ||
|
|
0e82a7669d | ||
|
|
6d11ea0fcc |
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -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
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -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>
|
||||||
|
|
||||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
2496
backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' ||
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal 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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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(
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
4
backend/src/vendor/express.js
vendored
4
backend/src/vendor/express.js
vendored
@@ -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;
|
||||||
|
|||||||
26
backend/src/vendor/open-api.js
vendored
26
backend/src/vendor/open-api.js
vendored
@@ -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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
40
package-lock.json
generated
@@ -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="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user