Compare commits

...

22 Commits

Author SHA1 Message Date
xream
7213cea16c feat: Stash 正式版支持 SS2022, 测试版(>=3.1.0) 支持 VLESS REALITY(xtls-rprx-vision)
Some checks are pending
build / build (push) Waiting to run
2025-04-03 15:47:35 +08:00
xream
260b1e5332 docs(README): 增加赞助商信息 2025-04-03 15:23:15 +08:00
xream
73e5d53f48 feat: Loon 输入输出支持 VLESS XTLS/REALITY, VMess REALITY. 需 includeUnsupportedProxy 或 build >= 842 自动开启)
Some checks failed
build / build (push) Has been cancelled
2025-04-01 18:22:28 +08:00
xream
39829fa97a feat: QX 输入值支持 =
Some checks failed
build / build (push) Has been cancelled
2025-03-29 19:52:40 +08:00
xream
93d524331a feat: QX 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2025-03-29 14:17:59 +08:00
xream
e0c6cc4453 feat: 正则排序支持顺序/倒序/原顺序(前端 > 2.15.10)
Some checks are pending
build / build (push) Waiting to run
2025-03-28 12:46:51 +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
23 changed files with 306 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] 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)
Deprecated:
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms
@@ -48,7 +51,6 @@ Core functionalities:
- [x] Plain JSON
- [x] Stash
- [x] Clash.Meta(mihomo)
- [x] Clash
- [x] Surfboard
- [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
@@ -60,6 +62,10 @@ Core functionalities:
- [x] V2Ray
- [x] V2Ray URI
Deprecated:
- [x] Clash
## 2. Subscription Formatting
### Filtering
@@ -122,3 +128,9 @@ This project is under the GPL V3 LICENSE.
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
- Special thanks to @Orz-3 and @58xinian for their awesome icons.
## Sponsors
[![image](./support.nodeseek.com_page_promotion_id=8.png)](https://yxvm.com)
[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project.

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.18.7",
"version": "2.19.16",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {
@@ -32,6 +32,7 @@
"cron": "^3.1.6",
"dns-packet": "^5.6.1",
"express": "^4.17.1",
"mime-types": "^2.1.35",
"http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",

View File

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

View File

@@ -634,6 +634,9 @@ function URI_VLESS() {
}
if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs;
if (['none'].includes(proxy.network)) {
proxy.network = 'tcp';
}
}
if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws';

View File

@@ -54,13 +54,13 @@ 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/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/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/ip_mode/public_key/short_id/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/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";
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_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; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }

View File

@@ -52,13 +52,13 @@ 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/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/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/ip_mode/public_key/short_id/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/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";
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_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; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }

View File

@@ -145,14 +145,14 @@ port = digits:[0-9]+ {
}
}
username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }

View File

@@ -143,14 +143,14 @@ port = digits:[0-9]+ {
}
}
username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }

View File

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

View File

