Compare commits

...

27 Commits

Author SHA1 Message Date
xream
b0a544b879 feat: 正则排序支持顺序/倒序/原顺序(前端 > 2.15.10) 2025-03-28 12:24:48 +08:00
xream
80955aa339 doc: 标记 Clash Deprecated 2025-03-27 19:53:46 +08:00
xream
4d27e5bdac feat: 脚本链接叠加参数调整
Some checks are pending
build / build (push) Waiting to run
2025-03-27 12:52:19 +08:00
xream
e2011de69e feat: Loon 解析器支持参数 resourceUrlOnly 仅使用远程资源, 忽略 Loon 自身解析数据
Some checks failed
build / build (push) Has been cancelled
2025-03-26 00:26:31 +08:00
xream
9568f4d6d9 feat: 优化日志, Loon 解析器自动读取 build 2025-03-25 23:58:28 +08:00
xream
543641de9d feat: VLESS 兼容 Shadowrocket 传输层 none 2025-03-25 23:35:23 +08:00
xream
2fbc589a8a feat: Loon 输入输出支持 VLESS REALITY(flow 为 xtls-rprx-vision). 需 includeUnsupportedProxy 或 build >= 838 自动开启) 2025-03-25 22:22:29 +08:00
xream
c854614efc feat: 调整 User-Agent 判断
Some checks are pending
build / build (push) Waiting to run
2025-03-25 17:49:47 +08:00
xream
16a5995d21 fix: 修复 ss shadow-tls
Some checks failed
build / build (push) Has been cancelled
2025-03-23 14:32:24 +08:00
xream
15b55f6d1a feat: 更新文件时, 更新同步配置; 更新单条订阅/组合订阅时, 更新 mihomo 覆写
Some checks failed
build / build (push) Has been cancelled
2025-03-21 00:36:42 +08:00
xream
8e5ce26e7b fix: 修复重置后端数据后无默认字段的问题
Some checks are pending
build / build (push) Waiting to run
2025-03-20 22:03:06 +08:00
Aritro37
c5d8aff73c fix: 修复聚合模式下,名称带有中文或特殊符号的分享token判断异常的问题 2025-03-20 21:57:42 +08:00
xream
5696492dde release: bump version
Some checks are pending
build / build (push) Waiting to run
2025-03-19 16:11:49 +08:00
Aritro37
e6d05fd873 perf: 增加 MERGE 模式下的信息输出 2025-03-19 16:08:48 +08:00
Aritro37
4111b8fabf fix: 修复 SUB_STORE_FRONTEND_PATH 使用绝对目录时前端资源 Content-Type 响应错误的问题 2025-03-19 15:52:37 +08:00
Aritro37
dfc619a181 feat: 引入SUB_STORE_BACKEND_MERGE 变量实现前后端端口合并及安全增强
1. 新增SUB_STORE_BACKEND_MERGE配置变量,支持功能整合模式:
   - 当设置SUB_STORE_BACKEND_MERGE为非空任意值时,后端支持同时处理API和前端资源请求
   - 新增配置示例:
     #合并前后端端口
     SUB_STORE_BACKEND_MERGE=true
     #设置接口安全地址
     SUB_STORE_FRONTEND_BACKEND_PATH=/safe-api
     #设置前端文件的路径
     SUB_STORE_FRONTEND_PATH=./dist
     #后端监听的端口
     SUB_STORE_BACKEND_API_PORT=3000
     #后端监听的HOST
     SUB_STORE_BACKEND_API_HOST="127.0.0.1"

2. 合并后支持前端在子路由界面刷新:
   - 原前端在subs、files、sync等页面刷新时会出现404问题,合并后修复了该问题
