Compare commits

...

24 Commits

Author SHA1 Message Date
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
22 changed files with 468 additions and 167 deletions

View File

@@ -32,7 +32,7 @@ Core functionalities:
example: `socks5+tls://user:pass@ip:port#name` 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 Proxies YAML
- [x] Clash Proxy JSON(single line) - [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)

View File

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

View File

@@ -6,6 +6,7 @@ import {
isIPv4, isIPv4,
isIPv6, isIPv6,
isValidPortNumber, isValidPortNumber,
isValidUUID,
isNotBlank, isNotBlank,
ipAddress, ipAddress,
getRandomPort, getRandomPort,
@@ -21,6 +22,8 @@ import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo'; import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import { isPresent } from './producers/utils';
import { doh } from '@/utils/dns';
function preprocess(raw) { function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) { for (const processor of PROXY_PREPROCESSORS) {
@@ -75,7 +78,16 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`); $.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( async function processFn(
@@ -214,10 +226,22 @@ function produce(proxies, targetPlatform, type, opts = {}) {
); );
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter( proxies = proxies.filter((proxy) => {
(proxy) => // 检查代理是否支持目标平台
!(proxy.supported && proxy.supported[targetPlatform] === false), 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) => { proxies = proxies.map((proxy) => {
proxy._resolved = proxy.resolved; proxy._resolved = proxy.resolved;
@@ -303,6 +327,8 @@ export const ProxyUtils = {
MMDB, MMDB,
Gist, Gist,
download, download,
isValidUUID,
doh,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -400,9 +426,14 @@ function lastParse(proxy) {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) { ) {
proxy.tls = true; proxy.tls = true;
} }
@@ -572,6 +603,20 @@ function lastParse(proxy) {
if (!proxy['tls-fingerprint'] && caStr) { if (!proxy['tls-fingerprint'] && caStr) {
proxy['tls-fingerprint'] = rs.generateFingerprint(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; return proxy;
} }

View File

@@ -76,7 +76,46 @@ function URI_PROXY() {
}; };
return { name, test, parse }; 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). // Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme // reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() { function URI_SS() {
@@ -113,6 +152,7 @@ function URI_SS() {
query = parsed[2]; query = parsed[2];
} }
content = Base64.decode(content); content = Base64.decode(content);
if (query) { if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) { if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/); const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
@@ -126,8 +166,11 @@ function URI_SS() {
} }
content = `${content}${query}`; content = `${content}${query}`;
} }
userInfoStr = content.split('@')[0]; userInfoStr = content.match(/(^.*)@/)?.[1];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/); serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
} else if (content.includes('?')) {
const parsed = content.match(/(\?.*)$/);
query = parsed[1];
} }
const serverAndPort = serverAndPortArray[1]; const serverAndPort = serverAndPortArray[1];
@@ -147,6 +190,8 @@ function URI_SS() {
// handle obfs // handle obfs
const pluginMatch = content.match(/[?&]plugin=([^&]+)/); const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
if (pluginMatch) { if (pluginMatch) {
const pluginInfo = ( const pluginInfo = (
'plugin=' + decodeURIComponent(pluginMatch[1]) 'plugin=' + decodeURIComponent(pluginMatch[1])
@@ -190,6 +235,25 @@ function URI_SS() {
); );
} }
} }
// 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)) { if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true; proxy['udp-over-tcp'] = true;
} }
@@ -796,8 +860,11 @@ function URI_TUIC() {
const parse = (line) => { const parse = (line) => {
line = line.split(/tuic:\/\//)[1]; line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, uuid, password, server, ___, port, ____, addons = '', name] = let [__, auth, server, port, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
auth = decodeURIComponent(auth);
let [uuid, ...passwordParts] = auth.split(':');
let password = passwordParts.join(':');
port = parseInt(`${port}`, 10); port = parseInt(`${port}`, 10);
if (isNaN(port)) { if (isNaN(port)) {
port = 443; port = 443;
@@ -819,12 +886,14 @@ function URI_TUIC() {
for (const addon of addons.split('&')) { for (const addon of addons.split('&')) {
let [key, value] = addon.split('='); let [key, value] = addon.split('=');
key = key.replace(/_/, '-'); key = key.replace(/_/g, '-');
value = decodeURIComponent(value); value = decodeURIComponent(value);
if (['alpn'].includes(key)) { if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined; proxy[key] = value ? value.split(',') : undefined;
} else if (['allow-insecure'].includes(key)) { } else if (['allow-insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value); 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)) { } else if (['disable-sni', 'reduce-rtt'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value); proxy[key] = /(TRUE)|1/i.test(value);
} else { } else {
@@ -962,6 +1031,7 @@ function Clash_All() {
const proxy = JSON.parse(line); const proxy = JSON.parse(line);
if ( if (
![ ![
'anytls',
'mieru', 'mieru',
'juicity', 'juicity',
'ss', 'ss',
@@ -1464,6 +1534,7 @@ function isIP(ip) {
export default [ export default [
URI_PROXY(), URI_PROXY(),
URI_SOCKS(),
URI_SS(), URI_SS(),
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),

View File

@@ -16,6 +16,7 @@ function Base64Encoded() {
const keys = [ const keys = [
'dm1lc3M', // vmess 'dm1lc3M', // vmess
'c3NyOi8v', // ssr:// 'c3NyOi8v', // ssr://
'c29ja3M6Ly', // socks://
'dHJvamFu', // trojan 'dHJvamFu', // trojan
'c3M6Ly', // ss:/ 'c3M6Ly', // ss:/
'c3NkOi8v', // ssd:// 'c3NkOi8v', // ssd://
@@ -62,7 +63,7 @@ function Clash() {
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity // 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
// 匹配 short-id 冒号后面的值(包含空格和引号) // 匹配 short-id 冒号后面的值(包含空格和引号)
const afterReplace = raw.replace( const afterReplace = raw.replace(
/short-id:([ ]*[^,\n}]*)/g, /short-id:([ \t]*[^#\n,}]*)/g,
(matched, value) => { (matched, value) => {
const afterTrim = value.trim(); const afterTrim = value.trim();

View File

@@ -365,7 +365,9 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
if (output?.$file?.type === 'mihomoProfile') { if (output?.$file?.type === 'mihomoProfile') {
try { try {
let patch = YAML.safeLoad(script); let patch = YAML.safeLoad(script);
if (typeof patch !== 'object') patch = {}; // if (typeof patch !== 'object') patch = {};
if (typeof patch !== 'object')
throw new Error('patch is not an object');
output.$content = ProxyUtils.yaml.safeDump( output.$content = ProxyUtils.yaml.safeDump(
deepMerge( deepMerge(
{ {

View File

@@ -141,6 +141,7 @@ export default function Clash_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;

View File

@@ -105,6 +105,9 @@ export default function ClashMeta_Producer() {
password: proxy['shadow-tls-password'], password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'], version: proxy['shadow-tls-version'],
}; };
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
} }
} }
@@ -160,6 +163,7 @@ export default function ClashMeta_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;

View File

@@ -1,6 +1,8 @@
import { isPresent } from './utils';
export default function Egern_Producer() { export default function Egern_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies, type, opts = {}) => { const produce = (proxies, type) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies // https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies const list = proxies
.filter((proxy) => { .filter((proxy) => {
@@ -71,6 +73,7 @@ export default function Egern_Producer() {
return true; return true;
}) })
.map((proxy) => { .map((proxy) => {
const original = { ...proxy };
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server; proxy.sni = proxy.server;
} }
@@ -184,6 +187,7 @@ export default function Egern_Producer() {
websocket: proxy.websocket, websocket: proxy.websocket,
}; };
} else if (proxy.type === 'vmess') { } else if (proxy.type === 'vmess') {
// Egern传输层支持 ws/wss/http1/http2/tls不配置则为 tcp
let security = proxy.cipher; let security = proxy.cipher;
if ( if (
security && security &&
@@ -212,9 +216,11 @@ export default function Egern_Producer() {
}; };
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
proxy.transport = { proxy.transport = {
http: { http1: {
method: proxy['http-opts']?.method, 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: { headers: {
Host: Array.isArray( Host: Array.isArray(
proxy['http-opts']?.headers?.Host, proxy['http-opts']?.headers?.Host,
@@ -225,9 +231,29 @@ export default function Egern_Producer() {
skip_tls_verify: proxy['skip-cert-verify'], skip_tls_verify: proxy['skip-cert-verify'],
}, },
}; };
} else if (proxy.network === 'tcp' || !proxy.network) { } else if (proxy.network === 'h2') {
proxy.transport = { 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, sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls skip_tls_verify: proxy.tls
? proxy['skip-cert-verify'] ? proxy['skip-cert-verify']
@@ -269,7 +295,9 @@ export default function Egern_Producer() {
proxy.transport = { proxy.transport = {
http: { http: {
method: proxy['http-opts']?.method, 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: { headers: {
Host: Array.isArray( Host: Array.isArray(
proxy['http-opts']?.headers?.Host, proxy['http-opts']?.headers?.Host,
@@ -307,6 +335,39 @@ export default function Egern_Producer() {
// skip_tls_verify: proxy['skip-cert-verify'], // 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.subName;
delete proxy.collectionName; delete proxy.collectionName;

View File

@@ -341,10 +341,9 @@ function vmess(proxy) {
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
const ip_version =
ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
@@ -416,10 +415,9 @@ function vless(proxy) {
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
const ip_version =
ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }

View File

@@ -8,7 +8,7 @@ export default function Shadowrocket_Producer() {
if (opts['include-unsupported-proxy']) return true; if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') { if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false; return false;
} else if (['mieru'].includes(proxy.type)) { } else if (['mieru', 'anytls'].includes(proxy.type)) {
return false; return false;
} }
return true; return true;
@@ -110,6 +110,21 @@ export default function Shadowrocket_Producer() {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
delete proxy.sni; delete proxy.sni;
} }
} else if (proxy.type === 'ss') {
if (
isPresent(proxy, 'shadow-tls-password') &&
!isPresent(proxy, 'plugin')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
}
} }
if ( if (
@@ -163,6 +178,7 @@ export default function Shadowrocket_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;

View File

@@ -641,6 +641,23 @@ const tuic5Parser = (proxy = {}) => {
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return 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 wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6'] const local_address = ['ip', 'ipv6']
@@ -829,6 +846,9 @@ export default function singbox_Producer() {
case 'wireguard': case 'wireguard':
list.push(wireguardParser(proxy)); list.push(wireguardParser(proxy));
break; break;
case 'anytls':
list.push(anytlsParser(proxy));
break;
default: default:
throw new Error( throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`, `Platform sing-box does not support proxy type: ${proxy.type}`,

View File

@@ -247,6 +247,7 @@ export default function Stash_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;

View File

@@ -27,6 +27,11 @@ export default function URI_Producer() {
proxy.server = `[${proxy.server}]`; proxy.server = `[${proxy.server}]`;
} }
switch (proxy.type) { switch (proxy.type) {
case 'socks5':
result = `socks://${encodeURIComponent(
Base64.encode(`${proxy.username}:${proxy.password}`),
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
break;
case 'ss': case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`; const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${ result = `ss://${
@@ -516,10 +521,13 @@ export default function URI_Producer() {
['disable-sni', 'reduce-rtt'].includes(key) && ['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key] proxy[key]
) { ) {
tuicParams.push(`${i}=1`); tuicParams.push(`${i.replace(/-/g, '_')}=1`);
} else if (proxy[key]) { } else if (proxy[key]) {
tuicParams.push( tuicParams.push(
`${i}=${encodeURIComponent(proxy[key])}`, `${i.replace(
/-/g,
'_',
)}=${encodeURIComponent(proxy[key])}`,
); );
} }
} }

View File

@@ -59,7 +59,18 @@ function getCollection(req, res) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', '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)); .send(JSON.stringify(collection));
} else { } else {

View File

@@ -209,7 +209,18 @@ function getWholeFile(req, res) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', '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)); .send(JSON.stringify(file));
} else { } else {

View File

@@ -27,7 +27,18 @@ export default function register($app) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', '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( .send(
$.env.isNode $.env.isNode

View File

@@ -15,46 +15,48 @@ export default function register($app) {
async function previewFile(req, res) { async function previewFile(req, res) {
try { try {
const file = req.body; const file = req.body;
let content; let content = '';
if ( if (file.type !== 'mihomoProfile') {
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 '';
}
}),
);
if ( if (
!file.ignoreFailedRemoteFile && file.source === 'local' &&
Object.keys(errors).length > 0 !['localFirst', 'remoteFirst'].includes(file.mergeSources)
) { ) {
throw new Error( content = file.content;
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( } 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') { if (
content.unshift(file.content); !file.ignoreFailedRemoteFile &&
} else if (file.mergeSources === 'remoteFirst') { Object.keys(errors).length > 0
content.push(file.content); ) {
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 // parse proxies

View File

@@ -264,7 +264,18 @@ function getSubscription(req, res) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', '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)); .send(JSON.stringify(sub));
} else { } else {

View File

@@ -410,105 +410,117 @@ async function produceArtifact({
const allFiles = $.read(FILES_KEY); const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name); const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`); if (!file) throw new Error(`找不到文件 ${name}`);
let raw; let raw = '';
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { console.log(file);
raw = content; if (file.type !== 'mihomoProfile') {
} 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;
if ( if (
ignoreFailedRemoteFile != null && content &&
ignoreFailedRemoteFile !== '' !['localFirst', 'remoteFirst'].includes(mergeSources)
) { ) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; raw = content;
} } else if (url) {
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) { const errors = {};
throw new Error( raw = await Promise.all(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( 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 (mergeSources === 'localFirst') { if (
raw.unshift(content); ignoreFailedRemoteFile != null &&
} else if (mergeSources === 'remoteFirst') { ignoreFailedRemoteFile !== ''
raw.push(content); ) {
} fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
} else if ( }
file.source === 'local' && if (
!['localFirst', 'remoteFirst'].includes(file.mergeSources) !fileIgnoreFailedRemoteFile &&
) { Object.keys(errors).length > 0
raw = file.content; ) {
} else { throw new Error(
const errors = {}; `文件 ${file.name} 的远程文件 ${Object.keys(
raw = await Promise.all( errors,
file.url ).join(', ')} 发生错误, 请查看日志`,
.split(/[\r\n]+/) );
.map((i) => i.trim()) }
.filter((i) => i.length) if (mergeSources === 'localFirst') {
.map(async (url) => { raw.unshift(content);
try { } else if (mergeSources === 'remoteFirst') {
return await download( raw.push(content);
url, }
ua || file.ua, } else if (
undefined, file.source === 'local' &&
file.proxy || proxy, !['localFirst', 'remoteFirst'].includes(file.mergeSources)
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) { ) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; raw = file.content;
} } else {
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) { const errors = {};
throw new Error( raw = await Promise.all(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( 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 (file.mergeSources === 'localFirst') { if (
raw.unshift(file.content); ignoreFailedRemoteFile != null &&
} else if (file.mergeSources === 'remoteFirst') { ignoreFailedRemoteFile !== ''
raw.push(file.content); ) {
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(); const files = (Array.isArray(raw) ? raw : [raw]).flat();

View File

@@ -117,7 +117,17 @@ function numberToString(value) {
: BigInt(value).toString(); : 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 { export {
isValidUUID,
ipAddress, ipAddress,
isIPv4, isIPv4,
isIPv6, isIPv6,

View File

@@ -24,6 +24,10 @@ function operator(proxies = [], targetPlatform, context) {
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp` // 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块 // 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 为传入的脚本参数 // $arguments 为传入的脚本参数
@@ -69,6 +73,7 @@ function operator(proxies = [], targetPlatform, context) {
// Gist, // Gist 类 // Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js // download, // 内部的下载方法, 见 backend/src/utils/download.js
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269 // 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 // 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009