refactor: streamline validation utilities

This commit is contained in:
Memory
2026-08-28 21:51:20 +08:00
committed by GitHub
parent 061faeefd1
commit 26dd08e07b

View File

@@ -1,32 +1,34 @@
import validator from 'validator' import validator from 'validator'
const domainValidator = (value: string): boolean => { export interface ValidationResult {
if (value.length > 253 || value.length < 2) return false ok: boolean
error?: string
// 检查是否为合法的 FQDN (完全限定域名)
if (validator.isFQDN(value, { require_tld: true })) return true
// 允许特殊的本地域名
return ['localhost', 'local', 'localdomain'].includes(value.toLowerCase())
} }
const domainSuffixValidator = (value: string): boolean => { type BooleanValidator = (value: string) => boolean
// 域名后缀验证 - 可以是完整域名或带通配符的域名后缀
return validator.isFQDN(value, { require_tld: true, allow_wildcard: true })
}
const domainKeywordValidator = (value: string): boolean => { const LOCAL_DOMAINS = new Set(['localhost', 'local', 'localdomain'])
// 域名关键字不能包含逗号和空格 const NETWORK_TYPES = new Set(['tcp', 'udp'])
return ( const LOCAL_BYPASS_PLATFORMS = new Set<string>(['win32', 'darwin'])
value.length > 0 && const INBOUND_TYPES = new Set([
validator.isWhitelisted( 'http',
value, 'https',
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._' 'socks',
) 'socks4',
) 'socks5',
} 'tproxy',
'redir',
'mixed'
])
const domainRegexValidator = (value: string): boolean => { const DOMAIN_KEYWORD_PATTERN = /^[a-zA-Z0-9._-]+$/
const DOMAIN_WILDCARD_PATTERN = /^[a-zA-Z0-9.*?-]+$/
const WINDOWS_PATH_PATTERN = /^[a-zA-Z]:[\\/].+/
const UNIX_PATH_PATTERN = /^\/.+/
const ANDROID_PACKAGE_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i
const PROCESS_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/
const isValidRegex: BooleanValidator = (value) => {
try { try {
new RegExp(value) new RegExp(value)
return true return true
@@ -35,62 +37,82 @@ const domainRegexValidator = (value: string): boolean => {
} }
} }
const portValidator = (value: string): boolean => { const isNamedIdentifier = (value: string, ignore = '-_'): boolean =>
return validator.isPort(value) value.length > 0 && validator.isAlphanumeric(value, 'en-US', { ignore })
}
const ipv4CIDRValidator = (value: string): boolean => { const replaceWildcards = (value: string): string => value.replace(/\*/g, 'a').replace(/\?/g, 'a')
// 验证 IPv4 CIDR 格式 (例如192.168.1.0/24)
if (!value.includes('/')) return false
const [ip, cidr] = value.split('/') const isValidHostname = (host: string): boolean =>
const cidrNum = parseInt(cidr, 10) validator.isFQDN(host, { require_tld: false }) ||
validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })
return validator.isIP(ip, 4) && !isNaN(cidrNum) && cidrNum >= 0 && cidrNum <= 32 const validationResult = (ok: boolean, error: string): ValidationResult =>
} ok ? { ok: true } : { ok: false, error }
const ipv6CIDRValidator = (value: string): boolean => { const integerValidator =
// 验证 IPv6 CIDR 格式 (例如2001:db8::/32) (min: number, max: number): BooleanValidator =>
if (!value.includes('/')) return false (value) =>
validator.isInt(value, { min, max })
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 isValid = (result: ValidationResult): boolean => result.ok
// 便捷函数:获取错误信息
export const getError = (result: ValidationResult): string | undefined => result.error export const getError = (result: ValidationResult): string | undefined => result.error
// IP CIDR 验证器(同时支持 IPv4 和 IPv6 // Domain rules
const ipCIDRValidator = (value: string): boolean => {
return ipv4CIDRValidator(value) || ipv6CIDRValidator(value) export const domainValidator: BooleanValidator = (value) => {
if (value.length < 2 || value.length > 253) return false
return validator.isFQDN(value, { require_tld: true }) || LOCAL_DOMAINS.has(value.toLowerCase())
} }
const sysProxyBypassValidator = ( export const domainSuffixValidator: BooleanValidator = (value) =>
validator.isFQDN(value, { require_tld: true, allow_wildcard: true })
export const domainKeywordValidator: BooleanValidator = (value) =>
DOMAIN_KEYWORD_PATTERN.test(value)
export const domainRegexValidator = isValidRegex
export const domainWildcardValidator: BooleanValidator = (value) => {
if (!DOMAIN_WILDCARD_PATTERN.test(value)) return false
const normalizedDomain = replaceWildcards(value)
return (
normalizedDomain.includes('.') && validator.isFQDN(normalizedDomain, { require_tld: false })
)
}
// Network rules
export const portValidator: BooleanValidator = (value) => validator.isPort(value)
export const portRangeValidator: BooleanValidator = (value) => {
const [start, end, ...rest] = value.split('-')
if (end === undefined) return validator.isPort(start)
if (rest.length > 0) return false
return validator.isPort(start) && validator.isPort(end) && Number(start) <= Number(end)
}
export const ipv4CIDRValidator: BooleanValidator = (value) => validator.isIPRange(value, 4)
export const ipv6CIDRValidator: BooleanValidator = (value) => validator.isIPRange(value, 6)
export const ipCIDRValidator: BooleanValidator = (value) => validator.isIPRange(value)
export const sysProxyBypassValidator = (
value: string, value: string,
targetPlatform: NodeJS.Platform | string targetPlatform: NodeJS.Platform | string
): boolean => { ): boolean => {
const entry = value.trim() const entry = value.trim()
if (entry === '') return false if (!entry) return false
if (validator.isIP(entry)) return true if (validator.isIP(entry)) return true
if (targetPlatform !== 'win32' && validator.isIPRange(entry)) return true if (targetPlatform !== 'win32' && validator.isIPRange(entry)) return true
if ( const normalizedEntry = entry.toLowerCase()
(targetPlatform === 'win32' || targetPlatform === 'darwin') && if (LOCAL_BYPASS_PLATFORMS.has(targetPlatform) && normalizedEntry === '<local>') return true
entry.toLowerCase() === '<local>'
) {
return true
}
if (targetPlatform === 'win32' && /[*?]/.test(entry)) { if (targetPlatform === 'win32' && /[*?]/.test(entry)) {
const normalizedPattern = entry.replace(/\*/g, 'wildcard').replace(/\?/g, 'q') return validator.isFQDN(entry.replace(/\*/g, 'wildcard').replace(/\?/g, 'q'), {
return validator.isFQDN(normalizedPattern, {
require_tld: false, require_tld: false,
allow_numeric_tld: true allow_numeric_tld: true
}) })
@@ -103,321 +125,106 @@ const sysProxyBypassValidator = (
}) })
} }
// DOMAIN-WILDCARD 验证器 - 仅支持 * 和 ? 通配符 // Rule values
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 验证器 - 站点名称验证 export const geositeValidator: BooleanValidator = (value) => isNamedIdentifier(value)
const geositeValidator = (value: string): boolean => { export const geoipValidator: BooleanValidator = (value) => validator.isISO31661Alpha2(value)
// GEOSITE 名称只能包含字母、数字、连字符和下划线 export const asnValidator = integerValidator(1, 4_294_967_295)
return validator.isAlphanumeric(value, 'en-US', { ignore: '-_' }) && value.length > 0 export const uidValidator = integerValidator(0, 65_535)
} export const dscpValidator = integerValidator(0, 63)
export const networkValidator: BooleanValidator = (value) => NETWORK_TYPES.has(value.toLowerCase())
// GEOIP 验证器 - 国家代码验证ISO 3166-1 alpha-2 export const inTypeValidator: BooleanValidator = (value) =>
const geoipValidator = (value: string): boolean => { value.split('/').every((type) => INBOUND_TYPES.has(type.toLowerCase()))
// 支持 2 位国家代码(大小写不敏感)
return validator.isAlpha(value) && value.length === 2
}
// ASN 验证器 - 自治系统号验证 export const inUserValidator: BooleanValidator = (value) =>
const asnValidator = (value: string): boolean => { value.length > 0 && value.split('/').every((user) => isNamedIdentifier(user, '-_.'))
// ASN 范围1 - 4294967295 (32-bit)
return validator.isInt(value, { min: 1, max: 4294967295 })
}
// UID 验证器 - Linux 用户 ID 验证 export const inNameValidator: BooleanValidator = (value) => isNamedIdentifier(value)
const uidValidator = (value: string): boolean => { export const ruleSetValidator: BooleanValidator = (value) => isNamedIdentifier(value)
// UID 范围0 - 65535 (大多数系统)
return validator.isInt(value, { min: 0, max: 65535 })
}
// DSCP 验证器 - 区分服务代码点验证 // Process rules
const dscpValidator = (value: string): boolean => {
// DSCP 范围0 - 63 (6-bit)
return validator.isInt(value, { min: 0, max: 63 })
}
// NETWORK 验证器 - 网络类型验证 export const processPathValidator: BooleanValidator = (value) =>
const networkValidator = (value: string): boolean => { WINDOWS_PATH_PATTERN.test(value) ||
return validator.isIn(value.toLowerCase(), ['tcp', 'udp']) UNIX_PATH_PATTERN.test(value) ||
} ANDROID_PACKAGE_PATTERN.test(value)
// 进程路径验证器 export const processPathWildcardValidator: BooleanValidator = (value) =>
const processPathValidator = (value: string): boolean => { value.length > 0 && processPathValidator(replaceWildcards(value))
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)
}
// 进程路径通配符验证器 export const processPathRegexValidator = isValidRegex
const processPathWildcardValidator = (value: string): boolean => {
if (value.length === 0) return false
// 包含通配符的路径,移除通配符后检查路径格式
const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a')
return processPathValidator(withoutWildcards)
}
// 进程路径正则验证器 export const processNameValidator: BooleanValidator = (value) => PROCESS_NAME_PATTERN.test(value)
const processPathRegexValidator = (value: string): boolean => {
try {
new RegExp(value)
return true
} catch {
return false
}
}
// 进程名称验证器 export const processNameWildcardValidator: BooleanValidator = (value) =>
const processNameValidator = (value: string): boolean => { value.length > 0 && processNameValidator(replaceWildcards(value))
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)
}
// 进程名称通配符验证器 export const processNameRegexValidator = isValidRegex
const processNameWildcardValidator = (value: string): boolean => {
if (value.length === 0) return false
// 移除通配符后检查进程名格式
const withoutWildcards = value.replace(/\*/g, 'a').replace(/\?/g, 'a')
return processNameValidator(withoutWildcards)
}
// 进程名称正则验证器 // Logical rules
const processNameRegexValidator = (value: string): boolean => {
try {
new RegExp(value)
return true
} catch {
return false
}
}
// IN-TYPE 验证器 - 入站类型验证 export const logicRuleValidator: BooleanValidator = (value) => {
const inTypeValidator = (value: string): boolean => { if (!value.startsWith('(') || !value.endsWith(')')) return false
// 支持单个或多个类型(用 / 分隔)
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 let depth = 0
for (const char of value) { for (const char of value) {
if (char === '(') depth++ if (char === '(') depth += 1
if (char === ')') depth-- if (char === ')') depth -= 1
if (depth < 0) return false if (depth < 0) return false
} }
return depth === 0 && value.startsWith('(') && value.endsWith(')')
return depth === 0
} }
// SUB-RULE 验证器 - 子规则验证 export const subRuleValidator: BooleanValidator = (value) => {
const subRuleValidator = (value: string): boolean => { if (!value) return false
if (value.length === 0) return false return value.startsWith('(') && value.endsWith(')')
// 格式:(RULE_TYPE,payload) 或 provider_name ? logicRuleValidator(value)
if (value.startsWith('(') && value.endsWith(')')) { : ruleSetValidator(value)
return logicRuleValidator(value)
}
// 如果不是括号格式,则视为 provider 名称
return ruleSetValidator(value)
} }
// 端口范围验证器(支持单个端口或范围) // Structured validation results
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 { export const isIPv4 = (ip: string): ValidationResult =>
domainValidator, validationResult(validator.isIP(ip, 4), '不是有效的 IPv4 地址')
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,
sysProxyBypassValidator
}
// 通用验证结果类型 export const isIPv6 = (ip: string): ValidationResult =>
export interface ValidationResult { validationResult(validator.isIP(ip, 6), '不是有效的 IPv6 地址')
ok: boolean
error?: string
}
// 验证 IPv4 地址 export const isValidPort = (port: string): ValidationResult =>
export const isIPv4 = (ip: string): ValidationResult => { validationResult(validator.isPort(port), '端口号必须在 1-65535 范围内')
if (!validator.isIP(ip, 4)) {
return { ok: false, error: '不是有效的 IPv4 地址' }
}
return { ok: true }
}
// 验证 IPv6 地址 const validateListenAddress = (
export const isIPv6 = (ip: string): ValidationResult => { input: string | undefined,
if (!validator.isIP(ip, 6)) { allowUnbracketedIPv6: boolean
return { ok: false, error: '不是有效的 IPv6 地址' } ): ValidationResult => {
} const value = input?.trim()
return { ok: true } if (!value) return { ok: true }
}
// 验证端口 if (/^:\d+$/.test(value)) return isValidPort(value.slice(1))
export const isValidPort = (port: string): ValidationResult => {
if (!validator.isPort(port)) {
return { ok: false, error: '端口号必须在 1-65535 范围内' }
}
return { ok: true }
}
// 验证监听地址 const separatorIndex = value.lastIndexOf(':')
export const isValidListenAddress = (s: string | undefined): ValidationResult => { if (separatorIndex < 0) return { ok: false, error: '应包含端口号' }
if (!s || s.trim() === '') return { ok: true }
const v = s.trim() const host = value.slice(0, separatorIndex)
const portResult = isValidPort(value.slice(separatorIndex + 1))
// 格式::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 if (!portResult.ok) return portResult
// 格式:[IPv6]:port
if (host.startsWith('[') && host.endsWith(']')) { if (host.startsWith('[') && host.endsWith(']')) {
const inner = host.slice(1, -1) return isIPv6(host.slice(1, -1))
return isIPv6(inner)
} }
// IPv4 地址 const validHost =
if (validator.isIP(host, 4)) { validator.isIP(host, 4) ||
return { ok: true } (allowUnbracketedIPv6 && validator.isIP(host, 6)) ||
isValidHostname(host)
return validationResult(validHost, '主机名包含非法字符')
} }
// 域名或主机名 (使用宽松的 FQDN 验证) export const isValidListenAddress = (input: string | undefined): ValidationResult =>
if ( validateListenAddress(input, false)
validator.isFQDN(host, { require_tld: false }) ||
validator.isAlphanumeric(host, 'en-US', { ignore: '-.' })
) {
return { ok: true }
}
return { ok: false, error: '主机名包含非法字符' } export const isValidListenAddressFull = (input: string | undefined): ValidationResult =>
} validateListenAddress(input, true)
// 验证监听地址(完整版,包含 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: '主机名包含非法字符' }
}