mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4973454f58 | ||
|
|
4c6ba2cdc8 | ||
|
|
9cbbd0e86f | ||
|
|
0320a77451 | ||
|
|
afb9296158 | ||
|
|
9b0c15ebc2 | ||
|
|
46738d5947 | ||
|
|
1f505752ae | ||
|
|
0734a3d563 | ||
|
|
497bc264e3 | ||
|
|
feb207b333 | ||
|
|
9ac1112b37 | ||
|
|
96769598ef | ||
|
|
f8ed6a3342 | ||
|
|
99b19c410d | ||
|
|
9e54507bbb | ||
|
|
20afa0ad22 | ||
|
|
c5b6960b35 | ||
|
|
4dd86cb368 | ||
|
|
4a0319e95f | ||
|
|
090d8a978f | ||
|
|
bc9fae6062 | ||
|
|
048344268c | ||
|
|
c5746f6a6b | ||
|
|
5cb226da62 | ||
|
|
d229047744 | ||
|
|
cb21a8e6ec | ||
|
|
537a00e8a9 | ||
|
|
b770578cba | ||
|
|
47a95e5a3d | ||
|
|
e99f13d487 | ||
|
|
fcab8401e0 | ||
|
|
431b1a3c8e | ||
|
|
36d46003d6 | ||
|
|
ff71f12996 | ||
|
|
f7c08e3a56 | ||
|
|
6eea8bb2d0 | ||
|
|
fc90e22a48 | ||
|
|
26d47b019b | ||
|
|
8e49a78f45 | ||
|
|
edee10cee3 | ||
|
|
20d958d74f | ||
|
|
6427f99545 | ||
|
|
7d2ea10206 | ||
|
|
e862235cb8 | ||
|
|
38f1728e42 | ||
|
|
d963be87f8 | ||
|
|
390e4540d2 | ||
|
|
0bd00406f3 | ||
|
|
b9ce4e8f20 | ||
|
|
de15bbf3ea | ||
|
|
5d6bd1415b | ||
|
|
6e9c3ead4c | ||
|
|
b3ccd5743a | ||
|
|
e18c215fe4 | ||
|
|
e4b54b43a1 | ||
|
|
21726bf950 | ||
|
|
f6ca9af00f | ||
|
|
39b79b6ca4 | ||
|
|
999271fa9d | ||
|
|
5de35c7720 |
27
.github/workflows/main.yml
vendored
27
.github/workflows/main.yml
vendored
@@ -1,15 +1,15 @@
|
||||
name: build
|
||||
on:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'backend/package.json'
|
||||
- "backend/package.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'backend/package.json'
|
||||
- "backend/package.json"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -17,11 +17,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'master'
|
||||
ref: "master"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "14"
|
||||
node-version: "16"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
@@ -34,17 +34,28 @@ jobs:
|
||||
run: |
|
||||
cd backend
|
||||
pnpm run build
|
||||
- name: Bundle
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D estrella
|
||||
pnpm run bundle
|
||||
- id: tag
|
||||
name: Generate release tag
|
||||
run: |
|
||||
cd backend
|
||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||
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
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body_path: ./backend/CHANGELOG.md
|
||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||
files: |
|
||||
./backend/sub-store.min.js
|
||||
@@ -52,3 +63,9 @@ jobs:
|
||||
./backend/dist/sub-store-1.min.js
|
||||
./backend/dist/sub-store-parser.loon.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*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
# Dist files
|
||||
backend/dist/*
|
||||
!backend/dist/.gitkeep
|
||||
backend/sub-store.min.js
|
||||
|
||||
CHANGELOG.md
|
||||
|
||||
30
README.md
30
README.md
@@ -10,8 +10,8 @@
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||
</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)
|
||||
|
||||
Core functionalities:
|
||||
@@ -19,6 +19,8 @@ Core functionalities:
|
||||
1. Conversion among various formats.
|
||||
2. Subscription formatting.
|
||||
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
|
||||
|
||||
@@ -29,17 +31,25 @@ Core functionalities:
|
||||
- [x] SSD URI
|
||||
- [x] V2RayN URI
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP)
|
||||
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, SSR(external, only for macOS))
|
||||
- [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
|
||||
|
||||
- [x] QX
|
||||
- [x] Loon
|
||||
- [x] Surge
|
||||
- [x] Stash & Clash
|
||||
- [x] Stash
|
||||
- [x] Clash.Meta
|
||||
- [x] Clash
|
||||
- [x] ShadowRocket
|
||||
- [x] V2Ray
|
||||
- [x] V2Ray URI
|
||||
- [x] Plain JSON
|
||||
|
||||
## 2. Subscription Formatting
|
||||
|
||||
@@ -61,6 +71,7 @@ Core functionalities:
|
||||
- [x] **Regex rename operator**: replace by regex in proxy names.
|
||||
- [x] **Regex delete operator**: delete by regex in proxy names.
|
||||
- [x] **Script operator**: modify proxy by script.
|
||||
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
|
||||
|
||||
### Development
|
||||
|
||||
@@ -88,7 +99,12 @@ This project is under the GPL V3 LICENSE.
|
||||
|
||||
[](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
|
||||
|
||||
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
- Special thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @updated: <%= updated %>
|
||||
* @version: <%= pkg.version %>
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
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",
|
||||
"version": "2.13.4",
|
||||
"version": "2.14.46",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -8,7 +8,9 @@
|
||||
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
||||
"serve": "node sub-store.min.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",
|
||||
"license": "GPL-3.0",
|
||||
@@ -30,7 +32,7 @@
|
||||
"@babel/preset-env": "^7.18.0",
|
||||
"@babel/register": "^7.17.7",
|
||||
"@types/gulp": "^4.0.9",
|
||||
"axios": "^0.20.0",
|
||||
"axios": "^0.21.2",
|
||||
"babel-plugin-relative-path-import": "^2.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-pack-flat": "^3.4.2",
|
||||
@@ -51,4 +53,4 @@
|
||||
"prettier-plugin-sort-imports": "^1.6.1",
|
||||
"tinyify": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8528
backend/pnpm-lock.yaml
generated
8528
backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,5 @@ export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
||||
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
|
||||
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
|
||||
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import download from '@/utils/download';
|
||||
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
@@ -36,7 +36,7 @@ function parse(raw) {
|
||||
if (lastParser) {
|
||||
const [proxy, error] = tryParse(lastParser, line);
|
||||
if (!error) {
|
||||
proxies.push(proxy);
|
||||
proxies.push(lastParse(proxy));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function parse(raw) {
|
||||
for (const parser of PROXY_PARSERS) {
|
||||
const [proxy, error] = tryParse(parser, line);
|
||||
if (!error) {
|
||||
proxies.push(proxy);
|
||||
proxies.push(lastParse(proxy));
|
||||
lastParser = parser;
|
||||
success = true;
|
||||
$.info(`${parser.name} is activated`);
|
||||
@@ -90,8 +90,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
||||
$.error(
|
||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
// skip the script if download failed.
|
||||
continue;
|
||||
throw new Error(`无法下载脚本: ${url}`);
|
||||
}
|
||||
} else {
|
||||
script = content;
|
||||
@@ -137,10 +136,21 @@ function produce(proxies, targetPlatform) {
|
||||
|
||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
let localPort = 10000;
|
||||
return proxies
|
||||
.map((proxy) => {
|
||||
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) {
|
||||
$.error(
|
||||
`Cannot produce proxy: ${JSON.stringify(
|
||||
@@ -182,3 +192,59 @@ function safeMatch(parser, line) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function lastParse(proxy) {
|
||||
if (proxy.type === 'trojan') {
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
if (transportHost) {
|
||||
proxy.sni = transportHost;
|
||||
}
|
||||
}
|
||||
if (!proxy.sni && !isIP(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;
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,19 @@ function URI_SS() {
|
||||
};
|
||||
content = content.split('#')[0]; // strip proxy name
|
||||
// handle IPV4 and IPV6
|
||||
const serverAndPort = content.match(/@([^/]*)(\/|$)/)[1];
|
||||
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
let userInfoStr = Base64.decode(content.split('@')[0]);
|
||||
if (!serverAndPortArray) {
|
||||
content = Base64.decode(content);
|
||||
userInfoStr = content.split('@')[0];
|
||||
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
}
|
||||
const serverAndPort = serverAndPortArray[1];
|
||||
const portIdx = serverAndPort.lastIndexOf(':');
|
||||
proxy.server = serverAndPort.substring(0, portIdx);
|
||||
proxy.port = serverAndPort.substring(portIdx + 1);
|
||||
|
||||
const userInfo = Base64.decode(content.split('@')[0]).split(':');
|
||||
const userInfo = userInfoStr.split(':');
|
||||
proxy.cipher = userInfo[0];
|
||||
proxy.password = userInfo[1];
|
||||
|
||||
@@ -150,7 +157,7 @@ function URI_VMess() {
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split('vmess://')[1];
|
||||
const content = Base64.decode(line);
|
||||
let content = Base64.decode(line);
|
||||
if (/=\s*vmess/.test(content)) {
|
||||
// Quantumult VMess URI format
|
||||
const partitions = content.split(',').map((p) => p.trim());
|
||||
@@ -202,30 +209,94 @@ function URI_VMess() {
|
||||
}
|
||||
return proxy;
|
||||
} else {
|
||||
// V2rayN URI format
|
||||
const params = JSON.parse(content);
|
||||
let params = {};
|
||||
|
||||
try {
|
||||
// V2rayN URI format
|
||||
params = JSON.parse(content);
|
||||
} catch (e) {
|
||||
// Shadowrocket URI format
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
|
||||
content = Base64.decode(base64Line);
|
||||
|
||||
for (const addon of qs.split('&')) {
|
||||
const [key, valueRaw] = addon.split('=');
|
||||
let value = valueRaw;
|
||||
value = decodeURIComponent(valueRaw);
|
||||
if (value.indexOf(',') === -1) {
|
||||
params[key] = value;
|
||||
} else {
|
||||
params[key] = value.split(',');
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [___, cipher, uuid, server, port] =
|
||||
/(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
|
||||
|
||||
params.scy = cipher;
|
||||
params.id = uuid;
|
||||
params.port = port;
|
||||
params.add = server;
|
||||
}
|
||||
const proxy = {
|
||||
name: params.ps,
|
||||
name: params.ps ?? params.remark,
|
||||
type: 'vmess',
|
||||
server: params.add,
|
||||
port: params.port,
|
||||
cipher: 'auto', // V2rayN has no default cipher! use aes-128-gcm as default.
|
||||
port: parseInt(getIfPresent(params.port), 10),
|
||||
cipher: getIfPresent(params.scy, 'auto'),
|
||||
uuid: params.id,
|
||||
alterId: getIfPresent(params.aid, 0),
|
||||
tls: params.tls === 'tls' || params.tls === true,
|
||||
alterId: parseInt(
|
||||
getIfPresent(params.aid ?? params.alterId, 0),
|
||||
10,
|
||||
),
|
||||
tls: ['tls', true, 1, '1'].includes(params.tls),
|
||||
'skip-cert-verify': isPresent(params.verify_cert)
|
||||
? !params.verify_cert
|
||||
: undefined,
|
||||
};
|
||||
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
|
||||
if (proxy.tls && proxy.sni) {
|
||||
proxy.sni = params.sni;
|
||||
}
|
||||
// handle obfs
|
||||
if (params.net === 'ws') {
|
||||
if (params.net === 'ws' || params.obfs === 'websocket') {
|
||||
proxy.network = 'ws';
|
||||
proxy['ws-opts'] = {
|
||||
path: getIfNotBlank(params.path),
|
||||
headers: { Host: getIfNotBlank(params.host) },
|
||||
};
|
||||
if (proxy.tls && params.host) {
|
||||
proxy.sni = params.host;
|
||||
} else if (
|
||||
['tcp', 'http'].includes(params.net) ||
|
||||
params.obfs === 'http'
|
||||
) {
|
||||
proxy.network = 'http';
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = params.host ?? params.obfsParam;
|
||||
let transportPath = params.path;
|
||||
|
||||
if (proxy.network === 'http') {
|
||||
if (transportHost) {
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
}
|
||||
if (transportPath) {
|
||||
transportPath = Array.isArray(transportPath)
|
||||
? transportPath[0]
|
||||
: transportPath;
|
||||
}
|
||||
}
|
||||
if (transportPath || transportHost) {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
path: getIfNotBlank(transportPath),
|
||||
headers: { Host: getIfNotBlank(transportHost) },
|
||||
};
|
||||
} else {
|
||||
delete proxy.network;
|
||||
}
|
||||
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
|
||||
// sni 优先级应高于 host
|
||||
if (proxy.tls && !proxy.sni && transportHost) {
|
||||
proxy.sni = transportHost;
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
@@ -234,6 +305,85 @@ function URI_VMess() {
|
||||
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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
// Trojan URI format
|
||||
function URI_Trojan() {
|
||||
const name = 'URI Trojan Parser';
|
||||
@@ -270,6 +420,10 @@ function Clash_All() {
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'hysteria',
|
||||
'wireguard',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -278,9 +432,19 @@ function Clash_All() {
|
||||
}
|
||||
|
||||
// handle vmess sni
|
||||
if (proxy.type === 'vmess') {
|
||||
if (['vmess', 'vless'].includes(proxy.type)) {
|
||||
proxy.sni = proxy.servername;
|
||||
delete proxy.servername;
|
||||
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;
|
||||
@@ -417,6 +581,113 @@ function Loon_Http() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Loon_WireGuard() {
|
||||
const name = 'Loon WireGuard Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
|
||||
};
|
||||
|
||||
const parse = (line) => {
|
||||
const name = line.match(
|
||||
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
|
||||
)?.[1];
|
||||
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
|
||||
let peers = line.match(
|
||||
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
|
||||
)?.[1];
|
||||
let serverPort = peers.match(
|
||||
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
|
||||
);
|
||||
let server = serverPort?.[2];
|
||||
let port = parseInt(serverPort?.[3], 10);
|
||||
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
|
||||
if (mtu) {
|
||||
mtu = parseInt(mtu, 10);
|
||||
}
|
||||
let keepalive = line.match(
|
||||
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (keepalive) {
|
||||
keepalive = parseInt(keepalive, 10);
|
||||
}
|
||||
let reserved = peers.match(
|
||||
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (reserved) {
|
||||
reserved = JSON.parse(reserved);
|
||||
}
|
||||
|
||||
let dns;
|
||||
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
|
||||
let dnsv6 = line.match(
|
||||
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (dnsv4 || dnsv6) {
|
||||
dns = [];
|
||||
if (dnsv4) {
|
||||
dns.push(dnsv4);
|
||||
}
|
||||
if (dnsv6) {
|
||||
dns.push(dnsv6);
|
||||
}
|
||||
}
|
||||
let allowedIps = peers
|
||||
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
|
||||
?.split(',')
|
||||
.map((i) => i.trim());
|
||||
let preSharedKey = peers.match(
|
||||
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let ip = line.match(
|
||||
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let ipv6 = line.match(
|
||||
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let publicKey = peers.match(
|
||||
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
const proxy = {
|
||||
type: 'wireguard',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
ip,
|
||||
ipv6,
|
||||
'private-key': line.match(
|
||||
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2],
|
||||
'public-key': publicKey,
|
||||
mtu,
|
||||
keepalive,
|
||||
reserved,
|
||||
'allowed-ips': allowedIps,
|
||||
'preshared-key': preSharedKey,
|
||||
dns,
|
||||
udp: true,
|
||||
peers: [
|
||||
{
|
||||
server,
|
||||
port,
|
||||
ip,
|
||||
ipv6,
|
||||
'public-key': publicKey,
|
||||
'pre-shared-key': preSharedKey,
|
||||
allowed_ips: allowedIps,
|
||||
reserved,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
proxy;
|
||||
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
|
||||
proxy['remote-dns-resolve'] = true;
|
||||
}
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_SS() {
|
||||
const name = 'Surge SS Parser';
|
||||
const test = (line) => {
|
||||
@@ -474,10 +745,20 @@ function Surge_Snell() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_Tuic() {
|
||||
const name = 'Surge Tuic Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*tuic(-v5)??/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [
|
||||
URI_SS(),
|
||||
URI_SSR(),
|
||||
URI_VMess(),
|
||||
URI_VLESS(),
|
||||
URI_Trojan(),
|
||||
Clash_All(),
|
||||
Surge_SS(),
|
||||
@@ -485,6 +766,7 @@ export default [
|
||||
Surge_Trojan(),
|
||||
Surge_Http(),
|
||||
Surge_Snell(),
|
||||
Surge_Tuic(),
|
||||
Surge_Socks5(),
|
||||
Loon_SS(),
|
||||
Loon_SSR(),
|
||||
@@ -492,6 +774,7 @@ export default [
|
||||
Loon_Vless(),
|
||||
Loon_Trojan(),
|
||||
Loon_Http(),
|
||||
Loon_WireGuard(),
|
||||
QX_SS(),
|
||||
QX_SSR(),
|
||||
QX_VMess(),
|
||||
|
||||
@@ -25,11 +25,14 @@ const grammars = String.raw`
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@@ -73,6 +76,13 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
@@ -175,6 +185,11 @@ uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" 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(""); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
@@ -23,11 +23,14 @@
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@@ -71,6 +74,13 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
@@ -173,6 +183,11 @@ uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" 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(""); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
@@ -79,7 +79,7 @@ port = digits:[0-9]+ {
|
||||
}
|
||||
}
|
||||
|
||||
params = "?" head:param tail:("&"@param)* {
|
||||
params = "/"? "?" head:param tail:("&"@param)* {
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
|
||||
@@ -87,6 +87,16 @@ params = "?" head:param tail:("&"@param)* {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
|
||||
proxy.udp = toBool(params["udp"]);
|
||||
proxy.tfo = toBool(params["tfo"]);
|
||||
@@ -94,7 +104,7 @@ params = "?" head:param tail:("&"@param)* {
|
||||
|
||||
param = kv/single;
|
||||
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ port = digits:[0-9]+ {
|
||||
}
|
||||
}
|
||||
|
||||
params = "?" head:param tail:("&"@param)* {
|
||||
params = "/"? "?" head:param tail:("&"@param)* {
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
|
||||
@@ -85,6 +85,16 @@ params = "?" head:param tail:("&"@param)* {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
|
||||
proxy.udp = toBool(params["udp"]);
|
||||
proxy.tfo = toBool(params["tfo"]);
|
||||
@@ -92,7 +102,7 @@ params = "?" head:param tail:("&"@param)* {
|
||||
|
||||
param = kv/single;
|
||||
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,22 +95,10 @@ function FullConfig() {
|
||||
return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
const regex = /^\[server_local]|\[Proxy]/gm;
|
||||
const match = regex.exec(raw);
|
||||
const results = [];
|
||||
|
||||
let first = true;
|
||||
if (match) {
|
||||
raw = raw.substring(match.index);
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!first && !line.test(/^\s*\[/)) {
|
||||
results.push(line);
|
||||
}
|
||||
// skip the first line
|
||||
first = false;
|
||||
}
|
||||
return results.join('\n');
|
||||
}
|
||||
const match = raw.match(
|
||||
/^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
|
||||
)?.[1];
|
||||
return match || raw;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import { FULL } from '@/utils/logical';
|
||||
import { getFlag } from '@/utils/geo';
|
||||
@@ -377,6 +378,46 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, 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 }) {
|
||||
@@ -565,7 +606,8 @@ async function ApplyFilter(filter, objs) {
|
||||
selected = await filter.func(objs);
|
||||
} catch (err) {
|
||||
// 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]);
|
||||
}
|
||||
@@ -577,7 +619,8 @@ async function ApplyOperator(operator, objs) {
|
||||
if (output_) output = output_;
|
||||
} catch (err) {
|
||||
// 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;
|
||||
}
|
||||
@@ -633,6 +676,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'$httpClient',
|
||||
'$notification',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
@@ -645,6 +689,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
// eslint-disable-next-line no-undef
|
||||
$notification,
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
);
|
||||
} else {
|
||||
return new Function(
|
||||
@@ -652,7 +697,8 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'$substore',
|
||||
'lodash',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
`${script}\n return ${name}`,
|
||||
)($arguments, $, lodash, ProxyUtils);
|
||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,28 @@ export default function Clash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
// filter unsupported proxies
|
||||
proxies = proxies.filter((proxy) =>
|
||||
['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes(
|
||||
proxy.type,
|
||||
),
|
||||
);
|
||||
proxies = proxies.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'wireguard',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
return false;
|
||||
} else if (
|
||||
proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
@@ -25,8 +42,54 @@ export default function Clash_Producer() {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === 'vless') {
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
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'].includes(proxy.type)) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
|
||||
117
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
117
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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'];
|
||||
}
|
||||
|
||||
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'].includes(proxy.type)) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import Surge_Producer from './surge';
|
||||
import SurgeMac_Producer from './surgemac';
|
||||
import Clash_Producer from './clash';
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import Stash_Producer from './stash';
|
||||
import Loon_Producer from './loon';
|
||||
import URI_Producer from './uri';
|
||||
import V2Ray_Producer from './v2ray';
|
||||
import QX_Producer from './qx';
|
||||
import ShadowRocket_Producer from './shadowrocket';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
@@ -14,9 +18,13 @@ function JSON_Producer() {
|
||||
export default {
|
||||
QX: QX_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
SurgeMac: SurgeMac_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
Clash: Clash_Producer(),
|
||||
ClashMeta: ClashMeta_Producer(),
|
||||
URI: URI_Producer(),
|
||||
V2Ray: V2Ray_Producer(),
|
||||
JSON: JSON_Producer(),
|
||||
Stash: Stash_Producer(),
|
||||
ShadowRocket: ShadowRocket_Producer(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
const targetPlatform = 'Loon';
|
||||
import { isPresent, Result } from './utils';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
|
||||
export default function Loon_Producer() {
|
||||
const produce = (proxy) => {
|
||||
@@ -17,6 +18,8 @@ export default function Loon_Producer() {
|
||||
return vless(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'wireguard':
|
||||
return wireguard(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -98,7 +101,7 @@ function trojan(proxy) {
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -127,9 +130,7 @@ function trojan(proxy) {
|
||||
function vmess(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${
|
||||
proxy.cipher === 'auto' ? 'none' : proxy.cipher
|
||||
},"${proxy.uuid}"`,
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
// transport
|
||||
@@ -137,21 +138,23 @@ function vmess(proxy) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts'].path}`,
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['http-opts'].path}`,
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['http-opts'].headers.Host}`,
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -189,6 +192,9 @@ function vmess(proxy) {
|
||||
}
|
||||
|
||||
function vless(proxy) {
|
||||
if (proxy['reality-opts']) {
|
||||
throw new Error(`reality is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||
@@ -199,21 +205,23 @@ function vless(proxy) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts'].path}`,
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['http-opts'].path}`,
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['http-opts'].headers.Host}`,
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -266,3 +274,63 @@ function http(proxy) {
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function wireguard(proxy) {
|
||||
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
|
||||
proxy.server = proxy.peers[0].server;
|
||||
proxy.port = proxy.peers[0].port;
|
||||
proxy.ip = proxy.peers[0].ip;
|
||||
proxy.ipv6 = proxy.peers[0].ipv6;
|
||||
proxy['public-key'] = proxy.peers[0]['public-key'];
|
||||
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
||||
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
|
||||
proxy.reserved = proxy.peers[0].reserved;
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=wireguard`);
|
||||
|
||||
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
|
||||
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,private-key="${proxy['private-key']}"`,
|
||||
'private-key',
|
||||
);
|
||||
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
|
||||
|
||||
if (proxy.dns) {
|
||||
if (Array.isArray(proxy.dns)) {
|
||||
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
||||
proxy.dns = proxy.dns.find((i) => isIPv4(i));
|
||||
}
|
||||
}
|
||||
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
|
||||
result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
|
||||
result.appendIfPresent(
|
||||
`,keepalive=${proxy['persistent-keepalive']}`,
|
||||
'persistent-keepalive',
|
||||
);
|
||||
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
|
||||
const allowedIps = Array.isArray(proxy['allowed-ips'])
|
||||
? proxy['allowed-ips'].join(',')
|
||||
: proxy['allowed-ips'];
|
||||
let reserved = Array.isArray(proxy.reserved)
|
||||
? proxy.reserved.join(',')
|
||||
: proxy.reserved;
|
||||
if (reserved) {
|
||||
reserved = `,reserved=[${reserved}]`;
|
||||
}
|
||||
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
if (presharedKey) {
|
||||
presharedKey = `,preshared-key="${presharedKey}"`;
|
||||
}
|
||||
result.append(
|
||||
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
|
||||
allowedIps ?? '0.0.0.0/0,::/0'
|
||||
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
|
||||
presharedKey ?? ''
|
||||
}}]`,
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -62,18 +62,20 @@ function shadowsocks(proxy) {
|
||||
);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -133,11 +135,11 @@ function trojan(proxy) {
|
||||
if (needTls(proxy)) append(`,obfs=wss`);
|
||||
else append(`,obfs=ws`);
|
||||
appendIfPresent(
|
||||
`,obfs-uri=${proxy['ws-opts'].path}`,
|
||||
`,obfs-uri=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -150,18 +152,20 @@ function trojan(proxy) {
|
||||
append(`,over-tls=true`);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -206,12 +210,18 @@ function vmess(proxy) {
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
appendIfPresent(
|
||||
`,obfs-uri=${proxy[`${proxy.network}-opts`].path}`,
|
||||
`,obfs-uri=${
|
||||
Array.isArray(transportPath) ? transportPath[0] : transportPath
|
||||
}`,
|
||||
`${proxy.network}-opts.path`,
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`,
|
||||
`,obfs-host=${
|
||||
Array.isArray(transportHost) ? transportHost[0] : transportHost
|
||||
}`,
|
||||
`${proxy.network}-opts.headers.Host`,
|
||||
);
|
||||
} else {
|
||||
@@ -219,18 +229,20 @@ function vmess(proxy) {
|
||||
if (proxy.tls) append(`,obfs=over-tls`);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
@@ -266,18 +278,20 @@ function http(proxy) {
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -306,18 +320,20 @@ function socks5(proxy) {
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -332,11 +348,5 @@ function socks5(proxy) {
|
||||
}
|
||||
|
||||
function needTls(proxy) {
|
||||
return (
|
||||
proxy.tls ||
|
||||
proxy.sni ||
|
||||
typeof proxy['skip-cert-verify'] !== 'undefined' ||
|
||||
typeof proxy['tls-fingerprint'] !== 'undefined' ||
|
||||
typeof proxy['tls-host'] !== 'undefined'
|
||||
);
|
||||
return proxy.tls;
|
||||
}
|
||||
|
||||
122
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
122
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function ShadowRocket_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
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'].includes(proxy.type)) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -6,6 +6,28 @@ export default function Stash_Producer() {
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'wireguard',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
@@ -19,8 +41,88 @@ export default function Stash_Producer() {
|
||||
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'].includes(proxy.type)) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
|
||||
@@ -4,6 +4,14 @@ import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'Surge';
|
||||
|
||||
const ipVersions = {
|
||||
dual: 'dual',
|
||||
ipv4: 'v4-only',
|
||||
ipv6: 'v6-only',
|
||||
'ipv4-prefer': 'prefer-v4',
|
||||
'ipv6-prefer': 'prefer-v6',
|
||||
};
|
||||
|
||||
export default function Surge_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
@@ -19,6 +27,8 @@ export default function Surge_Producer() {
|
||||
return socks5(proxy);
|
||||
case 'snell':
|
||||
return snell(proxy);
|
||||
case 'tuic':
|
||||
return tuic(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -55,6 +65,10 @@ function shadowsocks(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -87,6 +101,10 @@ function trojan(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -127,6 +145,9 @@ function vmess(proxy) {
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -155,6 +176,10 @@ function http(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -185,6 +210,10 @@ function socks5(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -196,20 +225,67 @@ function snell(proxy) {
|
||||
|
||||
// obfs
|
||||
result.appendIfPresent(
|
||||
`,obfs=${proxy['obfs-opts'].mode}`,
|
||||
`,obfs=${proxy['obfs-opts']?.mode}`,
|
||||
'obfs-opts.mode',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,obfs-host=${proxy['obfs-opts'].host}`,
|
||||
`,obfs-host=${proxy['obfs-opts']?.host}`,
|
||||
'obfs-opts.host',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,obfs-uri=${proxy['obfs-opts'].path}`,
|
||||
`,obfs-uri=${proxy['obfs-opts']?.path}`,
|
||||
'obfs-opts.path',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// reuse
|
||||
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function tuic(proxy) {
|
||||
const result = new Result(proxy);
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
let type = proxy.type;
|
||||
if (!proxy.token || proxy.token.length === 0) {
|
||||
type = 'tuic-v5';
|
||||
}
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,token=${proxy.token}`, 'token');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
|
||||
'alpn',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy['fast-open']}`, 'fast-open');
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -225,7 +301,13 @@ function handleTransport(result, proxy) {
|
||||
if (isPresent(proxy, 'ws-opts.headers')) {
|
||||
const headers = proxy['ws-opts'].headers;
|
||||
const value = Object.keys(headers)
|
||||
.map((k) => `${k}:${headers[k]}`)
|
||||
.map((k) => {
|
||||
let v = headers[k];
|
||||
if (['Host'].includes(k)) {
|
||||
v = `"${v}"`;
|
||||
}
|
||||
return `${k}:${v}`;
|
||||
})
|
||||
.join('|');
|
||||
if (isNotBlank(value)) {
|
||||
result.append(`,ws-headers=${value}`);
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -55,27 +55,156 @@ export default function URI_Producer() {
|
||||
break;
|
||||
case 'vmess':
|
||||
// V2RayN URI format
|
||||
let type = '';
|
||||
let net = proxy.network || 'tcp';
|
||||
if (proxy.network === 'http') {
|
||||
net = 'tcp';
|
||||
type = 'http';
|
||||
}
|
||||
result = {
|
||||
v: '2',
|
||||
ps: proxy.name,
|
||||
add: proxy.server,
|
||||
port: proxy.port,
|
||||
id: proxy.uuid,
|
||||
type: '',
|
||||
type,
|
||||
aid: 0,
|
||||
net: proxy.network || 'tcp',
|
||||
net,
|
||||
tls: proxy.tls ? 'tls' : '',
|
||||
};
|
||||
if (proxy.tls && proxy.sni) {
|
||||
result.sni = proxy.sni;
|
||||
}
|
||||
// obfs
|
||||
if (proxy.network === 'ws') {
|
||||
result.path = proxy['ws-opts'].path || '/';
|
||||
result.host = proxy['ws-opts'].headers.Host || proxy.server;
|
||||
if (proxy.network) {
|
||||
let vmessTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let vmessTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (vmessTransportPath) {
|
||||
result.path = Array.isArray(vmessTransportPath)
|
||||
? vmessTransportPath[0]
|
||||
: vmessTransportPath;
|
||||
}
|
||||
if (vmessTransportHost) {
|
||||
result.host = Array.isArray(vmessTransportHost)
|
||||
? vmessTransportHost[0]
|
||||
: vmessTransportHost;
|
||||
}
|
||||
}
|
||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||
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,
|
||||
)}`;
|
||||
|
||||
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':
|
||||
let trojanTransport = '';
|
||||
if (proxy.network) {
|
||||
trojanTransport = `&type=${proxy.network}`;
|
||||
let trojanTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let trojanTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (trojanTransportPath) {
|
||||
trojanTransport += `&path=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportPath)
|
||||
? trojanTransportPath[0]
|
||||
: trojanTransportPath,
|
||||
)}`;
|
||||
}
|
||||
if (trojanTransportHost) {
|
||||
trojanTransport += `&host=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportHost)
|
||||
? trojanTransportHost[0]
|
||||
: trojanTransportHost,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
result = `trojan://${proxy.password}@${proxy.server}:${
|
||||
proxy.port
|
||||
}#${encodeURIComponent(proxy.name)}`;
|
||||
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
||||
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
||||
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
|
||||
12
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
12
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import URI_Producer from './uri';
|
||||
|
||||
const URI = URI_Producer();
|
||||
|
||||
export default function V2Ray_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) =>
|
||||
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function AllRuleParser() {
|
||||
}
|
||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
||||
* @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
|
||||
*/
|
||||
import { version } from '../package.json';
|
||||
|
||||
@@ -19,7 +19,10 @@ export default function register($app) {
|
||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||
|
||||
// RESTful APIs
|
||||
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
|
||||
$app.route('/api/artifacts')
|
||||
.get(getAllArtifacts)
|
||||
.post(createArtifact)
|
||||
.put(replaceArtifact);
|
||||
|
||||
$app.route('/api/artifact/:name')
|
||||
.get(getArtifact)
|
||||
@@ -32,6 +35,12 @@ function getAllArtifacts(req, res) {
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
function replaceArtifact(req, res) {
|
||||
const allArtifacts = req.body;
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function getArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
@@ -131,7 +140,12 @@ async function deleteArtifact(req, res) {
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
await syncToGist(files);
|
||||
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
||||
try {
|
||||
await syncToGist(files);
|
||||
} catch (i) {
|
||||
$.error(`Function syncToGist: ${name} : ${i}`);
|
||||
}
|
||||
}
|
||||
// delete local cache
|
||||
deleteByName(allArtifacts, name);
|
||||
|
||||
@@ -14,7 +14,8 @@ export default function register($app) {
|
||||
|
||||
$app.route('/api/collections')
|
||||
.get(getAllCollections)
|
||||
.post(createCollection);
|
||||
.post(createCollection)
|
||||
.put(replaceCollection);
|
||||
}
|
||||
|
||||
// collection API
|
||||
@@ -111,3 +112,9 @@ function getAllCollections(req, res) {
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
success(res, allCols);
|
||||
}
|
||||
|
||||
function replaceCollection(req, res) {
|
||||
const allCols = req.body;
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,18 @@ async function downloadSubscription(req, res) {
|
||||
});
|
||||
|
||||
if (sub.source !== 'local') {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
try {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||||
err,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,15 +58,15 @@ async function downloadSubscription(req, res) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载订阅失败`,
|
||||
`❌ 无法下载订阅:${name}!`,
|
||||
`🤔 原因:${JSON.stringify(err)}`,
|
||||
`🤔 原因:${err.message ?? err}`,
|
||||
);
|
||||
$.error(JSON.stringify(err));
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`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) {
|
||||
const sub = findByName(allSubs, subnames[0]);
|
||||
if (sub.source !== 'local') {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
try {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
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(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download collection: ${name}`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,8 +125,10 @@ async function gistBackup(req, res) {
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
// perform migration after restoring from gist
|
||||
$.info(`perform migration after restoring from gist...`);
|
||||
migrate();
|
||||
$.info(`migration completed`);
|
||||
$.info(`还原备份完成`);
|
||||
break;
|
||||
}
|
||||
success(res);
|
||||
@@ -136,7 +138,7 @@ async function gistBackup(req, res) {
|
||||
new InternalServerError(
|
||||
'BACKUP_FAILED',
|
||||
`Failed to ${action} data to gist!`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,12 @@ export async function updateGitHubAvatar() {
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (e) {
|
||||
$.error('Failed to fetch GitHub avatar for User: ' + username);
|
||||
} catch (err) {
|
||||
$.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);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error('Failed to fetch artifact store for User: ' + githubUser);
|
||||
$.error(
|
||||
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ export default function register($app) {
|
||||
.patch(updateSubscription)
|
||||
.delete(deleteSubscription);
|
||||
|
||||
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
|
||||
$app.route('/api/subs')
|
||||
.get(getAllSubscriptions)
|
||||
.post(createSubscription)
|
||||
.put(replaceSubscriptions);
|
||||
}
|
||||
|
||||
// subscriptions API
|
||||
@@ -66,8 +69,12 @@ async function getFlowInfo(req, res) {
|
||||
}
|
||||
|
||||
// unit is KB
|
||||
const upload = Number(flowHeaders.match(/upload=(\d+)/)[1]);
|
||||
const download = Number(flowHeaders.match(/download=(\d+)/)[1]);
|
||||
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
|
||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||
|
||||
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
|
||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||
|
||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
||||
|
||||
// optional expire timestamp
|
||||
@@ -198,3 +205,9 @@ function getAllSubscriptions(req, res) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
success(res, allSubs);
|
||||
}
|
||||
|
||||
function replaceSubscriptions(req, res) {
|
||||
const allSubs = req.body;
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ export default function register($app) {
|
||||
async function produceArtifact({ type, name, platform }) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
// produce Clash node format for ShadowRocket
|
||||
if (platform === 'ShadowRocket') platform = 'Clash';
|
||||
|
||||
if (type === 'subscription') {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
@@ -45,6 +42,9 @@ async function produceArtifact({ type, name, platform }) {
|
||||
sub.process || [],
|
||||
platform,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
@@ -70,6 +70,7 @@ async function produceArtifact({ type, name, platform }) {
|
||||
const collection = findByName(allCols, name);
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
const errors = {};
|
||||
let processed = 0;
|
||||
|
||||
await Promise.all(
|
||||
@@ -100,6 +101,7 @@ async function produceArtifact({ type, name, platform }) {
|
||||
);
|
||||
} catch (err) {
|
||||
processed++;
|
||||
errors[name] = err;
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
@@ -111,10 +113,18 @@ 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
|
||||
let proxies = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name]),
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
// apply own processors
|
||||
@@ -124,7 +134,7 @@ async function produceArtifact({ type, name, platform }) {
|
||||
platform,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`组合订阅中不含有效节点!`);
|
||||
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
@@ -245,20 +255,20 @@ async function syncArtifact(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
|
||||
$.info(
|
||||
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
||||
artifact,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
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({
|
||||
[encodeURIComponent(artifact.name)]: {
|
||||
content: output,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(url, ua) {
|
||||
const { isNode } = ENV();
|
||||
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||
const id = hex_md5(ua + url);
|
||||
if (tasks.has(id)) {
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
|
||||
@@ -39,6 +40,8 @@ export default async function download(url, ua) {
|
||||
}
|
||||
});
|
||||
|
||||
tasks.set(id, result);
|
||||
if (!isNode) {
|
||||
tasks.set(id, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function getFlag(name) {
|
||||
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
|
||||
'🇲🇹': ['Malta', '马耳他'],
|
||||
'🇲🇽': ['Mexico', '墨西哥'],
|
||||
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'],
|
||||
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
||||
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
|
||||
'🇳🇴': ['Norway', '挪威'],
|
||||
'🇳🇵': ['Nepal', '尼泊尔'],
|
||||
|
||||
@@ -40,6 +40,10 @@ export default class Gist {
|
||||
}
|
||||
|
||||
async upload(files) {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const id = await this.locate();
|
||||
|
||||
if (id === -1) {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
let ua = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
UA = headers[k];
|
||||
ua = UA.toLowerCase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surge Mac') !== -1) {
|
||||
return 'SurgeMac';
|
||||
} else if (UA.indexOf('Surge') !== -1) {
|
||||
return 'Surge';
|
||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||
@@ -17,6 +21,15 @@ export function getPlatformFromHeaders(headers) {
|
||||
return 'ShadowRocket';
|
||||
} else if (UA.indexOf('Stash') !== -1) {
|
||||
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 {
|
||||
return 'JSON';
|
||||
}
|
||||
|
||||
@@ -16,8 +16,13 @@ class ResourceCache {
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
const [id, updated] = entry;
|
||||
if (new Date().getTime() - updated > this.expires) {
|
||||
if (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
@@ -26,9 +31,6 @@ class ResourceCache {
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
Object.keys(this.resourceCache).forEach((id) => {
|
||||
$.delete(`#${id}`);
|
||||
});
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
@@ -38,17 +40,16 @@ class ResourceCache {
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id];
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return $.read(`#${id}`);
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = new Date().getTime();
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
$.write(value, `#${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
backend/src/utils/script-resource-cache.js
Normal file
105
backend/src/utils/script-resource-cache.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
SCRIPT_RESOURCE_CACHE_KEY,
|
||||
CSR_EXPIRATION_TIME_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
class ResourceCache {
|
||||
constructor() {
|
||||
this.expires = getExpiredTime();
|
||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
// clear obsolete cached resource
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
const [id, updated] = entry;
|
||||
if (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
gettime(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].time;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
function getExpiredTime() {
|
||||
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
|
||||
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
|
||||
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
|
||||
}
|
||||
let expiration = 1728e5;
|
||||
if ($.env.isLoon) {
|
||||
const loont = {
|
||||
// Loon 插件自义定
|
||||
'1\u5206\u949f': 6e4,
|
||||
'5\u5206\u949f': 3e5,
|
||||
'10\u5206\u949f': 6e5,
|
||||
'30\u5206\u949f': 18e5, // "30分钟"
|
||||
'1\u5c0f\u65f6': 36e5,
|
||||
'2\u5c0f\u65f6': 72e5,
|
||||
'3\u5c0f\u65f6': 108e5,
|
||||
'6\u5c0f\u65f6': 216e5,
|
||||
'12\u5c0f\u65f6': 432e5,
|
||||
'24\u5c0f\u65f6': 864e5,
|
||||
'48\u5c0f\u65f6': 1728e5,
|
||||
'72\u5c0f\u65f6': 2592e5, // "72小时"
|
||||
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
|
||||
};
|
||||
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
|
||||
// console.log(intimed);
|
||||
if (intimed in loont) {
|
||||
expiration = loont[intimed];
|
||||
if (expiration === 'readcachets') {
|
||||
expiration = intimed;
|
||||
}
|
||||
}
|
||||
return expiration;
|
||||
} else {
|
||||
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache();
|
||||
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 bodyParser = eval(`require("body-parser")`);
|
||||
const app = express_();
|
||||
app.use(bodyParser.json({ verify: rawBodySaver }));
|
||||
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
|
||||
app.use(
|
||||
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
||||
);
|
||||
|
||||
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,14 +2,18 @@
|
||||
#!desc=高级订阅管理工具
|
||||
#!openUrl=https://sub.store
|
||||
#!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
|
||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||
|
||||
[Rule]
|
||||
DOMAIN,sub-store.vercel.app,PROXY
|
||||
|
||||
[MITM]
|
||||
hostname=sub.store
|
||||
|
||||
[Script]
|
||||
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
|
||||
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
|
||||
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
|
||||
|
||||
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
|
||||
@@ -1,20 +1,32 @@
|
||||
# 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
|
||||
安装使用[插件](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
|
||||
安装使用[模块](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
|
||||
订阅[重写](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
|
||||
安装使用[ 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
|
||||
安装使用[模块](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
|
||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||
|
||||
12
config/Surge-Noability.sgmodule
Normal file
12
config/Surge-Noability.sgmodule
Normal file
@@ -0,0 +1,12 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 则可以使用此脚本
|
||||
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
|
||||
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
11
config/Surge-ability.sgmodule
Normal file
11
config/Surge-ability.sgmodule
Normal file
@@ -0,0 +1,11 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
|
||||
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
@@ -1,10 +1,11 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[MITM]
|
||||
hostname=%APPEND% sub.store
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
|
||||
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
|
||||
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
|
||||
@@ -17,8 +17,13 @@ class ResourceCache {
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
const [id, updated] = entry;
|
||||
if (new Date().getTime() - updated > this.expires) {
|
||||
if (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
@@ -27,9 +32,6 @@ class ResourceCache {
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
Object.keys(this.resourceCache).forEach((id) => {
|
||||
$.delete(`#${id}`);
|
||||
});
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
@@ -39,17 +41,16 @@ class ResourceCache {
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id];
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return $.read(`#${id}`);
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = new Date().getTime();
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value }
|
||||
this._persist();
|
||||
$.write(value, `#${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user