Compare commits

...

53 Commits

Author SHA1 Message Date
xream
153802c7c4 feat: Loon SOCKS5 UDP 2024-08-26 00:33:22 +08:00
xream
19418b631f feat(uri): VMess URI 输入支持 allowInsecure(输出不支持, 与 2dust/v2rayN 分享链接逻辑一致) 2024-08-18 15:53:13 +08:00
xream
97caeed208 feat(geo): 增加 利雅得 Riyadh 2024-08-17 14:06:28 +08:00
xream
dd8d1d85e8 feat: 支持 Loon tls-pubkey-sha256, tls-cert-sha256 2024-07-30 22:17:25 +08:00
xream
14ed56b5d5 chore: 传输层应该有配置, 暂时不考虑兼容不给配置的节点 2024-07-24 11:27:33 +08:00
xream
9785271c5b chore: 增加部分 clash.meta(mihomo) 内核客户端的 User-Agent(clash-verge, flclash) 2024-07-20 14:48:39 +08:00
xream
05bdf95a29 feat: 处理端口跳跃(感谢亚托莉佬) 2024-07-19 15:23:44 +08:00
xream
317a804b36 fix: 修复 URI 报错 2024-07-19 14:33:34 +08:00
xream
10ec8a25a2 feat: 处理不规范的 hysteria2 节点 2024-07-19 09:45:28 +08:00
xream
aa0943a909 fix: 被识别为 IP4P 的域名解析结果均增加 _IP4P 字段; 修复报错 2024-07-18 19:48:01 +08:00
xream
a0c1bbbf70 fix: 域名解析修复; 结果增加 _IP4P 字段 2024-07-18 19:42:57 +08:00
xream
fea9de4fae feat: IP4P 合并进 IPv6; ProxyUtils 中增加 ipAddress 2024-07-18 18:35:22 +08:00
xream
cddd1818fe chore: bump release version 2024-07-08 02:51:21 +08:00
xream
f94830b2df Merge pull request #339 from zhiqiang02/add-tai-wan-keyword
Add 'Tai Wan' as a keyword for Taiwan flag
2024-07-08 02:50:13 +08:00
zhiqiang02
e02a26040e Add 'Tai Wan' as a keyword for Taiwan flag
碰到了某机场奇奇怪怪的节点名
2024-07-08 02:38:50 +08:00
xream
6906efdd55 chore: bump release version 2024-07-02 21:04:14 +08:00
xream
9558b63261 Merge pull request #336 from cooip-jm/patch-1
处理grpc-opts为 {} 的情况
2024-07-02 21:03:30 +08:00
cooip-jm
4bfdef17ee 处理grpc-opts为 {} 的情况
该字段仅影响sing-box内核,对mihomo无影响
2024-07-02 21:01:11 +08:00
xream
9d29fc8a09 feat: 处理 reality-opts 为 {} 的情况 2024-07-02 20:39:04 +08:00
xream
f524920c13 feat: 文件支持设置 查询流量信息订阅链接. 服务器版中使用此链接可在响应中传递订阅流量信息 2024-06-28 18:34:26 +08:00
xream
bfe072cbdf feat: 域名解析支持自定义 EDNS(需新版前端) 2024-06-22 11:45:37 +08:00
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
xream
b6f848a6e6 feat: ProxyUtils.removeFlag 2024-06-02 18:30:53 +08:00
xream
99d058bcf1 feat: 支持 flowUrl 2024-06-02 16:03:01 +08:00
xream
533103e765 feat: 进一步优化乐观缓存和同步配置的逻辑 2024-06-01 20:09:57 +08:00
xream
cf82764171 feat: 进一步优化乐观缓存和同步配置的逻辑 2024-06-01 19:50:16 +08:00
xream
7b783c1fe3 fix: 简单修复乐观缓存(当异步更新乐观缓存时, 若存在常规缓存, 将使用常规缓存) 2024-05-31 20:52:01 +08:00
xream
372eff9a44 chore: 文案 2024-05-31 11:42:23 +08:00
xream
d3b5a529d7 doc: README 2024-05-30 21:32:48 +08:00
xream
8049134bb5 feat: Surge includeUnsupportedProxy 去除 HTTP 传输层(不一定能通, 由服务端配置确定) 2024-05-30 18:41:56 +08:00
xream
3f620700a4 feat: GUIforCores 请求增加参数 proxy, timeout 2024-05-30 17:19:38 +08:00
xream
9e64a68481 fix: VMess URI 输入传输层为 HTTP 时, path 默认为 / 2024-05-30 14:28:02 +08:00
xream
9ce5916414 fix: 乐观缓存未捕获错误 2024-05-30 13:10:08 +08:00
xream
047c21fe70 feat: 节点上的额外参数调整为下划线开头, 原参数目前仍保留, 若有脚本需要使用这些参数请尽快修改(_subName, _collectionName, _resolved, _no-resolve) 2024-05-30 04:48:13 +08:00
xream
47849dc6d0 feat: 节点上的额外参数调整为下划线开头, 原参数目前仍保留, 若有脚本需要使用这些参数请尽快修改(_subName, _collectionName, _resolved) 2024-05-30 04:28:54 +08:00
xream
af06086c1b chore: 去除 Surge/Surfboard 输出节点名中的逗号和等号 2024-05-29 19:15:52 +08:00
xream
4a6a147667 feat: 新增 定时处理订阅 功能, 避免 App 内拉取超时 2024-05-28 12:05:35 +08:00
xream
c6540d14cd feat: Surge Beta 模块支持定时处理订阅. 一般用于定时处理耗时较长的订阅, 以更新缓存. 这样 Surge 中拉取时就能用到缓存, 不至于总是超时 2024-05-28 02:31:25 +08:00
35 changed files with 905 additions and 244 deletions

