Compare commits

...

9 Commits

Author SHA1 Message Date
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
13 changed files with 148 additions and 32 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.19.0",
"version": "2.19.9",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {

View File

@@ -613,7 +613,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

@@ -60,7 +60,7 @@ vmess = tag equals "vmess"i address method uuid (transport/transport_host/transp
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

@@ -58,7 +58,7 @@ vmess = tag equals "vmess"i address method uuid (transport/transport_host/transp
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

@@ -23,7 +23,7 @@ export default function Loon_Producer() {
case 'vmess':
return vmess(proxy);
case 'vless':
return vless(proxy);
return vless(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'socks5':
@@ -347,10 +347,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 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);
result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -399,7 +417,21 @@ function vless(proxy) {
);
// 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(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',

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 >= 838,
});
} 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 >= 838,
});
} 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

@@ -54,7 +54,7 @@ export default function serve() {
next();
return;
}
const pathname = req._parsedUrl.pathname || '/';
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');

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,10 +66,12 @@ 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' &&
@@ -84,6 +86,14 @@ export function shouldIncludeUnsupportedProxy(platform, ua) {
) {
return true;
}
// Loon 的 UA 不规范, version 取出来是 build
if (
platform === 'Loon' &&
target === 'Loon' &&
gte(version, '838.0.0')
) {
return true;
}
} catch (e) {
$.error(`获取版本号失败: ${e}`);
}