2025-03-19 15:26:17 +08:00
Aritro37
ff5283a66f fix: 修复使用 .env 时 /api/utils/env 接口中的 env 字段为空的问题 2025-03-19 15:07:01 +08:00
xream
6c54518e84 chore: 日志
Some checks failed
build / build (push) Has been cancelled
2025-03-18 13:34:50 +08:00
Aritro37
dd92a26e6c Perf: 提前加载 .env;后端复用前端 Path
Some checks are pending
build / build (push) Waiting to run
2025-03-17 22:01:38 +08:00
xream
bb5c9d43d0 Merge pull request #430 from Aritro37/master
feat: 支持通过.env配置环境变量,后端支持设置前置路由
2025-03-17 17:07:37 +08:00
Aritro37
e54ac92357 feat: 支持通过.env配置环境变量,后端支持设置前置路由 2025-03-17 17:04:24 +08:00
xream
507e37021c feat: 增加更多的同步配置日志
Some checks are pending
build / build (push) Waiting to run
2025-03-16 15:48:47 +08:00
xream
a70dc7b913 feat: undici 配置重定向
Some checks are pending
build / build (push) Waiting to run
2025-03-15 22:50:47 +08:00
xream
fc56df7bfd fix: 处理 YAML short-idnull 的情况
Some checks are pending
build / build (push) Waiting to run
2025-03-15 16:29:19 +08:00
xream
1281df59f3 feat: 增强 VMess URI 解析兼容性; 修改导出文件名格式
Some checks failed
build / build (push) Has been cancelled
2025-03-13 20:19:45 +08:00
xream
1faa3fb793 fix: 修复 VMess URI IPv6 格式
Some checks are pending
build / build (push) Waiting to run
2025-03-13 19:26:53 +08:00
xream
47307716b2 feat: url 支持 credentials; 修改导出文件名格式 2025-03-13 17:42:48 +08:00
25 changed files with 350 additions and 96 deletions

View File

@@ -41,6 +41,9 @@ 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] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS) - [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
Deprecated:
- [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
@@ -48,7 +51,6 @@ Core functionalities:
- [x] Plain JSON - [x] Plain JSON
- [x] Stash - [x] Stash
- [x] Clash.Meta(mihomo) - [x] Clash.Meta(mihomo)
- [x] Clash
- [x] Surfboard - [x] Surfboard
- [x] Surge - [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself) - [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
@@ -60,6 +62,10 @@ Core functionalities:
- [x] V2Ray - [x] V2Ray
- [x] V2Ray URI - [x] V2Ray URI
Deprecated:
- [x] Clash
## 2. Subscription Formatting ## 2. Subscription Formatting
### Filtering ### Filtering

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.18.0", "version": "2.19.11",
"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": {
@@ -27,10 +27,12 @@
"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", "buffer": "^6.0.3",
"dotenv": "^16.4.7",
"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", "dns-packet": "^5.6.1",
"express": "^4.17.1", "express": "^4.17.1",
"mime-types": "^2.1.35",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5", "ip-address": "^9.0.5",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",

View File

@@ -114,12 +114,7 @@ async function processFn(
if (item.type.indexOf('Script') !== -1) { if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
let noCache;
let url = content || ''; let url = content || '';
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
}
// extract link arguments // extract link arguments
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
@@ -138,7 +133,14 @@ async function processFn(
} }
} }
} }
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`; url = `${url.split('#')[0]}${
rawArgs[2]
? `#${rawArgs[2]}`
: $arguments?.noCache != null ||
$arguments?.insecure != null
? `#${rawArgs[1]}`
: ''
}`;
const downloadUrlMatch = url.match( const downloadUrlMatch = url.match(
/^\/api\/(file|module)\/(.+)/, /^\/api\/(file|module)\/(.+)/,
); );
@@ -613,7 +615,7 @@ function lastParse(proxy) {
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr); proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
} }
if ( if (
['shadowsocks'].includes(proxy.type) && ['ss'].includes(proxy.type) &&
isPresent(proxy, 'shadow-tls-password') isPresent(proxy, 'shadow-tls-password')
) { ) {
proxy.plugin = 'shadow-tls'; proxy.plugin = 'shadow-tls';

View File

@@ -342,7 +342,7 @@ function URI_VMess() {
}; };
const parse = (line) => { const parse = (line) => {
line = line.split('vmess://')[1]; line = line.split('vmess://')[1];
let content = Base64.decode(line); let content = Base64.decode(line.replace(/\?.*?$/, ''));
if (/=\s*vmess/.test(content)) { if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format // Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim()); const partitions = content.split(',').map((p) => p.trim());
@@ -634,6 +634,9 @@ function URI_VLESS() {
} }
if (!proxy.network && isShadowrocket && params.obfs) { if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs; proxy.network = params.obfs;
if (['none'].includes(proxy.network)) {
proxy.network = 'tcp';
}
} }
if (['websocket'].includes(proxy.network)) { if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws'; proxy.network = 'ws';

View File

@@ -60,7 +60,7 @@ vmess = tag equals "vmess"i address method uuid (transport/transport_host/transp
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
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/ip_mode/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/ip_mode/flow/public_key/short_id/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
@@ -180,6 +180,10 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } 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'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }

View File

@@ -58,7 +58,7 @@ vmess = tag equals "vmess"i address method uuid (transport/transport_host/transp
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
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/ip_mode/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/ip_mode/flow/public_key/short_id/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
@@ -178,6 +178,10 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } 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'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }

View File

@@ -75,6 +75,8 @@ function Clash() {
// 是否被引号包裹 // 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) { if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`; return `short-id: ${afterTrim}`;
} else if (['null'].includes(afterTrim)) {
return `short-id: ${afterTrim}`;
} else { } else {
return `short-id: "${afterTrim}"`; return `short-id: "${afterTrim}"`;
} }

