Compare commits

..

25 Commits

Author SHA1 Message Date
xream
751e50bf99 chore: YAML 解析兼容 2024-01-30 22:23:57 +08:00
xream
a91f978042 feat: 远程订阅 URL 新增参数 validCheck 将检查订阅有效期和剩余流量 2024-01-30 14:14:57 +08:00
xream
1248e6b32a fix: v2ray-plugin 2024-01-30 03:24:42 +08:00
xream
d0f255d9c6 feat: h2 传输层修正 host 为数组, path 不为数组 2024-01-30 02:17:58 +08:00
xream
060415584e feat: 支持 tls 的 plugin 中跳过证书验证; Surge Shadow TLS 默认版本为 2; sing-box 支持 Shadowsocks Shadow TLS 2024-01-30 01:53:22 +08:00
xream
8a2087c53a feat: 支持更多不规范的 SS URI; 去除 Surfboard 节点名中的等号; 支持 Mihomo shadowsocks shadow-tls 2024-01-30 00:49:41 +08:00
xream
64117c50c7 feat: 支持更多不规范的 VLESS/SS URI 2024-01-29 23:38:21 +08:00
xream
6564d9497a chore: 增加定时任务默认为每天 0 点的说明 2024-01-24 15:17:45 +08:00
xream
1c03e46bbb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明(增加双引号 确保兼容性) 2024-01-23 12:45:02 +08:00
xream
084b385fdb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明 2024-01-23 04:03:35 +08:00
xream
290b9b5411 chore: 新增 Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类和参数设置 2024-01-22 04:48:34 +08:00
xream
e5c1ae9ed8 feat: 优化流量解析规则 2024-01-20 23:00:37 +08:00
xream
9b6d9d49f9 Merge pull request #275 from dnomd343/master
fix: 流量信息匹配错误
2024-01-20 22:40:13 +08:00
xream
a12adf5255 chore: 脚本操作时不使用空值合并运算符 2024-01-20 22:14:23 +08:00
Dnomd343
8682f14ee7 fix: scientific counting matching error 2024-01-20 13:16:11 +08:00
xream
b3de7a4bc5 feat: 优化调整 Gist 同步逻辑; 增加 GitLab Snippet 同步 2024-01-20 05:33:31 +08:00
xream
099ae5ad83 fix: 配置接口补齐错误处理 2024-01-20 00:50:35 +08:00
xream
c7d00ac512 feat: 域名解析支持类型和过滤 2024-01-19 21:43:54 +08:00
xream
ca0d800bbb release: backend version 2.14.184 2024-01-19 12:54:40 +08:00
xream
31b48d7a6c Merge pull request #274 from izhangxm/feat_add_proxy_convter_api
增加规则转换与协议转换API接口
2024-01-19 12:35:32 +08:00
makabaka
ab96ae9413 增加规则转换与协议转换API接口 2024-01-19 12:23:04 +08:00
xream
3fc507b576 feat: 解析并删除旧的 ws-path ws-headers 字段 2024-01-19 10:18:27 +08:00
xream
2f2dbbdb68 release: backend version 2.14.182 2024-01-18 17:17:15 +08:00
xream
1543e76841 Merge pull request #273 from izhangxm/master
修复clash规则头部有注释的情况下规则转换功能失败的问题
2024-01-18 17:07:59 +08:00
makabaka
74c4719806 fix_clashprovider_test 2024-01-18 15:09:24 +08:00
37 changed files with 862 additions and 180 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.181", "version": "2.14.196",
"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

