From 1d477e6fe104ad1f23d6159eafedfc4530f26113 Mon Sep 17 00:00:00 2001 From: Roy Li Date: Tue, 18 Apr 2023 15:01:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=86=E5=8C=96=20getNodeNames?= =?UTF-8?q?=EF=BC=9B=E5=A2=9E=E5=8A=A0=20Clash=20=E7=9A=84=20shadow-tls=20?= =?UTF-8?q?=E5=92=8C=20Wireguard=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 + src/utils/clash.ts | 75 +++++++++- src/utils/index.ts | 9 +- src/utils/quantumult.ts | 39 ++++- src/utils/surfboard.ts | 153 +++++++++++-------- src/utils/surge.ts | 250 ++++++++++++++++++-------------- src/validators/common.ts | 1 + src/validators/surgio-config.ts | 1 + 8 files changed, 353 insertions(+), 176 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a53dcf0bd..c9ad2c7e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,5 +19,6 @@ module.exports = { rules: { '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-explicit-any': 0, }, }; diff --git a/src/utils/clash.ts b/src/utils/clash.ts index 40f596d52..757e4be2e 100644 --- a/src/utils/clash.ts +++ b/src/utils/clash.ts @@ -10,7 +10,19 @@ import { SimpleNodeConfig, SortedNodeNameFilterType, } from '../types'; -import { applyFilter } from './filter'; +import { + applyFilter, + httpFilter, + httpsFilter, + shadowsocksFilter, + shadowsocksrFilter, + snellFilter, + socks5Filter, + trojanFilter, + tuicFilter, + vmessFilter, +} from './filter'; +import { getPortFromHost } from './index'; const logger = createLogger({ service: 'surgio:utils:clash' }); @@ -32,6 +44,16 @@ export const getClashNodes = function ( switch (nodeConfig.type) { case NodeTypeEnum.Shadowsocks: + if ( + nodeConfig.shadowTls && + !nodeConfig.clashConfig?.enableShadowTls + ) { + logger.warn( + `尚未开启 Clash 的 shadow-tls 支持,节点 ${nodeConfig.nodeName} 将被忽略。如需开启,请在配置文件中设置 clashConfig.enableShadowTls 为 true。`, + ); + return null; + } + return { type: 'ss', cipher: nodeConfig.method, @@ -71,6 +93,21 @@ export const getClashNodes = function ( }, } : null), + ...(nodeConfig.shadowTls && nodeConfig.clashConfig?.enableShadowTls + ? { + plugin: 'shadow-tls', + 'client-fingerprint': 'chrome', + 'plugin-opts': { + password: nodeConfig.shadowTls.password, + ...(nodeConfig.shadowTls.version + ? { version: nodeConfig.shadowTls.version } + : null), + ...(nodeConfig.shadowTls.sni + ? { host: nodeConfig.shadowTls.sni } + : null), + }, + } + : null), }; case NodeTypeEnum.Vmess: @@ -211,7 +248,7 @@ export const getClashNodes = function ( case NodeTypeEnum.Tuic: if (!nodeConfig.clashConfig?.enableTuic) { logger.warn( - `默认不为 Clash 生成 Tuic 节点,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableTuic 为 true。`, + `尚未开启 Clash 的 TUIC 支持,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableTuic 为 true。`, ); return null; } @@ -235,6 +272,24 @@ export const getClashNodes = function ( 'skip-cert-verify': nodeConfig.skipCertVerify === true, }; + case NodeTypeEnum.Wireguard: + return { + type: 'wireguard', + name: nodeConfig.nodeName, + server: nodeConfig.endpoint, + port: getPortFromHost(nodeConfig.endpoint), + 'private-key': nodeConfig.privateKey, + 'public-key': nodeConfig.publicKey, + ip: nodeConfig.selfIp, + ...(nodeConfig.selfIpV6 ? { ipv6: nodeConfig.selfIpV6 } : null), + ...(nodeConfig.mtu ? { mtu: nodeConfig.mtu } : null), + ...(nodeConfig.presharedKey + ? { 'preshared-key': nodeConfig.presharedKey } + : null), + ...(nodeConfig.dnsServer ? { dns: nodeConfig.dnsServer } : null), + udp: true, + }; + // istanbul ignore next default: logger.warn( @@ -265,7 +320,21 @@ export const getClashNodeNames = function ( } result = result.concat( - applyFilter(list, filter).map((item) => item.nodeName), + applyFilter( + list.filter( + (item) => + shadowsocksFilter(item) || + shadowsocksrFilter(item) || + vmessFilter(item) || + httpFilter(item) || + httpsFilter(item) || + trojanFilter(item) || + snellFilter(item) || + socks5Filter(item) || + (item.clashConfig?.enableTuic && tuicFilter(item)), + ), + filter, + ).map((item) => item.nodeName), ); return result; diff --git a/src/utils/index.ts b/src/utils/index.ts index 94da66f0b..d88c30d1c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,7 +6,6 @@ import queryString from 'query-string'; import { JsonObject } from 'type-fest'; import { URL, URLSearchParams } from 'url'; import URLSafeBase64 from 'urlsafe-base64'; -import YAML from 'yaml'; import net from 'net'; import crypto from 'crypto'; import { camelCase, snakeCase, paramCase } from 'change-case'; @@ -589,3 +588,11 @@ export const isGFWFree = (): boolean => export const assertNever = (x: never): never => { throw new TypeError(`Unexpected object: ${x}`); }; + +export const getPortFromHost = (host: string): number => { + const match = host.match(/:(\d+)$/); + if (match) { + return Number(match[1]); + } + throw new Error(`Invalid host: ${host}`); +}; diff --git a/src/utils/quantumult.ts b/src/utils/quantumult.ts index e251bb1f0..f3b29a454 100644 --- a/src/utils/quantumult.ts +++ b/src/utils/quantumult.ts @@ -6,9 +6,19 @@ import { NodeNameFilterType, NodeTypeEnum, PossibleNodeConfigType, + SimpleNodeConfig, SortedNodeNameFilterType, } from '../types'; -import { applyFilter } from './filter'; +import { + applyFilter, + httpFilter, + httpsFilter, + shadowsocksFilter, + shadowsocksrFilter, + socks5Filter, + trojanFilter, + vmessFilter, +} from './filter'; import { pickAndFormatStringList } from './index'; const logger = createLogger({ service: 'surgio:utils:quantumult' }); @@ -278,3 +288,30 @@ export const getQuantumultXNodes = function ( return result.join('\n'); }; + +export const getQuantumultXNodeNames = function ( + list: ReadonlyArray, + filter?: NodeNameFilterType | SortedNodeNameFilterType, + separator?: string, +): string { + // istanbul ignore next + if (arguments.length === 2 && typeof filter === 'undefined') { + throw new Error(ERR_INVALID_FILTER); + } + + return applyFilter( + list.filter( + (item) => + shadowsocksFilter(item) || + shadowsocksrFilter(item) || + vmessFilter(item) || + httpFilter(item) || + httpsFilter(item) || + trojanFilter(item) || + socks5Filter(item), + ), + filter, + ) + .map((item) => item.nodeName) + .join(separator || ', '); +}; diff --git a/src/utils/surfboard.ts b/src/utils/surfboard.ts index b76d8976c..57a7cd779 100644 --- a/src/utils/surfboard.ts +++ b/src/utils/surfboard.ts @@ -1,18 +1,30 @@ import { createLogger } from '@surgio/logger'; import _ from 'lodash'; + import { ERR_INVALID_FILTER, OBFS_UA } from '../constant'; import { HttpNodeConfig, HttpsNodeConfig, NodeFilterType, + NodeNameFilterType, NodeTypeEnum, PossibleNodeConfigType, ShadowsocksNodeConfig, + SimpleNodeConfig, SortedNodeNameFilterType, VmessNodeConfig, } from '../types'; import { pickAndFormatStringList } from './index'; -import { applyFilter } from './filter'; +import { + applyFilter, + httpFilter, + httpsFilter, + shadowsocksFilter, + shadowsocksrFilter, + socks5Filter, + trojanFilter, + vmessFilter, +} from './filter'; const logger = createLogger({ service: 'surgio:utils:surfboard' }); @@ -40,26 +52,22 @@ export const getSurfboardNodes = function ( .map((nodeConfig): string | undefined => { switch (nodeConfig.type) { case NodeTypeEnum.Shadowsocks: { - const config = nodeConfig as ShadowsocksNodeConfig; - - if (config.obfs && ['ws', 'wss'].includes(config.obfs)) { + if (nodeConfig.obfs && ['ws', 'wss'].includes(nodeConfig.obfs)) { logger.warn( - `不支持为 Surfboard 生成 v2ray-plugin 的 Shadowsocks 节点,节点 ${ - nodeConfig!.nodeName - } 会被省略`, + `不支持为 Surfboard 生成 v2ray-plugin 的 Shadowsocks 节点,节点 ${nodeConfig.nodeName} 会被省略`, ); return void 0; } return [ - config.nodeName, + nodeConfig.nodeName, [ 'ss', - config.hostname, - config.port, - 'encrypt-method=' + config.method, + nodeConfig.hostname, + nodeConfig.port, + 'encrypt-method=' + nodeConfig.method, ...pickAndFormatStringList( - config, + nodeConfig, ['password', 'udpRelay', 'obfs', 'obfsHost'], { keyFormat: 'kebabCase', @@ -70,91 +78,87 @@ export const getSurfboardNodes = function ( } case NodeTypeEnum.HTTPS: { - const config = nodeConfig as HttpsNodeConfig; - return [ - config.nodeName, + nodeConfig.nodeName, [ 'https', - config.hostname, - config.port, - config.username, - config.password, - ...(typeof config.skipCertVerify === 'boolean' - ? [`skip-cert-verify=${config.skipCertVerify}`] + nodeConfig.hostname, + nodeConfig.port, + nodeConfig.username, + nodeConfig.password, + ...(typeof nodeConfig.skipCertVerify === 'boolean' + ? [`skip-cert-verify=${nodeConfig.skipCertVerify}`] : []), - ...pickAndFormatStringList(config, ['sni']), + ...pickAndFormatStringList(nodeConfig, ['sni']), ].join(', '), ].join(' = '); } case NodeTypeEnum.HTTP: { - const config = nodeConfig as HttpNodeConfig; - return [ - config.nodeName, + nodeConfig.nodeName, [ 'http', - config.hostname, - config.port, - config.username, - config.password, + nodeConfig.hostname, + nodeConfig.port, + nodeConfig.username, + nodeConfig.password, ].join(', '), ].join(' = '); } case NodeTypeEnum.Vmess: { - const config = nodeConfig as VmessNodeConfig; - - const configList = [ + const result = [ 'vmess', - config.hostname, - config.port, - `username=${config.uuid}`, + nodeConfig.hostname, + nodeConfig.port, + `username=${nodeConfig.uuid}`, ]; if ( - ['chacha20-ietf-poly1305', 'aes-128-gcm'].includes(config.method) + ['chacha20-ietf-poly1305', 'aes-128-gcm'].includes( + nodeConfig.method, + ) ) { - configList.push(`encrypt-method=${config.method}`); + result.push(`encrypt-method=${nodeConfig.method}`); } - if (config.network === 'ws') { - configList.push('ws=true'); - configList.push(`ws-path=${config.path}`); - configList.push( + if (nodeConfig.network === 'ws') { + result.push('ws=true'); + result.push(`ws-path=${nodeConfig.path}`); + result.push( 'ws-headers=' + JSON.stringify( getSurfboardExtendHeaders({ - host: config.host || config.hostname, + host: nodeConfig.host || nodeConfig.hostname, 'user-agent': OBFS_UA, - ..._.omit(config.wsHeaders, ['host']), // host 本质上是一个头信息,所以可能存在冲突的情况。以 host 属性为准。 + ..._.omit(nodeConfig.wsHeaders, ['host']), // host 本质上是一个头信息,所以可能存在冲突的情况。以 host 属性为准。 }), ), ); } - if (config.tls) { - configList.push( + if (nodeConfig.tls) { + result.push( 'tls=true', - ...(typeof config.skipCertVerify === 'boolean' - ? [`skip-cert-verify=${config.skipCertVerify}`] + ...(typeof nodeConfig.skipCertVerify === 'boolean' + ? [`skip-cert-verify=${nodeConfig.skipCertVerify}`] : []), - ...(config.host ? [`sni=${config.host}`] : []), + ...(nodeConfig.host ? [`sni=${nodeConfig.host}`] : []), ); } if (nodeConfig?.surfboardConfig?.vmessAEAD) { - configList.push('vmess-aead=true'); + result.push('vmess-aead=true'); } else { - configList.push('vmess-aead=false'); + result.push('vmess-aead=false'); } - return [config.nodeName, configList.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Trojan: { - const configList: string[] = [ + const result: string[] = [ 'trojan', nodeConfig.hostname, `${nodeConfig.port}`, @@ -166,11 +170,11 @@ export const getSurfboardNodes = function ( ]; if (nodeConfig.network === 'ws') { - configList.push('ws=true'); - configList.push(`ws-path=${nodeConfig.wsPath}`); + result.push('ws=true'); + result.push(`ws-path=${nodeConfig.wsPath}`); if (nodeConfig.wsHeaders) { - configList.push( + result.push( 'ws-headers=' + JSON.stringify( getSurfboardExtendHeaders(nodeConfig.wsHeaders), @@ -179,11 +183,11 @@ export const getSurfboardNodes = function ( } } - return [nodeConfig.nodeName, configList.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Socks5: { - const config = [ + const result = [ nodeConfig.tls === true ? 'socks5-tls' : 'socks5', nodeConfig.hostname, nodeConfig.port, @@ -195,7 +199,7 @@ export const getSurfboardNodes = function ( ]; if (nodeConfig.tls === true) { - config.push( + result.push( ...(typeof nodeConfig.skipCertVerify === 'boolean' ? [`skip-cert-verify=${nodeConfig.skipCertVerify}`] : []), @@ -205,15 +209,13 @@ export const getSurfboardNodes = function ( ); } - return [nodeConfig.nodeName, config.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } // istanbul ignore next default: logger.warn( - `不支持为 Surfboard 生成 ${(nodeConfig as any).type} 的节点,节点 ${ - (nodeConfig as any).nodeName - } 会被省略`, + `不支持为 Surfboard 生成 ${nodeConfig.type} 的节点,节点 ${nodeConfig.nodeName} 会被省略`, ); return void 0; } @@ -222,3 +224,30 @@ export const getSurfboardNodes = function ( return result.join('\n'); }; + +export const getSurfboardNodeNames = function ( + list: ReadonlyArray, + filter?: NodeNameFilterType | SortedNodeNameFilterType, + separator?: string, +): string { + // istanbul ignore next + if (arguments.length === 2 && typeof filter === 'undefined') { + throw new Error(ERR_INVALID_FILTER); + } + + return applyFilter( + list.filter( + (item) => + shadowsocksFilter(item) || + shadowsocksrFilter(item) || + vmessFilter(item) || + httpFilter(item) || + httpsFilter(item) || + trojanFilter(item) || + socks5Filter(item), + ), + filter, + ) + .map((item) => item.nodeName) + .join(separator || ', '); +}; diff --git a/src/utils/surge.ts b/src/utils/surge.ts index 5ac48426f..d6d42200b 100644 --- a/src/utils/surge.ts +++ b/src/utils/surge.ts @@ -1,20 +1,29 @@ import { createLogger } from '@surgio/logger'; import _ from 'lodash'; + import { ERR_INVALID_FILTER, OBFS_UA } from '../constant'; import { - HttpNodeConfig, - HttpsNodeConfig, NodeFilterType, + NodeNameFilterType, NodeTypeEnum, PossibleNodeConfigType, - ShadowsocksNodeConfig, - ShadowsocksrNodeConfig, - SnellNodeConfig, + SimpleNodeConfig, SortedNodeNameFilterType, - VmessNodeConfig, } from '../types'; -import { isIp, pickAndFormatStringList } from './index'; -import { applyFilter } from './filter'; +import { isIp, pickAndFormatStringList } from './'; +import { + applyFilter, + httpFilter, + httpsFilter, + shadowsocksFilter, + shadowsocksrFilter, + snellFilter, + socks5Filter, + trojanFilter, + tuicFilter, + vmessFilter, + wireguardFilter, +} from './filter'; const logger = createLogger({ service: 'surgio:utils:surge' }); @@ -42,26 +51,22 @@ export const getSurgeNodes = function ( .map((nodeConfig): string | undefined => { switch (nodeConfig.type) { case NodeTypeEnum.Shadowsocks: { - const config = nodeConfig as ShadowsocksNodeConfig; - - if (config.obfs && ['ws', 'wss'].includes(config.obfs)) { + if (nodeConfig.obfs && ['ws', 'wss'].includes(nodeConfig.obfs)) { logger.warn( - `不支持为 Surge 生成 v2ray-plugin 的 Shadowsocks 节点,节点 ${ - nodeConfig!.nodeName - } 会被省略`, + `不支持为 Surge 生成 v2ray-plugin 的 Shadowsocks 节点,节点 ${nodeConfig.nodeName} 会被省略`, ); return void 0; } return [ - config.nodeName, + nodeConfig.nodeName, [ 'ss', - config.hostname, - config.port, - 'encrypt-method=' + config.method, + nodeConfig.hostname, + nodeConfig.port, + 'encrypt-method=' + nodeConfig.method, ...pickAndFormatStringList( - config, + nodeConfig, [ 'password', 'udpRelay', @@ -82,18 +87,16 @@ export const getSurgeNodes = function ( } case NodeTypeEnum.HTTPS: { - const config = nodeConfig as HttpsNodeConfig; - return [ - config.nodeName, + nodeConfig.nodeName, [ 'https', - config.hostname, - config.port, - config.username, - config.password, + nodeConfig.hostname, + nodeConfig.port, + nodeConfig.username, + nodeConfig.password, ...pickAndFormatStringList( - config, + nodeConfig, [ 'sni', 'tfo', @@ -114,18 +117,16 @@ export const getSurgeNodes = function ( } case NodeTypeEnum.HTTP: { - const config = nodeConfig as HttpNodeConfig; - return [ - config.nodeName, + nodeConfig.nodeName, [ 'http', - config.hostname, - config.port, - config.username, - config.password, + nodeConfig.hostname, + nodeConfig.port, + nodeConfig.username, + nodeConfig.password, ...pickAndFormatStringList( - config, + nodeConfig, ['tfo', 'mptcp', 'underlyingProxy', 'testUrl'], { keyFormat: 'kebabCase', @@ -137,16 +138,14 @@ export const getSurgeNodes = function ( } case NodeTypeEnum.Snell: { - const config = nodeConfig as SnellNodeConfig; - return [ - config.nodeName, + nodeConfig.nodeName, [ 'snell', - config.hostname, - config.port, + nodeConfig.hostname, + nodeConfig.port, ...pickAndFormatStringList( - config, + nodeConfig, [ 'psk', 'obfs', @@ -168,10 +167,8 @@ export const getSurgeNodes = function ( } case NodeTypeEnum.Shadowsocksr: { - const config = nodeConfig as ShadowsocksrNodeConfig; - // istanbul ignore next - if (!config.binPath) { + if (!nodeConfig.binPath) { throw new Error( '请按照文档 https://url.royli.dev/vdGh2 添加 Shadowsocksr 二进制文件路径', ); @@ -179,104 +176,104 @@ export const getSurgeNodes = function ( const args = [ '-s', - config.hostname, + nodeConfig.hostname, '-p', - `${config.port}`, + `${nodeConfig.port}`, '-m', - config.method, + nodeConfig.method, '-o', - config.obfs, + nodeConfig.obfs, '-O', - config.protocol, + nodeConfig.protocol, '-k', - config.password, + nodeConfig.password, '-l', - `${config.localPort}`, + `${nodeConfig.localPort}`, '-b', '127.0.0.1', ]; - if (config.protoparam) { - args.push('-G', config.protoparam); + if (nodeConfig.protoparam) { + args.push('-G', nodeConfig.protoparam); } - if (config.obfsparam) { - args.push('-g', config.obfsparam); + if (nodeConfig.obfsparam) { + args.push('-g', nodeConfig.obfsparam); } - const configString = [ + const nodeConfigString = [ 'external', - `exec = ${JSON.stringify(config.binPath)}`, + `exec = ${JSON.stringify(nodeConfig.binPath)}`, ...args.map((arg) => `args = ${JSON.stringify(arg)}`), - `local-port = ${config.localPort}`, + `local-port = ${nodeConfig.localPort}`, ]; - if (config.localPort === 0) { + if (nodeConfig.localPort === 0) { throw new Error( - `为 Surge 生成 SSR 配置时必须为 Provider ${config.provider?.name} 设置 startPort,参考 https://url.royli.dev/bWcpe`, + `为 Surge 生成 SSR 配置时必须为 Provider ${nodeConfig.provider?.name} 设置 startPort,参考 https://url.royli.dev/bWcpe`, ); } - if (config.hostnameIp && config.hostnameIp.length) { - configString.push( - ...config.hostnameIp.map((item) => `addresses = ${item}`), + if (nodeConfig.hostnameIp && nodeConfig.hostnameIp.length) { + nodeConfigString.push( + ...nodeConfig.hostnameIp.map((item) => `addresses = ${item}`), ); } - if (isIp(config.hostname)) { - configString.push(`addresses = ${config.hostname}`); + if (isIp(nodeConfig.hostname)) { + nodeConfigString.push(`addresses = ${nodeConfig.hostname}`); } - return [config.nodeName, configString.join(', ')].join(' = '); + return [nodeConfig.nodeName, nodeConfigString.join(', ')].join(' = '); } case NodeTypeEnum.Vmess: { - const config = nodeConfig as VmessNodeConfig; - - const configList = [ + const result = [ 'vmess', - config.hostname, - config.port, - `username=${config.uuid}`, + nodeConfig.hostname, + nodeConfig.port, + `username=${nodeConfig.uuid}`, ]; if ( - ['chacha20-ietf-poly1305', 'aes-128-gcm'].includes(config.method) + ['chacha20-ietf-poly1305', 'aes-128-gcm'].includes( + nodeConfig.method, + ) ) { - configList.push(`encrypt-method=${config.method}`); + result.push(`encrypt-method=${nodeConfig.method}`); } - if (config.network === 'ws') { - configList.push('ws=true'); - configList.push(`ws-path=${config.path}`); - configList.push( + if (nodeConfig.network === 'ws') { + result.push('ws=true'); + result.push(`ws-path=${nodeConfig.path}`); + result.push( 'ws-headers=' + JSON.stringify( getSurgeExtendHeaders({ - host: config.host || config.hostname, + host: nodeConfig.host || nodeConfig.hostname, 'user-agent': OBFS_UA, - ..._.omit(config.wsHeaders, ['host']), // host 本质上是一个头信息,所以可能存在冲突的情况。以 host 属性为准。 + ..._.omit(nodeConfig.wsHeaders, ['host']), // host 本质上是一个头信息,所以可能存在冲突的情况。以 host 属性为准。 }), ), ); } - if (config.tls) { - configList.push( + if (nodeConfig.tls) { + result.push( 'tls=true', ...pickAndFormatStringList( - config, + nodeConfig, ['tls13', 'skipCertVerify', 'serverCertFingerprintSha256'], { keyFormat: 'kebabCase', }, ), - ...(config.host ? [`sni=${config.host}`] : []), + ...(nodeConfig.host ? [`sni=${nodeConfig.host}`] : []), ); } - configList.push( + result.push( ...pickAndFormatStringList( - config, + nodeConfig, ['tfo', 'mptcp', 'underlyingProxy', 'testUrl'], { keyFormat: 'kebabCase', @@ -285,18 +282,18 @@ export const getSurgeNodes = function ( ); if (nodeConfig?.surgeConfig?.vmessAEAD) { - configList.push('vmess-aead=true'); + result.push('vmess-aead=true'); } else { - configList.push('vmess-aead=false'); + result.push('vmess-aead=false'); } - configList.push(...parseShadowTlsConfig(nodeConfig)); + result.push(...parseShadowTlsConfig(nodeConfig)); - return [config.nodeName, configList.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Trojan: { - const configList: string[] = [ + const result: string[] = [ 'trojan', nodeConfig.hostname, `${nodeConfig.port}`, @@ -321,22 +318,22 @@ export const getSurgeNodes = function ( ]; if (nodeConfig.network === 'ws') { - configList.push('ws=true'); - configList.push(`ws-path=${nodeConfig.wsPath}`); + result.push('ws=true'); + result.push(`ws-path=${nodeConfig.wsPath}`); if (nodeConfig.wsHeaders) { - configList.push( + result.push( 'ws-headers=' + JSON.stringify(getSurgeExtendHeaders(nodeConfig.wsHeaders)), ); } } - return [nodeConfig.nodeName, configList.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Socks5: { - const config = [ + const result = [ nodeConfig.tls === true ? 'socks5-tls' : 'socks5', nodeConfig.hostname, nodeConfig.port, @@ -362,7 +359,7 @@ export const getSurgeNodes = function ( ]; if (nodeConfig.tls === true) { - config.push( + result.push( ...(typeof nodeConfig.skipCertVerify === 'boolean' ? [`skip-cert-verify=${nodeConfig.skipCertVerify}`] : []), @@ -372,11 +369,11 @@ export const getSurgeNodes = function ( ); } - return [nodeConfig.nodeName, config.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Tuic: { - const config = [ + const result = [ 'tuic', nodeConfig.hostname, nodeConfig.port, @@ -399,7 +396,7 @@ export const getSurgeNodes = function ( : []), ]; - return [nodeConfig.nodeName, config.join(', ')].join(' = '); + return [nodeConfig.nodeName, result.join(', ')].join(' = '); } case NodeTypeEnum.Wireguard: @@ -432,7 +429,7 @@ export const getSurgeWireguardNodes = ( return undefined; } - const configSection: string[] = [ + const nodeConfigSection: string[] = [ `[WireGuard ${nodeConfig.nodeName}]`, `self-ip=${nodeConfig.selfIp}`, `private-key=${nodeConfig.privateKey}`, @@ -454,7 +451,7 @@ export const getSurgeWireguardNodes = ( for (const key of optionalKeys) { if (nodeConfig[key] !== undefined) { - configSection.push( + nodeConfigSection.push( ...pickAndFormatStringList(nodeConfig, [key], { keyFormat: 'kebabCase', }), @@ -476,23 +473,58 @@ export const getSurgeWireguardNodes = ( peerConfig.push(`keepalive=${nodeConfig.keepAlive}`); } - configSection.push(`peer=(${peerConfig.join(', ')})`); + nodeConfigSection.push(`peer=(${peerConfig.join(', ')})`); - return configSection.join('\n'); + return nodeConfigSection.join('\n'); }) .filter((item): item is string => item !== undefined); return result.join('\n\n'); }; -function parseShadowTlsConfig(config: PossibleNodeConfigType) { +export const getSurgeNodeNames = function ( + list: ReadonlyArray, + filter?: NodeNameFilterType | SortedNodeNameFilterType, + separator?: string, +): string { + // istanbul ignore next + if (arguments.length === 2 && typeof filter === 'undefined') { + throw new Error(ERR_INVALID_FILTER); + } + + return applyFilter( + list.filter( + (item) => + shadowsocksFilter(item) || + shadowsocksrFilter(item) || + vmessFilter(item) || + snellFilter(item) || + tuicFilter(item) || + httpFilter(item) || + httpsFilter(item) || + trojanFilter(item) || + socks5Filter(item) || + wireguardFilter(item), + ), + filter, + ) + .map((item) => item.nodeName) + .join(separator || ', '); +}; + +function parseShadowTlsConfig(nodeConfig: PossibleNodeConfigType) { const result: string[] = []; - if (config.shadowTls) { - result.push(`shadow-tls-password=${config.shadowTls.password}`); + if (nodeConfig.shadowTls) { + result.push(`shadow-tls-password=${nodeConfig.shadowTls.password}`); - if (config.shadowTls.sni) { - result.push(`shadow-tls-sni=${config.shadowTls.sni}`); + if (nodeConfig.shadowTls.sni) { + result.push(`shadow-tls-sni=${nodeConfig.shadowTls.sni}`); + } + if (nodeConfig.shadowTls.version && nodeConfig.shadowTls.version !== 2) { + logger.warning( + `Surge 目前可能不支持 shadow-tls v${nodeConfig.shadowTls.version},请前往 https://manual.nssurge.com/policy/proxy.html#shadow-tls 查看最新支持版本`, + ); } } diff --git a/src/validators/common.ts b/src/validators/common.ts index 71c3bda1d..f2d75078a 100644 --- a/src/validators/common.ts +++ b/src/validators/common.ts @@ -12,6 +12,7 @@ export const SimpleNodeConfigValidator = z.object({ mptcp: z.boolean().optional(), shadowTls: z .object({ + version: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(), password: z.string(), sni: z.string().optional(), }) diff --git a/src/validators/surgio-config.ts b/src/validators/surgio-config.ts index 2ac5628bc..ffdebc08e 100644 --- a/src/validators/surgio-config.ts +++ b/src/validators/surgio-config.ts @@ -62,6 +62,7 @@ export const SurgioConfigValidator = z.object({ clashConfig: z .object({ enableTuic: z.oboolean(), + enableShadowTls: z.oboolean(), }) .optional(), gateway: z