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

215 lines
5.8 KiB
TypeScript
Raw Normal View History

2025-12-12 18:04:57 +08:00
import validator from 'validator'
import { parse as parseDomain } from 'tldts'
/**
*
* 使 validator.js
*/
export interface DomainValidationResult {
isValid: boolean
error?: string
}
export class DomainValidator {
/**
* example.com
* @param domain -
* @returns
*/
static validateDomain(domain: string): DomainValidationResult {
// 1. 检查是否为空
if (!domain || domain.trim().length === 0) {
return {
isValid: false,
error: '域名不能为空'
}
}
const trimmedDomain = domain.trim()
// 2. 检查是否包含空格
if (trimmedDomain.includes(' ')) {
return {
isValid: false,
error: '域名不能包含空格'
}
}
// 3. 检查长度(使用 validator 包)
if (!validator.isLength(trimmedDomain, { min: 1, max: 253 })) {
return {
isValid: false,
error: '域名长度不能超过 253 个字符'
}
}
// 4. 使用 tldts 做域名语义校验(优先)
const info = parseDomain(trimmedDomain)
if (!info.domain || info.isIp === true) {
return {
isValid: false,
error: '域名格式无效'
}
}
// 5. 使用 validator.js 的 isFQDN 兜底,确保严格性
if (!validator.isFQDN(trimmedDomain, {
require_tld: true,
allow_underscores: false,
allow_trailing_dot: false,
allow_numeric_tld: false,
allow_wildcard: false,
})) {
return {
isValid: false,
error: '域名格式无效'
}
}
return { isValid: true }
}
/**
* www.example.com, api.test.org
* @param subdomain -
* @returns
*/
static validateSubdomain(subdomain: string): DomainValidationResult {
// 先进行基本域名验证
const basicValidation = this.validateDomain(subdomain)
if (!basicValidation.isValid) {
return basicValidation
}
// 子域名必须至少包含 3 个部分(如 www.example.com
const labels = subdomain.trim().split('.')
if (labels.length < 3) {
return {
isValid: false,
error: '子域名必须至少包含 3 个部分(如 www.example.com'
}
}
return {
isValid: true
}
}
/**
*
* @param domains -
* @returns
*/
static validateDomainBatch(domains: string[]): Array<DomainValidationResult & { index: number; originalDomain: string }> {
return domains.map((domain, index) => ({
...this.validateDomain(domain),
index,
originalDomain: domain
}))
}
/**
*
* @param subdomains -
* @returns
*/
static validateSubdomainBatch(subdomains: string[]): Array<DomainValidationResult & { index: number; originalDomain: string }> {
return subdomains.map((subdomain, index) => ({
...this.validateSubdomain(subdomain),
index,
originalDomain: subdomain
}))
}
/**
*
*/
static normalize(domain: string): string | null {
const result = this.validateDomain(domain)
if (!result.isValid) {
return null
}
return domain.trim().toLowerCase()
}
/**
* 使 PSL - Public Suffix List
* @param subdomain - www.example.com, blog.github.io
* @returns example.com, blog.github.io null
*
*
* - www.example.com example.com
* - api.test.example.com example.com
* - blog.github.io blog.github.io ()
* - www.bbc.co.uk bbc.co.uk ( TLD)
*/
static extractRootDomain(subdomain: string): string | null {
const trimmed = subdomain.trim().toLowerCase()
if (!trimmed) return null
// 使用 tldts 解析域名
const parsed = parseDomain(trimmed)
if (!parsed.domain) {
return null
}
return parsed.domain
}
/**
*
* @param subdomains -
* @returns { grouped: Map<根域名, 子域名[]>, invalid: 无效的子域名[] }
*/
static groupSubdomainsByRootDomain(subdomains: string[]): {
grouped: Map<string, string[]>
invalid: string[]
} {
const grouped = new Map<string, string[]>()
const invalid: string[] = []
for (const subdomain of subdomains) {
const rootDomain = this.extractRootDomain(subdomain)
if (!rootDomain) {
invalid.push(subdomain)
continue
}
if (!grouped.has(rootDomain)) {
grouped.set(rootDomain, [])
}
grouped.get(rootDomain)!.push(subdomain)
}
return { grouped, invalid }
}
/**
*
* @param subdomain - www.example.com, api.example.com
* @param rootDomain - example.com
* @returns
*
*
* - isSubdomainOf('www.example.com', 'example.com') true
* - isSubdomainOf('api.test.example.com', 'example.com') true
* - isSubdomainOf('www.test.com', 'example.com') false
*/
static isSubdomainOf(subdomain: string, rootDomain: string): boolean {
const trimmedSubdomain = subdomain.trim().toLowerCase()
const trimmedRootDomain = rootDomain.trim().toLowerCase()
if (!trimmedSubdomain || !trimmedRootDomain) {
return false
}
// 提取子域名的根域名
const extractedRoot = this.extractRootDomain(trimmedSubdomain)
// 比较提取的根域名与目标根域名
return extractedRoot === trimmedRootDomain
}
}