@@ -1,4 +1,4 @@
import YAML from 'static-js-yaml'; import YAML from '@/utils/yaml';
import download from '@/utils/download'; import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils'; import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
@@ -224,6 +224,20 @@ function lastParse(proxy) {
.replace(/^\[/, '') .replace(/^\[/, '')
.replace(/\]$/, ''); .replace(/\]$/, '');
} }
if (proxy.network === 'ws') {
if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {
proxy['ws-opts'] = {};
if (proxy['ws-path']) {
proxy['ws-opts'].path = proxy['ws-path'];
}
if (proxy['ws-headers']) {
proxy['ws-opts'].headers = proxy['ws-headers'];
}
}
delete proxy['ws-path'];
delete proxy['ws-headers'];
}
if (proxy.type === 'trojan') { if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') { if (proxy.network === 'tcp') {
delete proxy.network; delete proxy.network;
@@ -240,11 +254,26 @@ function lastParse(proxy) {
if (proxy.network) { if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host; let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
if (transporthost && !transportHost) { if (proxy.network === 'h2') {
if (!transporthost && transportHost) {
proxy[`${proxy.network}-opts`].headers.host = transportHost;
delete proxy[`${proxy.network}-opts`].headers.Host;
}
} else if (transporthost && !transportHost) {
proxy[`${proxy.network}-opts`].headers.Host = transporthost; proxy[`${proxy.network}-opts`].headers.Host = transporthost;
delete proxy[`${proxy.network}-opts`].headers.host; delete proxy[`${proxy.network}-opts`].headers.host;
} }
} }
if (proxy.network === 'h2') {
const host = proxy['h2-opts']?.headers?.host;
const path = proxy['h2-opts']?.path;
if (host && !Array.isArray(host)) {
proxy['h2-opts'].headers.host = [host];
}
if (Array.isArray(path)) {
proxy['h2-opts'].path = path[0];
}
}
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {
if (proxy.network) { if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;

View File

@@ -32,8 +32,27 @@ function URI_SS() {
// handle IPV4 and IPV6 // handle IPV4 and IPV6
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/); let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]); let userInfoStr = Base64.decode(content.split('@')[0]);
let query = '';
if (!serverAndPortArray) { if (!serverAndPortArray) {
if (content.includes('?')) {
const parsed = content.match(/^(.*)(\?.*)$/);
content = parsed[1];
query = parsed[2];
}
content = Base64.decode(content); content = Base64.decode(content);
if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
let v2rayPlugin = parsed[2];
if (v2rayPlugin) {
proxy.plugin = 'v2ray-plugin';
proxy['plugin-opts'] = JSON.parse(
Base64.decode(v2rayPlugin),
);
}
}
content = `${content}${query}`;
}
userInfoStr = content.split('@')[0]; userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/); serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
} }
@@ -70,7 +89,7 @@ function URI_SS() {
}; };
break; break;
case 'v2ray-plugin': case 'v2ray-plugin':
proxy.obfs = 'v2ray-plugin'; proxy.plugin = 'v2ray-plugin';
proxy['plugin-opts'] = { proxy['plugin-opts'] = {
mode: 'websocket', mode: 'websocket',
host: getIfNotBlank(params['obfs-host']), host: getIfNotBlank(params['obfs-host']),
@@ -84,6 +103,12 @@ function URI_SS() {
); );
} }
} }
if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true;
}
if (/(&|\?)tfo=(1|true)/i.test(query)) {
proxy.tfo = true;
}
return proxy; return proxy;
}; };
return { name, test, parse }; return { name, test, parse };
@@ -416,16 +441,31 @@ function URI_VLESS() {
if (!proxy.network && isShadowrocket && params.obfs) { if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs; proxy.network = params.obfs;
} }
if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws';
}
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) { if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {}; const opts = {};
if (params.host) { const host = params.host ?? params.obfsParam;
opts.headers = { Host: params.host }; if (host) {
if (params.obfsParam) {
try {
const parsed = JSON.parse(host);
opts.headers = parsed;
} catch (e) {
opts.headers = { Host: host };
}
} else {
opts.headers = { Host: host };
}
} }
if (params.serviceName) { if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName; opts[`${proxy.network}-service-name`] = params.serviceName;
} else if (isShadowrocket && params.path) { } else if (isShadowrocket && params.path) {
opts[`${proxy.network}-service-name`] = params.path; if (!['ws', 'http', 'h2'].includes(proxy.network)) {
delete params.path; opts[`${proxy.network}-service-name`] = params.path;
delete params.path;
}
} }
if (params.path) { if (params.path) {
opts.path = params.path; opts.path = params.path;

View File

@@ -30,6 +30,11 @@ const grammars = String.raw`
} }
} }
} }
function handleShadowTLS() {
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
proxy['shadow-tls-version'] = 2;
}
}
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
@@ -45,6 +50,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.host", obfs.host);
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
handleShadowTLS();
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
@@ -55,17 +61,21 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
} }
handleWebsocket(); handleWebsocket();
handleShadowTLS();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS();
} }
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS();
} }
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
handleShadowTLS();
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
@@ -75,26 +85,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.host", obfs.host); $set(proxy, "obfs-opts.host", obfs.host);
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
handleShadowTLS();
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/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/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS();
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/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/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS();
} }
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS();
} }
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/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/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS();
} }
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
handleShadowTLS();
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS();
} }
address = comma server:server comma port:port { address = comma server:server comma port:port {

View File

@@ -28,6 +28,11 @@
} }
} }
} }
function handleShadowTLS() {
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
proxy['shadow-tls-version'] = 2;
}
}
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
@@ -43,6 +48,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.host", obfs.host);
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
handleShadowTLS();
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
@@ -53,17 +59,21 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
} }
handleWebsocket(); handleWebsocket();
handleShadowTLS();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS();
} }
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS();
} }
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
handleShadowTLS();
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
@@ -73,26 +83,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.host", obfs.host); $set(proxy, "obfs-opts.host", obfs.host);
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
handleShadowTLS();
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/no_error_alert/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/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS();
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/no_error_alert/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/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS();
} }
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS();
} }
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/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/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS();
} }
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
handleShadowTLS();
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS();
} }
address = comma server:server comma port:port { address = comma server:server comma port:port {

View File

@@ -1,4 +1,4 @@
import { safeLoad } from 'static-js-yaml'; import { safeLoad } from '@/utils/yaml';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
function HTML() { function HTML() {

View File

@@ -14,6 +14,7 @@ import {
getFlowField, getFlowField,
getFlowHeaders, getFlowHeaders,
parseFlowHeaders, parseFlowHeaders,
validCheck,
flowTransfer, flowTransfer,
} from '@/utils/flow'; } from '@/utils/flow';
@@ -324,7 +325,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
const operator = createDynamicFunction( const operator = createDynamicFunction(
'operator', 'operator',
`async function operator(input = []) { `async function operator(input = []) {
if (input?.$files || input?.$content) { if (input && (input.$files || input.$content)) {
let { $content, $files } = input let { $content, $files } = input
${script} ${script}
return { $content, $files } return { $content, $files }
@@ -348,14 +349,14 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
} }
const DOMAIN_RESOLVERS = { const DOMAIN_RESOLVERS = {
Google: async function (domain) { Google: async function (domain, type) {
const id = hex_md5(`GOOGLE:${domain}`); const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent( url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain, domain,
)}&type=A`, )}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
@@ -389,14 +390,14 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Cloudflare: async function (domain) { Cloudflare: async function (domain, type) {
const id = hex_md5(`CLOUDFLARE:${domain}`); const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent( url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain, domain,
)}&type=A`, )}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
@@ -413,14 +414,14 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain) { Ali: async function (domain, type) {
const id = hex_md5(`ALI:${domain}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent( url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain, domain,
)}&type=A&short=1`, )}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
@@ -433,14 +434,14 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Tencent: async function (domain) { Tencent: async function (domain, type) {
const id = hex_md5(`ALI:${domain}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent( url: `http://119.28.28.28/d?type=${
domain, type === 'IPv6' ? 'AAAA' : 'A'
)}`, }&dn=${encodeURIComponent(domain)}`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
@@ -455,10 +456,13 @@ const DOMAIN_RESOLVERS = {
}, },
}; };
function ResolveDomainOperator({ provider }) { function ResolveDomainOperator({ provider, type, filter }) {
if (type === 'IPv6' && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 IPv6`);
}
const resolver = DOMAIN_RESOLVERS[provider]; const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) { if (!resolver) {
throw new Error(`Cannot find resolver: ${provider}`); throw new Error(`找不到域名解析服务提供方: ${provider}`);
} }
return { return {
name: 'Resolve Domain Operator', name: 'Resolve Domain Operator',
@@ -477,7 +481,7 @@ function ResolveDomainOperator({ provider }) {
const currentBatch = []; const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) { for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push( currentBatch.push(
resolver(domain) resolver(domain, type)
.then((ip) => { .then((ip) => {
results[domain] = ip; results[domain] = ip;
$.info( $.info(
@@ -504,7 +508,19 @@ function ResolveDomainOperator({ provider }) {
} }
}); });
return proxies; return proxies.filter((p) => {
if (filter === 'removeFailed') {
return p['no-resolve'] || p.resolved;
} else if (filter === 'IPOnly') {
return isIP(p.server);
} else if (filter === 'IPv4Only') {
return isIPv4(p.server);
} else if (filter === 'IPv6Only') {
return isIPv6(p.server);
} else {
return true;
}
});
}, },
}; };
} }
@@ -791,6 +807,7 @@ function createDynamicFunction(name, script, $arguments) {
getFlowHeaders, getFlowHeaders,
parseFlowHeaders, parseFlowHeaders,
flowTransfer, flowTransfer,
validCheck,
}; };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(

View File

@@ -107,6 +107,31 @@ export default function Clash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'h2'
) {
let path = proxy['h2-opts']?.path;
if (
isPresent(proxy, 'h2-opts.path') &&
Array.isArray(path)
) {
proxy['h2-opts'].path = path[0];
}
let host = proxy['h2-opts']?.headers?.host;
if (
isPresent(proxy, 'h2-opts.headers.Host') &&
!Array.isArray(host)
) {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
proxy['skip-cert-verify'];
}
}
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type, proxy.type,

