mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c558cfdcd | ||
|
|
75d88c02c7 | ||
|
|
99d5868cff | ||
|
|
e1489a3cf7 | ||
|
|
59fe16a7b0 | ||
|
|
562d349629 | ||
|
|
9ce14351c5 | ||
|
|
76e781c711 | ||
|
|
f0acf4a2a7 | ||
|
|
9abeb4ce7b | ||
|
|
153802c7c4 | ||
|
|
19418b631f | ||
|
|
97caeed208 | ||
|
|
dd8d1d85e8 | ||
|
|
14ed56b5d5 |
@@ -10,10 +10,10 @@
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
[](https://www.buymeacoffee.com/PengYM)
|
||||
|
||||
|
||||
Core functionalities:
|
||||
|
||||
1. Conversion among various formats.
|
||||
@@ -21,7 +21,7 @@ Core functionalities:
|
||||
3. Collect multiple subscriptions in one URL.
|
||||
|
||||
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
|
||||
|
||||
|
||||
## 1. Subscription Conversion
|
||||
|
||||
### Supported Input Formats
|
||||
@@ -98,7 +98,7 @@ or
|
||||
esbuild(experimental)
|
||||
|
||||
```
|
||||
pnpm run --parallel "/^dev:.*/"
|
||||
SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
@@ -111,7 +111,6 @@ This project is under the GPL V3 LICENSE.
|
||||
|
||||
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.358",
|
||||
"version": "2.14.375",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -77,7 +77,13 @@ function parse(raw) {
|
||||
return proxies;
|
||||
}
|
||||
|
||||
async function processFn(proxies, operators = [], targetPlatform, source) {
|
||||
async function processFn(
|
||||
proxies,
|
||||
operators = [],
|
||||
targetPlatform,
|
||||
source,
|
||||
$options,
|
||||
) {
|
||||
for (const item of operators) {
|
||||
// process script
|
||||
let script;
|
||||
@@ -176,6 +182,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
|
||||
targetPlatform,
|
||||
$arguments,
|
||||
source,
|
||||
$options,
|
||||
);
|
||||
} else {
|
||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||
@@ -237,21 +244,10 @@ function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
|
||||
$.log(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
let localPort = 10000;
|
||||
let list = proxies
|
||||
.map((proxy) => {
|
||||
try {
|
||||
let line = producer.produce(proxy, type, opts);
|
||||
if (
|
||||
line.length > 0 &&
|
||||
line.includes('__SubStoreLocalPort__')
|
||||
) {
|
||||
line = line.replace(
|
||||
/__SubStoreLocalPort__/g,
|
||||
localPort++,
|
||||
);
|
||||
}
|
||||
return line;
|
||||
return producer.produce(proxy, type, opts);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot produce proxy: ${JSON.stringify(
|
||||
@@ -424,8 +420,12 @@ function lastParse(proxy) {
|
||||
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||
}
|
||||
}
|
||||
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
|
||||
delete proxy.ports;
|
||||
if (['hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
if (proxy.ports) {
|
||||
proxy.ports = proxy.ports.replace(/\//g, ',');
|
||||
} else {
|
||||
delete proxy.ports;
|
||||
}
|
||||
}
|
||||
if (
|
||||
['hysteria2'].includes(proxy.type) &&
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isPresent,
|
||||
isNotBlank,
|
||||
getIfPresent,
|
||||
getRandomPort,
|
||||
} from '@/utils';
|
||||
import getSurgeParser from './peggy/surge';
|
||||
import getLoonParser from './peggy/loon';
|
||||
@@ -13,6 +14,19 @@ import getTrojanURIParser from './peggy/trojan-uri';
|
||||
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
function surge_port_hopping(raw) {
|
||||
const [parts, port_hopping] =
|
||||
raw.match(
|
||||
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
|
||||
) || [];
|
||||
return {
|
||||
port_hopping: port_hopping
|
||||
? port_hopping.replace(/;/g, ',')
|
||||
: undefined,
|
||||
line: parts ? raw.replace(parts, '') : raw,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
|
||||
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
|
||||
function URI_SS() {
|
||||
@@ -158,7 +172,7 @@ function URI_SSR() {
|
||||
for (const item of line) {
|
||||
let [key, val] = item.split('=');
|
||||
val = val.trim();
|
||||
if (val.length > 0) {
|
||||
if (val.length > 0 && val !== '(null)') {
|
||||
other_params[key] = val;
|
||||
}
|
||||
}
|
||||
@@ -296,6 +310,11 @@ function URI_VMess() {
|
||||
? !params.verify_cert
|
||||
: undefined,
|
||||
};
|
||||
if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
|
||||
params.allowInsecure,
|
||||
);
|
||||
}
|
||||
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
|
||||
if (proxy.tls && proxy.sni) {
|
||||
proxy.sni = params.sni;
|
||||
@@ -351,6 +370,7 @@ function URI_VMess() {
|
||||
transportPath = '/';
|
||||
}
|
||||
}
|
||||
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
|
||||
if (transportPath || transportHost) {
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
@@ -539,13 +559,42 @@ function URI_Hysteria2() {
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split(/(hysteria2|hy2):\/\//)[2];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, password, server, ___, port, ____, addons = '', name] =
|
||||
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
// 端口跳跃有两种写法:
|
||||
// 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
|
||||
// 端口部分支持 端口跳跃 的「多端口地址格式」。
|
||||
// https://hysteria.network/zh/docs/advanced/Port-Hopping
|
||||
// 2. 参数 mport
|
||||
let ports;
|
||||
/* eslint-disable no-unused-vars */
|
||||
let [
|
||||
__,
|
||||
password,
|
||||
server,
|
||||
___,
|
||||
port,
|
||||
____,
|
||||
_____,
|
||||
______,
|
||||
_______,
|
||||
________,
|
||||
addons = '',
|
||||
name,
|
||||
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
|
||||
line,
|
||||
);
|
||||
/* eslint-enable no-unused-vars */
|
||||
if (/^\d+$/.test(port)) {
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
port = 443;
|
||||
}
|
||||
} else if (port) {
|
||||
ports = port;
|
||||
port = getRandomPort(ports);
|
||||
} else {
|
||||
port = 443;
|
||||
}
|
||||
|
||||
password = decodeURIComponent(password);
|
||||
if (name != null) {
|
||||
name = decodeURIComponent(name);
|
||||
@@ -557,6 +606,7 @@ function URI_Hysteria2() {
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
ports,
|
||||
password,
|
||||
};
|
||||
|
||||
@@ -1289,7 +1339,12 @@ function Surge_Tuic() {
|
||||
const test = (line) => {
|
||||
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
const parse = (raw) => {
|
||||
const { port_hopping, line } = surge_port_hopping(raw);
|
||||
const proxy = getSurgeParser().parse(line);
|
||||
proxy['ports'] = port_hopping;
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
function Surge_WireGuard() {
|
||||
@@ -1306,7 +1361,12 @@ function Surge_Hysteria2() {
|
||||
const test = (line) => {
|
||||
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
const parse = (raw) => {
|
||||
const { port_hopping, line } = surge_port_hopping(raw);
|
||||
const proxy = getSurgeParser().parse(line);
|
||||
proxy['ports'] = port_hopping;
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
|
||||
@@ -54,31 +54,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
@@ -172,6 +172,8 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
|
||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
@@ -52,31 +52,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
@@ -170,6 +170,8 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
|
||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
@@ -91,11 +91,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
@@ -104,7 +104,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
@@ -151,6 +151,8 @@ port = digits:[0-9]+ {
|
||||
}
|
||||
}
|
||||
|
||||
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
|
||||
@@ -89,11 +89,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "tuic";
|
||||
handleShadowTLS();
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
handleShadowTLS();
|
||||
@@ -102,7 +102,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
|
||||
proxy.type = "wireguard-surge";
|
||||
handleShadowTLS();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
handleShadowTLS();
|
||||
}
|
||||
@@ -149,6 +149,8 @@ port = digits:[0-9]+ {
|
||||
}
|
||||
}
|
||||
|
||||
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
|
||||
@@ -316,7 +316,7 @@ function RegexDeleteOperator(regex) {
|
||||
1. This function name should be `operator`!
|
||||
2. Always declare variables before using them!
|
||||
*/
|
||||
function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
|
||||
return {
|
||||
name: 'Script Operator',
|
||||
func: async (proxies) => {
|
||||
@@ -326,6 +326,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
'operator',
|
||||
script,
|
||||
$arguments,
|
||||
$options,
|
||||
);
|
||||
output = operator(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
@@ -338,9 +339,9 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
'operator',
|
||||
`async function operator(input = []) {
|
||||
if (input && (input.$files || input.$content)) {
|
||||
let { $content, $files } = input
|
||||
let { $content, $files, $options } = input
|
||||
${script}
|
||||
return { $content, $files }
|
||||
return { $content, $files, $options }
|
||||
} else {
|
||||
let proxies = input
|
||||
let list = []
|
||||
@@ -352,6 +353,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
}
|
||||
}`,
|
||||
$arguments,
|
||||
$options,
|
||||
);
|
||||
output = operator(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
@@ -392,18 +394,23 @@ const DOMAIN_RESOLVERS = {
|
||||
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (!noCache && cached) return cached;
|
||||
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
|
||||
const res = await doh({
|
||||
url,
|
||||
domain,
|
||||
type: type === 'IPv6' ? 'AAAA' : 'A',
|
||||
type: answerType,
|
||||
timeout,
|
||||
edns,
|
||||
});
|
||||
|
||||
const { answers } = res;
|
||||
if (!Array.isArray(answers) || answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers.map((i) => i?.data).filter((i) => i);
|
||||
const result = answers
|
||||
.filter((i) => i?.type === answerType)
|
||||
.map((i) => i?.data)
|
||||
.filter((i) => i);
|
||||
if (result.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
@@ -789,7 +796,7 @@ function TypeFilter(types) {
|
||||
1. This function name should be `filter`!
|
||||
2. Always declare variables before using them!
|
||||
*/
|
||||
function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||
function ScriptFilter(script, targetPlatform, $arguments, source, $options) {
|
||||
return {
|
||||
name: 'Script Filter',
|
||||
func: async (proxies) => {
|
||||
@@ -799,6 +806,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||
'filter',
|
||||
script,
|
||||
$arguments,
|
||||
$options,
|
||||
);
|
||||
output = filter(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
@@ -821,6 +829,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||
return list
|
||||
}`,
|
||||
$arguments,
|
||||
$options,
|
||||
);
|
||||
output = filter(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
@@ -961,7 +970,7 @@ function clone(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
function createDynamicFunction(name, script, $arguments) {
|
||||
function createDynamicFunction(name, script, $arguments, $options) {
|
||||
const flowUtils = {
|
||||
getFlowField,
|
||||
getFlowHeaders,
|
||||
@@ -973,6 +982,7 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
'$options',
|
||||
'$substore',
|
||||
'lodash',
|
||||
'$persistentStore',
|
||||
@@ -982,9 +992,11 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
'produceArtifact',
|
||||
'require',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
$options,
|
||||
$,
|
||||
lodash,
|
||||
// eslint-disable-next-line no-undef
|
||||
@@ -997,26 +1009,30 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
scriptResourceCache,
|
||||
flowUtils,
|
||||
produceArtifact,
|
||||
eval(`typeof require !== "undefined"`) ? require : undefined,
|
||||
);
|
||||
} else {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
'$options',
|
||||
'$substore',
|
||||
'lodash',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
'produceArtifact',
|
||||
|
||||
'require',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
$options,
|
||||
$,
|
||||
lodash,
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
flowUtils,
|
||||
produceArtifact,
|
||||
eval(`typeof require !== "undefined"`) ? require : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,9 +163,11 @@ export default function Clash_Producer() {
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
if (type !== 'internal') {
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -178,9 +178,11 @@ export default function ClashMeta_Producer() {
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
if (type !== 'internal') {
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -153,6 +153,14 @@ function trojan(proxy) {
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -215,6 +223,14 @@ function vmess(proxy) {
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
@@ -286,6 +302,14 @@ function vless(proxy) {
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -339,6 +363,11 @@ function socks5(proxy) {
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -418,6 +447,14 @@ function hysteria2(proxy) {
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
|
||||
@@ -181,9 +181,11 @@ export default function ShadowRocket_Producer() {
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
if (type !== 'internal') {
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -204,11 +204,6 @@ const tlsParser = (proxy, parsedProxy) => {
|
||||
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
|
||||
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
|
||||
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
|
||||
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||
parsedProxy.tls.utls = {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
};
|
||||
if (proxy['reality-opts']) {
|
||||
parsedProxy.tls.reality = { enabled: true };
|
||||
if (proxy['reality-opts']['public-key'])
|
||||
@@ -217,7 +212,13 @@ const tlsParser = (proxy, parsedProxy) => {
|
||||
if (proxy['reality-opts']['short-id'])
|
||||
parsedProxy.tls.reality.short_id =
|
||||
proxy['reality-opts']['short-id'];
|
||||
parsedProxy.tls.utls = { enabled: true };
|
||||
}
|
||||
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
|
||||
parsedProxy.tls.utls = {
|
||||
enabled: true,
|
||||
fingerprint: proxy['client-fingerprint'],
|
||||
};
|
||||
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
|
||||
};
|
||||
|
||||
|
||||
@@ -272,9 +272,11 @@ export default function Stash_Producer() {
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
if (type !== 'internal') {
|
||||
for (const key in proxy) {
|
||||
if (proxy[key] == null || /^_/i.test(key)) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -675,6 +675,15 @@ function tuic(proxy) {
|
||||
'alpn',
|
||||
);
|
||||
|
||||
if (isPresent(proxy, 'ports')) {
|
||||
result.append(`,port-hopping=${proxy.ports.replace(/,/g, ';')}`);
|
||||
}
|
||||
|
||||
result.appendIfPresent(
|
||||
`,port-hopping-interval=${proxy['hop-interval']}`,
|
||||
'hop-interval',
|
||||
);
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
@@ -935,6 +944,15 @@ function hysteria2(proxy) {
|
||||
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
if (isPresent(proxy, 'ports')) {
|
||||
result.append(`,port-hopping=${proxy.ports.replace(/,/g, ';')}`);
|
||||
}
|
||||
|
||||
result.appendIfPresent(
|
||||
`,port-hopping-interval=${proxy['hop-interval']}`,
|
||||
'hop-interval',
|
||||
);
|
||||
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Result } from './utils';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { Result, isPresent } from './utils';
|
||||
import Surge_Producer from './surge';
|
||||
import { isIPv4, isIPv6, isPresent } from '@/utils';
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'SurgeMac';
|
||||
@@ -8,14 +10,22 @@ const targetPlatform = 'SurgeMac';
|
||||
const surge_Producer = Surge_Producer();
|
||||
|
||||
export default function SurgeMac_Producer() {
|
||||
const produce = (proxy) => {
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'external':
|
||||
return external(proxy);
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
default:
|
||||
return surge_Producer.produce(proxy);
|
||||
// case 'ssr':
|
||||
// return shadowsocksr(proxy);
|
||||
default: {
|
||||
try {
|
||||
return surge_Producer.produce(proxy, type, opts);
|
||||
} catch (e) {
|
||||
$.log(
|
||||
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
|
||||
);
|
||||
return mihomo(proxy, type, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return { produce };
|
||||
@@ -60,6 +70,7 @@ function external(proxy) {
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function shadowsocksr(proxy) {
|
||||
const external_proxy = {
|
||||
...proxy,
|
||||
@@ -84,6 +95,7 @@ function shadowsocksr(proxy) {
|
||||
for (const [key, value] of Object.entries({
|
||||
cipher: '-m',
|
||||
obfs: '-o',
|
||||
'obfs-param': '-g',
|
||||
password: '-k',
|
||||
port: '-p',
|
||||
protocol: '-O',
|
||||
@@ -92,12 +104,73 @@ function shadowsocksr(proxy) {
|
||||
'local-port': '-l',
|
||||
'local-address': '-b',
|
||||
})) {
|
||||
external_proxy.args.push(value);
|
||||
external_proxy.args.push(external_proxy[key]);
|
||||
if (external_proxy[key] != null) {
|
||||
external_proxy.args.push(value);
|
||||
external_proxy.args.push(external_proxy[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return external(external_proxy);
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function mihomo(proxy, type, opts) {
|
||||
const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
|
||||
if (clashProxy) {
|
||||
const localPort = opts?.localPort || proxy._localPort || 65535;
|
||||
const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
|
||||
? false
|
||||
: true;
|
||||
const external_proxy = {
|
||||
name: proxy.name,
|
||||
type: 'external',
|
||||
exec: proxy._exec || '/usr/local/bin/mihomo',
|
||||
'local-port': localPort,
|
||||
args: [
|
||||
'-config',
|
||||
Base64.encode(
|
||||
JSON.stringify({
|
||||
'mixed-port': localPort,
|
||||
ipv6,
|
||||
mode: 'global',
|
||||
dns: {
|
||||
enable: true,
|
||||
ipv6,
|
||||
nameserver: [
|
||||
'https://223.6.6.6/dns-query',
|
||||
'https://120.53.53.53/dns-query',
|
||||
],
|
||||
},
|
||||
proxies: [
|
||||
{
|
||||
...clashProxy,
|
||||
name: 'proxy',
|
||||
},
|
||||
],
|
||||
'proxy-groups': [
|
||||
{
|
||||
name: 'GLOBAL',
|
||||
type: 'select',
|
||||
proxies: ['proxy'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
],
|
||||
addresses: [],
|
||||
};
|
||||
|
||||
// https://manual.nssurge.com/policy/external-proxy.html
|
||||
if (isIP(proxy.server)) {
|
||||
external_proxy.addresses.push(proxy.server);
|
||||
} else {
|
||||
$.log(
|
||||
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
|
||||
);
|
||||
}
|
||||
opts.localPort = localPort - 1;
|
||||
return external(external_proxy);
|
||||
}
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
|
||||
@@ -70,6 +70,24 @@ async function downloadSubscription(req, res) {
|
||||
includeUnsupportedProxy,
|
||||
resultFormat,
|
||||
} = req.query;
|
||||
let $options = {};
|
||||
if (req.query.$options) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||
} catch (e) {
|
||||
for (const pair of req.query.$options.split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$options[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||
}
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
@@ -116,6 +134,7 @@ async function downloadSubscription(req, res) {
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||
},
|
||||
$options,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -247,6 +266,25 @@ async function downloadCollection(req, res) {
|
||||
resultFormat,
|
||||
} = req.query;
|
||||
|
||||
let $options = {};
|
||||
if (req.query.$options) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||
} catch (e) {
|
||||
for (const pair of req.query.$options.split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$options[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||
}
|
||||
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||
@@ -272,6 +310,7 @@ async function downloadCollection(req, res) {
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||
},
|
||||
$options,
|
||||
});
|
||||
|
||||
// forward flow header from the first subscription in this collection
|
||||
|
||||
@@ -60,6 +60,24 @@ async function getFile(req, res) {
|
||||
mergeSources,
|
||||
ignoreFailedRemoteFile,
|
||||
} = req.query;
|
||||
let $options = {};
|
||||
if (req.query.$options) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||
} catch (e) {
|
||||
for (const pair of req.query.$options.split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$options[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||
}
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程文件 URL: ${url}`);
|
||||
@@ -101,6 +119,7 @@ async function getFile(req, res) {
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteFile,
|
||||
$options,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import $ from '@/core/app';
|
||||
import migrate from '@/utils/migration';
|
||||
import download from '@/utils/download';
|
||||
import { syncArtifacts } from '@/restful/sync';
|
||||
import { gistBackupAction } from '@/restful/miscs';
|
||||
|
||||
import registerSubscriptionRoutes from './subscriptions';
|
||||
import registerCollectionRoutes from './collections';
|
||||
@@ -46,18 +47,76 @@ export default function serve() {
|
||||
if ($.env.isNode) {
|
||||
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON');
|
||||
if (backend_cron) {
|
||||
$.info(`[CRON] ${backend_cron} enabled`);
|
||||
$.info(`[SYNC CRON] ${backend_cron} enabled`);
|
||||
const { CronJob } = eval(`require("cron")`);
|
||||
new CronJob(
|
||||
backend_cron,
|
||||
async function () {
|
||||
try {
|
||||
$.info(`[CRON] ${backend_cron} started`);
|
||||
$.info(`[SYNC CRON] ${backend_cron} started`);
|
||||
await syncArtifacts();
|
||||
$.info(`[CRON] ${backend_cron} finished`);
|
||||
$.info(`[SYNC CRON] ${backend_cron} finished`);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[CRON] ${backend_cron} error: ${e.message ?? e}`,
|
||||
`[SYNC CRON] ${backend_cron} error: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}, // onTick
|
||||
null, // onComplete
|
||||
true, // start
|
||||
// 'Asia/Shanghai' // timeZone
|
||||
);
|
||||
}
|
||||
const backend_download_cron = eval(
|
||||
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
|
||||
);
|
||||
if (backend_download_cron) {
|
||||
$.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);
|
||||
const { CronJob } = eval(`require("cron")`);
|
||||
new CronJob(
|
||||
backend_download_cron,
|
||||
async function () {
|
||||
try {
|
||||
$.info(
|
||||
`[DOWNLOAD CRON] ${backend_download_cron} started`,
|
||||
);
|
||||
await gistBackupAction('download');
|
||||
$.info(
|
||||
`[DOWNLOAD CRON] ${backend_download_cron} finished`,
|
||||
);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[DOWNLOAD CRON] ${backend_download_cron} error: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}, // onTick
|
||||
null, // onComplete
|
||||
true, // start
|
||||
// 'Asia/Shanghai' // timeZone
|
||||
);
|
||||
}
|
||||
const backend_upload_cron = eval(
|
||||
'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',
|
||||
);
|
||||
if (backend_upload_cron) {
|
||||
$.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);
|
||||
const { CronJob } = eval(`require("cron")`);
|
||||
new CronJob(
|
||||
backend_upload_cron,
|
||||
async function () {
|
||||
try {
|
||||
$.info(`[UPLOAD CRON] ${backend_upload_cron} started`);
|
||||
await gistBackupAction('upload');
|
||||
$.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[UPLOAD CRON] ${backend_upload_cron} error: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}, // onTick
|
||||
|
||||
@@ -80,10 +80,72 @@ async function refresh(_, res) {
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function gistBackupAction(action) {
|
||||
// read token
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) throw new Error('GitHub Token is required for backup!');
|
||||
|
||||
const gist = new Gist({
|
||||
token: gistToken,
|
||||
key: GIST_BACKUP_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
let content;
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const updated = settings.syncTime;
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
// update syncTime
|
||||
settings.syncTime = new Date().getTime();
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
content = $.read('#sub-store');
|
||||
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
|
||||
$.info(`上传备份中...`);
|
||||
try {
|
||||
await gist.upload({
|
||||
[GIST_BACKUP_FILE_NAME]: { content },
|
||||
});
|
||||
$.info(`上传备份完成`);
|
||||
} catch (err) {
|
||||
// restore syncTime if upload failed
|
||||
settings.syncTime = updated;
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
throw err;
|
||||
}
|
||||
break;
|
||||
case 'download':
|
||||
$.info(`还原备份中...`);
|
||||
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
||||
try {
|
||||
if (Object.keys(JSON.parse(content).settings).length === 0) {
|
||||
throw new Error('备份文件应该至少包含 settings 字段');
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||
}
|
||||
// restore settings
|
||||
$.write(content, '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
content = JSON.parse(content);
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
$.info(`perform migration after restoring from gist...`);
|
||||
migrate();
|
||||
$.info(`migration completed`);
|
||||
$.info(`还原备份完成`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
async function gistBackup(req, res) {
|
||||
const { action } = req.query;
|
||||
// read token
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
failed(
|
||||
res,
|
||||
@@ -93,68 +155,8 @@ async function gistBackup(req, res) {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const gist = new Gist({
|
||||
token: gistToken,
|
||||
key: GIST_BACKUP_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
try {
|
||||
let content;
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const updated = settings.syncTime;
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
// update syncTime
|
||||
settings.syncTime = new Date().getTime();
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
content = $.read('#sub-store');
|
||||
if ($.env.isNode)
|
||||
content = JSON.stringify($.cache, null, ` `);
|
||||
$.info(`上传备份中...`);
|
||||
try {
|
||||
await gist.upload({
|
||||
[GIST_BACKUP_FILE_NAME]: { content },
|
||||
});
|
||||
} catch (err) {
|
||||
// restore syncTime if upload failed
|
||||
settings.syncTime = updated;
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
throw err;
|
||||
}
|
||||
break;
|
||||
case 'download':
|
||||
$.info(`还原备份中...`);
|
||||
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
||||
try {
|
||||
if (
|
||||
Object.keys(JSON.parse(content).settings).length ===
|
||||
0
|
||||
) {
|
||||
throw new Error(
|
||||
'备份文件应该至少包含 settings 字段',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||
}
|
||||
// restore settings
|
||||
$.write(content, '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
content = JSON.parse(content);
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
$.info(`perform migration after restoring from gist...`);
|
||||
migrate();
|
||||
$.info(`migration completed`);
|
||||
$.info(`还原备份完成`);
|
||||
break;
|
||||
}
|
||||
await gistBackupAction(action);
|
||||
success(res);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
@@ -171,3 +173,5 @@ async function gistBackup(req, res) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { gistBackupAction };
|
||||
|
||||
@@ -37,6 +37,7 @@ async function produceArtifact({
|
||||
produceOpts = {},
|
||||
subscription,
|
||||
awaitCustomCache,
|
||||
$options,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
@@ -158,6 +159,7 @@ async function produceArtifact({
|
||||
sub.process || [],
|
||||
platform,
|
||||
{ [sub.name]: sub },
|
||||
$options,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||
@@ -259,7 +261,11 @@ async function produceArtifact({
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
{ [sub.name]: sub, _collection: collection },
|
||||
{
|
||||
[sub.name]: sub,
|
||||
_collection: collection,
|
||||
$options,
|
||||
},
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
processed++;
|
||||
@@ -312,6 +318,7 @@ async function produceArtifact({
|
||||
collection.process || [],
|
||||
platform,
|
||||
{ _collection: collection },
|
||||
$options,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||
@@ -460,10 +467,10 @@ async function produceArtifact({
|
||||
const processed =
|
||||
Array.isArray(file.process) && file.process.length > 0
|
||||
? await ProxyUtils.process(
|
||||
{ $files: files, $content: filesContent },
|
||||
{ $files: files, $content: filesContent, $options },
|
||||
file.process,
|
||||
)
|
||||
: { $content: filesContent, $files: files };
|
||||
: { $content: filesContent, $files: files, $options };
|
||||
|
||||
return processed?.$content ?? '';
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ export function getFlag(name) {
|
||||
'沪俄',
|
||||
'Moscow',
|
||||
],
|
||||
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
|
||||
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
|
||||
'🇸🇪': ['Sweden', '瑞典'],
|
||||
'🇸🇬': [
|
||||
'Singapore',
|
||||
|
||||
37
config/Egern.yaml
Normal file
37
config/Egern.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Sub-Store
|
||||
description: '支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *'
|
||||
compat_arguments:
|
||||
ability: http-client-policy
|
||||
cronexp: 55 23 * * *
|
||||
sync: '"Sub-Store Sync"'
|
||||
timeout: '120'
|
||||
engine: auto
|
||||
produce: '"# Sub-Store Produce"'
|
||||
produce_cronexp: 50 */6 * * *
|
||||
produce_sub: '"sub1,sub2"'
|
||||
produce_col: '"col1,col2"'
|
||||
compat_arguments_desc: '\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
|
||||
scriptings:
|
||||
- http_request:
|
||||
name: Sub-Store Core
|
||||
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
|
||||
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
|
||||
body_required: true
|
||||
- http_request:
|
||||
name: Sub-Store Simple
|
||||
match: ^https?:\/\/sub\.store
|
||||
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
|
||||
body_required: true
|
||||
- schedule:
|
||||
name: '{{{sync}}}'
|
||||
cron: '{{{cronexp}}}'
|
||||
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
- schedule:
|
||||
name: '{{{produce}}}'
|
||||
cron: '{{{produce_cronexp}}}'
|
||||
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
arguments:
|
||||
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
|
||||
mitm:
|
||||
hostnames:
|
||||
- sub.store
|
||||
@@ -45,6 +45,9 @@ Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess
|
||||
### 5. Shadowrocket
|
||||
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。
|
||||
|
||||
### 6. Egern
|
||||
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。
|
||||
|
||||
## 使用 Sub-Store
|
||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
||||
@@ -56,4 +59,4 @@ https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%9
|
||||
|
||||
## 脚本使用说明
|
||||
|
||||
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||
|
||||
@@ -18,11 +18,22 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
||||
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
||||
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
|
||||
//
|
||||
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
|
||||
|
||||
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
|
||||
|
||||
// $arguments 为传入的脚本参数
|
||||
|
||||
// $options 为通过链接传入的参数
|
||||
// 例如: { arg1: 'a', arg2: 'b' }
|
||||
// 可这样传:
|
||||
// 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
|
||||
// /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
|
||||
// 或这样传:
|
||||
// 先这样处理 encodeURIComponent('arg1=a&arg2=b')
|
||||
// /api/file/foo?$options=arg1%3Da%26arg2%3Db
|
||||
|
||||
// console.log($options)
|
||||
|
||||
// targetPlatform 为输出的目标平台
|
||||
|
||||
// lodash
|
||||
@@ -54,6 +65,8 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// Gist, // Gist 类
|
||||
// }
|
||||
|
||||
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
|
||||
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
|
||||
// 示例: 给节点名添加前缀
|
||||
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
|
||||
// 示例: 给节点名添加旗帜
|
||||
@@ -131,7 +144,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// yaml.proxies.unshift(...clashMetaProxies)
|
||||
// $content = ProxyUtils.yaml.dump(yaml)
|
||||
|
||||
// { $content, $files } will be passed to the next operator
|
||||
// { $content, $files, $options } will be passed to the next operator
|
||||
// $content is the final content of the file
|
||||
|
||||
// flowUtils 为机场订阅流量信息处理工具
|
||||
@@ -139,7 +152,7 @@ function operator(proxies = [], targetPlatform, context) {
|
||||
// 1. https://t.me/zhetengsha/948
|
||||
|
||||
// context 为传入的上下文
|
||||
// 有三种情况, 按需判断
|
||||
// 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断
|
||||
|
||||
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
|
||||
|
||||
|
||||
Reference in New Issue
Block a user