View File

@@ -284,7 +284,7 @@ function SortOperator(order = 'asc') {
} }
// sort by regex // sort by regex
function RegexSortOperator(expressions) { function RegexSortOperator(expressions, order = 'asc') {
return { return {
name: 'Regex Sort Operator', name: 'Regex Sort Operator',
func: (proxies) => { func: (proxies) => {
@@ -295,8 +295,13 @@ function RegexSortOperator(expressions) {
if (oA && !oB) return -1; if (oA && !oB) return -1;
if (oB && !oA) return 1; if (oB && !oA) return 1;
if (oA && oB) return oA < oB ? -1 : 1; if (oA && oB) return oA < oB ? -1 : 1;
if ((!oA && !oB) || (oA && oB && oA === oB)) if (order === 'original') {
return a.name < b.name ? -1 : 1; // fallback to normal sort return 0;
} else if (order === 'desc') {
return a.name < b.name ? 1 : -1;
} else {
return a.name < b.name ? -1 : 1;
}
}); });
}, },
}; };

View File

@@ -23,7 +23,7 @@ export default function Loon_Producer() {
case 'vmess': case 'vmess':
return vmess(proxy); return vmess(proxy);
case 'vless': case 'vless':
return vless(proxy); return vless(proxy, opts['include-unsupported-proxy']);
case 'http': case 'http':
return http(proxy); return http(proxy);
case 'socks5': case 'socks5':
@@ -347,10 +347,28 @@ function vmess(proxy) {
return result.toString(); return result.toString();
} }
function vless(proxy) { function vless(proxy, includeUnsupportedProxy) {
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) { if (
!includeUnsupportedProxy &&
(typeof proxy.flow !== 'undefined' || proxy['reality-opts'])
) {
throw new Error(`VLESS XTLS/REALITY is not supported`); throw new Error(`VLESS XTLS/REALITY is not supported`);
} }
let isReality = false;
if (includeUnsupportedProxy) {
if (
proxy['reality-opts'] &&
['xtls-rprx-vision'].includes(proxy.flow)
) {
isReality = true;
} else if (proxy['reality-opts']) {
throw new Error(
`VLESS REALITY with flow(${proxy.flow}) is not supported`,
);
} else if (proxy.flow) {
throw new Error(`VLESS XTLS is not supported`);
}
}
const result = new Result(proxy); const result = new Result(proxy);
result.append( result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`, `${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -399,7 +417,21 @@ function vless(proxy) {
); );
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); if (isReality) {
result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,public-key="${proxy['reality-opts']['public-key']}"`,
'reality-opts.public-key',
);
result.appendIfPresent(
`,short-id=${proxy['reality-opts']['short-id']}`,
'reality-opts.short-id',
);
} else {
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
}
result.appendIfPresent( result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint', 'tls-fingerprint',

View File

@@ -23,7 +23,11 @@ export default function URI_Producer() {
) { ) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy.server && isIPv6(proxy.server)) { if (
!['vmess'].includes(proxy.type) &&
proxy.server &&
isIPv6(proxy.server)
) {
proxy.server = `[${proxy.server}]`; proxy.server = `[${proxy.server}]`;
} }
switch (proxy.type) { switch (proxy.type) {

View File

@@ -11,6 +11,7 @@
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */
import { version } from '../package.json'; import { version } from '../package.json';
import $ from '@/core/app';
console.log( console.log(
` `
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
@@ -18,7 +19,6 @@ console.log(
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`, `,
); );
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import serve from '@/restful'; import serve from '@/restful';

View File

@@ -89,8 +89,10 @@ async function doSync() {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const subNames = []; const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => { allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) { if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') { if (artifact.type === 'subscription') {
const subName = artifact.source; const subName = artifact.source;
const sub = findByName(allSubs, subName); const sub = findByName(allSubs, subName);
@@ -111,6 +113,13 @@ async function doSync() {
} }
}); });
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) { if (subNames.length > 0) {
await Promise.all( await Promise.all(
subNames.map(async (subName) => { subNames.map(async (subName) => {

View File

@@ -14,10 +14,12 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
` `
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version} Sub-Store -- v${version}
Loon -- ${$loon}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`, `,
); );
const build = $loon.match(/\((\d+)\)$/)?.[1];
let arg; let arg;
if (typeof $argument != 'undefined') { if (typeof $argument != 'undefined') {
arg = Object.fromEntries( arg = Object.fromEntries(
@@ -26,23 +28,28 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
} else { } else {
arg = {}; arg = {};
} }
console.log(`arg: ${JSON.stringify(arg)}`);
const RESOURCE_TYPE = { const RESOURCE_TYPE = {
PROXY: 1, PROXY: 1,
RULE: 2, RULE: 2,
}; };
if (!arg.resourceUrlOnly) {
result = resource; result = resource;
}
if (resourceType === RESOURCE_TYPE.PROXY) { if (resourceType === RESOURCE_TYPE.PROXY) {
try { if (!arg.resourceUrlOnly) {
let proxies = ProxyUtils.parse(resource); try {
result = ProxyUtils.produce(proxies, 'Loon', undefined, { let proxies = ProxyUtils.parse(resource);
'include-unsupported-proxy': arg?.includeUnsupportedProxy, result = ProxyUtils.produce(proxies, 'Loon', undefined, {
}); 'include-unsupported-proxy':
} catch (e) { arg?.includeUnsupportedProxy || build >= 838,
console.log('解析器: 使用 resource 出现错误'); });
console.log(e.message ?? e); } catch (e) {
console.log('解析器: 使用 resource 出现错误');
console.log(e.message ?? e);
}
} }
if ((!result || /^\s*$/.test(result)) && resourceUrl) { if ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`); console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
@@ -59,18 +66,21 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
); );
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
result = ProxyUtils.produce(proxies, 'Loon', undefined, { result = ProxyUtils.produce(proxies, 'Loon', undefined, {
'include-unsupported-proxy': arg?.includeUnsupportedProxy, 'include-unsupported-proxy':
arg?.includeUnsupportedProxy || build >= 838,
}); });
} catch (e) { } catch (e) {
console.log(e.message ?? e); console.log(e.message ?? e);
} }
} }
} else if (resourceType === RESOURCE_TYPE.RULE) { } else if (resourceType === RESOURCE_TYPE.RULE) {
try { if (!arg.resourceUrlOnly) {
const rules = RuleUtils.parse(resource); try {
result = RuleUtils.produce(rules, 'Loon'); const rules = RuleUtils.parse(resource);
} catch (e) { result = RuleUtils.produce(rules, 'Loon');
console.log(e.message ?? e); } catch (e) {
console.log(e.message ?? e);
}
} }
if ((!result || /^\s*$/.test(result)) && resourceUrl) { if ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`); console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);

View File

@@ -1,8 +1,9 @@
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors'; import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY); if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
@@ -60,16 +61,9 @@ function getCollection(req, res) {
.set( .set(
'content-disposition', 'content-disposition',
`attachment; filename="${encodeURIComponent( `attachment; filename="${encodeURIComponent(
`sub-store_collection_${name}_${new Date() `sub-store_collection_${name}_${formatDateTime(
.toLocaleString('zh-CN', { new Date(),
year: 'numeric', )}.json`,
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`, )}"`,
) )
.send(JSON.stringify(collection)); .send(JSON.stringify(collection));
@@ -112,7 +106,18 @@ function updateCollection(req, res) {
artifact.source = newCol.name; artifact.source = newCol.name;
} }
} }
// update all files referring this collection
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'collection' &&
file.sourceName === oldCol.name
) {
file.sourceName = newCol.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
} }
updateByName(allCols, name, newCol); updateByName(allCols, name, newCol);

