mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca508ba8a | ||
|
|
21b531a44d | ||
|
|
4e5b46a43d | ||
|
|
bf81ca4acf | ||
|
|
e7c0b23222 | ||
|
|
40fb0fd7f3 | ||
|
|
b061fca356 | ||
|
|
d3c6c99b0a | ||
|
|
3fbc280e28 | ||
|
|
9e3e4c6e46 | ||
|
|
bc0dd4b175 | ||
|
|
7603fac036 | ||
|
|
9acc161684 | ||
|
|
024582a99d | ||
|
|
1d31a80b9f | ||
|
|
b2d0276836 | ||
|
|
3211fbf357 | ||
|
|
33a17c2d66 | ||
|
|
2c89a0ddbd | ||
|
|
939022e5a3 | ||
|
|
59bca5670d | ||
|
|
07b38cf971 | ||
|
|
28186f596f | ||
|
|
ea31b1d0ec | ||
|
|
77191f9caa | ||
|
|
07a270963e | ||
|
|
f1e1d50a2c | ||
|
|
a65cd1f1c9 | ||
|
|
5b0e2e1ef2 | ||
|
|
b29266ac57 | ||
|
|
336ddd6706 | ||
|
|
25ec219659 | ||
|
|
41d24b131a | ||
|
|
ba78982f41 | ||
|
|
26193301b3 | ||
|
|
0141e48200 | ||
|
|
5ae6687b1f | ||
|
|
ad6d1ab441 | ||
|
|
f5aea14904 | ||
|
|
4f2c95f6ab | ||
|
|
be1e2c9979 | ||
|
|
347b19e30d | ||
|
|
f94a12bf6e | ||
|
|
bd510a9aa9 | ||
|
|
f02af9d643 | ||
|
|
af8e965866 | ||
|
|
4bebcff1d3 | ||
|
|
7b8f6f7e52 | ||
|
|
49c7107d20 | ||
|
|
8bfa4dbbf2 | ||
|
|
5e14d05c30 | ||
|
|
8c5dca71fb |
25
.github/workflows/main.yml
vendored
25
.github/workflows/main.yml
vendored
@@ -1,15 +1,15 @@
|
|||||||
name: build
|
name: build
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'backend/package.json'
|
- "backend/package.json"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'backend/package.json'
|
- "backend/package.json"
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: 'master'
|
ref: "master"
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -34,17 +34,28 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
- name: Bundle
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
pnpm i -D estrella
|
||||||
|
pnpm run bundle
|
||||||
- id: tag
|
- id: tag
|
||||||
name: Generate release tag
|
name: Generate release tag
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||||
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
|
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
|
||||||
|
- name: Prepare release
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
pnpm i -D conventional-changelog-cli
|
||||||
|
pnpm run changelog
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
body_path: ./backend/CHANGELOG.md
|
||||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||||
files: |
|
files: |
|
||||||
./backend/sub-store.min.js
|
./backend/sub-store.min.js
|
||||||
@@ -52,3 +63,9 @@ jobs:
|
|||||||
./backend/dist/sub-store-1.min.js
|
./backend/dist/sub-store-1.min.js
|
||||||
./backend/dist/sub-store-parser.loon.min.js
|
./backend/dist/sub-store-parser.loon.min.js
|
||||||
./backend/dist/cron-sync-artifacts.min.js
|
./backend/dist/cron-sync-artifacts.min.js
|
||||||
|
./backend/dist/sub-store.bundle.js
|
||||||
|
- name: Sync to GitLab
|
||||||
|
env:
|
||||||
|
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -127,4 +127,11 @@ out
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Dist files
|
||||||
|
backend/dist/*
|
||||||
|
!backend/dist/.gitkeep
|
||||||
|
backend/sub-store.min.js
|
||||||
|
|
||||||
|
CHANGELOG.md
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -10,8 +10,8 @@
|
|||||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml)     
|
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/PengYM)
|
[](https://www.buymeacoffee.com/PengYM)
|
||||||
|
|
||||||
Core functionalities:
|
Core functionalities:
|
||||||
@@ -19,6 +19,8 @@ Core functionalities:
|
|||||||
1. Conversion among various formats.
|
1. Conversion among various formats.
|
||||||
2. Subscription formatting.
|
2. Subscription formatting.
|
||||||
3. Collect multiple subscriptions in one URL.
|
3. Collect multiple subscriptions in one URL.
|
||||||
|
|
||||||
|
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
|
||||||
|
|
||||||
## 1. Subscription Conversion
|
## 1. Subscription Conversion
|
||||||
|
|
||||||
@@ -29,17 +31,25 @@ Core functionalities:
|
|||||||
- [x] SSD URI
|
- [x] SSD URI
|
||||||
- [x] V2RayN URI
|
- [x] V2RayN URI
|
||||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
|
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
|
||||||
- [x] Surge (SS, VMess, Trojan, HTTP)
|
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
||||||
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
|
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
||||||
|
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
||||||
|
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
||||||
|
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell)
|
||||||
|
|
||||||
### Supported Target Platforms
|
### Supported Target Platforms
|
||||||
|
|
||||||
- [x] QX
|
- [x] QX
|
||||||
- [x] Loon
|
- [x] Loon
|
||||||
- [x] Surge
|
- [x] Surge
|
||||||
- [x] Stash & Clash
|
- [x] Stash
|
||||||
|
- [x] Clash.Meta
|
||||||
|
- [x] Clash
|
||||||
- [x] ShadowRocket
|
- [x] ShadowRocket
|
||||||
|
- [x] V2Ray
|
||||||
|
- [x] V2Ray URI
|
||||||
|
- [x] Plain JSON
|
||||||
|
|
||||||
## 2. Subscription Formatting
|
## 2. Subscription Formatting
|
||||||
|
|
||||||
@@ -61,6 +71,7 @@ Core functionalities:
|
|||||||
- [x] **Regex rename operator**: replace by regex in proxy names.
|
- [x] **Regex rename operator**: replace by regex in proxy names.
|
||||||
- [x] **Regex delete operator**: delete by regex in proxy names.
|
- [x] **Regex delete operator**: delete by regex in proxy names.
|
||||||
- [x] **Script operator**: modify proxy by script.
|
- [x] **Script operator**: modify proxy by script.
|
||||||
|
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
@@ -88,6 +99,11 @@ This project is under the GPL V3 LICENSE.
|
|||||||
|
|
||||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* @updated: <%= updated %>
|
* @updated: <%= updated %>
|
||||||
* @version: <%= pkg.version %>
|
* @version: <%= pkg.version %>
|
||||||
* @author: Peng-YM
|
* @author: Peng-YM
|
||||||
* @github: https://github.com/Peng-YM/Sub-Store
|
* @github: https://github.com/sub-store-org/Sub-Store
|
||||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
23
backend/bundle.js
Normal file
23
backend/bundle.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
content = content.replace(
|
||||||
|
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
|
||||||
|
'$2',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { build } = require('estrella');
|
||||||
|
build({
|
||||||
|
entry: 'dist/sub-store.no-bundle.js',
|
||||||
|
outfile: 'dist/sub-store.bundle.js',
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
});
|
||||||
0
backend/dist/.gitkeep
vendored
Normal file
0
backend/dist/.gitkeep
vendored
Normal file
16
backend/dist/cron-sync-artifacts.min.js
vendored
16
backend/dist/cron-sync-artifacts.min.js
vendored
File diff suppressed because one or more lines are too long
16
backend/dist/sub-store-0.min.js
vendored
16
backend/dist/sub-store-0.min.js
vendored
File diff suppressed because one or more lines are too long
16
backend/dist/sub-store-1.min.js
vendored
16
backend/dist/sub-store-1.min.js
vendored
File diff suppressed because one or more lines are too long
16
backend/dist/sub-store-parser.loon.min.js
vendored
16
backend/dist/sub-store-parser.loon.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.14.29",
|
"version": "2.14.71",
|
||||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
||||||
"serve": "node sub-store.min.js",
|
"serve": "node sub-store.min.js",
|
||||||
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
||||||
"build": "gulp"
|
"build": "gulp",
|
||||||
|
"bundle": "node bundle.js",
|
||||||
|
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
|
||||||
},
|
},
|
||||||
"author": "Peng-YM",
|
"author": "Peng-YM",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
|
|||||||
export const SETTINGS_KEY = 'settings';
|
export const SETTINGS_KEY = 'settings';
|
||||||
export const SUBS_KEY = 'subs';
|
export const SUBS_KEY = 'subs';
|
||||||
export const COLLECTIONS_KEY = 'collections';
|
export const COLLECTIONS_KEY = 'collections';
|
||||||
|
export const FILES_KEY = 'files';
|
||||||
|
export const MODULES_KEY = 'modules';
|
||||||
export const ARTIFACTS_KEY = 'artifacts';
|
export const ARTIFACTS_KEY = 'artifacts';
|
||||||
export const RULES_KEY = 'rules';
|
export const RULES_KEY = 'rules';
|
||||||
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ function parse(raw) {
|
|||||||
return proxies;
|
return proxies;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(proxies, operators = [], targetPlatform) {
|
async function process(proxies, operators = [], targetPlatform, source) {
|
||||||
for (const item of operators) {
|
for (const item of operators) {
|
||||||
// process script
|
// process script
|
||||||
let script;
|
let script;
|
||||||
const $arguments = {};
|
let $arguments = {};
|
||||||
if (item.type.indexOf('Script') !== -1) {
|
if (item.type.indexOf('Script') !== -1) {
|
||||||
const { mode, content } = item.args;
|
const { mode, content } = item.args;
|
||||||
if (mode === 'link') {
|
if (mode === 'link') {
|
||||||
@@ -75,10 +75,19 @@ async function process(proxies, operators = [], targetPlatform) {
|
|||||||
// extract link arguments
|
// extract link arguments
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
if (rawArgs.length > 1) {
|
if (rawArgs.length > 1) {
|
||||||
for (const pair of rawArgs[1].split('&')) {
|
try {
|
||||||
const key = pair.split('=')[0];
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
const value = pair.split('=')[1] || true;
|
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||||
$arguments[key] = value;
|
} catch (e) {
|
||||||
|
for (const pair of rawArgs[1].split('&')) {
|
||||||
|
const key = pair.split('=')[0];
|
||||||
|
const value = pair.split('=')[1];
|
||||||
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||||
|
$arguments[key] =
|
||||||
|
value == null || value === ''
|
||||||
|
? true
|
||||||
|
: decodeURIComponent(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +99,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
|||||||
$.error(
|
$.error(
|
||||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||||
);
|
);
|
||||||
// skip the script if download failed.
|
throw new Error(`无法下载脚本: ${url}`);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
script = content;
|
script = content;
|
||||||
@@ -114,6 +122,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
|||||||
script,
|
script,
|
||||||
targetPlatform,
|
targetPlatform,
|
||||||
$arguments,
|
$arguments,
|
||||||
|
source,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||||
@@ -137,10 +146,21 @@ function produce(proxies, targetPlatform) {
|
|||||||
|
|
||||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||||
|
let localPort = 10000;
|
||||||
return proxies
|
return proxies
|
||||||
.map((proxy) => {
|
.map((proxy) => {
|
||||||
try {
|
try {
|
||||||
return producer.produce(proxy);
|
let line = producer.produce(proxy);
|
||||||
|
if (
|
||||||
|
line.length > 0 &&
|
||||||
|
line.includes('__SubStoreLocalPort__')
|
||||||
|
) {
|
||||||
|
line = line.replace(
|
||||||
|
/__SubStoreLocalPort__/g,
|
||||||
|
localPort++,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot produce proxy: ${JSON.stringify(
|
`Cannot produce proxy: ${JSON.stringify(
|
||||||
@@ -189,7 +209,7 @@ function lastParse(proxy) {
|
|||||||
delete proxy.network;
|
delete proxy.network;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
}
|
}
|
||||||
if (proxy.tls && !proxy.sni) {
|
if (proxy.tls && !proxy.sni) {
|
||||||
@@ -206,6 +226,32 @@ function lastParse(proxy) {
|
|||||||
proxy.sni = proxy.server;
|
proxy.sni = proxy.server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
|
||||||
|
if (
|
||||||
|
!proxy.tls &&
|
||||||
|
['ws', 'http'].includes(proxy.network) &&
|
||||||
|
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
|
||||||
|
!isIP(proxy.server)
|
||||||
|
) {
|
||||||
|
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
|
||||||
|
proxy[`${proxy.network}-opts`].headers =
|
||||||
|
proxy[`${proxy.network}-opts`].headers || {};
|
||||||
|
proxy[`${proxy.network}-opts`].headers.Host =
|
||||||
|
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
|
||||||
|
? [proxy.server]
|
||||||
|
: proxy.server;
|
||||||
|
}
|
||||||
|
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
|
||||||
|
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
|
||||||
|
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||||
|
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
|
if (transportHost && !Array.isArray(transportHost)) {
|
||||||
|
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
|
||||||
|
}
|
||||||
|
if (transportPath && !Array.isArray(transportPath)) {
|
||||||
|
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ function URI_VMess() {
|
|||||||
// V2rayN URI format
|
// V2rayN URI format
|
||||||
params = JSON.parse(content);
|
params = JSON.parse(content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error(e);
|
|
||||||
// Shadowrocket URI format
|
// Shadowrocket URI format
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
|
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
|
||||||
@@ -241,7 +240,7 @@ function URI_VMess() {
|
|||||||
params.add = server;
|
params.add = server;
|
||||||
}
|
}
|
||||||
const proxy = {
|
const proxy = {
|
||||||
name: params.ps ?? params.remark,
|
name: params.ps ?? params.remarks,
|
||||||
type: 'vmess',
|
type: 'vmess',
|
||||||
server: params.add,
|
server: params.add,
|
||||||
port: parseInt(getIfPresent(params.port), 10),
|
port: parseInt(getIfPresent(params.port), 10),
|
||||||
@@ -268,9 +267,19 @@ function URI_VMess() {
|
|||||||
params.obfs === 'http'
|
params.obfs === 'http'
|
||||||
) {
|
) {
|
||||||
proxy.network = 'http';
|
proxy.network = 'http';
|
||||||
|
} else if (['grpc'].includes(params.net)) {
|
||||||
|
proxy.network = 'grpc';
|
||||||
}
|
}
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
let transportHost = params.host ?? params.obfsParam;
|
let transportHost = params.host ?? params.obfsParam;
|
||||||
|
try {
|
||||||
|
const parsedObfs = JSON.parse(transportHost);
|
||||||
|
const parsedHost = parsedObfs?.Host;
|
||||||
|
if (parsedHost) {
|
||||||
|
transportHost = parsedHost;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
let transportPath = params.path;
|
let transportPath = params.path;
|
||||||
|
|
||||||
if (proxy.network === 'http') {
|
if (proxy.network === 'http') {
|
||||||
@@ -286,10 +295,17 @@ function URI_VMess() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (transportPath || transportHost) {
|
if (transportPath || transportHost) {
|
||||||
proxy[`${proxy.network}-opts`] = {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
path: getIfNotBlank(transportPath),
|
proxy[`${proxy.network}-opts`] = {
|
||||||
headers: { Host: getIfNotBlank(transportHost) },
|
'grpc-service-name': getIfNotBlank(transportPath),
|
||||||
};
|
'_grpc-type': getIfNotBlank(params.type),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
proxy[`${proxy.network}-opts`] = {
|
||||||
|
path: getIfNotBlank(transportPath),
|
||||||
|
headers: { Host: getIfNotBlank(transportHost) },
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete proxy.network;
|
delete proxy.network;
|
||||||
}
|
}
|
||||||
@@ -306,6 +322,136 @@ function URI_VMess() {
|
|||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function URI_VLESS() {
|
||||||
|
const name = 'URI VLESS Parser';
|
||||||
|
const test = (line) => {
|
||||||
|
return /^vless:\/\//.test(line);
|
||||||
|
};
|
||||||
|
const parse = (line) => {
|
||||||
|
line = line.split('vless://')[1];
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let [__, uuid, server, port, addons, name] =
|
||||||
|
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
||||||
|
port = parseInt(`${port}`, 10);
|
||||||
|
uuid = decodeURIComponent(uuid);
|
||||||
|
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
|
||||||
|
const proxy = {
|
||||||
|
type: 'vless',
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
port,
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
const params = {};
|
||||||
|
for (const addon of addons.split('&')) {
|
||||||
|
const [key, valueRaw] = addon.split('=');
|
||||||
|
let value = valueRaw;
|
||||||
|
value = decodeURIComponent(valueRaw);
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.tls = params.security && params.security !== 'none';
|
||||||
|
proxy.sni = params.sni;
|
||||||
|
proxy.flow = params.flow;
|
||||||
|
proxy['client-fingerprint'] = params.fp;
|
||||||
|
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
|
||||||
|
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
|
||||||
|
|
||||||
|
if (['reality'].includes(params.security)) {
|
||||||
|
const opts = {};
|
||||||
|
if (params.pbk) {
|
||||||
|
opts['public-key'] = params.pbk;
|
||||||
|
}
|
||||||
|
if (params.sid) {
|
||||||
|
opts['short-id'] = params.sid;
|
||||||
|
}
|
||||||
|
if (Object.keys(opts).length > 0) {
|
||||||
|
proxy[`${params.security}-opts`] = opts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.network = params.type;
|
||||||
|
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
||||||
|
const opts = {};
|
||||||
|
if (params.path) {
|
||||||
|
opts.path = params.path;
|
||||||
|
}
|
||||||
|
if (params.host) {
|
||||||
|
opts.headers = { Host: params.host };
|
||||||
|
}
|
||||||
|
if (params.serviceName) {
|
||||||
|
opts[`${proxy.network}-service-name`] = params.serviceName;
|
||||||
|
}
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
if (['grpc'].includes(proxy.network)) {
|
||||||
|
opts['_grpc-type'] = params.mode || 'gun';
|
||||||
|
}
|
||||||
|
if (Object.keys(opts).length > 0) {
|
||||||
|
proxy[`${proxy.network}-opts`] = opts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy.tls && !proxy.sni) {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
proxy.sni = proxy['ws-opts']?.headers?.Host;
|
||||||
|
} else if (proxy.network === 'http') {
|
||||||
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
|
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
function URI_Hysteria2() {
|
||||||
|
const name = 'URI Hysteria2 Parser';
|
||||||
|
const test = (line) => {
|
||||||
|
return /^hysteria2:\/\//.test(line);
|
||||||
|
};
|
||||||
|
const parse = (line) => {
|
||||||
|
line = line.split('hysteria2://')[1];
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let [__, password, server, ___, port, addons, name] =
|
||||||
|
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
||||||
|
port = parseInt(`${port}`, 10);
|
||||||
|
if (isNaN(port)) {
|
||||||
|
port = 443;
|
||||||
|
}
|
||||||
|
password = decodeURIComponent(password);
|
||||||
|
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
|
||||||
|
|
||||||
|
const proxy = {
|
||||||
|
type: 'hysteria2',
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
port,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
for (const addon of addons.split('&')) {
|
||||||
|
const [key, valueRaw] = addon.split('=');
|
||||||
|
let value = valueRaw;
|
||||||
|
value = decodeURIComponent(valueRaw);
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.sni = params.sni;
|
||||||
|
if (!proxy.sni && params.peer) {
|
||||||
|
proxy.sni = params.peer;
|
||||||
|
}
|
||||||
|
proxy.obfs = params.obfs;
|
||||||
|
proxy['obfs-password'] = params['obfs-password'];
|
||||||
|
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
|
||||||
|
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
|
||||||
|
proxy.fingerprint = params.pinSHA256;
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
|
||||||
// Trojan URI format
|
// Trojan URI format
|
||||||
function URI_Trojan() {
|
function URI_Trojan() {
|
||||||
const name = 'URI Trojan Parser';
|
const name = 'URI Trojan Parser';
|
||||||
@@ -345,6 +491,7 @@ function Clash_All() {
|
|||||||
'tuic',
|
'tuic',
|
||||||
'vless',
|
'vless',
|
||||||
'hysteria',
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
'wireguard',
|
'wireguard',
|
||||||
].includes(proxy.type)
|
].includes(proxy.type)
|
||||||
) {
|
) {
|
||||||
@@ -600,7 +747,6 @@ function Loon_WireGuard() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
// WARP+=wireguard,interface-ip=172.16.0.2,interface-ipv6=fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5,private-key=4PYTxwziqAnnrlRDFAkwfkCxjG6FBKdU03vV3t3c5kk=,mtu=1280,dns=162.159.36.1,dnsv6=2606:4700:4700::1111,keepalive=45,peers=[{public-key="Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=",allowed-ips="0.0.0.0/0",endpoint=162.159.192.1:2480,reserved=[209,98,59],preshared-key="31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM="}}]
|
|
||||||
|
|
||||||
proxy;
|
proxy;
|
||||||
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
|
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
|
||||||
@@ -662,7 +808,7 @@ function Surge_Socks5() {
|
|||||||
function Surge_Snell() {
|
function Surge_Snell() {
|
||||||
const name = 'Surge Snell Parser';
|
const name = 'Surge Snell Parser';
|
||||||
const test = (line) => {
|
const test = (line) => {
|
||||||
return /^.*=\s*snell?/.test(line.split(',')[0]);
|
return /^.*=\s*snell/.test(line.split(',')[0]);
|
||||||
};
|
};
|
||||||
const parse = (line) => getSurgeParser().parse(line);
|
const parse = (line) => getSurgeParser().parse(line);
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
@@ -671,7 +817,24 @@ function Surge_Snell() {
|
|||||||
function Surge_Tuic() {
|
function Surge_Tuic() {
|
||||||
const name = 'Surge Tuic Parser';
|
const name = 'Surge Tuic Parser';
|
||||||
const test = (line) => {
|
const test = (line) => {
|
||||||
return /^.*=\s*tuic(-v5)??/.test(line.split(',')[0]);
|
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
|
||||||
|
};
|
||||||
|
const parse = (line) => getSurgeParser().parse(line);
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
function Surge_WireGuard() {
|
||||||
|
const name = 'Surge WireGuard Parser';
|
||||||
|
const test = (line) => {
|
||||||
|
return /^.*=\s*wireguard/.test(line.split(',')[0]);
|
||||||
|
};
|
||||||
|
const parse = (line) => getSurgeParser().parse(line);
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
|
||||||
|
function Surge_Hysteria2() {
|
||||||
|
const name = 'Surge Hysteria2 Parser';
|
||||||
|
const test = (line) => {
|
||||||
|
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
|
||||||
};
|
};
|
||||||
const parse = (line) => getSurgeParser().parse(line);
|
const parse = (line) => getSurgeParser().parse(line);
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
@@ -681,6 +844,8 @@ export default [
|
|||||||
URI_SS(),
|
URI_SS(),
|
||||||
URI_SSR(),
|
URI_SSR(),
|
||||||
URI_VMess(),
|
URI_VMess(),
|
||||||
|
URI_VLESS(),
|
||||||
|
URI_Hysteria2(),
|
||||||
URI_Trojan(),
|
URI_Trojan(),
|
||||||
Clash_All(),
|
Clash_All(),
|
||||||
Surge_SS(),
|
Surge_SS(),
|
||||||
@@ -689,6 +854,8 @@ export default [
|
|||||||
Surge_Http(),
|
Surge_Http(),
|
||||||
Surge_Snell(),
|
Surge_Snell(),
|
||||||
Surge_Tuic(),
|
Surge_Tuic(),
|
||||||
|
Surge_WireGuard(),
|
||||||
|
Surge_Hysteria2(),
|
||||||
Surge_Socks5(),
|
Surge_Socks5(),
|
||||||
Loon_SS(),
|
Loon_SS(),
|
||||||
Loon_SSR(),
|
Loon_SSR(),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const grammars = String.raw`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls
|
|||||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
}
|
}
|
||||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/others)* {
|
||||||
proxy.type = "snell";
|
proxy.type = "snell";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -76,13 +76,19 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
|||||||
$set(proxy, "obfs-opts.path", obfs.path);
|
$set(proxy, "obfs-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
}
|
}
|
||||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
proxy.version = 5;
|
proxy.version = 5;
|
||||||
}
|
}
|
||||||
|
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
|
||||||
|
proxy.type = "wireguard-surge";
|
||||||
|
}
|
||||||
|
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/download_bandwidth/ecn/others)* {
|
||||||
|
proxy.type = "hysteria2";
|
||||||
|
}
|
||||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
}
|
}
|
||||||
@@ -185,8 +191,15 @@ uri = $[^,]+
|
|||||||
|
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
|
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||||
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||||
|
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||||
|
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||||
|
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||||
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
|
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls
|
|||||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
}
|
}
|
||||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/others)* {
|
||||||
proxy.type = "snell";
|
proxy.type = "snell";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -74,13 +74,19 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
|||||||
$set(proxy, "obfs-opts.path", obfs.path);
|
$set(proxy, "obfs-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
}
|
}
|
||||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
proxy.version = 5;
|
proxy.version = 5;
|
||||||
}
|
}
|
||||||
|
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
|
||||||
|
proxy.type = "wireguard-surge";
|
||||||
|
}
|
||||||
|
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/download_bandwidth/ecn/others)* {
|
||||||
|
proxy.type = "hysteria2";
|
||||||
|
}
|
||||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
}
|
}
|
||||||
@@ -183,8 +189,15 @@ uri = $[^,]+
|
|||||||
|
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
|
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||||
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||||
|
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||||
|
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||||
|
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||||
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
|
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lodash from 'lodash';
|
|||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
|
import env from '@/utils/env';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
|
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
|
||||||
@@ -294,7 +295,7 @@ function RegexDeleteOperator(regex) {
|
|||||||
1. This function name should be `operator`!
|
1. This function name should be `operator`!
|
||||||
2. Always declare variables before using them!
|
2. Always declare variables before using them!
|
||||||
*/
|
*/
|
||||||
function ScriptOperator(script, targetPlatform, $arguments) {
|
function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||||
return {
|
return {
|
||||||
name: 'Script Operator',
|
name: 'Script Operator',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
@@ -305,7 +306,7 @@ function ScriptOperator(script, targetPlatform, $arguments) {
|
|||||||
script,
|
script,
|
||||||
$arguments,
|
$arguments,
|
||||||
);
|
);
|
||||||
output = operator(proxies, targetPlatform);
|
output = operator(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
@@ -378,6 +379,46 @@ const DOMAIN_RESOLVERS = {
|
|||||||
resourceCache.set(id, result);
|
resourceCache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
Ali: async function (domain) {
|
||||||
|
const id = hex_md5(`ALI:${domain}`);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (cached) return cached;
|
||||||
|
const resp = await $.http.get({
|
||||||
|
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
|
||||||
|
domain,
|
||||||
|
)}&type=A&short=1`,
|
||||||
|
headers: {
|
||||||
|
accept: 'application/dns-json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const answers = JSON.parse(resp.body);
|
||||||
|
if (answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers[answers.length - 1];
|
||||||
|
resourceCache.set(id, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
Tencent: async function (domain) {
|
||||||
|
const id = hex_md5(`ALI:${domain}`);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (cached) return cached;
|
||||||
|
const resp = await $.http.get({
|
||||||
|
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
|
||||||
|
domain,
|
||||||
|
)}`,
|
||||||
|
headers: {
|
||||||
|
accept: 'application/dns-json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
|
||||||
|
if (answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers[answers.length - 1];
|
||||||
|
resourceCache.set(id, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function ResolveDomainOperator({ provider }) {
|
function ResolveDomainOperator({ provider }) {
|
||||||
@@ -522,7 +563,7 @@ function TypeFilter(types) {
|
|||||||
1. This function name should be `filter`!
|
1. This function name should be `filter`!
|
||||||
2. Always declare variables before using them!
|
2. Always declare variables before using them!
|
||||||
*/
|
*/
|
||||||
function ScriptFilter(script, targetPlatform, $arguments) {
|
function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||||
return {
|
return {
|
||||||
name: 'Script Filter',
|
name: 'Script Filter',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
@@ -533,7 +574,7 @@ function ScriptFilter(script, targetPlatform, $arguments) {
|
|||||||
script,
|
script,
|
||||||
$arguments,
|
$arguments,
|
||||||
);
|
);
|
||||||
output = filter(proxies, targetPlatform);
|
output = filter(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
@@ -566,7 +607,8 @@ async function ApplyFilter(filter, objs) {
|
|||||||
selected = await filter.func(objs);
|
selected = await filter.func(objs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// print log and skip this filter
|
// print log and skip this filter
|
||||||
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
||||||
|
throw new Error(`脚本过滤失败 ${err.message ?? err}`);
|
||||||
}
|
}
|
||||||
return objs.filter((_, i) => selected[i]);
|
return objs.filter((_, i) => selected[i]);
|
||||||
}
|
}
|
||||||
@@ -578,7 +620,8 @@ async function ApplyOperator(operator, objs) {
|
|||||||
if (output_) output = output_;
|
if (output_) output = output_;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// print log and skip this operator
|
// print log and skip this operator
|
||||||
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
|
$.error(`Cannot apply operator ${operator.name}! Reason: ${err}`);
|
||||||
|
throw new Error(`脚本操作失败 ${err.message ?? err}`);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
export default function Clash_Producer() {
|
export default function Clash_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies) => {
|
||||||
|
// VLESS XTLS is not supported by Clash
|
||||||
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
||||||
|
// github.com/Dreamacro/clash/pull/2891/files
|
||||||
// filter unsupported proxies
|
// filter unsupported proxies
|
||||||
proxies = proxies.filter((proxy) => {
|
proxies = proxies.filter((proxy) => {
|
||||||
if (
|
if (
|
||||||
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
|
|||||||
'ss',
|
'ss',
|
||||||
'ssr',
|
'ssr',
|
||||||
'vmess',
|
'vmess',
|
||||||
|
'vless',
|
||||||
'socks',
|
'socks',
|
||||||
'http',
|
'http',
|
||||||
'snell',
|
'snell',
|
||||||
'trojan',
|
'trojan',
|
||||||
'wireguard',
|
'wireguard',
|
||||||
].includes(proxy.type)
|
].includes(proxy.type) ||
|
||||||
) {
|
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||||
return false;
|
(proxy.type === 'vless' &&
|
||||||
} else if (
|
(typeof proxy.flow !== 'undefined' ||
|
||||||
proxy.type === 'snell' &&
|
proxy['reality-opts']))
|
||||||
String(proxy.version) === '4'
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -61,6 +64,11 @@ export default function Clash_Producer() {
|
|||||||
proxy['preshared-key'] =
|
proxy['preshared-key'] =
|
||||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -82,10 +90,22 @@ export default function Clash_Producer() {
|
|||||||
proxy['http-opts'].headers.Host = [httpHost];
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete proxy.tls;
|
delete proxy.tls;
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
if (
|
||||||
|
['grpc'].includes(proxy.network) &&
|
||||||
|
proxy[`${proxy.network}-opts`]
|
||||||
|
) {
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
})
|
})
|
||||||
.join('')
|
.join('')
|
||||||
|
|||||||
134
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
134
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||||
|
|
||||||
|
export default function ClashMeta_Producer() {
|
||||||
|
const type = 'ALL';
|
||||||
|
const produce = (proxies) => {
|
||||||
|
return (
|
||||||
|
'proxies:\n' +
|
||||||
|
proxies
|
||||||
|
.filter((proxy) => {
|
||||||
|
if (
|
||||||
|
proxy.type === 'snell' &&
|
||||||
|
String(proxy.version) === '4'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((proxy) => {
|
||||||
|
if (proxy.type === 'vmess') {
|
||||||
|
// handle vmess aead
|
||||||
|
if (isPresent(proxy, 'aead')) {
|
||||||
|
if (proxy.aead) {
|
||||||
|
proxy.alterId = 0;
|
||||||
|
}
|
||||||
|
delete proxy.aead;
|
||||||
|
}
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||||
|
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'cipher') &&
|
||||||
|
![
|
||||||
|
'auto',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'none',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
proxy.cipher = 'auto';
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'tuic') {
|
||||||
|
if (isPresent(proxy, 'alpn')) {
|
||||||
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: [proxy.alpn];
|
||||||
|
} else {
|
||||||
|
proxy.alpn = ['h3'];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'tfo') &&
|
||||||
|
!isPresent(proxy, 'fast-open')
|
||||||
|
) {
|
||||||
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
}
|
||||||
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||||
|
if (
|
||||||
|
(!proxy.token || proxy.token.length === 0) &&
|
||||||
|
!isPresent(proxy, 'version')
|
||||||
|
) {
|
||||||
|
proxy.version = 5;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'hysteria') {
|
||||||
|
if (isPresent(proxy, 'alpn')) {
|
||||||
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: [proxy.alpn];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'tfo') &&
|
||||||
|
!isPresent(proxy, 'fast-open')
|
||||||
|
) {
|
||||||
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'wireguard') {
|
||||||
|
proxy.keepalive =
|
||||||
|
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||||
|
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||||
|
proxy['preshared-key'] =
|
||||||
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'http'
|
||||||
|
) {
|
||||||
|
let httpPath = proxy['http-opts']?.path;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'http-opts.path') &&
|
||||||
|
!Array.isArray(httpPath)
|
||||||
|
) {
|
||||||
|
proxy['http-opts'].path = [httpPath];
|
||||||
|
}
|
||||||
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||||
|
!Array.isArray(httpHost)
|
||||||
|
) {
|
||||||
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete proxy['tls-fingerprint'];
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
if (
|
||||||
|
['grpc'].includes(proxy.network) &&
|
||||||
|
proxy[`${proxy.network}-opts`]
|
||||||
|
) {
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
}
|
||||||
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { type, produce };
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import Surge_Producer from './surge';
|
import Surge_Producer from './surge';
|
||||||
|
import SurgeMac_Producer from './surgemac';
|
||||||
import Clash_Producer from './clash';
|
import Clash_Producer from './clash';
|
||||||
|
import ClashMeta_Producer from './clashmeta';
|
||||||
import Stash_Producer from './stash';
|
import Stash_Producer from './stash';
|
||||||
import Loon_Producer from './loon';
|
import Loon_Producer from './loon';
|
||||||
import URI_Producer from './uri';
|
import URI_Producer from './uri';
|
||||||
@@ -16,8 +18,10 @@ function JSON_Producer() {
|
|||||||
export default {
|
export default {
|
||||||
QX: QX_Producer(),
|
QX: QX_Producer(),
|
||||||
Surge: Surge_Producer(),
|
Surge: Surge_Producer(),
|
||||||
|
SurgeMac: SurgeMac_Producer(),
|
||||||
Loon: Loon_Producer(),
|
Loon: Loon_Producer(),
|
||||||
Clash: Clash_Producer(),
|
Clash: Clash_Producer(),
|
||||||
|
ClashMeta: ClashMeta_Producer(),
|
||||||
URI: URI_Producer(),
|
URI: URI_Producer(),
|
||||||
V2Ray: V2Ray_Producer(),
|
V2Ray: V2Ray_Producer(),
|
||||||
JSON: JSON_Producer(),
|
JSON: JSON_Producer(),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ function trojan(proxy) {
|
|||||||
'ws-opts.path',
|
'ws-opts.path',
|
||||||
);
|
);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||||
'ws-opts.headers.Host',
|
'ws-opts.headers.Host',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -138,11 +138,11 @@ function vmess(proxy) {
|
|||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
result.append(`,transport=ws`);
|
result.append(`,transport=ws`);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,path=${proxy['ws-opts'].path}`,
|
`,path=${proxy['ws-opts']?.path}`,
|
||||||
'ws-opts.path',
|
'ws-opts.path',
|
||||||
);
|
);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||||
'ws-opts.headers.Host',
|
'ws-opts.headers.Host',
|
||||||
);
|
);
|
||||||
} else if (proxy.network === 'http') {
|
} else if (proxy.network === 'http') {
|
||||||
@@ -205,11 +205,11 @@ function vless(proxy) {
|
|||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
result.append(`,transport=ws`);
|
result.append(`,transport=ws`);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,path=${proxy['ws-opts'].path}`,
|
`,path=${proxy['ws-opts']?.path}`,
|
||||||
'ws-opts.path',
|
'ws-opts.path',
|
||||||
);
|
);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||||
'ws-opts.headers.Host',
|
'ws-opts.headers.Host',
|
||||||
);
|
);
|
||||||
} else if (proxy.network === 'http') {
|
} else if (proxy.network === 'http') {
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ function trojan(proxy) {
|
|||||||
if (needTls(proxy)) append(`,obfs=wss`);
|
if (needTls(proxy)) append(`,obfs=wss`);
|
||||||
else append(`,obfs=ws`);
|
else append(`,obfs=ws`);
|
||||||
appendIfPresent(
|
appendIfPresent(
|
||||||
`,obfs-uri=${proxy['ws-opts'].path}`,
|
`,obfs-uri=${proxy['ws-opts']?.path}`,
|
||||||
'ws-opts.path',
|
'ws-opts.path',
|
||||||
);
|
);
|
||||||
appendIfPresent(
|
appendIfPresent(
|
||||||
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
|
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
|
||||||
'ws-opts.headers.Host',
|
'ws-opts.headers.Host',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||||
|
|
||||||
export default function Stash_Producer() {
|
export default function ShadowRocket_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies) => {
|
||||||
return (
|
return (
|
||||||
@@ -81,6 +81,11 @@ export default function Stash_Producer() {
|
|||||||
proxy['preshared-key'] =
|
proxy['preshared-key'] =
|
||||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -103,11 +108,23 @@ export default function Stash_Producer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete proxy.tls;
|
delete proxy.tls;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
if (
|
||||||
|
['grpc'].includes(proxy.network) &&
|
||||||
|
proxy[`${proxy.network}-opts`]
|
||||||
|
) {
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
})
|
})
|
||||||
.join('')
|
.join('')
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ export default function Stash_Producer() {
|
|||||||
proxy['preshared-key'] =
|
proxy['preshared-key'] =
|
||||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -115,10 +120,22 @@ export default function Stash_Producer() {
|
|||||||
proxy['http-opts'].headers.Host = [httpHost];
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete proxy.tls;
|
delete proxy.tls;
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
if (
|
||||||
|
['grpc'].includes(proxy.network) &&
|
||||||
|
proxy[`${proxy.network}-opts`]
|
||||||
|
) {
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
})
|
})
|
||||||
.join('')
|
.join('')
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export default function Surge_Producer() {
|
|||||||
return snell(proxy);
|
return snell(proxy);
|
||||||
case 'tuic':
|
case 'tuic':
|
||||||
return tuic(proxy);
|
return tuic(proxy);
|
||||||
|
case 'wireguard-surge':
|
||||||
|
return wireguard(proxy);
|
||||||
|
case 'hysteria2':
|
||||||
|
return hysteria2(proxy);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
@@ -69,6 +73,12 @@ function shadowsocks(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +115,12 @@ function trojan(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +164,12 @@ function vmess(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +202,12 @@ function http(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +242,12 @@ function socks5(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +277,12 @@ function snell(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
// reuse
|
// reuse
|
||||||
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
|
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
|
||||||
|
|
||||||
@@ -286,6 +326,99 @@ function tuic(proxy) {
|
|||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
|
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireguard(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
|
||||||
|
result.append(`${proxy.name}=wireguard`);
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,section-name=${proxy['section-name']}`,
|
||||||
|
'section-name',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||||
|
'ip-version',
|
||||||
|
);
|
||||||
|
|
||||||
|
// test-url
|
||||||
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hysteria2(proxy) {
|
||||||
|
if (proxy.obfs || proxy['obfs-password']) {
|
||||||
|
throw new Error(`obfs is unsupported`);
|
||||||
|
}
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
|
||||||
|
|
||||||
|
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||||
|
'ip-version',
|
||||||
|
);
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,server-cert-fingerprint-sha256=${proxy.fingerprint}`,
|
||||||
|
'fingerprint',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy['fast-open']}`, 'fast-open');
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// test-url
|
||||||
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
|
||||||
|
// underlying-proxy
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||||
|
'underlying-proxy',
|
||||||
|
);
|
||||||
|
|
||||||
|
// download-bandwidth
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
|
||||||
|
'down',
|
||||||
|
);
|
||||||
|
|
||||||
|
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
65
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Result } from './utils';
|
||||||
|
import Surge_Producer from './surge';
|
||||||
|
|
||||||
|
const targetPlatform = 'SurgeMac';
|
||||||
|
|
||||||
|
const surge_Producer = Surge_Producer();
|
||||||
|
|
||||||
|
export default function SurgeMac_Producer() {
|
||||||
|
const produce = (proxy) => {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'ssr':
|
||||||
|
return shadowsocksr(proxy);
|
||||||
|
case 'ss':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'trojan':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'vmess':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'http':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'socks5':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'snell':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
case 'tuic':
|
||||||
|
return surge_Producer.produce(proxy);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { produce };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shadowsocksr(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
|
||||||
|
proxy.local_port = '__SubStoreLocalPort__';
|
||||||
|
proxy.local_address = proxy.local_address ?? '127.0.0.1';
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
`${proxy.name} = external, exec = "${
|
||||||
|
proxy.exec || '/usr/local/bin/ssr-local'
|
||||||
|
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries({
|
||||||
|
cipher: '-m',
|
||||||
|
obfs: '-o',
|
||||||
|
password: '-k',
|
||||||
|
port: '-p',
|
||||||
|
protocol: '-O',
|
||||||
|
'protocol-param': '-G',
|
||||||
|
server: '-s',
|
||||||
|
local_port: '-l',
|
||||||
|
local_address: '-b',
|
||||||
|
})) {
|
||||||
|
result.appendIfPresent(
|
||||||
|
`, args = "${value}", args = "${proxy[key]}"`,
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
@@ -91,9 +91,108 @@ export default function URI_Producer() {
|
|||||||
? vmessTransportHost[0]
|
? vmessTransportHost[0]
|
||||||
: vmessTransportHost;
|
: vmessTransportHost;
|
||||||
}
|
}
|
||||||
|
if (['grpc'].includes(proxy.network)) {
|
||||||
|
result.path =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
'grpc-service-name'
|
||||||
|
];
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
result.type =
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||||
|
'gun';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||||
break;
|
break;
|
||||||
|
case 'vless':
|
||||||
|
let security = 'none';
|
||||||
|
const isReality = proxy['reality-opts'];
|
||||||
|
let sid = '';
|
||||||
|
let pbk = '';
|
||||||
|
if (isReality) {
|
||||||
|
security = 'reality';
|
||||||
|
const publicKey = proxy['reality-opts']?.['public-key'];
|
||||||
|
if (publicKey) {
|
||||||
|
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
|
||||||
|
}
|
||||||
|
const shortId = proxy['reality-opts']?.['short-id'];
|
||||||
|
if (shortId) {
|
||||||
|
sid = `&sid=${encodeURIComponent(shortId)}`;
|
||||||
|
}
|
||||||
|
} else if (proxy.tls) {
|
||||||
|
security = 'tls';
|
||||||
|
}
|
||||||
|
let alpn = '';
|
||||||
|
if (proxy.alpn) {
|
||||||
|
alpn = `&alpn=${encodeURIComponent(
|
||||||
|
Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: proxy.alpn.join(','),
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
let allowInsecure = '';
|
||||||
|
if (proxy['skip-cert-verify']) {
|
||||||
|
allowInsecure = `&allowInsecure=1`;
|
||||||
|
}
|
||||||
|
let sni = '';
|
||||||
|
if (proxy.sni) {
|
||||||
|
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
|
||||||
|
}
|
||||||
|
let fp = '';
|
||||||
|
if (proxy['client-fingerprint']) {
|
||||||
|
fp = `&fp=${encodeURIComponent(
|
||||||
|
proxy['client-fingerprint'],
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
let flow = '';
|
||||||
|
if (proxy.flow) {
|
||||||
|
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
||||||
|
}
|
||||||
|
let vlessTransport = `&type=${encodeURIComponent(
|
||||||
|
proxy.network,
|
||||||
|
)}`;
|
||||||
|
if (['grpc'].includes(proxy.network)) {
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
vlessTransport += `&mode=${encodeURIComponent(
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vlessTransportServiceName =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`${proxy.network}-service-name`
|
||||||
|
];
|
||||||
|
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||||
|
let vlessTransportHost =
|
||||||
|
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
|
if (vlessTransportPath) {
|
||||||
|
vlessTransport += `&path=${encodeURIComponent(
|
||||||
|
Array.isArray(vlessTransportPath)
|
||||||
|
? vlessTransportPath[0]
|
||||||
|
: vlessTransportPath,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (vlessTransportHost) {
|
||||||
|
vlessTransport += `&host=${encodeURIComponent(
|
||||||
|
Array.isArray(vlessTransportHost)
|
||||||
|
? vlessTransportHost[0]
|
||||||
|
: vlessTransportHost,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (vlessTransportServiceName) {
|
||||||
|
vlessTransport += `&serviceName=${encodeURIComponent(
|
||||||
|
vlessTransportServiceName,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = `vless://${proxy.uuid}@${proxy.server}:${
|
||||||
|
proxy.port
|
||||||
|
}?${vlessTransport}&security=${encodeURIComponent(
|
||||||
|
security,
|
||||||
|
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
|
||||||
|
proxy.name,
|
||||||
|
)}`;
|
||||||
|
break;
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
let trojanTransport = '';
|
let trojanTransport = '';
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function AllRuleParser() {
|
|||||||
}
|
}
|
||||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
||||||
* @author: Peng-YM
|
* @author: Peng-YM
|
||||||
* @github: https://github.com/Peng-YM/Sub-Store
|
* @github: https://github.com/sub-store-org/Sub-Store
|
||||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||||
*/
|
*/
|
||||||
import { version } from '../package.json';
|
import { version } from '../package.json';
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
|
|||||||
import registerSettingRoutes from '@/restful/settings';
|
import registerSettingRoutes from '@/restful/settings';
|
||||||
import registerMiscRoutes from '@/restful/miscs';
|
import registerMiscRoutes from '@/restful/miscs';
|
||||||
import registerSortRoutes from '@/restful/sort';
|
import registerSortRoutes from '@/restful/sort';
|
||||||
|
import registerFileRoutes from '@/restful/file';
|
||||||
|
import registerModuleRoutes from '@/restful/module';
|
||||||
|
|
||||||
migrate();
|
migrate();
|
||||||
serve();
|
serve();
|
||||||
@@ -30,6 +32,8 @@ function serve() {
|
|||||||
// register routes
|
// register routes
|
||||||
registerCollectionRoutes($app);
|
registerCollectionRoutes($app);
|
||||||
registerSubscriptionRoutes($app);
|
registerSubscriptionRoutes($app);
|
||||||
|
registerFileRoutes($app);
|
||||||
|
registerModuleRoutes($app);
|
||||||
registerArtifactRoutes($app);
|
registerArtifactRoutes($app);
|
||||||
registerSettingRoutes($app);
|
registerSettingRoutes($app);
|
||||||
registerSortRoutes($app);
|
registerSortRoutes($app);
|
||||||
|
|||||||
@@ -32,10 +32,18 @@ async function downloadSubscription(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sub.source !== 'local') {
|
if (sub.source !== 'local') {
|
||||||
// forward flow headers
|
try {
|
||||||
const flowInfo = await getFlowHeaders(sub.url);
|
// forward flow headers
|
||||||
if (flowInfo) {
|
const flowInfo = await getFlowHeaders(sub.url);
|
||||||
res.set('subscription-userinfo', flowInfo);
|
if (flowInfo) {
|
||||||
|
res.set('subscription-userinfo', flowInfo);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||||||
|
err,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +58,15 @@ async function downloadSubscription(req, res) {
|
|||||||
$.notify(
|
$.notify(
|
||||||
`🌍 Sub-Store 下载订阅失败`,
|
`🌍 Sub-Store 下载订阅失败`,
|
||||||
`❌ 无法下载订阅:${name}!`,
|
`❌ 无法下载订阅:${name}!`,
|
||||||
`🤔 原因:${JSON.stringify(err)}`,
|
`🤔 原因:${err.message ?? err}`,
|
||||||
);
|
);
|
||||||
$.error(JSON.stringify(err));
|
$.error(err.message ?? err);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
'INTERNAL_SERVER_ERROR',
|
'INTERNAL_SERVER_ERROR',
|
||||||
`Failed to download subscription: ${name}`,
|
`Failed to download subscription: ${name}`,
|
||||||
`Reason: ${JSON.stringify(err)}`,
|
`Reason: ${err.message ?? err}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,9 +109,17 @@ async function downloadCollection(req, res) {
|
|||||||
if (subnames.length > 0) {
|
if (subnames.length > 0) {
|
||||||
const sub = findByName(allSubs, subnames[0]);
|
const sub = findByName(allSubs, subnames[0]);
|
||||||
if (sub.source !== 'local') {
|
if (sub.source !== 'local') {
|
||||||
const flowInfo = await getFlowHeaders(sub.url);
|
try {
|
||||||
if (flowInfo) {
|
const flowInfo = await getFlowHeaders(sub.url);
|
||||||
res.set('subscription-userinfo', flowInfo);
|
if (flowInfo) {
|
||||||
|
res.set('subscription-userinfo', flowInfo);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`组合订阅 ${name} 中的子订阅 ${
|
||||||
|
sub.name
|
||||||
|
} 获取流量信息时发生错误: ${err.message ?? err}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +142,7 @@ async function downloadCollection(req, res) {
|
|||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
'INTERNAL_SERVER_ERROR',
|
'INTERNAL_SERVER_ERROR',
|
||||||
`Failed to download collection: ${name}`,
|
`Failed to download collection: ${name}`,
|
||||||
`Reason: ${JSON.stringify(err)}`,
|
`Reason: ${err.message ?? err}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
109
backend/src/restful/file.js
Normal file
109
backend/src/restful/file.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||||
|
import { FILES_KEY } from '@/constants';
|
||||||
|
import { failed, success } from '@/restful/response';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||||
|
|
||||||
|
export default function register($app) {
|
||||||
|
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||||
|
|
||||||
|
$app.route('/api/file/:name')
|
||||||
|
.get(getFile)
|
||||||
|
.patch(updateFile)
|
||||||
|
.delete(deleteFile);
|
||||||
|
|
||||||
|
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// file API
|
||||||
|
function createFile(req, res) {
|
||||||
|
const file = req.body;
|
||||||
|
file.name = `${file.name ?? Date.now()}`;
|
||||||
|
$.info(`正在创建文件:${file.name}`);
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
if (findByName(allFiles, file.name)) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'DUPLICATE_KEY',
|
||||||
|
req.body.name
|
||||||
|
? `已存在 name 为 ${file.name} 的文件`
|
||||||
|
: `无法同时创建相同的文件 可稍后重试`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
allFiles.push(file);
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res, file, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFile(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const file = findByName(allFiles, name);
|
||||||
|
if (file) {
|
||||||
|
res.status(200).json(file.content);
|
||||||
|
} else {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
`FILE_NOT_FOUND`,
|
||||||
|
`File ${name} does not exist`,
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFile(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
let file = req.body;
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const oldFile = findByName(allFiles, name);
|
||||||
|
if (oldFile) {
|
||||||
|
const newFile = {
|
||||||
|
...oldFile,
|
||||||
|
...file,
|
||||||
|
};
|
||||||
|
$.info(`正在更新文件:${name}...`);
|
||||||
|
|
||||||
|
updateByName(allFiles, name, newFile);
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res, newFile);
|
||||||
|
} else {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
'RESOURCE_NOT_FOUND',
|
||||||
|
`File ${name} does not exist!`,
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
$.info(`正在删除文件:${name}`);
|
||||||
|
let allFiles = $.read(FILES_KEY);
|
||||||
|
deleteByName(allFiles, name);
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllFiles(req, res) {
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
success(
|
||||||
|
res, // eslint-disable-next-line no-unused-vars
|
||||||
|
allFiles.map(({ content, ...rest }) => rest),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceFile(req, res) {
|
||||||
|
const allFiles = req.body;
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res);
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import $ from '@/core/app';
|
|||||||
import registerSubscriptionRoutes from './subscriptions';
|
import registerSubscriptionRoutes from './subscriptions';
|
||||||
import registerCollectionRoutes from './collections';
|
import registerCollectionRoutes from './collections';
|
||||||
import registerArtifactRoutes from './artifacts';
|
import registerArtifactRoutes from './artifacts';
|
||||||
|
import registerFileRoutes from './file';
|
||||||
|
import registerModuleRoutes from './module';
|
||||||
import registerSyncRoutes from './sync';
|
import registerSyncRoutes from './sync';
|
||||||
import registerDownloadRoutes from './download';
|
import registerDownloadRoutes from './download';
|
||||||
import registerSettingRoutes from './settings';
|
import registerSettingRoutes from './settings';
|
||||||
@@ -23,6 +25,8 @@ export default function serve() {
|
|||||||
registerSortingRoutes($app);
|
registerSortingRoutes($app);
|
||||||
registerSettingRoutes($app);
|
registerSettingRoutes($app);
|
||||||
registerArtifactRoutes($app);
|
registerArtifactRoutes($app);
|
||||||
|
registerFileRoutes($app);
|
||||||
|
registerModuleRoutes($app);
|
||||||
registerSyncRoutes($app);
|
registerSyncRoutes($app);
|
||||||
registerNodeInfoRoutes($app);
|
registerNodeInfoRoutes($app);
|
||||||
registerMiscRoutes($app);
|
registerMiscRoutes($app);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { ENV } from '@/vendor/open-api';
|
import { ENV } from '@/vendor/open-api';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import { version as substoreVersion } from '../../package.json';
|
|
||||||
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
|
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
|
||||||
import Gist from '@/utils/gist';
|
import Gist from '@/utils/gist';
|
||||||
import migrate from '@/utils/migration';
|
import migrate from '@/utils/migration';
|
||||||
|
import env from '@/utils/env';
|
||||||
|
|
||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
// utils
|
// utils
|
||||||
@@ -49,19 +49,7 @@ export default function register($app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEnv(req, res) {
|
function getEnv(req, res) {
|
||||||
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
success(res, env);
|
||||||
let backend = 'Node';
|
|
||||||
if (isNode) backend = 'Node';
|
|
||||||
if (isQX) backend = 'QX';
|
|
||||||
if (isLoon) backend = 'Loon';
|
|
||||||
if (isSurge) backend = 'Surge';
|
|
||||||
if (isStash) backend = 'Stash';
|
|
||||||
if (isShadowRocket) backend = 'ShadowRocket';
|
|
||||||
|
|
||||||
success(res, {
|
|
||||||
backend,
|
|
||||||
version: substoreVersion,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh(_, res) {
|
async function refresh(_, res) {
|
||||||
@@ -118,6 +106,23 @@ async function gistBackup(req, res) {
|
|||||||
case 'download':
|
case 'download':
|
||||||
$.info(`还原备份中...`);
|
$.info(`还原备份中...`);
|
||||||
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
Object.keys(JSON.parse(content).settings).length ===
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'备份文件应该至少包含 settings 字段',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||||
|
err.message ?? err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||||
|
}
|
||||||
// restore settings
|
// restore settings
|
||||||
$.write(content, '#sub-store');
|
$.write(content, '#sub-store');
|
||||||
if ($.env.isNode) {
|
if ($.env.isNode) {
|
||||||
@@ -125,8 +130,10 @@ async function gistBackup(req, res) {
|
|||||||
$.cache = content;
|
$.cache = content;
|
||||||
$.persistCache();
|
$.persistCache();
|
||||||
}
|
}
|
||||||
// perform migration after restoring from gist
|
$.info(`perform migration after restoring from gist...`);
|
||||||
migrate();
|
migrate();
|
||||||
|
$.info(`migration completed`);
|
||||||
|
$.info(`还原备份完成`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
success(res);
|
success(res);
|
||||||
@@ -136,7 +143,7 @@ async function gistBackup(req, res) {
|
|||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
'BACKUP_FAILED',
|
'BACKUP_FAILED',
|
||||||
`Failed to ${action} data to gist!`,
|
`Failed to ${action} data to gist!`,
|
||||||
`Reason: ${JSON.stringify(err)}`,
|
`Reason: ${err.message ?? err}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
114
backend/src/restful/module.js
Normal file
114
backend/src/restful/module.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||||
|
import { MODULES_KEY } from '@/constants';
|
||||||
|
import { failed, success } from '@/restful/response';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||||
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
|
|
||||||
|
export default function register($app) {
|
||||||
|
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
|
||||||
|
|
||||||
|
$app.route('/api/module/:name')
|
||||||
|
.get(getModule)
|
||||||
|
.patch(updateModule)
|
||||||
|
.delete(deleteModule);
|
||||||
|
|
||||||
|
$app.route('/api/modules')
|
||||||
|
.get(getAllModules)
|
||||||
|
.post(createModule)
|
||||||
|
.put(replaceModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// module API
|
||||||
|
function createModule(req, res) {
|
||||||
|
const module = req.body;
|
||||||
|
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
|
||||||
|
$.info(`正在创建模块:${module.name}`);
|
||||||
|
const allModules = $.read(MODULES_KEY);
|
||||||
|
if (findByName(allModules, module.name)) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'DUPLICATE_KEY',
|
||||||
|
req.body.name
|
||||||
|
? `已存在 name 为 ${module.name} 的模块`
|
||||||
|
: `已存在相同的模块 请勿重复添加`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
allModules.push(module);
|
||||||
|
$.write(allModules, MODULES_KEY);
|
||||||
|
success(res, module, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModule(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
const allModules = $.read(MODULES_KEY);
|
||||||
|
const module = findByName(allModules, name);
|
||||||
|
if (module) {
|
||||||
|
res.status(200).json(module.content);
|
||||||
|
} else {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
`MODULE_NOT_FOUND`,
|
||||||
|
`Module ${name} does not exist`,
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModule(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
let module = req.body;
|
||||||
|
const allModules = $.read(MODULES_KEY);
|
||||||
|
const oldModule = findByName(allModules, name);
|
||||||
|
if (oldModule) {
|
||||||
|
const newModule = {
|
||||||
|
...oldModule,
|
||||||
|
...module,
|
||||||
|
};
|
||||||
|
$.info(`正在更新模块:${name}...`);
|
||||||
|
|
||||||
|
updateByName(allModules, name, newModule);
|
||||||
|
$.write(allModules, MODULES_KEY);
|
||||||
|
success(res, newModule);
|
||||||
|
} else {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
'RESOURCE_NOT_FOUND',
|
||||||
|
`Module ${name} does not exist!`,
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteModule(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
$.info(`正在删除模块:${name}`);
|
||||||
|
let allModules = $.read(MODULES_KEY);
|
||||||
|
deleteByName(allModules, name);
|
||||||
|
$.write(allModules, MODULES_KEY);
|
||||||
|
success(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllModules(req, res) {
|
||||||
|
const allModules = $.read(MODULES_KEY);
|
||||||
|
success(
|
||||||
|
res,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
allModules.map(({ content, ...rest }) => rest),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceModule(req, res) {
|
||||||
|
const allModules = req.body;
|
||||||
|
$.write(allModules, MODULES_KEY);
|
||||||
|
success(res);
|
||||||
|
}
|
||||||
@@ -12,98 +12,133 @@ export default function register($app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function compareSub(req, res) {
|
async function compareSub(req, res) {
|
||||||
const sub = req.body;
|
try {
|
||||||
const target = req.query.target || 'JSON';
|
const sub = req.body;
|
||||||
let content;
|
const target = req.query.target || 'JSON';
|
||||||
if (sub.source === 'local') {
|
let content;
|
||||||
content = sub.content;
|
if (sub.source === 'local') {
|
||||||
} else {
|
content = sub.content;
|
||||||
try {
|
} else {
|
||||||
content = await download(sub.url, sub.ua);
|
|
||||||
} catch (err) {
|
|
||||||
failed(
|
|
||||||
res,
|
|
||||||
new NetworkError(
|
|
||||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
|
||||||
'无法下载远程资源',
|
|
||||||
`Reason: ${err}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// parse proxies
|
|
||||||
const original = ProxyUtils.parse(content);
|
|
||||||
|
|
||||||
// add id
|
|
||||||
original.forEach((proxy, i) => {
|
|
||||||
proxy.id = i;
|
|
||||||
});
|
|
||||||
|
|
||||||
// apply processors
|
|
||||||
const processed = await ProxyUtils.process(
|
|
||||||
original,
|
|
||||||
sub.process || [],
|
|
||||||
target,
|
|
||||||
);
|
|
||||||
|
|
||||||
// produce
|
|
||||||
success(res, { original, processed });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function compareCollection(req, res) {
|
|
||||||
const allSubs = $.read(SUBS_KEY);
|
|
||||||
const collection = req.body;
|
|
||||||
const subnames = collection.subscriptions;
|
|
||||||
const results = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
subnames.map(async (name) => {
|
|
||||||
const sub = findByName(allSubs, name);
|
|
||||||
try {
|
try {
|
||||||
let raw;
|
content = await download(sub.url, sub.ua);
|
||||||
if (sub.source === 'local') {
|
|
||||||
raw = sub.content;
|
|
||||||
} else {
|
|
||||||
raw = await download(sub.url, sub.ua);
|
|
||||||
}
|
|
||||||
// parse proxies
|
|
||||||
let currentProxies = ProxyUtils.parse(raw);
|
|
||||||
// apply processors
|
|
||||||
currentProxies = await ProxyUtils.process(
|
|
||||||
currentProxies,
|
|
||||||
sub.process || [],
|
|
||||||
'JSON',
|
|
||||||
);
|
|
||||||
results[name] = currentProxies;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new NetworkError(
|
||||||
'PROCESS_FAILED',
|
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||||
`处理子订阅 ${name} 失败`,
|
'无法下载远程资源',
|
||||||
`Reason: ${err}`,
|
`Reason: ${err}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
);
|
// parse proxies
|
||||||
|
const original = ProxyUtils.parse(content);
|
||||||
|
|
||||||
// merge proxies with the original order
|
// add id
|
||||||
const original = Array.prototype.concat.apply(
|
original.forEach((proxy, i) => {
|
||||||
[],
|
proxy.id = i;
|
||||||
subnames.map((name) => results[name] || []),
|
proxy.subName = sub.name;
|
||||||
);
|
});
|
||||||
|
|
||||||
original.forEach((proxy, i) => {
|
// apply processors
|
||||||
proxy.id = i;
|
const processed = await ProxyUtils.process(
|
||||||
});
|
original,
|
||||||
|
sub.process || [],
|
||||||
|
target,
|
||||||
|
{ [sub.name]: sub },
|
||||||
|
);
|
||||||
|
|
||||||
const processed = await ProxyUtils.process(
|
// produce
|
||||||
original,
|
success(res, { original, processed });
|
||||||
collection.process || [],
|
} catch (err) {
|
||||||
'JSON',
|
$.error(err.message ?? err);
|
||||||
);
|
failed(
|
||||||
|
res,
|
||||||
success(res, { original, processed });
|
new InternalServerError(
|
||||||
|
`INTERNAL_SERVER_ERROR`,
|
||||||
|
`Failed to preview subscription`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compareCollection(req, res) {
|
||||||
|
try {
|
||||||
|
const allSubs = $.read(SUBS_KEY);
|
||||||
|
const collection = req.body;
|
||||||
|
const subnames = collection.subscriptions;
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
subnames.map(async (name) => {
|
||||||
|
const sub = findByName(allSubs, name);
|
||||||
|
try {
|
||||||
|
let raw;
|
||||||
|
if (sub.source === 'local') {
|
||||||
|
raw = sub.content;
|
||||||
|
} else {
|
||||||
|
raw = await download(sub.url, sub.ua);
|
||||||
|
}
|
||||||
|
// parse proxies
|
||||||
|
let currentProxies = ProxyUtils.parse(raw);
|
||||||
|
|
||||||
|
currentProxies.forEach((proxy) => {
|
||||||
|
proxy.subName = sub.name;
|
||||||
|
proxy.collectionName = collection.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
currentProxies = await ProxyUtils.process(
|
||||||
|
currentProxies,
|
||||||
|
sub.process || [],
|
||||||
|
'JSON',
|
||||||
|
{ [sub.name]: sub, _collection: collection },
|
||||||
|
);
|
||||||
|
results[name] = currentProxies;
|
||||||
|
} catch (err) {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
'PROCESS_FAILED',
|
||||||
|
`处理子订阅 ${name} 失败`,
|
||||||
|
`Reason: ${err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// merge proxies with the original order
|
||||||
|
const original = Array.prototype.concat.apply(
|
||||||
|
[],
|
||||||
|
subnames.map((name) => results[name] || []),
|
||||||
|
);
|
||||||
|
|
||||||
|
original.forEach((proxy, i) => {
|
||||||
|
proxy.id = i;
|
||||||
|
proxy.collectionName = collection.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const processed = await ProxyUtils.process(
|
||||||
|
original,
|
||||||
|
collection.process || [],
|
||||||
|
'JSON',
|
||||||
|
{ _collection: collection },
|
||||||
|
);
|
||||||
|
|
||||||
|
success(res, { original, processed });
|
||||||
|
} catch (err) {
|
||||||
|
$.error(err.message ?? err);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`INTERNAL_SERVER_ERROR`,
|
||||||
|
`Failed to preview collection`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ export async function updateGitHubAvatar() {
|
|||||||
.then((resp) => JSON.parse(resp.body));
|
.then((resp) => JSON.parse(resp.body));
|
||||||
settings.avatarUrl = data['avatar_url'];
|
settings.avatarUrl = data['avatar_url'];
|
||||||
$.write(settings, SETTINGS_KEY);
|
$.write(settings, SETTINGS_KEY);
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
$.error('Failed to fetch GitHub avatar for User: ' + username);
|
$.error(
|
||||||
|
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||||
|
err.message ?? err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +71,11 @@ export async function updateArtifactStore() {
|
|||||||
$.write(settings, SETTINGS_KEY);
|
$.write(settings, SETTINGS_KEY);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error('Failed to fetch artifact store for User: ' + githubUser);
|
$.error(
|
||||||
|
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
|
||||||
|
err.message ?? err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function register($app) {
|
|||||||
function sortSubs(req, res) {
|
function sortSubs(req, res) {
|
||||||
const orders = req.body;
|
const orders = req.body;
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||||
$.write(allSubs, SUBS_KEY);
|
$.write(allSubs, SUBS_KEY);
|
||||||
success(res, allSubs);
|
success(res, allSubs);
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
|
|||||||
function sortCollections(req, res) {
|
function sortCollections(req, res) {
|
||||||
const orders = req.body;
|
const orders = req.body;
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||||
$.write(allCols, COLLECTIONS_KEY);
|
$.write(allCols, COLLECTIONS_KEY);
|
||||||
success(res, allCols);
|
success(res, allCols);
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
|
|||||||
function sortArtifacts(req, res) {
|
function sortArtifacts(req, res) {
|
||||||
const orders = req.body;
|
const orders = req.body;
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
allArtifacts.sort(
|
||||||
|
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
|
||||||
|
);
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
success(res, allArtifacts);
|
success(res, allArtifacts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,26 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
}
|
}
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let proxies = ProxyUtils.parse(raw);
|
let proxies = ProxyUtils.parse(raw);
|
||||||
|
proxies.forEach((proxy) => {
|
||||||
|
proxy.subName = sub.name;
|
||||||
|
});
|
||||||
// apply processors
|
// apply processors
|
||||||
proxies = await ProxyUtils.process(
|
proxies = await ProxyUtils.process(
|
||||||
proxies,
|
proxies,
|
||||||
sub.process || [],
|
sub.process || [],
|
||||||
platform,
|
platform,
|
||||||
|
{ [sub.name]: sub },
|
||||||
);
|
);
|
||||||
|
if (proxies.length === 0) {
|
||||||
|
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||||
|
}
|
||||||
// check duplicate
|
// check duplicate
|
||||||
const exist = {};
|
const exist = {};
|
||||||
for (const proxy of proxies) {
|
for (const proxy of proxies) {
|
||||||
if (exist[proxy.name]) {
|
if (exist[proxy.name]) {
|
||||||
$.notify(
|
$.notify(
|
||||||
'🌍 Sub-Store',
|
'🌍 Sub-Store',
|
||||||
'⚠️ 订阅包含重复节点!',
|
`⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||||
'请仔细检测配置!',
|
'请仔细检测配置!',
|
||||||
{
|
{
|
||||||
'media-url':
|
'media-url':
|
||||||
@@ -67,6 +74,7 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
const collection = findByName(allCols, name);
|
const collection = findByName(allCols, name);
|
||||||
const subnames = collection.subscriptions;
|
const subnames = collection.subscriptions;
|
||||||
const results = {};
|
const results = {};
|
||||||
|
const errors = {};
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -82,11 +90,18 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
}
|
}
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let currentProxies = ProxyUtils.parse(raw);
|
let currentProxies = ProxyUtils.parse(raw);
|
||||||
|
|
||||||
|
currentProxies.forEach((proxy) => {
|
||||||
|
proxy.subName = sub.name;
|
||||||
|
proxy.collectionName = collection.name;
|
||||||
|
});
|
||||||
|
|
||||||
// apply processors
|
// apply processors
|
||||||
currentProxies = await ProxyUtils.process(
|
currentProxies = await ProxyUtils.process(
|
||||||
currentProxies,
|
currentProxies,
|
||||||
sub.process || [],
|
sub.process || [],
|
||||||
platform,
|
platform,
|
||||||
|
{ [sub.name]: sub, _collection: collection },
|
||||||
);
|
);
|
||||||
results[name] = currentProxies;
|
results[name] = currentProxies;
|
||||||
processed++;
|
processed++;
|
||||||
@@ -97,6 +112,7 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
processed++;
|
processed++;
|
||||||
|
errors[name] = err;
|
||||||
$.error(
|
$.error(
|
||||||
`❌ 处理组合订阅中的子订阅: ${
|
`❌ 处理组合订阅中的子订阅: ${
|
||||||
sub.name
|
sub.name
|
||||||
@@ -108,20 +124,33 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// merge proxies with the original order
|
// merge proxies with the original order
|
||||||
let proxies = Array.prototype.concat.apply(
|
let proxies = Array.prototype.concat.apply(
|
||||||
[],
|
[],
|
||||||
subnames.map((name) => results[name]),
|
subnames.map((name) => results[name] || []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
proxies.forEach((proxy) => {
|
||||||
|
proxy.collectionName = collection.name;
|
||||||
|
});
|
||||||
|
|
||||||
// apply own processors
|
// apply own processors
|
||||||
proxies = await ProxyUtils.process(
|
proxies = await ProxyUtils.process(
|
||||||
proxies,
|
proxies,
|
||||||
collection.process || [],
|
collection.process || [],
|
||||||
platform,
|
platform,
|
||||||
|
{ _collection: collection },
|
||||||
);
|
);
|
||||||
if (proxies.length === 0) {
|
if (proxies.length === 0) {
|
||||||
throw new Error(`组合订阅中不含有效节点!`);
|
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||||
}
|
}
|
||||||
// check duplicate
|
// check duplicate
|
||||||
const exist = {};
|
const exist = {};
|
||||||
@@ -129,7 +158,7 @@ async function produceArtifact({ type, name, platform }) {
|
|||||||
if (exist[proxy.name]) {
|
if (exist[proxy.name]) {
|
||||||
$.notify(
|
$.notify(
|
||||||
'🌍 Sub-Store',
|
'🌍 Sub-Store',
|
||||||
'⚠️ 订阅包含重复节点!',
|
`⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||||
'请仔细检测配置!',
|
'请仔细检测配置!',
|
||||||
{
|
{
|
||||||
'media-url':
|
'media-url':
|
||||||
@@ -242,20 +271,20 @@ async function syncArtifact(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = await produceArtifact({
|
|
||||||
type: artifact.type,
|
|
||||||
name: artifact.source,
|
|
||||||
platform: artifact.platform,
|
|
||||||
});
|
|
||||||
|
|
||||||
$.info(
|
|
||||||
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
|
||||||
artifact,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
|
const output = await produceArtifact({
|
||||||
|
type: artifact.type,
|
||||||
|
name: artifact.source,
|
||||||
|
platform: artifact.platform,
|
||||||
|
});
|
||||||
|
|
||||||
|
$.info(
|
||||||
|
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
||||||
|
artifact,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
const resp = await syncToGist({
|
const resp = await syncToGist({
|
||||||
[encodeURIComponent(artifact.name)]: {
|
[encodeURIComponent(artifact.name)]: {
|
||||||
content: output,
|
content: output,
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
|
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||||
|
import { findByName } from '@/utils/database';
|
||||||
import { HTTP, ENV } from '@/vendor/open-api';
|
import { HTTP, ENV } from '@/vendor/open-api';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
const tasks = new Map();
|
const tasks = new Map();
|
||||||
|
|
||||||
export default async function download(url, ua) {
|
export default async function download(url, ua) {
|
||||||
|
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||||
|
if (downloadUrlMatch) {
|
||||||
|
let type = downloadUrlMatch?.[1];
|
||||||
|
let name = downloadUrlMatch?.[2];
|
||||||
|
if (name == null) {
|
||||||
|
throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||||
|
}
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||||
|
const item = findByName($.read(key), name);
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`找不到本地 ${type}: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.content;
|
||||||
|
}
|
||||||
|
|
||||||
const { isNode } = ENV();
|
const { isNode } = ENV();
|
||||||
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||||
const id = hex_md5(ua + url);
|
const id = hex_md5(ua + url);
|
||||||
|
|||||||
16
backend/src/utils/env.js
Normal file
16
backend/src/utils/env.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { version as substoreVersion } from '../../package.json';
|
||||||
|
import { ENV } from '@/vendor/open-api';
|
||||||
|
|
||||||
|
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
||||||
|
let backend = 'Node';
|
||||||
|
if (isNode) backend = 'Node';
|
||||||
|
if (isQX) backend = 'QX';
|
||||||
|
if (isLoon) backend = 'Loon';
|
||||||
|
if (isSurge) backend = 'Surge';
|
||||||
|
if (isStash) backend = 'Stash';
|
||||||
|
if (isShadowRocket) backend = 'ShadowRocket';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
backend,
|
||||||
|
version: substoreVersion,
|
||||||
|
};
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
export function getPlatformFromHeaders(headers) {
|
export function getPlatformFromHeaders(headers) {
|
||||||
const keys = Object.keys(headers);
|
const keys = Object.keys(headers);
|
||||||
let UA = '';
|
let UA = '';
|
||||||
|
let ua = '';
|
||||||
for (let k of keys) {
|
for (let k of keys) {
|
||||||
if (/USER-AGENT/i.test(k)) {
|
if (/USER-AGENT/i.test(k)) {
|
||||||
UA = headers[k];
|
UA = headers[k];
|
||||||
|
ua = UA.toLowerCase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||||
return 'QX';
|
return 'QX';
|
||||||
|
} else if (UA.indexOf('Surge Mac') !== -1) {
|
||||||
|
return 'SurgeMac';
|
||||||
} else if (UA.indexOf('Surge') !== -1) {
|
} else if (UA.indexOf('Surge') !== -1) {
|
||||||
return 'Surge';
|
return 'Surge';
|
||||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||||
@@ -17,6 +21,15 @@ export function getPlatformFromHeaders(headers) {
|
|||||||
return 'ShadowRocket';
|
return 'ShadowRocket';
|
||||||
} else if (UA.indexOf('Stash') !== -1) {
|
} else if (UA.indexOf('Stash') !== -1) {
|
||||||
return 'Stash';
|
return 'Stash';
|
||||||
|
} else if (
|
||||||
|
ua === 'meta' ||
|
||||||
|
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
|
||||||
|
) {
|
||||||
|
return 'ClashMeta';
|
||||||
|
} else if (ua.indexOf('clash') !== -1) {
|
||||||
|
return 'Clash';
|
||||||
|
} else if (ua.indexOf('v2ray') !== -1) {
|
||||||
|
return 'V2Ray';
|
||||||
} else {
|
} else {
|
||||||
return 'JSON';
|
return 'JSON';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ResourceCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set(id, value) {
|
set(id, value) {
|
||||||
this.resourceCache[id] = { time: new Date().getTime(), data: value }
|
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||||
this._persist();
|
this._persist();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
backend/src/vendor/express.js
vendored
2
backend/src/vendor/express.js
vendored
@@ -17,7 +17,7 @@ export default function express({ substore: $, port }) {
|
|||||||
const express_ = eval(`require("express")`);
|
const express_ = eval(`require("express")`);
|
||||||
const bodyParser = eval(`require("body-parser")`);
|
const bodyParser = eval(`require("body-parser")`);
|
||||||
const app = express_();
|
const app = express_();
|
||||||
app.use(bodyParser.json({ verify: rawBodySaver }));
|
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
|
||||||
app.use(
|
app.use(
|
||||||
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
||||||
);
|
);
|
||||||
|
|||||||
32
backend/src/vendor/open-api.js
vendored
32
backend/src/vendor/open-api.js
vendored
@@ -49,27 +49,32 @@ export class OpenAPI {
|
|||||||
|
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
// create a json for root cache
|
// create a json for root cache
|
||||||
let fpath = 'root.json';
|
const basePath =
|
||||||
if (!this.node.fs.existsSync(fpath)) {
|
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
let rootPath = `${basePath}/root.json`;
|
||||||
|
|
||||||
|
this.log(`Root path: ${rootPath}`);
|
||||||
|
if (!this.node.fs.existsSync(rootPath)) {
|
||||||
|
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'wx',
|
||||||
});
|
});
|
||||||
this.root = {};
|
this.root = {};
|
||||||
} else {
|
} else {
|
||||||
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
this.root = JSON.parse(
|
||||||
|
this.node.fs.readFileSync(`${rootPath}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a json file with the given name if not exists
|
// create a json file with the given name if not exists
|
||||||
fpath = `${this.name}.json`;
|
let fpath = `${basePath}/${this.name}.json`;
|
||||||
|
this.log(`Data path: ${fpath}`);
|
||||||
if (!this.node.fs.existsSync(fpath)) {
|
if (!this.node.fs.existsSync(fpath)) {
|
||||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'wx',
|
||||||
});
|
});
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
} else {
|
} else {
|
||||||
this.cache = JSON.parse(
|
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
||||||
this.node.fs.readFileSync(`${this.name}.json`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,14 +85,17 @@ export class OpenAPI {
|
|||||||
if (isQX) $prefs.setValueForKey(data, this.name);
|
if (isQX) $prefs.setValueForKey(data, this.name);
|
||||||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
|
const basePath =
|
||||||
|
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||||
|
|
||||||
this.node.fs.writeFileSync(
|
this.node.fs.writeFileSync(
|
||||||
`${this.name}.json`,
|
`${basePath}/${this.name}.json`,
|
||||||
data,
|
data,
|
||||||
{ flag: 'w' },
|
{ flag: 'w' },
|
||||||
(err) => console.log(err),
|
(err) => console.log(err),
|
||||||
);
|
);
|
||||||
this.node.fs.writeFileSync(
|
this.node.fs.writeFileSync(
|
||||||
'root.json',
|
`${basePath}/root.json`,
|
||||||
JSON.stringify(this.root, null, 2),
|
JSON.stringify(this.root, null, 2),
|
||||||
{ flag: 'w' },
|
{ flag: 'w' },
|
||||||
(err) => console.log(err),
|
(err) => console.log(err),
|
||||||
@@ -308,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
|||||||
return (
|
return (
|
||||||
timer
|
timer
|
||||||
? Promise.race([timer, worker]).then((res) => {
|
? Promise.race([timer, worker]).then((res) => {
|
||||||
clearTimeout(timeoutid);
|
if (typeof clearTimeout !== 'undefined') {
|
||||||
|
clearTimeout(timeoutid);
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
: worker
|
: worker
|
||||||
|
|||||||
16
backend/sub-store.min.js
vendored
16
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
#!desc=高级订阅管理工具
|
#!desc=高级订阅管理工具
|
||||||
#!openUrl=https://sub.store
|
#!openUrl=https://sub.store
|
||||||
#!author=Peng-YM
|
#!author=Peng-YM
|
||||||
#!homepage=https://github.com/Peng-YM/Sub-Store
|
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||||
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
||||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
# Sub-Store 配置指南
|
# Sub-Store 配置指南
|
||||||
|
|
||||||
|
## 查看更新说明:
|
||||||
|
|
||||||
|
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
|
||||||
|
|
||||||
|
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||||
|
|
||||||
## 脚本配置:
|
## 脚本配置:
|
||||||
|
|
||||||
### 1. Loon
|
### 1. Loon
|
||||||
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。
|
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
|
||||||
|
|
||||||
### 2. Surge
|
### 2. Surge
|
||||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
|
||||||
|
|
||||||
|
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
|
||||||
|
|
||||||
|
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
|
||||||
|
|
||||||
|
|
||||||
### 3. QX
|
### 3. QX
|
||||||
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
|
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
|
||||||
|
|
||||||
### 4. Stash
|
### 4. Stash
|
||||||
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
|
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
|
||||||
|
|
||||||
### 5. Shadowrocket
|
### 5. Shadowrocket
|
||||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
|
||||||
|
|
||||||
## 使用 Sub-Store
|
## 使用 Sub-Store
|
||||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||||
|
|||||||
Reference in New Issue
Block a user