Compare commits

...

24 Commits

Author SHA1 Message Date
xream
fca508ba8a feat: Surge Hysteria2 输入/输出增加 ecn 参数 2023-10-12 22:15:10 +08:00
xream
21b531a44d feat: Surge TUIC 输入/输出增加 ecn 参数 2023-10-12 22:09:58 +08:00
xream
4e5b46a43d feat: Surge Hysteria2 输出增加 download-bandwidth(若有值但解析失败则为 0) 2023-10-12 00:39:10 +08:00
xream
bf81ca4acf feat: 输入增加 Hysteria2 URI 支持; Surge Hysteria2 输出增加 fingerprint 2023-10-11 23:35:42 +08:00
xream
e7c0b23222 feat: Surge 输入输出增加 Hysteria2 2023-10-09 23:42:22 +08:00
xream
40fb0fd7f3 feat: 兼容更多 VMess URI 格式 2023-10-09 17:36:11 +08:00
xream
b061fca356 feat: Surge Snell 输入支持解析 reuse 字段 2023-10-08 16:42:35 +08:00
xream
d3c6c99b0a feat: proxy 增加 subName(订阅名), collectionName(组合订阅名); 脚本增加第三个参数 env(包含订阅/组合订阅/环境/版本等信息) 2023-10-08 13:21:22 +08:00
xream
3fbc280e28 [+] 重复节点通知中增加订阅名称和重复节点名称 2023-10-02 16:21:08 +08:00
xream
9e3e4c6e46 [+] Surge 输出支持 underlying-proxy; VMess/Vless URI 支持 gRPC mode(默认为 gun) 2023-10-01 22:05:51 +08:00
xream
bc0dd4b175 feat: 支持 hysteria2 2023-09-22 14:43:43 +08:00
xream
7603fac036 fix: 修复部分环境无 clearTimeout 的问题 2023-09-18 20:09:03 +08:00
K
9acc161684 fix @ 2023-09-15 18:52:21 +08:00
xream
024582a99d fix: 修复 sub-store-0 路由 2023-09-15 18:42:53 +08:00
xream
1d31a80b9f fix: 修复文件和模块命名/重复添加的逻辑 2023-09-15 10:08:36 +08:00
xream
b2d0276836 feat: 文件和模块接口获取原始内容; 文件列表不返回原始内容 2023-09-14 18:51:23 +08:00
xream
3211fbf357 feat: 模块接口; 脚本参数支持 JSON 和 URL编码 2023-09-14 17:34:24 +08:00
xream
33a17c2d66 feat: 实验性支持本地脚本复用 2023-09-14 08:56:33 +08:00
xream
2c89a0ddbd feat: 支持 Clash VLESS 输出(与 Clash.Meta 的区别为: 无 XTLS 2023-09-11 02:35:36 +08:00
xream
939022e5a3 fix: 修复了 Clash.Meta 输出 VLESS 时 内部字段 sni 未作用到 servername 的问题 2023-09-09 14:03:40 +08:00
xream
59bca5670d fix: 预览时脚本下载报错导致的崩溃 2023-09-07 23:17:36 +08:00
Peng-YM
07b38cf971 release: backend version 2.14.49 2023-09-04 23:16:52 +08:00
Peng-YM
28186f596f feat: added the ability to change the base path for the data files
before starting node, use the command `export SUB_STORE_DATA_BASE_PATH="<YOUR_PATH>"`
2023-09-04 23:16:13 +08:00
xream
ea31b1d0ec fix: 排序接口修正为使用 name 排序 2023-09-04 21:31:55 +08:00
24 changed files with 750 additions and 152 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.48", "version": "2.14.71",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {

View File

@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings'; export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs'; export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections'; export const COLLECTIONS_KEY = 'collections';
export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts'; export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules'; export const RULES_KEY = 'rules';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';

View File

@@ -63,11 +63,11 @@ function parse(raw) {
return proxies; return proxies;
} }
async function process(proxies, operators = [], targetPlatform) { async function process(proxies, operators = [], targetPlatform, source) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
const $arguments = {}; let $arguments = {};
if (item.type.indexOf('Script') !== -1) { if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
@@ -75,10 +75,19 @@ async function process(proxies, operators = [], targetPlatform) {
// extract link arguments // extract link arguments
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
for (const pair of rawArgs[1].split('&')) { try {
const key = pair.split('=')[0]; // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
const value = pair.split('=')[1] || true; $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
$arguments[key] = value; } 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);
}
} }
} }
@@ -113,6 +122,7 @@ async function process(proxies, operators = [], targetPlatform) {
script, script,
targetPlatform, targetPlatform,
$arguments, $arguments,
source,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -199,7 +209,7 @@ function lastParse(proxy) {
delete proxy.network; delete proxy.network;
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
proxy.tls = true; proxy.tls = true;
} }
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {

View File

@@ -240,7 +240,7 @@ function URI_VMess() {
params.add = server; params.add = server;
} }
const proxy = { const proxy = {
name: params.ps ?? params.remark, name: params.ps ?? params.remarks,
type: 'vmess', type: 'vmess',
server: params.add, server: params.add,
port: parseInt(getIfPresent(params.port), 10), port: parseInt(getIfPresent(params.port), 10),
@@ -267,9 +267,19 @@ function URI_VMess() {
params.obfs === 'http' params.obfs === 'http'
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} }
if (proxy.network) { if (proxy.network) {
let transportHost = params.host ?? params.obfsParam; let transportHost = params.host ?? params.obfsParam;
try {
const parsedObfs = JSON.parse(transportHost);
const parsedHost = parsedObfs?.Host;
if (parsedHost) {
transportHost = parsedHost;
}
// eslint-disable-next-line no-empty
} catch (e) {}
let transportPath = params.path; let transportPath = params.path;
if (proxy.network === 'http') { if (proxy.network === 'http') {
@@ -285,10 +295,17 @@ function URI_VMess() {
} }
} }
if (transportPath || transportHost) { if (transportPath || transportHost) {
proxy[`${proxy.network}-opts`] = { if (['grpc'].includes(proxy.network)) {
path: getIfNotBlank(transportPath), proxy[`${proxy.network}-opts`] = {
headers: { Host: getIfNotBlank(transportHost) }, 'grpc-service-name': getIfNotBlank(transportPath),
}; '_grpc-type': getIfNotBlank(params.type),
};
} else {
proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
}
} else { } else {
delete proxy.network; delete proxy.network;
} }
@@ -365,6 +382,10 @@ function URI_VLESS() {
if (params.serviceName) { if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName; opts[`${proxy.network}-service-name`] = params.serviceName;
} }
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (Object.keys(opts).length > 0) { if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts; proxy[`${proxy.network}-opts`] = opts;
} }
@@ -383,6 +404,53 @@ function URI_VLESS() {
}; };
return { name, test, parse }; return { name, test, parse };
} }
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^hysteria2:\/\//.test(line);
};
const parse = (line) => {
line = line.split('hysteria2://')[1];
// eslint-disable-next-line no-unused-vars
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}`;
const proxy = {
type: 'hysteria2',
name,
server,
port,
password,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.sni = params.sni;
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
proxy.obfs = params.obfs;
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy.fingerprint = params.pinSHA256;
return proxy;
};
return { name, test, parse };
}
// Trojan URI format // Trojan URI format
function URI_Trojan() { function URI_Trojan() {
@@ -423,6 +491,7 @@ function Clash_All() {
'tuic', 'tuic',
'vless', 'vless',
'hysteria', 'hysteria',
'hysteria2',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
@@ -762,11 +831,21 @@ function Surge_WireGuard() {
return { name, test, parse }; return { name, test, parse };
} }
function Surge_Hysteria2() {
const name = 'Surge Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
export default [ export default [
URI_SS(), URI_SS(),
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),
URI_VLESS(), URI_VLESS(),
URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
Clash_All(), Clash_All(),
Surge_SS(), Surge_SS(),
@@ -776,6 +855,7 @@ export default [
Surge_Snell(), Surge_Snell(),
Surge_Tuic(), Surge_Tuic(),
Surge_WireGuard(), Surge_WireGuard(),
Surge_Hysteria2(),
Surge_Socks5(), Surge_Socks5(),
Loon_SS(), Loon_SS(),
Loon_SSR(), Loon_SSR(),

View File

@@ -32,7 +32,7 @@ const grammars = String.raw`
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
@@ -67,7 +67,7 @@ https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (fast_open/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -76,16 +76,19 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
} }
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -188,11 +191,14 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); } section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); } no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }

