Compare commits

...

45 Commits

Author SHA1 Message Date
xream
bbb9602f9f release: backend version 2.14.256 2024-03-15 08:12:06 +08:00
xream
6db6153672 Merge pull request #295 from makaspacex/master
[Feat]规则转换增加对GEOIP与GEOSITE的支持
2024-03-15 08:10:30 +08:00
makabaka
b66189948a 规则转换增加对GEOIP与GEOSITE的支持 2024-03-14 22:07:45 +08:00
xream
2611dccc73 feat: 支持设置查询远程订阅流量信息时的 User-Agent 2024-03-14 19:45:39 +08:00
xream
25d3cf6ca4 feat: 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js 2024-03-14 01:54:07 +08:00
xream
3637c5eb74 feat: SSH 协议跟进 clash.meta(mihomo) 的修改 2024-03-13 16:24:30 +08:00
xream
80d46597b4 feat: 支持使用代理/节点/策略获取订阅 2024-03-13 05:33:52 +08:00
xream
ca65e4209e feat: 支持自定义订阅流量信息 2024-03-12 01:17:56 +08:00
xream
53bb4866e7 fix: 修复订阅流量传递 2024-03-12 00:55:30 +08:00
xream
09495fa607 fix: 修复重置天数微妙的偏差 2024-03-11 19:33:51 +08:00
xream
4b27d40602 feat: 订阅支持开始日期和重置周期 2024-03-11 13:39:52 +08:00
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
xream
6a8cee3cd5 feat: 节点名称为空时, 添加默认节点名称 2024-02-17 17:41:54 +08:00
xream
9b2209cc8b chore: sing-box grpc servicename 应为字符串 2024-02-17 12:03:25 +08:00
xream
c585785c50 fix: 兼容不规范的 VLESS URI 2024-02-17 11:39:04 +08:00
xream
c3e5da7ee4 fix: 兼容不规范的 VLESS URI 2024-02-17 10:50:33 +08:00
xream
2ecad8bbb8 feat: 旗帜操作(支持更多选项) 2024-02-17 08:32:37 +08:00
xream
a642213928 feat: 脚本操作完整支持 /api/file/name 的内部文件调用路径 2024-02-15 03:30:53 +08:00
xream
f85e360ea8 feat: 同步配置前, 预处理订阅, 防止同时请求过多 2024-02-14 19:51:44 +08:00
xream
1b948cdf52 doc: 补充文档 2024-02-13 15:30:13 +08:00
xream
556d5f393c feat: 带参数 includeUnsupportedProxy 时, 支持输出 Surge WireGuard Section 2024-02-13 15:21:29 +08:00
xream
a8c05207c0 chore: dev 调整 2024-02-11 01:10:01 +08:00
xream
e97fb1e6d9 fix: 兼容更多 Trojan URI 格式 2024-02-08 23:06:39 +08:00
xream
e40b9a77c4 fix: 修复 Loon UDP 参数 2024-02-07 15:14:24 +08:00
xream
df0ac8a218 fix: 缓存不合法时即刻重置 2024-02-07 01:14:52 +08:00
41 changed files with 1397 additions and 236 deletions

View File

@@ -59,6 +59,17 @@ jobs:
./backend/dist/sub-store-parser.loon.min.js ./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js ./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.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 - name: Sync to GitLab
env: env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }} GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "web"]
path = web
url = https://github.com/sub-store-org/Sub-Store-Front-End.git

View File

@@ -26,15 +26,13 @@ Core functionalities:
### Supported Input Formats ### Supported Input Formats
- [x] SS URI - [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5)
- [x] SSR URI - [x] Clash Proxies YAML
- [x] SSD URI - [x] Clash Proxy JSON(single line)
- [x] V2RayN URI
- [x] Hysteria 2 URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2) - [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] 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, WireGuard(Surfboard to Surfboard)) - [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] 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] 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) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.211", "version": "2.14.256",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
@@ -9,7 +9,7 @@
"serve": "node sub-store.min.js", "serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js", "start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js", "dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
"dev:run": "nodemon -w sub-store.json -w sub-store.min.js sub-store.min.js", "dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
"build": "gulp", "build": "gulp",
"bundle": "node bundle.js" "bundle": "node bundle.js"
}, },

View File

