Compare commits

...

58 Commits

Author SHA1 Message Date
xream
75012503f8 fix: 修复 Clash Pre-processor 2025-01-08 19:49:34 +08:00
xream
85a3e2ee54 feat: 文件支持 Mihomo 配置, 支持使用覆写; target 名称适配大小写和别名
Some checks failed
build / build (push) Has been cancelled
2025-01-03 23:37:50 +08:00
xream
95b7557635 feat: Loon 正式支持 Shadowsocks 2022
Some checks failed
build / build (push) Has been cancelled
2024-12-31 23:24:35 +08:00
xream
14ca62db4a doc: demo.js
Some checks are pending
build / build (push) Waiting to run
2024-12-31 15:28:32 +08:00
xream
a2a754adb7 feat: sing-box 支持使用 _network 字段来设置 network 2024-12-31 15:27:14 +08:00
xream
6b23f82953 fix: 修复代理 App 版中路由 target 参数为空的情况
Some checks are pending
build / build (push) Waiting to run
2024-12-30 21:36:45 +08:00
xream
e071a7f253 feat: 组合订阅和文件的导出导入 2024-12-30 21:21:18 +08:00
xream
b9bba895e1 feat: 支持订阅级别的 noCache 2024-12-29 23:37:07 +08:00
xream
8090d678ee feat: 分享支持多一级路由指定输出目标 2024-12-29 22:08:24 +08:00
xream
ff4be7ac38 feat: 订阅支持开关 passThroughUA 透传请求的 User-Agent 2024-12-29 21:33:15 +08:00
xream
7e2109dc68 feat: 支持订阅参数 url 同时支持单条本地节点内容, 支持多一级路由指定输出目标 2024-12-29 21:03:52 +08:00
xream
278beae99a feat: 支持 Egern 前置代理 prev_hop 和 Hysteria2 端口跳跃 2024-12-29 20:05:55 +08:00
xream
3aedd5943d feat: sing-box includeUnsupportedProxy 开启支持 Hysteria2 端口跳跃 2024-12-29 16:07:33 +08:00
xream
222551eb20 feat: Egern 增加默认 sni 2024-12-28 21:00:40 +08:00
xream
0d5e1ab38b feat: 下载订阅的日志中增加请求 target 和实际输出 2024-12-28 17:42:26 +08:00
xream
a3ec98caa9 feat: Clash 订阅仅缓存 proxies 数据 2024-12-27 21:55:13 +08:00
xream
d9e4d814bb feat: geo 更新 2024-12-27 21:35:51 +08:00
xream
e843aa3702 feat: geo 更新 2024-12-26 03:40:53 +08:00
xream
66464645f2 feat: UDP 协议跳过设置 utls 2024-12-24 21:43:23 +08:00
xream
9ccd6b3816 doc: demo.js 2024-12-24 20:49:41 +08:00
xream
74be1e3d82 doc: README 2024-12-24 15:10:38 +08:00
xream
6d78eb7356 feat: Clash 系输入支持 mieru; 调整 juicity 和 mieru 相关过滤逻辑 2024-12-24 15:08:28 +08:00
xream
38eccca8b4 feat: 组合订阅支持手动设置流量信息. 支持使用链接. 此时使用响应内容 2024-12-24 01:20:38 +08:00
xream
33e5aeceb5 fix: 修复订阅不存在时不打印错误日志的问题 2024-12-23 14:14:10 +08:00
xream
837667edc9 feat: 手动设置流量信息时, 支持使用链接. 此时使用响应内容 2024-12-22 21:57:01 +08:00
xream
0069b0ce83 feat: sing-box 支持 detour 参数(之前只能用 underlying-proxy 或 dialer-proxy 来设置) 2024-12-22 20:06:00 +08:00
xream
fcc9d047ae fix: 修复 edns sourcePrefixLength 2024-12-21 21:13:09 +08:00
xream
382d22e622 feat: 支持 socks5, socks5+tls, http, https(便于输入) 格式输入 2024-12-16 21:06:46 +08:00
xream
06f3e97af2 feat: 支持 Shadowsocks 2022 的 URI 输入/输出 2024-12-15 23:03:41 +08:00
xream
bd87e9231e fix: 修复 Surge SOCKS5 解析 2024-12-13 02:27:03 +08:00
xream
d1d6d19542 feat: Mihomo 支持 direct 2024-12-12 18:35:55 +08:00
xream
08bf0b78bb feat: Surge 支持 direct 2024-12-12 18:22:25 +08:00
xream
9a3cd4f57c feat: 处理状态码 2024-12-12 15:35:19 +08:00
xream
d015c7867e fix: 修复 SS URI 解析 2024-12-08 11:24:46 +08:00
xream
4713b63083 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2024-12-05 23:50:51 +08:00
xream
dbf9e7c360 feat: 优化去除无效节点逻辑 感谢群友 Cooip JM 2024-12-05 12:45:07 +08:00
xream
4ea84118c4 feat: gRPC 支持 authority 2024-12-05 00:52:11 +08:00
xream
dda8113a42 feat: 增加 subscription-userinfo 兼容性 2024-12-04 00:56:53 +08:00
xream
f16b2d34f1 feat: geo 更新 2024-11-30 13:58:04 +08:00
xream
5b28e1a4c9 feat: 支持禁用节点操作 2024-11-29 21:03:29 +08:00
xream
8d0a71d983 feat: VMess URI 输出支持 alterId; Trojan 支持 fp 和 alpn 2024-11-28 16:04:52 +08:00
xream
815552d470 feat: 找不到资源时不通知, 仅保留日志 2024-11-28 15:44:55 +08:00
xream
9d90369594 feat: Trojan URI 支持省略端口号 2024-11-28 13:15:22 +08:00
xream
6aece471aa feat: Stash 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2024-11-27 15:20:20 +08:00
xream
99396773f6 ci: 去除 GitLab Sync 2024-11-26 15:01:04 +08:00
xream
e229408a2d feat: 默认缓存阈值 1024KB 2024-11-24 12:31:18 +08:00
xream
514414587b feat: 默认超时 8000ms 2024-11-24 12:13:52 +08:00
egerndaddy
d4c419745e Update Egern.yaml 2024-11-22 23:59:59 +08:00
xream
fe3da254f4 feat: 支持 Egern 输出 2024-11-21 13:16:04 +08:00
xream
7d8132d7cd feat: 默认输出格式改为 V2Ray; accept 为 application/json 时, 输出 JSON; 响应增加 X-Powered-By Sub-Store 2024-11-19 23:06:45 +08:00
xream
bc1247efaf feat: 手动设置的订阅流量信息会附加到订阅自己的流量信息之前 2024-11-17 23:40:03 +08:00
xream
dea937df66 feat: 默认查询流量信息的 User-AgentQuantumult%20X/1.0.30 (iPhone14,2; iOS 15.6) 改为 clash; 流量信息缓存逻辑调整 2024-11-17 02:10:38 +08:00
xream
cfb5a8e082 feat: 支持解析订阅中的 profile-web-page-url 字段 2024-11-17 01:02:28 +08:00
xream
4790bf47d1 feat: Surge 密码解析支持首尾成对的单引号双引号, 输出时增加双引号 2024-11-16 21:50:52 +08:00
xream
56fd495fb6 feat: 支持更多的 subscription-userinfo 2024-11-12 22:20:46 +08:00
xream
f4639d9a34 feat: 支持更多的 subscription-userinfo 2024-11-12 22:06:22 +08:00
xream
cc58a5541e feat: 订阅刷新按钮逻辑调整为无缓存刷新订阅和流量 2024-11-10 01:22:48 +08:00
xream
772f431887 feat: 模块版文件中增加 token 路由 2024-11-08 18:10:39 +08:00
41 changed files with 1257 additions and 243 deletions