View File

@@ -35,7 +35,7 @@ Core functionalities:
- [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] 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)
### Supported Target Platforms

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.317",
"version": "2.14.362",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -20,11 +20,15 @@
"@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6",
"dns-packet": "^5.6.1",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21",
"request": "^2.88.2",
"requests": "^0.3.0",

66
backend/pnpm-lock.yaml generated
View File

@@ -14,21 +14,33 @@ dependencies:
body-parser:
specifier: ^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:
specifier: ^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
dns-packet:
specifier: ^5.6.1
version: registry.npmmirror.com/dns-packet@5.6.1
express:
specifier: ^4.17.1
version: registry.npmmirror.com/express@4.17.1
http-proxy-middleware:
specifier: ^2.0.6
version: registry.npmmirror.com/http-proxy-middleware@2.0.6
ip-address:
specifier: ^9.0.5
version: registry.npmmirror.com/ip-address@9.0.5
js-base64:
specifier: ^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:
specifier: ^4.17.21
version: registry.npmmirror.com/lodash@4.17.21
@@ -1970,6 +1982,12 @@ packages:
'@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13
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:
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'
@@ -2704,7 +2722,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}
name: base64-js
version: 1.5.1
dev: true
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}
@@ -3075,6 +3092,15 @@ packages:
ieee754: registry.npmmirror.com/ieee754@1.2.1
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:
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
@@ -4044,6 +4070,15 @@ packages:
randombytes: registry.npmmirror.com/randombytes@2.1.0
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:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz}
name: doctrine
@@ -5871,7 +5906,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}
name: ieee754
version: 1.2.1
dev: true
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}
@@ -6000,6 +6034,16 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
registry.npmmirror.com/ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz}
name: ip-address
version: 9.0.5
engines: {node: '>= 12'}
dependencies:
jsbn: registry.npmmirror.com/jsbn@1.1.0
sprintf-js: registry.npmmirror.com/sprintf-js@1.1.3
dev: false
registry.npmmirror.com/ip6addr@0.2.5:
resolution: {integrity: sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ip6addr/-/ip6addr-0.2.5.tgz}
name: ip6addr
@@ -6545,6 +6589,12 @@ packages:
version: 0.1.1
dev: false
registry.npmmirror.com/jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz}
name: jsbn
version: 1.1.0
dev: false
registry.npmmirror.com/jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz}
name: jsesc
@@ -6634,6 +6684,12 @@ packages:
verror: registry.npmmirror.com/verror@1.10.0
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:
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
@@ -9058,6 +9114,12 @@ packages:
version: 1.0.3
dev: false
registry.npmmirror.com/sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz}
name: sprintf-js
version: 1.1.3
dev: false
registry.npmmirror.com/sshpk@1.16.1:
resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sshpk/-/sshpk-1.16.1.tgz}
name: sshpk

View File

