mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca65e4209e | ||
|
|
53bb4866e7 | ||
|
|
09495fa607 | ||
|
|
4b27d40602 | ||
|
|
518de2e919 | ||
|
|
078bf228de | ||
|
|
aaef97cf5d | ||
|
|
7beff4013f | ||
|
|
23cf81d0a5 | ||
|
|
572f2f5533 | ||
|
|
1c6d761e09 | ||
|
|
437297b8b0 | ||
|
|
ca437865e6 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.236",
|
||||
"version": "2.14.249",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -370,6 +370,10 @@ function lastParse(proxy) {
|
||||
delete proxy.ports;
|
||||
}
|
||||
if (['vless'].includes(proxy.type)) {
|
||||
// 非 reality, 空 flow 没有意义
|
||||
if (!proxy['reality-opts'] && !proxy.flow) {
|
||||
delete proxy.flow;
|
||||
}
|
||||
if (['http'].includes(proxy.network)) {
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
if (!transportPath) {
|
||||
|
||||
@@ -444,6 +444,7 @@ function URI_VLESS() {
|
||||
proxy[`${params.security}-opts`] = opts;
|
||||
}
|
||||
}
|
||||
|
||||
proxy.network = params.type;
|
||||
if (proxy.network === 'tcp' && params.headerType === 'http') {
|
||||
proxy.network = 'http';
|
||||
@@ -722,6 +723,7 @@ function Clash_All() {
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'wireguard',
|
||||
'ssh',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
@@ -77,7 +77,7 @@ http = tag equals "http" address (username password)? (usernamek passwordk)? (ip
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ssh";
|
||||
handleShadowTLS();
|
||||
}
|
||||
@@ -229,6 +229,7 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
|
||||
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
|
||||
@@ -75,7 +75,7 @@ http = tag equals "http" address (username password)? (usernamek passwordk)? (ip
|
||||
proxy.type = "http";
|
||||
handleShadowTLS();
|
||||
}
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ssh";
|
||||
handleShadowTLS();
|
||||
}
|
||||
@@ -227,6 +227,7 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
|
||||
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
parseFlowHeaders,
|
||||
validCheck,
|
||||
flowTransfer,
|
||||
getRmainingDays,
|
||||
} from '@/utils/flow';
|
||||
|
||||
/**
|
||||
@@ -357,11 +358,41 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseIP4P(IP4P) {
|
||||
let server;
|
||||
let port;
|
||||
try {
|
||||
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
|
||||
throw new Error(`Invalid IP4P: ${IP4P}`);
|
||||
}
|
||||
let array = IP4P.split(':');
|
||||
|
||||
port = parseInt(array[2], 16);
|
||||
let ipab = parseInt(array[3], 16);
|
||||
let ipcd = parseInt(array[4], 16);
|
||||
let ipa = ipab >> 8;
|
||||
let ipb = ipab & 0xff;
|
||||
let ipc = ipcd >> 8;
|
||||
let ipd = ipcd & 0xff;
|
||||
server = `${ipa}.${ipb}.${ipc}.${ipd}`;
|
||||
if (port <= 0 || port > 65535) {
|
||||
throw new Error(`Invalid port number: ${port}`);
|
||||
}
|
||||
if (!isIPv4(server)) {
|
||||
throw new Error(`Invalid IP address: ${server}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// throw new Error(`IP4P 解析失败: ${e}`);
|
||||
$.error(`IP4P 解析失败: ${e}`);
|
||||
}
|
||||
return { server, port };
|
||||
}
|
||||
|
||||
const DOMAIN_RESOLVERS = {
|
||||
Google: async function (domain, type) {
|
||||
Google: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`GOOGLE:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
@@ -382,10 +413,13 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
'IP-API': async function (domain) {
|
||||
'IP-API': async function (domain, type, noCache) {
|
||||
if (['IPv6'].includes(type)) {
|
||||
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
|
||||
}
|
||||
const id = hex_md5(`IP-API:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||
domain,
|
||||
@@ -399,10 +433,10 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Cloudflare: async function (domain, type) {
|
||||
Cloudflare: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
|
||||
domain,
|
||||
@@ -423,10 +457,10 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Ali: async function (domain, type) {
|
||||
Ali: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
@@ -443,10 +477,10 @@ const DOMAIN_RESOLVERS = {
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Tencent: async function (domain, type) {
|
||||
Tencent: async function (domain, type, noCache) {
|
||||
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
if (!noCache && cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://119.28.28.28/d?type=${
|
||||
type === 'IPv6' ? 'AAAA' : 'A'
|
||||
@@ -465,10 +499,12 @@ const DOMAIN_RESOLVERS = {
|
||||
},
|
||||
};
|
||||
|
||||
function ResolveDomainOperator({ provider, type, filter }) {
|
||||
if (type === 'IPv6' && ['IP-API'].includes(provider)) {
|
||||
throw new Error(`域名解析服务提供方 ${provider} 不支持 IPv6`);
|
||||
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
||||
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
|
||||
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
|
||||
}
|
||||
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
|
||||
|
||||
const resolver = DOMAIN_RESOLVERS[provider];
|
||||
if (!resolver) {
|
||||
throw new Error(`找不到域名解析服务提供方: ${provider}`);
|
||||
@@ -490,7 +526,7 @@ function ResolveDomainOperator({ provider, type, filter }) {
|
||||
const currentBatch = [];
|
||||
for (let domain of totalDomain.splice(0, limit)) {
|
||||
currentBatch.push(
|
||||
resolver(domain, type)
|
||||
resolver(domain, type, cache === 'disabled')
|
||||
.then((ip) => {
|
||||
results[domain] = ip;
|
||||
$.info(
|
||||
@@ -509,8 +545,19 @@ function ResolveDomainOperator({ provider, type, filter }) {
|
||||
proxies.forEach((p) => {
|
||||
if (!p['no-resolve']) {
|
||||
if (results[p.server]) {
|
||||
p.server = results[p.server];
|
||||
p.resolved = true;
|
||||
if (_type === 'IP4P') {
|
||||
const { server, port } = parseIP4P(
|
||||
results[p.server],
|
||||
);
|
||||
if (server && port) {
|
||||
p.server = server;
|
||||
p.port = port;
|
||||
p.resolved = true;
|
||||
}
|
||||
} else {
|
||||
p.server = results[p.server];
|
||||
p.resolved = true;
|
||||
}
|
||||
} else {
|
||||
p.resolved = false;
|
||||
}
|
||||
@@ -817,6 +864,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
parseFlowHeaders,
|
||||
flowTransfer,
|
||||
validCheck,
|
||||
getRmainingDays,
|
||||
};
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
|
||||
@@ -228,6 +228,9 @@ const sshParser = (proxy = {}) => {
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.user = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
// 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['server-fingerprint']) {
|
||||
parsedProxy.host_key = [proxy['server-fingerprint']];
|
||||
// https://manual.nssurge.com/policy/ssh.html
|
||||
|
||||
@@ -356,8 +356,15 @@ function ssh(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
// 所有的类似的字段都有双引号的问题 暂不处理
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
// https://manual.nssurge.com/policy/ssh.html
|
||||
// 需配合 Keystore
|
||||
result.appendIfPresent(
|
||||
`,private-key=${proxy['keystore-private-key']}`,
|
||||
'keystore-private-key',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,idle-timeout=${proxy['idle-timeout']}`,
|
||||
'idle-timeout',
|
||||
@@ -843,7 +850,7 @@ private-key = ${proxy['private-key']}`);
|
||||
}
|
||||
const peer = {
|
||||
'public-key': proxy['public-key'],
|
||||
'allowed-ips': allowedIps,
|
||||
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
|
||||
endpoint: `${proxy.server}:${proxy.port}`,
|
||||
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
|
||||
'client-id': reserved,
|
||||
|
||||
@@ -77,12 +77,44 @@ 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);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
@@ -92,6 +124,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 +211,42 @@ 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);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
@@ -190,6 +256,9 @@ async function downloadCollection(req, res) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
res.set('subscription-userinfo', sub.subUserinfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
} from './errors';
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
|
||||
import {
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
getRmainingDays,
|
||||
} from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
|
||||
@@ -43,32 +47,93 @@ async function getFlowInfo(req, res) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sub.source === 'local') {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'NO_FLOW_INFO',
|
||||
'N/A',
|
||||
`Local subscription ${name} has no flow information!`,
|
||||
),
|
||||
);
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
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 {
|
||||
const flowHeaders = await getFlowHeaders(sub.url);
|
||||
if (!flowHeaders) {
|
||||
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) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
new RequestInvalidError(
|
||||
'NO_FLOW_INFO',
|
||||
'No flow info',
|
||||
`Failed to fetch flow headers`,
|
||||
'N/A',
|
||||
`Subscription ${name}: noFlow`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
success(res, parseFlowHeaders(flowHeaders));
|
||||
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);
|
||||
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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
|
||||
@@ -35,13 +35,21 @@ async function produceArtifact({
|
||||
ignoreFailedRemoteFile,
|
||||
produceType,
|
||||
produceOpts = {},
|
||||
subscription,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
if (type === 'subscription') {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
if (!sub) throw new Error(`找不到订阅 ${name}`);
|
||||
let sub;
|
||||
if (name) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
sub = findByName(allSubs, name);
|
||||
if (!sub) throw new Error(`找不到订阅 ${name}`);
|
||||
} else if (subscription) {
|
||||
sub = subscription;
|
||||
} else {
|
||||
throw new Error('未提供订阅名称或订阅数据');
|
||||
}
|
||||
let raw;
|
||||
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||
raw = content;
|
||||
|
||||
@@ -53,7 +53,8 @@ export default async function download(rawUrl, ua, timeout) {
|
||||
// }
|
||||
|
||||
const { isNode } = ENV();
|
||||
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
|
||||
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
||||
$.read(SETTINGS_KEY);
|
||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
@@ -90,8 +91,22 @@ export default async function download(rawUrl, ua, timeout) {
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
throw new Error(new Error('远程资源内容为空'));
|
||||
let shouldCache = true;
|
||||
if (cacheThreshold) {
|
||||
const size = body.length / 1024;
|
||||
if (size > cacheThreshold) {
|
||||
$.info(
|
||||
`资源大小 ${size.toFixed(
|
||||
2,
|
||||
)} KB 超过了 ${cacheThreshold} KB, 不缓存`,
|
||||
);
|
||||
shouldCache = false;
|
||||
}
|
||||
}
|
||||
if (shouldCache) {
|
||||
resourceCache.set(id, body);
|
||||
}
|
||||
|
||||
resourceCache.set(id, body);
|
||||
result = body;
|
||||
} catch (e) {
|
||||
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { version as substoreVersion } from '../../package.json';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
|
||||
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
||||
const {
|
||||
isNode,
|
||||
isQX,
|
||||
isLoon,
|
||||
isSurge,
|
||||
isStash,
|
||||
isShadowRocket,
|
||||
isLanceX,
|
||||
isEgern,
|
||||
} = ENV();
|
||||
let backend = 'Node';
|
||||
if (isNode) backend = 'Node';
|
||||
if (isQX) backend = 'QX';
|
||||
@@ -9,8 +18,44 @@ if (isLoon) backend = 'Loon';
|
||||
if (isSurge) backend = 'Surge';
|
||||
if (isStash) backend = 'Stash';
|
||||
if (isShadowRocket) backend = 'ShadowRocket';
|
||||
if (isEgern) backend = 'Egern';
|
||||
if (isLanceX) backend = 'LanceX';
|
||||
|
||||
let meta = {};
|
||||
|
||||
try {
|
||||
if (typeof $environment !== 'undefined') {
|
||||
// eslint-disable-next-line no-undef
|
||||
meta.env = $environment;
|
||||
}
|
||||
if (typeof $loon !== 'undefined') {
|
||||
// eslint-disable-next-line no-undef
|
||||
meta.loon = $loon;
|
||||
}
|
||||
if (typeof $script !== 'undefined') {
|
||||
// eslint-disable-next-line no-undef
|
||||
meta.script = $script;
|
||||
}
|
||||
if (isNode) {
|
||||
meta.node = {
|
||||
version: eval('process.version'),
|
||||
argv: eval('process.argv'),
|
||||
filename: eval('__filename'),
|
||||
dirname: eval('__dirname'),
|
||||
env: {},
|
||||
};
|
||||
const env = eval('process.env');
|
||||
for (const key in env) {
|
||||
if (/^SUB_STORE_/.test(key)) {
|
||||
meta.node.env[key] = env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
|
||||
export default {
|
||||
backend,
|
||||
version: substoreVersion,
|
||||
meta,
|
||||
};
|
||||
|
||||
@@ -143,3 +143,60 @@ export function validCheck(flow) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRmainingDays(opt = {}) {
|
||||
try {
|
||||
let { resetDay, startDate, cycleDays } = opt;
|
||||
if (['string', 'number'].includes(typeof opt)) {
|
||||
resetDay = opt;
|
||||
}
|
||||
|
||||
if (startDate && cycleDays) {
|
||||
cycleDays = parseInt(cycleDays);
|
||||
if (isNaN(cycleDays) || cycleDays <= 0)
|
||||
throw new Error('重置周期应为正整数');
|
||||
if (!startDate || !Date.parse(startDate))
|
||||
throw new Error('开始日期不合法');
|
||||
|
||||
const start = new Date(startDate);
|
||||
const today = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (start.getTime() > today.getTime())
|
||||
throw new Error('开始日期应早于现在');
|
||||
|
||||
let resetDate = new Date(startDate);
|
||||
resetDate.setDate(resetDate.getDate() + cycleDays);
|
||||
|
||||
while (resetDate < today) {
|
||||
resetDate.setDate(resetDate.getDate() + cycleDays);
|
||||
}
|
||||
|
||||
resetDate.setHours(0, 0, 0, 0);
|
||||
const timeDiff = resetDate.getTime() - today.getTime();
|
||||
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
|
||||
return daysDiff;
|
||||
} else {
|
||||
if (!resetDay) throw new Error('未提供月重置日 resetDay');
|
||||
resetDay = parseInt(resetDay);
|
||||
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
|
||||
throw new Error('月重置日应为 1-31 之间的整数');
|
||||
let now = new Date();
|
||||
let today = now.getDate();
|
||||
let month = now.getMonth();
|
||||
let year = now.getFullYear();
|
||||
let daysInMonth;
|
||||
|
||||
if (resetDay > today) {
|
||||
daysInMonth = 0;
|
||||
} else {
|
||||
daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
|
||||
return daysInMonth - today + resetDay;
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`getRmainingDays failed: ${e.message ?? e}`);
|
||||
}
|
||||
}
|
||||
|
||||
13
backend/src/vendor/open-api.js
vendored
13
backend/src/vendor/open-api.js
vendored
@@ -6,6 +6,8 @@ const isNode = eval(`typeof process !== "undefined"`); // eval is needed in orde
|
||||
const isStash =
|
||||
'undefined' !== typeof $environment && $environment['stash-version'];
|
||||
const isShadowRocket = 'undefined' !== typeof $rocket;
|
||||
const isEgern = 'object' == typeof egern;
|
||||
const isLanceX = 'undefined' != typeof $native;
|
||||
|
||||
export class OpenAPI {
|
||||
constructor(name = 'untitled', debug = false) {
|
||||
@@ -251,7 +253,16 @@ export class OpenAPI {
|
||||
}
|
||||
|
||||
export function ENV() {
|
||||
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket };
|
||||
return {
|
||||
isQX,
|
||||
isLoon,
|
||||
isSurge,
|
||||
isNode,
|
||||
isStash,
|
||||
isShadowRocket,
|
||||
isEgern,
|
||||
isLanceX,
|
||||
};
|
||||
}
|
||||
|
||||
export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
|
||||
Reference in New Issue
Block a user