Compare commits

...

12 Commits

Author SHA1 Message Date
xream
8c943176a5 feat: VLESS URI 输入兼容 Shadowrocket 导出格式 2024-01-16 01:00:22 +08:00
xream
f4c4cdba67 fix: 修复响应头缓存 2024-01-14 23:44:15 +08:00
xream
ada03be05f chore: Loon 插件支持修改响应头缓存有效期 2024-01-14 23:39:27 +08:00
xream
5584225413 feat: 优化订阅流量获取, 启用共享缓存(默认一分钟) 并优先尝试 HEAD 方法 2024-01-14 23:37:55 +08:00
xream
5cbcf4fce4 feat: Node.js 版本体支持定时任务, 环境变量 SUB_STORE_BACKEND_CRON 2024-01-14 18:45:31 +08:00
xream
89931c0032 chore: 文案调整 2024-01-14 15:46:56 +08:00
xream
88f3198320 fix: 找不到资源时报错; 调整脚本操作类型判断; 执行脚本失败时, 同时输出普通脚本和快捷脚本错误 2024-01-14 15:45:08 +08:00
xream
27a14bb255 revert: 回滚文件模板功能 2024-01-14 12:33:36 +08:00
xream
5ecce27f4e feat: 脚本内部 produceArtifact 支持指定 produceType: 'internal', produceOpts: { 'include-unsupported-proxy': true } 来获得内部的数据结构; 订阅链接参数支持 type=internal&includeUnsupportedProxy=true; 文件支持 nunjucks 模板, 为 sing-box 增加的 Filter 用法 sub/col 为订阅/组合订阅中的节点名 {{ '订阅的name' | sub('美国|🇺🇸|us', 'i') }}, subNode/colNode 为订阅/组合订阅中的节点 {{ '订阅的name' | subNode('美国|🇺🇸|us', 'i') }}, 底层 produceArtifact('subscription', 'sing-box', 'internal', '美国|🇺🇸|us', 'i') 2024-01-14 12:13:29 +08:00
xream
12903d77f7 fix: 修复无脚本操作时 文件数据结构错误 2024-01-13 22:01:02 +08:00
xream
0c6ec7f82a revert: 同步接口支持 POST 2024-01-13 20:40:45 +08:00
xream
ba251ced34 fix: 同步接口支持 POST 2024-01-13 19:56:30 +08:00
19 changed files with 650 additions and 329 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.151",
"version": "2.14.166",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -17,6 +17,7 @@
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2",

32
backend/pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ dependencies:
connect-history-api-fallback:
specifier: ^2.0.0
version: registry.npmmirror.com/connect-history-api-fallback@2.0.0
cron:
specifier: ^3.1.6
version: registry.npmmirror.com/cron@3.1.6
express:
specifier: ^4.17.1
version: registry.npmmirror.com/express@4.17.1
@@ -162,11 +165,6 @@ packages:
dev: true
optional: true
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: true
registry.npmmirror.com/@ampproject/remapping@2.2.0:
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz}
name: '@ampproject/remapping'
@@ -2056,6 +2054,12 @@ packages:
'@types/node': registry.npmmirror.com/@types/node@17.0.35
dev: true
registry.npmmirror.com/@types/luxon@3.3.8:
resolution: {integrity: sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/luxon/-/luxon-3.3.8.tgz}
name: '@types/luxon'
version: 3.3.8
dev: false
registry.npmmirror.com/@types/minimatch@3.0.5:
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz}
name: '@types/minimatch'
@@ -3696,6 +3700,15 @@ packages:
sha.js: registry.npmmirror.com/sha.js@2.4.11
dev: true
registry.npmmirror.com/cron@3.1.6:
resolution: {integrity: sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cron/-/cron-3.1.6.tgz}
name: cron
version: 3.1.6
dependencies:
'@types/luxon': registry.npmmirror.com/@types/luxon@3.3.8
luxon: registry.npmmirror.com/luxon@3.4.4
dev: false
registry.npmmirror.com/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz}
name: cross-spawn
@@ -6851,6 +6864,13 @@ packages:
dependencies:
yallist: registry.npmmirror.com/yallist@4.0.0
registry.npmmirror.com/luxon@3.4.4:
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/luxon/-/luxon-3.4.4.tgz}
name: luxon
version: 3.4.4
engines: {node: '>=12'}
dev: false
registry.npmmirror.com/magic-string@0.23.2:
resolution: {integrity: sha512-oIUZaAxbcxYIp4AyLafV6OVKoB3YouZs0UTCJ8mOKBHNyJgGDaMJ4TgA+VylJh6fx7EQCC52XkbURxxG9IoJXA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/magic-string/-/magic-string-0.23.2.tgz}
name: magic-string
@@ -9320,7 +9340,7 @@ packages:
dependencies:
acorn: registry.npmmirror.com/acorn@8.7.1
commander: registry.npmmirror.com/commander@2.20.3
source-map: 0.6.1
source-map: registry.npmmirror.com/source-map@0.6.1
source-map-support: registry.npmmirror.com/source-map-support@0.5.21
dev: true