@@ -1,3 +1,5 @@
import { Buffer } from 'buffer';
import rs from '@/utils/rs';
import YAML from '@/utils/yaml';
import download from '@/utils/download';
import {
@@ -5,7 +7,8 @@ import {
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
ipAddress,
getRandomPort,
} from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
@@ -15,7 +18,7 @@ import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import { getFlag, getISO, MMDB } from '@/utils/geo';
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist';
function preprocess(raw) {
@@ -83,7 +86,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
const { mode, content } = item.args;
if (mode === 'link') {
let noCache;
let url = content;
let url = content || '';
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
@@ -199,6 +202,10 @@ function produce(proxies, targetPlatform, type, opts = {}) {
);
proxies = proxies.map((proxy) => {
proxy._subName = proxy.subName;
proxy._collectionName = proxy.collectionName;
proxy._resolved = proxy.resolved;
if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
@@ -214,6 +221,17 @@ function produce(proxies, targetPlatform, type, opts = {}) {
delete proxy['tls-fingerprint'];
}
}
// 处理 端口跳跃
if (proxy.ports) {
if (!['ClashMeta'].includes(targetPlatform)) {
proxy.ports = proxy.ports.replace(/\//g, ',');
}
if (!proxy.port) {
proxy.port = getRandomPort(proxy.ports);
}
}
return proxy;
});
@@ -267,11 +285,14 @@ export const ProxyUtils = {
parse,
process: processFn,
produce,
ipAddress,
getRandomPort,
isIPv4,
isIPv6,
isIP,
yaml: YAML,
getFlag,
removeFlag,
getISO,
MMDB,
Gist,
@@ -333,7 +354,11 @@ function lastParse(proxy) {
proxy.network = 'tcp';
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
proxy.tls = true;
}
if (proxy.network) {
@@ -402,7 +427,30 @@ function lastParse(proxy) {
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
delete proxy.ports;
}
if (
['hysteria2'].includes(proxy.type) &&
proxy.obfs &&
!['salamander'].includes(proxy.obfs) &&
!proxy['obfs-password']
) {
proxy['obfs-password'] = proxy.obfs;
proxy.obfs = 'salamander';
}
if (['vless'].includes(proxy.type)) {
// 删除 reality-opts: {}
if (
proxy['reality-opts'] &&
Object.keys(proxy['reality-opts']).length === 0
) {
delete proxy['reality-opts'];
}
// 删除 grpc-opts: {}
if (
proxy['grpc-opts'] &&
Object.keys(proxy['grpc-opts']).length === 0
) {
delete proxy['grpc-opts'];
}
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
@@ -417,6 +465,7 @@ function lastParse(proxy) {
}
}
}
if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`;
@@ -425,7 +474,7 @@ function lastParse(proxy) {
if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else {
proxy.name = utf8ArrayToStr(proxy.name);
proxy.name = Buffer.from(proxy.name).toString('utf8');
}
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);
@@ -433,9 +482,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)) {
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;
}

View File

@@ -296,6 +296,11 @@ function URI_VMess() {
? !params.verify_cert
: undefined,
};
if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
params.allowInsecure,
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
@@ -305,8 +310,9 @@ function URI_VMess() {
if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws';
} else if (
['tcp', 'http'].includes(params.net) ||
params.obfs === 'http'
['http'].includes(params.net) ||
['http'].includes(params.obfs) ||
['http'].includes(params.type)
) {
proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
@@ -317,6 +323,8 @@ function URI_VMess() {
) {
proxy.network = 'ws';
httpupgrade = true;
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
}
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
@@ -332,6 +340,10 @@ function URI_VMess() {
if (proxy.network === 'http') {
if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开
transportHost = transportHost
.split(',')
.map((i) => i.trim());
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
@@ -340,8 +352,11 @@ function URI_VMess() {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
} else {
transportPath = '/';
}
}
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (transportPath || transportHost) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {

View File

@@ -54,31 +54,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "trojan";
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/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
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/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
@@ -172,12 +172,16 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
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(); }
comma = _ "," _

View File

@@ -52,31 +52,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "trojan";
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/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
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/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
@@ -170,12 +170,15 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
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(); }
comma = _ "," _

View File

@@ -1,13 +1,15 @@
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { isIPv4, isIPv6, ipAddress } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
import { getFlag, removeFlag } from '@/utils/geo';
import { doh } from '@/utils/dns';
import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import env from '@/utils/env';
import {
@@ -362,9 +364,6 @@ function parseIP4P(IP4P) {
let server;
let port;
try {
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
throw new Error(`Invalid IP4P: ${IP4P}`);
}
let array = IP4P.split(':');
port = parseInt(array[2], 16);
@@ -389,31 +388,59 @@ function parseIP4P(IP4P) {
}
const DOMAIN_RESOLVERS = {
Google: async function (domain, type, noCache) {
Custom: async function (domain, type, noCache, timeout, edns, 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,
edns,
});
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, edns) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
)}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&edns_client_subnet=${edns}`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`);
}
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');
}
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain, type, noCache) {
'IP-API': async function (domain, type, noCache, timeout) {
if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
}
@@ -424,16 +451,23 @@ const DOMAIN_RESOLVERS = {
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
)}?lang=zh-CN`,
timeout,
});
const body = JSON.parse(resp.body);
if (body['status'] !== 'success') {
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);
return result;
},
Cloudflare: async function (domain, type, noCache) {
Cloudflare: async function (domain, type, noCache, timeout) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
@@ -444,80 +478,112 @@ const DOMAIN_RESOLVERS = {
headers: {
accept: 'application/dns-json',
},
timeout,
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`);
}
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');
}
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
Ali: async function (domain, type, noCache) {
Ali: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?edns_client_subnet=223.6.6.6/24&name=${encodeURIComponent(
url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/24&name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
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');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`);
Tencent: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`TENCENT:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?ip=119.28.28.28&type=${
url: `http://119.28.28.28/d?ip=${edns}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&dn=${encodeURIComponent(domain)}`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0 || String(answers) === '0') {
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);
return result;
},
};
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
function ResolveDomainOperator({
provider,
type: _type,
filter,
cache,
url,
timeout,
edns: _edns,
}) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`);
}
let edns = _edns || '223.6.6.6';
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
$.info(
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
);
return {
name: 'Resolve Domain Operator',
func: async (proxies) => {
proxies.forEach((p, i) => {
if (!p['_no-resolve'] && p['no-resolve']) {
proxies[i]['_no-resolve'] = p['no-resolve'];
}
});
const results = {};
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
const totalDomain = [
...new Set(
proxies
.filter((p) => !isIP(p.server) && !p['no-resolve'])
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
.map((c) => c.server),
),
];
@@ -526,7 +592,14 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push(
resolver(domain, type, cache === 'disabled')
resolver(
domain,
type,
cache === 'disabled',
requestTimeout,
edns,
url,
)
.then((ip) => {
results[domain] = ip;
$.info(
@@ -543,34 +616,58 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
await Promise.all(currentBatch);
}
proxies.forEach((p) => {
if (!p['no-resolve']) {
if (!p['_no-resolve']) {
if (results[p.server]) {
if (_type === 'IP4P') {
const { server, port } = parseIP4P(
results[p.server],
);
if (server && port) {
p._resolved_ips = results[p.server];
let ip = Array.isArray(results[p.server])
? results[p.server][
Math.floor(
Math.random() * results[p.server].length,
)
]
: results[p.server];
if (type === 'IPv6' && isIPv6(ip)) {
try {
ip = new ipAddress.Address6(ip).correctForm();
} catch (e) {
$.error(
`Failed to parse IPv6 address: ${ip}: ${e}`,
);
}
if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {
p._IP4P = ip;
const { server, port } = parseIP4P(ip);
if (server && port) {
p._domain = p.server;
p.server = server;
p.port = port;
p.resolved = true;
p._IPv4 = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} else if (!p.resolved) {
p.resolved = false;
}
} else {
p._domain = p.server;
p.server = server;
p.port = port;
p.server = ip;
p.resolved = true;
p._IPv4 = p.server;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} else {
p.resolved = false;
}
} else {
p._domain = p.server;
p.server = results[p.server];
p.server = ip;
p.resolved = true;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
}
} else {
} else if (!p.resolved) {
p.resolved = false;
}
}
@@ -578,7 +675,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
return proxies.filter((p) => {
if (filter === 'removeFailed') {
return isIP(p.server) || p['no-resolve'] || p.resolved;
return isIP(p.server) || p['_no-resolve'] || p.resolved;
} else if (filter === 'IPOnly') {
return isIP(p.server);
} else if (filter === 'IPv4Only') {
@@ -864,13 +961,6 @@ function clone(object) {
return JSON.parse(JSON.stringify(object));
}
// remove flag
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim();
}
function createDynamicFunction(name, script, $arguments) {
const flowUtils = {
getFlowField,

View File

@@ -133,9 +133,13 @@ export default function Clash_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -158,6 +162,7 @@ export default function Clash_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];

View File

@@ -149,9 +149,13 @@ export default function ClashMeta_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -173,6 +177,7 @@ export default function ClashMeta_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];

View File

@@ -153,6 +153,14 @@ function trojan(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -215,6 +223,14 @@ function vmess(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -286,6 +302,14 @@ function vless(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -339,6 +363,11 @@ function socks5(proxy) {
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
@@ -408,8 +437,8 @@ function wireguard(proxy) {
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
throw new Error(`only salamander obfs is supported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
@@ -418,11 +447,23 @@ function hysteria2(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
result.append(`,salamander-password=${proxy['obfs-password']}`);
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');

View File

@@ -152,9 +152,13 @@ export default function ShadowRocket_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -176,6 +180,7 @@ export default function ShadowRocket_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];

