mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c943176a5 | ||
|
|
f4c4cdba67 | ||
|
|
ada03be05f | ||
|
|
5584225413 | ||
|
|
5cbcf4fce4 | ||
|
|
89931c0032 | ||
|
|
88f3198320 | ||
|
|
27a14bb255 | ||
|
|
5ecce27f4e | ||
|
|
12903d77f7 | ||
|
|
0c6ec7f82a | ||
|
|
ba251ced34 | ||
|
|
d96a0421f7 | ||
|
|
aff7ddf41e | ||
|
|
164ae9a7a8 | ||
|
|
3aacd26b79 | ||
|
|
5915416232 | ||
|
|
c059296224 | ||
|
|
9ae70eca09 | ||
|
|
d0acf49b83 | ||
|
|
c51f3511dd | ||
|
|
ee2fcc7ee3 | ||
|
|
95615d1877 | ||
|
|
962bcda9dd | ||
|
|
9bb4739d56 | ||
|
|
de1d40f41a | ||
|
|
c0ab301160 | ||
|
|
a22df97a51 | ||
|
|
45772ade4d | ||
|
|
e8dab545f5 | ||
|
|
c2bd80207a | ||
|
|
bc5ae9a2ef | ||
|
|
36db057e32 | ||
|
|
5ac73b863a | ||
|
|
23042c33d6 | ||
|
|
4ca5f5e355 | ||
|
|
f10e5913fb | ||
|
|
8b75c11587 | ||
|
|
c287dcad3b | ||
|
|
ce6cd794c8 | ||
|
|
e05475aa5e | ||
|
|
c35e9d37ae | ||
|
|
8f2dbfe3df | ||
|
|
a0a998dfdd | ||
|
|
12491ac7c0 |
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -37,7 +37,6 @@ jobs:
|
|||||||
- name: Bundle
|
- name: Bundle
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
pnpm i -D estrella
|
|
||||||
pnpm run bundle
|
pnpm run bundle
|
||||||
- id: tag
|
- id: tag
|
||||||
name: Generate release tag
|
name: Generate release tag
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center" color="#6a737d">
|
<p align="center" color="#6a737d">
|
||||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||||
@@ -31,23 +31,25 @@ Core functionalities:
|
|||||||
- [x] SSD URI
|
- [x] SSD URI
|
||||||
- [x] V2RayN URI
|
- [x] V2RayN URI
|
||||||
- [x] Hysteria2 URI
|
- [x] Hysteria2 URI
|
||||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5)
|
||||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
|
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2)
|
||||||
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
||||||
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
|
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
|
||||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
|
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
|
||||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
|
||||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
|
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)
|
||||||
|
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||||
|
|
||||||
### Supported Target Platforms
|
### Supported Target Platforms
|
||||||
|
|
||||||
- [x] QX
|
- [x] QX
|
||||||
- [x] Loon
|
- [x] Loon
|
||||||
- [x] Surge
|
- [x] Surge
|
||||||
|
- [x] Surfboard
|
||||||
- [x] Stash
|
- [x] Stash
|
||||||
- [x] Clash.Meta
|
- [x] Clash.Meta
|
||||||
- [x] Clash
|
- [x] Clash
|
||||||
- [x] ShadowRocket
|
- [x] Shadowrocket
|
||||||
- [x] V2Ray
|
- [x] V2Ray
|
||||||
- [x] V2Ray URI
|
- [x] V2Ray URI
|
||||||
- [x] Plain JSON
|
- [x] Plain JSON
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.14.122",
|
"version": "2.14.166",
|
||||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"automerge": "1.0.1-preview.7",
|
"automerge": "1.0.1-preview.7",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"connect-history-api-fallback": "^2.0.0",
|
"connect-history-api-fallback": "^2.0.0",
|
||||||
|
"cron": "^3.1.6",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"js-base64": "^3.7.2",
|
"js-base64": "^3.7.2",
|
||||||
|
|||||||
32
backend/pnpm-lock.yaml
generated
32
backend/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ dependencies:
|
|||||||
connect-history-api-fallback:
|
connect-history-api-fallback:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: registry.npmmirror.com/connect-history-api-fallback@2.0.0
|
version: registry.npmmirror.com/connect-history-api-fallback@2.0.0
|
||||||
|
cron:
|
||||||
|
specifier: ^3.1.6
|
||||||
|
version: registry.npmmirror.com/cron@3.1.6
|
||||||
express:
|
express:
|
||||||
specifier: ^4.17.1
|
specifier: ^4.17.1
|
||||||
version: registry.npmmirror.com/express@4.17.1
|
version: registry.npmmirror.com/express@4.17.1
|
||||||
@@ -162,11 +165,6 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/source-map@0.6.1:
|
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
registry.npmmirror.com/@ampproject/remapping@2.2.0:
|
registry.npmmirror.com/@ampproject/remapping@2.2.0:
|
||||||
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz}
|
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz}
|
||||||
name: '@ampproject/remapping'
|
name: '@ampproject/remapping'
|
||||||
@@ -2056,6 +2054,12 @@ packages:
|
|||||||
'@types/node': registry.npmmirror.com/@types/node@17.0.35
|
'@types/node': registry.npmmirror.com/@types/node@17.0.35
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/luxon@3.3.8:
|
||||||
|
resolution: {integrity: sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/luxon/-/luxon-3.3.8.tgz}
|
||||||
|
name: '@types/luxon'
|
||||||
|
version: 3.3.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/@types/minimatch@3.0.5:
|
registry.npmmirror.com/@types/minimatch@3.0.5:
|
||||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz}
|
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz}
|
||||||
name: '@types/minimatch'
|
name: '@types/minimatch'
|
||||||
@@ -3696,6 +3700,15 @@ packages:
|
|||||||
sha.js: registry.npmmirror.com/sha.js@2.4.11
|
sha.js: registry.npmmirror.com/sha.js@2.4.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/cron@3.1.6:
|
||||||
|
resolution: {integrity: sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cron/-/cron-3.1.6.tgz}
|
||||||
|
name: cron
|
||||||
|
version: 3.1.6
|
||||||
|
dependencies:
|
||||||
|
'@types/luxon': registry.npmmirror.com/@types/luxon@3.3.8
|
||||||
|
luxon: registry.npmmirror.com/luxon@3.4.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/cross-spawn@7.0.3:
|
registry.npmmirror.com/cross-spawn@7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz}
|
||||||
name: cross-spawn
|
name: cross-spawn
|
||||||
@@ -6851,6 +6864,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: registry.npmmirror.com/yallist@4.0.0
|
yallist: registry.npmmirror.com/yallist@4.0.0
|
||||||
|
|
||||||
|
registry.npmmirror.com/luxon@3.4.4:
|
||||||
|
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/luxon/-/luxon-3.4.4.tgz}
|
||||||
|
name: luxon
|
||||||
|
version: 3.4.4
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/magic-string@0.23.2:
|
registry.npmmirror.com/magic-string@0.23.2:
|
||||||
resolution: {integrity: sha512-oIUZaAxbcxYIp4AyLafV6OVKoB3YouZs0UTCJ8mOKBHNyJgGDaMJ4TgA+VylJh6fx7EQCC52XkbURxxG9IoJXA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/magic-string/-/magic-string-0.23.2.tgz}
|
resolution: {integrity: sha512-oIUZaAxbcxYIp4AyLafV6OVKoB3YouZs0UTCJ8mOKBHNyJgGDaMJ4TgA+VylJh6fx7EQCC52XkbURxxG9IoJXA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/magic-string/-/magic-string-0.23.2.tgz}
|
||||||
name: magic-string
|
name: magic-string
|
||||||
@@ -9320,7 +9340,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn: registry.npmmirror.com/acorn@8.7.1
|
acorn: registry.npmmirror.com/acorn@8.7.1
|
||||||
commander: registry.npmmirror.com/commander@2.20.3
|
commander: registry.npmmirror.com/commander@2.20.3
|
||||||
source-map: 0.6.1
|
source-map: registry.npmmirror.com/source-map@0.6.1
|
||||||
source-map-support: registry.npmmirror.com/source-map-support@0.5.21
|
source-map-support: registry.npmmirror.com/source-map-support@0.5.21
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
|||||||
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||||
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||||
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
||||||
|
export const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource';
|
||||||
|
export const CHR_EXPIRATION_TIME_KEY = '#sub-store-chr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 1 min
|
||||||
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
|
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 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
|
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import YAML from 'static-js-yaml';
|
||||||
import download from '@/utils/download';
|
import download from '@/utils/download';
|
||||||
import { isIPv4, isIPv6 } from '@/utils';
|
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
|
||||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||||
import PROXY_PREPROCESSORS from './preprocessors';
|
import PROXY_PREPROCESSORS from './preprocessors';
|
||||||
import PROXY_PRODUCERS from './producers';
|
import PROXY_PRODUCERS from './producers';
|
||||||
@@ -59,7 +60,6 @@ function parse(raw) {
|
|||||||
$.error(`Failed to parse line: ${line}`);
|
$.error(`Failed to parse line: ${line}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies;
|
return proxies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
return proxies;
|
return proxies;
|
||||||
}
|
}
|
||||||
|
|
||||||
function produce(proxies, targetPlatform, type) {
|
function produce(proxies, targetPlatform, type, opts = {}) {
|
||||||
const producer = PROXY_PRODUCERS[targetPlatform];
|
const producer = PROXY_PRODUCERS[targetPlatform];
|
||||||
if (!producer) {
|
if (!producer) {
|
||||||
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
||||||
@@ -154,10 +154,10 @@ function produce(proxies, targetPlatform, type) {
|
|||||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||||
let localPort = 10000;
|
let localPort = 10000;
|
||||||
return proxies
|
const list = proxies
|
||||||
.map((proxy) => {
|
.map((proxy) => {
|
||||||
try {
|
try {
|
||||||
let line = producer.produce(proxy, type);
|
let line = producer.produce(proxy, type, opts);
|
||||||
if (
|
if (
|
||||||
line.length > 0 &&
|
line.length > 0 &&
|
||||||
line.includes('__SubStoreLocalPort__')
|
line.includes('__SubStoreLocalPort__')
|
||||||
@@ -179,10 +179,10 @@ function produce(proxies, targetPlatform, type) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0);
|
||||||
.join('\n');
|
return type === 'internal' ? list : list.join('\n');
|
||||||
} else if (producer.type === 'ALL') {
|
} else if (producer.type === 'ALL') {
|
||||||
return producer.produce(proxies, type);
|
return producer.produce(proxies, type, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +193,7 @@ export const ProxyUtils = {
|
|||||||
isIPv4,
|
isIPv4,
|
||||||
isIPv6,
|
isIPv6,
|
||||||
isIP,
|
isIP,
|
||||||
|
yaml: YAML,
|
||||||
};
|
};
|
||||||
|
|
||||||
function tryParse(parser, line) {
|
function tryParse(parser, line) {
|
||||||
@@ -214,8 +215,11 @@ function safeMatch(parser, line) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function lastParse(proxy) {
|
function lastParse(proxy) {
|
||||||
|
if (isValidPortNumber(proxy.port)) {
|
||||||
|
proxy.port = parseInt(proxy.port, 10);
|
||||||
|
}
|
||||||
if (proxy.server) {
|
if (proxy.server) {
|
||||||
proxy.server = proxy.server
|
proxy.server = `${proxy.server}`
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^\[/, '')
|
.replace(/^\[/, '')
|
||||||
.replace(/\]$/, '');
|
.replace(/\]$/, '');
|
||||||
@@ -276,6 +280,9 @@ function lastParse(proxy) {
|
|||||||
proxy[`${proxy.network}-opts`].path = [transportPath];
|
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
|
||||||
|
delete proxy.ports;
|
||||||
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -331,12 +331,27 @@ function URI_VLESS() {
|
|||||||
};
|
};
|
||||||
const parse = (line) => {
|
const parse = (line) => {
|
||||||
line = line.split('vless://')[1];
|
line = line.split('vless://')[1];
|
||||||
|
let isShadowrocket;
|
||||||
|
let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||||
|
if (!parsed) {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line);
|
||||||
|
line = `${Base64.decode(base64)}${other}`;
|
||||||
|
parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||||
|
isShadowrocket = true;
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let [__, uuid, server, port, addons, name] =
|
let [__, uuid, server, port, ___, addons = '', name] = parsed;
|
||||||
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
if (isShadowrocket) {
|
||||||
|
uuid = uuid.replace(/^.*?:/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
port = parseInt(`${port}`, 10);
|
port = parseInt(`${port}`, 10);
|
||||||
uuid = decodeURIComponent(uuid);
|
uuid = decodeURIComponent(uuid);
|
||||||
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
|
if (name != null) {
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
}
|
||||||
|
|
||||||
const proxy = {
|
const proxy = {
|
||||||
type: 'vless',
|
type: 'vless',
|
||||||
name,
|
name,
|
||||||
@@ -352,9 +367,24 @@ function URI_VLESS() {
|
|||||||
params[key] = value;
|
params[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxy.name = name ?? params.remarks ?? `VLESS ${server}:${port}`;
|
||||||
|
|
||||||
proxy.tls = params.security && params.security !== 'none';
|
proxy.tls = params.security && params.security !== 'none';
|
||||||
proxy.sni = params.sni;
|
if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
|
||||||
|
proxy.tls = true;
|
||||||
|
params.security = params.security ?? 'reality';
|
||||||
|
}
|
||||||
|
proxy.sni = params.sni ?? params.peer;
|
||||||
proxy.flow = params.flow;
|
proxy.flow = params.flow;
|
||||||
|
if (!proxy.flow && isShadowrocket && params.xtls) {
|
||||||
|
// "none" is undefined
|
||||||
|
const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][
|
||||||
|
params.xtls
|
||||||
|
];
|
||||||
|
if (flow) {
|
||||||
|
proxy.flow = flow;
|
||||||
|
}
|
||||||
|
}
|
||||||
proxy['client-fingerprint'] = params.fp;
|
proxy['client-fingerprint'] = params.fp;
|
||||||
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
|
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
|
||||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
|
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
|
||||||
@@ -368,21 +398,28 @@ function URI_VLESS() {
|
|||||||
opts['short-id'] = params.sid;
|
opts['short-id'] = params.sid;
|
||||||
}
|
}
|
||||||
if (Object.keys(opts).length > 0) {
|
if (Object.keys(opts).length > 0) {
|
||||||
|
// proxy[`${params.security}-opts`] = opts;
|
||||||
proxy[`${params.security}-opts`] = opts;
|
proxy[`${params.security}-opts`] = opts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.network = params.type;
|
proxy.network = params.type;
|
||||||
|
if (!proxy.network && isShadowrocket && params.obfs) {
|
||||||
|
proxy.network = params.obfs;
|
||||||
|
}
|
||||||
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
||||||
const opts = {};
|
const opts = {};
|
||||||
if (params.path) {
|
|
||||||
opts.path = params.path;
|
|
||||||
}
|
|
||||||
if (params.host) {
|
if (params.host) {
|
||||||
opts.headers = { Host: params.host };
|
opts.headers = { Host: params.host };
|
||||||
}
|
}
|
||||||
if (params.serviceName) {
|
if (params.serviceName) {
|
||||||
opts[`${proxy.network}-service-name`] = params.serviceName;
|
opts[`${proxy.network}-service-name`] = params.serviceName;
|
||||||
|
} else if (isShadowrocket && params.path) {
|
||||||
|
opts[`${proxy.network}-service-name`] = params.path;
|
||||||
|
delete params.path;
|
||||||
|
}
|
||||||
|
if (params.path) {
|
||||||
|
opts.path = params.path;
|
||||||
}
|
}
|
||||||
// https://github.com/XTLS/Xray-core/issues/91
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
if (['grpc'].includes(proxy.network)) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
@@ -409,19 +446,22 @@ function URI_VLESS() {
|
|||||||
function URI_Hysteria2() {
|
function URI_Hysteria2() {
|
||||||
const name = 'URI Hysteria2 Parser';
|
const name = 'URI Hysteria2 Parser';
|
||||||
const test = (line) => {
|
const test = (line) => {
|
||||||
return /^hysteria2:\/\//.test(line);
|
return /^(hysteria2|hy2):\/\//.test(line);
|
||||||
};
|
};
|
||||||
const parse = (line) => {
|
const parse = (line) => {
|
||||||
line = line.split('hysteria2://')[1];
|
line = line.split(/(hysteria2|hy2):\/\//)[2];
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let [__, password, server, ___, port, addons, name] =
|
let [__, password, server, ___, port, ____, addons = '', name] =
|
||||||
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||||
port = parseInt(`${port}`, 10);
|
port = parseInt(`${port}`, 10);
|
||||||
if (isNaN(port)) {
|
if (isNaN(port)) {
|
||||||
port = 443;
|
port = 443;
|
||||||
}
|
}
|
||||||
password = decodeURIComponent(password);
|
password = decodeURIComponent(password);
|
||||||
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
|
if (name != null) {
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
}
|
||||||
|
name = name ?? `Hysteria2 ${server}:${port}`;
|
||||||
|
|
||||||
const proxy = {
|
const proxy = {
|
||||||
type: 'hysteria2',
|
type: 'hysteria2',
|
||||||
@@ -742,6 +782,7 @@ function Loon_WireGuard() {
|
|||||||
let publicKey = peers.match(
|
let publicKey = peers.match(
|
||||||
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||||
)?.[2];
|
)?.[2];
|
||||||
|
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||||
const proxy = {
|
const proxy = {
|
||||||
type: 'wireguard',
|
type: 'wireguard',
|
||||||
name,
|
name,
|
||||||
@@ -768,7 +809,7 @@ function Loon_WireGuard() {
|
|||||||
ipv6,
|
ipv6,
|
||||||
'public-key': publicKey,
|
'public-key': publicKey,
|
||||||
'pre-shared-key': preSharedKey,
|
'pre-shared-key': preSharedKey,
|
||||||
allowed_ips: allowedIps,
|
'allowed-ips': allowedIps,
|
||||||
reserved,
|
reserved,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -90,11 +90,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
|||||||
|
|
||||||
if (params["type"]) {
|
if (params["type"]) {
|
||||||
proxy.network = params["type"]
|
proxy.network = params["type"]
|
||||||
if (params["path"]) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
proxy[proxy.network + '-opts'] = {
|
||||||
}
|
'grpc-service-name': params["serviceName"],
|
||||||
if (params["host"]) {
|
'_grpc-type': params["mode"],
|
||||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
};
|
||||||
|
} else {
|
||||||
|
if (params["path"]) {
|
||||||
|
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||||
|
}
|
||||||
|
if (params["host"]) {
|
||||||
|
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
|||||||
|
|
||||||
if (params["type"]) {
|
if (params["type"]) {
|
||||||
proxy.network = params["type"]
|
proxy.network = params["type"]
|
||||||
if (params["path"]) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
proxy[proxy.network + '-opts'] = {
|
||||||
}
|
'grpc-service-name': params["serviceName"],
|
||||||
if (params["host"]) {
|
'_grpc-type': params["mode"],
|
||||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
};
|
||||||
|
} else {
|
||||||
|
if (params["path"]) {
|
||||||
|
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||||
|
}
|
||||||
|
if (params["host"]) {
|
||||||
|
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,15 @@ import lodash from 'lodash';
|
|||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
|
import { produceArtifact } from '@/restful/sync';
|
||||||
|
|
||||||
import env from '@/utils/env';
|
import env from '@/utils/env';
|
||||||
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
|
import {
|
||||||
|
getFlowField,
|
||||||
|
getFlowHeaders,
|
||||||
|
parseFlowHeaders,
|
||||||
|
flowTransfer,
|
||||||
|
} from '@/utils/flow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
|
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
|
||||||
@@ -316,11 +323,20 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
|||||||
await (async function () {
|
await (async function () {
|
||||||
const operator = createDynamicFunction(
|
const operator = createDynamicFunction(
|
||||||
'operator',
|
'operator',
|
||||||
`async function operator(proxies = []) {
|
`async function operator(input = []) {
|
||||||
return proxies.map(($server = {}) => {
|
if (input?.$files || input?.$content) {
|
||||||
${script}
|
let { $content, $files } = input
|
||||||
return $server
|
${script}
|
||||||
})
|
return { $content, $files }
|
||||||
|
} else {
|
||||||
|
let proxies = input
|
||||||
|
let list = []
|
||||||
|
for await (let $server of proxies) {
|
||||||
|
${script}
|
||||||
|
list.push($server)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
}`,
|
}`,
|
||||||
$arguments,
|
$arguments,
|
||||||
);
|
);
|
||||||
@@ -610,10 +626,16 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
|||||||
await (async function () {
|
await (async function () {
|
||||||
const filter = createDynamicFunction(
|
const filter = createDynamicFunction(
|
||||||
'filter',
|
'filter',
|
||||||
`async function filter(proxies = []) {
|
`async function filter(input = []) {
|
||||||
return proxies.filter(($server = {}) => {
|
let proxies = input
|
||||||
${script}
|
let list = []
|
||||||
})
|
const fn = async ($server) => {
|
||||||
|
${script}
|
||||||
|
}
|
||||||
|
for await (let $server of proxies) {
|
||||||
|
list.push(await fn($server))
|
||||||
|
}
|
||||||
|
return list
|
||||||
}`,
|
}`,
|
||||||
$arguments,
|
$arguments,
|
||||||
);
|
);
|
||||||
@@ -649,20 +671,21 @@ async function ApplyFilter(filter, objs) {
|
|||||||
try {
|
try {
|
||||||
selected = await filter.func(objs);
|
selected = await filter.func(objs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// print log and skip this filter
|
|
||||||
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
|
||||||
let funcErr = '';
|
let funcErr = '';
|
||||||
let funcErrMsg = `${err.message ?? err}`;
|
let funcErrMsg = `${err.message ?? err}`;
|
||||||
if (funcErrMsg.includes('$server is not defined')) {
|
if (funcErrMsg.includes('$server is not defined')) {
|
||||||
funcErr = '';
|
funcErr = '';
|
||||||
} else {
|
} else {
|
||||||
|
$.error(
|
||||||
|
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
|
||||||
|
);
|
||||||
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
|
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
selected = await filter.nodeFunc(objs);
|
selected = await filter.nodeFunc(objs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot apply filter ${filter.name}(node script)! Reason: ${err}`,
|
`Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,
|
||||||
);
|
);
|
||||||
let nodeErr = '';
|
let nodeErr = '';
|
||||||
let nodeErrMsg = `${err.message ?? err}`;
|
let nodeErrMsg = `${err.message ?? err}`;
|
||||||
@@ -670,7 +693,7 @@ async function ApplyFilter(filter, objs) {
|
|||||||
nodeErr = '';
|
nodeErr = '';
|
||||||
funcErr = `执行失败 ${funcErrMsg}`;
|
funcErr = `执行失败 ${funcErrMsg}`;
|
||||||
} else {
|
} else {
|
||||||
nodeErr = `执行节点快捷过滤脚本 失败 ${nodeErr}`;
|
nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
|
||||||
}
|
}
|
||||||
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
|
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
|
||||||
}
|
}
|
||||||
@@ -684,14 +707,20 @@ async function ApplyOperator(operator, objs) {
|
|||||||
const output_ = await operator.func(output);
|
const output_ = await operator.func(output);
|
||||||
if (output_) output = output_;
|
if (output_) output = output_;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
|
||||||
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
|
||||||
);
|
|
||||||
let funcErr = '';
|
let funcErr = '';
|
||||||
let funcErrMsg = `${err.message ?? err}`;
|
let funcErrMsg = `${err.message ?? err}`;
|
||||||
if (funcErrMsg.includes('$server is not defined')) {
|
if (
|
||||||
|
funcErrMsg.includes('$server is not defined') ||
|
||||||
|
funcErrMsg.includes('$content is not defined') ||
|
||||||
|
funcErrMsg.includes('$files is not defined') ||
|
||||||
|
output?.$files ||
|
||||||
|
output?.$content
|
||||||
|
) {
|
||||||
funcErr = '';
|
funcErr = '';
|
||||||
} else {
|
} else {
|
||||||
|
$.error(
|
||||||
|
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
||||||
|
);
|
||||||
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -699,7 +728,7 @@ async function ApplyOperator(operator, objs) {
|
|||||||
if (output_) output = output_;
|
if (output_) output = output_;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
|
`Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,
|
||||||
);
|
);
|
||||||
let nodeErr = '';
|
let nodeErr = '';
|
||||||
let nodeErrMsg = `${err.message ?? err}`;
|
let nodeErrMsg = `${err.message ?? err}`;
|
||||||
@@ -707,7 +736,7 @@ async function ApplyOperator(operator, objs) {
|
|||||||
nodeErr = '';
|
nodeErr = '';
|
||||||
funcErr = `执行失败 ${funcErrMsg}`;
|
funcErr = `执行失败 ${funcErrMsg}`;
|
||||||
} else {
|
} else {
|
||||||
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
|
nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
|
||||||
}
|
}
|
||||||
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
||||||
}
|
}
|
||||||
@@ -757,7 +786,12 @@ function removeFlag(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDynamicFunction(name, script, $arguments) {
|
function createDynamicFunction(name, script, $arguments) {
|
||||||
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
|
const flowUtils = {
|
||||||
|
getFlowField,
|
||||||
|
getFlowHeaders,
|
||||||
|
parseFlowHeaders,
|
||||||
|
flowTransfer,
|
||||||
|
};
|
||||||
if ($.env.isLoon) {
|
if ($.env.isLoon) {
|
||||||
return new Function(
|
return new Function(
|
||||||
'$arguments',
|
'$arguments',
|
||||||
@@ -769,6 +803,7 @@ function createDynamicFunction(name, script, $arguments) {
|
|||||||
'ProxyUtils',
|
'ProxyUtils',
|
||||||
'scriptResourceCache',
|
'scriptResourceCache',
|
||||||
'flowUtils',
|
'flowUtils',
|
||||||
|
'produceArtifact',
|
||||||
`${script}\n return ${name}`,
|
`${script}\n return ${name}`,
|
||||||
)(
|
)(
|
||||||
$arguments,
|
$arguments,
|
||||||
@@ -783,6 +818,7 @@ function createDynamicFunction(name, script, $arguments) {
|
|||||||
ProxyUtils,
|
ProxyUtils,
|
||||||
scriptResourceCache,
|
scriptResourceCache,
|
||||||
flowUtils,
|
flowUtils,
|
||||||
|
produceArtifact,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new Function(
|
return new Function(
|
||||||
@@ -792,8 +828,17 @@ function createDynamicFunction(name, script, $arguments) {
|
|||||||
'ProxyUtils',
|
'ProxyUtils',
|
||||||
'scriptResourceCache',
|
'scriptResourceCache',
|
||||||
'flowUtils',
|
'flowUtils',
|
||||||
|
'produceArtifact',
|
||||||
|
|
||||||
`${script}\n return ${name}`,
|
`${script}\n return ${name}`,
|
||||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
|
)(
|
||||||
|
$arguments,
|
||||||
|
$,
|
||||||
|
lodash,
|
||||||
|
ProxyUtils,
|
||||||
|
scriptResourceCache,
|
||||||
|
flowUtils,
|
||||||
|
produceArtifact,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,118 +2,139 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function Clash_Producer() {
|
export default function Clash_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
// VLESS XTLS is not supported by Clash
|
// VLESS XTLS is not supported by Clash
|
||||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
||||||
// github.com/Dreamacro/clash/pull/2891/files
|
// github.com/Dreamacro/clash/pull/2891/files
|
||||||
// filter unsupported proxies
|
// filter unsupported proxies
|
||||||
proxies = proxies.filter((proxy) => {
|
// https://clash.wiki/configuration/outbound.html#shadowsocks
|
||||||
if (
|
const list = proxies
|
||||||
![
|
.filter((proxy) => {
|
||||||
'ss',
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
'ssr',
|
if (
|
||||||
'vmess',
|
![
|
||||||
'vless',
|
'ss',
|
||||||
'socks5',
|
'ssr',
|
||||||
'http',
|
'vmess',
|
||||||
'snell',
|
'vless',
|
||||||
'trojan',
|
'socks5',
|
||||||
'wireguard',
|
'http',
|
||||||
].includes(proxy.type) ||
|
'snell',
|
||||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
'trojan',
|
||||||
(proxy.type === 'vless' &&
|
'wireguard',
|
||||||
(typeof proxy.flow !== 'undefined' ||
|
].includes(proxy.type) ||
|
||||||
proxy['reality-opts']))
|
(proxy.type === 'ss' &&
|
||||||
) {
|
![
|
||||||
return false;
|
'aes-128-gcm',
|
||||||
}
|
'aes-192-gcm',
|
||||||
return true;
|
'aes-256-gcm',
|
||||||
});
|
'aes-128-cfb',
|
||||||
return (
|
'aes-192-cfb',
|
||||||
'proxies:\n' +
|
'aes-256-cfb',
|
||||||
proxies
|
'aes-128-ctr',
|
||||||
.map((proxy) => {
|
'aes-192-ctr',
|
||||||
if (proxy.type === 'vmess') {
|
'aes-256-ctr',
|
||||||
// handle vmess aead
|
'rc4-md5',
|
||||||
if (isPresent(proxy, 'aead')) {
|
'chacha20-ietf',
|
||||||
if (proxy.aead) {
|
'xchacha20',
|
||||||
proxy.alterId = 0;
|
'chacha20-ietf-poly1305',
|
||||||
}
|
'xchacha20-ietf-poly1305',
|
||||||
delete proxy.aead;
|
].includes(proxy.cipher)) ||
|
||||||
}
|
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||||
if (isPresent(proxy, 'sni')) {
|
(proxy.type === 'vless' &&
|
||||||
proxy.servername = proxy.sni;
|
(typeof proxy.flow !== 'undefined' ||
|
||||||
delete proxy.sni;
|
proxy['reality-opts']))
|
||||||
}
|
) {
|
||||||
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
|
return false;
|
||||||
if (
|
}
|
||||||
isPresent(proxy, 'cipher') &&
|
return true;
|
||||||
![
|
})
|
||||||
'auto',
|
.map((proxy) => {
|
||||||
'aes-128-gcm',
|
if (proxy.type === 'vmess') {
|
||||||
'chacha20-poly1305',
|
// handle vmess aead
|
||||||
'none',
|
if (isPresent(proxy, 'aead')) {
|
||||||
].includes(proxy.cipher)
|
if (proxy.aead) {
|
||||||
) {
|
proxy.alterId = 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
delete proxy.aead;
|
||||||
}
|
}
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'cipher') &&
|
||||||
|
![
|
||||||
|
'auto',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'none',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
proxy.cipher = 'auto';
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'wireguard') {
|
||||||
|
proxy.keepalive =
|
||||||
|
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||||
|
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||||
|
proxy['preshared-key'] =
|
||||||
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'http'
|
||||||
|
) {
|
||||||
|
let httpPath = proxy['http-opts']?.path;
|
||||||
if (
|
if (
|
||||||
['vmess', 'vless'].includes(proxy.type) &&
|
isPresent(proxy, 'http-opts.path') &&
|
||||||
proxy.network === 'http'
|
!Array.isArray(httpPath)
|
||||||
) {
|
) {
|
||||||
let httpPath = proxy['http-opts']?.path;
|
proxy['http-opts'].path = [httpPath];
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||||
proxy.type,
|
!Array.isArray(httpHost)
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
delete proxy.tls;
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
if (proxy['tls-fingerprint']) {
|
if (proxy['tls-fingerprint']) {
|
||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
delete proxy.subName;
|
delete proxy.subName;
|
||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
if (
|
if (
|
||||||
['grpc'].includes(proxy.network) &&
|
['grpc'].includes(proxy.network) &&
|
||||||
proxy[`${proxy.network}-opts`]
|
proxy[`${proxy.network}-opts`]
|
||||||
) {
|
) {
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
}
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
return proxy;
|
||||||
})
|
});
|
||||||
.join('')
|
return type === 'internal'
|
||||||
);
|
? list
|
||||||
|
: 'proxies:\n' +
|
||||||
|
list
|
||||||
|
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||||
|
.join('');
|
||||||
};
|
};
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function ClashMeta_Producer() {
|
export default function ClashMeta_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies, type) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
const list = proxies
|
const list = proxies
|
||||||
.filter((proxy) => {
|
.filter((proxy) => {
|
||||||
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import Loon_Producer from './loon';
|
|||||||
import URI_Producer from './uri';
|
import URI_Producer from './uri';
|
||||||
import V2Ray_Producer from './v2ray';
|
import V2Ray_Producer from './v2ray';
|
||||||
import QX_Producer from './qx';
|
import QX_Producer from './qx';
|
||||||
import ShadowRocket_Producer from './shadowrocket';
|
import Shadowrocket_Producer from './shadowrocket';
|
||||||
|
import Surfboard_Producer from './surfboard';
|
||||||
|
import singbox_Producer from './sing-box';
|
||||||
|
|
||||||
function JSON_Producer() {
|
function JSON_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
@@ -17,6 +19,7 @@ function JSON_Producer() {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
QX: QX_Producer(),
|
QX: QX_Producer(),
|
||||||
|
QuantumultX: QX_Producer(),
|
||||||
Surge: Surge_Producer(),
|
Surge: Surge_Producer(),
|
||||||
SurgeMac: SurgeMac_Producer(),
|
SurgeMac: SurgeMac_Producer(),
|
||||||
Loon: Loon_Producer(),
|
Loon: Loon_Producer(),
|
||||||
@@ -26,5 +29,8 @@ export default {
|
|||||||
V2Ray: V2Ray_Producer(),
|
V2Ray: V2Ray_Producer(),
|
||||||
JSON: JSON_Producer(),
|
JSON: JSON_Producer(),
|
||||||
Stash: Stash_Producer(),
|
Stash: Stash_Producer(),
|
||||||
ShadowRocket: ShadowRocket_Producer(),
|
Shadowrocket: Shadowrocket_Producer(),
|
||||||
|
ShadowRocket: Shadowrocket_Producer(),
|
||||||
|
Surfboard: Surfboard_Producer(),
|
||||||
|
'sing-box': singbox_Producer(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -285,7 +285,8 @@ function wireguard(proxy) {
|
|||||||
proxy.ipv6 = proxy.peers[0].ipv6;
|
proxy.ipv6 = proxy.peers[0].ipv6;
|
||||||
proxy['public-key'] = proxy.peers[0]['public-key'];
|
proxy['public-key'] = proxy.peers[0]['public-key'];
|
||||||
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
||||||
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
|
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||||
|
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
|
||||||
proxy.reserved = proxy.peers[0].reserved;
|
proxy.reserved = proxy.peers[0].reserved;
|
||||||
}
|
}
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
|
|||||||
@@ -2,162 +2,161 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function ShadowRocket_Producer() {
|
export default function ShadowRocket_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
return (
|
const list = proxies
|
||||||
'proxies:\n' +
|
.filter((proxy) => {
|
||||||
proxies
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
.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 (
|
if (
|
||||||
proxy.type === 'snell' &&
|
isPresent(proxy, 'cipher') &&
|
||||||
String(proxy.version) === '4'
|
![
|
||||||
|
'auto',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'none',
|
||||||
|
].includes(proxy.cipher)
|
||||||
) {
|
) {
|
||||||
return false;
|
proxy.cipher = 'auto';
|
||||||
}
|
}
|
||||||
return true;
|
} else if (proxy.type === 'tuic') {
|
||||||
})
|
if (isPresent(proxy, 'alpn')) {
|
||||||
.map((proxy) => {
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
if (proxy.type === 'vmess') {
|
? proxy.alpn
|
||||||
// handle vmess aead
|
: [proxy.alpn];
|
||||||
if (isPresent(proxy, 'aead')) {
|
} else {
|
||||||
if (proxy.aead) {
|
proxy.alpn = ['h3'];
|
||||||
proxy.alterId = 0;
|
|
||||||
}
|
|
||||||
delete proxy.aead;
|
|
||||||
}
|
|
||||||
if (isPresent(proxy, 'sni')) {
|
|
||||||
proxy.servername = proxy.sni;
|
|
||||||
delete proxy.sni;
|
|
||||||
}
|
|
||||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
|
||||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
|
||||||
if (
|
|
||||||
isPresent(proxy, 'cipher') &&
|
|
||||||
![
|
|
||||||
'auto',
|
|
||||||
'aes-128-gcm',
|
|
||||||
'chacha20-poly1305',
|
|
||||||
'none',
|
|
||||||
].includes(proxy.cipher)
|
|
||||||
) {
|
|
||||||
proxy.cipher = 'auto';
|
|
||||||
}
|
|
||||||
} else if (proxy.type === 'tuic') {
|
|
||||||
if (isPresent(proxy, 'alpn')) {
|
|
||||||
proxy.alpn = Array.isArray(proxy.alpn)
|
|
||||||
? proxy.alpn
|
|
||||||
: [proxy.alpn];
|
|
||||||
} else {
|
|
||||||
proxy.alpn = ['h3'];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isPresent(proxy, 'tfo') &&
|
|
||||||
!isPresent(proxy, 'fast-open')
|
|
||||||
) {
|
|
||||||
proxy['fast-open'] = proxy.tfo;
|
|
||||||
}
|
|
||||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
|
||||||
if (
|
|
||||||
(!proxy.token || proxy.token.length === 0) &&
|
|
||||||
!isPresent(proxy, 'version')
|
|
||||||
) {
|
|
||||||
proxy.version = 5;
|
|
||||||
}
|
|
||||||
} else if (proxy.type === 'hysteria') {
|
|
||||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
|
||||||
if (
|
|
||||||
isPresent(proxy, 'auth_str') &&
|
|
||||||
!isPresent(proxy, 'auth-str')
|
|
||||||
) {
|
|
||||||
proxy['auth-str'] = proxy['auth_str'];
|
|
||||||
}
|
|
||||||
if (isPresent(proxy, 'alpn')) {
|
|
||||||
proxy.alpn = Array.isArray(proxy.alpn)
|
|
||||||
? proxy.alpn
|
|
||||||
: [proxy.alpn];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isPresent(proxy, 'tfo') &&
|
|
||||||
!isPresent(proxy, 'fast-open')
|
|
||||||
) {
|
|
||||||
proxy['fast-open'] = proxy.tfo;
|
|
||||||
}
|
|
||||||
} else if (proxy.type === 'hysteria2') {
|
|
||||||
if (
|
|
||||||
proxy['obfs-password'] &&
|
|
||||||
proxy.obfs == 'salamander'
|
|
||||||
) {
|
|
||||||
proxy.obfs = proxy['obfs-password'];
|
|
||||||
delete proxy['obfs-password'];
|
|
||||||
}
|
|
||||||
if (isPresent(proxy, 'alpn')) {
|
|
||||||
proxy.alpn = Array.isArray(proxy.alpn)
|
|
||||||
? proxy.alpn
|
|
||||||
: [proxy.alpn];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isPresent(proxy, 'tfo') &&
|
|
||||||
!isPresent(proxy, 'fast-open')
|
|
||||||
) {
|
|
||||||
proxy['fast-open'] = proxy.tfo;
|
|
||||||
}
|
|
||||||
} else if (proxy.type === 'wireguard') {
|
|
||||||
proxy.keepalive =
|
|
||||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
|
||||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
|
||||||
proxy['preshared-key'] =
|
|
||||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
|
||||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
|
||||||
} else if (proxy.type === 'vless') {
|
|
||||||
if (isPresent(proxy, 'sni')) {
|
|
||||||
proxy.servername = proxy.sni;
|
|
||||||
delete proxy.sni;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'tfo') &&
|
||||||
|
!isPresent(proxy, 'fast-open')
|
||||||
|
) {
|
||||||
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
}
|
||||||
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||||
|
if (
|
||||||
|
(!proxy.token || proxy.token.length === 0) &&
|
||||||
|
!isPresent(proxy, 'version')
|
||||||
|
) {
|
||||||
|
proxy.version = 5;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'hysteria') {
|
||||||
|
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'auth_str') &&
|
||||||
|
!isPresent(proxy, 'auth-str')
|
||||||
|
) {
|
||||||
|
proxy['auth-str'] = proxy['auth_str'];
|
||||||
|
}
|
||||||
|
if (isPresent(proxy, 'alpn')) {
|
||||||
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: [proxy.alpn];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'tfo') &&
|
||||||
|
!isPresent(proxy, 'fast-open')
|
||||||
|
) {
|
||||||
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'hysteria2') {
|
||||||
|
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
|
||||||
|
proxy.obfs = proxy['obfs-password'];
|
||||||
|
delete proxy['obfs-password'];
|
||||||
|
}
|
||||||
|
if (isPresent(proxy, 'alpn')) {
|
||||||
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: [proxy.alpn];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'tfo') &&
|
||||||
|
!isPresent(proxy, 'fast-open')
|
||||||
|
) {
|
||||||
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'wireguard') {
|
||||||
|
proxy.keepalive =
|
||||||
|
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||||
|
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||||
|
proxy['preshared-key'] =
|
||||||
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'http'
|
||||||
|
) {
|
||||||
|
let httpPath = proxy['http-opts']?.path;
|
||||||
if (
|
if (
|
||||||
['vmess', 'vless'].includes(proxy.type) &&
|
isPresent(proxy, 'http-opts.path') &&
|
||||||
proxy.network === 'http'
|
!Array.isArray(httpPath)
|
||||||
) {
|
) {
|
||||||
let httpPath = proxy['http-opts']?.path;
|
proxy['http-opts'].path = [httpPath];
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||||
|
!Array.isArray(httpHost)
|
||||||
|
) {
|
||||||
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||||
proxy.type,
|
proxy.type,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
delete proxy.tls;
|
delete proxy.tls;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proxy['tls-fingerprint']) {
|
if (proxy['tls-fingerprint']) {
|
||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
delete proxy.subName;
|
delete proxy.subName;
|
||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
if (
|
if (
|
||||||
['grpc'].includes(proxy.network) &&
|
['grpc'].includes(proxy.network) &&
|
||||||
proxy[`${proxy.network}-opts`]
|
proxy[`${proxy.network}-opts`]
|
||||||
) {
|
) {
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
}
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
return proxy;
|
||||||
})
|
});
|
||||||
.join('')
|
return type === 'internal'
|
||||||
);
|
? list
|
||||||
|
: 'proxies:\n' +
|
||||||
|
list
|
||||||
|
.map((proxy) => {
|
||||||
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
};
|
};
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
688
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
688
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
import ClashMeta_Producer from './clashmeta';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
|
const tfoParser = (proxy, parsedProxy) => {
|
||||||
|
parsedProxy.tcp_fast_open = false;
|
||||||
|
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
|
||||||
|
};
|
||||||
|
|
||||||
|
const smuxParser = (smux, proxy) => {
|
||||||
|
if (!smux || !smux.enabled) return;
|
||||||
|
proxy.multiplex = { enabled: true };
|
||||||
|
proxy.multiplex.protocol = smux.protocol;
|
||||||
|
if (smux['max-connections'])
|
||||||
|
proxy.multiplex.max_connections = parseInt(
|
||||||
|
`${smux['max-connections']}`,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
if (smux['max-streams'])
|
||||||
|
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
|
||||||
|
if (smux['min-streams'])
|
||||||
|
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
|
||||||
|
if (smux.padding) proxy.multiplex.padding = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wsParser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'ws', headers: {} };
|
||||||
|
if (proxy['ws-opts']) {
|
||||||
|
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
|
||||||
|
if (wsPath !== '') transport.path = `${wsPath}`;
|
||||||
|
if (Object.keys(wsHeaders).length > 0) {
|
||||||
|
const headers = {};
|
||||||
|
for (const key of Object.keys(wsHeaders)) {
|
||||||
|
let value = wsHeaders[key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (!Array.isArray(value)) value = [`${value}`];
|
||||||
|
if (value.length > 0) headers[key] = value;
|
||||||
|
}
|
||||||
|
const { Host: wsHost } = headers;
|
||||||
|
if (wsHost.length === 1)
|
||||||
|
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||||
|
const [key, value] = item.split(':');
|
||||||
|
if (value.trim() === '') continue;
|
||||||
|
headers[key.trim()] = value.trim().split(',');
|
||||||
|
}
|
||||||
|
transport.headers = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['ws-headers']) {
|
||||||
|
const headers = {};
|
||||||
|
for (const key of Object.keys(proxy['ws-headers'])) {
|
||||||
|
let value = proxy['ws-headers'][key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (!Array.isArray(value)) value = [`${value}`];
|
||||||
|
if (value.length > 0) headers[key] = value;
|
||||||
|
}
|
||||||
|
const { Host: wsHost } = headers;
|
||||||
|
if (wsHost.length === 1)
|
||||||
|
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||||
|
const [key, value] = item.split(':');
|
||||||
|
if (value.trim() === '') continue;
|
||||||
|
headers[key.trim()] = value.trim().split(',');
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(headers))
|
||||||
|
transport.headers[key] = headers[key];
|
||||||
|
}
|
||||||
|
if (proxy['ws-path'] && proxy['ws-path'] !== '')
|
||||||
|
transport.path = `${proxy['ws-path']}`;
|
||||||
|
if (transport.path) {
|
||||||
|
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [_, path = '', ed = ''] = reg.exec(transport.path);
|
||||||
|
transport.path = path;
|
||||||
|
if (ed !== '') {
|
||||||
|
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
|
||||||
|
transport.max_early_data = parseInt(ed, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.headers.Host[0];
|
||||||
|
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
|
||||||
|
transport.type = 'httpupgrade';
|
||||||
|
if (transport.headers.Host) {
|
||||||
|
transport.host = transport.headers.Host[0];
|
||||||
|
delete transport.headers.Host;
|
||||||
|
}
|
||||||
|
if (transport.max_early_data) delete transport.max_early_data;
|
||||||
|
if (transport.early_data_header_name)
|
||||||
|
delete transport.early_data_header_name;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(transport.headers)) {
|
||||||
|
const value = transport.headers[key];
|
||||||
|
if (value.length === 1) transport.headers[key] = value[0];
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const h1Parser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'http', headers: {} };
|
||||||
|
if (proxy['http-opts']) {
|
||||||
|
const {
|
||||||
|
method = '',
|
||||||
|
path: h1Path = '',
|
||||||
|
headers: h1Headers = {},
|
||||||
|
} = proxy['http-opts'];
|
||||||
|
if (method !== '') transport.method = method;
|
||||||
|
if (Array.isArray(h1Path)) {
|
||||||
|
transport.path = `${h1Path[0]}`;
|
||||||
|
} else if (h1Path !== '') transport.path = `${h1Path}`;
|
||||||
|
for (const key of Object.keys(h1Headers)) {
|
||||||
|
let value = h1Headers[key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (key.toLowerCase() === 'host') {
|
||||||
|
let host = value;
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value))
|
||||||
|
value = `${value}`.split(',').map((i) => i.trim());
|
||||||
|
if (value.length > 0) transport.headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['http-host'] && proxy['http-host'] !== '') {
|
||||||
|
let host = proxy['http-host'];
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
if (!transport.host) return;
|
||||||
|
if (proxy['http-path'] && proxy['http-path'] !== '') {
|
||||||
|
const path = proxy['http-path'];
|
||||||
|
if (Array.isArray(path)) {
|
||||||
|
transport.path = `${path[0]}`;
|
||||||
|
} else if (path !== '') transport.path = `${path}`;
|
||||||
|
}
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.host[0];
|
||||||
|
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||||
|
for (const key of Object.keys(transport.headers)) {
|
||||||
|
const value = transport.headers[key];
|
||||||
|
if (value.length === 1) transport.headers[key] = value[0];
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const h2Parser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'http' };
|
||||||
|
if (proxy['h2-opts']) {
|
||||||
|
let { host = '', path = '' } = proxy['h2-opts'];
|
||||||
|
if (path !== '') transport.path = `${path}`;
|
||||||
|
if (host !== '') {
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
|
||||||
|
let host = proxy['h2-host'];
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
if (proxy['h2-path'] && proxy['h2-path'] !== '')
|
||||||
|
transport.path = `${proxy['h2-path']}`;
|
||||||
|
parsedProxy.tls.enabled = true;
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.host[0];
|
||||||
|
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const grpcParser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'grpc' };
|
||||||
|
if (proxy['grpc-opts']) {
|
||||||
|
const serviceName = proxy['grpc-opts']['grpc-service-name'];
|
||||||
|
if (serviceName && serviceName !== '')
|
||||||
|
transport.service_name = serviceName;
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tlsParser = (proxy, parsedProxy) => {
|
||||||
|
if (proxy.tls) parsedProxy.tls.enabled = true;
|
||||||
|
if (proxy.servername && proxy.servername !== '')
|
||||||
|
parsedProxy.tls.server_name = proxy.servername;
|
||||||
|
if (proxy.peer && proxy.peer !== '')
|
||||||
|
parsedProxy.tls.server_name = proxy.peer;
|
||||||
|
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
|
||||||
|
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
|
||||||
|
if (proxy.insecure) parsedProxy.tls.insecure = true;
|
||||||
|
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
|
||||||
|
if (typeof proxy.alpn === 'string') {
|
||||||
|
parsedProxy.tls.alpn = [proxy.alpn];
|
||||||
|
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
|
||||||
|
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
|
||||||
|
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
|
||||||
|
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
|
||||||
|
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||||
|
parsedProxy.tls.utls = {
|
||||||
|
enabled: true,
|
||||||
|
fingerprint: proxy['client-fingerprint'],
|
||||||
|
};
|
||||||
|
if (proxy['reality-opts']) {
|
||||||
|
parsedProxy.tls.reality = { enabled: true };
|
||||||
|
if (proxy['reality-opts']['public-key'])
|
||||||
|
parsedProxy.tls.reality.public_key =
|
||||||
|
proxy['reality-opts']['public-key'];
|
||||||
|
if (proxy['reality-opts']['short-id'])
|
||||||
|
parsedProxy.tls.reality.short_id =
|
||||||
|
proxy['reality-opts']['short-id'];
|
||||||
|
}
|
||||||
|
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'http',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.username) parsedProxy.username = proxy.username;
|
||||||
|
if (proxy.password) parsedProxy.password = proxy.password;
|
||||||
|
if (proxy.headers) {
|
||||||
|
parsedProxy.headers = {};
|
||||||
|
for (const k of Object.keys(proxy.headers)) {
|
||||||
|
parsedProxy.headers[k] = `${proxy.headers[k]}`;
|
||||||
|
}
|
||||||
|
if (Object.keys(parsedProxy.headers).length === 0)
|
||||||
|
delete parsedProxy.headers;
|
||||||
|
}
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const socks5Parser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'socks',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
version: '5',
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.username) parsedProxy.username = proxy.username;
|
||||||
|
if (proxy.password) parsedProxy.password = proxy.password;
|
||||||
|
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ssParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'shadowsocks',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
method: proxy.cipher,
|
||||||
|
password: proxy.password,
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
if (proxy.plugin) {
|
||||||
|
const optArr = [];
|
||||||
|
if (proxy.plugin === 'obfs') {
|
||||||
|
parsedProxy.plugin = 'obfs-local';
|
||||||
|
parsedProxy.plugin_opts = '';
|
||||||
|
if (proxy['obfs-host'])
|
||||||
|
proxy['plugin-opts'].host = proxy['obfs-host'];
|
||||||
|
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||||
|
switch (k) {
|
||||||
|
case 'mode':
|
||||||
|
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (proxy.plugin === 'v2ray-plugin') {
|
||||||
|
parsedProxy.plugin = 'v2ray-plugin';
|
||||||
|
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
|
||||||
|
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
|
||||||
|
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||||
|
switch (k) {
|
||||||
|
case 'tls':
|
||||||
|
if (proxy['plugin-opts'].tls) optArr.push('tls');
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
optArr.push(`host=${proxy['plugin-opts'].host}`);
|
||||||
|
break;
|
||||||
|
case 'path':
|
||||||
|
optArr.push(`path=${proxy['plugin-opts'].path}`);
|
||||||
|
break;
|
||||||
|
case 'headers':
|
||||||
|
optArr.push(
|
||||||
|
`headers=${JSON.stringify(
|
||||||
|
proxy['plugin-opts'].headers,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'mux':
|
||||||
|
if (proxy['plugin-opts'].mux)
|
||||||
|
parsedProxy.multiplex = { enabled: true };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
parsedProxy.plugin_opts = optArr.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const ssrParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'shadowsocksr',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
method: proxy.cipher,
|
||||||
|
password: proxy.password,
|
||||||
|
obfs: proxy.obfs,
|
||||||
|
protocol: proxy.protocol,
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
|
||||||
|
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
|
||||||
|
parsedProxy.protocol_param = proxy['protocol-param'];
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const vmessParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'vmess',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
security: proxy.cipher,
|
||||||
|
alter_id: parseInt(`${proxy.alterId}`, 10),
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'auto',
|
||||||
|
'none',
|
||||||
|
'zero',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'aes-128-ctr',
|
||||||
|
].indexOf(parsedProxy.security) === -1
|
||||||
|
)
|
||||||
|
parsedProxy.security = 'auto';
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const vlessParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'vless',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const trojanParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'trojan',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const hysteriaParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'hysteria',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
disable_mtu_discovery: false,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
|
||||||
|
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
|
||||||
|
if (reg.test(`${proxy.up}`)) {
|
||||||
|
parsedProxy.up = `${proxy.up}`;
|
||||||
|
} else {
|
||||||
|
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||||
|
}
|
||||||
|
if (reg.test(`${proxy.down}`)) {
|
||||||
|
parsedProxy.down = `${proxy.down}`;
|
||||||
|
} else {
|
||||||
|
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||||
|
}
|
||||||
|
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
|
||||||
|
if (proxy.recv_window_conn)
|
||||||
|
parsedProxy.recv_window_conn = proxy.recv_window_conn;
|
||||||
|
if (proxy['recv-window-conn'])
|
||||||
|
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
|
||||||
|
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
|
||||||
|
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
|
||||||
|
if (proxy.disable_mtu_discovery) {
|
||||||
|
if (typeof proxy.disable_mtu_discovery === 'boolean') {
|
||||||
|
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
|
||||||
|
} else {
|
||||||
|
if (proxy.disable_mtu_discovery === 1)
|
||||||
|
parsedProxy.disable_mtu_discovery = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const hysteria2Parser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'hysteria2',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
obfs: {},
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||||
|
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||||
|
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
|
||||||
|
if (proxy['obfs-password'])
|
||||||
|
parsedProxy.obfs.password = proxy['obfs-password'];
|
||||||
|
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const tuic5Parser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'tuic',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
password: proxy.password,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (
|
||||||
|
proxy['congestion-controller'] &&
|
||||||
|
proxy['congestion-controller'] !== 'cubic'
|
||||||
|
)
|
||||||
|
parsedProxy.congestion_control = proxy['congestion-controller'];
|
||||||
|
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
|
||||||
|
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
|
||||||
|
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
|
||||||
|
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
|
||||||
|
if (proxy['heartbeat-interval'])
|
||||||
|
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wireguardParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'wireguard',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
local_address: [proxy.ip, proxy.ipv6],
|
||||||
|
private_key: proxy['private-key'],
|
||||||
|
peer_public_key: proxy['public-key'],
|
||||||
|
pre_shared_key: proxy['pre-shared-key'],
|
||||||
|
reserved: [],
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (typeof proxy.reserved === 'string') {
|
||||||
|
parsedProxy.reserved.push(proxy.reserved);
|
||||||
|
} else {
|
||||||
|
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
|
||||||
|
}
|
||||||
|
if (proxy.peers && proxy.peers.length > 0) {
|
||||||
|
parsedProxy.peers = [];
|
||||||
|
for (const p of proxy.peers) {
|
||||||
|
const peer = {
|
||||||
|
server: p.server,
|
||||||
|
server_port: parseInt(`${p.port}`, 10),
|
||||||
|
public_key: p['public-key'],
|
||||||
|
allowed_ips: p.allowed_ips,
|
||||||
|
reserved: [],
|
||||||
|
};
|
||||||
|
if (typeof p.reserved === 'string') {
|
||||||
|
peer.reserved.push(p.reserved);
|
||||||
|
} else {
|
||||||
|
for (const r of p.reserved) peer.reserved.push(r);
|
||||||
|
}
|
||||||
|
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
|
||||||
|
parsedProxy.peers.push(peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function singbox_Producer() {
|
||||||
|
const type = 'ALL';
|
||||||
|
const produce = (proxies, type, opts = {}) => {
|
||||||
|
const list = [];
|
||||||
|
ClashMeta_Producer()
|
||||||
|
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
|
||||||
|
.map((proxy) => {
|
||||||
|
try {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'http':
|
||||||
|
list.push(httpParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'socks5':
|
||||||
|
if (proxy.tls) {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
list.push(socks5Parser(proxy));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ss':
|
||||||
|
if (proxy.plugin === 'shadow-tls') {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
list.push(ssParser(proxy));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ssr':
|
||||||
|
if (opts['include-unsupported-proxy']) {
|
||||||
|
list.push(ssrParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'vmess':
|
||||||
|
if (
|
||||||
|
!proxy.network ||
|
||||||
|
['ws', 'grpc', 'h2', 'http'].includes(
|
||||||
|
proxy.network,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
list.push(vmessParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'vless':
|
||||||
|
if (
|
||||||
|
!proxy.flow ||
|
||||||
|
['xtls-rprx-vision'].includes(proxy.flow)
|
||||||
|
) {
|
||||||
|
list.push(vlessParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'trojan':
|
||||||
|
if (!proxy.flow) {
|
||||||
|
list.push(trojanParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hysteria':
|
||||||
|
list.push(hysteriaParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'hysteria2':
|
||||||
|
list.push(hysteria2Parser(proxy));
|
||||||
|
break;
|
||||||
|
case 'tuic':
|
||||||
|
if (!proxy.token || proxy.token.length === 0) {
|
||||||
|
list.push(tuic5Parser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: TUIC v4`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'wireguard':
|
||||||
|
list.push(wireguardParser(proxy));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
$.error(e.message ?? e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return type === 'internal' ? list : JSON.stringify(list, null, 2);
|
||||||
|
};
|
||||||
|
return { type, produce };
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
export default function Stash_Producer() {
|
export default function Stash_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies) => {
|
||||||
|
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
|
||||||
return (
|
return (
|
||||||
'proxies:\n' +
|
'proxies:\n' +
|
||||||
proxies
|
proxies
|
||||||
@@ -22,6 +23,23 @@ export default function Stash_Producer() {
|
|||||||
'hysteria',
|
'hysteria',
|
||||||
'hysteria2',
|
'hysteria2',
|
||||||
].includes(proxy.type) ||
|
].includes(proxy.type) ||
|
||||||
|
(proxy.type === 'ss' &&
|
||||||
|
![
|
||||||
|
'aes-128-gcm',
|
||||||
|
'aes-192-gcm',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'rc4-md5',
|
||||||
|
'chacha20-ietf',
|
||||||
|
'xchacha20',
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'xchacha20-ietf-poly1305',
|
||||||
|
].includes(proxy.cipher)) ||
|
||||||
(proxy.type === 'snell' &&
|
(proxy.type === 'snell' &&
|
||||||
String(proxy.version) === '4') ||
|
String(proxy.version) === '4') ||
|
||||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
(proxy.type === 'vless' && proxy['reality-opts'])
|
||||||
|
|||||||
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { Result, isPresent } from './utils';
|
||||||
|
import { isNotBlank } from '@/utils';
|
||||||
|
// import $ from '@/core/app';
|
||||||
|
|
||||||
|
const targetPlatform = 'Surfboard';
|
||||||
|
|
||||||
|
export default function Surfboard_Producer() {
|
||||||
|
const produce = (proxy) => {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'ss':
|
||||||
|
return shadowsocks(proxy);
|
||||||
|
case 'trojan':
|
||||||
|
return trojan(proxy);
|
||||||
|
case 'vmess':
|
||||||
|
return vmess(proxy);
|
||||||
|
case 'http':
|
||||||
|
return http(proxy);
|
||||||
|
case 'socks5':
|
||||||
|
return socks5(proxy);
|
||||||
|
case 'wireguard-surge':
|
||||||
|
return wireguard(proxy);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { produce };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shadowsocks(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||||
|
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// obfs
|
||||||
|
if (isPresent(proxy, 'plugin')) {
|
||||||
|
if (proxy.plugin === 'obfs') {
|
||||||
|
result.append(`,obfs=${proxy['plugin-opts'].mode}`);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,obfs-host=${proxy['plugin-opts'].host}`,
|
||||||
|
'plugin-opts.host',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||||
|
'plugin-opts.path',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trojan(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// transport
|
||||||
|
handleTransport(result, proxy);
|
||||||
|
|
||||||
|
// tls
|
||||||
|
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vmess(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||||
|
|
||||||
|
// transport
|
||||||
|
handleTransport(result, proxy);
|
||||||
|
|
||||||
|
// AEAD
|
||||||
|
if (isPresent(proxy, 'aead')) {
|
||||||
|
result.append(`,vmess-aead=${proxy.aead}`);
|
||||||
|
} else {
|
||||||
|
result.append(`,vmess-aead=${proxy.alterId === 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tls
|
||||||
|
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function http(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const type = proxy.tls ? 'https' : 'http';
|
||||||
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function socks5(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
||||||
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireguard(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
|
||||||
|
result.append(`${proxy.name}=wireguard`);
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,section-name=${proxy['section-name']}`,
|
||||||
|
'section-name',
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTransport(result, proxy) {
|
||||||
|
if (isPresent(proxy, 'network')) {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
result.append(`,ws=true`);
|
||||||
|
if (isPresent(proxy, 'ws-opts')) {
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,ws-path=${proxy['ws-opts'].path}`,
|
||||||
|
'ws-opts.path',
|
||||||
|
);
|
||||||
|
if (isPresent(proxy, 'ws-opts.headers')) {
|
||||||
|
const headers = proxy['ws-opts'].headers;
|
||||||
|
const value = Object.keys(headers)
|
||||||
|
.map((k) => {
|
||||||
|
let v = headers[k];
|
||||||
|
if (['Host'].includes(k)) {
|
||||||
|
v = `"${v}"`;
|
||||||
|
}
|
||||||
|
return `${k}:${v}`;
|
||||||
|
})
|
||||||
|
.join('|');
|
||||||
|
if (isNotBlank(value)) {
|
||||||
|
result.append(`,ws-headers=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -416,6 +416,9 @@ function snell(proxy) {
|
|||||||
'obfs-opts.path',
|
'obfs-opts.path',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Result } from './utils';
|
import { Result } from './utils';
|
||||||
import Surge_Producer from './surge';
|
import Surge_Producer from './surge';
|
||||||
|
|
||||||
const targetPlatform = 'SurgeMac';
|
// const targetPlatform = 'SurgeMac';
|
||||||
|
|
||||||
const surge_Producer = Surge_Producer();
|
const surge_Producer = Surge_Producer();
|
||||||
|
|
||||||
@@ -10,24 +10,9 @@ export default function SurgeMac_Producer() {
|
|||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'ssr':
|
case 'ssr':
|
||||||
return shadowsocksr(proxy);
|
return shadowsocksr(proxy);
|
||||||
case 'ss':
|
default:
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'trojan':
|
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'vmess':
|
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'http':
|
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'socks5':
|
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'snell':
|
|
||||||
return surge_Producer.produce(proxy);
|
|
||||||
case 'tuic':
|
|
||||||
return surge_Producer.produce(proxy);
|
return surge_Producer.produce(proxy);
|
||||||
}
|
}
|
||||||
throw new Error(
|
|
||||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
return { produce };
|
return { produce };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,21 @@ export default function URI_Producer() {
|
|||||||
let trojanTransport = '';
|
let trojanTransport = '';
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
trojanTransport = `&type=${proxy.network}`;
|
trojanTransport = `&type=${proxy.network}`;
|
||||||
|
if (['grpc'].includes(proxy.network)) {
|
||||||
|
let trojanTransportServiceName =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`${proxy.network}-service-name`
|
||||||
|
];
|
||||||
|
if (trojanTransportServiceName) {
|
||||||
|
trojanTransport += `&serviceName=${encodeURIComponent(
|
||||||
|
trojanTransportServiceName,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
trojanTransport += `&mode=${encodeURIComponent(
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||||
|
'gun',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
let trojanTransportPath =
|
let trojanTransportPath =
|
||||||
proxy[`${proxy.network}-opts`]?.path;
|
proxy[`${proxy.network}-opts`]?.path;
|
||||||
let trojanTransportHost =
|
let trojanTransportHost =
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ async function downloadSubscription(req, res) {
|
|||||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||||
|
|
||||||
$.info(`正在下载订阅:${name}`);
|
$.info(`正在下载订阅:${name}`);
|
||||||
let { url, ua, content, mergeSources } = req.query;
|
let {
|
||||||
|
url,
|
||||||
|
ua,
|
||||||
|
content,
|
||||||
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
includeUnsupportedProxy,
|
||||||
|
} = req.query;
|
||||||
if (url) {
|
if (url) {
|
||||||
url = decodeURIComponent(url);
|
url = decodeURIComponent(url);
|
||||||
$.info(`指定远程订阅 URL: ${url}`);
|
$.info(`指定远程订阅 URL: ${url}`);
|
||||||
@@ -37,6 +45,18 @@ async function downloadSubscription(req, res) {
|
|||||||
mergeSources = decodeURIComponent(mergeSources);
|
mergeSources = decodeURIComponent(mergeSources);
|
||||||
$.info(`指定合并来源: ${mergeSources}`);
|
$.info(`指定合并来源: ${mergeSources}`);
|
||||||
}
|
}
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||||
|
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||||
|
}
|
||||||
|
if (produceType) {
|
||||||
|
produceType = decodeURIComponent(produceType);
|
||||||
|
$.info(`指定生产类型: ${produceType}`);
|
||||||
|
}
|
||||||
|
if (includeUnsupportedProxy) {
|
||||||
|
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
|
||||||
|
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
@@ -50,6 +70,11 @@ async function downloadSubscription(req, res) {
|
|||||||
ua,
|
ua,
|
||||||
content,
|
content,
|
||||||
mergeSources,
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sub.source !== 'local' || url) {
|
if (sub.source !== 'local' || url) {
|
||||||
@@ -116,12 +141,34 @@ async function downloadCollection(req, res) {
|
|||||||
|
|
||||||
$.info(`正在下载组合订阅:${name}`);
|
$.info(`正在下载组合订阅:${name}`);
|
||||||
|
|
||||||
|
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
|
||||||
|
req.query;
|
||||||
|
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||||
|
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||||
|
}
|
||||||
|
if (produceType) {
|
||||||
|
produceType = decodeURIComponent(produceType);
|
||||||
|
$.info(`指定生产类型: ${produceType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeUnsupportedProxy) {
|
||||||
|
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
|
||||||
|
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
try {
|
try {
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
name,
|
name,
|
||||||
platform,
|
platform,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// forward flow header from the first subscription in this collection
|
// forward flow header from the first subscription in this collection
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { deleteByName, findByName, updateByName } from '@/utils/database';
|
|||||||
import { FILES_KEY } from '@/constants';
|
import { FILES_KEY } from '@/constants';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
import {
|
||||||
|
RequestInvalidError,
|
||||||
|
ResourceNotFoundError,
|
||||||
|
InternalServerError,
|
||||||
|
} from '@/restful/errors';
|
||||||
|
import { produceArtifact } from '@/restful/sync';
|
||||||
|
|
||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||||
@@ -12,7 +17,10 @@ export default function register($app) {
|
|||||||
.patch(updateFile)
|
.patch(updateFile)
|
||||||
.delete(deleteFile);
|
.delete(deleteFile);
|
||||||
|
|
||||||
|
$app.route('/api/wholeFile/:name').get(getWholeFile);
|
||||||
|
|
||||||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||||
|
$app.route('/api/wholeFiles').get(getAllWholeFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// file API
|
// file API
|
||||||
@@ -37,13 +45,85 @@ function createFile(req, res) {
|
|||||||
success(res, file, 201);
|
success(res, file, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFile(req, res) {
|
async function getFile(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
|
||||||
|
$.info(`正在下载文件:${name}`);
|
||||||
|
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
|
||||||
|
if (url) {
|
||||||
|
url = decodeURIComponent(url);
|
||||||
|
$.info(`指定远程文件 URL: ${url}`);
|
||||||
|
}
|
||||||
|
if (ua) {
|
||||||
|
ua = decodeURIComponent(ua);
|
||||||
|
$.info(`指定远程文件 User-Agent: ${ua}`);
|
||||||
|
}
|
||||||
|
if (content) {
|
||||||
|
content = decodeURIComponent(content);
|
||||||
|
$.info(`指定本地文件: ${content}`);
|
||||||
|
}
|
||||||
|
if (mergeSources) {
|
||||||
|
mergeSources = decodeURIComponent(mergeSources);
|
||||||
|
$.info(`指定合并来源: ${mergeSources}`);
|
||||||
|
}
|
||||||
|
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
|
||||||
|
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
|
||||||
|
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const file = findByName(allFiles, name);
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const output = await produceArtifact({
|
||||||
|
type: 'file',
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
ua,
|
||||||
|
content,
|
||||||
|
mergeSources,
|
||||||
|
ignoreFailedRemoteFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||||
|
output ?? '',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
$.notify(
|
||||||
|
`🌍 Sub-Store 下载文件失败`,
|
||||||
|
`❌ 无法下载文件:${name}!`,
|
||||||
|
`🤔 原因:${err.message ?? err}`,
|
||||||
|
);
|
||||||
|
$.error(err.message ?? err);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
'INTERNAL_SERVER_ERROR',
|
||||||
|
`Failed to download file: ${name}`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}!`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
'RESOURCE_NOT_FOUND',
|
||||||
|
`File ${name} does not exist!`,
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getWholeFile(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
const allFiles = $.read(FILES_KEY);
|
const allFiles = $.read(FILES_KEY);
|
||||||
const file = findByName(allFiles, name);
|
const file = findByName(allFiles, name);
|
||||||
if (file) {
|
if (file) {
|
||||||
res.status(200).json(file.content);
|
success(res, file);
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
@@ -102,6 +182,11 @@ function getAllFiles(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllWholeFiles(req, res) {
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
success(res, allFiles);
|
||||||
|
}
|
||||||
|
|
||||||
function replaceFile(req, res) {
|
function replaceFile(req, res) {
|
||||||
const allFiles = req.body;
|
const allFiles = req.body;
|
||||||
$.write(allFiles, FILES_KEY);
|
$.write(allFiles, FILES_KEY);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import express from '@/vendor/express';
|
import express from '@/vendor/express';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
import migrate from '@/utils/migration';
|
||||||
|
import download from '@/utils/download';
|
||||||
|
import { syncArtifacts } from '@/restful/sync';
|
||||||
|
|
||||||
import registerSubscriptionRoutes from './subscriptions';
|
import registerSubscriptionRoutes from './subscriptions';
|
||||||
import registerCollectionRoutes from './collections';
|
import registerCollectionRoutes from './collections';
|
||||||
@@ -39,8 +42,32 @@ export default function serve() {
|
|||||||
$app.start();
|
$app.start();
|
||||||
|
|
||||||
if ($.env.isNode) {
|
if ($.env.isNode) {
|
||||||
|
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON');
|
||||||
|
if (backend_cron) {
|
||||||
|
$.info(`[CRON] ${backend_cron} enabled`);
|
||||||
|
const { CronJob } = eval(`require("cron")`);
|
||||||
|
new CronJob(
|
||||||
|
backend_cron,
|
||||||
|
async function () {
|
||||||
|
try {
|
||||||
|
$.info(`[CRON] ${backend_cron} started`);
|
||||||
|
await syncArtifacts();
|
||||||
|
$.info(`[CRON] ${backend_cron} finished`);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`[CRON] ${backend_cron} error: ${e.message ?? e}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, // onTick
|
||||||
|
null, // onComplete
|
||||||
|
true, // start
|
||||||
|
// 'Asia/Shanghai' // timeZone
|
||||||
|
);
|
||||||
|
}
|
||||||
const path = eval(`require("path")`);
|
const path = eval(`require("path")`);
|
||||||
const fs = eval(`require("fs")`);
|
const fs = eval(`require("fs")`);
|
||||||
|
const data_url = eval('process.env.SUB_STORE_DATA_URL');
|
||||||
|
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
|
||||||
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
|
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
|
||||||
const fe_host =
|
const fe_host =
|
||||||
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
|
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
|
||||||
@@ -67,20 +94,88 @@ export default function serve() {
|
|||||||
|
|
||||||
const staticFileMiddleware = express_.static(fe_path);
|
const staticFileMiddleware = express_.static(fe_path);
|
||||||
|
|
||||||
app.use('/api', createProxyMiddleware(`http://127.0.0.1:${port}`));
|
let be_api_rewrite = '';
|
||||||
|
let be_download_rewrite = '';
|
||||||
|
let be_api = '/api/';
|
||||||
|
let be_download = '/download/';
|
||||||
|
if (fe_be_path) {
|
||||||
|
if (!fe_be_path.startsWith('/')) {
|
||||||
|
throw new Error(
|
||||||
|
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
be_api_rewrite = `${
|
||||||
|
fe_be_path === '/' ? '' : fe_be_path
|
||||||
|
}${be_api}`;
|
||||||
|
be_download_rewrite = `${
|
||||||
|
fe_be_path === '/' ? '' : fe_be_path
|
||||||
|
}${be_download}`;
|
||||||
|
app.use(
|
||||||
|
be_api_rewrite,
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `http://127.0.0.1:${port}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
return path.startsWith(be_api_rewrite)
|
||||||
|
? path.replace(be_api_rewrite, be_api)
|
||||||
|
: path;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
be_download_rewrite,
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `http://127.0.0.1:${port}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
return path.startsWith(be_download_rewrite)
|
||||||
|
? path.replace(be_download_rewrite, be_download)
|
||||||
|
: path;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
app.use(staticFileMiddleware);
|
app.use(staticFileMiddleware);
|
||||||
app.use(
|
app.use(
|
||||||
history({
|
history({
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
verbose: true,
|
verbose: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(staticFileMiddleware);
|
app.use(staticFileMiddleware);
|
||||||
|
|
||||||
const listener = app.listen(fe_port, fe_host, () => {
|
const listener = app.listen(fe_port, fe_host, () => {
|
||||||
const { address, port } = listener.address();
|
const { address: fe_address, port: fe_port } =
|
||||||
$.info(`[FRONTEND] ${address}:${port}`);
|
listener.address();
|
||||||
|
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
|
||||||
|
if (fe_be_path) {
|
||||||
|
$.info(
|
||||||
|
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
|
||||||
|
);
|
||||||
|
$.info(
|
||||||
|
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data_url) {
|
||||||
|
$.info(`[BACKEND] downloading data from ${data_url}`);
|
||||||
|
download(data_url)
|
||||||
|
.then((content) => {
|
||||||
|
$.write(content, '#sub-store');
|
||||||
|
|
||||||
|
$.cache = JSON.parse(content);
|
||||||
|
$.persistCache();
|
||||||
|
|
||||||
|
migrate();
|
||||||
|
$.info(`[BACKEND] restored data from ${data_url}`);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
$.error(`[BACKEND] restore data failed`);
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ function getModule(req, res) {
|
|||||||
const allModules = $.read(MODULES_KEY);
|
const allModules = $.read(MODULES_KEY);
|
||||||
const module = findByName(allModules, name);
|
const module = findByName(allModules, name);
|
||||||
if (module) {
|
if (module) {
|
||||||
res.status(200).json(module.content);
|
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||||
|
module.content,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ async function getNodeInfo(req, res) {
|
|||||||
const info = await $http
|
const info = await $http
|
||||||
.get({
|
.get({
|
||||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||||
proxy.server,
|
`${proxy.server}`
|
||||||
|
.trim()
|
||||||
|
.replace(/^\[/, '')
|
||||||
|
.replace(/\]$/, ''),
|
||||||
)}?lang=${lang}`,
|
)}?lang=${lang}`,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InternalServerError, NetworkError } from './errors';
|
import { InternalServerError } from './errors';
|
||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
import { findByName } from '@/utils/database';
|
import { findByName } from '@/utils/database';
|
||||||
import { success, failed } from './response';
|
import { success, failed } from './response';
|
||||||
@@ -9,6 +9,85 @@ import $ from '@/core/app';
|
|||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
$app.post('/api/preview/sub', compareSub);
|
$app.post('/api/preview/sub', compareSub);
|
||||||
$app.post('/api/preview/collection', compareCollection);
|
$app.post('/api/preview/collection', compareCollection);
|
||||||
|
$app.post('/api/preview/file', previewFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewFile(req, res) {
|
||||||
|
try {
|
||||||
|
const file = req.body;
|
||||||
|
let content;
|
||||||
|
if (
|
||||||
|
file.source === 'local' &&
|
||||||
|
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||||
|
) {
|
||||||
|
content = file.content;
|
||||||
|
} else {
|
||||||
|
const errors = {};
|
||||||
|
content = await Promise.all(
|
||||||
|
file.url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, file.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!file.ignoreFailedRemoteFile &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.mergeSources === 'localFirst') {
|
||||||
|
content.unshift(file.content);
|
||||||
|
} else if (file.mergeSources === 'remoteFirst') {
|
||||||
|
content.push(file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// parse proxies
|
||||||
|
const files = (Array.isArray(content) ? content : [content]).flat();
|
||||||
|
let filesContent = files
|
||||||
|
.filter((i) => i != null && i !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
const processed =
|
||||||
|
Array.isArray(file.process) && file.process.length > 0
|
||||||
|
? await ProxyUtils.process(
|
||||||
|
{ $files: files, $content: filesContent },
|
||||||
|
file.process,
|
||||||
|
)
|
||||||
|
: { $content: filesContent, $files: files };
|
||||||
|
|
||||||
|
// produce
|
||||||
|
success(res, {
|
||||||
|
original: filesContent,
|
||||||
|
processed: processed?.$content ?? '',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
$.error(err.message ?? err);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`INTERNAL_SERVER_ERROR`,
|
||||||
|
`Failed to preview file`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compareSub(req, res) {
|
async function compareSub(req, res) {
|
||||||
@@ -22,24 +101,31 @@ async function compareSub(req, res) {
|
|||||||
) {
|
) {
|
||||||
content = sub.content;
|
content = sub.content;
|
||||||
} else {
|
} else {
|
||||||
try {
|
const errors = {};
|
||||||
content = await Promise.all(
|
content = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, sub.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
failed(
|
|
||||||
res,
|
|
||||||
new NetworkError(
|
|
||||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
|
||||||
'无法下载远程资源',
|
|
||||||
`Reason: ${err}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
content.unshift(sub.content);
|
content.unshift(sub.content);
|
||||||
@@ -87,69 +173,95 @@ async function compareCollection(req, res) {
|
|||||||
const collection = req.body;
|
const collection = req.body;
|
||||||
const subnames = collection.subscriptions;
|
const subnames = collection.subscriptions;
|
||||||
const results = {};
|
const results = {};
|
||||||
let hasError;
|
const errors = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
subnames.map(async (name) => {
|
subnames.map(async (name) => {
|
||||||
if (!hasError) {
|
const sub = findByName(allSubs, name);
|
||||||
const sub = findByName(allSubs, name);
|
try {
|
||||||
try {
|
let raw;
|
||||||
let raw;
|
if (
|
||||||
if (
|
sub.source === 'local' &&
|
||||||
sub.source === 'local' &&
|
!['localFirst', 'remoteFirst'].includes(
|
||||||
!['localFirst', 'remoteFirst'].includes(
|
sub.mergeSources,
|
||||||
sub.mergeSources,
|
)
|
||||||
)
|
) {
|
||||||
) {
|
raw = sub.content;
|
||||||
raw = sub.content;
|
} else {
|
||||||
} else {
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
);
|
try {
|
||||||
if (sub.mergeSources === 'localFirst') {
|
return await download(url, sub.ua);
|
||||||
raw.unshift(sub.content);
|
} catch (err) {
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
errors[url] = err;
|
||||||
raw.push(sub.content);
|
$.error(
|
||||||
}
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
}
|
);
|
||||||
// parse proxies
|
return '';
|
||||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
}
|
||||||
.map((i) => ProxyUtils.parse(i))
|
}),
|
||||||
.flat();
|
|
||||||
|
|
||||||
currentProxies.forEach((proxy) => {
|
|
||||||
proxy.subName = sub.name;
|
|
||||||
proxy.collectionName = collection.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// apply processors
|
|
||||||
currentProxies = await ProxyUtils.process(
|
|
||||||
currentProxies,
|
|
||||||
sub.process || [],
|
|
||||||
'JSON',
|
|
||||||
{ [sub.name]: sub, _collection: collection },
|
|
||||||
);
|
);
|
||||||
results[name] = currentProxies;
|
if (
|
||||||
} catch (err) {
|
!sub.ignoreFailedRemoteSub &&
|
||||||
if (!hasError) {
|
Object.keys(errors).length > 0
|
||||||
hasError = true;
|
) {
|
||||||
failed(
|
throw new Error(
|
||||||
res,
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||||
new InternalServerError(
|
errors,
|
||||||
'PROCESS_FAILED',
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
`处理子订阅 ${name} 失败`,
|
|
||||||
`Reason: ${err}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (sub.mergeSources === 'localFirst') {
|
||||||
|
raw.unshift(sub.content);
|
||||||
|
} else if (sub.mergeSources === 'remoteFirst') {
|
||||||
|
raw.push(sub.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// parse proxies
|
||||||
|
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
||||||
|
.map((i) => ProxyUtils.parse(i))
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
currentProxies.forEach((proxy) => {
|
||||||
|
proxy.subName = sub.name;
|
||||||
|
proxy.collectionName = collection.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
currentProxies = await ProxyUtils.process(
|
||||||
|
currentProxies,
|
||||||
|
sub.process || [],
|
||||||
|
'JSON',
|
||||||
|
{ [sub.name]: sub, _collection: collection },
|
||||||
|
);
|
||||||
|
results[name] = currentProxies;
|
||||||
|
} catch (err) {
|
||||||
|
errors[name] = err;
|
||||||
|
|
||||||
|
$.error(
|
||||||
|
`❌ 处理组合订阅中的子订阅: ${
|
||||||
|
sub.name
|
||||||
|
}时出现错误:${err}!进度--${
|
||||||
|
100 * (processed / subnames.length).toFixed(1)
|
||||||
|
}%`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (hasError) return;
|
if (
|
||||||
|
!collection.ignoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
// merge proxies with the original order
|
// merge proxies with the original order
|
||||||
const original = Array.prototype.concat.apply(
|
const original = Array.prototype.concat.apply(
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
import {
|
||||||
|
ARTIFACTS_KEY,
|
||||||
|
COLLECTIONS_KEY,
|
||||||
|
SUBS_KEY,
|
||||||
|
FILES_KEY,
|
||||||
|
} from '@/constants';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { success } from '@/restful/response';
|
import { success } from '@/restful/response';
|
||||||
|
|
||||||
@@ -6,6 +11,7 @@ export default function register($app) {
|
|||||||
$app.post('/api/sort/subs', sortSubs);
|
$app.post('/api/sort/subs', sortSubs);
|
||||||
$app.post('/api/sort/collections', sortCollections);
|
$app.post('/api/sort/collections', sortCollections);
|
||||||
$app.post('/api/sort/artifacts', sortArtifacts);
|
$app.post('/api/sort/artifacts', sortArtifacts);
|
||||||
|
$app.post('/api/sort/files', sortFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortSubs(req, res) {
|
function sortSubs(req, res) {
|
||||||
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
|
|||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
success(res, allArtifacts);
|
success(res, allArtifacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortFiles(req, res) {
|
||||||
|
const orders = req.body;
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res, allFiles);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
COLLECTIONS_KEY,
|
COLLECTIONS_KEY,
|
||||||
RULES_KEY,
|
RULES_KEY,
|
||||||
SUBS_KEY,
|
SUBS_KEY,
|
||||||
|
FILES_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||||
@@ -30,23 +31,50 @@ async function produceArtifact({
|
|||||||
ua,
|
ua,
|
||||||
content,
|
content,
|
||||||
mergeSources,
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
ignoreFailedRemoteFile,
|
||||||
|
produceType,
|
||||||
|
produceOpts = {},
|
||||||
}) {
|
}) {
|
||||||
platform = platform || 'JSON';
|
platform = platform || 'JSON';
|
||||||
|
|
||||||
if (type === 'subscription') {
|
if (type === 'subscription') {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
|
if (!sub) throw new Error(`找不到订阅 ${name}`);
|
||||||
let raw;
|
let raw;
|
||||||
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||||
raw = content;
|
raw = content;
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
url
|
url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, ua || sub.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (mergeSources === 'localFirst') {
|
if (mergeSources === 'localFirst') {
|
||||||
raw.unshift(content);
|
raw.unshift(content);
|
||||||
} else if (mergeSources === 'remoteFirst') {
|
} else if (mergeSources === 'remoteFirst') {
|
||||||
@@ -58,13 +86,35 @@ async function produceArtifact({
|
|||||||
) {
|
) {
|
||||||
raw = sub.content;
|
raw = sub.content;
|
||||||
} else {
|
} else {
|
||||||
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, ua || sub.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
raw.unshift(sub.content);
|
raw.unshift(sub.content);
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
} else if (sub.mergeSources === 'remoteFirst') {
|
||||||
@@ -107,11 +157,12 @@ async function produceArtifact({
|
|||||||
exist[proxy.name] = true;
|
exist[proxy.name] = true;
|
||||||
}
|
}
|
||||||
// produce
|
// produce
|
||||||
return ProxyUtils.produce(proxies, platform);
|
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||||
} else if (type === 'collection') {
|
} else if (type === 'collection') {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
const collection = findByName(allCols, name);
|
const collection = findByName(allCols, name);
|
||||||
|
if (!collection) throw new Error(`找不到组合订阅 ${name}`);
|
||||||
const subnames = collection.subscriptions;
|
const subnames = collection.subscriptions;
|
||||||
const results = {};
|
const results = {};
|
||||||
const errors = {};
|
const errors = {};
|
||||||
@@ -131,13 +182,34 @@ async function produceArtifact({
|
|||||||
) {
|
) {
|
||||||
raw = sub.content;
|
raw = sub.content;
|
||||||
} else {
|
} else {
|
||||||
|
const errors = {};
|
||||||
raw = await await Promise.all(
|
raw = await await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, sub.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
!sub.ignoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
raw.unshift(sub.content);
|
raw.unshift(sub.content);
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
} else if (sub.mergeSources === 'remoteFirst') {
|
||||||
@@ -174,15 +246,21 @@ async function produceArtifact({
|
|||||||
$.error(
|
$.error(
|
||||||
`❌ 处理组合订阅中的子订阅: ${
|
`❌ 处理组合订阅中的子订阅: ${
|
||||||
sub.name
|
sub.name
|
||||||
}时出现错误:${err},该订阅已被跳过!进度--${
|
}时出现错误:${err}!进度--${
|
||||||
100 * (processed / subnames.length).toFixed(1)
|
100 * (processed / subnames.length).toFixed(1)
|
||||||
}%`,
|
}%`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
|
||||||
if (Object.keys(errors).length > 0) {
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!collectionIgnoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
||||||
', ',
|
', ',
|
||||||
@@ -227,10 +305,11 @@ async function produceArtifact({
|
|||||||
}
|
}
|
||||||
exist[proxy.name] = true;
|
exist[proxy.name] = true;
|
||||||
}
|
}
|
||||||
return ProxyUtils.produce(proxies, platform);
|
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||||
} else if (type === 'rule') {
|
} else if (type === 'rule') {
|
||||||
const allRules = $.read(RULES_KEY);
|
const allRules = $.read(RULES_KEY);
|
||||||
const rule = findByName(allRules, name);
|
const rule = findByName(allRules, name);
|
||||||
|
if (!rule) throw new Error(`找不到规则 ${name}`);
|
||||||
let rules = [];
|
let rules = [];
|
||||||
for (let i = 0; i < rule.urls.length; i++) {
|
for (let i = 0; i < rule.urls.length; i++) {
|
||||||
const url = rule.urls[i];
|
const url = rule.urls[i];
|
||||||
@@ -255,10 +334,114 @@ async function produceArtifact({
|
|||||||
]);
|
]);
|
||||||
// produce output
|
// produce output
|
||||||
return RuleUtils.produce(rules, platform);
|
return RuleUtils.produce(rules, platform);
|
||||||
|
} else if (type === 'file') {
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const file = findByName(allFiles, name);
|
||||||
|
if (!file) throw new Error(`找不到文件 ${name}`);
|
||||||
|
let raw;
|
||||||
|
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||||
|
raw = content;
|
||||||
|
} else if (url) {
|
||||||
|
const errors = {};
|
||||||
|
raw = await Promise.all(
|
||||||
|
url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, ua || file.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||||
|
if (
|
||||||
|
ignoreFailedRemoteFile != null &&
|
||||||
|
ignoreFailedRemoteFile !== ''
|
||||||
|
) {
|
||||||
|
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||||
|
}
|
||||||
|
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mergeSources === 'localFirst') {
|
||||||
|
raw.unshift(content);
|
||||||
|
} else if (mergeSources === 'remoteFirst') {
|
||||||
|
raw.push(content);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
file.source === 'local' &&
|
||||||
|
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||||
|
) {
|
||||||
|
raw = file.content;
|
||||||
|
} else {
|
||||||
|
const errors = {};
|
||||||
|
raw = await Promise.all(
|
||||||
|
file.url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, ua || file.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||||
|
if (
|
||||||
|
ignoreFailedRemoteFile != null &&
|
||||||
|
ignoreFailedRemoteFile !== ''
|
||||||
|
) {
|
||||||
|
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||||
|
}
|
||||||
|
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.mergeSources === 'localFirst') {
|
||||||
|
raw.unshift(file.content);
|
||||||
|
} else if (file.mergeSources === 'remoteFirst') {
|
||||||
|
raw.push(file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
||||||
|
let filesContent = files
|
||||||
|
.filter((i) => i != null && i !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
const processed =
|
||||||
|
Array.isArray(file.process) && file.process.length > 0
|
||||||
|
? await ProxyUtils.process(
|
||||||
|
{ $files: files, $content: filesContent },
|
||||||
|
file.process,
|
||||||
|
)
|
||||||
|
: { $content: filesContent, $files: files };
|
||||||
|
|
||||||
|
return processed?.$content ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncAllArtifacts(_, res) {
|
async function syncArtifacts() {
|
||||||
$.info('开始同步所有远程配置...');
|
$.info('开始同步所有远程配置...');
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const files = {};
|
const files = {};
|
||||||
@@ -297,6 +480,15 @@ async function syncAllArtifacts(_, res) {
|
|||||||
|
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
$.info('全部订阅同步成功!');
|
$.info('全部订阅同步成功!');
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`同步订阅失败,原因:${e.message ?? e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function syncAllArtifacts(_, res) {
|
||||||
|
$.info('开始同步所有远程配置...');
|
||||||
|
try {
|
||||||
|
await syncArtifacts();
|
||||||
success(res);
|
success(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed(
|
failed(
|
||||||
@@ -314,10 +506,12 @@ async function syncAllArtifacts(_, res) {
|
|||||||
async function syncArtifact(req, res) {
|
async function syncArtifact(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
$.info(`开始同步远程配置 ${name}...`);
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const artifact = findByName(allArtifacts, name);
|
const artifact = findByName(allArtifacts, name);
|
||||||
|
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
|
$.error(`找不到远程配置 ${name}`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new ResourceNotFoundError(
|
new ResourceNotFoundError(
|
||||||
@@ -356,6 +550,7 @@ async function syncArtifact(req, res) {
|
|||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
success(res, artifact);
|
success(res, artifact);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
@@ -367,4 +562,4 @@ async function syncArtifact(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { produceArtifact };
|
export { produceArtifact, syncArtifacts };
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { findByName } from '@/utils/database';
|
|||||||
import { HTTP, ENV } from '@/vendor/open-api';
|
import { HTTP, ENV } from '@/vendor/open-api';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
|
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||||
|
import { getFlowField } from '@/utils/flow';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
|
||||||
const tasks = new Map();
|
const tasks = new Map();
|
||||||
|
|
||||||
export default async function download(url, ua) {
|
export default async function download(url, ua, timeout) {
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
if (rawArgs.length > 1) {
|
if (rawArgs.length > 1) {
|
||||||
@@ -45,17 +47,19 @@ export default async function download(url, ua) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { isNode } = ENV();
|
const { isNode } = ENV();
|
||||||
const { defaultUserAgent } = $.read(SETTINGS_KEY);
|
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
|
||||||
ua = ua || defaultUserAgent || 'clash.meta';
|
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||||
const id = hex_md5(ua + url);
|
const requestTimeout = timeout || defaultTimeout;
|
||||||
|
const id = hex_md5(userAgent + url);
|
||||||
if (!isNode && tasks.has(id)) {
|
if (!isNode && tasks.has(id)) {
|
||||||
return tasks.get(id);
|
return tasks.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const http = HTTP({
|
const http = HTTP({
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': ua,
|
'User-Agent': userAgent,
|
||||||
},
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = new Promise((resolve, reject) => {
|
const result = new Promise((resolve, reject) => {
|
||||||
@@ -64,10 +68,18 @@ export default async function download(url, ua) {
|
|||||||
if (!$arguments?.noCache && cached) {
|
if (!$arguments?.noCache && cached) {
|
||||||
resolve(cached);
|
resolve(cached);
|
||||||
} else {
|
} else {
|
||||||
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
|
$.info(
|
||||||
|
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||||
|
);
|
||||||
http.get(url)
|
http.get(url)
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
const body = resp.body;
|
const { body, headers } = resp;
|
||||||
|
if (headers) {
|
||||||
|
const flowInfo = getFlowField(headers);
|
||||||
|
if (flowInfo) {
|
||||||
|
headersResourceCache.set(url, flowInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (body.replace(/\s/g, '').length === 0)
|
if (body.replace(/\s/g, '').length === 0)
|
||||||
reject(new Error('远程资源内容为空!'));
|
reject(new Error('远程资源内容为空!'));
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,21 +1,85 @@
|
|||||||
|
import { SETTINGS_KEY } from '@/constants';
|
||||||
import { HTTP } from '@/vendor/open-api';
|
import { HTTP } from '@/vendor/open-api';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||||
|
|
||||||
export async function getFlowHeaders(url) {
|
export function getFlowField(headers) {
|
||||||
const http = HTTP();
|
|
||||||
const { headers } = await http.get({
|
|
||||||
url: url
|
|
||||||
.split(/[\r\n]+/)
|
|
||||||
.map((i) => i.trim())
|
|
||||||
.filter((i) => i.length)[0],
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const subkey = Object.keys(headers).filter((k) =>
|
const subkey = Object.keys(headers).filter((k) =>
|
||||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||||
)[0];
|
)[0];
|
||||||
return headers[subkey];
|
return headers[subkey];
|
||||||
}
|
}
|
||||||
|
export async function getFlowHeaders(url, ua, timeout) {
|
||||||
|
let $arguments = {};
|
||||||
|
const rawArgs = url.split('#');
|
||||||
|
if (rawArgs.length > 1) {
|
||||||
|
try {
|
||||||
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
|
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||||
|
} catch (e) {
|
||||||
|
for (const pair of rawArgs[1].split('&')) {
|
||||||
|
const key = pair.split('=')[0];
|
||||||
|
const value = pair.split('=')[1];
|
||||||
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||||
|
$arguments[key] =
|
||||||
|
value == null || value === ''
|
||||||
|
? true
|
||||||
|
: decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cached = headersResourceCache.get(url);
|
||||||
|
let flowInfo;
|
||||||
|
if (!$arguments?.noCache && cached) {
|
||||||
|
// $.info(`使用缓存的流量信息: ${url}`);
|
||||||
|
flowInfo = cached;
|
||||||
|
} else {
|
||||||
|
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
|
||||||
|
const userAgent =
|
||||||
|
ua ||
|
||||||
|
defaultFlowUserAgent ||
|
||||||
|
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
|
||||||
|
const requestTimeout = timeout || defaultTimeout;
|
||||||
|
const http = HTTP();
|
||||||
|
try {
|
||||||
|
// $.info(`使用 HEAD 方法获取流量信息: ${url}`);
|
||||||
|
const { headers } = await http.head({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!flowInfo) {
|
||||||
|
$.info(`使用 GET 方法获取流量信息: ${url}`);
|
||||||
|
const { headers } = await http.get({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
}
|
||||||
|
if (flowInfo) {
|
||||||
|
headersResourceCache.set(url, flowInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flowInfo;
|
||||||
|
}
|
||||||
export function parseFlowHeaders(flowHeaders) {
|
export function parseFlowHeaders(flowHeaders) {
|
||||||
if (!flowHeaders) return;
|
if (!flowHeaders) return;
|
||||||
// unit is KB
|
// unit is KB
|
||||||
|
|||||||
107
backend/src/utils/headers-resource-cache.js
Normal file
107
backend/src/utils/headers-resource-cache.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import $ from '@/core/app';
|
||||||
|
import {
|
||||||
|
HEADERS_RESOURCE_CACHE_KEY,
|
||||||
|
CHR_EXPIRATION_TIME_KEY,
|
||||||
|
} from '@/constants';
|
||||||
|
|
||||||
|
class ResourceCache {
|
||||||
|
constructor() {
|
||||||
|
this.expires = getExpiredTime();
|
||||||
|
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
|
||||||
|
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
|
||||||
|
}
|
||||||
|
this.resourceCache = JSON.parse($.read(HEADERS_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), HEADERS_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(CHR_EXPIRATION_TIME_KEY));
|
||||||
|
if (!$.read(CHR_EXPIRATION_TIME_KEY)) {
|
||||||
|
$.write('6e4', CHR_EXPIRATION_TIME_KEY); // 1分钟
|
||||||
|
}
|
||||||
|
let expiration = 6e4;
|
||||||
|
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(
|
||||||
|
'#\u54cd\u5e94\u5934\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(CHR_EXPIRATION_TIME_KEY);
|
||||||
|
return expiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ResourceCache();
|
||||||
@@ -13,6 +13,12 @@ function isIPv6(ip) {
|
|||||||
return IPV6_REGEX.test(ip);
|
return IPV6_REGEX.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidPortNumber(port) {
|
||||||
|
return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isNotBlank(str) {
|
function isNotBlank(str) {
|
||||||
return typeof str === 'string' && str.trim().length > 0;
|
return typeof str === 'string' && str.trim().length > 0;
|
||||||
}
|
}
|
||||||
@@ -29,4 +35,12 @@ function getIfPresent(obj, defaultValue) {
|
|||||||
return isPresent(obj) ? obj : defaultValue;
|
return isPresent(obj) ? obj : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent };
|
export {
|
||||||
|
isIPv4,
|
||||||
|
isIPv6,
|
||||||
|
isValidPortNumber,
|
||||||
|
isNotBlank,
|
||||||
|
getIfNotBlank,
|
||||||
|
isPresent,
|
||||||
|
getIfPresent,
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export function getPlatformFromHeaders(headers) {
|
|||||||
}
|
}
|
||||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||||
return 'QX';
|
return 'QX';
|
||||||
|
} else if (UA.indexOf('Surfboard') !== -1) {
|
||||||
|
return 'Surfboard';
|
||||||
} else if (UA.indexOf('Surge Mac') !== -1) {
|
} else if (UA.indexOf('Surge Mac') !== -1) {
|
||||||
return 'SurgeMac';
|
return 'SurgeMac';
|
||||||
} else if (UA.indexOf('Surge') !== -1) {
|
} else if (UA.indexOf('Surge') !== -1) {
|
||||||
@@ -18,7 +20,7 @@ export function getPlatformFromHeaders(headers) {
|
|||||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||||
return 'Loon';
|
return 'Loon';
|
||||||
} else if (UA.indexOf('Shadowrocket') !== -1) {
|
} else if (UA.indexOf('Shadowrocket') !== -1) {
|
||||||
return 'ShadowRocket';
|
return 'Shadowrocket';
|
||||||
} else if (UA.indexOf('Stash') !== -1) {
|
} else if (UA.indexOf('Stash') !== -1) {
|
||||||
return 'Stash';
|
return 'Stash';
|
||||||
} else if (
|
} else if (
|
||||||
@@ -30,6 +32,8 @@ export function getPlatformFromHeaders(headers) {
|
|||||||
return 'Clash';
|
return 'Clash';
|
||||||
} else if (ua.indexOf('v2ray') !== -1) {
|
} else if (ua.indexOf('v2ray') !== -1) {
|
||||||
return 'V2Ray';
|
return 'V2Ray';
|
||||||
|
} else if (ua.indexOf('sing-box') !== -1) {
|
||||||
|
return 'sing-box';
|
||||||
} else {
|
} else {
|
||||||
return 'JSON';
|
return 'JSON';
|
||||||
}
|
}
|
||||||
|
|||||||
26
backend/src/vendor/open-api.js
vendored
26
backend/src/vendor/open-api.js
vendored
@@ -191,6 +191,32 @@ export class OpenAPI {
|
|||||||
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
||||||
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
||||||
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
||||||
|
|
||||||
|
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
|
||||||
|
if (push) {
|
||||||
|
const url = push
|
||||||
|
.replace(
|
||||||
|
'[推送标题]',
|
||||||
|
encodeURIComponent(title || 'Sub-Store'),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'[推送内容]',
|
||||||
|
encodeURIComponent(
|
||||||
|
[subtitle, content_].map((i) => i).join('\n'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const $http = HTTP();
|
||||||
|
$http
|
||||||
|
.get({ url })
|
||||||
|
.then((resp) => {
|
||||||
|
console.log(
|
||||||
|
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#!homepage=https://github.com/sub-store-org/Sub-Store
|
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||||
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
||||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||||
|
#!select = 响应头缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||||
|
|
||||||
[Rule]
|
[Rule]
|
||||||
DOMAIN,sub-store.vercel.app,PROXY
|
DOMAIN,sub-store.vercel.app,PROXY
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
|||||||
### 3. QX
|
### 3. QX
|
||||||
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
|
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
|
||||||
|
|
||||||
|
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
|
||||||
|
|
||||||
### 4. Stash
|
### 4. Stash
|
||||||
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
|
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user