View File

@@ -10,6 +10,8 @@ export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
export const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource';
export const CHR_EXPIRATION_TIME_KEY = '#sub-store-chr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 1 min
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour

View File

@@ -139,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
return proxies;
}
function produce(proxies, targetPlatform, type) {
function produce(proxies, targetPlatform, type, opts = {}) {
const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
@@ -154,10 +154,10 @@ function produce(proxies, targetPlatform, type) {
$.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
return proxies
const list = proxies
.map((proxy) => {
try {
let line = producer.produce(proxy, type);
let line = producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
@@ -179,10 +179,10 @@ function produce(proxies, targetPlatform, type) {
return '';
}
})
.filter((line) => line.length > 0)
.join('\n');
.filter((line) => line.length > 0);
return type === 'internal' ? list : list.join('\n');
} else if (producer.type === 'ALL') {
return producer.produce(proxies, type);
return producer.produce(proxies, type, opts);
}
}

View File

@@ -331,15 +331,27 @@ function URI_VLESS() {
};
const parse = (line) => {
line = line.split('vless://')[1];
let isShadowrocket;
let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
if (!parsed) {
// eslint-disable-next-line no-unused-vars
let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line);
line = `${Base64.decode(base64)}${other}`;
parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
isShadowrocket = true;
}
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, ___, addons = '', name] =
/^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
let [__, uuid, server, port, ___, addons = '', name] = parsed;
if (isShadowrocket) {
uuid = uuid.replace(/^.*?:/g, '');
}
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
@@ -355,9 +367,24 @@ function URI_VLESS() {
params[key] = value;
}
proxy.name = name ?? params.remarks ?? `VLESS ${server}:${port}`;
proxy.tls = params.security && params.security !== 'none';
proxy.sni = params.sni;
if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
proxy.tls = true;
params.security = params.security ?? 'reality';
}
proxy.sni = params.sni ?? params.peer;
proxy.flow = params.flow;
if (!proxy.flow && isShadowrocket && params.xtls) {
// "none" is undefined
const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][
params.xtls
];
if (flow) {
proxy.flow = flow;
}
}
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
@@ -371,21 +398,28 @@ function URI_VLESS() {
opts['short-id'] = params.sid;
}
if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs;
}
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {};
if (params.path) {
opts.path = params.path;
}
if (params.host) {
opts.headers = { Host: params.host };
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
} else if (isShadowrocket && params.path) {
opts[`${proxy.network}-service-name`] = params.path;
delete params.path;
}
if (params.path) {
opts.path = params.path;
}
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {

View File

@@ -10,7 +10,12 @@ import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import env from '@/utils/env';
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
import {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
flowTransfer,
} from '@/utils/flow';
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -319,7 +324,11 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
const operator = createDynamicFunction(
'operator',
`async function operator(input = []) {
if (Array.isArray(input)) {
if (input?.$files || input?.$content) {
let { $content, $files } = input
${script}
return { $content, $files }
} else {
let proxies = input
let list = []
for await (let $server of proxies) {
@@ -327,12 +336,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
list.push($server)
}
return list
} else {
let { $content, $files } = input
${script}
return { $content, $files }
}
}`,
$arguments,
);
@@ -681,7 +685,7 @@ async function ApplyFilter(filter, objs) {
selected = await filter.nodeFunc(objs);
} catch (err) {
$.error(
`Cannot apply filter ${filter.name}(node script)! Reason: ${err}`,
`Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
@@ -689,7 +693,7 @@ async function ApplyFilter(filter, objs) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行节点快捷过滤脚本 失败 ${nodeErr}`;
nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
}
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
}
@@ -708,7 +712,9 @@ async function ApplyOperator(operator, objs) {
if (
funcErrMsg.includes('$server is not defined') ||
funcErrMsg.includes('$content is not defined') ||
funcErrMsg.includes('$files is not defined')
funcErrMsg.includes('$files is not defined') ||
output?.$files ||
output?.$content
) {
funcErr = '';
} else {
@@ -722,7 +728,7 @@ async function ApplyOperator(operator, objs) {
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
`Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
@@ -730,7 +736,7 @@ async function ApplyOperator(operator, objs) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
}
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
}
@@ -780,7 +786,12 @@ function removeFlag(str) {
}
function createDynamicFunction(name, script, $arguments) {
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
const flowUtils = {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
flowTransfer,
};
if ($.env.isLoon) {
return new Function(
'$arguments',

View File

@@ -2,136 +2,139 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
const produce = (proxies, type, opts = {}) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies
// https://clash.wiki/configuration/outbound.html#shadowsocks
proxies = proxies.filter((proxy) => {
if (
![
'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}
return true;
});
return (
'proxies:\n' +
proxies
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
delete proxy.tls;
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -7,7 +7,7 @@ import Loon_Producer from './loon';
import URI_Producer from './uri';
import V2Ray_Producer from './v2ray';
import QX_Producer from './qx';
import ShadowRocket_Producer from './shadowrocket';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
@@ -19,6 +19,7 @@ function JSON_Producer() {
export default {
QX: QX_Producer(),
QuantumultX: QX_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
@@ -28,7 +29,8 @@ export default {
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(),
Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(),
Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(),
Surfboard: Surfboard_Producer(),
'sing-box': singbox_Producer(),
};

View File

@@ -2,162 +2,161 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ShadowRocket_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
const produce = (proxies, type, opts = {}) => {
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
return false;
proxy.cipher = 'auto';
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'hysteria2') {
if (
proxy['obfs-password'] &&
proxy.obfs == 'salamander'
) {
proxy.obfs = proxy['obfs-password'];
delete proxy['obfs-password'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'hysteria2') {
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
proxy.obfs = proxy['obfs-password'];
delete proxy['obfs-password'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => {
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('');
};
return { type, produce };
}

View File

@@ -582,7 +582,7 @@ const wireguardParser = (proxy = {}) => {
export default function singbox_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
const produce = (proxies, type, opts = {}) => {
const list = [];
ClashMeta_Producer()
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
@@ -610,9 +610,15 @@ export default function singbox_Producer() {
list.push(ssParser(proxy));
}
break;
// case 'ssr':
// list.push(ssrParser(proxy));
// break;
case 'ssr':
if (opts['include-unsupported-proxy']) {
list.push(ssrParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
break;
case 'vmess':
if (
!proxy.network ||

View File

@@ -20,7 +20,15 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteSub } = req.query;
let {
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
} = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
@@ -41,6 +49,14 @@ async function downloadSubscription(req, res) {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
@@ -55,6 +71,10 @@ async function downloadSubscription(req, res) {
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
if (sub.source !== 'local' || url) {
@@ -121,12 +141,22 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub } = req.query;
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
if (collection) {
try {
@@ -135,6 +165,10 @@ async function downloadCollection(req, res) {
name,
platform,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
// forward flow header from the first subscription in this collection

View File

@@ -2,6 +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 registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
@@ -41,6 +42,28 @@ export default function serve() {
$app.start();
if ($.env.isNode) {
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON');
if (backend_cron) {
$.info(`[CRON] ${backend_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_cron,
async function () {
try {
$.info(`[CRON] ${backend_cron} started`);
await syncArtifacts();
$.info(`[CRON] ${backend_cron} finished`);
} catch (e) {
$.error(
`[CRON] ${backend_cron} error: ${e.message ?? e}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const path = eval(`require("path")`);
const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL');

View File

@@ -59,7 +59,7 @@ async function previewFile(req, res) {
}
// parse proxies
const files = (Array.isArray(content) ? content : [content]).flat();
const filesContent = files
let filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
@@ -70,7 +70,7 @@ async function previewFile(req, res) {
{ $files: files, $content: filesContent },
file.process,
)
: filesContent;
: { $content: filesContent, $files: files };
// produce
success(res, {

View File

@@ -33,12 +33,15 @@ async function produceArtifact({
mergeSources,
ignoreFailedRemoteSub,
ignoreFailedRemoteFile,
produceType,
produceOpts = {},
}) {
platform = platform || 'JSON';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
@@ -154,11 +157,12 @@ async function produceArtifact({
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (!collection) throw new Error(`找不到组合订阅 ${name}`);
const subnames = collection.subscriptions;
const results = {};
const errors = {};
@@ -301,10 +305,11 @@ async function produceArtifact({
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
if (!rule) throw new Error(`找不到规则 ${name}`);
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
@@ -419,7 +424,7 @@ async function produceArtifact({
}
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
const filesContent = files
let filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
@@ -430,12 +435,13 @@ async function produceArtifact({
{ $files: files, $content: filesContent },
file.process,
)
: filesContent;
: { $content: filesContent, $files: files };
return processed?.$content ?? '';
}
}
async function syncAllArtifacts(_, res) {
async function syncArtifacts() {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
@@ -474,6 +480,15 @@ async function syncAllArtifacts(_, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!');
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
throw e;
}
}
async function syncAllArtifacts(_, res) {
$.info('开始同步所有远程配置...');
try {
await syncArtifacts();
success(res);
} catch (err) {
failed(
@@ -547,4 +562,4 @@ async function syncArtifact(req, res) {
}
}
export { produceArtifact };
export { produceArtifact, syncArtifacts };

View File

@@ -3,6 +3,8 @@ import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import { getFlowField } from '@/utils/flow';
import $ from '@/core/app';
const tasks = new Map();
@@ -71,7 +73,13 @@ export default async function download(url, ua, timeout) {
);
http.get(url)
.then((resp) => {
const body = resp.body;
const { body, headers } = resp;
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)
reject(new Error('远程资源内容为空!'));
else {

View File

@@ -1,30 +1,85 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export async function getFlowHeaders(url, ua, timeout) {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
export function getFlowField(headers) {
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
export async function getFlowHeaders(url, ua, timeout) {
let $arguments = {};
const rawArgs = url.split('#');
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);
}
}
}
const cached = headersResourceCache.get(url);
let flowInfo;
if (!$arguments?.noCache && cached) {
// $.info(`使用缓存的流量信息: ${url}`);
flowInfo = cached;
} else {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
try {
// $.info(`使用 HEAD 方法获取流量信息: ${url}`);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(`使用 GET 方法获取流量信息: ${url}`);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
flowInfo = getFlowField(headers);
}
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
}
}
return flowInfo;
}
export function parseFlowHeaders(flowHeaders) {
if (!flowHeaders) return;
// unit is KB

View File

@@ -0,0 +1,107 @@
import $ from '@/core/app';
import {
HEADERS_RESOURCE_CACHE_KEY,
CHR_EXPIRATION_TIME_KEY,
} from '@/constants';
class ResourceCache {
constructor() {
this.expires = getExpiredTime();
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), HEADERS_RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].data;
}
return null;
}
gettime(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].time;
}
return null;
}
set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
}
}
function getExpiredTime() {
// console.log($.read(CHR_EXPIRATION_TIME_KEY));
if (!$.read(CHR_EXPIRATION_TIME_KEY)) {
$.write('6e4', CHR_EXPIRATION_TIME_KEY); // 1分钟
}
let expiration = 6e4;
if ($.env.isLoon) {
const loont = {
// Loon 插件自义定
'1\u5206\u949f': 6e4,
'5\u5206\u949f': 3e5,
'10\u5206\u949f': 6e5,
'30\u5206\u949f': 18e5, // "30分钟"
'1\u5c0f\u65f6': 36e5,
'2\u5c0f\u65f6': 72e5,
'3\u5c0f\u65f6': 108e5,
'6\u5c0f\u65f6': 216e5,
'12\u5c0f\u65f6': 432e5,
'24\u5c0f\u65f6': 864e5,
'48\u5c0f\u65f6': 1728e5,
'72\u5c0f\u65f6': 2592e5, // "72小时"
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
};
let intimed = $.read(
'#\u54cd\u5e94\u5934\u7f13\u5b58\u6709\u6548\u671f',
); // Loon #响应头缓存有效期
// console.log(intimed);
if (intimed in loont) {
expiration = loont[intimed];
if (expiration === 'readcachets') {
expiration = intimed;
}
}
return expiration;
} else {
expiration = $.read(CHR_EXPIRATION_TIME_KEY);
return expiration;
}
}
export default new ResourceCache();

View File

@@ -20,7 +20,7 @@ export function getPlatformFromHeaders(headers) {
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'ShadowRocket';
return 'Shadowrocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else if (

View File

@@ -5,6 +5,7 @@
#!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
#!select = 响应头缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
[Rule]
DOMAIN,sub-store.vercel.app,PROXY