From 1c17cfb6835f8cfcc413f70c057998d476934058 Mon Sep 17 00:00:00 2001 From: Memory <134070804+Memory2314@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:46:41 +0800 Subject: [PATCH] refactor: external controller & rule editor * update external controller labels for clarity * add generate and toggle visibility for external controller secret * add validation for external controller and rule payloads * enhance rule payload validation UI with prominent error styling --- package.json | 2 + pnpm-lock.yaml | 17 + .../components/profiles/edit-rules-modal.tsx | 196 ++++----- src/renderer/src/locales/en-US.json | 5 +- src/renderer/src/locales/ru-RU.json | 6 +- src/renderer/src/locales/zh-CN.json | 5 +- src/renderer/src/locales/zh-TW.json | 5 +- src/renderer/src/pages/mihomo.tsx | 77 +++- src/renderer/src/utils/validate.ts | 375 ++++++++++++++++++ 9 files changed, 578 insertions(+), 110 deletions(-) create mode 100644 src/renderer/src/utils/validate.ts diff --git a/package.json b/package.json index 6a5fb3d..e5f4236 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "js-yaml": "^4.1.1", "plist": "^3.1.0", "sysproxy-rs": "file:src/native/sysproxy", + "validator": "^13.15.26", "webdav": "^5.8.0", "ws": "^8.18.3", "yaml": "^2.8.1" @@ -68,6 +69,7 @@ "@types/pubsub-js": "^1.8.6", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", + "@types/validator": "^13.15.10", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.52.0", "@typescript-eslint/parser": "^8.52.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2ba37a..fefe5a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: sysproxy-rs: specifier: file:src/native/sysproxy version: file:src/native/sysproxy + validator: + specifier: ^13.15.26 + version: 13.15.26 webdav: specifier: ^5.8.0 version: 5.8.0 @@ -108,6 +111,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) + '@types/validator': + specifier: ^13.15.10 + version: 13.15.10 '@types/ws': specifier: ^8.18.1 version: 8.18.1 @@ -2239,6 +2245,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/verror@1.10.11': resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} @@ -5093,6 +5102,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -7986,6 +7999,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/validator@13.15.10': {} + '@types/verror@1.10.11': optional: true @@ -11494,6 +11509,8 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.15.26: {} + vary@1.1.2: {} verror@1.10.1: diff --git a/src/renderer/src/components/profiles/edit-rules-modal.tsx b/src/renderer/src/components/profiles/edit-rules-modal.tsx index 0aa093f..c97d0f8 100644 --- a/src/renderer/src/components/profiles/edit-rules-modal.tsx +++ b/src/renderer/src/components/profiles/edit-rules-modal.tsx @@ -31,6 +31,33 @@ import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io' import { MdVerticalAlignTop, MdVerticalAlignBottom } from 'react-icons/md' import { platform } from '@renderer/utils/init' import { toast } from '@renderer/components/base/toast' +import { + domainValidator, + domainSuffixValidator, + domainKeywordValidator, + domainRegexValidator, + domainWildcardValidator, + geositeValidator, + geoipValidator, + asnValidator, + uidValidator, + dscpValidator, + networkValidator, + processPathValidator, + processPathWildcardValidator, + processPathRegexValidator, + processNameValidator, + processNameWildcardValidator, + processNameRegexValidator, + inTypeValidator, + inUserValidator, + inNameValidator, + ruleSetValidator, + logicRuleValidator, + subRuleValidator, + portRangeValidator, + ipCIDRValidator +} from '@renderer/utils/validate' interface Props { id: string @@ -45,53 +72,6 @@ interface RuleItem { offset?: number } -const domainValidator = (value: string): boolean => { - if (value.length > 253 || value.length < 2) return false - - return ( - new RegExp('^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\\.)+[a-zA-Z]{2,}$').test( - value - ) || ['localhost', 'local', 'localdomain'].includes(value.toLowerCase()) - ) -} - -const domainSuffixValidator = (value: string): boolean => { - return new RegExp( - '^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$' - ).test(value) -} - -const domainKeywordValidator = (value: string): boolean => { - return value.length > 0 && !value.includes(',') && !value.includes(' ') -} - -const domainRegexValidator = (value: string): boolean => { - try { - new RegExp(value) - return true - } catch { - return false - } -} - -const portValidator = (value: string): boolean => { - return new RegExp( - '^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$' - ).test(value) -} - -const ipv4CIDRValidator = (value: string): boolean => { - return new RegExp( - '^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$' - ).test(value) -} - -const ipv6CIDRValidator = (value: string): boolean => { - return new RegExp( - '^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$' - ).test(value) -} - // 内置路由规则 https://wiki.metacubex.one/config/rules/ const ruleDefinitionsMap = new Map< string, @@ -136,11 +116,20 @@ const ruleDefinitionsMap = new Map< validator: (value) => domainRegexValidator(value) } ], + [ + 'DOMAIN-WILDCARD', + { + name: 'DOMAIN-WILDCARD', + example: '*.google.com', + validator: (value) => domainWildcardValidator(value) + } + ], [ 'GEOSITE', { name: 'GEOSITE', - example: 'youtube' + example: 'youtube', + validator: (value) => geositeValidator(value) } ], [ @@ -149,14 +138,16 @@ const ruleDefinitionsMap = new Map< name: 'GEOIP', example: 'CN', noResolve: true, - src: true + src: true, + validator: (value) => geoipValidator(value) } ], [ 'SRC-GEOIP', { name: 'SRC-GEOIP', - example: 'CN' + example: 'CN', + validator: (value) => geoipValidator(value) } ], [ @@ -166,7 +157,7 @@ const ruleDefinitionsMap = new Map< example: '13335', noResolve: true, src: true, - validator: (value) => (+value ? true : false) + validator: (value) => asnValidator(value) } ], [ @@ -174,7 +165,7 @@ const ruleDefinitionsMap = new Map< { name: 'SRC-IP-ASN', example: '9808', - validator: (value) => (+value ? true : false) + validator: (value) => asnValidator(value) } ], [ @@ -184,7 +175,7 @@ const ruleDefinitionsMap = new Map< example: '127.0.0.0/8', noResolve: true, src: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value) + validator: (value) => ipCIDRValidator(value) } ], [ @@ -194,7 +185,7 @@ const ruleDefinitionsMap = new Map< example: '2620:0:2d0:200::7/32', noResolve: true, src: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value) + validator: (value) => ipCIDRValidator(value) } ], [ @@ -202,7 +193,7 @@ const ruleDefinitionsMap = new Map< { name: 'SRC-IP-CIDR', example: '192.168.1.201/32', - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value) + validator: (value) => ipCIDRValidator(value) } ], [ @@ -212,7 +203,7 @@ const ruleDefinitionsMap = new Map< example: '8.8.8.8/24', noResolve: true, src: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value) + validator: (value) => ipCIDRValidator(value) } ], [ @@ -220,7 +211,7 @@ const ruleDefinitionsMap = new Map< { name: 'SRC-IP-SUFFIX', example: '192.168.1.201/8', - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value) + validator: (value) => ipCIDRValidator(value) } ], [ @@ -228,7 +219,7 @@ const ruleDefinitionsMap = new Map< { name: 'SRC-PORT', example: '7777', - validator: (value) => portValidator(value) + validator: (value) => portRangeValidator(value) } ], [ @@ -236,7 +227,7 @@ const ruleDefinitionsMap = new Map< { name: 'DST-PORT', example: '80', - validator: (value) => portValidator(value) + validator: (value) => portRangeValidator(value) } ], [ @@ -244,21 +235,23 @@ const ruleDefinitionsMap = new Map< { name: 'IN-PORT', example: '7897', - validator: (value) => portValidator(value) + validator: (value) => portRangeValidator(value) } ], [ 'DSCP', { name: 'DSCP', - example: '4' + example: '4', + validator: (value) => dscpValidator(value) } ], [ 'PROCESS-NAME', { name: 'PROCESS-NAME', - example: platform === 'win32' ? 'chrome.exe' : 'curl' + example: platform === 'win32' ? 'chrome.exe' : 'curl', + validator: (value) => processNameValidator(value) } ], [ @@ -268,21 +261,40 @@ const ruleDefinitionsMap = new Map< example: platform === 'win32' ? 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' - : '/usr/bin/wget' + : '/usr/bin/wget', + validator: (value) => processPathValidator(value) + } + ], + [ + 'PROCESS-NAME-WILDCARD', + { + name: 'PROCESS-NAME-WILDCARD', + example: '*telegram*', + validator: (value) => processNameWildcardValidator(value) } ], [ 'PROCESS-NAME-REGEX', { name: 'PROCESS-NAME-REGEX', - example: '.*telegram.*' + example: '.*telegram.*', + validator: (value) => processNameRegexValidator(value) + } + ], + [ + 'PROCESS-PATH-WILDCARD', + { + name: 'PROCESS-PATH-WILDCARD', + example: platform === 'win32' ? '*\\chrome.exe' : '/usr/*/wget', + validator: (value) => processPathWildcardValidator(value) } ], [ 'PROCESS-PATH-REGEX', { name: 'PROCESS-PATH-REGEX', - example: platform === 'win32' ? '(?i).*Application\\chrome.*' : '.*bin/wget' + example: platform === 'win32' ? '(?i).*Application\\\\chrome.*' : '.*bin/wget', + validator: (value) => processPathRegexValidator(value) } ], [ @@ -290,7 +302,7 @@ const ruleDefinitionsMap = new Map< { name: 'NETWORK', example: 'udp', - validator: (value) => ['tcp', 'udp'].includes(value) + validator: (value) => networkValidator(value) } ], [ @@ -298,35 +310,39 @@ const ruleDefinitionsMap = new Map< { name: 'UID', example: '1001', - validator: (value) => (+value ? true : false) + validator: (value) => uidValidator(value) } ], [ 'IN-TYPE', { name: 'IN-TYPE', - example: 'SOCKS/HTTP' + example: 'SOCKS/HTTP', + validator: (value) => inTypeValidator(value) } ], [ 'IN-USER', { name: 'IN-USER', - example: 'mihomo' + example: 'mihomo', + validator: (value) => inUserValidator(value) } ], [ 'IN-NAME', { name: 'IN-NAME', - example: 'ss' + example: 'ss', + validator: (value) => inNameValidator(value) } ], [ 'SUB-RULE', { name: 'SUB-RULE', - example: '(NETWORK,tcp)' + example: '(NETWORK,tcp)', + validator: (value) => subRuleValidator(value) } ], [ @@ -335,28 +351,32 @@ const ruleDefinitionsMap = new Map< name: 'RULE-SET', example: 'providername', noResolve: true, - src: true + src: true, + validator: (value) => ruleSetValidator(value) } ], [ 'AND', { name: 'AND', - example: '((DOMAIN,baidu.com),(NETWORK,UDP))' + example: '((DOMAIN,baidu.com),(NETWORK,UDP))', + validator: (value) => logicRuleValidator(value) } ], [ 'OR', { name: 'OR', - example: '((NETWORK,UDP),(DOMAIN,baidu.com))' + example: '((NETWORK,UDP),(DOMAIN,baidu.com))', + validator: (value) => logicRuleValidator(value) } ], [ 'NOT', { name: 'NOT', - example: '((DOMAIN,baidu.com))' + example: '((DOMAIN,baidu.com))', + validator: (value) => logicRuleValidator(value) } ], [ @@ -1205,21 +1225,17 @@ const EditRulesModal: React.FC = (props) => { {type} ))} - - setNewRule({ ...newRule, payload: value })} - isDisabled={newRule.type === 'MATCH'} - color={ - newRule.payload && newRule.type !== 'MATCH' && !isPayloadValid - ? 'danger' - : 'default' - } - /> + + setNewRule({ ...newRule, payload: value })} + isDisabled={newRule.type === 'MATCH'} + className={`${newRule.payload && newRule.type !== 'MATCH' && !isPayloadValid ? 'border-red-500 ring-1 ring-red-500 rounded-lg' : ''}`} + /> { const [redirPortInput, setRedirPortInput] = useState(showRedirPort ?? redirPort) const [tproxyPortInput, setTproxyPortInput] = useState(showTproxyPort ?? tproxyPort) const [externalControllerInput, setExternalControllerInput] = useState(externalController) + const [externalControllerError, setExternalControllerError] = useState(() => { + const result = isValidListenAddress(externalController) + return isValid(result) ? null : (getError(result) ?? '格式错误') + }) const [secretInput, setSecretInput] = useState(secret) + const [isSecretVisible, setIsSecretVisible] = useState(false) const [lanAllowedIpsInput, setLanAllowedIpsInput] = useState(lanAllowedIps) const [lanDisallowedIpsInput, setLanDisallowedIpsInput] = useState(lanDisallowedIps) const [authenticationInput, setAuthenticationInput] = useState(authentication) @@ -994,11 +1002,12 @@ const Mihomo: React.FC = () => { )}
- {externalControllerInput !== externalController && ( + {externalControllerInput !== externalController && !externalControllerError && ( )} - { - setExternalControllerInput(v) - }} - /> + + { + setExternalControllerInput(v) + const result = isValidListenAddress(v) + setExternalControllerError(isValid(result) ? null : (getError(result) ?? '格式错误')) + }} + /> +
- + { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const randomSecret = Array.from({ length: 8 }, () => + chars[Math.floor(Math.random() * chars.length)] + ).join(''); + setSecretInput(randomSecret); + }} + > + + + } + divider + >
{secretInput !== secret && ( + } />
diff --git a/src/renderer/src/utils/validate.ts b/src/renderer/src/utils/validate.ts new file mode 100644 index 0000000..b121282 --- /dev/null +++ b/src/renderer/src/utils/validate.ts @@ -0,0 +1,375 @@ +import validator from 'validator' + +const domainValidator = (value: string): boolean => { + if (value.length > 253 || value.length < 2) return false + + // 检查是否为合法的 FQDN (完全限定域名) + if (validator.isFQDN(value, { require_tld: true })) return true + + // 允许特殊的本地域名 + return ['localhost', 'local', 'localdomain'].includes(value.toLowerCase()) +} + +const domainSuffixValidator = (value: string): boolean => { + // 域名后缀验证 - 可以是完整域名或带通配符的域名后缀 + return validator.isFQDN(value, { require_tld: true, allow_wildcard: true }) +} + +const domainKeywordValidator = (value: string): boolean => { + // 域名关键字不能包含逗号和空格 + return value.length > 0 && validator.isWhitelisted(value, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._') +} + +const domainRegexValidator = (value: string): boolean => { + try { + new RegExp(value) + return true + } catch { + return false + } +} + +const portValidator = (value: string): boolean => { + return validator.isPort(value) +} + +const ipv4CIDRValidator = (value: string): boolean => { + // 验证 IPv4 CIDR 格式 (例如: 192.168.1.0/24) + if (!value.includes('/')) return false + + const [ip, cidr] = value.split('/') + const cidrNum = parseInt(cidr, 10) + + return validator.isIP(ip, 4) && !isNaN(cidrNum) && cidrNum >= 0 && cidrNum <= 32 +} + +const ipv6CIDRValidator = (value: string): boolean => { + // 验证 IPv6 CIDR 格式 (例如: 2001:db8::/32) + if (!value.includes('/')) return false + + const [ip, cidr] = value.split('/') + const cidrNum = parseInt(cidr, 10) + + return validator.isIP(ip, 6) && !isNaN(cidrNum) && cidrNum >= 0 && cidrNum <= 128 +} + +// 便捷函数:将 ValidationResult 转换为布尔值 +export const isValid = (result: ValidationResult): boolean => result.ok + +// 便捷函数:获取错误信息 +export const getError = (result: ValidationResult): string | undefined => result.error + +// IP CIDR 验证器(同时支持 IPv4 和 IPv6) +const ipCIDRValidator = (value: string): boolean => { + return ipv4CIDRValidator(value) || ipv6CIDRValidator(value) +} + +// DOMAIN-WILDCARD 验证器 - 仅支持 * 和 ? 通配符 +const domainWildcardValidator = (value: string): boolean => { + if (value.length === 0) return false + // 检查是否只包含合法字符(字母、数字、点、*、?、-) + const validPattern = /^[a-zA-Z0-9.*?-]+$/ + if (!validPattern.test(value)) return false + // 移除通配符后验证基本格式 + const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a') + // 至少要有一个点(域名结构) + return withoutWildcards.includes('.') +} + +// GEOSITE 验证器 - 站点名称验证 +const geositeValidator = (value: string): boolean => { + // GEOSITE 名称只能包含字母、数字、连字符和下划线 + return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0 +} + +// GEOIP 验证器 - 国家代码验证(ISO 3166-1 alpha-2) +const geoipValidator = (value: string): boolean => { + // 支持2位国家代码(大小写不敏感) + return validator.isAlpha(value) && value.length === 2 +} + +// ASN 验证器 - 自治系统号验证 +const asnValidator = (value: string): boolean => { + // ASN 范围: 1 - 4294967295 (32-bit) + return validator.isInt(value, { min: 1, max: 4294967295 }) +} + +// UID 验证器 - Linux 用户 ID 验证 +const uidValidator = (value: string): boolean => { + // UID 范围: 0 - 65535 (大多数系统) + return validator.isInt(value, { min: 0, max: 65535 }) +} + +// DSCP 验证器 - 区分服务代码点验证 +const dscpValidator = (value: string): boolean => { + // DSCP 范围: 0 - 63 (6-bit) + return validator.isInt(value, { min: 0, max: 63 }) +} + +// NETWORK 验证器 - 网络类型验证 +const networkValidator = (value: string): boolean => { + return validator.isIn(value.toLowerCase(), ['tcp', 'udp']) +} + +// 进程路径验证器 +const processPathValidator = (value: string): boolean => { + if (value.length === 0) return false + // Windows 路径或 Unix 路径 + const windowsPath = /^[a-zA-Z]:[\\/].+/ + const unixPath = /^\/.*/ + const androidPackage = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i + return windowsPath.test(value) || unixPath.test(value) || androidPackage.test(value) +} + +// 进程路径通配符验证器 +const processPathWildcardValidator = (value: string): boolean => { + if (value.length === 0) return false + // 包含通配符的路径,移除通配符后检查路径格式 + const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a') + return processPathValidator(withoutWildcards) +} + +// 进程路径正则验证器 +const processPathRegexValidator = (value: string): boolean => { + try { + new RegExp(value) + return true + } catch { + return false + } +} + +// 进程名称验证器 +const processNameValidator = (value: string): boolean => { + if (value.length === 0) return false + // 进程名或 Android 包名 + const processName = /^[a-zA-Z0-9\-_.]+$/ + const androidPackage = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i + return processName.test(value) || androidPackage.test(value) +} + +// 进程名称通配符验证器 +const processNameWildcardValidator = (value: string): boolean => { + if (value.length === 0) return false + // 移除通配符后检查进程名格式 + const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a') + return processNameValidator(withoutWildcards) +} + +// 进程名称正则验证器 +const processNameRegexValidator = (value: string): boolean => { + try { + new RegExp(value) + return true + } catch { + return false + } +} + +// IN-TYPE 验证器 - 入站类型验证 +const inTypeValidator = (value: string): boolean => { + // 支持单个或多个类型(用 / 分隔) + const types = value.split('/') + const validTypes = ['http', 'https', 'socks', 'socks4', 'socks5', 'tproxy', 'redir', 'mixed'] + return types.length > 0 && types.every((type) => validator.isIn(type.toLowerCase(), validTypes)) +} + +// IN-USER 验证器 - 入站用户名验证 +const inUserValidator = (value: string): boolean => { + if (value.length === 0) return false + // 支持多个用户名(用 / 分隔) + const users = value.split('/') + return users.every((user) => user.length > 0 && validator.isAlphanumeric(user, 'en-US', { ignore: '-_.' })) +} + +// IN-NAME 验证器 - 入站名称验证 +const inNameValidator = (value: string): boolean => { + // 入站名称可以包含字母、数字、连字符和下划线 + return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0 +} + +// RULE-SET 验证器 - 规则集名称验证 +const ruleSetValidator = (value: string): boolean => { + // 规则集名称(对应 rule-providers 中定义的名称) + return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0 +} + +// 逻辑规则验证器 - AND, OR, NOT +const logicRuleValidator = (value: string): boolean => { + if (value.length === 0) return false + // 检查括号是否匹配 + let depth = 0 + for (const char of value) { + if (char === '(') depth++ + if (char === ')') depth-- + if (depth < 0) return false + } + return depth === 0 && value.startsWith('(') && value.endsWith(')') +} + +// SUB-RULE 验证器 - 子规则验证 +const subRuleValidator = (value: string): boolean => { + if (value.length === 0) return false + // 格式: (RULE_TYPE,payload) 或 provider_name + if (value.startsWith('(') && value.endsWith(')')) { + return logicRuleValidator(value) + } + // 如果不是括号格式,则视为 provider 名称 + return ruleSetValidator(value) +} + +// 端口范围验证器(支持单个端口或范围) +const portRangeValidator = (value: string): boolean => { + // 支持单个端口或范围格式,如: 80 或 8000-9000 + if (value.includes('-')) { + const [start, end] = value.split('-') + return validator.isPort(start) && validator.isPort(end) && parseInt(start) <= parseInt(end) + } + return validator.isPort(value) +} + +export { + domainValidator, + domainSuffixValidator, + domainKeywordValidator, + domainRegexValidator, + domainWildcardValidator, + geositeValidator, + geoipValidator, + asnValidator, + uidValidator, + dscpValidator, + networkValidator, + processPathValidator, + processPathWildcardValidator, + processPathRegexValidator, + processNameValidator, + processNameWildcardValidator, + processNameRegexValidator, + inTypeValidator, + inUserValidator, + inNameValidator, + ruleSetValidator, + logicRuleValidator, + subRuleValidator, + portValidator, + portRangeValidator, + ipv4CIDRValidator, + ipv6CIDRValidator, + ipCIDRValidator +} + +// 通用验证结果类型 +export interface ValidationResult { + ok: boolean + error?: string +} + +// 验证 IPv4 地址 +export const isIPv4 = (ip: string): ValidationResult => { + if (!validator.isIP(ip, 4)) { + return { ok: false, error: '不是有效的 IPv4 地址' } + } + return { ok: true } +} + +// 验证 IPv6 地址 +export const isIPv6 = (ip: string): ValidationResult => { + if (!validator.isIP(ip, 6)) { + return { ok: false, error: '不是有效的 IPv6 地址' } + } + return { ok: true } +} + +// 验证端口 +export const isValidPort = (port: string): ValidationResult => { + if (!validator.isPort(port)) { + return { ok: false, error: '端口号必须在 1-65535 范围内' } + } + return { ok: true } +} + +// 验证监听地址 +export const isValidListenAddress = (s: string | undefined): ValidationResult => { + if (!s || s.trim() === '') return { ok: true } + + const v = s.trim() + + // 格式: :port (仅端口) + if (v.startsWith(':')) { + return isValidPort(v.slice(1)) + } + + const idx = v.lastIndexOf(':') + if (idx === -1) return { ok: false, error: '应包含端口号' } + + const host = v.slice(0, idx) + const port = v.slice(idx + 1) + + // 验证端口 + const portResult = isValidPort(port) + if (!portResult.ok) return portResult + + // 格式: [IPv6]:port + if (host.startsWith('[') && host.endsWith(']')) { + const inner = host.slice(1, -1) + return isIPv6(inner) + } + + // IPv4 地址 + if (validator.isIP(host, 4)) { + return { ok: true } + } + + // 域名或主机名 (使用宽松的 FQDN 验证) + if (validator.isFQDN(host, { require_tld: false }) || validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })) { + return { ok: true } + } + + return { ok: false, error: '主机名包含非法字符' } +} + +// 验证监听地址(完整版,包含 0.0.0.0 和 ::) +export const isValidListenAddressFull = (s: string | undefined): ValidationResult => { + if (!s || s.trim() === '') return { ok: true } + + const v = s.trim() + + // 格式: :port (仅端口) + if (v.startsWith(':')) { + return isValidPort(v.slice(1)) + } + + const idx = v.lastIndexOf(':') + if (idx === -1) return { ok: false, error: '应包含端口号' } + + const host = v.slice(0, idx) + const port = v.slice(idx + 1) + + // 验证端口 + const portResult = isValidPort(port) + if (!portResult.ok) return portResult + + // 格式: [IPv6]:port + if (host.startsWith('[') && host.endsWith(']')) { + const inner = host.slice(1, -1) + return isIPv6(inner) + } + + // 特殊地址: 0.0.0.0 (监听所有 IPv4) 或 :: (监听所有 IPv6) + if (host === '0.0.0.0' || host === '::') { + return { ok: true } + } + + // IPv4 地址 + if (validator.isIP(host, 4)) { + return { ok: true } + } + + // 域名或主机名 (使用宽松的 FQDN 验证) + if (validator.isFQDN(host, { require_tld: false }) || validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })) { + return { ok: true } + } + + return { ok: false, error: '主机名包含非法字符' } +} \ No newline at end of file