View File

@@ -1,6 +1,6 @@
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow'; import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { FILES_KEY } from '@/constants'; import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
import { import {
@@ -9,6 +9,7 @@ import {
InternalServerError, InternalServerError,
} from '@/restful/errors'; } from '@/restful/errors';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY); if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
@@ -210,16 +211,9 @@ function getWholeFile(req, res) {
.set( .set(
'content-disposition', 'content-disposition',
`attachment; filename="${encodeURIComponent( `attachment; filename="${encodeURIComponent(
`sub-store_file_${name}_${new Date() `sub-store_file_${name}_${formatDateTime(
.toLocaleString('zh-CN', { new Date(),
year: 'numeric', )}.json`,
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`, )}"`,
) )
.send(JSON.stringify(file)); .send(JSON.stringify(file));
@@ -251,6 +245,20 @@ function updateFile(req, res) {
}; };
$.info(`正在更新文件:${name}...`); $.info(`正在更新文件:${name}...`);
if (name !== newFile.name) {
// update all artifacts referring this collection
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'file' &&
artifact.source === oldFile.name
) {
artifact.source = newFile.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allFiles, name, newFile); updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY); $.write(allFiles, FILES_KEY);
success(res, newFile); success(res, newFile);

View File

@@ -29,6 +29,72 @@ export default function serve() {
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::'; host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
} }
const $app = express({ substore: $, port, host }); const $app = express({ substore: $, port, host });
if ($.env.isNode) {
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
if (be_prefix || be_merge) {
if(!fe_be_path.startsWith('/')){
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
if (be_merge) {
$.info(`[BACKEND] MERGE mode is [ON].`);
$.info(`[BACKEND && FRONTEND] ${host}:${port}`);
}
$.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
$app.use((req, res, next) => {
if (req.path.startsWith(fe_be_path)) {
req.url = req.url.replace(fe_be_path, '') || '/';
if(be_merge && req.url.startsWith('/api/')){
req.query['share'] = 'true';
}
next();
return;
}
const pathname = decodeURIComponent(req._parsedUrl.pathname) || '/';
if(be_merge && req.path.startsWith('/share/') && req.query.token){
if (req.method.toLowerCase() !== 'get'){
res.status(405).send('Method not allowed');
return;
}
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
`/share/${t.type}/${t.name}` === pathname &&
(t.exp == null || t.exp > Date.now()),
);
if (token){
next();
return;
}
}
if (be_merge && fe_path && req.path.indexOf('/',1) == -1) {
if (req.path.indexOf('.') == -1){
req.url = "/index.html"
}
const express_ = eval(`require("express")`);
const mime_ = eval(`require("mime-types")`);
const path_ = eval(`require("path")`);
const staticFileMiddleware = express_.static(fe_path, {
setHeaders: (res, path) => {
const type = mime_.contentType(path_.extname(path));
if (type) {
res.set('Content-Type', type);
}
}
});
staticFileMiddleware(req, res, next);
return;
}
res.status(403).end('Forbbiden');
return;
});
}
}
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
@@ -175,7 +241,8 @@ export default function serve() {
const fe_abs_path = path.resolve( const fe_abs_path = path.resolve(
fe_path || path.join(__dirname, 'frontend'), fe_path || path.join(__dirname, 'frontend'),
); );
if (fe_path) { const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
if (fe_path && !be_merge) {
try { try {
fs.accessSync(path.join(fe_abs_path, 'index.html')); fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) { } catch (e) {
@@ -200,6 +267,9 @@ export default function serve() {
let be_download_rewrite = ''; let be_download_rewrite = '';
let be_api_rewrite = ''; let be_api_rewrite = '';
let be_share_rewrite = `${be_share}:type/:name`; let be_share_rewrite = `${be_share}:type/:name`;
let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
? fe_be_path
: '';
if (fe_be_path) { if (fe_be_path) {
if (!fe_be_path.startsWith('/')) { if (!fe_be_path.startsWith('/')) {
throw new Error( throw new Error(
@@ -216,7 +286,7 @@ export default function serve() {
app.use( app.use(
be_share_rewrite, be_share_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}`, target: `http://127.0.0.1:${port}${prefix}`,
changeOrigin: true, changeOrigin: true,
pathRewrite: async (path, req) => { pathRewrite: async (path, req) => {
if (req.method.toLowerCase() !== 'get') if (req.method.toLowerCase() !== 'get')
@@ -237,7 +307,7 @@ export default function serve() {
app.use( app.use(
be_api_rewrite, be_api_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}${be_api}`, target: `http://127.0.0.1:${port}${prefix}${be_api}`,
pathRewrite: async (path) => { pathRewrite: async (path) => {
return path.includes('?') return path.includes('?')
? `${path}&share=true` ? `${path}&share=true`
@@ -248,7 +318,7 @@ export default function serve() {
app.use( app.use(
be_download_rewrite, be_download_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}${be_download}`, target: `http://127.0.0.1:${port}${prefix}${be_download}`,
changeOrigin: true, changeOrigin: true,
}), }),
); );
@@ -269,10 +339,10 @@ export default function serve() {
$.info(`[FRONTEND] ${fe_address}:${fe_port}`); $.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) { if (fe_be_path) {
$.info( $.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`, `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
); );
$.info( $.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`, `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`,
); );
$.info( $.info(
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`, `[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,

View File

@@ -14,6 +14,7 @@ import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import env from '@/utils/env'; import env from '@/utils/env';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
// utils // utils
@@ -28,16 +29,7 @@ export default function register($app) {
.set( .set(
'content-disposition', 'content-disposition',
`attachment; filename="${encodeURIComponent( `attachment; filename="${encodeURIComponent(
`sub-store_data_${new Date() `sub-store_data_${formatDateTime(new Date())}.json`,
.toLocaleString('zh-CN', {
year: 'numeric',
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`, )}"`,
) )
.send( .send(

View File

@@ -134,11 +134,15 @@ export async function updateArtifactStore() {
settings.artifactStore = url; settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID'; settings.artifactStoreStatus = 'VALID';
} else { } else {
$.error(`找不到 Sub-Store Gist`); $.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
settings.artifactStoreStatus = 'NOT FOUND'; settings.artifactStoreStatus = 'NOT FOUND';
} }
} catch (err) { } catch (err) {
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`); $.error(
`查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
err.message ?? err
}`,
);
settings.artifactStoreStatus = 'ERROR'; settings.artifactStoreStatus = 'ERROR';
} }
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);

View File

@@ -5,7 +5,12 @@ import {
RequestInvalidError, RequestInvalidError,
} from './errors'; } from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import {
SUBS_KEY,
COLLECTIONS_KEY,
ARTIFACTS_KEY,
FILES_KEY,
} from '@/constants';
import { import {
getFlowHeaders, getFlowHeaders,
parseFlowHeaders, parseFlowHeaders,
@@ -13,6 +18,7 @@ import {
} from '@/utils/flow'; } from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
import { formatDateTime } from '@/utils';
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY); if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
@@ -265,16 +271,9 @@ function getSubscription(req, res) {
.set( .set(
'content-disposition', 'content-disposition',
`attachment; filename="${encodeURIComponent( `attachment; filename="${encodeURIComponent(
`sub-store_subscription_${name}_${new Date() `sub-store_subscription_${name}_${formatDateTime(
.toLocaleString('zh-CN', { new Date(),
year: 'numeric', )}.json`,
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`, )}"`,
) )
.send(JSON.stringify(sub)); .send(JSON.stringify(sub));
@@ -326,9 +325,20 @@ function updateSubscription(req, res) {
artifact.source = sub.name; artifact.source = sub.name;
} }
} }
// update all files referring this subscription
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'subscription' &&
file.sourceName == name
) {
file.sourceName = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
} }
updateByName(allSubs, name, newSub); updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);

View File

@@ -556,8 +556,10 @@ async function syncArtifacts() {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const subNames = []; const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => { allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) { if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') { if (artifact.type === 'subscription') {
const subName = artifact.source; const subName = artifact.source;
const sub = findByName(allSubs, subName); const sub = findByName(allSubs, subName);
@@ -578,6 +580,13 @@ async function syncArtifacts() {
} }
}); });
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) { if (subNames.length > 0) {
await Promise.all( await Promise.all(
subNames.map(async (subName) => { subNames.map(async (subName) => {

View File

@@ -280,7 +280,7 @@ export default class Gist {
return Promise.reject(err); return Promise.reject(err);
} }
} else { } else {
return Promise.reject('找不到 Sub-Store Gist'); return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
} }
} }
} }

View File

@@ -126,7 +126,32 @@ function isValidUUID(uuid) {
); );
} }
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
const pad = (num) => String(num).padStart(2, '0');
const replacements = {
YYYY: d.getFullYear(),
MM: pad(d.getMonth() + 1),
DD: pad(d.getDate()),
HH: pad(d.getHours()),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds()),
};
return format.replace(
/YYYY|MM|DD|HH|mm|ss/g,
(match) => replacements[match],
);
}
export { export {
formatDateTime,
isValidUUID, isValidUUID,
ipAddress, ipAddress,
isIPv4, isIPv4,

View File

@@ -4,6 +4,8 @@ import {
SCHEMA_VERSION_KEY, SCHEMA_VERSION_KEY,
ARTIFACTS_KEY, ARTIFACTS_KEY,
RULES_KEY, RULES_KEY,
FILES_KEY,
TOKENS_KEY,
} from '@/constants'; } from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -55,7 +57,17 @@ function doMigrationV2() {
const newRules = Object.values(rules); const newRules = Object.values(rules);
$.write(newRules, RULES_KEY); $.write(newRules, RULES_KEY);
// 5. delete builtin rules // 5. migrate files
const files = $.read(FILES_KEY) || {};
const newFiles = Object.values(files);
$.write(newFiles, FILES_KEY);
// 6. migrate tokens
const tokens = $.read(TOKENS_KEY) || {};
const newTokens = Object.values(tokens);
$.write(newTokens, TOKENS_KEY);
// 7. delete builtin rules
delete $.cache.builtin; delete $.cache.builtin;
$.info('Migration complete!'); $.info('Migration complete!');

View File

@@ -47,7 +47,7 @@ export function getPlatformFromUserAgent({ ua, UA, accept }) {
return 'Clash'; return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) { } else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray'; return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) { } else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) {
return 'sing-box'; return 'sing-box';
} else if (accept.indexOf('application/json') === 0) { } else if (accept.indexOf('application/json') === 0) {
return 'JSON'; return 'JSON';
@@ -66,10 +66,12 @@ export function shouldIncludeUnsupportedProxy(platform, ua) {
UA: ua, UA: ua,
ua: ua.toLowerCase(), ua: ua.toLowerCase(),
}); });
if (!['Stash', 'Egern'].includes(target)) { if (!['Stash', 'Egern', 'Loon'].includes(target)) {
return false; return false;
} }
const version = coerce(ua).version; const coerceVersion = coerce(ua);
$.log(JSON.stringify(coerceVersion, null, 2));
const { version } = coerceVersion;
if ( if (
platform === 'Stash' && platform === 'Stash' &&
target === 'Stash' && target === 'Stash' &&
@@ -84,6 +86,14 @@ export function shouldIncludeUnsupportedProxy(platform, ua) {
) { ) {
return true; return true;
} }
// Loon 的 UA 不规范, version 取出来是 build
if (
platform === 'Loon' &&
target === 'Loon' &&
gte(version, '838.0.0')
) {
return true;
}
} catch (e) { } catch (e) {
$.error(`获取版本号失败: ${e}`); $.error(`获取版本号失败: ${e}`);
} }

