Files
xingrin/frontend/lib/endpoint-validator.ts

224 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-12-12 18:04:57 +08:00
import validator from 'validator'
import { isIP } from 'is-ip'
/**
* Endpoint
* URL
* 使 validator.js URL
*/
export interface EndpointValidationResult {
isValid: boolean
error?: string
url?: URL
}
export class EndpointValidator {
/**
* Endpoint HTTP/HTTPS URL
* @param urlString - URL
* @returns
*/
static validate(urlString: string): EndpointValidationResult {
// 1. 检查是否为空
if (!urlString || urlString.trim().length === 0) {
return {
isValid: false,
error: 'Endpoint 不能为空'
}
}
const trimmedUrl = urlString.trim()
// 2. 检查是否包含空格
if (trimmedUrl.includes(' ')) {
return {
isValid: false,
error: 'Endpoint 不能包含空格'
}
}
// 3. 使用 validator.js 进行严格验证
if (!validator.isURL(trimmedUrl, {
protocols: ['http', 'https'],
require_protocol: true,
require_valid_protocol: true,
require_host: true,
allow_underscores: false,
allow_trailing_dot: false,
allow_protocol_relative_urls: false,
})) {
return {
isValid: false,
error: 'Endpoint 格式无效,必须是有效的 HTTP/HTTPS URL'
}
}
// 4. 尝试解析 URL双重验证
let parsedUrl: URL
try {
parsedUrl = new URL(trimmedUrl)
} catch (error) {
return {
isValid: false,
error: 'Endpoint 格式无效,无法解析'
}
}
// 5. 验证协议
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
return {
isValid: false,
error: '只支持 HTTP 和 HTTPS 协议'
}
}
// 6. 验证主机名
if (!parsedUrl.hostname || parsedUrl.hostname.length === 0) {
return {
isValid: false,
error: 'Endpoint 必须包含有效的主机名'
}
}
// 7. 检查主机名格式(域名或 IP
if (!this.isValidHostname(parsedUrl.hostname)) {
return {
isValid: false,
error: '主机名格式无效'
}
}
// 8. 检查端口号(如果有)
if (parsedUrl.port && !this.isValidPort(parsedUrl.port)) {
return {
isValid: false,
error: '端口号无效(必须是 1-65535'
}
}
// 9. 检查路径(可选,但如果有必须有效)
if (parsedUrl.pathname && parsedUrl.pathname.includes('..')) {
return {
isValid: false,
error: 'Endpoint 路径不能包含 ".."'
}
}
// 10. 检查是否包含危险字符
if (this.containsDangerousCharacters(trimmedUrl)) {
return {
isValid: false,
error: 'Endpoint 包含不安全的字符'
}
}
return {
isValid: true,
url: parsedUrl
}
}
/**
* Endpoint
* @param urls - URL
* @returns
*/
static validateBatch(urls: string[]): Array<EndpointValidationResult & { index: number; originalUrl: string }> {
return urls.map((url, index) => ({
...this.validate(url),
index,
originalUrl: url
}))
}
/**
* IP
*/
private static isValidHostname(hostname: string): boolean {
// 1) IP 校验(支持 IPv4/IPv6
if (isIP(hostname)) {
return true
}
// 2) 域名校验(使用 validator 的 FQDN 校验)
return validator.isFQDN(hostname, {
require_tld: true,
allow_underscores: false,
allow_trailing_dot: false,
allow_numeric_tld: false,
allow_wildcard: false,
})
}
/**
*
*/
private static isValidPort(port: string): boolean {
const portNum = parseInt(port, 10)
return !isNaN(portNum) && portNum >= 1 && portNum <= 65535
}
/**
* URL
*/
private static containsDangerousCharacters(url: string): boolean {
// 检查是否包含控制字符
const controlCharRegex = /[\x00-\x1F\x7F]/
if (controlCharRegex.test(url)) {
return true
}
// 检查是否包含 JavaScript 协议
if (url.toLowerCase().includes('javascript:')) {
return true
}
// 检查是否包含 data 协议
if (url.toLowerCase().includes('data:')) {
return true
}
return false
}
/**
* Endpoint
*/
static normalize(urlString: string): string | null {
const result = this.validate(urlString)
if (!result.isValid || !result.url) {
return null
}
// 返回规范化的 URL
return result.url.href
}
/**
* Endpoint
*/
static parse(urlString: string): {
protocol: string
hostname: string
port: string
pathname: string
search: string
hash: string
} | null {
const result = this.validate(urlString)
if (!result.isValid || !result.url) {
return null
}
return {
protocol: result.url.protocol,
hostname: result.url.hostname,
port: result.url.port,
pathname: result.url.pathname,
search: result.url.search,
hash: result.url.hash
}
}
}