Compare commits

..

16 Commits

Author SHA1 Message Date
xream
32dcca4a26 feat: 域名解析支持自定义 DoH(需新版前端) 2024-06-20 21:42:15 +08:00
xream
a5d77c39c8 feat: 域名解析增加超时参数(默认使用全局超时) 2024-06-20 13:41:05 +08:00
xream
6ea1b69a62 doc: demo.js 增加更多字段的说明 2024-06-20 11:44:07 +08:00
xream
2b3b9177e5 feat: 域名解析新增 _resolved_ips 为解析出的所有 IP 2024-06-20 11:28:17 +08:00
xream
91aab3ca7a fix: 修复 Tencent DNS 缓存 2024-06-20 10:59:06 +08:00
xream
c1a9fc6abc fix: 修复 Loon Hysteria2 salamander 混淆 2024-06-17 11:08:43 +08:00
xream
11d9ce7372 feat: 支持 Loon Hysteria2 salamander 混淆 2024-06-16 21:49:13 +08:00
xream
ad3d2270ac feat: 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint 2024-06-13 20:44:12 +08:00
xream
3ad42f2c10 feat: Stash 支持 juicity, ssh 2024-06-12 15:16:56 +08:00
xream
ec06eb8659 fix: sing-box tls cert 应该为数组 2024-06-10 19:10:57 +08:00
xream
4a23a4d8b6 fix: tlsParser typo 2024-06-10 19:07:19 +08:00
xream
913638a233 feat: /api/sub/flow/:name 接口支持指定远程订阅 url(可携带订阅 url 支持的参数, 例如 flowUserAgent) 2024-06-10 13:24:06 +08:00
xream
bf642ce0e6 fix: 兼容空的订阅链接 2024-06-09 01:42:40 +08:00
xream
1ecac9da92 chore: demo.js 2024-06-06 21:50:13 +08:00
xream
c5a417da8f feat: VMess URI 支持 TCP/H2 传输层 2024-06-03 21:14:07 +08:00
xream
8cd0545023 feat: ws, http, h2 传输层补全 path 2024-06-03 00:34:03 +08:00
24 changed files with 380 additions and 114 deletions

View File