View File

@@ -202,8 +202,8 @@ const tlsParser = (proxy, parsedProxy) => {
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.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'] !== '')
parsedProxy.tls.utls = {
enabled: true,

View File

@@ -21,6 +21,8 @@ export default function Stash_Producer() {
'wireguard',
'hysteria',
'hysteria2',
'ssh',
'juicity',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
@@ -232,9 +234,13 @@ export default function Stash_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -265,6 +271,7 @@ export default function Stash_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];

View File

@@ -6,7 +6,7 @@ const targetPlatform = 'Surfboard';
export default function Surfboard_Producer() {
const produce = (proxy) => {
proxy.name = proxy.name.replace(/=/g, '');
proxy.name = proxy.name.replace(/=|,/g, '');
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);

View File

@@ -14,13 +14,14 @@ const ipVersions = {
export default function Surge_Producer() {
const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, '');
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
return vmess(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'socks5':
@@ -263,7 +264,7 @@ function trojan(proxy) {
return result.toString();
}
function vmess(proxy) {
function vmess(proxy, includeUnsupportedProxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
@@ -277,7 +278,7 @@ function vmess(proxy) {
);
// transport
handleTransport(result, proxy);
handleTransport(result, proxy, includeUnsupportedProxy);
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -1012,7 +1013,7 @@ function hysteria2(proxy) {
return result.toString();
}
function handleTransport(result, proxy) {
function handleTransport(result, proxy, includeUnsupportedProxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,ws=true`);
@@ -1038,7 +1039,13 @@ function handleTransport(result, proxy) {
}
}
} else {
throw new Error(`network ${proxy.network} is unsupported`);
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
$.info(
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
);
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
}
}
}

View File

@@ -10,12 +10,17 @@ export default function URI_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy.server && isIPv6(proxy.server)) {

View File

@@ -1,12 +1,30 @@
/* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64';
import URI_Producer from './uri';
import $ from '@/core/app';
const URI = URI_Producer();
export default function V2Ray_Producer() {
const type = 'ALL';
const produce = (proxies) =>
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
const produce = (proxies) => {
let result = [];
proxies.map((proxy) => {
try {
result.push(URI.produce(proxy));
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
proxy,
null,
2,
)}\nReason: ${err}`,
);
}
});
return Base64.encode(result.join('\n'));
};
return { type, produce };
}

View File

@@ -11,17 +11,65 @@ import { syncToGist } from '@/restful/artifacts';
import { findByName } from '@/utils/database';
!(async function () {
const settings = $.read(SETTINGS_KEY);
// if GitHub token is not configured
if (!settings.githubUser || !settings.gistToken) return;
let arg;
if (typeof $argument != 'undefined') {
arg = Object.fromEntries(
// eslint-disable-next-line no-undef
$argument.split('&').map((item) => item.split('=')),
);
} else {
arg = {};
}
let sub_names = (arg?.subscription ?? arg?.sub ?? '')
.split(/,|/g)
.map((i) => i.trim())
.filter((i) => i.length > 0)
.map((i) => decodeURIComponent(i));
let col_names = (arg?.collection ?? arg?.col ?? '')
.split(/,|/g)
.map((i) => i.trim())
.filter((i) => i.length > 0)
.map((i) => decodeURIComponent(i));
if (sub_names.length > 0 || col_names.length > 0) {
if (sub_names.length > 0)
await produceArtifacts(sub_names, 'subscription');
if (col_names.length > 0)
await produceArtifacts(col_names, 'collection');
} else {
const settings = $.read(SETTINGS_KEY);
// if GitHub token is not configured
if (!settings.githubUser || !settings.gistToken) return;
const artifacts = $.read(ARTIFACTS_KEY);
if (!artifacts || artifacts.length === 0) return;
const artifacts = $.read(ARTIFACTS_KEY);
if (!artifacts || artifacts.length === 0) return;
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) await doSync();
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) await doSync();
}
})().finally(() => $.done());
async function produceArtifacts(names, type) {
try {
if (names.length > 0) {
$.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`);
await Promise.all(
names.map(async (name) => {
try {
await produceArtifact({
type,
name,
});
} catch (e) {
$.error(`${type} ${name} error: ${e.message ?? e}`);
}
}),
);
$.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`);
}
} catch (e) {
$.error(`produceArtifacts error: ${e.message ?? e}`);
}
}
async function doSync() {
console.log(
`
@@ -69,6 +117,7 @@ async function doSync() {
await produceArtifact({
type: 'subscription',
name: subName,
awaitCustomCache: true,
});
} catch (e) {
// $.error(`${e.message ?? e}`);

View File

@@ -123,10 +123,11 @@ async function downloadSubscription(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
url = `${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
url =
`${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {};
const rawArgs = url.split('#');
@@ -156,6 +157,7 @@ async function downloadSubscription(req, res) {
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
@@ -282,10 +284,11 @@ async function downloadCollection(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let url =
`${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {};
const rawArgs = url.split('#');
@@ -314,6 +317,7 @@ async function downloadCollection(req, res) {
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);

View File

@@ -1,4 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
@@ -50,7 +51,15 @@ async function getFile(req, res) {
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
let {
url,
subInfoUrl,
subInfoUserAgent,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
} = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
@@ -59,6 +68,14 @@ async function getFile(req, res) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
}
if (subInfoUrl) {
subInfoUrl = decodeURIComponent(subInfoUrl);
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
}
if (subInfoUserAgent) {
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`);
@@ -86,6 +103,26 @@ async function getFile(req, res) {
ignoreFailedRemoteFile,
});
try {
subInfoUrl = subInfoUrl || file.subInfoUrl;
if (subInfoUrl) {
// forward flow headers
const flowInfo = await getFlowHeaders(
subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);

View File

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

View File

@@ -36,6 +36,7 @@ async function produceArtifact({
produceType,
produceOpts = {},
subscription,
awaitCustomCache,
}) {
platform = platform || 'JSON';
@@ -67,6 +68,8 @@ async function produceArtifact({
ua || sub.ua,
undefined,
sub.proxy,
undefined,
awaitCustomCache,
);
} catch (err) {
errors[url] = err;
@@ -112,6 +115,8 @@ async function produceArtifact({
ua || sub.ua,
undefined,
sub.proxy,
undefined,
awaitCustomCache,
);
} catch (err) {
errors[url] = err;
@@ -503,6 +508,7 @@ async function syncArtifacts() {
await produceArtifact({
type: 'subscription',
name: subName,
awaitCustomCache: true,
});
} catch (e) {
// $.error(`${e.message ?? e}`);

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

@@ -0,0 +1,49 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
export async function doh({ url, domain, type = 'A', timeout, edns }) {
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: edns,
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,11 +15,12 @@ import $ from '@/core/app';
const tasks = new Map();
export default async function download(
rawUrl,
rawUrl = '',
ua,
timeout,
proxy,
skipCustomCache,
awaitCustomCache,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
@@ -41,25 +42,66 @@ export default async function download(
}
}
}
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined;
if (customCacheKey && !skipCustomCache) {
const cached = $.read(customCacheKey);
if (cached) {
const customCached = $.read(customCacheKey);
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
$.info(
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
);
return cached;
}
if (customCached) {
if (awaitCustomCache) {
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
try {
await download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
);
} catch (e) {
$.error(
`乐观缓存: URL ${url} 更新缓存发生错误 ${
e.message ?? e
}`,
);
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
resourceCache.set(id, customCached);
}
} else {
$.info(
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
).catch((e) => {
$.error(
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
e.message ?? e
}`,
);
});
}
return customCached;
}
}
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
@@ -79,12 +121,6 @@ export default async function download(
// return item.content;
// }
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
@@ -104,9 +140,13 @@ export default async function download(
// try to find in app cache
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached && !skipCustomCache) {
if (!$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}`);
result = cached;
if (customCacheKey) {
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
$.write(cached, customCacheKey);
}
} else {
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
@@ -177,6 +217,7 @@ export default async function download(
$arguments.flowUserAgent,
undefined,
proxy,
$arguments.flowUrl,
),
),
);

View File

@@ -10,8 +10,8 @@ export function getFlowField(headers) {
)[0];
return headers[subkey];
}
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl;
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
let url = flowUrl || rawUrl || '';
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
@@ -48,60 +48,76 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
try {
if (flowUrl) {
$.info(
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
const { body } = await http.get({
url: flowUrl,
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
flowInfo = getFlowField(headers);
flowInfo = body;
} else {
try {
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
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);

View File

@@ -293,7 +293,7 @@ export function getFlag(name) {
'沪俄',
'Moscow',
],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
'🇸🇪': ['Sweden', '瑞典'],
'🇸🇬': [
'Singapore',
@@ -329,6 +329,7 @@ export function getFlag(name) {
'台',
'臺',
'Taipei',
'Tai Wan',
],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
'🇺🇸': [
@@ -430,6 +431,13 @@ export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}
// remove flag
export function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim();
}
export class MMDB {
constructor({ country, asn } = {}) {
if ($.env.isNode) {
@@ -438,11 +446,21 @@ export class MMDB {
const countryFile =
country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');
// $.info(
// `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(
// countryFile,
// )}`,
// );
if (countryFile) {
this.countryReader = Reader.openBuffer(
fs.readFileSync(countryFile),
);
}
// $.info(
// `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(
// asnFile,
// )}`,
// );
if (asnFile) {
if (!fs.existsSync(asnFile))
throw new Error('GeoLite2 ASN MMDB does not exist');

View File

@@ -1,3 +1,4 @@
import * as ipAddress from 'ip-address';
// source: https://stackoverflow.com/a/36760050
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
@@ -46,54 +47,72 @@ function getPolicyDescriptor(str) {
};
}
const utf8ArrayToStr =
typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v))
: (function () {
var charCache = new Array(128); // Preallocate the cache for the common single byte chars
var charFromCodePt = String.fromCodePoint || String.fromCharCode;
var result = [];
// const utf8ArrayToStr =
// typeof TextDecoder !== 'undefined'
// ? (v) => new TextDecoder().decode(new Uint8Array(v))
// : (function () {
// var charCache = new Array(128); // Preallocate the cache for the common single byte chars
// var charFromCodePt = String.fromCodePoint || String.fromCharCode;
// var result = [];
return function (array) {
var codePt, byte1;
var buffLen = array.length;
// return function (array) {
// var codePt, byte1;
// var buffLen = array.length;
result.length = 0;
// result.length = 0;
for (var i = 0; i < buffLen; ) {
byte1 = array[i++];
// for (var i = 0; i < buffLen; ) {
// byte1 = array[i++];
if (byte1 <= 0x7f) {
codePt = byte1;
} else if (byte1 <= 0xdf) {
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
} else if (byte1 <= 0xef) {
codePt =
((byte1 & 0x0f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else if (String.fromCodePoint) {
codePt =
((byte1 & 0x07) << 18) |
((array[i++] & 0x3f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else {
codePt = 63; // Cannot convert four byte code points, so use "?" instead
i += 3;
}
// if (byte1 <= 0x7f) {
// codePt = byte1;
// } else if (byte1 <= 0xdf) {
// codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
// } else if (byte1 <= 0xef) {
// codePt =
// ((byte1 & 0x0f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else if (String.fromCodePoint) {
// codePt =
// ((byte1 & 0x07) << 18) |
// ((array[i++] & 0x3f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else {
// codePt = 63; // Cannot convert four byte code points, so use "?" instead
// i += 3;
// }
result.push(
charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)),
);
}
// result.push(
// charCache[codePt] ||
// (charCache[codePt] = charFromCodePt(codePt)),
// );
// }
return result.join('');
};
})();
// return result.join('');
// };
// })();
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomPort(portString) {
let portParts = portString.split(/,|\//);
let randomPart = portParts[Math.floor(Math.random() * portParts.length)];
if (randomPart.includes('-')) {
let [min, max] = randomPart.split('-').map(Number);
return getRandomInt(min, max);
} else {
return Number(randomPart);
}
}
export {
ipAddress,
isIPv4,
isIPv6,
isValidPortNumber,
@@ -101,6 +120,7 @@ export {
getIfNotBlank,
isPresent,
getIfPresent,
utf8ArrayToStr,
// utf8ArrayToStr,
getPolicyDescriptor,
getRandomPort,
};

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

@@ -28,7 +28,9 @@ export function getPlatformFromUserAgent({ ua, UA }) {
return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||
ua.indexOf('clash-verge') !== -1 ||
ua.indexOf('flclash') !== -1
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {

View File

@@ -341,7 +341,10 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
const request = isNode
? eval("require('request')")
: $httpClient;
const body = options.body;
const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
if (!isNode && opts.timeout) {
opts.timeout++;
let unit = 'ms';
@@ -382,6 +385,12 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
url: options.url,
headers: options.headers,
body: options.body,
options: {
Proxy: options.proxy,
Timeout: options.timeout
? options.timeout / 1000
: 15,
},
});
resolve({
statusCode: response.status,

View File

@@ -6,13 +6,23 @@ Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](http
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置:
## 服务器/云平台/Docker/Android 版
https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
## App 版
### 1. Loon
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge
#### 关于 Surge 的格外说明
Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)
定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
@@ -39,3 +49,11 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
## 链接参数说明
https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
## 脚本使用说明
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E

View File

@@ -1,8 +1,8 @@
#!name=Sub-Store(β)
#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto
#!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2"
#!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
[MITM]
hostname = %APPEND% sub.store
@@ -12,4 +12,6 @@ Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -1,8 +1,8 @@
#!name=Sub-Store
#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto
#!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2"
#!arguments-desc=\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
[MITM]
hostname = %APPEND% sub.store
@@ -12,4 +12,6 @@ Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -1,16 +1,25 @@
function operator(proxies = [], targetPlatform, context) {
// 支持快捷操作 不一定要写一个 function
// 可参考 https://t.me/zhetengsha/970
// https://t.me/zhetengsha/1009
// https://t.me/zhetengsha/1009
// proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 1. `no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `resolved` 字段
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
// 1. `_no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 5. `_subName` 为单条订阅名
// 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 为传入的脚本参数
@@ -20,7 +29,7 @@ function operator(proxies = [], targetPlatform, context) {
// $substore 为 OpenAPI
// 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
// scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache
@@ -33,17 +42,22 @@ function operator(proxies = [], targetPlatform, context) {
// parse, // 订阅解析
// process, // 节点操作/文件操作
// produce, // 输出订阅
// getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)
// ipAddress, // https://github.com/beaugunderson/ip-address
// isIPv4,
// isIPv6,
// isIP,
// yaml, // yaml 解析和生成
// getFlag, // 获取 emoji 旗帜
// removeFlag, // 移除 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// }
// 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 给节点名添加旗帜
// $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
// 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({
@@ -63,7 +77,7 @@ function operator(proxies = [], targetPlatform, context) {
// }
// })
// $content = proxies
// 2. sing-box
// 但是一般不需要这样用, 可参考
@@ -97,8 +111,8 @@ function operator(proxies = [], targetPlatform, context) {
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
// 见 https://t.me/zhetengsha/1428
//
// const content = ProxyUtils.produce(proxies, platform)
//
// const content = ProxyUtils.produce([...proxies], platform)
// // YAML
// ProxyUtils.yaml.load('YAML String')
@@ -117,12 +131,11 @@ function operator(proxies = [], targetPlatform, context) {
// yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files } will be passed to the next operator
// { $content, $files } will be passed to the next operator
// $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具
// 可参考:
// 可参考:
// 1. https://t.me/zhetengsha/948
// context 为传入的上下文
@@ -211,7 +224,7 @@ function operator(proxies = [], targetPlatform, context) {
// 参数说明
// 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
console.log(JSON.stringify(context, null, 2))
console.log(JSON.stringify(context, null, 2));
return proxies
return proxies;
}