Compare commits

..

21 Commits

Author SHA1 Message Date
xream
518de2e919 feat: 订阅支持每月重置天数 2024-03-10 23:08:56 +08:00
xream
078bf228de feat: produceArtifact 方法支持传入自定义 subscription; VLESS 非 reality 删除空 flow 2024-03-10 17:22:25 +08:00
xream
aaef97cf5d feat: SSH 新增 clash.meta(mihomo), 调整 Surge 和 sing-box 2024-03-08 19:01:01 +08:00
xream
7beff4013f feat: 订阅列表的流量信息兼容远程和本地合并的情况, 排除设置了不查询订阅信息的链接 2024-03-08 18:40:44 +08:00
xream
23cf81d0a5 feat: Node.js 版 /api/utils/env 增加 meta 信息 2024-03-08 14:20:55 +08:00
xream
572f2f5533 feat: OpenAPI 增加 isEgern, isLanceX; /api/utils/env 增加 meta 信息 2024-03-08 13:56:59 +08:00
xream
1c6d761e09 fix: 修复 Surge WireGuard allowed-ips 双引号 2024-03-07 17:24:49 +08:00
xream
437297b8b0 feat: 增加下载缓存阈值 2024-03-05 05:03:17 +08:00
xream
ca437865e6 feat: 域名解析新增 IP4P, 支持禁用缓存 2024-03-05 01:01:46 +08:00
xream
739100c873 feat: Stash/clash.meta(mihomo) 支持 interface-name 字段 2024-03-04 11:43:07 +08:00
xream
a4384f4f13 fix: 修复 Clash 节点名为 binary 的情况 2024-03-03 14:33:49 +08:00
xream
468d136f0e ci: git push assets to "release" branch 2024-02-28 23:07:16 +08:00
xream
b0c1157fe1 feat: 调整规则参数 2024-02-28 22:36:22 +08:00
xream
56626dabc7 Merge pull request #289 from makaspacex/master
修复IP-CIDR的option错误
2024-02-28 22:19:11 +08:00
makabaka
2a87f7b3c3 修复IP-CIDR的option错误 2024-02-28 20:06:03 +08:00
xream
81adfbc461 doc: README 2024-02-23 22:03:10 +08:00
xream
e04217c50d fix: 参数对象 2024-02-23 20:15:37 +08:00
xream
391a5aa2e4 chore: 调整默认定时为每天 23 点 55 分 2024-02-23 20:05:48 +08:00
xream
2f3b42f552 fix: Surge TUIC server-cert-fingerprint-sha256 2024-02-22 11:37:18 +08:00
xream
76302f9d53 fix: sing-box wireguard reserved 2024-02-21 19:09:23 +08:00
xream
1924e9735c feat: Surge 参数 tos allow-other-interface interface test-udp test-timeout hybrid; Stash 参数 benchmark-timeout; Surge 新增 SSH(仅密码方式), sing-box 新增 SSH 2024-02-18 05:25:31 +08:00
28 changed files with 675 additions and 87 deletions

View File

@@ -59,6 +59,17 @@ jobs:
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Git push assets to "release" branch
run: |
cd backend/dist || exit 1
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b release
git add .
git commit -m "release: ${{ steps.tag.outputs.release_tag }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin release
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}

View File

@@ -26,15 +26,13 @@ Core functionalities:
### Supported Input Formats
- [x] SS URI
- [x] SSR URI
- [x] SSD URI
- [x] V2RayN URI
- [x] Hysteria 2 URI
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5)
- [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria 2, SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)

View File

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

View File

@@ -1,6 +1,12 @@
import YAML from '@/utils/yaml';
import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber, isNotBlank } from '@/utils';
import {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
} from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
@@ -257,6 +263,10 @@ function safeMatch(parser, line) {
}
function lastParse(proxy) {
if (proxy.interface) {
proxy['interface-name'] = proxy.interface;
delete proxy.interface;
}
if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10);
}
@@ -360,6 +370,10 @@ function lastParse(proxy) {
delete proxy.ports;
}
if (['vless'].includes(proxy.type)) {
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
}
if (['http'].includes(proxy.network)) {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
if (!transportPath) {
@@ -370,6 +384,18 @@ function lastParse(proxy) {
}
}
}
if (typeof proxy.name !== 'string') {
try {
if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else {
proxy.name = utf8ArrayToStr(proxy.name);
}
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
}
return proxy;
}