@@ -35,7 +35,7 @@ Core functionalities:
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard)) - [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard) - [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.331", "version": "2.14.343",
"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": {
@@ -20,11 +20,14 @@
"@maxmind/geoip2-node": "^5.0.0", "@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7", "automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6", "cron": "^3.1.6",
"dns-packet": "^5.6.1",
"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",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"request": "^2.88.2", "request": "^2.88.2",
"requests": "^0.3.0", "requests": "^0.3.0",

41
backend/pnpm-lock.yaml generated
View File

@@ -14,12 +14,18 @@ dependencies:
body-parser: body-parser:
specifier: ^1.19.0 specifier: ^1.19.0
version: registry.npmmirror.com/body-parser@1.19.0 version: registry.npmmirror.com/body-parser@1.19.0
buffer:
specifier: ^6.0.3
version: registry.npmmirror.com/buffer@6.0.3
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: cron:
specifier: ^3.1.6 specifier: ^3.1.6
version: registry.npmmirror.com/cron@3.1.6 version: registry.npmmirror.com/cron@3.1.6
dns-packet:
specifier: ^5.6.1
version: registry.npmmirror.com/dns-packet@5.6.1
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
@@ -29,6 +35,9 @@ dependencies:
js-base64: js-base64:
specifier: ^3.7.2 specifier: ^3.7.2
version: registry.npmmirror.com/js-base64@3.7.2 version: registry.npmmirror.com/js-base64@3.7.2
jsrsasign:
specifier: ^11.1.0
version: registry.npmmirror.com/jsrsasign@11.1.0
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: registry.npmmirror.com/lodash@4.17.21 version: registry.npmmirror.com/lodash@4.17.21
@@ -1970,6 +1979,12 @@ packages:
'@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13 '@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13
dev: true dev: true
registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5:
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz}
name: '@leichtgewicht/ip-codec'
version: 2.0.5
dev: false
registry.npmmirror.com/@maxmind/geoip2-node@5.0.0: registry.npmmirror.com/@maxmind/geoip2-node@5.0.0:
resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@maxmind/geoip2-node/-/geoip2-node-5.0.0.tgz} resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@maxmind/geoip2-node/-/geoip2-node-5.0.0.tgz}
name: '@maxmind/geoip2-node' name: '@maxmind/geoip2-node'
@@ -2704,7 +2719,6 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz}
name: base64-js name: base64-js
version: 1.5.1 version: 1.5.1
dev: true
registry.npmmirror.com/base@0.11.2: registry.npmmirror.com/base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base/-/base-0.11.2.tgz} resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base/-/base-0.11.2.tgz}
@@ -3075,6 +3089,15 @@ packages:
ieee754: registry.npmmirror.com/ieee754@1.2.1 ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: true dev: true
registry.npmmirror.com/buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz}
name: buffer
version: 6.0.3
dependencies:
base64-js: registry.npmmirror.com/base64-js@1.5.1
ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: false
registry.npmmirror.com/builtin-status-codes@3.0.0: registry.npmmirror.com/builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz} resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz}
name: builtin-status-codes name: builtin-status-codes
@@ -4044,6 +4067,15 @@ packages:
randombytes: registry.npmmirror.com/randombytes@2.1.0 randombytes: registry.npmmirror.com/randombytes@2.1.0
dev: true dev: true
registry.npmmirror.com/dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dns-packet/-/dns-packet-5.6.1.tgz}
name: dns-packet
version: 5.6.1
engines: {node: '>=6'}
dependencies:
'@leichtgewicht/ip-codec': registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5
dev: false
registry.npmmirror.com/doctrine@3.0.0: registry.npmmirror.com/doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz}
name: doctrine name: doctrine
@@ -5871,7 +5903,6 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz}
name: ieee754 name: ieee754
version: 1.2.1 version: 1.2.1
dev: true
registry.npmmirror.com/ignore-by-default@1.0.1: registry.npmmirror.com/ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz} resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz}
@@ -6634,6 +6665,12 @@ packages:
verror: registry.npmmirror.com/verror@1.10.0 verror: registry.npmmirror.com/verror@1.10.0
dev: false dev: false
registry.npmmirror.com/jsrsasign@11.1.0:
resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsrsasign/-/jsrsasign-11.1.0.tgz}
name: jsrsasign
version: 11.1.0
dev: false
registry.npmmirror.com/just-debounce@1.1.0: registry.npmmirror.com/just-debounce@1.1.0:
resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/just-debounce/-/just-debounce-1.1.0.tgz} resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/just-debounce/-/just-debounce-1.1.0.tgz}
name: just-debounce name: just-debounce

View File

@@ -1,12 +1,8 @@
import { Buffer } from 'buffer';
import rs from '@/utils/rs';
import YAML from '@/utils/yaml'; import YAML from '@/utils/yaml';
import download from '@/utils/download'; import download from '@/utils/download';
import { import { isIPv4, isIPv6, isValidPortNumber, isNotBlank } from '@/utils';
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
} 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';
@@ -83,7 +79,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
let noCache; let noCache;
let url = content; let url = content || '';
if (url.endsWith('#noCache')) { if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, ''); url = url.replace(/#noCache$/, '');
noCache = true; noCache = true;
@@ -338,7 +334,11 @@ function lastParse(proxy) {
proxy.network = 'tcp'; proxy.network = 'tcp';
} }
} }
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
proxy.tls = true; proxy.tls = true;
} }
if (proxy.network) { if (proxy.network) {
@@ -422,6 +422,7 @@ function lastParse(proxy) {
} }
} }
} }
if (typeof proxy.name !== 'string') { if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) { if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`; proxy.name = `${proxy.name}`;
@@ -430,7 +431,7 @@ function lastParse(proxy) {
if (proxy.name?.data) { if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8'); proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else { } else {
proxy.name = utf8ArrayToStr(proxy.name); proxy.name = Buffer.from(proxy.name).toString('utf8');
} }
} catch (e) { } catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`); $.error(`proxy.name decode failed\nReason: ${e}`);
@@ -438,9 +439,46 @@ function lastParse(proxy) {
} }
} }
} }
if (['ws', 'http', 'h2'].includes(proxy.network)) {
if (
['ws', 'h2'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.path
) {
proxy[`${proxy.network}-opts`] =
proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].path = '/';
} else if (
proxy.network === 'http' &&
(!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
) {
proxy[`${proxy.network}-opts`] =
proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].path = ['/'];
}
}
if (['', 'off'].includes(proxy.sni)) { if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true; proxy['disable-sni'] = true;
} }
let caStr = proxy['ca_str'];
if (proxy['ca-str']) {
caStr = proxy['ca-str'];
} else if (caStr) {
delete proxy['ca_str'];
proxy['ca-str'] = caStr;
}
try {
if ($.env.isNode && !caStr && proxy['_ca']) {
caStr = $.node.fs.readFileSync(proxy['_ca'], {
encoding: 'utf8',
});
}
} catch (e) {
$.error(`Read ca file failed\nReason: ${e}`);
}
if (!proxy['tls-fingerprint'] && caStr) {
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
}
return proxy; return proxy;
} }

