mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9426f128c4 | ||
|
|
ebc7173c95 | ||
|
|
dd4e0cef68 | ||
|
|
b1618c3803 | ||
|
|
1b4c046b75 | ||
|
|
41034ceb46 | ||
|
|
6efb19c856 | ||
|
|
2cd30dfe68 | ||
|
|
d53947d820 | ||
|
|
7e75031e92 | ||
|
|
4a07c02dc1 | ||
|
|
95d6688539 | ||
|
|
a23e2ffcd6 | ||
|
|
fda1252d0e | ||
|
|
62c5c2e15b | ||
|
|
ffabcc9391 | ||
|
|
0825f15d04 | ||
|
|
fbf6b5ce6e | ||
|
|
3eb0816c88 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.16.44",
|
||||
"version": "2.16.63",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { produceArtifact } from '@/restful/sync';
|
||||
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
|
||||
import Gist from '@/utils/gist';
|
||||
import { isPresent } from './producers/utils';
|
||||
import { doh } from '@/utils/dns';
|
||||
|
||||
function preprocess(raw) {
|
||||
for (const processor of PROXY_PREPROCESSORS) {
|
||||
@@ -81,9 +82,9 @@ function parse(raw) {
|
||||
if (['vless', 'vmess'].includes(proxy.type)) {
|
||||
const isProxyUUIDValid = isValidUUID(proxy.uuid);
|
||||
if (!isProxyUUIDValid) {
|
||||
$.error(`UUID is invalid: ${proxy.name} ${proxy.uuid}`);
|
||||
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
|
||||
}
|
||||
return isProxyUUIDValid;
|
||||
// return isProxyUUIDValid;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -235,8 +236,8 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
if (['vless', 'vmess'].includes(proxy.type)) {
|
||||
const isProxyUUIDValid = isValidUUID(proxy.uuid);
|
||||
if (!isProxyUUIDValid)
|
||||
$.error(`UUID is invalid: ${proxy.name} ${proxy.uuid}`);
|
||||
return isProxyUUIDValid;
|
||||
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
|
||||
// return isProxyUUIDValid;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -326,6 +327,8 @@ export const ProxyUtils = {
|
||||
MMDB,
|
||||
Gist,
|
||||
download,
|
||||
isValidUUID,
|
||||
doh,
|
||||
};
|
||||
|
||||
function tryParse(parser, line) {
|
||||
@@ -423,9 +426,14 @@ function lastParse(proxy) {
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,8 @@ function URI_SS() {
|
||||
|
||||
// handle obfs
|
||||
const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
|
||||
const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
|
||||
|
||||
if (pluginMatch) {
|
||||
const pluginInfo = (
|
||||
'plugin=' + decodeURIComponent(pluginMatch[1])
|
||||
@@ -233,6 +235,25 @@ function URI_SS() {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Shadowrocket
|
||||
if (shadowTlsMatch) {
|
||||
const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));
|
||||
const version = getIfNotBlank(params['version']);
|
||||
const address = getIfNotBlank(params['address']);
|
||||
const port = getIfNotBlank(params['port']);
|
||||
proxy.plugin = 'shadow-tls';
|
||||
proxy['plugin-opts'] = {
|
||||
host: getIfNotBlank(params['host']),
|
||||
password: getIfNotBlank(params['password']),
|
||||
version: version ? parseInt(version, 10) : undefined,
|
||||
};
|
||||
if (address) {
|
||||
proxy.server = address;
|
||||
}
|
||||
if (port) {
|
||||
proxy.port = parseInt(port, 10);
|
||||
}
|
||||
}
|
||||
if (/(&|\?)uot=(1|true)/i.test(query)) {
|
||||
proxy['udp-over-tcp'] = true;
|
||||
}
|
||||
@@ -865,18 +886,18 @@ function URI_TUIC() {
|
||||
|
||||
for (const addon of addons.split('&')) {
|
||||
let [key, value] = addon.split('=');
|
||||
key = key.replace(/_/, '-');
|
||||
key = key.replace(/_/g, '-');
|
||||
value = decodeURIComponent(value);
|
||||
if (['alpn'].includes(key)) {
|
||||
proxy[key] = value ? value.split(',') : undefined;
|
||||
} else if (['allow_insecure'].includes(key)) {
|
||||
} else if (['allow-insecure'].includes(key)) {
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
|
||||
} else if (['fast_open'].includes(key)) {
|
||||
} else if (['fast-open'].includes(key)) {
|
||||
proxy.tfo = true;
|
||||
} else if (['disable_sni', 'reduce_rtt'].includes(key)) {
|
||||
proxy[key.replace(/_/g, '-')] = /(TRUE)|1/i.test(value);
|
||||
} else if (['disable-sni', 'reduce-rtt'].includes(key)) {
|
||||
proxy[key] = /(TRUE)|1/i.test(value);
|
||||
} else {
|
||||
proxy[key.replace(/_/g, '-')] = value;
|
||||
proxy[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,6 +1031,7 @@ function Clash_All() {
|
||||
const proxy = JSON.parse(line);
|
||||
if (
|
||||
![
|
||||
'anytls',
|
||||
'mieru',
|
||||
'juicity',
|
||||
'ss',
|
||||
|
||||
@@ -63,7 +63,7 @@ function Clash() {
|
||||
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
|
||||
// 匹配 short-id 冒号后面的值(包含空格和引号)
|
||||
const afterReplace = raw.replace(
|
||||
/short-id:([ ]*[^,\n}]*)/g,
|
||||
/short-id:([ \t]*[^#\n,}]*)/g,
|
||||
(matched, value) => {
|
||||
const afterTrim = value.trim();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
validCheck,
|
||||
flowTransfer,
|
||||
getRmainingDays,
|
||||
normalizeFlowHeader,
|
||||
} from '@/utils/flow';
|
||||
|
||||
function isObject(item) {
|
||||
@@ -365,24 +366,35 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
|
||||
if (output?.$file?.type === 'mihomoProfile') {
|
||||
try {
|
||||
let patch = YAML.safeLoad(script);
|
||||
let config;
|
||||
if (output?.$content) {
|
||||
try {
|
||||
config = YAML.safeLoad(output?.$content);
|
||||
} catch (e) {
|
||||
$.error(e.message ?? e);
|
||||
}
|
||||
}
|
||||
// if (typeof patch !== 'object') patch = {};
|
||||
if (typeof patch !== 'object')
|
||||
throw new Error('patch is not an object');
|
||||
output.$content = ProxyUtils.yaml.safeDump(
|
||||
deepMerge(
|
||||
{
|
||||
proxies: await produceArtifact({
|
||||
type:
|
||||
output?.$file?.sourceType ||
|
||||
'collection',
|
||||
name: output?.$file?.sourceName,
|
||||
platform: 'mihomo',
|
||||
produceType: 'internal',
|
||||
produceOpts: {
|
||||
'delete-underscore-fields': true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
config ||
|
||||
(output?.$file?.sourceType === 'none'
|
||||
? {}
|
||||
: {
|
||||
proxies: await produceArtifact({
|
||||
type:
|
||||
output?.$file?.sourceType ||
|
||||
'collection',
|
||||
name: output?.$file?.sourceName,
|
||||
platform: 'mihomo',
|
||||
produceType: 'internal',
|
||||
produceOpts: {
|
||||
'delete-underscore-fields': true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
patch,
|
||||
),
|
||||
);
|
||||
@@ -413,7 +425,15 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
|
||||
if($file.type === 'mihomoProfile') {
|
||||
${script}
|
||||
if(typeof main === 'function') {
|
||||
const config = {
|
||||
let config;
|
||||
if ($content) {
|
||||
try {
|
||||
config = ProxyUtils.yaml.safeLoad($content);
|
||||
} catch (e) {
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
}
|
||||
$content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
|
||||
proxies: await produceArtifact({
|
||||
type: $file.sourceType || 'collection',
|
||||
name: $file.sourceName,
|
||||
@@ -423,8 +443,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
|
||||
'delete-underscore-fields': true
|
||||
}
|
||||
}),
|
||||
}
|
||||
$content = ProxyUtils.yaml.safeDump(await main(config))
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
${script}
|
||||
@@ -1083,6 +1102,7 @@ function createDynamicFunction(name, script, $arguments, $options) {
|
||||
flowTransfer,
|
||||
validCheck,
|
||||
getRmainingDays,
|
||||
normalizeFlowHeader,
|
||||
};
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
|
||||
@@ -141,6 +141,7 @@ export default function Clash_Producer() {
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
|
||||
@@ -105,6 +105,9 @@ export default function ClashMeta_Producer() {
|
||||
password: proxy['shadow-tls-password'],
|
||||
version: proxy['shadow-tls-version'],
|
||||
};
|
||||
delete proxy['shadow-tls-password'];
|
||||
delete proxy['shadow-tls-sni'];
|
||||
delete proxy['shadow-tls-version'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +163,7 @@ export default function ClashMeta_Producer() {
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isPresent } from './utils';
|
||||
|
||||
export default function Egern_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
const produce = (proxies, type) => {
|
||||
// https://egernapp.com/zh-CN/docs/configuration/proxies
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
@@ -71,6 +73,7 @@ export default function Egern_Producer() {
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
const original = { ...proxy };
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
proxy.sni = proxy.server;
|
||||
}
|
||||
@@ -215,7 +218,9 @@ export default function Egern_Producer() {
|
||||
proxy.transport = {
|
||||
http1: {
|
||||
method: proxy['http-opts']?.method,
|
||||
path: proxy['http-opts']?.path,
|
||||
path: Array.isArray(proxy['http-opts']?.path)
|
||||
? proxy['http-opts']?.path[0]
|
||||
: proxy['http-opts']?.path,
|
||||
headers: {
|
||||
Host: Array.isArray(
|
||||
proxy['http-opts']?.headers?.Host,
|
||||
@@ -230,7 +235,9 @@ export default function Egern_Producer() {
|
||||
proxy.transport = {
|
||||
http2: {
|
||||
method: proxy['h2-opts']?.method,
|
||||
path: proxy['h2-opts']?.path,
|
||||
path: Array.isArray(proxy['h2-opts']?.path)
|
||||
? proxy['h2-opts']?.path[0]
|
||||
: proxy['h2-opts']?.path,
|
||||
headers: {
|
||||
Host: Array.isArray(
|
||||
proxy['h2-opts']?.headers?.Host,
|
||||
@@ -288,7 +295,9 @@ export default function Egern_Producer() {
|
||||
proxy.transport = {
|
||||
http: {
|
||||
method: proxy['http-opts']?.method,
|
||||
path: proxy['http-opts']?.path,
|
||||
path: Array.isArray(proxy['http-opts']?.path)
|
||||
? proxy['http-opts']?.path[0]
|
||||
: proxy['http-opts']?.path,
|
||||
headers: {
|
||||
Host: Array.isArray(
|
||||
proxy['http-opts']?.headers?.Host,
|
||||
@@ -326,6 +335,39 @@ export default function Egern_Producer() {
|
||||
// skip_tls_verify: proxy['skip-cert-verify'],
|
||||
};
|
||||
}
|
||||
if (
|
||||
[
|
||||
'http',
|
||||
'socks5',
|
||||
'ss',
|
||||
'trojan',
|
||||
'vless',
|
||||
'vmess',
|
||||
].includes(original.type)
|
||||
) {
|
||||
if (isPresent(original, 'shadow-tls-password')) {
|
||||
if (original['shadow-tls-version'] != 3)
|
||||
throw new Error(
|
||||
`shadow-tls version ${original['shadow-tls-version']} is not supported`,
|
||||
);
|
||||
proxy.shadow_tls = {
|
||||
password: original['shadow-tls-password'],
|
||||
sni: original['shadow-tls-sni'],
|
||||
};
|
||||
} else if (
|
||||
['shadow-tls'].includes(original.plugin) &&
|
||||
original['plugin-opts']
|
||||
) {
|
||||
if (original['plugin-opts'].version != 3)
|
||||
throw new Error(
|
||||
`shadow-tls version ${original['plugin-opts'].version} is not supported`,
|
||||
);
|
||||
proxy.shadow_tls = {
|
||||
password: original['plugin-opts'].password,
|
||||
sni: original['plugin-opts'].host,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
|
||||
@@ -341,10 +341,9 @@ function vmess(proxy) {
|
||||
// udp
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
const ip_version =
|
||||
ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||
}
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -416,10 +415,9 @@ function vless(proxy) {
|
||||
// udp
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
const ip_version =
|
||||
ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||
}
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Shadowrocket_Producer() {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||
return false;
|
||||
} else if (['mieru'].includes(proxy.type)) {
|
||||
} else if (['mieru', 'anytls'].includes(proxy.type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -110,6 +110,21 @@ export default function Shadowrocket_Producer() {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
} else if (proxy.type === 'ss') {
|
||||
if (
|
||||
isPresent(proxy, 'shadow-tls-password') &&
|
||||
!isPresent(proxy, 'plugin')
|
||||
) {
|
||||
proxy.plugin = 'shadow-tls';
|
||||
proxy['plugin-opts'] = {
|
||||
host: proxy['shadow-tls-sni'],
|
||||
password: proxy['shadow-tls-password'],
|
||||
version: proxy['shadow-tls-version'],
|
||||
};
|
||||
delete proxy['shadow-tls-password'];
|
||||
delete proxy['shadow-tls-sni'];
|
||||
delete proxy['shadow-tls-version'];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -163,6 +178,7 @@ export default function Shadowrocket_Producer() {
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
|
||||
@@ -641,6 +641,23 @@ const tuic5Parser = (proxy = {}) => {
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const anytlsParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'anytls',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
password: proxy.password,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (/^\d+$/.test(proxy['idle-session-check-interval']))
|
||||
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
|
||||
if (/^\d+$/.test(proxy['idle-session-timeout']))
|
||||
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
|
||||
detourParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const wireguardParser = (proxy = {}) => {
|
||||
const local_address = ['ip', 'ipv6']
|
||||
@@ -829,6 +846,9 @@ export default function singbox_Producer() {
|
||||
case 'wireguard':
|
||||
list.push(wireguardParser(proxy));
|
||||
break;
|
||||
case 'anytls':
|
||||
list.push(anytlsParser(proxy));
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||
|
||||
@@ -247,6 +247,7 @@ export default function Stash_Producer() {
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
|
||||
@@ -433,6 +433,9 @@ function ssh(proxy) {
|
||||
return result.toString();
|
||||
}
|
||||
function http(proxy) {
|
||||
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
|
||||
throw new Error(`headers is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
const type = proxy.tls ? 'https' : 'http';
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
import { failed } from '@/restful/response';
|
||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||
@@ -259,7 +259,10 @@ async function downloadSubscription(req, res) {
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
normalizeFlowHeader(flowInfo),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -293,10 +296,9 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[subUserInfo, flowInfo]
|
||||
.filter((i) => i)
|
||||
.join('; ')
|
||||
.replace(/\s*;\s*;\s*/g, ';'),
|
||||
normalizeFlowHeader(
|
||||
[subUserInfo, flowInfo].filter((i) => i).join(';'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -556,7 +558,7 @@ async function downloadCollection(req, res) {
|
||||
if (subUserInfo) {
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
subUserInfo.replace(/\s*;\s*;\s*/g, ';'),
|
||||
normalizeFlowHeader(subUserInfo),
|
||||
);
|
||||
}
|
||||
if (platform === 'JSON') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||
import { FILES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
@@ -148,7 +148,7 @@ async function getFile(req, res) {
|
||||
if (flowInfo) {
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
flowInfo.replace(/\s*;\s*;\s*/g, ';'),
|
||||
normalizeFlowHeader(flowInfo),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import express from '@/vendor/express';
|
||||
import $ from '@/core/app';
|
||||
import migrate from '@/utils/migration';
|
||||
import download from '@/utils/download';
|
||||
import { syncArtifacts } from '@/restful/sync';
|
||||
import { syncArtifacts, produceArtifact } from '@/restful/sync';
|
||||
import { gistBackupAction } from '@/restful/miscs';
|
||||
import { TOKENS_KEY } from '@/constants';
|
||||
|
||||
@@ -75,6 +75,39 @@ export default function serve() {
|
||||
// 'Asia/Shanghai' // timeZone
|
||||
);
|
||||
}
|
||||
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
|
||||
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
|
||||
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
|
||||
if (produce_cron) {
|
||||
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
|
||||
const { CronJob } = eval(`require("cron")`);
|
||||
produce_cron.split(/\s*;\s*/).map((item) => {
|
||||
const [cron, type, name] = item.split(/\s*,\s*/);
|
||||
new CronJob(
|
||||
cron.trim(),
|
||||
async function () {
|
||||
try {
|
||||
$.info(
|
||||
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
|
||||
);
|
||||
await produceArtifact({ type, name });
|
||||
$.info(
|
||||
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}, // onTick
|
||||
null, // onComplete
|
||||
true, // start
|
||||
// 'Asia/Shanghai' // timeZone
|
||||
);
|
||||
});
|
||||
}
|
||||
const backend_download_cron = eval(
|
||||
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
|
||||
);
|
||||
|
||||
@@ -109,6 +109,21 @@ async function gistBackupAction(action) {
|
||||
const updated = settings.syncTime;
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
try {
|
||||
content = $.read('#sub-store');
|
||||
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
|
||||
$.info(`下载备份, 与本地内容对比...`);
|
||||
const onlineContent = await gist.download(
|
||||
GIST_BACKUP_FILE_NAME,
|
||||
);
|
||||
if (onlineContent === content) {
|
||||
$.info(`内容一致, 无需上传备份`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
$.error(`${error.message ?? error}`);
|
||||
}
|
||||
|
||||
// update syncTime
|
||||
settings.syncTime = new Date().getTime();
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
|
||||
@@ -43,7 +43,7 @@ async function produceArtifact({
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
if (type === 'subscription') {
|
||||
if (['subscription', 'sub'].includes(type)) {
|
||||
let sub;
|
||||
if (name) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
@@ -190,7 +190,7 @@ async function produceArtifact({
|
||||
}
|
||||
// produce
|
||||
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||
} else if (type === 'collection') {
|
||||
} else if (['collection', 'col'].includes(type)) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
|
||||
@@ -313,3 +313,41 @@ export function getRmainingDays(opt = {}) {
|
||||
$.error(`getRmainingDays failed: ${e.message ?? e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeFlowHeader(flowHeaders) {
|
||||
try {
|
||||
// 使用 Map 保持顺序并处理重复键
|
||||
const kvMap = new Map();
|
||||
|
||||
flowHeaders
|
||||
.split(';')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((pair) => {
|
||||
const eqIndex = pair.indexOf('=');
|
||||
if (eqIndex === -1) return;
|
||||
|
||||
const key = pair.slice(0, eqIndex).trim();
|
||||
const encodedValue = pair.slice(eqIndex + 1).trim();
|
||||
|
||||
// 只保留第一个出现的 key
|
||||
if (!kvMap.has(key)) {
|
||||
try {
|
||||
// 解码 URI 组件并保留原始值作为 fallback
|
||||
const decodedValue = decodeURIComponent(encodedValue);
|
||||
kvMap.set(key, decodedValue);
|
||||
} catch (e) {
|
||||
kvMap.set(key, encodedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 拼接标准化字符串
|
||||
return Array.from(kvMap.entries())
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
|
||||
.join('; ');
|
||||
} catch (e) {
|
||||
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
|
||||
return flowHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ const ISOFlags = {
|
||||
'🇧🇬': ['BG', 'BGR'],
|
||||
'🇧🇭': ['BH', 'BHR'],
|
||||
'🇧🇴': ['BO', 'BOL'],
|
||||
'🇧🇳': ['BN', 'BRN'],
|
||||
'🇧🇷': ['BR', 'BRA'],
|
||||
'🇧🇹': ['BT', 'BTN'],
|
||||
'🇧🇾': ['BY', 'BLR'],
|
||||
'🇨🇦': ['CA', 'CAN'],
|
||||
'🇨🇭': ['CH', 'CHE'],
|
||||
@@ -40,6 +42,7 @@ const ISOFlags = {
|
||||
'🇬🇪': ['GE', 'GEO'],
|
||||
'🇬🇷': ['GR', 'GRC'],
|
||||
'🇬🇹': ['GT', 'GTM'],
|
||||
'🇬🇺': ['GU', 'GUM'],
|
||||
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
|
||||
'🇭🇷': ['HR', 'HRV'],
|
||||
'🇭🇺': ['HU', 'HUN'],
|
||||
@@ -59,12 +62,15 @@ const ISOFlags = {
|
||||
'🇮🇷': ['IR', 'IRN'],
|
||||
'🇮🇸': ['IS', 'ISL'],
|
||||
'🇮🇹': ['IT', 'ITA'],
|
||||
'🇱🇦': ['LA', 'LAO'],
|
||||
'🇱🇰': ['LK', 'LKA'],
|
||||
'🇱🇹': ['LT', 'LTU'],
|
||||
'🇱🇺': ['LU', 'LUX'],
|
||||
'🇱🇻': ['LV', 'LVA'],
|
||||
'🇲🇦': ['MA', 'MAR'],
|
||||
'🇲🇩': ['MD', 'MDA'],
|
||||
'🇳🇬': ['NG', 'NGA'],
|
||||
'🇲🇲': ['MM', 'MMR'],
|
||||
'🇲🇰': ['MK', 'MKD'],
|
||||
'🇲🇳': ['MN', 'MNG'],
|
||||
'🇲🇴': ['MO', 'MAC', 'CTM'],
|
||||
@@ -83,6 +89,7 @@ const ISOFlags = {
|
||||
'🇵🇷': ['PR', 'PRI'],
|
||||
'🇵🇹': ['PT', 'PRT'],
|
||||
'🇵🇾': ['PY', 'PRY'],
|
||||
'🇵🇬': ['PG', 'PNG'],
|
||||
'🇷🇴': ['RO', 'ROU'],
|
||||
'🇷🇸': ['RS', 'SRB'],
|
||||
'🇷🇪': ['RE', 'REU'],
|
||||
@@ -142,8 +149,10 @@ export function getFlag(name) {
|
||||
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
|
||||
'🇧🇭': ['Bahrain', '巴林'],
|
||||
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
||||
'🇧🇳': ['Brunei', '文莱', '汶萊'],
|
||||
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
||||
'🇧🇴': ['Bolivia', '玻利维亚'],
|
||||
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
|
||||
'🇨🇦': [
|
||||
'Canada',
|
||||
'加拿大',
|
||||
@@ -194,6 +203,7 @@ export function getFlag(name) {
|
||||
],
|
||||
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
||||
'🇬🇷': ['Greece', '希腊', '希臘'],
|
||||
'🇬🇺': ['Guam', '关岛', '關島'],
|
||||
'🇬🇹': ['Guatemala', '危地马拉'],
|
||||
'🇭🇰': [
|
||||
'Hongkong',
|
||||
@@ -254,11 +264,14 @@ export function getFlag(name) {
|
||||
'🇮🇷': ['Iran', '伊朗'],
|
||||
'🇮🇸': ['Iceland', '冰岛', '冰島'],
|
||||
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
|
||||
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
|
||||
'🇱🇦': ['Laos', '老挝', '老撾'],
|
||||
'🇱🇹': ['Lithuania', '立陶宛'],
|
||||
'🇱🇺': ['Luxembourg', '卢森堡'],
|
||||
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
|
||||
'🇲🇦': ['Morocco', '摩洛哥'],
|
||||
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
|
||||
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
|
||||
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
|
||||
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
|
||||
'🇲🇳': ['Mongolia', '蒙古'],
|
||||
@@ -284,6 +297,7 @@ export function getFlag(name) {
|
||||
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
|
||||
'🇵🇷': ['Puerto Rico', '波多黎各'],
|
||||
'🇵🇹': ['Portugal', '葡萄牙'],
|
||||
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
|
||||
'🇵🇾': ['Paraguay', '巴拉圭'],
|
||||
'🇷🇴': ['Romania', '罗马尼亚'],
|
||||
'🇷🇸': ['Serbia', '塞尔维亚'],
|
||||
|
||||
@@ -118,6 +118,7 @@ export default class Gist {
|
||||
.get('/gists?per_page=100&page=1')
|
||||
.then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
$.info(`获取到当前 GitHub 用户的 gist: ${gists.length} 个`);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g;
|
||||
|
||||
@@ -120,7 +120,7 @@ function numberToString(value) {
|
||||
function isValidUUID(uuid) {
|
||||
return (
|
||||
typeof uuid === 'string' &&
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||
uuid,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ResourceCache {
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
_cleanup(prefix, expires) {
|
||||
// clear obsolete cached resource
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
@@ -35,7 +35,11 @@ class ResourceCache {
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
if (
|
||||
new Date().getTime() - updated.time >
|
||||
(expires ?? this.expires) ||
|
||||
(prefix && id.startsWith(prefix))
|
||||
) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
@@ -52,10 +56,15 @@ class ResourceCache {
|
||||
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
get(id, expires, remove) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
if (updated) {
|
||||
if (new Date().getTime() - updated <= (expires ?? this.expires))
|
||||
return this.resourceCache[id].data;
|
||||
if (remove) {
|
||||
delete this.resourceCache[id];
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
|
||||
|
||||
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
|
||||
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
|
||||
// const fs = eval(`require("fs")`)
|
||||
// // const path = eval(`require("path")`)
|
||||
// fs.writeFileSync('/tmp/1.txt', $content, "utf8");
|
||||
|
||||
// $arguments 为传入的脚本参数
|
||||
|
||||
@@ -48,8 +52,32 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// scriptResourceCache 缓存
|
||||
// 可参考 https://t.me/zhetengsha/1003
|
||||
// const cache = scriptResourceCache
|
||||
// cache.set(id, data)
|
||||
// cache.get(id)
|
||||
// 设置
|
||||
// cache.set('a:1', 1)
|
||||
// cache.set('a:2', 2)
|
||||
// 获取
|
||||
// cache.get('a:1')
|
||||
// 支持第二个参数: 自定义过期时间
|
||||
// 支持第三个参数: 是否删除过期项
|
||||
// cache.get('a:2', 1000, true)
|
||||
|
||||
// 清理
|
||||
// cache._cleanup()
|
||||
// 支持第一个参数: 匹配前缀的项也一起删除
|
||||
// 支持第二个参数: 自定义过期时间
|
||||
// cache._cleanup('a:', 1000)
|
||||
|
||||
// 关于缓存时长
|
||||
|
||||
// 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
|
||||
|
||||
// 远程订阅缓存是 1 小时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
|
||||
|
||||
// 当使用相关脚本时, 若在对应的脚本中使用参数开启缓存, 可设置持久化缓存 sub-store-csr-expiration-time 的值来自定义默认缓存时长, 默认为 172800000 (48 * 3600 * 1000, 即 48 小时)
|
||||
|
||||
// 🎈Loon 可在插件中设置
|
||||
|
||||
// 其他平台同理, 持久化缓存数据在 JSON 里
|
||||
|
||||
// ProxyUtils 为节点处理工具
|
||||
// 可参考 https://t.me/zhetengsha/1066
|
||||
@@ -69,6 +97,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// Gist, // Gist 类
|
||||
// download, // 内部的下载方法, 见 backend/src/utils/download.js
|
||||
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
|
||||
// isValidUUID, // 辅助判断是否为有效的 UUID
|
||||
// }
|
||||
|
||||
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
|
||||
|
||||
Reference in New Issue
Block a user