View File

@@ -89,6 +89,18 @@ export default function ClashMeta_Producer() {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
delete proxy.sni; delete proxy.sni;
} }
} else if (proxy.type === 'ss') {
if (
isPresent(proxy, 'shadow-tls-password') &&
!isPresent(proxy, 'plugin')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
}
} }
if ( if (
@@ -110,7 +122,32 @@ export default function ClashMeta_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'h2'
) {
let path = proxy['h2-opts']?.path;
if (
isPresent(proxy, 'h2-opts.path') &&
Array.isArray(path)
) {
proxy['h2-opts'].path = path[0];
}
let host = proxy['h2-opts']?.headers?.host;
if (
isPresent(proxy, 'h2-opts.headers.Host') &&
!Array.isArray(host)
) {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
proxy['skip-cert-verify'];
}
}
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type, proxy.type,

View File

@@ -126,7 +126,31 @@ export default function ShadowRocket_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'h2'
) {
let path = proxy['h2-opts']?.path;
if (
isPresent(proxy, 'h2-opts.path') &&
Array.isArray(path)
) {
proxy['h2-opts'].path = path[0];
}
let host = proxy['h2-opts']?.headers?.host;
if (
isPresent(proxy, 'h2-opts.headers.Host') &&
!Array.isArray(host)
) {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
proxy['skip-cert-verify'];
}
}
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type, proxy.type,