View File

@@ -305,8 +305,9 @@ function URI_VMess() {
if (params.net === 'ws' || params.obfs === 'websocket') { if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws'; proxy.network = 'ws';
} else if ( } else if (
['tcp', 'http'].includes(params.net) || ['http'].includes(params.net) ||
params.obfs === 'http' ['http'].includes(params.obfs) ||
['http'].includes(params.type)
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) { } else if (['grpc'].includes(params.net)) {
@@ -317,6 +318,8 @@ function URI_VMess() {
) { ) {
proxy.network = 'ws'; proxy.network = 'ws';
httpupgrade = true; httpupgrade = true;
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
} }
if (proxy.network) { if (proxy.network) {
let transportHost = params.host ?? params.obfsParam; let transportHost = params.host ?? params.obfsParam;
@@ -332,6 +335,10 @@ function URI_VMess() {
if (proxy.network === 'http') { if (proxy.network === 'http') {
if (transportHost) { if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开
transportHost = transportHost
.split(',')
.map((i) => i.trim());
transportHost = Array.isArray(transportHost) transportHost = Array.isArray(transportHost)
? transportHost[0] ? transportHost[0]
: transportHost; : transportHost;

View File

@@ -68,7 +68,7 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
@@ -178,6 +178,7 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -66,7 +66,7 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
@@ -176,6 +176,7 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -3,11 +3,13 @@ import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical'; import { FULL } from '@/utils/logical';
import { getFlag, removeFlag } from '@/utils/geo'; import { getFlag, removeFlag } from '@/utils/geo';
import { doh } from '@/utils/dns';
import lodash from 'lodash'; 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 { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import env from '@/utils/env'; import env from '@/utils/env';
import { import {
@@ -389,7 +391,28 @@ function parseIP4P(IP4P) {
} }
const DOMAIN_RESOLVERS = { const DOMAIN_RESOLVERS = {
Google: async function (domain, type, noCache) { Custom: async function (domain, type, noCache, timeout, url) {
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const res = await doh({
url,
domain,
type: type === 'IPv6' ? 'AAAA' : 'A',
timeout,
});
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers.map((i) => i?.data).filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result);
return result;
},
Google: async function (domain, type, noCache, timeout) {
const id = hex_md5(`GOOGLE:${domain}:${type}`); const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
@@ -400,20 +423,24 @@ const DOMAIN_RESOLVERS = {
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`); throw new Error(`Status is ${body['Status']}`);
} }
const answers = body['Answer']; const answers = body['Answer'];
if (answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers.map((i) => i?.data).filter((i) => i);
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1].data;
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
'IP-API': async function (domain, type, noCache) { 'IP-API': async function (domain, type, noCache, timeout) {
if (['IPv6'].includes(type)) { if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`); throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
} }
@@ -424,16 +451,23 @@ const DOMAIN_RESOLVERS = {
url: `http://ip-api.com/json/${encodeURIComponent( url: `http://ip-api.com/json/${encodeURIComponent(
domain, domain,
)}?lang=zh-CN`, )}?lang=zh-CN`,
timeout,
}); });
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
if (body['status'] !== 'success') { if (body['status'] !== 'success') {
throw new Error(`Status is ${body['status']}`); throw new Error(`Status is ${body['status']}`);
} }
const result = body.query; if (!body.query || body.query === 0) {
throw new Error('No answers');
}
const result = [body.query];
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Cloudflare: async function (domain, type, noCache) { Cloudflare: async function (domain, type, noCache, timeout) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`); const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
@@ -444,20 +478,24 @@ const DOMAIN_RESOLVERS = {
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`); throw new Error(`Status is ${body['Status']}`);
} }
const answers = body['Answer']; const answers = body['Answer'];
if (answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers.map((i) => i?.data).filter((i) => i);
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1].data;
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain, type, noCache) { Ali: async function (domain, type, noCache, timeout) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
@@ -468,17 +506,21 @@ const DOMAIN_RESOLVERS = {
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const answers = JSON.parse(resp.body); const answers = JSON.parse(resp.body);
if (answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers;
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1];
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Tencent: async function (domain, type, noCache) { Tencent: async function (domain, type, noCache, timeout) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`TENCENT:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
@@ -488,27 +530,41 @@ const DOMAIN_RESOLVERS = {
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const answers = resp.body.split(';').map((i) => i.split(',')[0]); const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0 || String(answers) === '0') { if (answers.length === 0 || String(answers) === '0') {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1]; const result = answers;
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
}; };
function ResolveDomainOperator({ provider, type: _type, filter, cache }) { function ResolveDomainOperator({
provider,
type: _type,
filter,
cache,
url,
timeout,
}) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) { if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`); throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
} }
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4'; let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider]; const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) { if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`); throw new Error(`找不到域名解析服务提供方: ${provider}`);
} }
$.info(`Domain Resolver: [${_type}] ${provider} ${url || ''}`);
return { return {
name: 'Resolve Domain Operator', name: 'Resolve Domain Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -531,7 +587,13 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
const currentBatch = []; const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) { for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push( currentBatch.push(
resolver(domain, type, cache === 'disabled') resolver(
domain,
type,
cache === 'disabled',
requestTimeout,
url,
)
.then((ip) => { .then((ip) => {
results[domain] = ip; results[domain] = ip;
$.info( $.info(
@@ -550,10 +612,16 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
proxies.forEach((p) => { proxies.forEach((p) => {
if (!p['_no-resolve']) { if (!p['_no-resolve']) {
if (results[p.server]) { if (results[p.server]) {
p._resolved_ips = results[p.server];
const ip = Array.isArray(results[p.server])
? results[p.server][
Math.floor(
Math.random() * results[p.server].length,
)
]
: results[p.server];
if (_type === 'IP4P') { if (_type === 'IP4P') {
const { server, port } = parseIP4P( const { server, port } = parseIP4P(ip);
results[p.server],
);
if (server && port) { if (server && port) {
p._domain = p.server; p._domain = p.server;
p.server = server; p.server = server;
@@ -568,7 +636,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
} }
} else { } else {
p._domain = p.server; p._domain = p.server;
p.server = results[p.server]; p.server = ip;
p.resolved = true; p.resolved = true;
p[`_${type}`] = p.server; p[`_${type}`] = p.server;
if (!isIP(p._IP)) { if (!isIP(p._IP)) {

View File

@@ -133,9 +133,13 @@ export default function Clash_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }

View File

@@ -149,9 +149,13 @@ export default function ClashMeta_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }

View File

@@ -408,8 +408,8 @@ function wireguard(proxy) {
} }
function hysteria2(proxy) { function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) { if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
throw new Error(`obfs is unsupported`); throw new Error(`only salamander obfs is supported`);
} }
const result = new Result(proxy); const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`); result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
@@ -423,6 +423,10 @@ function hysteria2(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
result.append(`,salamander-password=${proxy['obfs-password']}`);
}
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');

View File

@@ -152,9 +152,13 @@ export default function ShadowRocket_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }

View File

@@ -202,8 +202,8 @@ const tlsParser = (proxy, parsedProxy) => {
parsedProxy.tls.alpn = [proxy.alpn]; parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) 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) 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['ca-str']) parsedProxy.tls.certificate = proxy['ca-str']; if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '') if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = { parsedProxy.tls.utls = {
enabled: true, enabled: true,

View File

@@ -21,6 +21,8 @@ export default function Stash_Producer() {
'wireguard', 'wireguard',
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'ssh',
'juicity',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'ss' && (proxy.type === 'ss' &&
![ ![
@@ -232,9 +234,13 @@ export default function Stash_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }

View File

@@ -16,7 +16,11 @@ export default function URI_Producer() {
delete proxy[key]; delete proxy[key];
} }
} }
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy.server && isIPv6(proxy.server)) { if (proxy.server && isIPv6(proxy.server)) {

View File

@@ -123,10 +123,11 @@ async function downloadSubscription(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) { ) {
try { try {
url = `${url || sub.url}` url =
`${url || sub.url}`
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length)?.[0]; .filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -283,10 +284,11 @@ async function downloadCollection(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) { ) {
try { try {
let url = `${sub.url}` let url =
`${sub.url}`
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length)?.[0]; .filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');

View File

@@ -34,6 +34,11 @@ export default function register($app) {
async function getFlowInfo(req, res) { async function getFlowInfo(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
let { url } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
if (!sub) { if (!sub) {
@@ -68,10 +73,11 @@ async function getFlowInfo(req, res) {
return; return;
} }
try { try {
let url = `${sub.url}` url =
`${url || sub.url}`
.split(/[\r\n]+/) .split(/[\r\n]+/)
.map((i) => i.trim()) .map((i) => i.trim())
.filter((i) => i.length)?.[0]; .filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');

55
backend/src/utils/dns.js Normal file
View File

@@ -0,0 +1,55 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
export async function doh({
url,
domain,
type = 'A',
timeout,
ip = '223.6.6.6',
}) {
const buf = dnsPacket.encode({
type: 'query',
id: 0,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
type,
name: domain,
},
],
additionals: [
{
type: 'OPT',
name: '.',
udpPayloadSize: 4096,
flags: 0,
options: [
{
code: 'CLIENT_SUBNET',
ip,
sourcePrefixLength: 24,
scopePrefixLength: 0,
},
],
},
],
});
const res = await $.http.get({
url: `${url}?dns=${buf
.toString('base64')
.toString('utf-8')
.replace(/=/g, '')}`,
headers: {
Accept: 'application/dns-message',
// 'Content-Type': 'application/dns-message',
},
// body: buf,
'binary-mode': true,
encoding: null, // 使用 null 编码以确保响应是原始二进制数据
timeout,
});
return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));
}

View File

@@ -15,7 +15,7 @@ import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download( export default async function download(
rawUrl, rawUrl = '',
ua, ua,
timeout, timeout,
proxy, proxy,

View File

@@ -11,7 +11,7 @@ export function getFlowField(headers) {
return headers[subkey]; return headers[subkey];
} }
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) { export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
let url = flowUrl || rawUrl; let url = flowUrl || rawUrl || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
url = url.split('#')[0]; url = url.split('#')[0];

View File

@@ -46,52 +46,52 @@ function getPolicyDescriptor(str) {
}; };
} }
const utf8ArrayToStr = // const utf8ArrayToStr =
typeof TextDecoder !== 'undefined' // typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v)) // ? (v) => new TextDecoder().decode(new Uint8Array(v))
: (function () { // : (function () {
var charCache = new Array(128); // Preallocate the cache for the common single byte chars // var charCache = new Array(128); // Preallocate the cache for the common single byte chars
var charFromCodePt = String.fromCodePoint || String.fromCharCode; // var charFromCodePt = String.fromCodePoint || String.fromCharCode;
var result = []; // var result = [];
return function (array) { // return function (array) {
var codePt, byte1; // var codePt, byte1;
var buffLen = array.length; // var buffLen = array.length;
result.length = 0; // result.length = 0;
for (var i = 0; i < buffLen; ) { // for (var i = 0; i < buffLen; ) {
byte1 = array[i++]; // byte1 = array[i++];
if (byte1 <= 0x7f) { // if (byte1 <= 0x7f) {
codePt = byte1; // codePt = byte1;
} else if (byte1 <= 0xdf) { // } else if (byte1 <= 0xdf) {
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f); // codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
} else if (byte1 <= 0xef) { // } else if (byte1 <= 0xef) {
codePt = // codePt =
((byte1 & 0x0f) << 12) | // ((byte1 & 0x0f) << 12) |
((array[i++] & 0x3f) << 6) | // ((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f); // (array[i++] & 0x3f);
} else if (String.fromCodePoint) { // } else if (String.fromCodePoint) {
codePt = // codePt =
((byte1 & 0x07) << 18) | // ((byte1 & 0x07) << 18) |
((array[i++] & 0x3f) << 12) | // ((array[i++] & 0x3f) << 12) |
((array[i++] & 0x3f) << 6) | // ((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f); // (array[i++] & 0x3f);
} else { // } else {
codePt = 63; // Cannot convert four byte code points, so use "?" instead // codePt = 63; // Cannot convert four byte code points, so use "?" instead
i += 3; // i += 3;
} // }
result.push( // result.push(
charCache[codePt] || // charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)), // (charCache[codePt] = charFromCodePt(codePt)),
); // );
} // }
return result.join(''); // return result.join('');
}; // };
})(); // })();
export { export {
isIPv4, isIPv4,
@@ -101,6 +101,6 @@ export {
getIfNotBlank, getIfNotBlank,
isPresent, isPresent,
getIfPresent, getIfPresent,
utf8ArrayToStr, // utf8ArrayToStr,
getPolicyDescriptor, getPolicyDescriptor,
}; };

11
backend/src/utils/rs.js Normal file
View File

@@ -0,0 +1,11 @@
import rs from 'jsrsasign';
export function generateFingerprint(caStr) {
const hex = rs.pemtohex(caStr);
const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');
return fingerPrint.match(/.{2}/g).join(':').toUpperCase();
}
export default {
generateFingerprint,
};

View File

@@ -341,7 +341,10 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
const request = isNode const request = isNode
? eval("require('request')") ? eval("require('request')")
: $httpClient; : $httpClient;
const body = options.body;
const opts = JSON.parse(JSON.stringify(options)); const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
if (!isNode && opts.timeout) { if (!isNode && opts.timeout) {
opts.timeout++; opts.timeout++;
let unit = 'ms'; let unit = 'ms';

View File

@@ -5,14 +5,22 @@ function operator(proxies = [], targetPlatform, context) {
// proxies 为传入的内部节点数组 // proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅 // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
// 1. `_no-resolve` 为不解析域名 // 1. `_no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `_resolved` 字段 // 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段 // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置) // 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 5. `_subName` 为单条订阅名 // 5. `_subName` 为单条订阅名
// 6. `_collectionName` 为组合订阅名 // 6. `_collectionName` 为组合订阅名
// 7. `tls-fingerprint` 为 tls 指纹
// 8. `underlying-proxy` 为前置代理
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
//
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
// $arguments 为传入的脚本参数 // $arguments 为传入的脚本参数
@@ -103,7 +111,7 @@ function operator(proxies = [], targetPlatform, context) {
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist // 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
// 见 https://t.me/zhetengsha/1428 // 见 https://t.me/zhetengsha/1428
// //
// const content = ProxyUtils.produce(proxies, platform) // const content = ProxyUtils.produce([...proxies], platform)
// // YAML // // YAML
// ProxyUtils.yaml.load('YAML String') // ProxyUtils.yaml.load('YAML String')