Compare commits

...

70 Commits

Author SHA1 Message Date
xream
ce8794463d feat: 区域过滤和协议过滤支持保留模式和过滤模式(前端需 > 2.14.344) 2025-03-02 10:07:38 +08:00
xream
9426f128c4 feat: Surge 输出会判断 HTTP 是否 headers 字段
Some checks are pending
build / build (push) Waiting to run
2025-03-01 21:43:14 +08:00
xream
ebc7173c95 feat: 文件类型为 mihomo 配置时, 来源可以为无
Some checks are pending
build / build (push) Waiting to run
2025-03-01 08:45:17 +08:00
xream
dd4e0cef68 feat: 扩展 scriptResourceCache 缓存, 详见 demo.js
Some checks are pending
build / build (push) Waiting to run
2025-02-28 15:54:04 +08:00
xream
b1618c3803 feat: 支持使用环境变量 SUB_STORE_PRODUCE_CRON 在后台定时处理订阅, 格式为 0 */2 * * *,sub,a;0 */3 * * *,col,b 2025-02-28 14:07:35 +08:00
xream
1b4c046b75 fix: mihomo 覆写可以多次使用
Some checks are pending
build / build (push) Waiting to run
2025-02-27 23:37:39 +08:00
xream
41034ceb46 feat: 规范化 subscription-userinfo 2025-02-27 23:23:32 +08:00
xream
6efb19c856 feat: geo 更新 2025-02-27 17:27:15 +08:00
xream
2cd30dfe68 feat: 内容无变化时 不进行上传; 增加 gist 数量日志
Some checks are pending
build / build (push) Waiting to run
2025-02-26 18:50:11 +08:00
xream
d53947d820 feat: sing-box 支持 anytls
Some checks failed
build / build (push) Has been cancelled
2025-02-23 09:48:09 +08:00
xream
7e75031e92 fix: 修复 short-id 正则
Some checks are pending
build / build (push) Waiting to run
2025-02-22 14:25:06 +08:00
xream
4a07c02dc1 feat: 支持 Shadowrocket Shadowsocks 输入中的 Shadow TLS 参数
Some checks failed
build / build (push) Has been cancelled
2025-02-21 01:44:34 +08:00
xream
95d6688539 fix: 修复 Shadowrocket 输出的 Shadow TLS
Some checks are pending
build / build (push) Waiting to run
2025-02-21 00:50:38 +08:00
xream
a23e2ffcd6 fix: uuid 只辅助判断, 不直接过滤 2025-02-20 22:52:35 +08:00
xream
fda1252d0e fix: 修复 Egern http 传输层 2025-02-20 22:24:39 +08:00
xream
62c5c2e15b fix: 修复 Loon ip-mode
Some checks are pending
build / build (push) Waiting to run
2025-02-19 17:15:31 +08:00
xream
ffabcc9391 feat: 支持 anytls 协议
Some checks are pending
build / build (push) Waiting to run
2025-02-19 17:01:40 +08:00
xream
0825f15d04 feat: Egern 支持 Shadow TLS
Some checks failed
build / build (push) Has been cancelled
2025-02-18 15:07:24 +08:00
xream
fbf6b5ce6e fix: UUID
Some checks failed
build / build (push) Has been cancelled
2025-02-16 05:05:33 +08:00
xream
3eb0816c88 fix: 修复 TUIC URI
Some checks are pending
build / build (push) Waiting to run
2025-02-15 20:47:34 +08:00
xream
8fc755ff02 fix: 文件类型为 mihomo 配置时, 不应处理本地或远程内容字段 2025-02-15 20:32:29 +08:00
xream
6d3d6fa1b3 feat: 仅匹配 UUIDv4 2025-02-15 19:58:34 +08:00
xream
4ef4431c2c feat: 兼容更多 TUIC URI 字段
Some checks are pending
build / build (push) Waiting to run
2025-02-14 23:27:01 +08:00
xream
5058662651 feat: 下载文件名增加前后缀
Some checks are pending
build / build (push) Waiting to run
2025-02-14 15:39:13 +08:00
xream
f9d120bac3 feat: 兼容 v2rayN 非标 TUIC URI
Some checks failed
build / build (push) Has been cancelled
2025-02-13 20:26:59 +08:00
xream
72a445ae33 doc: README 2025-02-12 22:39:18 +08:00
xream
5e2a87e250 fix: 修复 Shadowsocks URI 解析
Some checks are pending
build / build (push) Waiting to run
2025-02-12 19:21:24 +08:00
xream
71fc9affbf feat: 支持 v2ray SOCKS URI 的输入和输出
Some checks are pending
build / build (push) Waiting to run
2025-02-12 03:27:40 +08:00
xream
6f82294c49 fix: 修复 Egern VMess tcp 2025-02-11 23:56:45 +08:00
xream
7c398ba51c fix: 修复 mihomo 覆写配置无法使用普通脚本的问题
Some checks are pending
build / build (push) Waiting to run
2025-02-11 13:18:42 +08:00
xream
7002eee88d feat: 调整 Egern VMess 传输层
Some checks are pending
build / build (push) Waiting to run
2025-02-10 21:02:40 +08:00
xream
bd21d58fe7 feat: VMess/VLESS 校验 uuid
Some checks are pending
build / build (push) Waiting to run
2025-02-10 13:34:58 +08:00
xream
2ea46dcbf1 feat: Shadowsocks URI 部分逻辑修正
Some checks are pending
build / build (push) Waiting to run
2025-02-10 06:44:24 +08:00
xream
4a2a2297f6 feat: Shadowsocks URI 支持 Shadow TLS plugin 2025-02-10 06:32:17 +08:00
xream
07d5a913f0 feat: 同步配置逻辑优化
Some checks are pending
build / build (push) Waiting to run
2025-02-09 20:58:27 +08:00
xream
421df8f0d4 doc: README 2025-02-07 19:43:06 +08:00
xream
e14944dd19 feat: 调整 Egern VMess security 逻辑
Some checks failed
build / build (push) Has been cancelled
2025-02-06 18:18:15 +08:00
xream
bf18c51f6a feat: mihomo 和 Shadowrocket VMess cipher 支持 zero 2025-02-06 18:08:46 +08:00
xream
23e8fbd1b7 feat: Proxy URI Scheme 支持省略端口号(http 默认为 80, tls 默认为 443) 2025-02-06 14:59:50 +08:00
xream
b94b3c366b feat: Egern 正式支持 Shadowsocks 2022
Some checks are pending
build / build (push) Waiting to run
2025-02-06 00:04:54 +08:00
xream
afb5f7b880 feat: 支持 VLESS spx 参数; 支持 Trojan 结合 REALITY/XHTTP 2025-02-05 20:01:41 +08:00
xream
74ec133a79 feat: Loon 正式支持 Shadow-TLS
Some checks failed
build / build (push) Has been cancelled
2025-02-03 13:47:17 +08:00
xream
2a76eb6462 feat: mihomo snell 版本小于 3 的节点, 强制去除 udp 字段, 防止内核报错
Some checks are pending
build / build (push) Waiting to run
2025-02-02 18:59:14 +08:00
xream
9ac5e136a6 feat: 去除订阅流量信息中空字段, 增强兼容性 2025-02-02 18:39:46 +08:00
xream
38f5a97a20 fix: 修复 Surge 输入的 tfo
Some checks failed
build / build (push) Has been cancelled
2025-01-31 15:14:19 +08:00
xream
14a3488ce2 fix: 修复 Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议
Some checks failed
build / build (push) Has been cancelled
2025-01-26 20:41:57 +08:00
xream
6afec4f668 feat: Egern 增加 TUIC
Some checks failed
build / build (push) Has been cancelled
2025-01-23 08:22:48 +08:00
xream
b1874e510d feat: 支持 VLESS XHTTP extra
Some checks are pending
build / build (push) Waiting to run
2025-01-22 09:43:43 +08:00
xream
48aaaf5c99 doc: README 2025-01-21 12:02:49 +08:00
xream
7385e17a4c fix: 修复 Base64 合法性判断
Some checks failed
build / build (push) Has been cancelled
2025-01-17 16:34:30 +08:00
xream
c3daea55ab feat: Loon 节点支持 ip-mode
Some checks failed
build / build (push) Has been cancelled
2025-01-15 23:54:35 +08:00
xream
fc9ff48b1f fix: ss none 必须配置 password 2025-01-15 23:11:34 +08:00
xream
fb21890b68 fix: 修复组合订阅空 subscription-userinfo 的问题
Some checks failed
build / build (push) Has been cancelled
2025-01-14 11:34:02 +08:00
xream
2155cc9639 fix: 修复组合订阅中的单条订阅透传 User-Agent 2025-01-14 08:25:53 +08:00
xream
03e320cbd0 feat: 组合订阅中的单条订阅也支持透传 User-Agent
Some checks are pending
build / build (push) Waiting to run
2025-01-13 20:09:00 +08:00
xream
e325b9a39a feat: Loon 排除 XTLS; 切换使用 esbuild 打包 2025-01-13 16:03:52 +08:00
xream
87597f6fc2 ci: pnpm
Some checks are pending
build / build (push) Waiting to run
2025-01-13 14:44:34 +08:00
xream
3462d36c35 feat: Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议 2025-01-13 14:27:08 +08:00
xream
02946ec81c feat: Surge 默认开启 Shadowsocks 2022 2025-01-13 14:00:38 +08:00
xream
c963c872ff feat: Egern 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022
Some checks failed
build / build (push) Has been cancelled
2025-01-12 06:12:14 +08:00
xream
c4a1bb4ea1 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks/ShadowsocksR + Shadow TLS 2025-01-11 23:34:00 +08:00
xream
f96d9dea74 feat: 日志中增加上传配置的响应
Some checks failed
build / build (push) Has been cancelled
2025-01-09 18:56:36 +08:00
xream
01eb69d8ae ci: GitHub Action
Some checks are pending
build / build (push) Waiting to run
2025-01-09 09:35:07 +08:00
xream
797ba6f601 fix: 修复 Loon Shadow TLS 2025-01-09 09:30:16 +08:00
xream
128353a7f3 feat: gist 单页数量改为 100 2025-01-09 09:14:36 +08:00
xream
e6f6d51608 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks + Shadow TLS V3
Some checks are pending
build / build (push) Waiting to run
2025-01-08 22:52:00 +08:00
xream
589a6bfadb feat: Base64 Pre-processor 检测解码是否正常
Some checks are pending
build / build (push) Waiting to run
2025-01-08 20:13:46 +08:00
xream
75012503f8 fix: 修复 Clash Pre-processor 2025-01-08 19:49:34 +08:00
xream
85a3e2ee54 feat: 文件支持 Mihomo 配置, 支持使用覆写; target 名称适配大小写和别名
Some checks failed
build / build (push) Has been cancelled
2025-01-03 23:37:50 +08:00
xream
95b7557635 feat: Loon 正式支持 Shadowsocks 2022
Some checks failed
build / build (push) Has been cancelled
2024-12-31 23:24:35 +08:00
42 changed files with 7695 additions and 7524 deletions

