mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d8132d7cd | ||
|
|
bc1247efaf | ||
|
|
dea937df66 | ||
|
|
cfb5a8e082 | ||
|
|
4790bf47d1 | ||
|
|
56fd495fb6 | ||
|
|
f4639d9a34 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.415",
|
||||
"version": "2.14.423",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -193,7 +193,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
@@ -193,7 +193,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
|
||||
function trojan(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
@@ -366,7 +366,7 @@ function ssh(proxy) {
|
||||
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
// 所有的类似的字段都有双引号的问题 暂不处理
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||
|
||||
// https://manual.nssurge.com/policy/ssh.html
|
||||
// 需配合 Keystore
|
||||
@@ -431,7 +431,7 @@ function http(proxy) {
|
||||
const type = proxy.tls ? 'https' : 'http';
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
@@ -509,7 +509,7 @@ function socks5(proxy) {
|
||||
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
@@ -675,7 +675,7 @@ function tuic(proxy) {
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||
result.appendIfPresent(`,token=${proxy.token}`, 'token');
|
||||
|
||||
result.appendIfPresent(
|
||||
@@ -950,7 +950,7 @@ function hysteria2(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||
|
||||
if (isPresent(proxy, 'ports')) {
|
||||
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
|
||||
|
||||
@@ -158,7 +158,7 @@ async function downloadSubscription(req, res) {
|
||||
proxy,
|
||||
noCache,
|
||||
});
|
||||
|
||||
let flowInfo;
|
||||
if (
|
||||
sub.source !== 'local' ||
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
@@ -193,7 +193,7 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
if (!$arguments.noFlow) {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(
|
||||
flowInfo = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
@@ -213,7 +213,10 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
res.set('subscription-userinfo', sub.subUserinfo);
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
@@ -358,6 +361,7 @@ async function downloadCollection(req, res) {
|
||||
const subnames = collection.subscriptions;
|
||||
if (subnames.length > 0) {
|
||||
const sub = findByName(allSubs, subnames[0]);
|
||||
let flowInfo;
|
||||
if (
|
||||
sub.source !== 'local' ||
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
@@ -391,7 +395,7 @@ async function downloadCollection(req, res) {
|
||||
}
|
||||
}
|
||||
if (!$arguments.noFlow) {
|
||||
const flowInfo = await getFlowHeaders(
|
||||
flowInfo = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
@@ -411,7 +415,10 @@ async function downloadCollection(req, res) {
|
||||
}
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
res.set('subscription-userinfo', sub.subUserinfo);
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,58 +125,53 @@ async function getFlowInfo(req, res) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
try {
|
||||
success(res, {
|
||||
...parseFlowHeaders(sub.subUserinfo),
|
||||
remainingDays: getRmainingDays({
|
||||
resetDay: $arguments.resetDay,
|
||||
startDate: $arguments.startDate,
|
||||
cycleDays: $arguments.cycleDays,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`Failed to parse flow info for local subscription ${name}: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'NO_FLOW_INFO',
|
||||
'N/A',
|
||||
`Failed to parse flow info`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const flowHeaders = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
$arguments.flowUrl,
|
||||
const flowHeaders = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (!flowHeaders && !sub.subUserinfo) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'NO_FLOW_INFO',
|
||||
'No flow info',
|
||||
`Failed to fetch flow headers`,
|
||||
),
|
||||
);
|
||||
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,
|
||||
}),
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const remainingDays = getRmainingDays({
|
||||
resetDay: $arguments.resetDay,
|
||||
startDate: $arguments.startDate,
|
||||
cycleDays: $arguments.cycleDays,
|
||||
});
|
||||
const result = {
|
||||
...parseFlowHeaders(
|
||||
[sub.subUserinfo, flowHeaders].filter((i) => i).join('; '),
|
||||
),
|
||||
};
|
||||
if (remainingDays != null) {
|
||||
result.remainingDays = remainingDays;
|
||||
}
|
||||
success(res, result);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`Failed to parse flow info for local subscription ${name}: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'NO_FLOW_INFO',
|
||||
'N/A',
|
||||
`Failed to parse flow info`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
failed(
|
||||
|
||||
@@ -151,7 +151,7 @@ export default async function download(
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!noCache && !$arguments?.noCache && cached) {
|
||||
$.info(`使用缓存: ${url}`);
|
||||
$.info(`使用缓存: ${url}, ${userAgent}`);
|
||||
result = cached;
|
||||
if (customCacheKey) {
|
||||
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
|
||||
@@ -179,7 +179,7 @@ export default async function download(
|
||||
if (headers) {
|
||||
const flowInfo = getFlowField(headers);
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
headersResourceCache.set(id, flowInfo);
|
||||
}
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { SETTINGS_KEY } from '@/constants';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import { getPolicyDescriptor } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||
|
||||
export function getFlowField(headers) {
|
||||
const subkey = Object.keys(headers).filter((k) =>
|
||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
const keys = Object.keys(headers);
|
||||
let sub = '';
|
||||
let webPage = '';
|
||||
for (let k of keys) {
|
||||
const lower = k.toLowerCase();
|
||||
if (lower === 'subscription-userinfo') {
|
||||
sub = headers[k];
|
||||
} else if (lower === 'profile-web-page-url') {
|
||||
webPage = headers[k];
|
||||
}
|
||||
}
|
||||
|
||||
return `${sub || ''}${webPage ? `;app_url=${webPage}` : ''}`;
|
||||
}
|
||||
export async function getFlowHeaders(
|
||||
rawUrl,
|
||||
@@ -41,29 +51,26 @@ export async function getFlowHeaders(
|
||||
return;
|
||||
}
|
||||
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||
const cached = headersResourceCache.get(url);
|
||||
const insecure = $arguments?.insecure
|
||||
? $.env.isNode
|
||||
? { strictSSL: false }
|
||||
: { insecure: true }
|
||||
: undefined;
|
||||
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
|
||||
$.read(SETTINGS_KEY);
|
||||
let proxy = customProxy || defaultProxy;
|
||||
if ($.env.isNode) {
|
||||
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
|
||||
}
|
||||
const userAgent = ua || defaultFlowUserAgent || 'clash';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
const cached = headersResourceCache.get(id);
|
||||
let flowInfo;
|
||||
if (!$arguments?.noCache && cached) {
|
||||
// $.info(`使用缓存的流量信息: ${url}`);
|
||||
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
|
||||
flowInfo = cached;
|
||||
} else {
|
||||
const insecure = $arguments?.insecure
|
||||
? $.env.isNode
|
||||
? { strictSSL: false }
|
||||
: { insecure: true }
|
||||
: undefined;
|
||||
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
|
||||
$.read(SETTINGS_KEY);
|
||||
let proxy = customProxy || defaultProxy;
|
||||
if ($.env.isNode) {
|
||||
proxy =
|
||||
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
|
||||
}
|
||||
const userAgent =
|
||||
ua ||
|
||||
defaultFlowUserAgent ||
|
||||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const http = HTTP();
|
||||
if (flowUrl) {
|
||||
$.info(
|
||||
@@ -159,7 +166,7 @@ export async function getFlowHeaders(
|
||||
}
|
||||
}
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(url, flowInfo);
|
||||
headersResourceCache.set(id, flowInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +197,29 @@ export function parseFlowHeaders(flowHeaders) {
|
||||
? Number(expireMatch[1] + expireMatch[2])
|
||||
: undefined;
|
||||
|
||||
return { expires, total, usage: { upload, download } };
|
||||
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
|
||||
const remainingDays = remainingDaysMatch
|
||||
? Number(remainingDaysMatch[1])
|
||||
: undefined;
|
||||
|
||||
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
|
||||
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
|
||||
|
||||
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
|
||||
const planName = planNameMatch
|
||||
? decodeURIComponent(planNameMatch[1])
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
expires,
|
||||
total,
|
||||
usage: { upload, download },
|
||||
remainingDays,
|
||||
appUrl,
|
||||
planName,
|
||||
};
|
||||
}
|
||||
|
||||
export function flowTransfer(flow, unit = 'B') {
|
||||
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let unitIndex = unitList.indexOf(unit);
|
||||
|
||||
@@ -2,16 +2,20 @@ export function getUserAgentFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
let ua = '';
|
||||
let accept = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
const lower = k.toLowerCase();
|
||||
if (lower === 'user-agent') {
|
||||
UA = headers[k];
|
||||
ua = UA.toLowerCase();
|
||||
break;
|
||||
} else if (lower === 'accept') {
|
||||
accept = headers[k];
|
||||
}
|
||||
}
|
||||
return { UA, ua };
|
||||
return { UA, ua, accept };
|
||||
}
|
||||
export function getPlatformFromUserAgent({ ua, UA }) {
|
||||
|
||||
export function getPlatformFromUserAgent({ ua, UA, accept }) {
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surfboard') !== -1) {
|
||||
@@ -35,15 +39,18 @@ export function getPlatformFromUserAgent({ ua, UA }) {
|
||||
return 'ClashMeta';
|
||||
} else if (ua.indexOf('clash') !== -1) {
|
||||
return 'Clash';
|
||||
} else if (ua.indexOf('v2ray') !== -1) {
|
||||
} else if (ua.indexOf('v2ray') !== -1 || ua.indexOf('egern') !== -1) {
|
||||
return 'V2Ray';
|
||||
} else if (ua.indexOf('sing-box') !== -1) {
|
||||
return 'sing-box';
|
||||
} else {
|
||||
} else if (accept.indexOf('application/json') === 0) {
|
||||
return 'JSON';
|
||||
} else {
|
||||
return 'V2Ray';
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const { UA, ua } = getUserAgentFromHeaders(headers);
|
||||
return getPlatformFromUserAgent({ ua, UA });
|
||||
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
|
||||
return getPlatformFromUserAgent({ ua, UA, accept });
|
||||
}
|
||||
|
||||
1
backend/src/vendor/express.js
vendored
1
backend/src/vendor/express.js
vendored
@@ -9,6 +9,7 @@ export default function express({ substore: $, port, host }) {
|
||||
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
|
||||
'Access-Control-Allow-Headers':
|
||||
'Origin, X-Requested-With, Content-Type, Accept',
|
||||
'X-Powered-By': 'Sub-Store',
|
||||
};
|
||||
|
||||
// node support
|
||||
|
||||
Reference in New Issue
Block a user