Compare commits

..

5 Commits

9 changed files with 224 additions and 49 deletions

View File

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

View File

@@ -231,6 +231,10 @@ const sshParser = (proxy = {}) => {
// https://wiki.metacubex.one/config/proxies/ssh
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
if (proxy['private-key'])
parsedProxy.private_key_path = proxy['private-key'];
if (proxy['private-key-passphrase'])
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
if (proxy['server-fingerprint']) {
parsedProxy.host_key = [proxy['server-fingerprint']];
// https://manual.nssurge.com/policy/ssh.html
@@ -240,6 +244,9 @@ const sshParser = (proxy = {}) => {
proxy['server-fingerprint'].split(' ')[0],
];
}
if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
if (proxy['host-key-algorithms'])
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
return parsedProxy;

View File

@@ -77,12 +77,49 @@ async function downloadSubscription(req, res) {
},
});
if (sub.source !== 'local' || url) {
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ||
url
) {
try {
// forward flow headers
const flowInfo = await getFlowHeaders(url || sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
url = `${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
undefined,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
@@ -92,6 +129,9 @@ async function downloadSubscription(req, res) {
);
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
@@ -176,11 +216,47 @@ async function downloadCollection(req, res) {
const subnames = collection.subscriptions;
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') {
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
undefined,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
@@ -190,6 +266,9 @@ async function downloadCollection(req, res) {
);
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
}
if (platform === 'JSON') {

View File

@@ -109,7 +109,12 @@ async function compareSub(req, res) {
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -195,7 +200,12 @@ async function compareCollection(req, res) {
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(

View File

@@ -51,14 +51,20 @@ async function getFlowInfo(req, res) {
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
if (sub.subUserinfo) {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
});
} else {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
}
return;
}
try {
@@ -97,26 +103,42 @@ async function getFlowInfo(req, res) {
);
return;
}
const flowHeaders = await getFlowHeaders(url);
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
if (sub.subUserinfo) {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} else {
const flowHeaders = await getFlowHeaders(
url,
undefined,
undefined,
sub.proxy,
);
return;
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} catch (err) {
failed(
res,

View File

@@ -62,7 +62,12 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -102,7 +107,12 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -198,7 +208,12 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(

View File

@@ -1,6 +1,7 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import {
@@ -13,7 +14,7 @@ import $ from '@/core/app';
const tasks = new Map();
export default async function download(rawUrl, ua, timeout) {
export default async function download(rawUrl, ua, timeout, proxy) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
const rawArgs = url.split('#');
@@ -52,7 +53,7 @@ export default async function download(rawUrl, ua, timeout) {
// return item.content;
// }
const { isNode } = ENV();
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
@@ -65,6 +66,10 @@ export default async function download(rawUrl, ua, timeout) {
const http = HTTP({
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
: {}),
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
},
timeout: requestTimeout,
});
@@ -78,10 +83,16 @@ export default async function download(rawUrl, ua, timeout) {
result = cached;
} else {
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get(url);
const { body, headers } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
if (headers) {
const flowInfo = getFlowField(headers);
@@ -116,7 +127,11 @@ export default async function download(rawUrl, ua, timeout) {
// 检查订阅有效性
if ($arguments?.validCheck) {
await validCheck(parseFlowHeaders(await getFlowHeaders(url)));
await validCheck(
parseFlowHeaders(
await getFlowHeaders(url, undefined, undefined, proxy),
),
);
}
if (!isNode) {

View File

@@ -1,5 +1,6 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api';
import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
@@ -9,7 +10,7 @@ export function getFlowField(headers) {
)[0];
return headers[subkey];
}
export async function getFlowHeaders(rawUrl, ua, timeout) {
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl;
let $arguments = {};
const rawArgs = url.split('#');
@@ -33,6 +34,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
if ($arguments?.noFlow) {
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url);
let flowInfo;
if (!$arguments?.noCache && cached) {
@@ -55,8 +57,21 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
.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) {
@@ -178,7 +193,7 @@ export function getRmainingDays(opt = {}) {
return daysDiff;
} else {
if (!resetDay) throw new Error('未提供月重置日 resetDay');
if (!resetDay) return;
resetDay = parseInt(resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
throw new Error('月重置日应为 1-31 之间的整数');

View File

@@ -35,6 +35,17 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue;
}
function getPolicyDescriptor(str) {
if (!str) return {};
return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
? {
'policy-descriptor': str,
}
: {
policy: str,
};
}
const utf8ArrayToStr =
typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v))
@@ -91,4 +102,5 @@ export {
isPresent,
getIfPresent,
utf8ArrayToStr,
getPolicyDescriptor,
};