View File

@@ -263,6 +263,37 @@ const socks5Parser = (proxy = {}) => {
return parsedProxy; return parsedProxy;
}; };
const shadowTLSParser = (proxy = {}) => {
const ssPart = {
tag: proxy.name,
type: 'shadowsocks',
method: proxy.cipher,
password: proxy.password,
detour: `${proxy.name}_shadowtls`,
};
const stPart = {
tag: `${proxy.name}_shadowtls`,
type: 'shadowtls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
version: proxy['plugin-opts'].version,
password: proxy['plugin-opts'].password,
tls: {
enabled: true,
server_name: proxy['plugin-opts'].host,
utls: {
enabled: true,
fingerprint: proxy['client-fingerprint'],
},
},
};
if (stPart.server_port < 1 || stPart.server_port > 65535)
throw '端口值非法';
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
tfoParser(proxy, stPart);
smuxParser(proxy.smux, ssPart);
return { type: 'ss-with-st', ssPart, stPart };
};
const ssParser = (proxy = {}) => { const ssParser = (proxy = {}) => {
const parsedProxy = { const parsedProxy = {
tag: proxy.name, tag: proxy.name,
@@ -607,9 +638,10 @@ export default function singbox_Producer() {
break; break;
case 'ss': case 'ss':
if (proxy.plugin === 'shadow-tls') { if (proxy.plugin === 'shadow-tls') {
throw new Error( const { ssPart, stPart } =
`Platform sing-box does not support proxy type: ${proxy.type} with shadow-tls`, shadowTLSParser(proxy);
); list.push(ssPart);
list.push(stPart);
} else { } else {
list.push(ssParser(proxy)); list.push(ssParser(proxy));
} }

View File

@@ -206,6 +206,31 @@ export default function Stash_Producer() {
proxy['http-opts'].headers.Host = [httpHost]; proxy['http-opts'].headers.Host = [httpHost];
} }
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'h2'
) {
let path = proxy['h2-opts']?.path;
if (
isPresent(proxy, 'h2-opts.path') &&
Array.isArray(path)
) {
proxy['h2-opts'].path = path[0];
}
let host = proxy['h2-opts']?.headers?.host;
if (
isPresent(proxy, 'h2-opts.headers.Host') &&
!Array.isArray(host)
) {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
proxy['skip-cert-verify'];
}
}
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( ['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type, proxy.type,

View File

@@ -6,6 +6,7 @@ const targetPlatform = 'Surfboard';
export default function Surfboard_Producer() { export default function Surfboard_Producer() {
const produce = (proxy) => { const produce = (proxy) => {
proxy.name = proxy.name.replace(/=/g, '');
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
return shadowsocks(proxy); return shadowsocks(proxy);

View File

@@ -69,7 +69,7 @@ function shadowsocks(proxy) {
`,obfs-uri=${proxy['plugin-opts'].path}`, `,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path', 'plugin-opts.path',
); );
} else { } else if (!['shadow-tls'].includes(proxy.plugin)) {
throw new Error(`plugin ${proxy.plugin} is not supported`); throw new Error(`plugin ${proxy.plugin} is not supported`);
} }
} }
@@ -95,6 +95,24 @@ function shadowsocks(proxy) {
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`, `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni', 'shadow-tls-sni',
); );
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
}
} }
// block-quic // block-quic

View File

@@ -39,6 +39,12 @@ export default function URI_Producer() {
); );
} }
} }
if (proxy['udp-over-tcp']) {
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
}
if (proxy.tfo) {
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
}
result += `#${encodeURIComponent(proxy.name)}`; result += `#${encodeURIComponent(proxy.name)}`;
break; break;
case 'ssr': case 'ssr':

View File

@@ -8,7 +8,7 @@ function HTML() {
function ClashProvider() { function ClashProvider() {
const name = 'Clash Provider'; const name = 'Clash Provider';
const test = (raw) => raw.indexOf('payload:') === 0; const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
const parse = (raw) => { const parse = (raw) => {
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, ''); return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
}; };

View File

@@ -1,4 +1,4 @@
import YAML from 'static-js-yaml'; import YAML from '@/utils/yaml';
function QXFilter() { function QXFilter() {
const type = 'SINGLE'; const type = 'SINGLE';

View File

@@ -54,9 +54,18 @@ async function doSync() {
if (artifact.sync) { if (artifact.sync) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
} }
} }

View File

@@ -35,13 +35,14 @@ export default function register($app) {
async function restoreArtifacts(_, res) { async function restoreArtifacts(_, res) {
$.info('开始恢复远程配置...'); $.info('开始恢复远程配置...');
try { try {
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
return Promise.reject('未设置 GitHub Token'); return Promise.reject('未设置 GitHub Token');
} }
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
try { try {
@@ -243,13 +244,14 @@ function validateArtifactName(name) {
} }
async function syncToGist(files) { async function syncToGist(files) {
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
return Promise.reject('未设置 GitHub Token'); return Promise.reject('未设置 GitHub Token');
} }
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
return manager.upload(files); return manager.upload(files);
} }

View File

@@ -16,6 +16,7 @@ import registerPreviewRoutes from './preview';
import registerSortingRoutes from './sort'; import registerSortingRoutes from './sort';
import registerMiscRoutes from './miscs'; import registerMiscRoutes from './miscs';
import registerNodeInfoRoutes from './node-info'; import registerNodeInfoRoutes from './node-info';
import registerParserRoutes from './parser';
export default function serve() { export default function serve() {
let port; let port;
@@ -38,6 +39,7 @@ export default function serve() {
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);
registerMiscRoutes($app); registerMiscRoutes($app);
registerParserRoutes($app);
$app.start(); $app.start();

View File

@@ -1,7 +1,7 @@
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 { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; import { updateArtifactStore, updateAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import { import {
GIST_BACKUP_FILE_NAME, GIST_BACKUP_FILE_NAME,
@@ -68,7 +68,7 @@ function getEnv(req, res) {
async function refresh(_, res) { async function refresh(_, res) {
// 1. get GitHub avatar and artifact store // 1. get GitHub avatar and artifact store
await updateGitHubAvatar(); await updateAvatar();
await updateArtifactStore(); await updateArtifactStore();
// 2. clear resource cache // 2. clear resource cache
@@ -79,7 +79,7 @@ async function refresh(_, res) {
async function gistBackup(req, res) { async function gistBackup(req, res) {
const { action } = req.query; const { action } = req.query;
// read token // read token
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
failed( failed(
res, res,
@@ -92,6 +92,7 @@ async function gistBackup(req, res) {
const gist = new Gist({ const gist = new Gist({
token: gistToken, token: gistToken,
key: GIST_BACKUP_KEY, key: GIST_BACKUP_KEY,
syncPlatform,
}); });
try { try {
let content; let content;

View File

@@ -0,0 +1,54 @@
import { success, failed } from '@/restful/response';
import { ProxyUtils } from '@/core/proxy-utils';
import { RuleUtils } from '@/core/rule-utils';
export default function register($app) {
$app.route('/api/proxy/parse').post(proxy_parser);
$app.route('/api/rule/parse').post(rule_parser);
}
/***
* 感谢 izhangxm 的 PR!
* 目前没有节点操作, 没有支持完整参数, 以后再完善一下
*/
/***
* 代理服务器协议转换接口。
* 请求方法为POST数据为json。需要提供data和client字段。
* data: string, 协议数据每行一个或者是clash
* client: string, 目标平台名称见backend/src/core/proxy-utils/producers/index.js
*
*/
function proxy_parser(req, res) {
const { data, client, content, platform } = req.body;
var result = {};
try {
var proxies = ProxyUtils.parse(data ?? content);
var par_res = ProxyUtils.produce(proxies, client ?? platform);
result['par_res'] = par_res;
} catch (err) {
failed(res, err);
return;
}
success(res, result);
}
/**
* 规则转换接口。
* 请求方法为POST数据为json。需要提供data和client字段。
* data: string, 多行规则字符串
* client: string, 目标平台名称具体见backend/src/core/rule-utils/producers.js
*/
function rule_parser(req, res) {
const { data, client, content, platform } = req.body;
var result = {};
try {
const rules = RuleUtils.parse(data ?? content);
var par_res = RuleUtils.produce(rules, client ?? platform);
result['par_res'] = par_res;
} catch (err) {
failed(res, err);
return;
}
success(res, result);
}

View File

@@ -1,5 +1,6 @@
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants'; import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
import { success } from './response'; import { success, failed } from './response';
import { InternalServerError } from '@/restful/errors';
import $ from '@/core/app'; import $ from '@/core/app';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
@@ -10,53 +11,105 @@ export default function register($app) {
} }
async function getSettings(req, res) { async function getSettings(req, res) {
let settings = $.read(SETTINGS_KEY); try {
if (!settings) { let settings = $.read(SETTINGS_KEY);
settings = {}; if (!settings) {
$.write(settings, SETTINGS_KEY); settings = {};
} $.write(settings, SETTINGS_KEY);
}
if (!settings.avatarUrl) await updateGitHubAvatar(); if (!settings.avatarUrl) await updateAvatar();
if (!settings.artifactStore) await updateArtifactStore(); if (!settings.artifactStore) await updateArtifactStore();
success(res, settings);
// TODO: 缺错误处理 前端也缺 success(res, settings);
} catch (e) {
$.error(`Failed to get settings: ${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_GET_SETTINGS`,
`Failed to get settings`,
`Reason: ${e.message ?? e}`,
),
);
}
} }
async function updateSettings(req, res) { async function updateSettings(req, res) {
const settings = $.read(SETTINGS_KEY); try {
const newSettings = { const settings = $.read(SETTINGS_KEY);
...settings, const newSettings = {
...req.body, ...settings,
}; ...req.body,
$.write(newSettings, SETTINGS_KEY); };
await updateGitHubAvatar(); $.write(newSettings, SETTINGS_KEY);
await updateArtifactStore(); await updateAvatar();
success(res, newSettings); await updateArtifactStore();
// TODO: 缺错误处理 前端也缺 success(res, newSettings);
} catch (e) {
$.error(`Failed to update settings: ${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_UPDATE_SETTINGS`,
`Failed to update settings`,
`Reason: ${e.message ?? e}`,
),
);
}
} }
export async function updateGitHubAvatar() { export async function updateAvatar() {
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
const username = settings.githubUser; const { githubUser: username, syncPlatform } = settings;
if (username) { if (username) {
try { if (syncPlatform === 'gitlab') {
const data = await $.http try {
.get({ const data = await $.http
url: `https://api.github.com/users/${username}`, .get({
headers: { url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
'User-Agent': username,
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', )}`,
}, headers: {
}) 'User-Agent':
.then((resp) => JSON.parse(resp.body)); 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
settings.avatarUrl = data['avatar_url']; },
$.write(settings, SETTINGS_KEY); })
} catch (err) { .then((resp) => JSON.parse(resp.body));
$.error( settings.avatarUrl = data[0]['avatar_url'].replace(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${ /(\?|&)s=\d+(&|$)/,
err.message ?? err '$1s=160$2',
}`, );
); $.write(settings, SETTINGS_KEY);
} catch (err) {
$.error(
`Failed to fetch GitLab avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
}
} else {
try {
const data = await $.http
.get({
url: `https://api.github.com/users/${encodeURIComponent(
username,
)}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
})
.then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY);
} catch (err) {
$.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
}
} }
} }
} }
@@ -64,19 +117,21 @@ export async function updateGitHubAvatar() {
export async function updateArtifactStore() { export async function updateArtifactStore() {
$.log('Updating artifact store'); $.log('Updating artifact store');
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
const { gistToken } = settings; const { gistToken, syncPlatform } = settings;
if (gistToken) { if (gistToken) {
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
try { try {
const gist = await manager.locate(); const gist = await manager.locate();
if (gist?.html_url) { const url = gist?.html_url ?? gist?.web_url;
$.log(`找到 Sub-Store Gist: ${gist.html_url}`); if (url) {
$.log(`找到 Sub-Store Gist: ${url}`);
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误 // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
settings.artifactStore = gist.html_url; settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID'; settings.artifactStoreStatus = 'VALID';
} else { } else {
$.error(`找不到 Sub-Store Gist`); $.error(`找不到 Sub-Store Gist`);

View File

@@ -492,9 +492,18 @@ async function syncArtifacts() {
if (artifact.sync) { if (artifact.sync) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
} }
} }
@@ -582,9 +591,16 @@ async function syncArtifact(req, res) {
}); });
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(files.map((item) => [item.path, item]));
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact); success(res, artifact);
} catch (err) { } catch (err) {

View File

@@ -4,7 +4,12 @@ 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 headersResourceCache from '@/utils/headers-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache';
import { getFlowField } from '@/utils/flow'; import {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
validCheck,
} from '@/utils/flow';
import $ from '@/core/app'; import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
@@ -64,36 +69,40 @@ export default async function download(rawUrl, ua, timeout) {
timeout: requestTimeout, timeout: requestTimeout,
}); });
const result = new Promise((resolve, reject) => { let result;
// try to find in app cache
const cached = resourceCache.get(id); // try to find in app cache
if (!$arguments?.noCache && cached) { const cached = resourceCache.get(id);
resolve(cached); if (!$arguments?.noCache && cached) {
} else { result = cached;
$.info( } else {
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`, $.info(
); `Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
http.get(url) );
.then((resp) => { try {
const { body, headers } = resp; const { body, headers } = await http.get(url);
if (headers) {
const flowInfo = getFlowField(headers); if (headers) {
if (flowInfo) { const flowInfo = getFlowField(headers);
headersResourceCache.set(url, flowInfo); if (flowInfo) {
} headersResourceCache.set(url, flowInfo);
} }
if (body.replace(/\s/g, '').length === 0) }
reject(new Error('远程资源内容为空!')); if (body.replace(/\s/g, '').length === 0)
else { throw new Error(new Error('远程资源内容为空'));
resourceCache.set(id, body);
resolve(body); resourceCache.set(id, body);
} result = body;
}) } catch (e) {
.catch(() => { throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
reject(new Error(`无法下载 URL${url}`));
});
} }
}); }
// 检查订阅有效性
if ($arguments?.validCheck) {
await validCheck(parseFlowHeaders(await getFlowHeaders(url)));
}
if (!isNode) { if (!isNode) {
tasks.set(id, result); tasks.set(id, result);

View File

@@ -88,17 +88,27 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
export function parseFlowHeaders(flowHeaders) { export function parseFlowHeaders(flowHeaders) {
if (!flowHeaders) return; if (!flowHeaders) return;
// unit is KB // unit is KB
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/); const uploadMatch = flowHeaders.match(
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const upload = Number(uploadMatch[1] + uploadMatch[2]); const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/); const downloadMatch = flowHeaders.match(
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const download = Number(downloadMatch[1] + downloadMatch[2]); const download = Number(downloadMatch[1] + downloadMatch[2]);
const totalMatch = flowHeaders.match(
const total = Number(flowHeaders.match(/total=(\d+)/)[1]); /total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const total = Number(totalMatch[1] + totalMatch[2]);
// optional expire timestamp // optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/); const expireMatch = flowHeaders.match(
const expires = match ? Number(match[1]) : undefined; /expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const expires = expireMatch
? Number(expireMatch[1] + expireMatch[2])
: undefined;
return { expires, total, usage: { upload, download } }; return { expires, total, usage: { upload, download } };
} }
@@ -110,3 +120,26 @@ export function flowTransfer(flow, unit = 'B') {
? { value: flow.toFixed(1), unit: unit } ? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]); : flowTransfer(flow / 1024, unitList[++unitIndex]);
} }
export function validCheck(flow) {
if (!flow) {
throw new Error('没有流量信息');
}
if (flow?.expires && flow.expires * 1000 < Date.now()) {
const date = new Date(flow.expires * 1000).toLocaleDateString();
throw new Error(`订阅已过期: ${date}`);
}
if (flow?.total) {
const upload = flow.usage?.upload || 0;
const download = flow.usage?.download || 0;
if (flow.total - upload - download < 0) {
const current = upload + download;
const currT = flowTransfer(Math.abs(current));
currT.value = current < 0 ? '-' + currT.value : currT.value;
const totalT = flowTransfer(flow.total);
throw new Error(
`流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
);
}
}
}

View File

@@ -4,64 +4,216 @@ import { HTTP } from '@/vendor/open-api';
* Gist backup * Gist backup
*/ */
export default class Gist { export default class Gist {
constructor({ token, key }) { constructor({ token, key, syncPlatform }) {
this.http = HTTP({ if (syncPlatform === 'gitlab') {
baseURL: 'https://api.github.com', this.headers = {
headers: { 'PRIVATE-TOKEN': `${token}`,
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
};
this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers },
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
const body = JSON.parse(resp.body);
return Promise.reject(
`ERROR: ${body.message?.error ?? body.message}`,
);
} else {
return resp;
}
},
},
});
} else {
this.headers = {
Authorization: `token ${token}`, Authorization: `token ${token}`,
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
}, };
events: { this.http = HTTP({
onResponse: (resp) => { baseURL: 'https://api.github.com',
if (/^[45]/.test(String(resp.statusCode))) { headers: { ...this.headers },
return Promise.reject( events: {
`ERROR: ${JSON.parse(resp.body).message}`, onResponse: (resp) => {
); if (/^[45]/.test(String(resp.statusCode))) {
} else { return Promise.reject(
return resp; `ERROR: ${JSON.parse(resp.body).message}`,
} );
} else {
return resp;
}
},
}, },
}, });
}); }
this.key = key; this.key = key;
this.syncPlatform = syncPlatform;
} }
async locate() { async locate() {
return this.http.get('/gists').then((response) => { if (this.syncPlatform === 'gitlab') {
const gists = JSON.parse(response.body); return this.http.get('/snippets').then((response) => {
for (let g of gists) { const gists = JSON.parse(response.body);
if (g.description === this.key) {
return g; for (let g of gists) {
if (g.title === this.key) {
return g;
}
} }
} return;
return; });
}); } else {
return this.http.get('/gists').then((response) => {
const gists = JSON.parse(response.body);
for (let g of gists) {
if (g.description === this.key) {
return g;
}
}
return;
});
}
} }
async upload(files) { async upload(input) {
if (Object.keys(files).length === 0) { if (Object.keys(input).length === 0) {
return Promise.reject('未提供需上传的文件'); return Promise.reject('未提供需上传的文件');
} }
const gist = await this.locate(); const gist = await this.locate();
let files = input;
if (gist?.id) { if (gist?.id) {
// update an existing gist if (this.syncPlatform === 'gitlab') {
return this.http.patch({ gist.files = gist.files.reduce((acc, item) => {
url: `/gists/${gist.id}`, acc[item.path] = item;
body: JSON.stringify({ files }), return acc;
}, {});
}
// console.log(`files`, files);
// console.log(`gist`, gist.files);
let actions = [];
const result = { ...gist.files };
Object.keys(files).map((key) => {
if (result[key]) {
if (
files[key].content == null ||
files[key].content === ''
) {
delete result[key];
actions.push({
action: 'delete',
file_path: key,
});
} else {
result[key] = files[key];
actions.push({
action: 'update',
file_path: key,
content: files[key].content,
});
}
} else {
if (
files[key].content == null ||
files[key].content === ''
) {
delete result[key];
delete files[key];
} else {
result[key] = files[key];
actions.push({
action: 'create',
file_path: key,
content: files[key].content,
});
}
}
}); });
console.log(`result`, result);
console.log(`files`, files);
console.log(`actions`, actions);
if (this.syncPlatform === 'gitlab') {
if (Object.keys(result).length === 0) {
return Promise.reject(
'本次操作将导致所有文件的内容都为空, 无法更新 snippet',
);
}
if (Object.keys(result).length > 10) {
return Promise.reject(
'本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
);
}
files = actions;
return this.http.put({
headers: {
...this.headers,
'Content-Type': 'application/json',
},
url: `/snippets/${gist.id}`,
body: JSON.stringify({ files }),
});
} else {
if (Object.keys(result).length === 0) {
return Promise.reject(
'本次操作将导致所有文件的内容都为空, 无法更新 gist',
);
}
return this.http.patch({
url: `/gists/${gist.id}`,
body: JSON.stringify({ files }),
});
}
} else { } else {
// create a new gist for backup files = Object.entries(files).reduce((acc, [key, file]) => {
return this.http.post({ if (file.content !== null && file.content !== '') {
url: '/gists', acc[key] = file;
body: JSON.stringify({ }
description: this.key, return acc;
public: false, }, {});
files, if (this.syncPlatform === 'gitlab') {
}), if (Object.keys(files).length === 0) {
}); return Promise.reject(
'所有文件的内容都为空, 无法创建 snippet',
);
}
files = Object.keys(files).map((key) => ({
file_path: key,
content: files[key].content,
}));
return this.http.post({
headers: {
...this.headers,
'Content-Type': 'application/json',
},
url: '/snippets',
body: JSON.stringify({
title: this.key,
visibility: 'private',
files,
}),
});
} else {
if (Object.keys(files).length === 0) {
return Promise.reject(
'所有文件的内容都为空, 无法创建 gist',
);
}
return this.http.post({
url: '/gists',
body: JSON.stringify({
description: this.key,
public: false,
files,
}),
});
}
} }
} }

29
backend/src/utils/yaml.js Normal file
View File

@@ -0,0 +1,29 @@
import YAML from 'static-js-yaml';
function retry(fn, content, ...args) {
try {
return fn(content, ...args);
} catch (e) {
return fn(content.replace(/!<str>/g, ''), ...args);
}
}
export function safeLoad(content, ...args) {
return retry(YAML.safeLoad, content, ...args);
}
export function load(content, ...args) {
return retry(YAML.load, content, ...args);
}
export function safeDump(...args) {
return YAML.safeDump(...args);
}
export function dump(...args) {
return YAML.dump(...args);
}
export default {
safeLoad,
load,
safeDump,
dump,
};

View File

@@ -314,6 +314,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
request[method.toLowerCase()]( request[method.toLowerCase()](
options, options,
(err, response, body) => { (err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
if (err) reject(err); if (err) reject(err);
else else
resolve({ resolve({

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 #!desc=高级订阅管理工具. 定时任务默认为每天 0 点
#!openUrl=https://sub.store #!openUrl=https://sub.store
#!author=Peng-YM #!author=Peng-YM
#!homepage=https://github.com/sub-store-org/Sub-Store #!homepage=https://github.com/sub-store-org/Sub-Store

View File

@@ -1,7 +1,7 @@
{ {
"name":"Sub-Store", "name": "Sub-Store",
"description":"", "description": "定时任务默认为每天 0 点",
"task":[ "task": [
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" "0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
] ]
} }

View File

@@ -12,6 +12,9 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。 安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge ### 2. Surge
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的分类和参数设置): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule) 2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)

View File

@@ -1,5 +1,5 @@
name: Sub-Store name: Sub-Store
desc: 高级订阅管理工具 @Peng-YM desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 0 点
http: http:
mitm: mitm:
@@ -33,4 +33,4 @@ script-providers:
cron-sync-artifacts: cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
interval: 86400 interval: 86400

View File

@@ -0,0 +1,15 @@
#!name=Sub-Store(β)
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 0 0 * * *
#!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:0 0 * * *,sync:"Sub-Store Sync"
#!arguments-desc="\n1⃣ ability\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n⚠ Surge 上时候可能会爆内存\n不需要使用的时候应该关闭\n填写任意其他值关闭\n\n2⃣ cronexp\n同步配置定时任务\n默认为每天 0 点\n\n3⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务"
[MITM]
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability="{{{ability}}}"
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -1,5 +1,6 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数 #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
#!category=订阅管理
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store

View File

@@ -1,5 +1,6 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本 #!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 0 点
#!category=订阅管理
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store

View File

@@ -1,5 +1,6 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数 #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
#!category=订阅管理
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store