View File

@@ -30,7 +30,7 @@
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
@@ -65,7 +65,7 @@ https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (fast_open/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/reuse/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -74,16 +74,19 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/ecn/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
} }
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
} }
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -186,11 +189,14 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); } section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); } no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }

View File

@@ -7,6 +7,7 @@ import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import env from '@/utils/env';
/** /**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -294,7 +295,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`! 1. This function name should be `operator`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptOperator(script, targetPlatform, $arguments) { function ScriptOperator(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -305,7 +306,7 @@ function ScriptOperator(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = operator(proxies, targetPlatform); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -562,7 +563,7 @@ function TypeFilter(types) {
1. This function name should be `filter`! 1. This function name should be `filter`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptFilter(script, targetPlatform, $arguments) { function ScriptFilter(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -573,7 +574,7 @@ function ScriptFilter(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = filter(proxies, targetPlatform); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },

View File

@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() { export default function Clash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// 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 // filter unsupported proxies
proxies = proxies.filter((proxy) => { proxies = proxies.filter((proxy) => {
if ( if (
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'vless',
'socks', 'socks',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'wireguard', 'wireguard',
].includes(proxy.type) ].includes(proxy.type) ||
) { (proxy.type === 'snell' && String(proxy.version) === '4') ||
return false; (proxy.type === 'vless' &&
} else if ( (typeof proxy.flow !== 'undefined' ||
proxy.type === 'snell' && proxy['reality-opts']))
String(proxy.version) === '4'
) { ) {
return false; return false;
} }
@@ -87,10 +90,22 @@ export default function Clash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
delete 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'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -81,6 +81,11 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-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 ( if (
@@ -103,11 +108,23 @@ export default function ClashMeta_Producer() {
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
delete 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'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -108,11 +108,23 @@ export default function ShadowRocket_Producer() {
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
delete 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'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -120,10 +120,22 @@ export default function Stash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (['trojan', 'tuic', 'hysteria'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
delete 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'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -31,6 +31,8 @@ export default function Surge_Producer() {
return tuic(proxy); return tuic(proxy);
case 'wireguard-surge': case 'wireguard-surge':
return wireguard(proxy); return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -71,6 +73,12 @@ function shadowsocks(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -107,6 +115,12 @@ function trojan(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -150,6 +164,12 @@ function vmess(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -182,6 +202,12 @@ function http(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -216,6 +242,12 @@ function socks5(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -245,6 +277,12 @@ function snell(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// reuse // reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
@@ -288,6 +326,14 @@ function tuic(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString(); return result.toString();
} }
@@ -304,10 +350,7 @@ function wireguard(proxy) {
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert', 'no-error-alert',
); );
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent( result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, `,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version', 'ip-version',
@@ -316,6 +359,66 @@ function wireguard(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy.fingerprint}`,
'fingerprint',
);
// tfo
result.appendIfPresent(`,tfo=${proxy['fast-open']}`, 'fast-open');
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString(); return result.toString();
} }

View File

@@ -91,6 +91,16 @@ export default function URI_Producer() {
? vmessTransportHost[0] ? vmessTransportHost[0]
: vmessTransportHost; : vmessTransportHost;
} }
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
'grpc-service-name'
];
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
}
} }
result = 'vmess://' + Base64.encode(JSON.stringify(result)); result = 'vmess://' + Base64.encode(JSON.stringify(result));
break; break;
@@ -141,6 +151,12 @@ export default function URI_Producer() {
let vlessTransport = `&type=${encodeURIComponent( let vlessTransport = `&type=${encodeURIComponent(
proxy.network, proxy.network,
)}`; )}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
}
let vlessTransportServiceName = let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[ proxy[`${proxy.network}-opts`]?.[

View File

@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
import registerSettingRoutes from '@/restful/settings'; import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs'; import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort'; import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerModuleRoutes from '@/restful/module';
migrate(); migrate();
serve(); serve();
@@ -30,6 +32,8 @@ function serve() {
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerSortRoutes($app); registerSortRoutes($app);

109
backend/src/restful/file.js Normal file
View File

@@ -0,0 +1,109 @@
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';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
}
// file API
function createFile(req, res) {
const file = req.body;
file.name = `${file.name ?? Date.now()}`;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${file.name} 的文件`
: `无法同时创建相同的文件 可稍后重试`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
function getFile(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);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(
res, // eslint-disable-next-line no-unused-vars
allFiles.map(({ content, ...rest }) => rest),
);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -4,6 +4,8 @@ import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts'; import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync'; import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings'; import registerSettingRoutes from './settings';
@@ -23,6 +25,8 @@ export default function serve() {
registerSortingRoutes($app); registerSortingRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);
registerMiscRoutes($app); registerMiscRoutes($app);

View File

@@ -1,7 +1,6 @@
import $ from '@/core/app'; import $ from '@/core/app';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import { import {
@@ -12,6 +11,7 @@ import {
import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import env from '@/utils/env';
export default function register($app) { export default function register($app) {
// utils // utils
@@ -49,19 +49,7 @@ export default function register($app) {
} }
function getEnv(req, res) { function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV(); success(res, env);
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
} }
async function refresh(_, res) { async function refresh(_, res) {

View File

@@ -0,0 +1,114 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { MODULES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { hex_md5 } from '@/vendor/md5';
export default function register($app) {
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
$app.route('/api/module/:name')
.get(getModule)
.patch(updateModule)
.delete(deleteModule);
$app.route('/api/modules')
.get(getAllModules)
.post(createModule)
.put(replaceModule);
}
// module API
function createModule(req, res) {
const module = req.body;
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
$.info(`正在创建模块:${module.name}`);
const allModules = $.read(MODULES_KEY);
if (findByName(allModules, module.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${module.name} 的模块`
: `已存在相同的模块 请勿重复添加`,
),
);
}
allModules.push(module);
$.write(allModules, MODULES_KEY);
success(res, module, 201);
}
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
res.status(200).json(module.content);
} else {
failed(
res,
new ResourceNotFoundError(
`MODULE_NOT_FOUND`,
`Module ${name} does not exist`,
404,
),
);
}
}
function updateModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let module = req.body;
const allModules = $.read(MODULES_KEY);
const oldModule = findByName(allModules, name);
if (oldModule) {
const newModule = {
...oldModule,
...module,
};
$.info(`正在更新模块:${name}...`);
updateByName(allModules, name, newModule);
$.write(allModules, MODULES_KEY);
success(res, newModule);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Module ${name} does not exist!`,
),
404,
);
}
}
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);
$.write(allModules, MODULES_KEY);
success(res);
}
function getAllModules(req, res) {
const allModules = $.read(MODULES_KEY);
success(
res,
// eslint-disable-next-line no-unused-vars
allModules.map(({ content, ...rest }) => rest),
);
}
function replaceModule(req, res) {
const allModules = req.body;
$.write(allModules, MODULES_KEY);
success(res);
}

View File

@@ -12,98 +12,133 @@ export default function register($app) {
} }
async function compareSub(req, res) { async function compareSub(req, res) {
const sub = req.body; try {
const target = req.query.target || 'JSON'; const sub = req.body;
let content; const target = req.query.target || 'JSON';
if (sub.source === 'local') { let content;
content = sub.content; if (sub.source === 'local') {
} else { content = sub.content;
try { } else {
content = await download(sub.url, sub.ua);
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
}
// parse proxies
const original = ProxyUtils.parse(content);
// add id
original.forEach((proxy, i) => {
proxy.id = i;
});
// apply processors
const processed = await ProxyUtils.process(
original,
sub.process || [],
target,
);
// produce
success(res, { original, processed });
}
async function compareCollection(req, res) {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try { try {
let raw; content = await download(sub.url, sub.ua);
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
);
results[name] = currentProxies;
} catch (err) { } catch (err) {
failed( failed(
res, res,
new InternalServerError( new NetworkError(
'PROCESS_FAILED', 'FAILED_TO_DOWNLOAD_RESOURCE',
`处理子订阅 ${name} 失败`, '无法下载远程资源',
`Reason: ${err}`, `Reason: ${err}`,
), ),
); );
return;
} }
}), }
); // parse proxies
const original = ProxyUtils.parse(content);
// merge proxies with the original order // add id
const original = Array.prototype.concat.apply( original.forEach((proxy, i) => {
[], proxy.id = i;
subnames.map((name) => results[name] || []), proxy.subName = sub.name;
); });
original.forEach((proxy, i) => { // apply processors
proxy.id = i; const processed = await ProxyUtils.process(
}); original,
sub.process || [],
target,
{ [sub.name]: sub },
);
const processed = await ProxyUtils.process( // produce
original, success(res, { original, processed });
collection.process || [], } catch (err) {
'JSON', $.error(err.message ?? err);
); failed(
res,
success(res, { original, processed }); new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview subscription`,
`Reason: ${err.message ?? err}`,
),
);
}
}
async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
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) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
`Reason: ${err}`,
),
);
}
}),
);
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name] || []),
);
original.forEach((proxy, i) => {
proxy.id = i;
proxy.collectionName = collection.name;
});
const processed = await ProxyUtils.process(
original,
collection.process || [],
'JSON',
{ _collection: collection },
);
success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview collection`,
`Reason: ${err.message ?? err}`,
),
);
}
} }

View File

@@ -11,7 +11,7 @@ export default function register($app) {
function sortSubs(req, res) { function sortSubs(req, res) {
const orders = req.body; const orders = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
success(res, allSubs); success(res, allSubs);
} }
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) { function sortCollections(req, res) {
const orders = req.body; const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
success(res, allCols); success(res, allCols);
} }
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) { function sortArtifacts(req, res) {
const orders = req.body; const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts); success(res, allArtifacts);
} }

View File

@@ -36,11 +36,15 @@ async function produceArtifact({ type, name, platform }) {
} }
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
proxies.forEach((proxy) => {
proxy.subName = sub.name;
});
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`); throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -51,7 +55,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -86,11 +90,18 @@ async function produceArtifact({ type, name, platform }) {
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = ProxyUtils.parse(raw);
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors // apply processors
currentProxies = await ProxyUtils.process( currentProxies = await ProxyUtils.process(
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection },
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -127,11 +138,16 @@ async function produceArtifact({ type, name, platform }) {
subnames.map((name) => results[name] || []), subnames.map((name) => results[name] || []),
); );
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
});
// apply own processors // apply own processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -142,7 +158,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':

View File

@@ -1,10 +1,30 @@
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(url, ua) { export default async function download(url, ua) {
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到本地 ${type}: ${name}`);
}
return item.content;
}
const { isNode } = ENV(); const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url); const id = hex_md5(ua + url);

16
backend/src/utils/env.js Normal file
View File

@@ -0,0 +1,16 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
export default {
backend,
version: substoreVersion,
};

View File

@@ -49,27 +49,32 @@ export class OpenAPI {
if (isNode) { if (isNode) {
// create a json for root cache // create a json for root cache
let fpath = 'root.json'; const basePath =
if (!this.node.fs.existsSync(fpath)) { eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.root = {}; this.root = {};
} else { } else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`)); this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} }
// create a json file with the given name if not exists // create a json file with the given name if not exists
fpath = `${this.name}.json`; let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.cache = {}; this.cache = {};
} else { } else {
this.cache = JSON.parse( this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.node.fs.readFileSync(`${this.name}.json`),
);
} }
} }
} }
@@ -80,14 +85,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name); if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) { if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
`${this.name}.json`, `${basePath}/${this.name}.json`,
data, data,
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
); );
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
'root.json', `${basePath}/root.json`,
JSON.stringify(this.root, null, 2), JSON.stringify(this.root, null, 2),
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
@@ -308,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
return ( return (
timer timer
? Promise.race([timer, worker]).then((res) => { ? Promise.race([timer, worker]).then((res) => {
clearTimeout(timeoutid); if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutid);
}
return res; return res;
}) })
: worker : worker