View File

@@ -18,6 +18,10 @@ export class OpenAPI {
this.http = HTTP(); this.http = HTTP();
this.env = ENV(); this.env = ENV();
if (isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
this.node = (() => { this.node = (() => {
if (isNode) { if (isNode) {
const fs = eval("require('fs')"); const fs = eval("require('fs')");
@@ -360,7 +364,12 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
} }
if (isNode) { if (isNode) {
const undici = eval("require('undici')"); const undici = eval("require('undici')");
const { ProxyAgent, EnvHttpProxyAgent, request } = undici; const {
ProxyAgent,
EnvHttpProxyAgent,
request,
interceptors,
} = undici;
const agentOpts = { const agentOpts = {
connect: { connect: {
rejectUnauthorized: rejectUnauthorized:
@@ -373,15 +382,32 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
headersTimeout: opts.timeout, headersTimeout: opts.timeout,
}; };
try { try {
const url = new URL(opts.url);
if (url.username || url.password) {
opts.headers = {
...(opts.headers || {}),
Authorization: `Basic ${Buffer.from(
`${url.username || ''}:${
url.password || ''
}`,
).toString('base64')}`,
};
}
const response = await request(opts.url, { const response = await request(opts.url, {
...opts, ...opts,
method: method.toUpperCase(), method: method.toUpperCase(),
dispatcher: opts.proxy dispatcher: (opts.proxy
? new ProxyAgent({ ? new ProxyAgent({
...agentOpts, ...agentOpts,
uri: opts.proxy, uri: opts.proxy,
}) })
: new EnvHttpProxyAgent(agentOpts), : new EnvHttpProxyAgent(agentOpts)
).compose(
interceptors.redirect({
maxRedirections: 3,
throwOnMaxRedirects: true,
}),
),
}); });
resolve({ resolve({
statusCode: response.statusCode, statusCode: response.statusCode,