View File

@@ -1,5 +1,6 @@
name: build
on:
workflow_dispatch:
push:
branches:
- master
@@ -26,18 +27,18 @@ jobs:
run: |
npm install -g pnpm
cd backend && pnpm i --no-frozen-lockfile
- name: Test
run: |
cd backend
pnpm test
- name: Build
run: |
cd backend
pnpm run build
# - name: Test
# run: |
# cd backend
# pnpm test
# - name: Build
# run: |
# cd backend
# pnpm run build
- name: Bundle
run: |
cd backend
pnpm run bundle
pnpm bundle:esbuild
- id: tag
name: Generate release tag
run: |

View File

@@ -26,13 +26,13 @@ Core functionalities:
### Supported Input Formats
> ⚠️ Do not use `Shadowrocket` to export URI and then import it as input. It is not a standard URI.
> ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs.
- [x] Normal Proxy(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
example: `socks5+tls://user:pass@ip:port#name`
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [x] URI(SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)

View File

@@ -5,7 +5,7 @@
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
* Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!
* @updated: <%= updated %>
* @version: <%= pkg.version %>
* @author: Peng-YM

View File

@@ -1,7 +1,7 @@
{
"name": "sub-store",
"version": "2.16.1",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"version": "2.16.64",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -12,6 +12,7 @@
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
"build": "gulp",
"bundle": "node bundle.js",
"bundle:esbuild": "node bundle-esbuild.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
},
"author": "Peng-YM",
@@ -33,6 +34,7 @@
"ms": "^2.1.3",
"nanoid": "^3.3.3",
"request": "^2.88.2",
"semver": "^7.6.3",
"static-js-yaml": "^1.0.0"
},
"devDependencies": {

13502
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import {
isIPv4,
isIPv6,
isValidPortNumber,
isValidUUID,
isNotBlank,
ipAddress,
getRandomPort,
@@ -21,6 +22,8 @@ import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist';
import { isPresent } from './producers/utils';
import { doh } from '@/utils/dns';
function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) {
@@ -75,7 +78,16 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`);
}
}
return proxies;
return proxies.filter((proxy) => {
if (['vless', 'vmess'].includes(proxy.type)) {
const isProxyUUIDValid = isValidUUID(proxy.uuid);
if (!isProxyUUIDValid) {
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
}
// return isProxyUUIDValid;
}
return true;
});
}
async function processFn(
@@ -214,10 +226,22 @@ function produce(proxies, targetPlatform, type, opts = {}) {
);
// filter unsupported proxies
proxies = proxies.filter(
(proxy) =>
!(proxy.supported && proxy.supported[targetPlatform] === false),
);
proxies = proxies.filter((proxy) => {
// 检查代理是否支持目标平台
if (proxy.supported && proxy.supported[targetPlatform] === false) {
return false;
}
// 对于 vless 和 vmess 代理,需要额外验证 UUID
if (['vless', 'vmess'].includes(proxy.type)) {
const isProxyUUIDValid = isValidUUID(proxy.uuid);
if (!isProxyUUIDValid)
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
// return isProxyUUIDValid;
}
return true;
});
proxies = proxies.map((proxy) => {
proxy._resolved = proxy.resolved;
@@ -303,6 +327,8 @@ export const ProxyUtils = {
MMDB,
Gist,
download,
isValidUUID,
doh,
};
function tryParse(parser, line) {
@@ -343,6 +369,14 @@ function lastParse(proxy) {
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}
if (
['ss'].includes(proxy.type) &&
proxy.cipher === 'none' &&
!proxy.password
) {
// https://github.com/MetaCubeX/mihomo/issues/1677
proxy.password = '';
}
if (proxy.interface) {
proxy['interface-name'] = proxy.interface;
delete proxy.interface;
@@ -392,9 +426,14 @@ function lastParse(proxy) {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
proxy.tls = true;
}
@@ -564,6 +603,20 @@ function lastParse(proxy) {
if (!proxy['tls-fingerprint'] && caStr) {
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
}
if (
['shadowsocks'].includes(proxy.type) &&
isPresent(proxy, 'shadow-tls-password')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-version'];
}
return proxy;
}

View File

@@ -11,6 +11,7 @@ import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri';
import $ from '@/core/app';
import { Base64 } from 'js-base64';
@@ -40,8 +41,21 @@ function URI_PROXY() {
// eslint-disable-next-line no-unused-vars
let [__, type, tls, username, password, server, port, query, name] =
line.match(
/^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?):(\d+?)(\?.*?)?(?:#(.*?))?$/,
/^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
if (tls) {
port = 443;
} else if (type === 'http') {
port = 80;
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
$.info(`port is not present in line: ${line}, set to ${port}`);
}
const proxy = {
name:
@@ -62,7 +76,46 @@ function URI_PROXY() {
};
return { name, test, parse };
}
function URI_SOCKS() {
const name = 'URI SOCKS Parser';
const test = (line) => {
return /^socks:\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, auth, server, port, query, name] = line.match(
/^(socks)?:\/\/(?:(.*)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
let username, password;
if (auth) {
const parsed = Base64.decode(decodeURIComponent(auth)).split(':');
username = parsed[0];
password = parsed[1];
}
const proxy = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type: 'socks5',
server,
port,
username,
password,
};
return proxy;
};
return { name, test, parse };
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() {
@@ -99,6 +152,7 @@ function URI_SS() {
query = parsed[2];
}
content = Base64.decode(content);
if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
@@ -112,8 +166,11 @@ function URI_SS() {
}
content = `${content}${query}`;
}
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
userInfoStr = content.match(/(^.*)@/)?.[1];
serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
} else if (content.includes('?')) {
const parsed = content.match(/(\?.*)$/);
query = parsed[1];
}
const serverAndPort = serverAndPortArray[1];
@@ -132,11 +189,12 @@ function URI_SS() {
// }
// handle obfs
const idx = content.indexOf('?plugin=');
if (idx !== -1) {
const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
if (pluginMatch) {
const pluginInfo = (
'plugin=' +
decodeURIComponent(content.split('?plugin=')[1].split('&')[0])
'plugin=' + decodeURIComponent(pluginMatch[1])
).split(';');
const params = {};
for (const item of pluginInfo) {
@@ -161,12 +219,41 @@ function URI_SS() {
tls: getIfPresent(params.tls),
};
break;
case 'shadow-tls': {
proxy.plugin = 'shadow-tls';
const version = getIfNotBlank(params['version']);
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
break;
}
default:
throw new Error(
`Unsupported plugin option: ${params.plugin}`,
);
}
}
// Shadowrocket
if (shadowTlsMatch) {
const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));
const version = getIfNotBlank(params['version']);
const address = getIfNotBlank(params['address']);
const port = getIfNotBlank(params['port']);
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
if (address) {
proxy.server = address;
}
if (port) {
proxy.port = parseInt(port, 10);
}
}
if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true;
}
@@ -529,6 +616,9 @@ function URI_VLESS() {
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts;
@@ -596,6 +686,13 @@ function URI_VLESS() {
// mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none即不使用伪装头部但不可以为空字符串。
proxy.headerType = params.headerType || 'none';
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
}
return proxy;
@@ -763,8 +860,11 @@ function URI_TUIC() {
const parse = (line) => {
line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, password, server, ___, port, ____, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
let [__, auth, server, port, addons = '', name] =
/^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
auth = decodeURIComponent(auth);
let [uuid, ...passwordParts] = auth.split(':');
let password = passwordParts.join(':');
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
@@ -786,12 +886,14 @@ function URI_TUIC() {
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
key = key.replace(/_/g, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['allow-insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['fast-open'].includes(key)) {
proxy.tfo = true;
} else if (['disable-sni', 'reduce-rtt'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else {
@@ -929,6 +1031,7 @@ function Clash_All() {
const proxy = JSON.parse(line);
if (
![
'anytls',
'mieru',
'juicity',
'ss',
@@ -1431,6 +1534,7 @@ function isIP(ip) {
export default [
URI_PROXY(),
URI_SOCKS(),
URI_SS(),
URI_SSR(),
URI_VMess(),

View File

@@ -39,12 +39,12 @@ start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2
return proxy;
}
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)*{
proxy.type = "ssr";
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)* {
proxy.type = "ss";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -54,31 +54,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "socks5";
}
@@ -169,6 +169,11 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = 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_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
@@ -177,6 +182,7 @@ tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pu
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }

View File

@@ -37,12 +37,12 @@ start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2
return proxy;
}
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)*{
proxy.type = "ssr";
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)* {
proxy.type = "ss";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,31 +52,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
proxy.type = "socks5";
}
@@ -167,6 +167,11 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = 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_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
@@ -175,6 +180,7 @@ tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pu
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }

View File

@@ -41,7 +41,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy;
}
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/udp_port/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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,7 +52,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/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)* {
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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -63,25 +63,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/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)* {
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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/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)* {
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/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/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)* {
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/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
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)* {
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/tfo/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)* {
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/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -104,20 +104,20 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/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/fast_open/tfo/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* {
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}

View File

@@ -39,7 +39,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy;
}
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/udp_port/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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -50,7 +50,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/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)* {
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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -61,25 +61,25 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/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)* {
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/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/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)* {
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/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/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)* {
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/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
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)* {
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/tfo/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)* {
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/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -106,16 +106,16 @@ hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* {
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {

View File

@@ -80,6 +80,9 @@ port = digits:[0-9]+ {
}
params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
@@ -115,6 +118,27 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
}
proxy.udp = toBool(params["udp"]);

View File

@@ -78,6 +78,9 @@ port = digits:[0-9]+ {
}
params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
@@ -113,6 +116,27 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
}
proxy.udp = toBool(params["udp"]);

View File

@@ -1,5 +1,6 @@
import { safeLoad } from '@/utils/yaml';
import { Base64 } from 'js-base64';
import $ from '@/core/app';
function HTML() {
const name = 'HTML';
@@ -15,6 +16,7 @@ function Base64Encoded() {
const keys = [
'dm1lc3M', // vmess
'c3NyOi8v', // ssr://
'c29ja3M6Ly', // socks://
'dHJvamFu', // trojan
'c3M6Ly', // ss:/
'c3NkOi8v', // ssd://
@@ -35,8 +37,15 @@ function Base64Encoded() {
);
};
const parse = function (raw) {
raw = Base64.decode(raw);
return raw;
const decoded = Base64.decode(raw);
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
$.error(
`Base64 Pre-processor error: decoded line does not start with protocol`,
);
return raw;
}
return decoded;
};
return { name, test, parse };
}
@@ -48,43 +57,48 @@ function Clash() {
const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies);
};
const parse = function (raw) {
const parse = function (raw, includeProxies) {
// Clash YAML format
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
// 匹配 short-id 冒号后面的值(包含空格和引号)
const afterReplace = raw.replace(
/short-id:([ ]*[^,\n}]*)/g,
/short-id:([ \t]*[^#\n,}]*)/g,
(matched, value) => {
const afterTrim = value.trim();
// 为空
if (!afterTrim || afterTrim === '') {
return 'short-id: ""'
}
// 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`;
} else {
return `short-id: "${afterTrim}"`
}
}
const afterTrim = value.trim();
// 为空
if (!afterTrim || afterTrim === '') {
return 'short-id: ""';
}
// 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`;
} else {
return `short-id: "${afterTrim}"`;
}
},
);
const {
proxies,
'global-client-fingerprint': globalClientFingerprint,
} = safeLoad(afterReplace);
return proxies
.map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
if (globalClientFingerprint && !p['client-fingerprint']) {
p['client-fingerprint'] = globalClientFingerprint;
}
return JSON.stringify(p);
})
.join('\n');
return (
(includeProxies ? 'proxies:\n' : '') +
proxies
.map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
if (globalClientFingerprint && !p['client-fingerprint']) {
p['client-fingerprint'] = globalClientFingerprint;
}
return `${includeProxies ? ' - ' : ''}${JSON.stringify(
p,
)}\n`;
})
.join('')
);
};
return { name, test, parse };
}

