mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b7557635 | ||
|
|
14ca62db4a | ||
|
|
a2a754adb7 | ||
|
|
6b23f82953 | ||
|
|
e071a7f253 | ||
|
|
b9bba895e1 | ||
|
|
8090d678ee | ||
|
|
ff4be7ac38 | ||
|
|
7e2109dc68 | ||
|
|
278beae99a | ||
|
|
3aedd5943d | ||
|
|
222551eb20 | ||
|
|
0d5e1ab38b | ||
|
|
a3ec98caa9 | ||
|
|
d9e4d814bb | ||
|
|
e843aa3702 | ||
|
|
66464645f2 | ||
|
|
9ccd6b3816 | ||
|
|
74be1e3d82 | ||
|
|
6d78eb7356 | ||
|
|
38eccca8b4 | ||
|
|
33e5aeceb5 | ||
|
|
837667edc9 | ||
|
|
0069b0ce83 | ||
|
|
fcc9d047ae |
@@ -39,7 +39,7 @@ Core functionalities:
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
|
||||
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
|
||||
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
|
||||
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
|
||||
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru)
|
||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.444",
|
||||
"version": "2.16.2",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -929,6 +929,8 @@ function Clash_All() {
|
||||
const proxy = JSON.parse(line);
|
||||
if (
|
||||
![
|
||||
'mieru',
|
||||
'juicity',
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
|
||||
@@ -8,6 +8,8 @@ export default function ClashMeta_Producer() {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||
return false;
|
||||
} else if (['juicity'].includes(proxy.type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
@@ -72,6 +72,15 @@ export default function Egern_Producer() {
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
proxy.sni = proxy.server;
|
||||
}
|
||||
const prev_hop =
|
||||
proxy.prev_hop ||
|
||||
proxy['underlying-proxy'] ||
|
||||
proxy['dialer-proxy'] ||
|
||||
proxy.detour;
|
||||
|
||||
if (proxy.type === 'http') {
|
||||
proxy = {
|
||||
type: 'http',
|
||||
@@ -130,6 +139,8 @@ export default function Egern_Producer() {
|
||||
next_hop: proxy.next_hop,
|
||||
sni: proxy.sni,
|
||||
skip_tls_verify: proxy['skip-cert-verify'],
|
||||
port_hopping: proxy.ports,
|
||||
port_hopping_interval: proxy['hop-interval'],
|
||||
};
|
||||
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
|
||||
proxy.obfs = 'salamander';
|
||||
@@ -284,6 +295,7 @@ export default function Egern_Producer() {
|
||||
[proxy.type]: {
|
||||
...proxy,
|
||||
type: undefined,
|
||||
prev_hop,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function Loon_Producer() {
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocks(proxy, includeUnsupportedProxy) {
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
if (
|
||||
![
|
||||
@@ -56,9 +56,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
...(includeUnsupportedProxy
|
||||
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm']
|
||||
: []),
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
|
||||
@@ -8,6 +8,8 @@ export default function ShadowRocket_Producer() {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||
return false;
|
||||
} else if (['mieru'].includes(proxy.type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
@@ -3,7 +3,11 @@ import $ from '@/core/app';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
|
||||
const detourParser = (proxy, parsedProxy) => {
|
||||
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
|
||||
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
|
||||
};
|
||||
const networkParser = (proxy, parsedProxy) => {
|
||||
if (['tcp', 'udp'].includes(proxy._network))
|
||||
parsedProxy.network = proxy._network;
|
||||
};
|
||||
const tfoParser = (proxy, parsedProxy) => {
|
||||
parsedProxy.tcp_fast_open = false;
|
||||
@@ -214,7 +218,11 @@ const tlsParser = (proxy, parsedProxy) => {
|
||||
proxy['reality-opts']['short-id'];
|
||||
parsedProxy.tls.utls = { enabled: true };
|
||||
}
|
||||
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||
if (
|
||||
!['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
|
||||
proxy['client-fingerprint'] &&
|
||||
proxy['client-fingerprint'] !== ''
|
||||
)
|
||||
parsedProxy.tls.utls = {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
@@ -301,6 +309,7 @@ const socks5Parser = (proxy = {}) => {
|
||||
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;
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
@@ -352,6 +361,7 @@ const ssParser = (proxy = {}) => {
|
||||
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;
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
@@ -466,7 +476,7 @@ const vmessParser = (proxy = {}) => {
|
||||
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
|
||||
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
@@ -489,7 +499,7 @@ const vlessParser = (proxy = {}) => {
|
||||
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
@@ -510,7 +520,7 @@ const trojanParser = (proxy = {}) => {
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
@@ -558,13 +568,14 @@ const hysteriaParser = (proxy = {}) => {
|
||||
parsedProxy.disable_mtu_discovery = true;
|
||||
}
|
||||
}
|
||||
networkParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const hysteria2Parser = (proxy = {}) => {
|
||||
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
|
||||
const parsedProxy = {
|
||||
tag: proxy.name,
|
||||
type: 'hysteria2',
|
||||
@@ -576,12 +587,23 @@ const hysteria2Parser = (proxy = {}) => {
|
||||
};
|
||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||
throw 'invalid port';
|
||||
if (includeUnsupportedProxy) {
|
||||
if (proxy['hop-interval'])
|
||||
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
|
||||
? `${proxy['hop-interval']}s`
|
||||
: proxy['hop-interval'];
|
||||
if (proxy['ports'])
|
||||
parsedProxy.server_ports = proxy['ports']
|
||||
.split(/\s*,\s*/)
|
||||
.map((p) => p.replace(/\s*-\s*/g, ':'));
|
||||
}
|
||||
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;
|
||||
networkParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
@@ -612,6 +634,7 @@ const tuic5Parser = (proxy = {}) => {
|
||||
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
|
||||
if (proxy['heartbeat-interval'])
|
||||
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
@@ -669,6 +692,7 @@ const wireguardParser = (proxy = {}) => {
|
||||
parsedProxy.peers.push(peer);
|
||||
}
|
||||
}
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
@@ -786,7 +810,12 @@ export default function singbox_Producer() {
|
||||
list.push(hysteriaParser(proxy));
|
||||
break;
|
||||
case 'hysteria2':
|
||||
list.push(hysteria2Parser(proxy));
|
||||
list.push(
|
||||
hysteria2Parser(
|
||||
proxy,
|
||||
opts['include-unsupported-proxy'],
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'tuic':
|
||||
if (!proxy.token || proxy.token.length === 0) {
|
||||
|
||||
@@ -47,7 +47,16 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
|
||||
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
|
||||
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
|
||||
try {
|
||||
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
|
||||
let raw = await download(
|
||||
resourceUrl,
|
||||
arg?.ua,
|
||||
arg?.timeout,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
let proxies = ProxyUtils.parse(raw);
|
||||
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
|
||||
'include-unsupported-proxy': arg?.includeUnsupportedProxy,
|
||||
|
||||
@@ -50,11 +50,21 @@ function createCollection(req, res) {
|
||||
|
||||
function getCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
let { raw } = req.query;
|
||||
name = decodeURIComponent(name);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
if (collection) {
|
||||
success(res, collection);
|
||||
if (raw) {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(name)}.json"`,
|
||||
)
|
||||
.send(JSON.stringify(collection));
|
||||
} else {
|
||||
success(res, collection);
|
||||
}
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
|
||||
@@ -13,11 +13,44 @@ import { getISO } from '@/utils/geo';
|
||||
import env from '@/utils/env';
|
||||
|
||||
export default function register($app) {
|
||||
$app.get('/share/col/:name/:target', async (req, res) => {
|
||||
const { target } = req.params;
|
||||
if (target) {
|
||||
req.query.target = target;
|
||||
$.info(`使用路由指定目标: ${target}`);
|
||||
}
|
||||
await downloadCollection(req, res);
|
||||
});
|
||||
$app.get('/share/col/:name', downloadCollection);
|
||||
$app.get('/share/sub/:name/:target', async (req, res) => {
|
||||
const { target } = req.params;
|
||||
if (target) {
|
||||
req.query.target = target;
|
||||
$.info(`使用路由指定目标: ${target}`);
|
||||
}
|
||||
await downloadSubscription(req, res);
|
||||
});
|
||||
$app.get('/share/sub/:name', downloadSubscription);
|
||||
|
||||
$app.get('/download/collection/:name/:target', async (req, res) => {
|
||||
const { target } = req.params;
|
||||
if (target) {
|
||||
req.query.target = target;
|
||||
$.info(`使用路由指定目标: ${target}`);
|
||||
}
|
||||
await downloadCollection(req, res);
|
||||
});
|
||||
$app.get('/download/collection/:name', downloadCollection);
|
||||
$app.get('/download/:name/:target', async (req, res) => {
|
||||
const { target } = req.params;
|
||||
if (target) {
|
||||
req.query.target = target;
|
||||
$.info(`使用路由指定目标: ${target}`);
|
||||
}
|
||||
await downloadSubscription(req, res);
|
||||
});
|
||||
$app.get('/download/:name', downloadSubscription);
|
||||
|
||||
$app.get(
|
||||
'/download/collection/:name/api/v1/server/details',
|
||||
async (req, res) => {
|
||||
@@ -59,11 +92,9 @@ async function downloadSubscription(req, res) {
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||||
$.info(
|
||||
`正在下载订阅:${name}\n请求 User-Agent: ${
|
||||
req.headers['user-agent'] || req.headers['User-Agent']
|
||||
}`,
|
||||
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
|
||||
);
|
||||
let {
|
||||
url,
|
||||
@@ -98,6 +129,14 @@ async function downloadSubscription(req, res) {
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
content = url;
|
||||
$.info(`URL 不是链接,视为本地订阅`);
|
||||
}
|
||||
}
|
||||
if (content) {
|
||||
content = decodeURIComponent(content);
|
||||
$.info(`指定本地订阅: ${content}`);
|
||||
}
|
||||
if (proxy) {
|
||||
proxy = decodeURIComponent(proxy);
|
||||
@@ -107,10 +146,7 @@ async function downloadSubscription(req, res) {
|
||||
ua = decodeURIComponent(ua);
|
||||
$.info(`指定远程订阅 User-Agent: ${ua}`);
|
||||
}
|
||||
if (content) {
|
||||
content = decodeURIComponent(content);
|
||||
$.info(`指定本地订阅: ${content}`);
|
||||
}
|
||||
|
||||
if (mergeSources) {
|
||||
mergeSources = decodeURIComponent(mergeSources);
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
@@ -140,6 +176,13 @@ async function downloadSubscription(req, res) {
|
||||
const sub = findByName(allSubs, name);
|
||||
if (sub) {
|
||||
try {
|
||||
const passThroughUA = sub.passThroughUA;
|
||||
if (passThroughUA) {
|
||||
$.info(
|
||||
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
|
||||
);
|
||||
ua = reqUA;
|
||||
}
|
||||
let output = await produceArtifact({
|
||||
type: 'subscription',
|
||||
name,
|
||||
@@ -213,9 +256,29 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
let subUserInfo;
|
||||
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||
try {
|
||||
subUserInfo = await getFlowHeaders(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
proxy || sub.proxy,
|
||||
sub.subUserinfo,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`订阅 ${name} 使用自定义流量链接 ${
|
||||
sub.subUserinfo
|
||||
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
subUserInfo = sub.subUserinfo;
|
||||
}
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
|
||||
[subUserInfo, flowInfo].filter((i) => i).join('; '),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,7 +318,7 @@ async function downloadSubscription(req, res) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.error(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
@@ -283,7 +346,7 @@ async function downloadCollection(req, res) {
|
||||
$.info(
|
||||
`正在下载组合订阅:${name}\n请求 User-Agent: ${
|
||||
req.headers['user-agent'] || req.headers['User-Agent']
|
||||
}`,
|
||||
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
|
||||
);
|
||||
|
||||
let {
|
||||
@@ -355,13 +418,12 @@ async function downloadCollection(req, res) {
|
||||
proxy,
|
||||
noCache,
|
||||
});
|
||||
|
||||
let subUserInfoOfSub;
|
||||
// forward flow header from the first subscription in this collection
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const subnames = collection.subscriptions;
|
||||
if (subnames.length > 0) {
|
||||
const sub = findByName(allSubs, subnames[0]);
|
||||
let flowInfo;
|
||||
if (
|
||||
sub.source !== 'local' ||
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
@@ -395,16 +457,13 @@ async function downloadCollection(req, res) {
|
||||
}
|
||||
}
|
||||
if (!$arguments.noFlow) {
|
||||
flowInfo = await getFlowHeaders(
|
||||
subUserInfoOfSub = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
proxy || sub.proxy || collection.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
@@ -415,13 +474,61 @@ async function downloadCollection(req, res) {
|
||||
}
|
||||
}
|
||||
if (sub.subUserinfo) {
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[sub.subUserinfo, flowInfo].filter((i) => i).join('; '),
|
||||
);
|
||||
let subUserInfo;
|
||||
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||
try {
|
||||
subUserInfo = await getFlowHeaders(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
proxy || sub.proxy,
|
||||
sub.subUserinfo,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`组合订阅 ${name} 使用自定义流量链接 ${
|
||||
sub.subUserinfo
|
||||
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
subUserInfo = sub.subUserinfo;
|
||||
}
|
||||
subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
|
||||
.filter((i) => i)
|
||||
.join('; ');
|
||||
}
|
||||
}
|
||||
|
||||
$.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);
|
||||
|
||||
let subUserInfoOfCol;
|
||||
if (/^https?:\/\//.test(collection.subUserinfo)) {
|
||||
try {
|
||||
subUserInfoOfCol = await getFlowHeaders(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
proxy || collection.proxy,
|
||||
collection.subUserinfo,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`组合订阅 ${name} 使用自定义流量链接 ${
|
||||
collection.subUserinfo
|
||||
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
subUserInfoOfCol = collection.subUserinfo;
|
||||
}
|
||||
res.set(
|
||||
'subscription-userinfo',
|
||||
[subUserInfoOfCol, subUserInfoOfSub]
|
||||
.filter((i) => i)
|
||||
.join('; '),
|
||||
);
|
||||
|
||||
if (platform === 'JSON') {
|
||||
if (resultFormat === 'nezha') {
|
||||
output = nezhaTransform(output);
|
||||
|
||||
@@ -62,6 +62,7 @@ async function getFile(req, res) {
|
||||
mergeSources,
|
||||
ignoreFailedRemoteFile,
|
||||
proxy,
|
||||
noCache,
|
||||
} = req.query;
|
||||
let $options = {};
|
||||
if (req.query.$options) {
|
||||
@@ -113,6 +114,9 @@ async function getFile(req, res) {
|
||||
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
|
||||
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||||
}
|
||||
if (noCache) {
|
||||
$.info(`指定不使用缓存: ${noCache}`);
|
||||
}
|
||||
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
@@ -128,6 +132,7 @@ async function getFile(req, res) {
|
||||
ignoreFailedRemoteFile,
|
||||
$options,
|
||||
proxy,
|
||||
noCache,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -179,7 +184,7 @@ async function getFile(req, res) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.error(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}!`);
|
||||
$.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
@@ -192,11 +197,21 @@ async function getFile(req, res) {
|
||||
}
|
||||
function getWholeFile(req, res) {
|
||||
let { name } = req.params;
|
||||
let { raw } = req.query;
|
||||
name = decodeURIComponent(name);
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (file) {
|
||||
success(res, file);
|
||||
if (raw) {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(name)}.json"`,
|
||||
)
|
||||
.send(JSON.stringify(file));
|
||||
} else {
|
||||
success(res, file);
|
||||
}
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
|
||||
@@ -114,6 +114,10 @@ async function compareSub(req, res) {
|
||||
sub.ua,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
@@ -219,6 +223,10 @@ async function compareCollection(req, res) {
|
||||
sub.ua,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
|
||||
@@ -57,9 +57,29 @@ async function getFlowInfo(req, res) {
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
if (sub.subUserinfo) {
|
||||
let subUserInfo;
|
||||
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||
try {
|
||||
subUserInfo = await getFlowHeaders(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
sub.subUserinfo,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`订阅 ${name} 使用自定义流量链接 ${
|
||||
sub.subUserinfo
|
||||
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
subUserInfo = sub.subUserinfo;
|
||||
}
|
||||
try {
|
||||
success(res, {
|
||||
...parseFlowHeaders(sub.subUserinfo),
|
||||
...parseFlowHeaders(subUserInfo),
|
||||
});
|
||||
} catch (e) {
|
||||
$.error(
|
||||
@@ -149,9 +169,29 @@ async function getFlowInfo(req, res) {
|
||||
startDate: $arguments.startDate,
|
||||
cycleDays: $arguments.cycleDays,
|
||||
});
|
||||
let subUserInfo;
|
||||
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||
try {
|
||||
subUserInfo = await getFlowHeaders(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
sub.proxy,
|
||||
sub.subUserinfo,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`订阅 ${name} 使用自定义流量链接 ${
|
||||
sub.subUserinfo
|
||||
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
subUserInfo = sub.subUserinfo;
|
||||
}
|
||||
const result = {
|
||||
...parseFlowHeaders(
|
||||
[sub.subUserinfo, flowHeaders].filter((i) => i).join('; '),
|
||||
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
|
||||
),
|
||||
};
|
||||
if (remainingDays != null) {
|
||||
|
||||
@@ -73,7 +73,8 @@ async function produceArtifact({
|
||||
proxy || sub.proxy,
|
||||
undefined,
|
||||
awaitCustomCache,
|
||||
noCache,
|
||||
noCache || sub.noCache,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
@@ -121,7 +122,8 @@ async function produceArtifact({
|
||||
proxy || sub.proxy,
|
||||
undefined,
|
||||
awaitCustomCache,
|
||||
noCache,
|
||||
noCache || sub.noCache,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
@@ -242,7 +244,8 @@ async function produceArtifact({
|
||||
collection.proxy,
|
||||
undefined,
|
||||
undefined,
|
||||
noCache,
|
||||
noCache || sub.noCache,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import $ from '@/core/app';
|
||||
import dnsPacket from 'dns-packet';
|
||||
import { Buffer } from 'buffer';
|
||||
import { isIPv4 } from '@/utils';
|
||||
|
||||
export async function doh({ url, domain, type = 'A', timeout, edns }) {
|
||||
const buf = dnsPacket.encode({
|
||||
@@ -23,7 +24,7 @@ export async function doh({ url, domain, type = 'A', timeout, edns }) {
|
||||
{
|
||||
code: 'CLIENT_SUBNET',
|
||||
ip: edns,
|
||||
sourcePrefixLength: 24,
|
||||
sourcePrefixLength: isIPv4(edns) ? 24 : 56,
|
||||
scopePrefixLength: 0,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
validCheck,
|
||||
} from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
|
||||
const clashPreprocessor = PROXY_PREPROCESSORS.find(
|
||||
(processor) => processor.name === 'Clash Pre-processor',
|
||||
);
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
@@ -22,6 +26,7 @@ export default async function download(
|
||||
skipCustomCache,
|
||||
awaitCustomCache,
|
||||
noCache,
|
||||
preprocess,
|
||||
) {
|
||||
let $arguments = {};
|
||||
let url = rawUrl.replace(/#noFlow$/, '');
|
||||
@@ -87,6 +92,9 @@ export default async function download(
|
||||
timeout,
|
||||
proxy,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
preprocess,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
@@ -107,6 +115,9 @@ export default async function download(
|
||||
timeout,
|
||||
proxy,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
preprocess,
|
||||
).catch((e) => {
|
||||
$.error(
|
||||
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
|
||||
@@ -169,10 +180,10 @@ export default async function download(
|
||||
: { insecure: true }
|
||||
: undefined;
|
||||
$.info(
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nURL: ${url}`,
|
||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
|
||||
);
|
||||
try {
|
||||
const { body, headers, statusCode } = await http.get({
|
||||
let { body, headers, statusCode } = await http.get({
|
||||
url,
|
||||
...(proxy ? { proxy } : {}),
|
||||
...(isLoon && proxy ? { node: proxy } : {}),
|
||||
@@ -193,6 +204,15 @@ export default async function download(
|
||||
}
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
throw new Error(new Error('远程资源内容为空'));
|
||||
if (preprocess) {
|
||||
try {
|
||||
if (clashPreprocessor.test(body)) {
|
||||
body = clashPreprocessor.parse(body);
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`Clash Pre-processor error: ${e}`);
|
||||
}
|
||||
}
|
||||
let shouldCache = true;
|
||||
if (cacheThreshold) {
|
||||
const size = body.length / 1024;
|
||||
|
||||
@@ -167,6 +167,9 @@ export async function getFlowHeaders(
|
||||
flowInfo = getFlowField(headers);
|
||||
}
|
||||
}
|
||||
if (flowInfo) {
|
||||
flowInfo = flowInfo.trim();
|
||||
}
|
||||
if (flowInfo) {
|
||||
headersResourceCache.set(id, flowInfo);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const ISOFlags = {
|
||||
'🇧🇪': ['BE', 'BEL'],
|
||||
'🇧🇬': ['BG', 'BGR'],
|
||||
'🇧🇭': ['BH', 'BHR'],
|
||||
'🇧🇴': ['BO', 'BOL'],
|
||||
'🇧🇷': ['BR', 'BRA'],
|
||||
'🇧🇾': ['BY', 'BLR'],
|
||||
'🇨🇦': ['CA', 'CAN'],
|
||||
@@ -38,6 +39,7 @@ const ISOFlags = {
|
||||
'🇬🇧': ['GB', 'GBR', 'UK'],
|
||||
'🇬🇪': ['GE', 'GEO'],
|
||||
'🇬🇷': ['GR', 'GRC'],
|
||||
'🇬🇹': ['GT', 'GTM'],
|
||||
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
|
||||
'🇭🇷': ['HR', 'HRV'],
|
||||
'🇭🇺': ['HU', 'HUN'],
|
||||
@@ -141,6 +143,7 @@ export function getFlag(name) {
|
||||
'🇧🇭': ['Bahrain', '巴林'],
|
||||
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
||||
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
||||
'🇧🇴': ['Bolivia', '玻利维亚'],
|
||||
'🇨🇦': [
|
||||
'Canada',
|
||||
'加拿大',
|
||||
@@ -191,6 +194,7 @@ export function getFlag(name) {
|
||||
],
|
||||
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
||||
'🇬🇷': ['Greece', '希腊', '希臘'],
|
||||
'🇬🇹': ['Guatemala', '危地马拉'],
|
||||
'🇭🇰': [
|
||||
'Hongkong',
|
||||
'香港',
|
||||
|
||||
@@ -21,6 +21,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
|
||||
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
|
||||
// 15. `ip-version` 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
|
||||
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
|
||||
|
||||
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
|
||||
|
||||
@@ -165,6 +166,20 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
|
||||
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
|
||||
|
||||
// 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
|
||||
|
||||
// let name = ''
|
||||
// for (const [key, value] of Object.entries(env.source)) {
|
||||
// if (!key.startsWith('_')) {
|
||||
// name = value.displayName || value.name
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if (!name) {
|
||||
// const collection = env.source._collection
|
||||
// name = collection.displayName || collection.name
|
||||
// }
|
||||
|
||||
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
|
||||
// {
|
||||
// "source": {
|
||||
|
||||
Reference in New Issue
Block a user