View File

@@ -76,8 +76,8 @@ jobs:
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 }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline
# - name: Sync to GitLab
# env:
# GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
# run: |
# curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

View File

@@ -7,7 +7,7 @@
</div>
<p align="center" color="#6a737d">
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
</p>
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
@@ -28,14 +28,18 @@ Core functionalities:
> ⚠️ Do not use `Shadowrocket` to export URI and then import it as input. It is not a standard URI.
- [x] Normal Proxy(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
example: `socks5+tls://user:pass@ip:port#name`
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [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, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
@@ -49,6 +53,7 @@ Core functionalities:
- [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
- [x] Loon
- [x] Egern
- [x] Shadowrocket
- [x] QX
- [x] sing-box

View File

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

View File

@@ -86,6 +86,16 @@ async function processFn(
$options,
) {
for (const item of operators) {
if (item.disabled) {
$.log(
`Skipping disabled operator: "${
item.type
}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None'
}`,
);
continue;
}
// process script
let script;
let $arguments = {};

View File

@@ -27,6 +27,42 @@ function surge_port_hopping(raw) {
};
}
function URI_PROXY() {
// socks5+tls
// socks5
// http, https(可以这么写)
const name = 'URI PROXY Parser';
const test = (line) => {
return /^(socks5\+tls|socks5|http|https):\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, tls, username, password, server, port, query, name] =
line.match(
/^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?):(\d+?)(\?.*?)?(?:#(.*?))?$/,
);
const proxy = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type,
tls: tls ? true : false,
server,
port,
username:
username != null ? decodeURIComponent(username) : undefined,
password:
password != null ? decodeURIComponent(password) : undefined,
};
return proxy;
};
return { name, test, parse };
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() {
@@ -46,7 +82,15 @@ function URI_SS() {
content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大
let userInfoStr;
if (rawUserInfoStr?.startsWith('2022-blake3-')) {
userInfoStr = rawUserInfoStr;
} else {
userInfoStr = Base64.decode(rawUserInfoStr);
}
let query = '';
if (!serverAndPortArray) {
if (content.includes('?')) {
@@ -71,16 +115,21 @@ function URI_SS() {
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
}
const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
/\d+/,
)?.[0];
const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo[1];
proxy.password = userInfo[2];
let userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo?.[1];
proxy.password = userInfo?.[2];
// if (!proxy.cipher || !proxy.password) {
// userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/);
// proxy.cipher = userInfo?.[1];
// proxy.password = userInfo?.[2];
// }
// handle obfs
const idx = content.indexOf('?plugin=');
@@ -340,6 +389,10 @@ function URI_VMess() {
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
}
// 暂不支持 tcp + host + path
// else if (params.net === 'tcp' || proxy.network === 'tcp') {
// proxy.network = 'tcp';
// }
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {
@@ -376,6 +429,7 @@ function URI_VMess() {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
'_grpc-authority': getIfNotBlank(params.authority),
};
} else {
const opts = {
@@ -511,6 +565,9 @@ function URI_VLESS() {
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
if (['grpc'].includes(proxy.network) && params.authority) {
opts['_grpc-authority'] = params.authority;
}
} else if (isShadowrocket && params.path) {
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
opts[`${proxy.network}-service-name`] = params.path;
@@ -838,6 +895,11 @@ function URI_Trojan() {
};
const parse = (line) => {
const matched = /^(trojan:\/\/.*?@.*?)(:(\d+))?\/?(\?.*?)?$/.exec(line);
const port = matched?.[2];
if (!port) {
line = line.replace(matched[1], `${matched[1]}:443`);
}
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser();
const proxy = parser.parse(newLine);
@@ -867,6 +929,8 @@ function Clash_All() {
const proxy = JSON.parse(line);
if (
![
'mieru',
'juicity',
'ss',
'ssr',
'vmess',
@@ -880,6 +944,7 @@ function Clash_All() {
'hysteria2',
'wireguard',
'ssh',
'direct',
].includes(proxy.type)
) {
throw new Error(
@@ -1178,6 +1243,14 @@ function Loon_WireGuard() {
return { name, test, parse };
}
function Surge_Direct() {
const name = 'Surge Direct Parser';
const test = (line) => {
return /^.*=\s*direct/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_SSH() {
const name = 'Surge SSH Parser';
const test = (line) => {
@@ -1357,6 +1430,7 @@ function isIP(ip) {
}
export default [
URI_PROXY(),
URI_SS(),
URI_SSR(),
URI_VMess(),
@@ -1367,6 +1441,7 @@ export default [
URI_Hysteria2(),
URI_Trojan(),
Clash_All(),
Surge_Direct(),
Surge_SSH(),
Surge_SS(),
Surge_VMess(),

View File

@@ -120,7 +120,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
username = & {
let j = peg$currPos;

View File

@@ -118,7 +118,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
username = & {
let j = peg$currPos;

View File

@@ -37,7 +37,7 @@ const grammars = String.raw`
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
return proxy;
}
@@ -108,15 +108,18 @@ hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying
proxy.type = "hysteria2";
handleShadowTLS();
}
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)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/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/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)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/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();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
@@ -193,7 +196,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }

View File

@@ -35,7 +35,7 @@
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
return proxy;
}
@@ -106,16 +106,18 @@ hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying
proxy.type = "hysteria2";
handleShadowTLS();
}
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)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/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/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)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/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();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
proxy.port = port;
@@ -191,7 +193,7 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }

View File

@@ -82,6 +82,8 @@ port = digits:[0-9]+ {
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
if (toBool(params["ws"])) {
proxy.network = "ws";
@@ -99,6 +101,7 @@ params = "?" head:param tail:("&"@param)* {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
'_grpc-authority': params["authority"],
};
} else {
if (params["path"]) {

View File

@@ -80,6 +80,8 @@ port = digits:[0-9]+ {
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
if (toBool(params["ws"])) {
proxy.network = "ws";
@@ -97,6 +99,7 @@ params = "?" head:param tail:("&"@param)* {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
'_grpc-authority': params["authority"],
};
} else {
if (params["path"]) {

View File

@@ -48,7 +48,7 @@ function Clash() {
const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies);
};
const parse = function (raw) {
const parse = function (raw, includeProxies) {
// Clash YAML format
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
@@ -56,35 +56,40 @@ function Clash() {
const afterReplace = raw.replace(
/short-id:([ ]*[^,\n}]*)/g,
(matched, value) => {
const afterTrim = value.trim();
// 为空
if (!afterTrim || afterTrim === '') {
return 'short-id: ""'
}
// 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`;
} else {
return `short-id: "${afterTrim}"`
}
}
const afterTrim = value.trim();
// 为空
if (!afterTrim || afterTrim === '') {
return 'short-id: ""';
}
// 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`;
} else {
return `short-id: "${afterTrim}"`;
}
},
);
const {
proxies,
'global-client-fingerprint': globalClientFingerprint,
} = safeLoad(afterReplace);
return proxies
.map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
if (globalClientFingerprint && !p['client-fingerprint']) {
p['client-fingerprint'] = globalClientFingerprint;
}
return JSON.stringify(p);
})
.join('\n');
return (
(includeProxies ? 'proxies:\n' : '') +
proxies
.map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
if (globalClientFingerprint && !p['client-fingerprint']) {
p['client-fingerprint'] = globalClientFingerprint;
}
return `${includeProxies ? ' - ' : ''}${JSON.stringify(
p,
)}\n`;
})
.join('')
);
};
return { name, test, parse };
}

View File

@@ -10,6 +10,7 @@ import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import YAML from '@/utils/yaml';
import env from '@/utils/env';
import {
@@ -21,6 +22,46 @@ import {
getRmainingDays,
} from '@/utils/flow';
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function trimWrap(str) {
if (str.startsWith('<') && str.endsWith('>')) {
return str.slice(1, -1);
}
return str;
}
function deepMerge(target, _other) {
const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
for (const key in other) {
if (isObject(other[key])) {
if (key.endsWith('!')) {
const k = trimWrap(key.slice(0, -1));
target[k] = other[key];
} else {
const k = trimWrap(key);
if (!target[k]) Object.assign(target, { [k]: {} });
deepMerge(target[k], other[k]);
}
} else if (Array.isArray(other[key])) {
if (key.startsWith('+')) {
const k = trimWrap(key.slice(1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...other[key], ...target[k]];
} else if (key.endsWith('+')) {
const k = trimWrap(key.slice(0, -1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...target[k], ...other[key]];
} else {
const k = trimWrap(key);
Object.assign(target, { [k]: other[key] });
}
} else {
Object.assign(target, { [key]: other[key] });
}
}
return target;
}
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
@@ -321,6 +362,33 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
name: 'Script Operator',
func: async (proxies) => {
let output = proxies;
if (output?.$file?.type === 'mihomoProfile') {
try {
let patch = YAML.safeLoad(script);
if (typeof patch !== 'object') patch = {};
output.$content = ProxyUtils.yaml.safeDump(
deepMerge(
{
proxies: await produceArtifact({
type:
output?.$file?.sourceType ||
'collection',
name: output?.$file?.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true,
},
}),
},
patch,
),
);
return output;
} catch (e) {
// console.log(e);
}
}
await (async function () {
const operator = createDynamicFunction(
'operator',
@@ -339,9 +407,27 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
'operator',
`async function operator(input = []) {
if (input && (input.$files || input.$content)) {
let { $content, $files, $options } = input
${script}
return { $content, $files, $options }
let { $content, $files, $options, $file } = input
if($file.type === 'mihomoProfile') {
${script}
if(typeof main === 'function') {
const config = {
proxies: await produceArtifact({
type: $file.sourceType || 'collection',
name: $file.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true
}
}),
}
$content = ProxyUtils.yaml.safeDump(await main(config))
}
} else {
${script}
}
return { $content, $files, $options, $file }
} else {
let proxies = input
let list = []
@@ -561,7 +647,7 @@ function ResolveDomainOperator({
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
const requestTimeout = timeout || defaultTimeout || 8000;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
@@ -699,24 +785,45 @@ function isIP(ip) {
ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
function isAscii(str) {
// eslint-disable-next-line no-control-regex
var pattern = /^[\x00-\x7F]+$/; // ASCII 范围的 Unicode 编码
return pattern.test(str);
}
/**************************** Filters ***************************************/
// filter useless proxies
function UselessFilter() {
const KEYWORDS = [
'网址',
'流量',
'时间',
'应急',
'过期',
'Bandwidth',
'expire',
];
return {
name: 'Useless Filter',
func: RegexFilter({
regex: KEYWORDS,
keep: false,
}).func,
func: (proxies) => {
return proxies.map((proxy) => {
if (proxy.cipher && !isAscii(proxy.cipher)) {
return false;
} else if (proxy.password && !isAscii(proxy.password)) {
return false;
} else {
if (proxy.network) {
let transportHosts =
proxy[`${proxy.network}-opts`]?.headers?.Host ||
proxy[`${proxy.network}-opts`]?.headers?.host;
transportHosts = Array.isArray(transportHosts)
? transportHosts
: [transportHosts];
if (
transportHosts.some(
(host) => host && !isAscii(host),
)
) {
return false;
}
}
return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test(
proxy.name,
);
}
});
},
};
}

View File

@@ -175,6 +175,7 @@ export default function Clash_Producer() {
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -8,6 +8,8 @@ export default function ClashMeta_Producer() {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
} else if (['juicity'].includes(proxy.type)) {
return false;
}
return true;
})
@@ -178,7 +180,7 @@ export default function ClashMeta_Producer() {
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
if (type !== 'internal' || opts['delete-underscore-fields']) {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
@@ -190,6 +192,7 @@ export default function ClashMeta_Producer() {
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -0,0 +1,310 @@
export default function Egern_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'http',
'socks5',
'ss',
'trojan',
'hysteria2',
'vless',
'vmess',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
((proxy.plugin === 'obfs' &&
!['http', 'tls'].includes(
proxy['plugin-opts']?.mode,
)) ||
![
'chacha20-ietf-poly1305',
'chacha20-poly1305',
'aes-256-gcm',
'aes-128-gcm',
'none',
'tbale',
'rc4',
'rc4-md5',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'cast5-cfb',
'des-cfb',
'idea-cfb',
'rc2-cfb',
'seed-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
].includes(proxy.cipher))) ||
(proxy.type === 'vmess' &&
(![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
'zero',
].includes(proxy.cipher) ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) ||
(proxy.type === 'trojan' &&
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts'] ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network)))
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server;
}
const prev_hop =
proxy.prev_hop ||
proxy['underlying-proxy'] ||
proxy['dialer-proxy'] ||
proxy.detour;
if (proxy.type === 'http') {
proxy = {
type: 'http',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'socks5') {
proxy = {
type: 'socks5',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'ss') {
proxy = {
type: 'shadowsocks',
name: proxy.name,
method:
proxy.cipher === 'chacha20-ietf-poly1305'
? 'chacha20-poly1305'
: proxy.cipher,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
if (proxy.plugin === 'obfs') {
proxy.obfs = proxy['plugin-opts'].mode;
proxy.obfs_host = proxy['plugin-opts'].host;
proxy.obfs_uri = proxy['plugin-opts'].path;
}
} else if (proxy.type === 'hysteria2') {
proxy = {
type: 'hysteria2',
name: proxy.name,
server: proxy.server,
port: proxy.port,
auth: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
proxy.obfs = 'salamander';
proxy.obfs_password = proxy['obfs-password'];
}
} else if (proxy.type === 'trojan') {
if (proxy.network === 'ws') {
proxy.websocket = {
path: proxy['ws-opts']?.path,
host: proxy['ws-opts']?.headers?.Host,
};
}
proxy = {
type: 'trojan',
name: proxy.name,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
websocket: proxy.websocket,
};
} else if (proxy.type === 'vmess') {
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vmess',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
} else if (proxy.type === 'vless') {
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vless',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
return {
[proxy.type]: {
...proxy,
type: undefined,
prev_hop,
},
};
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -10,6 +10,7 @@ import QX_Producer from './qx';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
import Egern_Producer from './egern';
function JSON_Producer() {
const type = 'ALL';
@@ -19,19 +20,37 @@ function JSON_Producer() {
}
export default {
qx: QX_Producer(),
QX: QX_Producer(),
QuantumultX: QX_Producer(),
surge: Surge_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
Clash: Clash_Producer(),
meta: ClashMeta_Producer(),
clashmeta: ClashMeta_Producer(),
'clash.meta': ClashMeta_Producer(),
'Clash.Meta': ClashMeta_Producer(),
ClashMeta: ClashMeta_Producer(),
mihomo: ClashMeta_Producer(),
Mihomo: ClashMeta_Producer(),
uri: URI_Producer(),
URI: URI_Producer(),
v2: V2Ray_Producer(),
v2ray: V2Ray_Producer(),
V2Ray: V2Ray_Producer(),
json: JSON_Producer(),
JSON: JSON_Producer(),
stash: Stash_Producer(),
Stash: Stash_Producer(),
shadowrocket: Shadowrocket_Producer(),
Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(),
surfboard: Surfboard_Producer(),
Surfboard: Surfboard_Producer(),
singbox: singbox_Producer(),
'sing-box': singbox_Producer(),
egern: Egern_Producer(),
Egern: Egern_Producer(),
};

View File

@@ -4,10 +4,10 @@ import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
export default function Loon_Producer() {
const produce = (proxy) => {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
return shadowsocks(proxy, opts['include-unsupported-proxy']);
case 'ssr':
return shadowsocksr(proxy);
case 'trojan':
@@ -56,6 +56,8 @@ function shadowsocks(proxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);

View File

@@ -8,6 +8,8 @@ export default function ShadowRocket_Producer() {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
} else if (['mieru'].includes(proxy.type)) {
return false;
}
return true;
})
@@ -193,6 +195,7 @@ export default function ShadowRocket_Producer() {
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -3,7 +3,11 @@ import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const detourParser = (proxy, parsedProxy) => {
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
};
const networkParser = (proxy, parsedProxy) => {
if (['tcp', 'udp'].includes(proxy._network))
parsedProxy.network = proxy._network;
};
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
@@ -214,7 +218,11 @@ const tlsParser = (proxy, parsedProxy) => {
proxy['reality-opts']['short-id'];
parsedProxy.tls.utls = { enabled: true };
}
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
if (
!['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
proxy['client-fingerprint'] &&
proxy['client-fingerprint'] !== ''
)
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
@@ -301,6 +309,7 @@ const socks5Parser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy;
@@ -352,6 +361,7 @@ const ssParser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -466,7 +476,7 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -489,7 +499,7 @@ const vlessParser = (proxy = {}) => {
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -510,7 +520,7 @@ const trojanParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -558,13 +568,14 @@ const hysteriaParser = (proxy = {}) => {
parsedProxy.disable_mtu_discovery = true;
}
}
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',
@@ -576,12 +587,23 @@ const hysteria2Parser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (includeUnsupportedProxy) {
if (proxy['hop-interval'])
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
? `${proxy['hop-interval']}s`
: proxy['hop-interval'];
if (proxy['ports'])
parsedProxy.server_ports = proxy['ports']
.split(/\s*,\s*/)
.map((p) => p.replace(/\s*-\s*/g, ':'));
}
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
@@ -612,6 +634,7 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
@@ -669,6 +692,7 @@ const wireguardParser = (proxy = {}) => {
parsedProxy.peers.push(peer);
}
}
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
@@ -786,7 +810,12 @@ export default function singbox_Producer() {
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
list.push(
hysteria2Parser(
proxy,
opts['include-unsupported-proxy'],
),
);
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {

View File

@@ -6,7 +6,6 @@ export default function Stash_Producer() {
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'ss',
@@ -40,6 +39,12 @@ export default function Stash_Producer() {
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
...(opts['include-unsupported-proxy']
? [
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
]
: []),
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts'])
@@ -284,6 +289,7 @@ export default function Stash_Producer() {
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -27,6 +27,8 @@ export default function Surge_Producer() {
return vmess(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'direct':
return direct(proxy);
case 'socks5':
return socks5(proxy);
case 'snell':
@@ -93,7 +95,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -193,7 +195,7 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -366,7 +368,7 @@ function ssh(proxy) {
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
@@ -431,7 +433,7 @@ function http(proxy) {
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -503,13 +505,61 @@ function http(proxy) {
return result.toString();
}
function direct(proxy) {
const result = new Result(proxy);
const type = 'direct';
result.append(`${proxy.name}=${type}`);
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 socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -675,7 +725,7 @@ function tuic(proxy) {
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
@@ -950,7 +1000,7 @@ function hysteria2(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);

View File

@@ -29,9 +29,13 @@ export default function URI_Producer() {
switch (proxy.type) {
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
proxy.port
}${proxy.plugin ? '/' : ''}`;
result = `ss://${
proxy.cipher?.startsWith('2022-blake3-')
? `${encodeURIComponent(
proxy.cipher,
)}:${encodeURIComponent(proxy.password)}`
: Base64.encode(userinfo)
}@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
if (proxy.plugin) {
result += '?plugin=';
const opts = proxy['plugin-opts'];
@@ -102,7 +106,7 @@ export default function URI_Producer() {
port: proxy.port,
id: proxy.uuid,
type,
aid: 0,
aid: proxy.alterId || 0,
net,
tls: proxy.tls ? 'tls' : '',
};
@@ -134,6 +138,8 @@ export default function URI_Producer() {
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
result.host =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
@@ -196,6 +202,13 @@ export default function URI_Producer() {
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
const authority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (authority) {
vlessTransport += `&authority=${encodeURIComponent(
authority,
)}`;
}
}
let vlessTransportServiceName =
@@ -261,11 +274,18 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let trojanTransportAuthority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
if (trojanTransportAuthority) {
trojanTransport += `&authority=${encodeURIComponent(
trojanTransportAuthority,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
@@ -290,11 +310,27 @@ export default function URI_Producer() {
)}`;
}
}
let trojanFp = '';
if (proxy['client-fingerprint']) {
trojanFp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let trojanAlpn = '';
if (proxy.alpn) {
trojanAlpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
}${trojanTransport}${trojanAlpn}${trojanFp}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'hysteria2':
let hysteria2params = [];

View File

@@ -37,7 +37,9 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
if (resourceType === RESOURCE_TYPE.PROXY) {
try {
let proxies = ProxyUtils.parse(resource);
result = ProxyUtils.produce(proxies, 'Loon');
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
'include-unsupported-proxy': arg?.includeUnsupportedProxy,
});
} catch (e) {
console.log('解析器: 使用 resource 出现错误');
console.log(e.message ?? e);
@@ -45,9 +47,20 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
try {
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
let raw = await download(
resourceUrl,
arg?.ua,
arg?.timeout,
undefined,
undefined,
undefined,
undefined,
true,
);
let proxies = ProxyUtils.parse(raw);
result = ProxyUtils.produce(proxies, 'Loon');
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
'include-unsupported-proxy': arg?.includeUnsupportedProxy,
});
} catch (e) {
console.log(e.message ?? e);
}

View File

@@ -21,6 +21,7 @@ import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerTokenRoutes from '@/restful/token';
import registerModuleRoutes from '@/restful/module';
migrate();
@@ -32,6 +33,7 @@ function serve() {
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerTokenRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app);

View File

@@ -50,11 +50,21 @@ function createCollection(req, res) {
function getCollection(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (collection) {
success(res, collection);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(collection));
} else {
success(res, collection);
}
} else {
failed(
res,

View File

@@ -13,11 +13,44 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/download/:name', downloadSubscription);
$app.get(
'/download/collection/:name/api/v1/server/details',
async (req, res) => {
@@ -59,11 +92,9 @@ async function downloadSubscription(req, res) {
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}`,
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
let {
url,
@@ -75,6 +106,7 @@ async function downloadSubscription(req, res) {
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {};
if (req.query.$options) {
@@ -97,6 +129,14 @@ async function downloadSubscription(req, res) {
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
if (!/^https?:\/\//.test(url)) {
content = url;
$.info(`URL 不是链接,视为本地订阅`);
}
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
@@ -106,10 +146,7 @@ async function downloadSubscription(req, res) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
@@ -131,10 +168,21 @@ async function downloadSubscription(req, res) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
try {
const passThroughUA = sub.passThroughUA;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
);
ua = reqUA;
}
let output = await produceArtifact({
type: 'subscription',
name,
@@ -151,8 +199,9 @@ async function downloadSubscription(req, res) {
},
$options,
proxy,
noCache,
});
let flowInfo;
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
@@ -187,7 +236,7 @@ async function downloadSubscription(req, res) {
}
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
@@ -207,7 +256,30 @@ async function downloadSubscription(req, res) {
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
res.set(
'subscription-userinfo',
[subUserInfo, flowInfo].filter((i) => i).join('; '),
);
}
if (platform === 'JSON') {
@@ -246,7 +318,7 @@ async function downloadSubscription(req, res) {
);
}
} else {
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}`);
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -274,7 +346,7 @@ async function downloadCollection(req, res) {
$.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}`,
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
let {
@@ -283,6 +355,7 @@ async function downloadCollection(req, res) {
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {};
@@ -325,6 +398,9 @@ async function downloadCollection(req, res) {
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
if (collection) {
try {
@@ -340,8 +416,9 @@ async function downloadCollection(req, res) {
},
$options,
proxy,
noCache,
});
let subUserInfoOfSub;
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subnames = collection.subscriptions;
@@ -380,16 +457,13 @@ async function downloadCollection(req, res) {
}
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
subUserInfoOfSub = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy || collection.proxy,
$arguments.flowUrl,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
@@ -400,10 +474,61 @@ async function downloadCollection(req, res) {
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
}
}
$.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);
let subUserInfoOfCol;
if (/^https?:\/\//.test(collection.subUserinfo)) {
try {
subUserInfoOfCol = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || collection.proxy,
collection.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
collection.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfoOfCol = collection.subUserinfo;
}
res.set(
'subscription-userinfo',
[subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; '),
);
if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
@@ -439,7 +564,7 @@ async function downloadCollection(req, res) {
);
}
} else {
$.notify(
$.error(
`🌍 Sub-Store 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);

View File

@@ -62,6 +62,7 @@ async function getFile(req, res) {
mergeSources,
ignoreFailedRemoteFile,
proxy,
noCache,
} = req.query;
let $options = {};
if (req.query.$options) {
@@ -113,6 +114,9 @@ async function getFile(req, res) {
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
@@ -128,6 +132,7 @@ async function getFile(req, res) {
ignoreFailedRemoteFile,
$options,
proxy,
noCache,
});
try {
@@ -179,7 +184,7 @@ async function getFile(req, res) {
);
}
} else {
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}`);
$.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -192,11 +197,21 @@ async function getFile(req, res) {
}
function getWholeFile(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(file));
} else {
success(res, file);
}
} else {
failed(
res,

View File

@@ -67,7 +67,7 @@ async function previewFile(req, res) {
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
{ $files: files, $content: filesContent, $file: file },
file.process,
)
: { $content: filesContent, $files: files };
@@ -114,6 +114,10 @@ async function compareSub(req, res) {
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;
@@ -219,6 +223,10 @@ async function compareCollection(req, res) {
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;

View File

@@ -57,9 +57,29 @@ async function getFlowInfo(req, res) {
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
try {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
...parseFlowHeaders(subUserInfo),
});
} catch (e) {
$.error(
@@ -125,58 +145,73 @@ async function getFlowInfo(req, res) {
);
return;
}
if (sub.subUserinfo) {
try {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} else {
const flowHeaders = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
const flowHeaders = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (!flowHeaders && !sub.subUserinfo) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
return;
}
try {
const remainingDays = getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
});
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
const result = {
...parseFlowHeaders(
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
),
};
if (remainingDays != null) {
result.remainingDays = remainingDays;
}
success(res, result);
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} catch (err) {
failed(

View File

@@ -39,6 +39,7 @@ async function produceArtifact({
awaitCustomCache,
$options,
proxy,
noCache,
}) {
platform = platform || 'JSON';
@@ -72,6 +73,8 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
@@ -119,6 +122,8 @@ async function produceArtifact({
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
@@ -237,6 +242,10 @@ async function produceArtifact({
proxy ||
sub.proxy ||
collection.proxy,
undefined,
undefined,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
@@ -410,6 +419,9 @@ async function produceArtifact({
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -458,6 +470,9 @@ async function produceArtifact({
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
@@ -497,7 +512,12 @@ async function produceArtifact({
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent, $options },
{
$files: files,
$content: filesContent,
$options,
$file: file,
},
file.process,
)
: { $content: filesContent, $files: files, $options };

View File

@@ -140,7 +140,7 @@ async function signToken(req, res) {
);
}
}
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
// const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const nanoid = eval(`require("nanoid")`);
const tokens = $.read(TOKENS_KEY) || [];
// const now = Date.now();
@@ -166,7 +166,7 @@ async function signToken(req, res) {
$.write(tokens, TOKENS_KEY);
return success(res, {
token,
secret,
// secret,
});
} catch (e) {
return failed(

View File

@@ -1,6 +1,7 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
import { isIPv4 } from '@/utils';
export async function doh({ url, domain, type = 'A', timeout, edns }) {
const buf = dnsPacket.encode({
@@ -23,7 +24,7 @@ export async function doh({ url, domain, type = 'A', timeout, edns }) {
{
code: 'CLIENT_SUBNET',
ip: edns,
sourcePrefixLength: 24,
sourcePrefixLength: isIPv4(edns) ? 24 : 56,
scopePrefixLength: 0,
},
],

View File

@@ -11,6 +11,10 @@ import {
validCheck,
} from '@/utils/flow';
import $ from '@/core/app';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor',
);
const tasks = new Map();
@@ -21,6 +25,8 @@ export default async function download(
customProxy,
skipCustomCache,
awaitCustomCache,
noCache,
preprocess,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
@@ -43,14 +49,19 @@ export default async function download(
}
}
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const {
defaultProxy,
defaultUserAgent,
defaultTimeout,
cacheThreshold: defaultCacheThreshold,
} = $.read(SETTINGS_KEY);
const cacheThreshold = defaultCacheThreshold || 1024;
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
@@ -65,7 +76,7 @@ export default async function download(
if (customCacheKey && !skipCustomCache) {
const customCached = $.read(customCacheKey);
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
if (!noCache && !$arguments?.noCache && cached) {
$.info(
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
);
@@ -81,6 +92,9 @@ export default async function download(
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
);
} catch (e) {
$.error(
@@ -101,6 +115,9 @@ export default async function download(
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
).catch((e) => {
$.error(
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
@@ -149,8 +166,8 @@ export default async function download(
// try to find in app cache
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}`);
if (!noCache && !$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}, ${userAgent}`);
result = cached;
if (customCacheKey) {
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
@@ -163,10 +180,10 @@ export default async function download(
: { insecure: true }
: undefined;
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get({
let { body, headers, statusCode } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
@@ -174,15 +191,28 @@ export default async function download(
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
$.info(`statusCode: ${statusCode}`);
if (statusCode < 200 || statusCode >= 400) {
throw new Error(`statusCode: ${statusCode}`);
}
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
headersResourceCache.set(id, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空'));
if (preprocess) {
try {
if (clashPreprocessor.test(body)) {
body = clashPreprocessor.parse(body, true);
}
} catch (e) {
$.error(`Clash Pre-processor error: ${e}`);
}
}
let shouldCache = true;
if (cacheThreshold) {
const size = body.length / 1024;

View File

@@ -1,14 +1,26 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export function getFlowField(headers) {
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
const keys = Object.keys(headers);
let sub = '';
let webPage = '';
for (let k of keys) {
const lower = k.toLowerCase();
if (lower === 'subscription-userinfo') {
sub = headers[k];
} else if (lower === 'profile-web-page-url') {
webPage = headers[k];
}
}
return `${sub || ''}${
webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''
}`;
}
export async function getFlowHeaders(
rawUrl,
@@ -41,29 +53,26 @@ export async function getFlowHeaders(
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url);
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultFlowUserAgent || 'clash';
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
const cached = headersResourceCache.get(id);
let flowInfo;
if (!$arguments?.noCache && cached) {
// $.info(`使用缓存的流量信息: ${url}`);
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
flowInfo = cached;
} else {
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
if (flowUrl) {
$.info(
@@ -159,7 +168,10 @@ export async function getFlowHeaders(
}
}
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
flowInfo = flowInfo.trim();
}
if (flowInfo) {
headersResourceCache.set(id, flowInfo);
}
}
@@ -190,8 +202,29 @@ export function parseFlowHeaders(flowHeaders) {
? Number(expireMatch[1] + expireMatch[2])
: undefined;
return { expires, total, usage: { upload, download } };
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
const remainingDays = remainingDaysMatch
? Number(remainingDaysMatch[1])
: undefined;
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
const planName = planNameMatch
? decodeURIComponent(planNameMatch[1])
: undefined;
return {
expires,
total,
usage: { upload, download },
remainingDays,
appUrl,
planName,
};
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);

View File

@@ -17,6 +17,7 @@ const ISOFlags = {
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇴': ['BO', 'BOL'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
@@ -38,6 +39,7 @@ const ISOFlags = {
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇬🇹': ['GT', 'GTM'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
@@ -141,6 +143,7 @@ export function getFlag(name) {
'🇧🇭': ['Bahrain', '巴林'],
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
'🇧🇴': ['Bolivia', '玻利维亚'],
'🇨🇦': [
'Canada',
'加拿大',
@@ -151,6 +154,7 @@ export function getFlag(name) {
'滑铁卢',
'多伦多',
'Waterloo',
'Toronto',
],
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
'🇨🇱': ['Chile', '智利'],
@@ -190,6 +194,7 @@ export function getFlag(name) {
],
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
'🇬🇷': ['Greece', '希腊', '希臘'],
'🇬🇹': ['Guatemala', '危地马拉'],
'🇭🇰': [
'Hongkong',
'香港',
@@ -245,7 +250,7 @@ export function getFlag(name) {
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
'🇮🇱': ['Israel', '以色列'],
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
'🇮🇷': ['Iran', '伊朗'],
'🇮🇸': ['Iceland', '冰岛', '冰島'],
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
@@ -261,7 +266,14 @@ export function getFlag(name) {
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇱': [
'Netherlands',
'荷兰',
'荷蘭',
'尼德蘭',
'阿姆斯特丹',
'Amsterdam',
],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
@@ -269,7 +281,7 @@ export function getFlag(name) {
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
'🇵🇰': ['Pakistan', '巴基斯坦'],
'🇵🇱': ['Poland', '波兰', '波蘭'],
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
'🇵🇷': ['Puerto Rico', '波多黎各'],
'🇵🇹': ['Portugal', '葡萄牙'],
'🇵🇾': ['Paraguay', '巴拉圭'],
@@ -294,7 +306,7 @@ export function getFlag(name) {
'Moscow',
],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
'🇸🇪': ['Sweden', '瑞典'],
'🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
'🇸🇬': [
'Singapore',
'新加坡',
@@ -314,7 +326,7 @@ export function getFlag(name) {
'🇸🇰': ['Slovakia', '斯洛伐克'],
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
'🇹🇳': ['Tunisia', '突尼斯'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
'🇹🇼': [
'Taiwan',
'台湾',
@@ -341,6 +353,7 @@ export function getFlag(name) {
'波特兰',
'达拉斯',
'俄勒冈',
'Oregon',
'凤凰城',
'费利蒙',
'硅谷',
@@ -354,10 +367,17 @@ export function getFlag(name) {
'沪美',
'哥伦布',
'纽约',
'New York',
'Los Angeles',
'San Jose',
'Sillicon Valley',
'Michigan',
'俄亥俄',
'Ohio',
'马纳萨斯',
'Manassas',
'弗吉尼亚',
'Virginia',
],
'🇺🇾': ['Uruguay', '乌拉圭'],
'🇻🇪': ['Venezuela', '委内瑞拉'],
@@ -418,8 +438,12 @@ export function getFlag(name) {
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
)
) {
//console.log(`ISOFlag = ${flag}`)
return (Flag = flag);
const isCN2 =
flag == '🇨🇳' &&
RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
if (!isCN2) {
return (Flag = flag);
}
}
}

View File

@@ -41,7 +41,7 @@ export default class Gist {
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
timeout: timeout || 8000,
events: {
onResponse: (resp) => {
@@ -81,7 +81,7 @@ export default class Gist {
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
timeout: timeout || 8000,
events: {
onResponse: (resp) => {

View File

@@ -2,18 +2,24 @@ export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
let accept = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
const lower = k.toLowerCase();
if (lower === 'user-agent') {
UA = headers[k];
ua = UA.toLowerCase();
break;
} else if (lower === 'accept') {
accept = headers[k];
}
}
return { UA, ua };
return { UA, ua, accept };
}
export function getPlatformFromUserAgent({ ua, UA }) {
export function getPlatformFromUserAgent({ ua, UA, accept }) {
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (ua.indexOf('egern') !== -1) {
return 'Egern';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
@@ -39,11 +45,14 @@ export function getPlatformFromUserAgent({ ua, UA }) {
return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) {
return 'sing-box';
} else {
} else if (accept.indexOf('application/json') === 0) {
return 'JSON';
} else {
return 'V2Ray';
}
}
export function getPlatformFromHeaders(headers) {
const { UA, ua } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA });
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA, accept });
}

View File

@@ -9,6 +9,7 @@ export default function express({ substore: $, port, host }) {
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
'X-Powered-By': 'Sub-Store',
};
// node support

View File

@@ -1,10 +1,10 @@
name: Sub-Store
description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *"
description: '支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *'
compat_arguments:
ability: http-client-policy
cronexp: 55 23 * * *
sync: '"Sub-Store Sync"'
timeout: "120"
timeout: '120'
engine: auto
produce: '"# Sub-Store Produce"'
produce_cronexp: 50 */6 * * *
@@ -12,26 +12,27 @@ compat_arguments:
produce_col: '"col1,col2"'
compat_arguments_desc: '\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
scriptings:
- http_request:
name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true
- http_request:
name: Sub-Store Simple
match: ^https?:\/\/sub\.store
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true
- schedule:
name: "{{{sync}}}"
cron: "{{{cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule:
name: "{{{produce}}}"
cron: "{{{produce_cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
- http_request:
name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true
- http_request:
name: Sub-Store Simple
match: ^https?:\/\/sub\.store
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true
- schedule:
name: '{{{sync}}}'
cron: '{{{cronexp}}}'
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule:
name: '{{{produce}}}'
cron: '{{{produce_cronexp}}}'
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
mitm:
hostnames:
includes:
- sub.store

View File

@@ -21,6 +21,7 @@ function operator(proxies = [], targetPlatform, context) {
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
@@ -165,6 +166,20 @@ function operator(proxies = [], targetPlatform, context) {
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
// 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
// let name = ''
// for (const [key, value] of Object.entries(env.source)) {
// if (!key.startsWith('_')) {
// name = value.displayName || value.name
// break
// }
// }
// if (!name) {
// const collection = env.source._collection
// name = collection.displayName || collection.name
// }
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
// {
// "source": {