mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0ab301160 | ||
|
|
a22df97a51 | ||
|
|
45772ade4d | ||
|
|
e8dab545f5 | ||
|
|
c2bd80207a | ||
|
|
bc5ae9a2ef | ||
|
|
36db057e32 | ||
|
|
5ac73b863a | ||
|
|
23042c33d6 | ||
|
|
4ca5f5e355 |
20
README.md
20
README.md
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<p align="center" color="#6a737d">
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||
@@ -31,23 +31,25 @@ Core functionalities:
|
||||
- [x] SSD URI
|
||||
- [x] V2RayN URI
|
||||
- [x] Hysteria2 URI
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
||||
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
|
||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
|
||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
||||
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
|
||||
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
|
||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
|
||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||
|
||||
### Supported Target Platforms
|
||||
|
||||
- [x] QX
|
||||
- [x] Loon
|
||||
- [x] Surge
|
||||
- [x] Surfboard
|
||||
- [x] Stash
|
||||
- [x] Clash.Meta
|
||||
- [x] Clash
|
||||
- [x] ShadowRocket
|
||||
- [x] Shadowrocket
|
||||
- [x] V2Ray
|
||||
- [x] V2Ray URI
|
||||
- [x] Plain JSON
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.130",
|
||||
"version": "2.14.136",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import download from '@/utils/download';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
@@ -214,6 +214,9 @@ function safeMatch(parser, line) {
|
||||
}
|
||||
|
||||
function lastParse(proxy) {
|
||||
if (isValidPortNumber(proxy.port)) {
|
||||
proxy.port = parseInt(proxy.port, 10);
|
||||
}
|
||||
if (proxy.server) {
|
||||
proxy.server = proxy.server
|
||||
.trim()
|
||||
|
||||
@@ -90,11 +90,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[proxy.network + '-opts'] = {
|
||||
'grpc-service-name': params["serviceName"],
|
||||
'_grpc-type': params["mode"],
|
||||
};
|
||||
} else {
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,11 +88,18 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[proxy.network + '-opts'] = {
|
||||
'grpc-service-name': params["serviceName"],
|
||||
'_grpc-type': params["mode"],
|
||||
};
|
||||
} else {
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import URI_Producer from './uri';
|
||||
import V2Ray_Producer from './v2ray';
|
||||
import QX_Producer from './qx';
|
||||
import ShadowRocket_Producer from './shadowrocket';
|
||||
import Surfboard_Producer from './surfboard';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
@@ -27,4 +28,5 @@ export default {
|
||||
JSON: JSON_Producer(),
|
||||
Stash: Stash_Producer(),
|
||||
ShadowRocket: ShadowRocket_Producer(),
|
||||
Surfboard: Surfboard_Producer(),
|
||||
};
|
||||
|
||||
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
199
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Result, isPresent } from './utils';
|
||||
import { isNotBlank } from '@/utils';
|
||||
// import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'Surfboard';
|
||||
|
||||
export default function Surfboard_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'wireguard-surge':
|
||||
return wireguard(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
// obfs
|
||||
if (isPresent(proxy, 'plugin')) {
|
||||
if (proxy.plugin === 'obfs') {
|
||||
result.append(`,obfs=${proxy['plugin-opts'].mode}`);
|
||||
result.appendIfPresent(
|
||||
`,obfs-host=${proxy['plugin-opts'].host}`,
|
||||
'plugin-opts.host',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||
'plugin-opts.path',
|
||||
);
|
||||
} else {
|
||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function trojan(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
// transport
|
||||
handleTransport(result, proxy);
|
||||
|
||||
// tls
|
||||
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function vmess(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||
|
||||
// transport
|
||||
handleTransport(result, proxy);
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
result.append(`,vmess-aead=${proxy.aead}`);
|
||||
} else {
|
||||
result.append(`,vmess-aead=${proxy.alterId === 0}`);
|
||||
}
|
||||
|
||||
// tls
|
||||
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function http(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const type = proxy.tls ? 'https' : 'http';
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function socks5(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function wireguard(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
result.append(`${proxy.name}=wireguard`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,section-name=${proxy['section-name']}`,
|
||||
'section-name',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function handleTransport(result, proxy) {
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,ws=true`);
|
||||
if (isPresent(proxy, 'ws-opts')) {
|
||||
result.appendIfPresent(
|
||||
`,ws-path=${proxy['ws-opts'].path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
if (isPresent(proxy, 'ws-opts.headers')) {
|
||||
const headers = proxy['ws-opts'].headers;
|
||||
const value = Object.keys(headers)
|
||||
.map((k) => {
|
||||
let v = headers[k];
|
||||
if (['Host'].includes(k)) {
|
||||
v = `"${v}"`;
|
||||
}
|
||||
return `${k}:${v}`;
|
||||
})
|
||||
.join('|');
|
||||
if (isNotBlank(value)) {
|
||||
result.append(`,ws-headers=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Result } from './utils';
|
||||
import Surge_Producer from './surge';
|
||||
|
||||
const targetPlatform = 'SurgeMac';
|
||||
// const targetPlatform = 'SurgeMac';
|
||||
|
||||
const surge_Producer = Surge_Producer();
|
||||
|
||||
@@ -10,24 +10,9 @@ export default function SurgeMac_Producer() {
|
||||
switch (proxy.type) {
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
case 'ss':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'trojan':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'vmess':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'http':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'socks5':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'snell':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'tuic':
|
||||
default:
|
||||
return surge_Producer.produce(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
@@ -201,6 +201,21 @@ export default function URI_Producer() {
|
||||
let trojanTransport = '';
|
||||
if (proxy.network) {
|
||||
trojanTransport = `&type=${proxy.network}`;
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
let trojanTransportServiceName =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`${proxy.network}-service-name`
|
||||
];
|
||||
if (trojanTransportServiceName) {
|
||||
trojanTransport += `&serviceName=${encodeURIComponent(
|
||||
trojanTransportServiceName,
|
||||
)}`;
|
||||
}
|
||||
trojanTransport += `&mode=${encodeURIComponent(
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||
'gun',
|
||||
)}`;
|
||||
}
|
||||
let trojanTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let trojanTransportHost =
|
||||
|
||||
@@ -20,7 +20,7 @@ async function downloadSubscription(req, res) {
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
$.info(`正在下载订阅:${name}`);
|
||||
let { url, ua, content, mergeSources } = req.query;
|
||||
let { url, ua, content, mergeSources, ignoreFailedRemoteSub } = req.query;
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
@@ -37,6 +37,10 @@ async function downloadSubscription(req, res) {
|
||||
mergeSources = decodeURIComponent(mergeSources);
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
}
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||
}
|
||||
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
@@ -50,6 +54,7 @@ async function downloadSubscription(req, res) {
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteSub,
|
||||
});
|
||||
|
||||
if (sub.source !== 'local' || url) {
|
||||
@@ -116,12 +121,20 @@ async function downloadCollection(req, res) {
|
||||
|
||||
$.info(`正在下载组合订阅:${name}`);
|
||||
|
||||
let { ignoreFailedRemoteSub } = req.query;
|
||||
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'collection',
|
||||
name,
|
||||
platform,
|
||||
ignoreFailedRemoteSub,
|
||||
});
|
||||
|
||||
// forward flow header from the first subscription in this collection
|
||||
|
||||
@@ -22,7 +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(/\]$/, '')
|
||||
)}?lang=${lang}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InternalServerError, NetworkError } from './errors';
|
||||
import { InternalServerError } from './errors';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { success, failed } from './response';
|
||||
@@ -22,24 +22,31 @@ async function compareSub(req, res) {
|
||||
) {
|
||||
content = sub.content;
|
||||
} else {
|
||||
try {
|
||||
content = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
const errors = {};
|
||||
content = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, sub.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||
'无法下载远程资源',
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
content.unshift(sub.content);
|
||||
@@ -87,69 +94,95 @@ async function compareCollection(req, res) {
|
||||
const collection = req.body;
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
let hasError;
|
||||
const errors = {};
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
if (!hasError) {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
let raw;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
);
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
currentProxies.forEach((proxy) => {
|
||||
proxy.subName = sub.name;
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
{ [sub.name]: sub, _collection: collection },
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
let raw;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, sub.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
} catch (err) {
|
||||
if (!hasError) {
|
||||
hasError = true;
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'PROCESS_FAILED',
|
||||
`处理子订阅 ${name} 失败`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
if (
|
||||
!sub.ignoreFailedRemoteSub &&
|
||||
Object.keys(errors).length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
currentProxies.forEach((proxy) => {
|
||||
proxy.subName = sub.name;
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
{ [sub.name]: sub, _collection: collection },
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
} catch (err) {
|
||||
errors[name] = err;
|
||||
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
}时出现错误:${err}!进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}%`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (hasError) return;
|
||||
if (
|
||||
!collection.ignoreFailedRemoteSub &&
|
||||
Object.keys(errors).length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
|
||||
@@ -30,6 +30,7 @@ async function produceArtifact({
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteSub,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
@@ -40,13 +41,35 @@ async function produceArtifact({
|
||||
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((url) => download(url, ua)),
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, ua || sub.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||
}
|
||||
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (mergeSources === 'localFirst') {
|
||||
raw.unshift(content);
|
||||
} else if (mergeSources === 'remoteFirst') {
|
||||
@@ -58,13 +81,35 @@ async function produceArtifact({
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, ua || sub.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||
}
|
||||
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
@@ -131,13 +176,34 @@ async function produceArtifact({
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
.map(async (url) => {
|
||||
try {
|
||||
return await download(url, sub.ua);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (
|
||||
!sub.ignoreFailedRemoteSub &&
|
||||
Object.keys(errors).length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
@@ -174,15 +240,21 @@ async function produceArtifact({
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
}时出现错误:${err},该订阅已被跳过!进度--${
|
||||
}时出现错误:${err}!进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}%`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||
}
|
||||
if (
|
||||
!collectionIgnoreFailedRemoteSub &&
|
||||
Object.keys(errors).length > 0
|
||||
) {
|
||||
throw new Error(
|
||||
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
|
||||
@@ -13,6 +13,12 @@ function isIPv6(ip) {
|
||||
return IPV6_REGEX.test(ip);
|
||||
}
|
||||
|
||||
function isValidPortNumber(port) {
|
||||
return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
function isNotBlank(str) {
|
||||
return typeof str === 'string' && str.trim().length > 0;
|
||||
}
|
||||
@@ -29,4 +35,12 @@ function getIfPresent(obj, defaultValue) {
|
||||
return isPresent(obj) ? obj : defaultValue;
|
||||
}
|
||||
|
||||
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent };
|
||||
export {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isValidPortNumber,
|
||||
isNotBlank,
|
||||
getIfNotBlank,
|
||||
isPresent,
|
||||
getIfPresent,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ export function getPlatformFromHeaders(headers) {
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surfboard') !== -1) {
|
||||
return 'Surfboard';
|
||||
} else if (UA.indexOf('Surge Mac') !== -1) {
|
||||
return 'SurgeMac';
|
||||
} else if (UA.indexOf('Surge') !== -1) {
|
||||
|
||||
@@ -22,6 +22,8 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
### 3. QX
|
||||
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
|
||||
|
||||
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
|
||||
|
||||
### 4. Stash
|
||||
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user