@@ -21,9 +21,9 @@ export default function Loon_Producer() {
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
return vmess(proxy, opts['include-unsupported-proxy']);
case 'vless':
return vless(proxy);
return vless(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'socks5':
@@ -269,7 +269,17 @@ function trojan(proxy) {
return result.toString();
}
function vmess(proxy) {
function vmess(proxy, includeUnsupportedProxy) {
if (!includeUnsupportedProxy && proxy['reality-opts']) {
throw new Error(`VMess REALITY is not supported`);
}
let isReality = false;
if (includeUnsupportedProxy) {
if (proxy['reality-opts']) {
isReality = true;
}
}
const result = new Result(proxy);
result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
@@ -317,16 +327,28 @@ function vmess(proxy) {
'skip-cert-verify',
);
// 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',
);
if (isReality) {
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 {
// 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')) {
@@ -347,10 +369,28 @@ function vmess(proxy) {
return result.toString();
}
function vless(proxy) {
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
function vless(proxy, includeUnsupportedProxy) {
if (
!includeUnsupportedProxy &&
(typeof proxy.flow !== 'undefined' || proxy['reality-opts'])
) {
throw new Error(`VLESS XTLS/REALITY is not supported`);
}
let isXtls = false;
let isReality = false;
if (includeUnsupportedProxy) {
if (proxy['reality-opts']) {
isReality = true;
}
if (typeof proxy.flow !== 'undefined') {
if (['xtls-rprx-vision'].includes(proxy.flow)) {
isXtls = true;
} else {
throw new Error(`VLESS flow(${proxy.flow}) is not supported`);
}
}
}
const result = new Result(proxy);
result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -398,16 +438,31 @@ function vless(proxy) {
'skip-cert-verify',
);
// 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',
);
if (isXtls) {
result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
}
if (isReality) {
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 {
// 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');

View File

@@ -7,7 +7,7 @@ export default function QX_Producer() {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
return shadowsocks(proxy, opts['include-unsupported-proxy']);
case 'ssr':
return shadowsocksr(proxy);
case 'trojan':
@@ -28,7 +28,7 @@ export default function QX_Producer() {
return { produce };
}
function shadowsocks(proxy) {
function shadowsocks(proxy, includeUnsupportedProxy) {
const result = new Result(proxy);
const append = result.append.bind(result);
const appendIfPresent = result.appendIfPresent.bind(result);
@@ -58,6 +58,9 @@ function shadowsocks(proxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
...(includeUnsupportedProxy
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm']
: []),
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);

View File

@@ -575,7 +575,7 @@ const hysteriaParser = (proxy = {}) => {
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
const hysteria2Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',

View File

@@ -39,12 +39,8 @@ export default function Stash_Producer() {
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
...(opts['include-unsupported-proxy']
? [
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
]
: []),
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(opts['include-unsupported-proxy']

View File

@@ -19,10 +19,6 @@ console.log(
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
if ($.env.isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
import migrate from '@/utils/migration';
import serve from '@/restful';

View File

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

View File

@@ -1,5 +1,5 @@
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 $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
@@ -106,7 +106,18 @@ function updateCollection(req, res) {
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(allFiles, FILES_KEY);
}
updateByName(allCols, name, newCol);

View File

@@ -1,6 +1,6 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
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 $ from '@/core/app';
import {
@@ -245,6 +245,20 @@ function updateFile(req, res) {
};
$.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);
$.write(allFiles, FILES_KEY);
success(res, newFile);

View File

@@ -30,25 +30,68 @@ export default function serve() {
}
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');
if (be_prefix) {
if (!fe_be_path.startsWith('/')) {
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 /',
);
}
$.info(
`[BACKEND PREFIX] ${host}:${port}${fe_be_path} -> ${host}:${port}`,
);
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)) {
const newPath = req.url.replace(fe_be_path, '') || '/';
req.url = newPath;
req.url = req.url.replace(fe_be_path, '') || '/';
if(be_merge && req.url.startsWith('/api/')){
req.query['share'] = 'true';
}
next();
} else {
res.status(403).send();
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;
});
}
}
@@ -198,7 +241,8 @@ export default function serve() {
const fe_abs_path = path.resolve(
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 {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {

View File

@@ -5,7 +5,12 @@ import {
RequestInvalidError,
} from './errors';
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 {
getFlowHeaders,
parseFlowHeaders,
@@ -320,9 +325,20 @@ function updateSubscription(req, res) {
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(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
}
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY);

View File

@@ -4,6 +4,8 @@ import {
SCHEMA_VERSION_KEY,
ARTIFACTS_KEY,
RULES_KEY,
FILES_KEY,
TOKENS_KEY,
} from '@/constants';
import $ from '@/core/app';
@@ -55,7 +57,17 @@ function doMigrationV2() {
const newRules = Object.values(rules);
$.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;
$.info('Migration complete!');

View File

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

View File

@@ -18,6 +18,10 @@ export class OpenAPI {
this.http = HTTP();
this.env = ENV();
if (isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
this.node = (() => {
if (isNode) {
const fs = eval("require('fs')");

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB