mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c943176a5 | ||
|
|
f4c4cdba67 | ||
|
|
ada03be05f | ||
|
|
5584225413 | ||
|
|
5cbcf4fce4 | ||
|
|
89931c0032 | ||
|
|
88f3198320 | ||
|
|
27a14bb255 | ||
|
|
5ecce27f4e | ||
|
|
12903d77f7 | ||
|
|
0c6ec7f82a | ||
|
|
ba251ced34 | ||
|
|
d96a0421f7 | ||
|
|
aff7ddf41e | ||
|
|
164ae9a7a8 | ||
|
|
3aacd26b79 | ||
|
|
5915416232 | ||
|
|
c059296224 | ||
|
|
9ae70eca09 | ||
|
|
d0acf49b83 | ||
|
|
c51f3511dd | ||
|
|
ee2fcc7ee3 | ||
|
|
95615d1877 | ||
|
|
962bcda9dd | ||
|
|
9bb4739d56 | ||
|
|
de1d40f41a |
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -37,7 +37,6 @@ jobs:
|
||||
- name: Bundle
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D estrella
|
||||
pnpm run bundle
|
||||
- id: tag
|
||||
name: Generate release tag
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.136",
|
||||
"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
32
backend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
import download from '@/utils/download';
|
||||
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
@@ -59,7 +60,6 @@ function parse(raw) {
|
||||
$.error(`Failed to parse line: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
return proxies;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ export const ProxyUtils = {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isIP,
|
||||
yaml: YAML,
|
||||
};
|
||||
|
||||
function tryParse(parser, line) {
|
||||
@@ -218,7 +219,7 @@ function lastParse(proxy) {
|
||||
proxy.port = parseInt(proxy.port, 10);
|
||||
}
|
||||
if (proxy.server) {
|
||||
proxy.server = proxy.server
|
||||
proxy.server = `${proxy.server}`
|
||||
.trim()
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\]$/, '');
|
||||
|
||||
@@ -331,12 +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);
|
||||
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
|
||||
if (name != null) {
|
||||
name = decodeURIComponent(name);
|
||||
}
|
||||
|
||||
const proxy = {
|
||||
type: 'vless',
|
||||
name,
|
||||
@@ -352,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);
|
||||
@@ -368,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)) {
|
||||
@@ -414,14 +451,17 @@ function URI_Hysteria2() {
|
||||
const parse = (line) => {
|
||||
line = line.split(/(hysteria2|hy2):\/\//)[2];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, password, server, ___, port, addons, name] =
|
||||
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
||||
let [__, password, server, ___, port, ____, addons = '', name] =
|
||||
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
port = 443;
|
||||
}
|
||||
password = decodeURIComponent(password);
|
||||
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
|
||||
if (name != null) {
|
||||
name = decodeURIComponent(name);
|
||||
}
|
||||
name = name ?? `Hysteria2 ${server}:${port}`;
|
||||
|
||||
const proxy = {
|
||||
type: 'hysteria2',
|
||||
@@ -742,6 +782,7 @@ function Loon_WireGuard() {
|
||||
let publicKey = peers.match(
|
||||
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||
const proxy = {
|
||||
type: 'wireguard',
|
||||
name,
|
||||
@@ -768,7 +809,7 @@ function Loon_WireGuard() {
|
||||
ipv6,
|
||||
'public-key': publicKey,
|
||||
'pre-shared-key': preSharedKey,
|
||||
allowed_ips: allowedIps,
|
||||
'allowed-ips': allowedIps,
|
||||
reserved,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -7,8 +7,15 @@ import lodash from 'lodash';
|
||||
import $ from '@/core/app';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
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:
|
||||
@@ -316,11 +323,20 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
await (async function () {
|
||||
const operator = createDynamicFunction(
|
||||
'operator',
|
||||
`async function operator(proxies = []) {
|
||||
return proxies.map(($server = {}) => {
|
||||
${script}
|
||||
return $server
|
||||
})
|
||||
`async function operator(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) {
|
||||
${script}
|
||||
list.push($server)
|
||||
}
|
||||
return list
|
||||
}
|
||||
}`,
|
||||
$arguments,
|
||||
);
|
||||
@@ -610,10 +626,16 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||
await (async function () {
|
||||
const filter = createDynamicFunction(
|
||||
'filter',
|
||||
`async function filter(proxies = []) {
|
||||
return proxies.filter(($server = {}) => {
|
||||
${script}
|
||||
})
|
||||
`async function filter(input = []) {
|
||||
let proxies = input
|
||||
let list = []
|
||||
const fn = async ($server) => {
|
||||
${script}
|
||||
}
|
||||
for await (let $server of proxies) {
|
||||
list.push(await fn($server))
|
||||
}
|
||||
return list
|
||||
}`,
|
||||
$arguments,
|
||||
);
|
||||
@@ -649,20 +671,21 @@ async function ApplyFilter(filter, objs) {
|
||||
try {
|
||||
selected = await filter.func(objs);
|
||||
} catch (err) {
|
||||
// print log and skip this filter
|
||||
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
||||
let funcErr = '';
|
||||
let funcErrMsg = `${err.message ?? err}`;
|
||||
if (funcErrMsg.includes('$server is not defined')) {
|
||||
funcErr = '';
|
||||
} else {
|
||||
$.error(
|
||||
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
|
||||
);
|
||||
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
|
||||
}
|
||||
try {
|
||||
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}`;
|
||||
@@ -670,7 +693,7 @@ async function ApplyFilter(filter, objs) {
|
||||
nodeErr = '';
|
||||
funcErr = `执行失败 ${funcErrMsg}`;
|
||||
} else {
|
||||
nodeErr = `执行节点快捷过滤脚本 失败 ${nodeErr}`;
|
||||
nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
|
||||
}
|
||||
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
|
||||
}
|
||||
@@ -684,14 +707,20 @@ async function ApplyOperator(operator, objs) {
|
||||
const output_ = await operator.func(output);
|
||||
if (output_) output = output_;
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
||||
);
|
||||
let funcErr = '';
|
||||
let funcErrMsg = `${err.message ?? err}`;
|
||||
if (funcErrMsg.includes('$server is not defined')) {
|
||||
if (
|
||||
funcErrMsg.includes('$server is not defined') ||
|
||||
funcErrMsg.includes('$content is not defined') ||
|
||||
funcErrMsg.includes('$files is not defined') ||
|
||||
output?.$files ||
|
||||
output?.$content
|
||||
) {
|
||||
funcErr = '';
|
||||
} else {
|
||||
$.error(
|
||||
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
||||
);
|
||||
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
||||
}
|
||||
try {
|
||||
@@ -699,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}`;
|
||||
@@ -707,7 +736,7 @@ async function ApplyOperator(operator, objs) {
|
||||
nodeErr = '';
|
||||
funcErr = `执行失败 ${funcErrMsg}`;
|
||||
} else {
|
||||
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
|
||||
nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
|
||||
}
|
||||
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
||||
}
|
||||
@@ -757,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',
|
||||
@@ -769,6 +803,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
'produceArtifact',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
@@ -783,6 +818,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
flowUtils,
|
||||
produceArtifact,
|
||||
);
|
||||
} else {
|
||||
return new Function(
|
||||
@@ -792,8 +828,17 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
'produceArtifact',
|
||||
|
||||
`${script}\n return ${name}`,
|
||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
|
||||
)(
|
||||
$arguments,
|
||||
$,
|
||||
lodash,
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
flowUtils,
|
||||
produceArtifact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function ClashMeta_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type) => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ 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';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
@@ -18,6 +19,7 @@ function JSON_Producer() {
|
||||
|
||||
export default {
|
||||
QX: QX_Producer(),
|
||||
QuantumultX: QX_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
SurgeMac: SurgeMac_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
@@ -27,6 +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(),
|
||||
};
|
||||
|
||||
@@ -285,7 +285,8 @@ function wireguard(proxy) {
|
||||
proxy.ipv6 = proxy.peers[0].ipv6;
|
||||
proxy['public-key'] = proxy.peers[0]['public-key'];
|
||||
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
||||
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
|
||||
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
|
||||
proxy.reserved = proxy.peers[0].reserved;
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
688
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
688
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
@@ -0,0 +1,688 @@
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const tfoParser = (proxy, parsedProxy) => {
|
||||
parsedProxy.tcp_fast_open = false;
|
||||
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
|
||||
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
|
||||
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
|
||||
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
|
||||
};
|
||||
|
||||
const smuxParser = (smux, proxy) => {
|
||||
if (!smux || !smux.enabled) return;
|
||||
proxy.multiplex = { enabled: true };
|
||||
proxy.multiplex.protocol = smux.protocol;
|
||||
if (smux['max-connections'])
|
||||
proxy.multiplex.max_connections = parseInt(
|
||||
`${smux['max-connections']}`,
|
||||
10,
|
||||
);
|
||||
if (smux['max-streams'])
|
||||
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
|
||||
if (smux['min-streams'])
|
||||
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
|
||||
if (smux.padding) proxy.multiplex.padding = true;
|
||||
};
|
||||
|
||||
const wsParser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'ws', headers: {} };
|
||||
if (proxy['ws-opts']) {
|
||||
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
|
||||
if (wsPath !== '') transport.path = `${wsPath}`;
|
||||
if (Object.keys(wsHeaders).length > 0) {
|
||||
const headers = {};
|
||||
for (const key of Object.keys(wsHeaders)) {
|
||||
let value = wsHeaders[key];
|
||||
if (value === '') continue;
|
||||
if (!Array.isArray(value)) value = [`${value}`];
|
||||
if (value.length > 0) headers[key] = value;
|
||||
}
|
||||
const { Host: wsHost } = headers;
|
||||
if (wsHost.length === 1)
|
||||
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||
const [key, value] = item.split(':');
|
||||
if (value.trim() === '') continue;
|
||||
headers[key.trim()] = value.trim().split(',');
|
||||
}
|
||||
transport.headers = headers;
|
||||
}
|
||||
}
|
||||
if (proxy['ws-headers']) {
|
||||
const headers = {};
|
||||
for (const key of Object.keys(proxy['ws-headers'])) {
|
||||
let value = proxy['ws-headers'][key];
|
||||
if (value === '') continue;
|
||||
if (!Array.isArray(value)) value = [`${value}`];
|
||||
if (value.length > 0) headers[key] = value;
|
||||
}
|
||||
const { Host: wsHost } = headers;
|
||||
if (wsHost.length === 1)
|
||||
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||
const [key, value] = item.split(':');
|
||||
if (value.trim() === '') continue;
|
||||
headers[key.trim()] = value.trim().split(',');
|
||||
}
|
||||
for (const key of Object.keys(headers))
|
||||
transport.headers[key] = headers[key];
|
||||
}
|
||||
if (proxy['ws-path'] && proxy['ws-path'] !== '')
|
||||
transport.path = `${proxy['ws-path']}`;
|
||||
if (transport.path) {
|
||||
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, path = '', ed = ''] = reg.exec(transport.path);
|
||||
transport.path = path;
|
||||
if (ed !== '') {
|
||||
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
|
||||
transport.max_early_data = parseInt(ed, 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedProxy.tls.insecure)
|
||||
parsedProxy.tls.server_name = transport.headers.Host[0];
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
|
||||
transport.type = 'httpupgrade';
|
||||
if (transport.headers.Host) {
|
||||
transport.host = transport.headers.Host[0];
|
||||
delete transport.headers.Host;
|
||||
}
|
||||
if (transport.max_early_data) delete transport.max_early_data;
|
||||
if (transport.early_data_header_name)
|
||||
delete transport.early_data_header_name;
|
||||
}
|
||||
for (const key of Object.keys(transport.headers)) {
|
||||
const value = transport.headers[key];
|
||||
if (value.length === 1) transport.headers[key] = value[0];
|
||||
}
|
||||
parsedProxy.transport = transport;
|
||||
};
|
||||
|
||||
const h1Parser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'http', headers: {} };
|
||||
if (proxy['http-opts']) {
|
||||
const {
|
||||
method = '',
|
||||
path: h1Path = '',
|
||||
headers: h1Headers = {},
|
||||
} = proxy['http-opts'];
|
||||
if (method !== '') transport.method = method;
|
||||
if (Array.isArray(h1Path)) {
|
||||
transport.path = `${h1Path[0]}`;
|
||||
} else if (h1Path !== '') transport.path = `${h1Path}`;
|
||||
for (const key of Object.keys(h1Headers)) {
|
||||
let value = h1Headers[key];
|
||||
if (value === '') continue;
|
||||
if (key.toLowerCase() === 'host') {
|
||||
let host = value;
|
||||
if (!Array.isArray(host))
|
||||
host = `${host}`.split(',').map((i) => i.trim());
|
||||
if (host.length > 0) transport.host = host;
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(value))
|
||||
value = `${value}`.split(',').map((i) => i.trim());
|
||||
if (value.length > 0) transport.headers[key] = value;
|
||||
}
|
||||
}
|
||||
if (proxy['http-host'] && proxy['http-host'] !== '') {
|
||||
let host = proxy['http-host'];
|
||||
if (!Array.isArray(host))
|
||||
host = `${host}`.split(',').map((i) => i.trim());
|
||||
if (host.length > 0) transport.host = host;
|
||||
}
|
||||
if (!transport.host) return;
|
||||
if (proxy['http-path'] && proxy['http-path'] !== '') {
|
||||
const path = proxy['http-path'];
|
||||
if (Array.isArray(path)) {
|
||||
transport.path = `${path[0]}`;
|
||||
} else if (path !== '') transport.path = `${path}`;
|
||||
}
|
||||
if (parsedProxy.tls.insecure)
|
||||
parsedProxy.tls.server_name = transport.host[0];
|
||||
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||
for (const key of Object.keys(transport.headers)) {
|
||||
const value = transport.headers[key];
|
||||
if (value.length === 1) transport.headers[key] = value[0];
|
||||
}
|
||||
parsedProxy.transport = transport;
|
||||
};
|
||||
|
||||
const h2Parser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'http' };
|
||||
if (proxy['h2-opts']) {
|
||||
let { host = '', path = '' } = proxy['h2-opts'];
|
||||
if (path !== '') transport.path = `${path}`;
|
||||
if (host !== '') {
|
||||
if (!Array.isArray(host))
|
||||
host = `${host}`.split(',').map((i) => i.trim());
|
||||
if (host.length > 0) transport.host = host;
|
||||
}
|
||||
}
|
||||
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
|
||||
let host = proxy['h2-host'];
|
||||
if (!Array.isArray(host))
|
||||
host = `${host}`.split(',').map((i) => i.trim());
|
||||
if (host.length > 0) transport.host = host;
|
||||
}
|
||||
if (proxy['h2-path'] && proxy['h2-path'] !== '')
|
||||
transport.path = `${proxy['h2-path']}`;
|
||||
parsedProxy.tls.enabled = true;
|
||||
if (parsedProxy.tls.insecure)
|
||||
parsedProxy.tls.server_name = transport.host[0];
|
||||
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||
parsedProxy.transport = transport;
|
||||
};
|
||||
|
||||
const grpcParser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'grpc' };
|
||||
if (proxy['grpc-opts']) {
|
||||
const serviceName = proxy['grpc-opts']['grpc-service-name'];
|
||||
if (serviceName && serviceName !== '')
|
||||
transport.service_name = serviceName;
|
||||
}
|
||||
parsedProxy.transport = transport;
|
||||
};
|
||||
|
||||
const tlsParser = (proxy, parsedProxy) => {
|
||||
if (proxy.tls) parsedProxy.tls.enabled = true;
|
||||
if (proxy.servername && proxy.servername !== '')
|
||||
parsedProxy.tls.server_name = proxy.servername;
|
||||
if (proxy.peer && proxy.peer !== '')
|
||||
parsedProxy.tls.server_name = proxy.peer;
|
||||
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
|
||||
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
|
||||
if (proxy.insecure) parsedProxy.tls.insecure = true;
|
||||
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
|
||||
if (typeof proxy.alpn === 'string') {
|
||||
parsedProxy.tls.alpn = [proxy.alpn];
|
||||
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
|
||||
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
|
||||
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
|
||||
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
|
||||
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||
parsedProxy.tls.utls = {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
};
|
||||
if (proxy['reality-opts']) {
|
||||
parsedProxy.tls.reality = { enabled: true };
|
||||
if (proxy['reality-opts']['public-key'])
|
||||
parsedProxy.tls.reality.public_key =
|
||||
proxy['reality-opts']['public-key'];
|
||||
if (proxy['reality-opts']['short-id'])
|
||||
parsedProxy.tls.reality.short_id =
|
||||
proxy['reality-opts']['short-id'];
|
||||
}
|
||||
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
|
||||
};
|
||||
|
||||
const httpParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'http',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.username = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
if (proxy.headers) {
|
||||
parsedProxy.headers = {};
|
||||
for (const k of Object.keys(proxy.headers)) {
|
||||
parsedProxy.headers[k] = `${proxy.headers[k]}`;
|
||||
}
|
||||
if (Object.keys(parsedProxy.headers).length === 0)
|
||||
delete parsedProxy.headers;
|
||||
}
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const socks5Parser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'socks',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
password: proxy.password,
|
||||
version: '5',
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.username) parsedProxy.username = proxy.username;
|
||||
if (proxy.password) parsedProxy.password = proxy.password;
|
||||
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const ssParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'shadowsocks',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
if (proxy.plugin) {
|
||||
const optArr = [];
|
||||
if (proxy.plugin === 'obfs') {
|
||||
parsedProxy.plugin = 'obfs-local';
|
||||
parsedProxy.plugin_opts = '';
|
||||
if (proxy['obfs-host'])
|
||||
proxy['plugin-opts'].host = proxy['obfs-host'];
|
||||
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||
switch (k) {
|
||||
case 'mode':
|
||||
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
|
||||
break;
|
||||
case 'host':
|
||||
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
|
||||
break;
|
||||
default:
|
||||
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (proxy.plugin === 'v2ray-plugin') {
|
||||
parsedProxy.plugin = 'v2ray-plugin';
|
||||
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
|
||||
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
|
||||
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||
switch (k) {
|
||||
case 'tls':
|
||||
if (proxy['plugin-opts'].tls) optArr.push('tls');
|
||||
break;
|
||||
case 'host':
|
||||
optArr.push(`host=${proxy['plugin-opts'].host}`);
|
||||
break;
|
||||
case 'path':
|
||||
optArr.push(`path=${proxy['plugin-opts'].path}`);
|
||||
break;
|
||||
case 'headers':
|
||||
optArr.push(
|
||||
`headers=${JSON.stringify(
|
||||
proxy['plugin-opts'].headers,
|
||||
)}`,
|
||||
);
|
||||
break;
|
||||
case 'mux':
|
||||
if (proxy['plugin-opts'].mux)
|
||||
parsedProxy.multiplex = { enabled: true };
|
||||
break;
|
||||
default:
|
||||
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
parsedProxy.plugin_opts = optArr.join(';');
|
||||
}
|
||||
|
||||
return parsedProxy;
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const ssrParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'shadowsocksr',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
obfs: proxy.obfs,
|
||||
protocol: proxy.protocol,
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
|
||||
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
|
||||
parsedProxy.protocol_param = proxy['protocol-param'];
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const vmessParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'vmess',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
uuid: proxy.uuid,
|
||||
security: proxy.cipher,
|
||||
alter_id: parseInt(`${proxy.alterId}`, 10),
|
||||
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (
|
||||
[
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'aes-128-ctr',
|
||||
].indexOf(parsedProxy.security) === -1
|
||||
)
|
||||
parsedProxy.security = 'auto';
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
|
||||
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
|
||||
tfoParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const vlessParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'vless',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
uuid: proxy.uuid,
|
||||
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const trojanParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'trojan',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
password: proxy.password,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||
|
||||
tfoParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const hysteriaParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'hysteria',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
disable_mtu_discovery: false,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
|
||||
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
|
||||
if (reg.test(`${proxy.up}`)) {
|
||||
parsedProxy.up = `${proxy.up}`;
|
||||
} else {
|
||||
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||
}
|
||||
if (reg.test(`${proxy.down}`)) {
|
||||
parsedProxy.down = `${proxy.down}`;
|
||||
} else {
|
||||
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||
}
|
||||
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
|
||||
if (proxy.recv_window_conn)
|
||||
parsedProxy.recv_window_conn = proxy.recv_window_conn;
|
||||
if (proxy['recv-window-conn'])
|
||||
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
|
||||
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
|
||||
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
|
||||
if (proxy.disable_mtu_discovery) {
|
||||
if (typeof proxy.disable_mtu_discovery === 'boolean') {
|
||||
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
|
||||
} else {
|
||||
if (proxy.disable_mtu_discovery === 1)
|
||||
parsedProxy.disable_mtu_discovery = true;
|
||||
}
|
||||
}
|
||||
tlsParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const hysteria2Parser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'hysteria2',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
password: proxy.password,
|
||||
obfs: {},
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
|
||||
if (proxy['obfs-password'])
|
||||
parsedProxy.obfs.password = proxy['obfs-password'];
|
||||
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
|
||||
tlsParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const tuic5Parser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'tuic',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
uuid: proxy.uuid,
|
||||
password: proxy.password,
|
||||
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (
|
||||
proxy['congestion-controller'] &&
|
||||
proxy['congestion-controller'] !== 'cubic'
|
||||
)
|
||||
parsedProxy.congestion_control = proxy['congestion-controller'];
|
||||
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
|
||||
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
|
||||
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
|
||||
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
|
||||
if (proxy['heartbeat-interval'])
|
||||
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
|
||||
tfoParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const wireguardParser = (proxy = {}) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'wireguard',
|
||||
server: proxy.server,
|
||||
server_port: parseInt(`${proxy.port}`, 10),
|
||||
local_address: [proxy.ip, proxy.ipv6],
|
||||
private_key: proxy['private-key'],
|
||||
peer_public_key: proxy['public-key'],
|
||||
pre_shared_key: proxy['pre-shared-key'],
|
||||
reserved: [],
|
||||
};
|
||||
if (parsedProxy.server_port < 1 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (typeof proxy.reserved === 'string') {
|
||||
parsedProxy.reserved.push(proxy.reserved);
|
||||
} else {
|
||||
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
|
||||
}
|
||||
if (proxy.peers && proxy.peers.length > 0) {
|
||||
parsedProxy.peers = [];
|
||||
for (const p of proxy.peers) {
|
||||
const peer = {
|
||||
server: p.server,
|
||||
server_port: parseInt(`${p.port}`, 10),
|
||||
public_key: p['public-key'],
|
||||
allowed_ips: p.allowed_ips,
|
||||
reserved: [],
|
||||
};
|
||||
if (typeof p.reserved === 'string') {
|
||||
peer.reserved.push(p.reserved);
|
||||
} else {
|
||||
for (const r of p.reserved) peer.reserved.push(r);
|
||||
}
|
||||
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
|
||||
parsedProxy.peers.push(peer);
|
||||
}
|
||||
}
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
export default function singbox_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
const list = [];
|
||||
ClashMeta_Producer()
|
||||
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
|
||||
.map((proxy) => {
|
||||
try {
|
||||
switch (proxy.type) {
|
||||
case 'http':
|
||||
list.push(httpParser(proxy));
|
||||
break;
|
||||
case 'socks5':
|
||||
if (proxy.tls) {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
|
||||
);
|
||||
} else {
|
||||
list.push(socks5Parser(proxy));
|
||||
}
|
||||
break;
|
||||
case 'ss':
|
||||
if (proxy.plugin === 'shadow-tls') {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`,
|
||||
);
|
||||
} else {
|
||||
list.push(ssParser(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 ||
|
||||
['ws', 'grpc', 'h2', 'http'].includes(
|
||||
proxy.network,
|
||||
)
|
||||
) {
|
||||
list.push(vmessParser(proxy));
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'vless':
|
||||
if (
|
||||
!proxy.flow ||
|
||||
['xtls-rprx-vision'].includes(proxy.flow)
|
||||
) {
|
||||
list.push(vlessParser(proxy));
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'trojan':
|
||||
if (!proxy.flow) {
|
||||
list.push(trojanParser(proxy));
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'hysteria':
|
||||
list.push(hysteriaParser(proxy));
|
||||
break;
|
||||
case 'hysteria2':
|
||||
list.push(hysteria2Parser(proxy));
|
||||
break;
|
||||
case 'tuic':
|
||||
if (!proxy.token || proxy.token.length === 0) {
|
||||
list.push(tuic5Parser(proxy));
|
||||
} else {
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: TUIC v4`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'wireguard':
|
||||
list.push(wireguardParser(proxy));
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log(e);
|
||||
$.error(e.message ?? e);
|
||||
}
|
||||
});
|
||||
return type === 'internal' ? list : JSON.stringify(list, null, 2);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -416,6 +416,9 @@ function snell(proxy) {
|
||||
'obfs-opts.path',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,12 @@ import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { FILES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import {
|
||||
RequestInvalidError,
|
||||
ResourceNotFoundError,
|
||||
InternalServerError,
|
||||
} from '@/restful/errors';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||
@@ -12,7 +17,10 @@ export default function register($app) {
|
||||
.patch(updateFile)
|
||||
.delete(deleteFile);
|
||||
|
||||
$app.route('/api/wholeFile/:name').get(getWholeFile);
|
||||
|
||||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||
$app.route('/api/wholeFiles').get(getAllWholeFiles);
|
||||
}
|
||||
|
||||
// file API
|
||||
@@ -37,13 +45,85 @@ function createFile(req, res) {
|
||||
success(res, file, 201);
|
||||
}
|
||||
|
||||
function getFile(req, res) {
|
||||
async function getFile(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
$.info(`正在下载文件:${name}`);
|
||||
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程文件 URL: ${url}`);
|
||||
}
|
||||
if (ua) {
|
||||
ua = decodeURIComponent(ua);
|
||||
$.info(`指定远程文件 User-Agent: ${ua}`);
|
||||
}
|
||||
if (content) {
|
||||
content = decodeURIComponent(content);
|
||||
$.info(`指定本地文件: ${content}`);
|
||||
}
|
||||
if (mergeSources) {
|
||||
mergeSources = decodeURIComponent(mergeSources);
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
}
|
||||
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
|
||||
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
|
||||
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||||
}
|
||||
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (file) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'file',
|
||||
name,
|
||||
url,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteFile,
|
||||
});
|
||||
|
||||
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||
output ?? '',
|
||||
);
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载文件失败`,
|
||||
`❌ 无法下载文件:${name}!`,
|
||||
`🤔 原因:${err.message ?? err}`,
|
||||
);
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download file: ${name}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`File ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
function getWholeFile(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (file) {
|
||||
res.status(200).json(file.content);
|
||||
success(res, file);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
@@ -102,6 +182,11 @@ function getAllFiles(req, res) {
|
||||
);
|
||||
}
|
||||
|
||||
function getAllWholeFiles(req, res) {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
success(res, allFiles);
|
||||
}
|
||||
|
||||
function replaceFile(req, res) {
|
||||
const allFiles = req.body;
|
||||
$.write(allFiles, FILES_KEY);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -47,7 +47,9 @@ function getModule(req, res) {
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
const module = findByName(allModules, name);
|
||||
if (module) {
|
||||
res.status(200).json(module.content);
|
||||
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||
module.content,
|
||||
);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
|
||||
@@ -22,10 +22,10 @@ async function getNodeInfo(req, res) {
|
||||
const info = await $http
|
||||
.get({
|
||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||
proxy.server
|
||||
`${proxy.server}`
|
||||
.trim()
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\]$/, '')
|
||||
.replace(/\]$/, ''),
|
||||
)}?lang=${lang}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
|
||||
@@ -9,6 +9,85 @@ import $ from '@/core/app';
|
||||
export default function register($app) {
|
||||
$app.post('/api/preview/sub', compareSub);
|
||||
$app.post('/api/preview/collection', compareCollection);
|
||||
$app.post('/api/preview/file', previewFile);
|
||||
}
|
||||
|
||||
async function previewFile(req, res) {
|
||||
try {
|
||||
const file = req.body;
|
||||
let content;
|
||||
if (
|
||||
file.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||
) {
|
||||
content = file.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
content = await Promise.all(
|
||||
file.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, file.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
!file.ignoreFailedRemoteFile &&
|
||||
Object.keys(errors).length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (file.mergeSources === 'localFirst') {
|
||||
content.unshift(file.content);
|
||||
} else if (file.mergeSources === 'remoteFirst') {
|
||||
content.push(file.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
const files = (Array.isArray(content) ? content : [content]).flat();
|
||||
let filesContent = files
|
||||
.filter((i) => i != null && i !== '')
|
||||
.join('\n');
|
||||
|
||||
// apply processors
|
||||
const processed =
|
||||
Array.isArray(file.process) && file.process.length > 0
|
||||
? await ProxyUtils.process(
|
||||
{ $files: files, $content: filesContent },
|
||||
file.process,
|
||||
)
|
||||
: { $content: filesContent, $files: files };
|
||||
|
||||
// produce
|
||||
success(res, {
|
||||
original: filesContent,
|
||||
processed: processed?.$content ?? '',
|
||||
});
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`INTERNAL_SERVER_ERROR`,
|
||||
`Failed to preview file`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function compareSub(req, res) {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||
import {
|
||||
ARTIFACTS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
SUBS_KEY,
|
||||
FILES_KEY,
|
||||
} from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { success } from '@/restful/response';
|
||||
|
||||
@@ -6,6 +11,7 @@ export default function register($app) {
|
||||
$app.post('/api/sort/subs', sortSubs);
|
||||
$app.post('/api/sort/collections', sortCollections);
|
||||
$app.post('/api/sort/artifacts', sortArtifacts);
|
||||
$app.post('/api/sort/files', sortFiles);
|
||||
}
|
||||
|
||||
function sortSubs(req, res) {
|
||||
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
function sortFiles(req, res) {
|
||||
const orders = req.body;
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res, allFiles);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
COLLECTIONS_KEY,
|
||||
RULES_KEY,
|
||||
SUBS_KEY,
|
||||
FILES_KEY,
|
||||
} from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||
@@ -31,12 +32,16 @@ async function produceArtifact({
|
||||
content,
|
||||
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;
|
||||
@@ -152,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 = {};
|
||||
@@ -299,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];
|
||||
@@ -327,10 +334,114 @@ async function produceArtifact({
|
||||
]);
|
||||
// produce output
|
||||
return RuleUtils.produce(rules, platform);
|
||||
} else if (type === 'file') {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (!file) throw new Error(`找不到文件 ${name}`);
|
||||
let raw;
|
||||
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||
raw = content;
|
||||
} else if (url) {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, ua || file.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||
if (
|
||||
ignoreFailedRemoteFile != null &&
|
||||
ignoreFailedRemoteFile !== ''
|
||||
) {
|
||||
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||
}
|
||||
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (mergeSources === 'localFirst') {
|
||||
raw.unshift(content);
|
||||
} else if (mergeSources === 'remoteFirst') {
|
||||
raw.push(content);
|
||||
}
|
||||
} else if (
|
||||
file.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||
) {
|
||||
raw = file.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
file.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, ua || file.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||
if (
|
||||
ignoreFailedRemoteFile != null &&
|
||||
ignoreFailedRemoteFile !== ''
|
||||
) {
|
||||
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||
}
|
||||
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (file.mergeSources === 'localFirst') {
|
||||
raw.unshift(file.content);
|
||||
} else if (file.mergeSources === 'remoteFirst') {
|
||||
raw.push(file.content);
|
||||
}
|
||||
}
|
||||
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
||||
let filesContent = files
|
||||
.filter((i) => i != null && i !== '')
|
||||
.join('\n');
|
||||
|
||||
// apply processors
|
||||
const processed =
|
||||
Array.isArray(file.process) && file.process.length > 0
|
||||
? await ProxyUtils.process(
|
||||
{ $files: files, $content: filesContent },
|
||||
file.process,
|
||||
)
|
||||
: { $content: filesContent, $files: files };
|
||||
|
||||
return processed?.$content ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
async function syncAllArtifacts(_, res) {
|
||||
async function syncArtifacts() {
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
@@ -369,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(
|
||||
@@ -386,10 +506,12 @@ async function syncAllArtifacts(_, res) {
|
||||
async function syncArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
$.info(`开始同步远程配置 ${name}...`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const artifact = findByName(allArtifacts, name);
|
||||
|
||||
if (!artifact) {
|
||||
$.error(`找不到远程配置 ${name}`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
@@ -428,6 +550,7 @@ async function syncArtifact(req, res) {
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, artifact);
|
||||
} catch (err) {
|
||||
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
@@ -439,4 +562,4 @@ async function syncArtifact(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
export { produceArtifact };
|
||||
export { produceArtifact, syncArtifacts };
|
||||
|
||||
@@ -3,11 +3,13 @@ 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();
|
||||
|
||||
export default async function download(url, ua) {
|
||||
export default async function download(url, ua, timeout) {
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
@@ -45,17 +47,19 @@ export default async function download(url, ua) {
|
||||
}
|
||||
|
||||
const { isNode } = ENV();
|
||||
const { defaultUserAgent } = $.read(SETTINGS_KEY);
|
||||
ua = ua || defaultUserAgent || 'clash.meta';
|
||||
const id = hex_md5(ua + url);
|
||||
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
|
||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||
const requestTimeout = timeout || defaultTimeout;
|
||||
const id = hex_md5(userAgent + url);
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
|
||||
const http = HTTP({
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
@@ -64,10 +68,18 @@ export default async function download(url, ua) {
|
||||
if (!$arguments?.noCache && cached) {
|
||||
resolve(cached);
|
||||
} else {
|
||||
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
|
||||
);
|
||||
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 {
|
||||
|
||||
@@ -1,21 +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) {
|
||||
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': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
||||
},
|
||||
});
|
||||
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
|
||||
|
||||
107
backend/src/utils/headers-resource-cache.js
Normal file
107
backend/src/utils/headers-resource-cache.js
Normal 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();
|
||||
@@ -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 (
|
||||
@@ -32,6 +32,8 @@ export function getPlatformFromHeaders(headers) {
|
||||
return 'Clash';
|
||||
} else if (ua.indexOf('v2ray') !== -1) {
|
||||
return 'V2Ray';
|
||||
} else if (ua.indexOf('sing-box') !== -1) {
|
||||
return 'sing-box';
|
||||
} else {
|
||||
return 'JSON';
|
||||
}
|
||||
|
||||
26
backend/src/vendor/open-api.js
vendored
26
backend/src/vendor/open-api.js
vendored
@@ -191,6 +191,32 @@ export class OpenAPI {
|
||||
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
||||
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
||||
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
||||
|
||||
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
|
||||
if (push) {
|
||||
const url = push
|
||||
.replace(
|
||||
'[推送标题]',
|
||||
encodeURIComponent(title || 'Sub-Store'),
|
||||
)
|
||||
.replace(
|
||||
'[推送内容]',
|
||||
encodeURIComponent(
|
||||
[subtitle, content_].map((i) => i).join('\n'),
|
||||
),
|
||||
);
|
||||
const $http = HTTP();
|
||||
$http
|
||||
.get({ url })
|
||||
.then((resp) => {
|
||||
console.log(
|
||||
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user