@@ -1,11 +1,20 @@
import YAML from '@/utils/yaml'; import YAML from '@/utils/yaml';
import download from '@/utils/download'; import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils'; import {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
} from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers'; import PROXY_PRODUCERS from './producers';
import PROXY_PARSERS from './parsers'; import PROXY_PARSERS from './parsers';
import $ from '@/core/app'; import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
function preprocess(raw) { function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) { for (const processor of PROXY_PREPROCESSORS) {
@@ -95,18 +104,50 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
} }
} }
} }
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`;
const downloadUrlMatch = url.match(
/^\/api\/(file|module)\/(.+)/,
);
if (downloadUrlMatch) {
let type = '';
try {
type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到 ${type}: ${name}`);
}
// if this is a remote script, download it if (type === 'module') {
try { script = item.content;
script = await download( } else {
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`, script = await produceArtifact({
); type: 'file',
// $.info(`Script loaded: >>>\n ${script}`); name,
} catch (err) { });
$.error( }
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, } catch (err) {
); $.error(
throw new Error(`无法下载脚本: ${url}`); `Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else {
// if this is a remote script, download it
try {
script = await download(url);
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法下载脚本: ${url}`);
}
} }
} else { } else {
script = content; script = content;
@@ -151,6 +192,13 @@ function produce(proxies, targetPlatform, type, opts = {}) {
!(proxy.supported && proxy.supported[targetPlatform] === false), !(proxy.supported && proxy.supported[targetPlatform] === false),
); );
proxies = proxies.map((proxy) => {
if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
return proxy;
});
$.info(`Producing proxies for target: ${targetPlatform}`); $.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000; let localPort = 10000;
@@ -215,6 +263,10 @@ function safeMatch(parser, line) {
} }
function lastParse(proxy) { function lastParse(proxy) {
if (proxy.interface) {
proxy['interface-name'] = proxy.interface;
delete proxy.interface;
}
if (isValidPortNumber(proxy.port)) { if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10); proxy.port = parseInt(proxy.port, 10);
} }
@@ -318,6 +370,10 @@ function lastParse(proxy) {
delete proxy.ports; delete proxy.ports;
} }
if (['vless'].includes(proxy.type)) { if (['vless'].includes(proxy.type)) {
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
}
if (['http'].includes(proxy.network)) { if (['http'].includes(proxy.network)) {
let transportPath = proxy[`${proxy.network}-opts`]?.path; let transportPath = proxy[`${proxy.network}-opts`]?.path;
if (!transportPath) { if (!transportPath) {
@@ -328,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; return proxy;
} }

View File

@@ -273,11 +273,17 @@ function URI_VMess() {
params.port = port; params.port = port;
params.add = server; params.add = server;
} }
const server = params.add;
const port = parseInt(getIfPresent(params.port), 10);
const proxy = { const proxy = {
name: params.ps ?? params.remarks, name:
params.ps ??
params.remarks ??
params.remark ??
`VMess ${server}:${port}`,
type: 'vmess', type: 'vmess',
server: params.add, server,
port: parseInt(getIfPresent(params.port), 10), port,
cipher: getIfPresent(params.scy, 'auto'), cipher: getIfPresent(params.scy, 'auto'),
uuid: params.id, uuid: params.id,
alterId: parseInt( alterId: parseInt(
@@ -399,14 +405,18 @@ function URI_VLESS() {
params[key] = value; params[key] = value;
} }
proxy.name = name ?? params.remarks ?? `VLESS ${server}:${port}`; proxy.name =
name ??
params.remarks ??
params.remark ??
`VLESS ${server}:${port}`;
proxy.tls = params.security && params.security !== 'none'; proxy.tls = params.security && params.security !== 'none';
if (isShadowrocket && /TRUE|1/i.test(params.tls)) { if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
proxy.tls = true; proxy.tls = true;
params.security = params.security ?? 'reality'; params.security = params.security ?? 'reality';
} }
proxy.sni = params.sni ?? params.peer; proxy.sni = params.sni || params.peer;
proxy.flow = params.flow; proxy.flow = params.flow;
if (!proxy.flow && isShadowrocket && params.xtls) { if (!proxy.flow && isShadowrocket && params.xtls) {
// "none" is undefined // "none" is undefined
@@ -434,6 +444,7 @@ function URI_VLESS() {
proxy[`${params.security}-opts`] = opts; proxy[`${params.security}-opts`] = opts;
} }
} }
proxy.network = params.type; proxy.network = params.type;
if (proxy.network === 'tcp' && params.headerType === 'http') { if (proxy.network === 'tcp' && params.headerType === 'http') {
proxy.network = 'http'; proxy.network = 'http';
@@ -712,6 +723,7 @@ function Clash_All() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'wireguard', 'wireguard',
'ssh',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
throw new Error( throw new Error(
@@ -742,6 +754,9 @@ function Clash_All() {
if (proxy['benchmark-url']) { if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url']; proxy['test-url'] = proxy['benchmark-url'];
} }
if (proxy['benchmark-timeout']) {
proxy['test-timeout'] = proxy['benchmark-timeout'];
}
return proxy; return proxy;
}; };
@@ -1003,6 +1018,14 @@ function Loon_WireGuard() {
return { name, test, parse }; 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() { function Surge_SS() {
const name = 'Surge SS Parser'; const name = 'Surge SS Parser';
const test = (line) => { const test = (line) => {
@@ -1173,6 +1196,7 @@ export default [
URI_Hysteria2(), URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
Clash_All(), Clash_All(),
Surge_SSH(),
Surge_SS(), Surge_SS(),
Surge_VMess(), Surge_VMess(),
Surge_Trojan(), 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; 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"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,7 +52,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
} }
handleShadowTLS(); 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.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -63,21 +63,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket(); handleWebsocket();
handleShadowTLS(); 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"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS(); 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.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); 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"; proxy.type = "http";
handleShadowTLS(); 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"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { 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(); 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"; proxy.type = "tuic";
handleShadowTLS(); 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.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); 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"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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"; proxy.type = "hysteria2";
handleShadowTLS(); 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"; proxy.type = "socks5";
handleShadowTLS(); 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.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); 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(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
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(""); } 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_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(""); } 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; 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"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -50,7 +50,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
} }
handleShadowTLS(); 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.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -61,21 +61,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket(); handleWebsocket();
handleShadowTLS(); 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"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS(); 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.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); 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"; proxy.type = "http";
handleShadowTLS(); 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"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { 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(); 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"; proxy.type = "tuic";
handleShadowTLS(); 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.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); 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"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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"; proxy.type = "hysteria2";
handleShadowTLS(); 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"; proxy.type = "socks5";
handleShadowTLS(); 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.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); 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(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
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(""); } 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_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(""); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }

View File

@@ -30,7 +30,7 @@ start = (trojan) {
return proxy return proxy
} }
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{ trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan"; proxy.type = "trojan";
proxy.password = password; proxy.password = password;
proxy.server = server; proxy.server = server;
@@ -79,7 +79,7 @@ port = digits:[0-9]+ {
} }
} }
params = "/"? "?" head:param tail:("&"@param)* { params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];

View File

@@ -28,7 +28,7 @@ start = (trojan) {
return proxy return proxy
} }
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{ trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan"; proxy.type = "trojan";
proxy.password = password; proxy.password = password;
proxy.server = server; proxy.server = server;
@@ -77,7 +77,7 @@ port = digits:[0-9]+ {
} }
} }
params = "/"? "?" head:param tail:("&"@param)* { params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];

View File

@@ -16,6 +16,7 @@ import {
parseFlowHeaders, parseFlowHeaders,
validCheck, validCheck,
flowTransfer, flowTransfer,
getRmainingDays,
} from '@/utils/flow'; } from '@/utils/flow';
/** /**
@@ -121,7 +122,7 @@ function QuickSettingOperator(args) {
} }
// add or remove flag for proxies // add or remove flag for proxies
function FlagOperator({ mode }) { function FlagOperator({ mode, tw }) {
return { return {
name: 'Flag Operator', name: 'Flag Operator',
func: (proxies) => { func: (proxies) => {
@@ -135,7 +136,13 @@ function FlagOperator({ mode }) {
// remove old flag // remove old flag
proxy.name = removeFlag(proxy.name); proxy.name = removeFlag(proxy.name);
proxy.name = newFlag + ' ' + proxy.name; proxy.name = newFlag + ' ' + proxy.name;
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳'); if (tw == 'ws') {
proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');
} else if (tw == 'tw') {
// 不变
} else {
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
}
} }
return proxy; return proxy;
}); });
@@ -351,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 = { const DOMAIN_RESOLVERS = {
Google: async function (domain, type) { Google: async function (domain, type, noCache) {
const id = hex_md5(`GOOGLE:${domain}:${type}`); const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent( url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain, domain,
@@ -376,10 +413,13 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return 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 id = hex_md5(`IP-API:${domain}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://ip-api.com/json/${encodeURIComponent( url: `http://ip-api.com/json/${encodeURIComponent(
domain, domain,
@@ -393,10 +433,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Cloudflare: async function (domain, type) { Cloudflare: async function (domain, type, noCache) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`); const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent( url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain, domain,
@@ -417,10 +457,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain, type) { Ali: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent( url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain, domain,
@@ -437,10 +477,10 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Tencent: async function (domain, type) { Tencent: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://119.28.28.28/d?type=${ url: `http://119.28.28.28/d?type=${
type === 'IPv6' ? 'AAAA' : 'A' type === 'IPv6' ? 'AAAA' : 'A'
@@ -459,10 +499,12 @@ const DOMAIN_RESOLVERS = {
}, },
}; };
function ResolveDomainOperator({ provider, type, filter }) { function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
if (type === 'IPv6' && ['IP-API'].includes(provider)) { if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 IPv6`); throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
} }
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider]; const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) { if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`); throw new Error(`找不到域名解析服务提供方: ${provider}`);
@@ -484,7 +526,7 @@ function ResolveDomainOperator({ provider, type, filter }) {
const currentBatch = []; const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) { for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push( currentBatch.push(
resolver(domain, type) resolver(domain, type, cache === 'disabled')
.then((ip) => { .then((ip) => {
results[domain] = ip; results[domain] = ip;
$.info( $.info(
@@ -503,8 +545,19 @@ function ResolveDomainOperator({ provider, type, filter }) {
proxies.forEach((p) => { proxies.forEach((p) => {
if (!p['no-resolve']) { if (!p['no-resolve']) {
if (results[p.server]) { if (results[p.server]) {
p.server = results[p.server]; if (_type === 'IP4P') {
p.resolved = true; 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 { } else {
p.resolved = false; p.resolved = false;
} }
@@ -811,6 +864,7 @@ function createDynamicFunction(name, script, $arguments) {
parseFlowHeaders, parseFlowHeaders,
flowTransfer, flowTransfer,
validCheck, validCheck,
getRmainingDays,
}; };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(

View File

@@ -83,7 +83,9 @@ function shadowsocks(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -109,7 +111,9 @@ function shadowsocksr(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -152,7 +156,9 @@ function trojan(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -219,7 +225,9 @@ function vmess(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -281,7 +289,9 @@ function vless(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -304,8 +314,6 @@ function http(proxy) {
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString(); return result.toString();
} }
@@ -336,7 +344,11 @@ function wireguard(proxy) {
if (proxy.dns) { if (proxy.dns) {
if (Array.isArray(proxy.dns)) { if (Array.isArray(proxy.dns)) {
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i)); proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
proxy.dns = proxy.dns.find((i) => isIPv4(i)); let dns = proxy.dns.find((i) => isIPv4(i));
if (!dns) {
dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));
}
proxy.dns = dns;
} }
} }
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns'); result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
@@ -390,7 +402,9 @@ function hysteria2(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp // udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp'); if (proxy.udp) {
result.append(`,udp=true`);
}
// download-bandwidth // download-bandwidth
result.appendIfPresent( result.appendIfPresent(

View File

@@ -178,8 +178,8 @@ const grpcParser = (proxy, parsedProxy) => {
const transport = { type: 'grpc' }; const transport = { type: 'grpc' };
if (proxy['grpc-opts']) { if (proxy['grpc-opts']) {
const serviceName = proxy['grpc-opts']['grpc-service-name']; const serviceName = proxy['grpc-opts']['grpc-service-name'];
if (serviceName && serviceName !== '') if (serviceName != null && serviceName !== '')
transport.service_name = serviceName; transport.service_name = `${serviceName}`;
} }
parsedProxy.transport = transport; parsedProxy.transport = transport;
}; };
@@ -217,6 +217,41 @@ const tlsParser = (proxy, parsedProxy) => {
if (!parsedProxy.tls.enabled) delete parsedProxy.tls; 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['private-key'])
parsedProxy.private_key_path = proxy['private-key'];
if (proxy['private-key-passphrase'])
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
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['host-key']) parsedProxy.host_key = proxy['host-key'];
if (proxy['host-key-algorithms'])
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
return parsedProxy;
};
const httpParser = (proxy = {}) => { const httpParser = (proxy = {}) => {
const parsedProxy = { const parsedProxy = {
tag: proxy.name, tag: proxy.name,
@@ -588,8 +623,10 @@ const wireguardParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') { if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved); parsedProxy.reserved.push(proxy.reserved);
} else { } else if (Array.isArray(proxy.reserved)) {
for (const r of proxy.reserved) parsedProxy.reserved.push(r); for (const r of proxy.reserved) parsedProxy.reserved.push(r);
} else {
delete parsedProxy.reserved;
} }
if (proxy.peers && proxy.peers.length > 0) { if (proxy.peers && proxy.peers.length > 0) {
parsedProxy.peers = []; parsedProxy.peers = [];
@@ -603,8 +640,10 @@ const wireguardParser = (proxy = {}) => {
}; };
if (typeof p.reserved === 'string') { if (typeof p.reserved === 'string') {
peer.reserved.push(p.reserved); peer.reserved.push(p.reserved);
} else { } else if (Array.isArray(p.reserved)) {
for (const r of p.reserved) peer.reserved.push(r); 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']; if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
parsedProxy.peers.push(peer); parsedProxy.peers.push(peer);
@@ -624,6 +663,9 @@ export default function singbox_Producer() {
.map((proxy) => { .map((proxy) => {
try { try {
switch (proxy.type) { switch (proxy.type) {
case 'ssh':
list.push(sshParser(proxy));
break;
case 'http': case 'http':
list.push(httpParser(proxy)); list.push(httpParser(proxy));
break; break;

View File

@@ -250,6 +250,10 @@ export default function Stash_Producer() {
proxy['benchmark-url'] = proxy['test-url']; proxy['benchmark-url'] = proxy['test-url'];
delete 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.subName;
delete proxy.collectionName; delete proxy.collectionName;

View File

@@ -1,5 +1,5 @@
import { Result, isPresent } from './utils'; import { Result, isPresent } from './utils';
import { isNotBlank } from '@/utils'; import { isNotBlank, getIfNotBlank } from '@/utils';
import $ from '@/core/app'; import $ from '@/core/app';
const targetPlatform = 'Surge'; const targetPlatform = 'Surge';
@@ -13,7 +13,7 @@ const ipVersions = {
}; };
export default function Surge_Producer() { export default function Surge_Producer() {
const produce = (proxy) => { const produce = (proxy, type, opts = {}) => {
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
return shadowsocks(proxy); return shadowsocks(proxy);
@@ -30,9 +30,15 @@ export default function Surge_Producer() {
case 'tuic': case 'tuic':
return tuic(proxy); return tuic(proxy);
case 'wireguard-surge': case 'wireguard-surge':
return wireguard(proxy); return wireguard_surge(proxy);
case 'hysteria2': case 'hysteria2':
return hysteria2(proxy); return hysteria2(proxy);
case 'ssh':
return ssh(proxy);
}
if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
return wireguard(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -82,10 +88,8 @@ function shadowsocks(proxy) {
result.append(`,encrypt-method=${proxy.cipher}`); result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -117,6 +121,21 @@ function shadowsocks(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -167,10 +186,8 @@ function trojan(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -204,6 +221,21 @@ function trojan(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -236,10 +268,8 @@ function vmess(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -280,6 +310,21 @@ function vmess(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -307,6 +352,71 @@ function vmess(proxy) {
return result.toString(); 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) { function http(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http'; const type = proxy.tls ? 'https' : 'http';
@@ -314,10 +424,8 @@ function http(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -345,6 +453,21 @@ function http(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -379,10 +502,8 @@ function socks5(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -412,6 +533,21 @@ function socks5(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -445,10 +581,8 @@ function snell(proxy) {
result.appendIfPresent(`,version=${proxy.version}`, 'version'); result.appendIfPresent(`,version=${proxy.version}`, 'version');
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -477,6 +611,21 @@ function snell(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -525,10 +674,8 @@ function tuic(proxy) {
'alpn', 'alpn',
); );
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -557,6 +704,21 @@ function tuic(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -587,6 +749,122 @@ function tuic(proxy) {
} }
function wireguard(proxy) { function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
proxy.server = proxy.peers[0].server;
proxy.port = proxy.peers[0].port;
proxy.ip = proxy.peers[0].ip;
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
proxy.reserved = proxy.peers[0].reserved;
}
const result = new Result(proxy);
result.append(`# WireGuard Proxy ${proxy.name}
${proxy.name}=wireguard`);
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
// 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')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.append(`
# WireGuard Section ${proxy.name}
[WireGuard ${proxy['section-name']}]
private-key = ${proxy['private-key']}`);
result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip');
result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');
if (proxy.dns) {
if (Array.isArray(proxy.dns)) {
proxy.dns = proxy.dns.join(', ');
}
result.append(`\ndns-server = ${proxy.dns}`);
}
result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu');
if (ip_version === 'prefer-v6') {
result.append(`\nprefer-ipv6 = true`);
}
const allowedIps = Array.isArray(proxy['allowed-ips'])
? proxy['allowed-ips'].join(',')
: proxy['allowed-ips'];
let reserved = Array.isArray(proxy.reserved)
? proxy.reserved.join('/')
: proxy.reserved;
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
if (presharedKey) {
presharedKey = `,preshared-key="${presharedKey}"`;
}
const peer = {
'public-key': proxy['public-key'],
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
endpoint: `${proxy.server}:${proxy.port}`,
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
'client-id': reserved,
'preshared-key': presharedKey,
};
result.append(
`\npeer = (${Object.keys(peer)
.filter((k) => peer[k] != null)
.map((k) => `${k} = ${peer[k]}`)
.join(', ')})`,
);
return result.toString();
}
function wireguard_surge(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`); result.append(`${proxy.name}=wireguard`);
@@ -600,13 +878,26 @@ function wireguard(proxy) {
'no-error-alert', 'no-error-alert',
); );
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, '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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {
@@ -643,10 +934,8 @@ function hysteria2(proxy) {
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent( const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`, result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
'ip-version',
);
result.appendIfPresent( result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`, `,no-error-alert=${proxy['no-error-alert']}`,
@@ -673,6 +962,21 @@ function hysteria2(proxy) {
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, '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 // shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) { if (isPresent(proxy, 'shadow-tls-password')) {

View File

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

View File

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

View File

@@ -1,8 +1,14 @@
import { version } from '../../package.json'; import { version } from '../../package.json';
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants'; import {
SETTINGS_KEY,
ARTIFACTS_KEY,
SUBS_KEY,
COLLECTIONS_KEY,
} from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { syncToGist } from '@/restful/artifacts'; import { syncToGist } from '@/restful/artifacts';
import { findByName } from '@/utils/database';
!(async function () { !(async function () {
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
@@ -30,23 +36,83 @@ async function doSync() {
const files = {}; const files = {};
try { try {
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
} else if (artifact.type === 'collection') {
const collection = findByName(allCols, artifact.source);
if (collection && Array.isArray(collection.subscriptions)) {
collection.subscriptions.map((subName) => {
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
});
}
}
}
});
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
try {
await produceArtifact({
type: 'subscription',
name: subName,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
}
}),
);
}
await Promise.all( await Promise.all(
allArtifacts.map(async (artifact) => { allArtifacts.map(async (artifact) => {
if (artifact.sync) { try {
$.info(`正在同步云配置:${artifact.name}...`); if (artifact.sync && artifact.source) {
const output = await produceArtifact({ $.info(`正在同步云配置:${artifact.name}...`);
type: artifact.type, const output = await produceArtifact({
name: artifact.source, type: artifact.type,
platform: artifact.platform, name: artifact.source,
}); platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
},
});
files[encodeURIComponent(artifact.name)] = { // if (!output || output.length === 0)
content: output, // throw new Error('该配置的结果为空 不进行上传');
};
files[encodeURIComponent(artifact.name)] = {
content: output,
};
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
);
invalid.push(artifact.name);
} }
}), }),
); );
if (invalid.length > 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
);
}
const resp = await syncToGist(files); const resp = await syncToGist(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
@@ -71,8 +137,8 @@ async function doSync() {
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 Sub-Store', '全部订阅同步成功!'); $.notify('🌍 Sub-Store', '全部订阅同步成功!');
} catch (err) { } catch (e) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`); $.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`);
$.error(`无法同步订阅配置到 Gist原因${err}`); $.error(`无法同步订阅配置到 Gist原因${e}`);
} }
} }

View File

@@ -77,12 +77,49 @@ async function downloadSubscription(req, res) {
}, },
}); });
if (sub.source !== 'local' || url) { if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ||
url
) {
try { try {
// forward flow headers url = `${url || sub.url}`
const flowInfo = await getFlowHeaders(url || sub.url); .split(/[\r\n]+/)
if (flowInfo) { .map((i) => i.trim())
res.set('subscription-userinfo', flowInfo); .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) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} }
} catch (err) { } catch (err) {
$.error( $.error(
@@ -92,6 +129,9 @@ async function downloadSubscription(req, res) {
); );
} }
} }
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
if (platform === 'JSON') { if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send( res.set('Content-Type', 'application/json;charset=utf-8').send(
@@ -176,11 +216,47 @@ async function downloadCollection(req, res) {
const subnames = collection.subscriptions; const subnames = collection.subscriptions;
if (subnames.length > 0) { if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]); const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') { if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try { try {
const flowInfo = await getFlowHeaders(sub.url); let url = `${sub.url}`
if (flowInfo) { .split(/[\r\n]+/)
res.set('subscription-userinfo', flowInfo); .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) {
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} }
} catch (err) { } catch (err) {
$.error( $.error(
@@ -190,6 +266,9 @@ async function downloadCollection(req, res) {
); );
} }
} }
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
} }
if (platform === 'JSON') { if (platform === 'JSON') {

View File

@@ -109,7 +109,12 @@ async function compareSub(req, res) {
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -195,7 +200,12 @@ async function compareCollection(req, res) {
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(

View File

@@ -6,7 +6,11 @@ import {
} from './errors'; } from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; 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 { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -43,32 +47,98 @@ async function getFlowInfo(req, res) {
); );
return; return;
} }
if (sub.source === 'local') { if (
failed( sub.source === 'local' &&
res, !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
new RequestInvalidError( ) {
'NO_FLOW_INFO', if (sub.subUserinfo) {
'N/A', success(res, {
`Local subscription ${name} has no flow information!`, ...parseFlowHeaders(sub.subUserinfo),
), });
); } else {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
}
return; return;
} }
try { try {
const flowHeaders = await getFlowHeaders(sub.url); let url = `${sub.url}`
if (!flowHeaders) { .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( failed(
res, res,
new InternalServerError( new RequestInvalidError(
'NO_FLOW_INFO', 'NO_FLOW_INFO',
'No flow info', 'N/A',
`Failed to fetch flow headers`, `Subscription ${name}: noFlow`,
), ),
); );
return; return;
} }
if (sub.subUserinfo) {
success(res, parseFlowHeaders(flowHeaders)); success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} else {
const flowHeaders = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
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,
}),
});
}
} catch (err) { } catch (err) {
failed( failed(
res, res,

View File

@@ -35,13 +35,21 @@ async function produceArtifact({
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
produceType, produceType,
produceOpts = {}, produceOpts = {},
subscription,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
if (type === 'subscription') { if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY); let sub;
const sub = findByName(allSubs, name); if (name) {
if (!sub) throw new Error(`找不到订阅 ${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; let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content; raw = content;
@@ -54,7 +62,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, ua || sub.ua); return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -94,7 +107,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, ua || sub.ua); return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -190,7 +208,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -448,6 +471,46 @@ async function syncArtifacts() {
try { try {
const invalid = []; const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
} else if (artifact.type === 'collection') {
const collection = findByName(allCols, artifact.source);
if (collection && Array.isArray(collection.subscriptions)) {
collection.subscriptions.map((subName) => {
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
});
}
}
}
});
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
try {
await produceArtifact({
type: 'subscription',
name: subName,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
}
}),
);
}
await Promise.all( await Promise.all(
allArtifacts.map(async (artifact) => { allArtifacts.map(async (artifact) => {
try { try {

View File

@@ -1,7 +1,7 @@
import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache';
import { import {
@@ -14,7 +14,7 @@ import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(rawUrl, ua, timeout) { export default async function download(rawUrl, ua, timeout, proxy) {
let $arguments = {}; let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, ''); let url = rawUrl.replace(/#noFlow$/, '');
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -36,25 +36,26 @@ export default async function download(rawUrl, ua, timeout) {
} }
} }
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/); // const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) { // if (downloadUrlMatch) {
let type = downloadUrlMatch?.[1]; // let type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2]; // let name = downloadUrlMatch?.[2];
if (name == null) { // if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`); // throw new Error(`本地 ${type} URL 无效: ${url}`);
} // }
name = decodeURIComponent(name); // name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY; // const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name); // const item = findByName($.read(key), name);
if (!item) { // if (!item) {
throw new Error(`找不到本地 ${type}: ${name}`); // throw new Error(`找不到本地 ${type}: ${name}`);
} // }
return item.content; // return item.content;
} // }
const { isNode } = ENV(); const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY); const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta'; const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout; const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url); const id = hex_md5(userAgent + url);
@@ -65,6 +66,10 @@ export default async function download(rawUrl, ua, timeout) {
const http = HTTP({ const http = HTTP({
headers: { headers: {
'User-Agent': userAgent, 'User-Agent': userAgent,
...(isStash && proxy
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
: {}),
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
}, },
timeout: requestTimeout, timeout: requestTimeout,
}); });
@@ -74,13 +79,20 @@ export default async function download(rawUrl, ua, timeout) {
// try to find in app cache // try to find in app cache
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) { if (!$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}`);
result = cached; result = cached;
} else { } else {
$.info( $.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`, `Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
); );
try { try {
const { body, headers } = await http.get(url); const { body, headers } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
if (headers) { if (headers) {
const flowInfo = getFlowField(headers); const flowInfo = getFlowField(headers);
@@ -90,8 +102,22 @@ export default async function download(rawUrl, ua, timeout) {
} }
if (body.replace(/\s/g, '').length === 0) if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空')); 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; result = body;
} catch (e) { } catch (e) {
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`); throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
@@ -101,7 +127,16 @@ export default async function download(rawUrl, ua, timeout) {
// 检查订阅有效性 // 检查订阅有效性
if ($arguments?.validCheck) { if ($arguments?.validCheck) {
await validCheck(parseFlowHeaders(await getFlowHeaders(url))); await validCheck(
parseFlowHeaders(
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
),
),
);
} }
if (!isNode) { if (!isNode) {

View File

@@ -1,7 +1,16 @@
import { version as substoreVersion } from '../../package.json'; import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api'; 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'; let backend = 'Node';
if (isNode) backend = 'Node'; if (isNode) backend = 'Node';
if (isQX) backend = 'QX'; if (isQX) backend = 'QX';
@@ -9,8 +18,44 @@ if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge'; if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash'; if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket'; 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 { export default {
backend, backend,
version: substoreVersion, version: substoreVersion,
meta,
}; };

View File

@@ -1,5 +1,6 @@
import { SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app'; import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache';
@@ -9,7 +10,7 @@ export function getFlowField(headers) {
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export async function getFlowHeaders(rawUrl, ua, timeout) { export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl; let url = rawUrl;
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -33,6 +34,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
if ($arguments?.noFlow) { if ($arguments?.noFlow) {
return; return;
} }
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url); const cached = headersResourceCache.get(url);
let flowInfo; let flowInfo;
if (!$arguments?.noCache && cached) { if (!$arguments?.noCache && cached) {
@@ -47,7 +49,11 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
const requestTimeout = timeout || defaultTimeout; const requestTimeout = timeout || defaultTimeout;
const http = HTTP(); const http = HTTP();
try { try {
// $.info(`使用 HEAD 方法获取流量信息: ${url}`); $.info(
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.head({ const { headers } = await http.head({
url: url url: url
.split(/[\r\n]+/) .split(/[\r\n]+/)
@@ -55,17 +61,36 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
.filter((i) => i.length)[0], .filter((i) => i.length)[0],
headers: { headers: {
'User-Agent': userAgent, 'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
}, },
timeout: requestTimeout, timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
}); });
flowInfo = getFlowField(headers); flowInfo = getFlowField(headers);
} catch (e) { } catch (e) {
$.error( $.error(
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`, `使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
); );
} }
if (!flowInfo) { if (!flowInfo) {
$.info(`使用 GET 方法获取流量信息: ${url}`); $.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.get({ const { headers } = await http.get({
url: url url: url
.split(/[\r\n]+/) .split(/[\r\n]+/)
@@ -143,3 +168,60 @@ export function validCheck(flow) {
} }
} }
} }
export function getRmainingDays(opt = {}) {
try {
let { resetDay, startDate, cycleDays } = opt;
if (['string', 'number'].includes(typeof opt)) {
resetDay = opt;
}
if (startDate && cycleDays) {
cycleDays = parseInt(cycleDays);
if (isNaN(cycleDays) || cycleDays <= 0)
throw new Error('重置周期应为正整数');
if (!startDate || !Date.parse(startDate))
throw new Error('开始日期不合法');
const start = new Date(startDate);
const today = new Date();
start.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
if (start.getTime() > today.getTime())
throw new Error('开始日期应早于现在');
let resetDate = new Date(startDate);
resetDate.setDate(resetDate.getDate() + cycleDays);
while (resetDate < today) {
resetDate.setDate(resetDate.getDate() + cycleDays);
}
resetDate.setHours(0, 0, 0, 0);
const timeDiff = resetDate.getTime() - today.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
return daysDiff;
} else {
if (!resetDay) return;
resetDay = parseInt(resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
throw new Error('月重置日应为 1-31 之间的整数');
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;
}
} catch (e) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}

View File

@@ -213,11 +213,16 @@ export function getFlag(name) {
'🇹🇼': [ '🇹🇼': [
'Taiwan', 'Taiwan',
'台湾', '台湾',
'臺灣',
'台灣',
'中華民國',
'中华民国',
'台北', '台北',
'台中', '台中',
'新北', '新北',
'彰化', '彰化',
'台', '台',
'臺',
'Taipei', 'Taipei',
], ],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'], '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
@@ -371,7 +376,7 @@ export function getFlag(name) {
'🇹🇭': ['TH', 'THA'], '🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'], '🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'], '🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'], '🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'], '🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'], '🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
'🇺🇾': ['UY', 'URY'], '🇺🇾': ['UY', 'URY'],

View File

@@ -10,7 +10,17 @@ class ResourceCache {
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) { if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
$.write('{}', HEADERS_RESOURCE_CACHE_KEY); $.write('{}', HEADERS_RESOURCE_CACHE_KEY);
} }
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY)); try {
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
} catch (e) {
$.error(
`解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
e?.message ?? e
}`,
);
this.resourceCache = {};
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
}
this._cleanup(); this._cleanup();
} }

View File

@@ -35,6 +35,64 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue; return isPresent(obj) ? obj : defaultValue;
} }
function getPolicyDescriptor(str) {
if (!str) return {};
return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
? {
'policy-descriptor': str,
}
: {
policy: str,
};
}
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 { export {
isIPv4, isIPv4,
isIPv6, isIPv6,
@@ -43,4 +101,6 @@ export {
getIfNotBlank, getIfNotBlank,
isPresent, isPresent,
getIfPresent, getIfPresent,
utf8ArrayToStr,
getPolicyDescriptor,
}; };

View File

@@ -7,7 +7,17 @@ class ResourceCache {
if (!$.read(RESOURCE_CACHE_KEY)) { if (!$.read(RESOURCE_CACHE_KEY)) {
$.write('{}', RESOURCE_CACHE_KEY); $.write('{}', RESOURCE_CACHE_KEY);
} }
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY)); try {
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
} catch (e) {
$.error(
`解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
e?.message ?? e
}`,
);
this.resourceCache = {};
$.write('{}', RESOURCE_CACHE_KEY);
}
this._cleanup(); this._cleanup();
} }

View File

@@ -10,7 +10,17 @@ class ResourceCache {
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) { if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY); $.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
} }
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY)); try {
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
} catch (e) {
$.error(
`解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
e?.message ?? e
}`,
);
this.resourceCache = {};
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
}
this._cleanup(); this._cleanup();
} }

View File

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

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具. 定时任务默认为每天 0 点 #!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分
#!openUrl=https://sub.store #!openUrl=https://sub.store
#!author=Peng-YM #!author=Peng-YM
#!homepage=https://github.com/sub-store-org/Sub-Store #!homepage=https://github.com/sub-store-org/Sub-Store
@@ -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\/((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 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", "name": "Sub-Store",
"description": "定时任务默认为每天 0 点", "description": "定时任务默认为每天 23 点 55 分",
"task": [ "task": [
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" "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

@@ -17,7 +17,9 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule) 2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
> 最新 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) 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 name: Sub-Store
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 0 点 desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分
icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
http: http:
@@ -20,7 +20,7 @@ http:
cron: cron:
script: script:
- name: cron-sync-artifacts - name: cron-sync-artifacts
cron: "0 0 * * *" cron: "55 23 * * *"
timeout: 120 timeout: 120
script-providers: script-providers:

View File

@@ -1,8 +1,8 @@
#!name=Sub-Store(β) #!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=订阅管理 #!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:0 0 * * *,sync:"Sub-Store Sync" #!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默认为每天 0 点\n\n3⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务" #!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] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store

View File

@@ -1,13 +1,13 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点 #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!category=订阅管理 #!category=订阅管理
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗脚本或者cname脚本] 则可以使用此脚本 # 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗脚本或者cname脚本] 则可以使用此脚本
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 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 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 #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 0 点 #!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
#!category=订阅管理 #!category=订阅管理
[MITM] [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 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 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 #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 0 点 #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!category=订阅管理 #!category=订阅管理
[MITM] [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 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 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

@@ -35,6 +35,49 @@ function operator(proxies = [], targetPlatform, context) {
// yaml, // yaml 解析和生成 // yaml, // yaml 解析和生成
// } // }
// 1. Surge 输出 WireGuard 完整配置
// let proxies = await produceArtifact({
// type: 'subscription',
// name: 'sub',
// platform: 'Surge',
// produceOpts: {
// 'include-unsupported-proxy': true,
// }
// })
// $content = proxies
// 2. sing-box
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// let singboxProxies = await produceArtifact({
// type: 'subscription', // type: 'subscription' 或 'collection'
// name: 'sub', // subscription name
// platform: 'sing-box', // target platform
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )
// })
// // JSON
// $content = JSON.stringify({}, null, 2)
// 3. clash.meta
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// let clashMetaProxies = await produceArtifact({
// type: 'subscription',
// name: 'sub',
// platform: 'ClashMeta',
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
// }))
// // YAML
// $content = ProxyUtils.yaml.safeDump({})
// { $content, $files } will be passed to the next operator
// $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具 // flowUtils 为机场订阅流量信息处理工具
// 可参考 https://t.me/zhetengsha/948 // 可参考 https://t.me/zhetengsha/948
// https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104 // https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104

1
web

Submodule web deleted from b10b708c34