View File

@@ -10,6 +10,7 @@ import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import YAML from '@/utils/yaml';
import env from '@/utils/env';
import {
@@ -19,8 +20,49 @@ import {
validCheck,
flowTransfer,
getRmainingDays,
normalizeFlowHeader,
} from '@/utils/flow';
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function trimWrap(str) {
if (str.startsWith('<') && str.endsWith('>')) {
return str.slice(1, -1);
}
return str;
}
function deepMerge(target, _other) {
const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
for (const key in other) {
if (isObject(other[key])) {
if (key.endsWith('!')) {
const k = trimWrap(key.slice(0, -1));
target[k] = other[key];
} else {
const k = trimWrap(key);
if (!target[k]) Object.assign(target, { [k]: {} });
deepMerge(target[k], other[k]);
}
} else if (Array.isArray(other[key])) {
if (key.startsWith('+')) {
const k = trimWrap(key.slice(1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...other[key], ...target[k]];
} else if (key.endsWith('+')) {
const k = trimWrap(key.slice(0, -1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...target[k], ...other[key]];
} else {
const k = trimWrap(key);
Object.assign(target, { [k]: other[key] });
}
} else {
Object.assign(target, { [key]: other[key] });
}
}
return target;
}
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
@@ -321,6 +363,46 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
name: 'Script Operator',
func: async (proxies) => {
let output = proxies;
if (output?.$file?.type === 'mihomoProfile') {
try {
let patch = YAML.safeLoad(script);
let config;
if (output?.$content) {
try {
config = YAML.safeLoad(output?.$content);
} catch (e) {
$.error(e.message ?? e);
}
}
// if (typeof patch !== 'object') patch = {};
if (typeof patch !== 'object')
throw new Error('patch is not an object');
output.$content = ProxyUtils.yaml.safeDump(
deepMerge(
config ||
(output?.$file?.sourceType === 'none'
? {}
: {
proxies: await produceArtifact({
type:
output?.$file?.sourceType ||
'collection',
name: output?.$file?.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true,
},
}),
}),
patch,
),
);
return output;
} catch (e) {
// console.log(e);
}
}
await (async function () {
const operator = createDynamicFunction(
'operator',
@@ -339,9 +421,34 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
'operator',
`async function operator(input = []) {
if (input && (input.$files || input.$content)) {
let { $content, $files, $options } = input
${script}
return { $content, $files, $options }
let { $content, $files, $options, $file } = input
if($file.type === 'mihomoProfile') {
${script}
if(typeof main === 'function') {
let config;
if ($content) {
try {
config = ProxyUtils.yaml.safeLoad($content);
} catch (e) {
console.log(e.message ?? e);
}
}
$content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
proxies: await produceArtifact({
type: $file.sourceType || 'collection',
name: $file.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true
}
}),
})))
}
} else {
${script}
}
return { $content, $files, $options, $file }
} else {
let proxies = input
let list = []
@@ -742,7 +849,12 @@ function UselessFilter() {
}
// filter by regions
function RegionFilter(regions) {
function RegionFilter(input) {
let regions = input?.value || input;
if (!Array.isArray(regions)) {
regions = [];
}
const keep = input?.keep || true;
const REGION_MAP = {
HK: '🇭🇰',
TW: '🇹🇼',
@@ -759,7 +871,8 @@ function RegionFilter(regions) {
// this would be high memory usage
return proxies.map((proxy) => {
const flag = getFlag(proxy.name);
return regions.some((r) => REGION_MAP[r] === flag);
const selected = regions.some((r) => REGION_MAP[r] === flag);
return keep ? selected : !selected;
});
},
};
@@ -791,11 +904,19 @@ function buildRegex(str, ...options) {
}
// filter by proxy types
function TypeFilter(types) {
function TypeFilter(input) {
let types = input?.value || input;
if (!Array.isArray(types)) {
types = [];
}
const keep = input?.keep || true;
return {
name: 'Type Filter',
func: (proxies) => {
return proxies.map((proxy) => types.some((t) => proxy.type === t));
return proxies.map((proxy) => {
const selected = types.some((t) => proxy.type === t);
return keep ? selected : !selected;
});
},
};
}
@@ -995,6 +1116,7 @@ function createDynamicFunction(name, script, $arguments, $options) {
flowTransfer,
validCheck,
getRmainingDays,
normalizeFlowHeader,
};
if ($.env.isLoon) {
return new Function(

View File

@@ -81,6 +81,8 @@ export default function Clash_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -139,6 +141,7 @@ export default function Clash_Producer() {
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;

View File

@@ -32,9 +32,10 @@ export default function ClashMeta_Producer() {
isPresent(proxy, 'cipher') &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
@@ -86,6 +87,8 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -102,6 +105,9 @@ export default function ClashMeta_Producer() {
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
}
}
@@ -157,6 +163,7 @@ export default function ClashMeta_Producer() {
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;
@@ -180,7 +187,7 @@ export default function ClashMeta_Producer() {
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
if (type !== 'internal' || opts['delete-underscore-fields']) {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];

View File

@@ -1,10 +1,12 @@
import { isPresent } from './utils';
export default function Egern_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
const produce = (proxies, type) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
// if (opts['include-unsupported-proxy']) return true;
if (
![
'http',
@@ -14,6 +16,7 @@ export default function Egern_Producer() {
'hysteria2',
'vless',
'vmess',
'tuic',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
((proxy.plugin === 'obfs' &&
@@ -47,17 +50,12 @@ export default function Egern_Producer() {
'salsa20',
'chacha20',
'chacha20-ietf',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher))) ||
(proxy.type === 'vmess' &&
(![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
'zero',
].includes(proxy.cipher) ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) ||
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
(proxy.type === 'trojan' &&
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
@@ -65,13 +63,17 @@ export default function Egern_Producer() {
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts'] ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network)))
proxy.network))) ||
(proxy.type === 'tuic' &&
proxy.token &&
proxy.token.length !== 0)
) {
return false;
}
return true;
})
.map((proxy) => {
const original = { ...proxy };
if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server;
}
@@ -146,6 +148,23 @@ export default function Egern_Producer() {
proxy.obfs = 'salamander';
proxy.obfs_password = proxy['obfs-password'];
}
} else if (proxy.type === 'tuic') {
proxy = {
type: 'tuic',
name: proxy.name,
server: proxy.server,
port: proxy.port,
uuid: proxy.uuid,
password: proxy.password,
next_hop: proxy.next_hop,
sni: proxy.sni,
alpn: Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn || 'h3'],
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
} else if (proxy.type === 'trojan') {
if (proxy.network === 'ws') {
proxy.websocket = {
@@ -168,6 +187,20 @@ export default function Egern_Producer() {
websocket: proxy.websocket,
};
} else if (proxy.type === 'vmess') {
// Egern传输层支持 ws/wss/http1/http2/tls不配置则为 tcp
let security = proxy.cipher;
if (
security &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
].includes(security)
) {
security = 'auto';
}
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
@@ -183,9 +216,11 @@ export default function Egern_Producer() {
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
http1: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
@@ -196,9 +231,29 @@ export default function Egern_Producer() {
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
} else if (proxy.network === 'h2') {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
http2: {
method: proxy['h2-opts']?.method,
path: Array.isArray(proxy['h2-opts']?.path)
? proxy['h2-opts']?.path[0]
: proxy['h2-opts']?.path,
headers: {
Host: Array.isArray(
proxy['h2-opts']?.headers?.Host,
)
? proxy['h2-opts']?.headers?.Host[0]
: proxy['h2-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (
(proxy.network === 'tcp' || !proxy.network) &&
proxy.tls
) {
proxy.transport = {
tls: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
@@ -212,7 +267,7 @@ export default function Egern_Producer() {
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
security,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
@@ -240,7 +295,9 @@ export default function Egern_Producer() {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path,
path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
@@ -278,6 +335,39 @@ export default function Egern_Producer() {
// skip_tls_verify: proxy['skip-cert-verify'],
};
}
if (
[
'http',
'socks5',
'ss',
'trojan',
'vless',
'vmess',
].includes(original.type)
) {
if (isPresent(original, 'shadow-tls-password')) {
if (original['shadow-tls-version'] != 3)
throw new Error(
`shadow-tls version ${original['shadow-tls-version']} is not supported`,
);
proxy.shadow_tls = {
password: original['shadow-tls-password'],
sni: original['shadow-tls-sni'],
};
} else if (
['shadow-tls'].includes(original.plugin) &&
original['plugin-opts']
) {
if (original['plugin-opts'].version != 3)
throw new Error(
`shadow-tls version ${original['plugin-opts'].version} is not supported`,
);
proxy.shadow_tls = {
password: original['plugin-opts'].password,
sni: original['plugin-opts'].host,
};
}
}
delete proxy.subName;
delete proxy.collectionName;

View File

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

View File

@@ -3,11 +3,19 @@ const targetPlatform = 'Loon';
import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Loon_Producer() {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy, opts['include-unsupported-proxy']);
return shadowsocks(proxy);
case 'ssr':
return shadowsocksr(proxy);
case 'trojan':
@@ -32,7 +40,7 @@ export default function Loon_Producer() {
return { produce };
}
function shadowsocks(proxy, includeUnsupportedProxy) {
function shadowsocks(proxy) {
const result = new Result(proxy);
if (
![
@@ -56,9 +64,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
...(includeUnsupportedProxy
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm']
: []),
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
@@ -79,11 +86,50 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
`,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path',
);
} else {
} else if (!['shadow-tls'].includes(proxy.plugin)) {
throw new Error(`plugin ${proxy.plugin} is not supported`);
}
}
// 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',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -92,10 +138,13 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
function shadowsocksr(proxy) {
function shadowsocksr(proxy, includeUnsupportedProxy) {
const result = new Result(proxy);
result.append(
`${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
@@ -112,6 +161,45 @@ function shadowsocksr(proxy) {
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
// 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',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -120,6 +208,9 @@ function shadowsocksr(proxy) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -172,6 +263,8 @@ function trojan(proxy) {
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -249,12 +342,14 @@ function vmess(proxy) {
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
function vless(proxy) {
if (proxy['reality-opts']) {
throw new Error(`VLESS REALITY is unsupported`);
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
throw new Error(`VLESS XTLS/REALITY is not supported`);
}
const result = new Result(proxy);
result.append(
@@ -321,6 +416,8 @@ function vless(proxy) {
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -342,6 +439,8 @@ function http(proxy) {
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -370,6 +469,8 @@ function socks5(proxy) {
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -435,6 +536,8 @@ function wireguard(proxy) {
presharedKey ?? ''
}}]`,
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -482,6 +585,8 @@ function hysteria2(proxy) {
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}

View File

@@ -1,6 +1,6 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ShadowRocket_Producer() {
export default function Shadowrocket_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
const list = proxies
@@ -8,7 +8,7 @@ export default function ShadowRocket_Producer() {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
} else if (['mieru'].includes(proxy.type)) {
} else if (['mieru', 'anytls'].includes(proxy.type)) {
return false;
}
return true;
@@ -32,9 +32,10 @@ export default function ShadowRocket_Producer() {
isPresent(proxy, 'cipher') &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
@@ -102,11 +103,28 @@ export default function ShadowRocket_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} else if (proxy.type === 'ss') {
if (
isPresent(proxy, 'shadow-tls-password') &&
!isPresent(proxy, 'plugin')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
}
}
if (
@@ -160,6 +178,7 @@ export default function ShadowRocket_Producer() {
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;

View File

@@ -641,6 +641,23 @@ const tuic5Parser = (proxy = {}) => {
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const anytlsParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'anytls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (/^\d+$/.test(proxy['idle-session-check-interval']))
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
if (/^\d+$/.test(proxy['idle-session-timeout']))
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6']
@@ -829,6 +846,9 @@ export default function singbox_Producer() {
case 'wireguard':
list.push(wireguardParser(proxy));
break;
case 'anytls':
list.push(anytlsParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,

View File

@@ -187,6 +187,8 @@ export default function Stash_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -245,6 +247,7 @@ export default function Stash_Producer() {
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;

View File

@@ -53,7 +53,7 @@ export default function Surge_Producer() {
return { produce };
}
function shadowsocks(proxy, includeUnsupportedProxy) {
function shadowsocks(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
if (!proxy.cipher) {
@@ -87,9 +87,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
'chacha20',
'chacha20-ietf',
'none',
...(includeUnsupportedProxy
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm']
: []),
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
@@ -127,8 +126,6 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
@@ -160,6 +157,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
@@ -177,6 +176,11 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
@@ -429,6 +433,9 @@ function ssh(proxy) {
return result.toString();
}
function http(proxy) {
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
throw new Error(`headers is unsupported`);
}
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);

View File

@@ -27,6 +27,11 @@ export default function URI_Producer() {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) {
case 'socks5':
result = `socks://${encodeURIComponent(
Base64.encode(`${proxy.username}:${proxy.password}`),
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
break;
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${
@@ -54,6 +59,11 @@ export default function URI_Producer() {
}${opts.tls ? ';tls' : ''}`,
);
break;
case 'shadow-tls':
result += encodeURIComponent(
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
);
break;
default:
throw new Error(
`Unsupported plugin option: ${proxy.plugin}`,
@@ -149,6 +159,7 @@ export default function URI_Producer() {
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
let spx = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
@@ -159,6 +170,10 @@ export default function URI_Producer() {
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
spx = `&spx=${encodeURIComponent(spiderX)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
@@ -188,6 +203,14 @@ export default function URI_Producer() {
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let extra = '';
if (proxy._extra) {
extra = `&extra=${encodeURIComponent(proxy._extra)}`;
}
let mode = '';
if (proxy._mode) {
mode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
let vlessType = proxy.network;
if (
proxy.network === 'ws' &&
@@ -254,7 +277,7 @@ export default function URI_Producer() {
proxy.port
}?security=${encodeURIComponent(
security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
proxy.name,
)}`;
break;
@@ -324,11 +347,41 @@ export default function URI_Producer() {
: proxy.alpn.join(','),
)}`;
}
const trojanIsReality = proxy['reality-opts'];
let trojanSid = '';
let trojanPbk = '';
let trojanSpx = '';
let trojanSecurity = '';
let trojanMode = '';
let trojanExtra = '';
if (trojanIsReality) {
trojanSecurity = `&security=reality`;
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
trojanSid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
}
if (proxy._extra) {
trojanExtra = `&extra=${encodeURIComponent(
proxy._extra,
)}`;
}
if (proxy._mode) {
trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}${trojanAlpn}${trojanFp}#${encodeURIComponent(
}${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
proxy.name,
)}`;
break;
@@ -468,10 +521,13 @@ export default function URI_Producer() {
['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key]
) {
tuicParams.push(`${i}=1`);
tuicParams.push(`${i.replace(/-/g, '_')}=1`);
} else if (proxy[key]) {
tuicParams.push(
`${i}=${encodeURIComponent(proxy[key])}`,
`${i.replace(
/-/g,
'_',
)}=${encodeURIComponent(proxy[key])}`,
);
}
}

View File

@@ -84,6 +84,7 @@ async function doSync() {
const files = {};
try {
const valid = [];
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
@@ -156,27 +157,46 @@ async function doSync() {
files[encodeURIComponent(artifact.name)] = {
content: output,
};
valid.push(artifact.name);
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
`生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
);
invalid.push(artifact.name);
}
}),
);
if (invalid.length > 0) {
$.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
if (valid.length === 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) {
if (artifact.sync) {
if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
@@ -204,9 +224,18 @@ async function doSync() {
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 Sub-Store', '全部订阅同步成功');
$.info('上传配置成功');
if (invalid.length > 0) {
$.notify(
'🌍 Sub-Store',
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.notify('🌍 Sub-Store', '同步配置完成');
}
} catch (e) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`);
$.error(`无法同步订阅配置到 Gist原因${e}`);
$.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
$.error(`无法同步配置到 Gist原因${e}`);
}
}

View File

@@ -59,7 +59,18 @@ function getCollection(req, res) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
`attachment; filename="${encodeURIComponent(
`sub-store_collection_${name}_${new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`,
)
.send(JSON.stringify(collection));
} else {

View File

@@ -1,8 +1,11 @@
import { getPlatformFromHeaders } from '@/utils/user-agent';
import {
getPlatformFromHeaders,
shouldIncludeUnsupportedProxy,
} from '@/utils/user-agent';
import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import $ from '@/core/app';
import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -161,7 +164,19 @@ async function downloadSubscription(req, res) {
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
@@ -244,7 +259,10 @@ async function downloadSubscription(req, res) {
$arguments.flowUrl,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
}
}
} catch (err) {
@@ -278,7 +296,9 @@ async function downloadSubscription(req, res) {
}
res.set(
'subscription-userinfo',
[subUserInfo, flowInfo].filter((i) => i).join('; '),
normalizeFlowHeader(
[subUserInfo, flowInfo].filter((i) => i).join(';'),
),
);
}
@@ -342,11 +362,9 @@ async function downloadCollection(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
`正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
let {
@@ -393,7 +411,18 @@ async function downloadCollection(req, res) {
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
@@ -417,6 +446,7 @@ async function downloadCollection(req, res) {
$options,
proxy,
noCache,
ua: reqUA,
});
let subUserInfoOfSub;
// forward flow header from the first subscription in this collection
@@ -522,13 +552,15 @@ async function downloadCollection(req, res) {
} else {
subUserInfoOfCol = collection.subUserinfo;
}
res.set(
'subscription-userinfo',
[subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; '),
);
const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
if (subUserInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(subUserInfo),
);
}
if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);

View File

@@ -1,5 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
@@ -146,7 +146,10 @@ async function getFile(req, res) {
proxy || file.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
}
}
} catch (err) {
@@ -206,7 +209,18 @@ function getWholeFile(req, res) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
`attachment; filename="${encodeURIComponent(
`sub-store_file_${name}_${new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`,
)
.send(JSON.stringify(file));
} else {

View File

@@ -2,7 +2,7 @@ import express from '@/vendor/express';
import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import { syncArtifacts, produceArtifact } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';
@@ -75,6 +75,39 @@ export default function serve() {
// 'Asia/Shanghai' // timeZone
);
}
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
if (produce_cron) {
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
produce_cron.split(/\s*;\s*/).map((item) => {
const [cron, type, name] = item.split(/\s*,\s*/);
new CronJob(
cron.trim(),
async function () {
try {
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
);
await produceArtifact({ type, name });
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
);
} catch (e) {
$.error(
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
});
}
const backend_download_cron = eval(
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
);

View File

@@ -27,7 +27,18 @@ export default function register($app) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
'attachment; filename="sub-store.json"',
`attachment; filename="${encodeURIComponent(
`sub-store_data_${new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`,
)
.send(
$.env.isNode
@@ -98,6 +109,21 @@ async function gistBackupAction(action) {
const updated = settings.syncTime;
switch (action) {
case 'upload':
try {
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
$.info(`下载备份, 与本地内容对比...`);
const onlineContent = await gist.download(
GIST_BACKUP_FILE_NAME,
);
if (onlineContent === content) {
$.info(`内容一致, 无需上传备份`);
return;
}
} catch (error) {
$.error(`${error.message ?? error}`);
}
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);

View File

@@ -15,46 +15,48 @@ export default function register($app) {
async function previewFile(req, res) {
try {
const file = req.body;
let content;
if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let content = '';
if (file.type !== 'mihomoProfile') {
if (
!file.ignoreFailedRemoteFile &&
Object.keys(errors).length > 0
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
if (
!file.ignoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
}
}
}
// parse proxies
@@ -67,7 +69,7 @@ async function previewFile(req, res) {
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
{ $files: files, $content: filesContent, $file: file },
file.process,
)
: { $content: filesContent, $files: files };

View File

@@ -264,7 +264,18 @@ function getSubscription(req, res) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`,
`attachment; filename="${encodeURIComponent(
`sub-store_subscription_${name}_${new Date()
.toLocaleString('zh-CN', {
year: 'numeric',
day: 'numeric',
month: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})
.replace(/\D/g, '')}.json`,
)}"`,
)
.send(JSON.stringify(sub));
} else {

View File

@@ -43,7 +43,7 @@ async function produceArtifact({
}) {
platform = platform || 'JSON';
if (type === 'subscription') {
if (['subscription', 'sub'].includes(type)) {
let sub;
if (name) {
const allSubs = $.read(SUBS_KEY);
@@ -190,7 +190,7 @@ async function produceArtifact({
}
// produce
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
} else if (['collection', 'col'].includes(type)) {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
@@ -216,6 +216,14 @@ async function produceArtifact({
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
const passThroughUA = sub.passThroughUA;
let reqUA = sub.ua;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
);
reqUA = ua;
}
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
@@ -237,7 +245,7 @@ async function produceArtifact({
try {
return await download(
url,
sub.ua,
reqUA,
undefined,
proxy ||
sub.proxy ||
@@ -402,105 +410,117 @@ async function produceArtifact({
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
let raw = '';
console.log(file);
if (file.type !== 'mihomoProfile') {
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
content &&
!['localFirst', 'remoteFirst'].includes(mergeSources)
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (
!fileIgnoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (
!fileIgnoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
@@ -512,7 +532,12 @@ async function produceArtifact({
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent, $options },
{
$files: files,
$content: filesContent,
$options,
$file: file,
},
file.process,
)
: { $content: filesContent, $files: files, $options };
@@ -527,6 +552,7 @@ async function syncArtifacts() {
const files = {};
try {
const valid = [];
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
@@ -601,27 +627,47 @@ async function syncArtifacts() {
files[encodeURIComponent(artifact.name)] = {
content: output,
};
valid.push(artifact.name);
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
`生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
);
invalid.push(artifact.name);
}
}),
);
if (invalid.length > 0) {
$.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
if (valid.length === 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) {
if (artifact.sync) {
if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
@@ -649,9 +695,17 @@ async function syncArtifacts() {
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功');
$.info('上传配置成功');
if (invalid.length > 0) {
throw new Error(
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.info(`同步配置成功 ${valid.length}`);
}
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
$.error(`同步配置失败,原因:${e.message ?? e}`);
throw e;
}
}
@@ -661,7 +715,7 @@ async function syncAllArtifacts(_, res) {
await syncArtifacts();
success(res);
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
$.error(`同步配置失败,原因:${e.message ?? e}`);
failed(
res,
new InternalServerError(
@@ -738,6 +792,16 @@ async function syncArtifact(req, res) {
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {

View File

@@ -207,7 +207,7 @@ export default async function download(
if (preprocess) {
try {
if (clashPreprocessor.test(body)) {
body = clashPreprocessor.parse(body);
body = clashPreprocessor.parse(body, true);
}
} catch (e) {
$.error(`Clash Pre-processor error: ${e}`);

View File

@@ -313,3 +313,41 @@ export function getRmainingDays(opt = {}) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}
export function normalizeFlowHeader(flowHeaders) {
try {
// 使用 Map 保持顺序并处理重复键
const kvMap = new Map();
flowHeaders
.split(';')
.map((p) => p.trim())
.filter(Boolean)
.forEach((pair) => {
const eqIndex = pair.indexOf('=');
if (eqIndex === -1) return;
const key = pair.slice(0, eqIndex).trim();
const encodedValue = pair.slice(eqIndex + 1).trim();
// 只保留第一个出现的 key
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
const decodedValue = decodeURIComponent(encodedValue);
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);
}
}
});
// 拼接标准化字符串
return Array.from(kvMap.entries())
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
.join('; ');
} catch (e) {
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
return flowHeaders;
}
}

View File

@@ -18,7 +18,9 @@ const ISOFlags = {
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇴': ['BO', 'BOL'],
'🇧🇳': ['BN', 'BRN'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇹': ['BT', 'BTN'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
@@ -40,6 +42,7 @@ const ISOFlags = {
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇬🇹': ['GT', 'GTM'],
'🇬🇺': ['GU', 'GUM'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
@@ -59,12 +62,15 @@ const ISOFlags = {
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇦': ['LA', 'LAO'],
'🇱🇰': ['LK', 'LKA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇲': ['MM', 'MMR'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
@@ -83,6 +89,7 @@ const ISOFlags = {
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇵🇬': ['PG', 'PNG'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
@@ -142,8 +149,10 @@ export function getFlag(name) {
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
'🇧🇭': ['Bahrain', '巴林'],
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
'🇧🇳': ['Brunei', '文莱', '汶萊'],
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
'🇧🇴': ['Bolivia', '玻利维亚'],
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
'🇨🇦': [
'Canada',
'加拿大',
@@ -194,6 +203,7 @@ export function getFlag(name) {
],
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
'🇬🇷': ['Greece', '希腊', '希臘'],
'🇬🇺': ['Guam', '关岛', '關島'],
'🇬🇹': ['Guatemala', '危地马拉'],
'🇭🇰': [
'Hongkong',
@@ -254,11 +264,14 @@ export function getFlag(name) {
'🇮🇷': ['Iran', '伊朗'],
'🇮🇸': ['Iceland', '冰岛', '冰島'],
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
'🇱🇦': ['Laos', '老挝', '老撾'],
'🇱🇹': ['Lithuania', '立陶宛'],
'🇱🇺': ['Luxembourg', '卢森堡'],
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
'🇲🇦': ['Morocco', '摩洛哥'],
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
'🇲🇳': ['Mongolia', '蒙古'],
@@ -284,6 +297,7 @@ export function getFlag(name) {
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
'🇵🇷': ['Puerto Rico', '波多黎各'],
'🇵🇹': ['Portugal', '葡萄牙'],
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
'🇵🇾': ['Paraguay', '巴拉圭'],
'🇷🇴': ['Romania', '罗马尼亚'],
'🇷🇸': ['Serbia', '塞尔维亚'],

View File

@@ -114,15 +114,18 @@ export default class Gist {
return;
});
} else {
return this.http.get('/gists').then((response) => {
const gists = JSON.parse(response.body);
for (let g of gists) {
if (g.description === this.key) {
return g;
return this.http
.get('/gists?per_page=100&page=1')
.then((response) => {
const gists = JSON.parse(response.body);
$.info(`获取到当前 GitHub 用户的 gist: ${gists.length}`);
for (let g of gists) {
if (g.description === this.key) {
return g;
}
}
}
return;
});
return;
});
}
}

View File

@@ -117,7 +117,17 @@ function numberToString(value) {
: BigInt(value).toString();
}
function isValidUUID(uuid) {
return (
typeof uuid === 'string' &&
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
uuid,
)
);
}
export {
isValidUUID,
ipAddress,
isIPv4,
isIPv6,

View File

@@ -24,7 +24,7 @@ class ResourceCache {
this._cleanup();
}
_cleanup() {
_cleanup(prefix, expires) {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
@@ -35,7 +35,11 @@ class ResourceCache {
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
if (
new Date().getTime() - updated.time >
(expires ?? this.expires) ||
(prefix && id.startsWith(prefix))
) {
delete this.resourceCache[id];
clear = true;
}
@@ -52,10 +56,15 @@ class ResourceCache {
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
}
get(id) {
get(id, expires, remove) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].data;
if (updated) {
if (new Date().getTime() - updated <= (expires ?? this.expires))
return this.resourceCache[id].data;
if (remove) {
delete this.resourceCache[id];
this._persist();
}
}
return null;
}

View File

@@ -1,3 +1,7 @@
import gte from 'semver/functions/gte';
import coerce from 'semver/functions/coerce';
import $ from '@/core/app';
export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
@@ -56,3 +60,32 @@ export function getPlatformFromHeaders(headers) {
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA, accept });
}
export function shouldIncludeUnsupportedProxy(platform, ua) {
try {
const target = getPlatformFromUserAgent({
UA: ua,
ua: ua.toLowerCase(),
});
if (!['Stash', 'Egern'].includes(target)) {
return false;
}
const version = coerce(ua).version;
if (
platform === 'Stash' &&
target === 'Stash' &&
gte(version, '2.8.0')
) {
return true;
}
if (
platform === 'Egern' &&
target === 'Egern' &&
gte(version, '1.29.0')
) {
return true;
}
} catch (e) {
$.error(`获取版本号失败: ${e}`);
}
return false;
}

View File

@@ -24,6 +24,10 @@ function operator(proxies = [], targetPlatform, context) {
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
// const fs = eval(`require("fs")`)
// // const path = eval(`require("path")`)
// fs.writeFileSync('/tmp/1.txt', $content, "utf8");
// $arguments 为传入的脚本参数
@@ -48,8 +52,32 @@ function operator(proxies = [], targetPlatform, context) {
// scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache
// cache.set(id, data)
// cache.get(id)
// 设置
// cache.set('a:1', 1)
// cache.set('a:2', 2)
// 获取
// cache.get('a:1')
// 支持第二个参数: 自定义过期时间
// 支持第三个参数: 是否删除过期项
// cache.get('a:2', 1000, true)
// 清理
// cache._cleanup()
// 支持第一个参数: 匹配前缀的项也一起删除
// 支持第二个参数: 自定义过期时间
// cache._cleanup('a:', 1000)
// 关于缓存时长
// 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
// 远程订阅缓存是 1 小时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
// 当使用相关脚本时, 若在对应的脚本中使用参数开启缓存, 可设置持久化缓存 sub-store-csr-expiration-time 的值来自定义默认缓存时长, 默认为 172800000 (48 * 3600 * 1000, 即 48 小时)
// 🎈Loon 可在插件中设置
// 其他平台同理, 持久化缓存数据在 JSON 里
// ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066
@@ -69,6 +97,7 @@ function operator(proxies = [], targetPlatform, context) {
// Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
// isValidUUID, // 辅助判断是否为有效的 UUID
// }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009