View File

@@ -444,6 +444,7 @@ function URI_VLESS() {
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (proxy.network === 'tcp' && params.headerType === 'http') {
proxy.network = 'http';
@@ -722,6 +723,7 @@ function Clash_All() {
'hysteria',
'hysteria2',
'wireguard',
'ssh',
].includes(proxy.type)
) {
throw new Error(
@@ -752,6 +754,9 @@ function Clash_All() {
if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url'];
}
if (proxy['benchmark-timeout']) {
proxy['test-timeout'] = proxy['benchmark-timeout'];
}
return proxy;
};
@@ -1013,6 +1018,14 @@ function Loon_WireGuard() {
return { name, test, parse };
}
function Surge_SSH() {
const name = 'Surge SSH Parser';
const test = (line) => {
return /^.*=\s*ssh/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_SS() {
const name = 'Surge SS Parser';
const test = (line) => {
@@ -1183,6 +1196,7 @@ export default [
URI_Hysteria2(),
URI_Trojan(),
Clash_All(),
Surge_SSH(),
Surge_SS(),
Surge_VMess(),
Surge_Trojan(),

View File

@@ -37,11 +37,11 @@ const grammars = String.raw`
}
}
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/ssh) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/test_url/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,7 +52,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/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.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -63,21 +63,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/test_url/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/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/test_url/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -87,28 +91,28 @@ 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/test_url/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/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)* {
proxy.type = "tuic";
handleShadowTLS();
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/test_url/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/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)* {
proxy.type = "tuic";
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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
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/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)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
@@ -218,6 +222,15 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }

View File

@@ -35,11 +35,11 @@
}
}
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/ssh) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/test_url/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -50,7 +50,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/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.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -61,21 +61,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/test_url/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/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/test_url/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -85,28 +89,28 @@ 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/test_url/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/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)* {
proxy.type = "tuic";
handleShadowTLS();
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/test_url/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/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)* {
proxy.type = "tuic";
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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
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/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)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/underlying_proxy/test_url/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/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
@@ -216,6 +220,15 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }

View File

@@ -16,6 +16,7 @@ import {
parseFlowHeaders,
validCheck,
flowTransfer,
getRmainingDays,
} from '@/utils/flow';
/**
@@ -357,11 +358,41 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
};
}
function parseIP4P(IP4P) {
let server;
let port;
try {
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
throw new Error(`Invalid IP4P: ${IP4P}`);
}
let array = IP4P.split(':');
port = parseInt(array[2], 16);
let ipab = parseInt(array[3], 16);
let ipcd = parseInt(array[4], 16);
let ipa = ipab >> 8;
let ipb = ipab & 0xff;
let ipc = ipcd >> 8;
let ipd = ipcd & 0xff;
server = `${ipa}.${ipb}.${ipc}.${ipd}`;
if (port <= 0 || port > 65535) {
throw new Error(`Invalid port number: ${port}`);
}
if (!isIPv4(server)) {
throw new Error(`Invalid IP address: ${server}`);
}
} catch (e) {
// throw new Error(`IP4P 解析失败: ${e}`);
$.error(`IP4P 解析失败: ${e}`);
}
return { server, port };
}
const DOMAIN_RESOLVERS = {
Google: async function (domain, type) {
Google: async function (domain, type, noCache) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
@@ -382,10 +413,13 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain) {
'IP-API': async function (domain, type, noCache) {
if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
}
const id = hex_md5(`IP-API:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
@@ -399,10 +433,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain, type) {
Cloudflare: async function (domain, type, noCache) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain,
@@ -423,10 +457,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Ali: async function (domain, type) {
Ali: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
@@ -443,10 +477,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain, type) {
Tencent: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=${
type === 'IPv6' ? 'AAAA' : 'A'
@@ -465,10 +499,12 @@ const DOMAIN_RESOLVERS = {
},
};
function ResolveDomainOperator({ provider, type, filter }) {
if (type === 'IPv6' && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 IPv6`);
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`);
@@ -490,7 +526,7 @@ function ResolveDomainOperator({ provider, type, filter }) {
const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push(
resolver(domain, type)
resolver(domain, type, cache === 'disabled')
.then((ip) => {
results[domain] = ip;
$.info(
@@ -509,8 +545,19 @@ function ResolveDomainOperator({ provider, type, filter }) {
proxies.forEach((p) => {
if (!p['no-resolve']) {
if (results[p.server]) {
p.server = results[p.server];
p.resolved = true;
if (_type === 'IP4P') {
const { server, port } = parseIP4P(
results[p.server],
);
if (server && port) {
p.server = server;
p.port = port;
p.resolved = true;
}
} else {
p.server = results[p.server];
p.resolved = true;
}
} else {
p.resolved = false;
}
@@ -817,6 +864,7 @@ function createDynamicFunction(name, script, $arguments) {
parseFlowHeaders,
flowTransfer,
validCheck,
getRmainingDays,
};
if ($.env.isLoon) {
return new Function(

View File

@@ -217,6 +217,34 @@ const tlsParser = (proxy, parsedProxy) => {
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
const sshParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'ssh',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.user = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
// https://wiki.metacubex.one/config/proxies/ssh
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
if (proxy['server-fingerprint']) {
parsedProxy.host_key = [proxy['server-fingerprint']];
// https://manual.nssurge.com/policy/ssh.html
// Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)
// TODO: ?
parsedProxy.host_key_algorithms = [
proxy['server-fingerprint'].split(' ')[0],
];
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
return parsedProxy;
};
const httpParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
@@ -588,8 +616,10 @@ const wireguardParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved);
} else {
} else if (Array.isArray(proxy.reserved)) {
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
} else {
delete parsedProxy.reserved;
}
if (proxy.peers && proxy.peers.length > 0) {
parsedProxy.peers = [];
@@ -603,8 +633,10 @@ const wireguardParser = (proxy = {}) => {
};
if (typeof p.reserved === 'string') {
peer.reserved.push(p.reserved);
} else {
} else if (Array.isArray(p.reserved)) {
for (const r of p.reserved) peer.reserved.push(r);
} else {
delete peer.reserved;
}
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
parsedProxy.peers.push(peer);
@@ -624,6 +656,9 @@ export default function singbox_Producer() {
.map((proxy) => {
try {
switch (proxy.type) {
case 'ssh':
list.push(sshParser(proxy));
break;
case 'http':
list.push(httpParser(proxy));
break;

View File

@@ -250,6 +250,10 @@ export default function Stash_Producer() {
proxy['benchmark-url'] = proxy['test-url'];
delete proxy['test-url'];
}
if (proxy['test-timeout']) {
proxy['benchmark-timeout'] = proxy['test-timeout'];
delete proxy['test-timeout'];
}
delete proxy.subName;
delete proxy.collectionName;

View File

@@ -33,6 +33,8 @@ export default function Surge_Producer() {
return wireguard_surge(proxy);
case 'hysteria2':
return hysteria2(proxy);
case 'ssh':
return ssh(proxy);
}
if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
@@ -119,6 +121,21 @@ function shadowsocks(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -204,6 +221,21 @@ function trojan(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -278,6 +310,21 @@ function vmess(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -305,6 +352,71 @@ function vmess(proxy) {
return result.toString();
}
function ssh(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,${proxy.password}`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
result.appendIfPresent(
`,private-key=${proxy['keystore-private-key']}`,
'keystore-private-key',
);
result.appendIfPresent(
`,idle-timeout=${proxy['idle-timeout']}`,
'idle-timeout',
);
result.appendIfPresent(
`,server-fingerprint="${proxy['server-fingerprint']}"`,
'server-fingerprint',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function http(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
@@ -341,6 +453,21 @@ function http(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -406,6 +533,21 @@ function socks5(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -469,6 +611,21 @@ function snell(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -547,6 +704,21 @@ function tuic(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -609,6 +781,21 @@ ${proxy.name}=wireguard`);
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -663,7 +850,7 @@ private-key = ${proxy['private-key']}`);
}
const peer = {
'public-key': proxy['public-key'],
'allowed-ips': allowedIps,
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
endpoint: `${proxy.server}:${proxy.port}`,
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
'client-id': reserved,
@@ -696,6 +883,21 @@ function wireguard_surge(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -760,6 +962,21 @@ function hysteria2(proxy) {
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {

View File

@@ -9,7 +9,7 @@ const RULE_TYPES_MAPPING = [
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
[/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
];
function AllRuleParser() {

View File

@@ -30,8 +30,9 @@ function SurgeRuleSet() {
const type = 'SINGLE';
const func = (rule) => {
let output = `${rule.type},${rule.content}`;
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : '';
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
output +=
rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
}
return output;
};
@@ -44,6 +45,12 @@ function LoonRules() {
// skip unsupported rules
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
// Loon only supports the no-resolve option
rule.options = rule.options.filter((option) =>
['no-resolve'].includes(option),
);
}
return SurgeRuleSet().func(rule);
};
return { type, func };
@@ -62,8 +69,17 @@ function ClashRuleProvider() {
let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content
}`;
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : '';
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
if (rule.options) {
// Clash only supports the no-resolve option
rule.options = rule.options.filter((option) =>
['no-resolve'].includes(option),
);
}
output +=
rule.options?.length > 0
? `,${rule.options.join(',')}`
: '';
}
return output;
}),

View File

@@ -6,7 +6,11 @@ import {
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
import {
getFlowHeaders,
parseFlowHeaders,
getRmainingDays,
} from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
@@ -43,7 +47,10 @@ async function getFlowInfo(req, res) {
);
return;
}
if (sub.source === 'local') {
if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
failed(
res,
new RequestInvalidError(
@@ -55,7 +62,42 @@ async function getFlowInfo(req, res) {
return;
}
try {
const flowHeaders = await getFlowHeaders(sub.url);
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if ($arguments.noFlow) {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Subscription ${name}: noFlow`,
),
);
return;
}
const flowHeaders = await getFlowHeaders(url);
if (!flowHeaders) {
failed(
res,
@@ -67,8 +109,10 @@ async function getFlowInfo(req, res) {
);
return;
}
success(res, parseFlowHeaders(flowHeaders));
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays($arguments.resetDay),
});
} catch (err) {
failed(
res,

View File

@@ -35,13 +35,21 @@ async function produceArtifact({
ignoreFailedRemoteFile,
produceType,
produceOpts = {},
subscription,
}) {
platform = platform || 'JSON';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
let sub;
if (name) {
const allSubs = $.read(SUBS_KEY);
sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
} else if (subscription) {
sub = subscription;
} else {
throw new Error('未提供订阅名称或订阅数据');
}
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;

View File

@@ -53,7 +53,8 @@ export default async function download(rawUrl, ua, timeout) {
// }
const { isNode } = ENV();
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
@@ -90,8 +91,22 @@ export default async function download(rawUrl, ua, timeout) {
}
if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空'));
let shouldCache = true;
if (cacheThreshold) {
const size = body.length / 1024;
if (size > cacheThreshold) {
$.info(
`资源大小 ${size.toFixed(
2,
)} KB 超过了 ${cacheThreshold} KB, 不缓存`,
);
shouldCache = false;
}
}
if (shouldCache) {
resourceCache.set(id, body);
}
resourceCache.set(id, body);
result = body;
} catch (e) {
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);

View File

@@ -1,7 +1,16 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
const {
isNode,
isQX,
isLoon,
isSurge,
isStash,
isShadowRocket,
isLanceX,
isEgern,
} = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
@@ -9,8 +18,44 @@ if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
if (isEgern) backend = 'Egern';
if (isLanceX) backend = 'LanceX';
let meta = {};
try {
if (typeof $environment !== 'undefined') {
// eslint-disable-next-line no-undef
meta.env = $environment;
}
if (typeof $loon !== 'undefined') {
// eslint-disable-next-line no-undef
meta.loon = $loon;
}
if (typeof $script !== 'undefined') {
// eslint-disable-next-line no-undef
meta.script = $script;
}
if (isNode) {
meta.node = {
version: eval('process.version'),
argv: eval('process.argv'),
filename: eval('__filename'),
dirname: eval('__dirname'),
env: {},
};
const env = eval('process.env');
for (const key in env) {
if (/^SUB_STORE_/.test(key)) {
meta.node.env[key] = env[key];
}
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
export default {
backend,
version: substoreVersion,
meta,
};

View File

@@ -143,3 +143,23 @@ export function validCheck(flow) {
}
}
}
export function getRmainingDays(_resetDay) {
if (!_resetDay) return;
const resetDay = parseInt(_resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31) return;
let now = new Date();
let today = now.getDate();
let month = now.getMonth();
let year = now.getFullYear();
let daysInMonth;
if (resetDay > today) {
daysInMonth = 0;
} else {
daysInMonth = new Date(year, month + 1, 0).getDate();
}
return daysInMonth - today + resetDay;
}

View File

@@ -35,6 +35,53 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue;
}
const utf8ArrayToStr =
typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v))
: (function () {
var charCache = new Array(128); // Preallocate the cache for the common single byte chars
var charFromCodePt = String.fromCodePoint || String.fromCharCode;
var result = [];
return function (array) {
var codePt, byte1;
var buffLen = array.length;
result.length = 0;
for (var i = 0; i < buffLen; ) {
byte1 = array[i++];
if (byte1 <= 0x7f) {
codePt = byte1;
} else if (byte1 <= 0xdf) {
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
} else if (byte1 <= 0xef) {
codePt =
((byte1 & 0x0f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else if (String.fromCodePoint) {
codePt =
((byte1 & 0x07) << 18) |
((array[i++] & 0x3f) << 12) |
((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f);
} else {
codePt = 63; // Cannot convert four byte code points, so use "?" instead
i += 3;
}
result.push(
charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)),
);
}
return result.join('');
};
})();
export {
isIPv4,
isIPv6,
@@ -43,4 +90,5 @@ export {
getIfNotBlank,
isPresent,
getIfPresent,
utf8ArrayToStr,
};

View File

@@ -6,6 +6,8 @@ const isNode = eval(`typeof process !== "undefined"`); // eval is needed in orde
const isStash =
'undefined' !== typeof $environment && $environment['stash-version'];
const isShadowRocket = 'undefined' !== typeof $rocket;
const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native;
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
@@ -251,7 +253,16 @@ export class OpenAPI {
}
export function ENV() {
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket };
return {
isQX,
isLoon,
isSurge,
isNode,
isStash,
isShadowRocket,
isEgern,
isLanceX,
};
}
export function HTTP(defaultOptions = { baseURL: '' }) {
@@ -312,7 +323,7 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
? eval("require('request')")
: $httpClient;
request[method.toLowerCase()](
options,
JSON.parse(JSON.stringify(options)),
(err, response, body) => {
// if (err) {
// console.log(err);

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具. 定时任务默认为每天 0 点
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分
#!openUrl=https://sub.store
#!author=Peng-YM
#!homepage=https://github.com/sub-store-org/Sub-Store
@@ -17,4 +17,4 @@ hostname=sub.store
http-request ^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, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync

View File

@@ -1,7 +1,7 @@
{
"name": "Sub-Store",
"description": "定时任务默认为每天 0 点",
"description": "定时任务默认为每天 23 点 55 分",
"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"
"55 23 * * * 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

@@ -19,6 +19,8 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
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)
> 最新 Surge iOS TestFlight 版本应该没有内存问题了 可以大胆尝试带 ability 参数版本
3. 固定不带 ability 参数版本: [`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)

View File

@@ -1,5 +1,5 @@
name: Sub-Store
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 0 点
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分
icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
http:
@@ -20,7 +20,7 @@ http:
cron:
script:
- name: cron-sync-artifacts
cron: "0 0 * * *"
cron: "55 23 * * *"
timeout: 120
script-providers:

View File

@@ -1,8 +1,8 @@
#!name=Sub-Store(β)
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 0 0 * * *
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!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若设为 # 可取消定时任务"
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,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默认为每天 23 点 55 分\n\n3⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务"
[MITM]
hostname = %APPEND% sub.store

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!category=订阅管理
[MITM]
@@ -10,4 +10,4 @@ hostname = %APPEND% sub.store
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
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
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=55 23 * * *,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,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 0 点
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
#!category=订阅管理
[MITM]
@@ -9,4 +9,4 @@ hostname = %APPEND% sub.store
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=http-client-policy
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
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=55 23 * * *,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,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!category=订阅管理
[MITM]
@@ -9,4 +9,4 @@ hostname = %APPEND% sub.store
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
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
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js