198 lines
6.3 KiB
TypeScript
198 lines
6.3 KiB
TypeScript
// @ts-nocheck
|
||
import { isValidDomain, type IsValidDomainOptions } from '../isValidDomain';
|
||
import { isIP } from '../isIP';
|
||
import { isRegExp } from '../isRegExp';
|
||
// import {merge} from '../merge';
|
||
|
||
/** URL 验证配置选项 */
|
||
export type IsURLOptions = {
|
||
/** 允许的协议列表(默认 ['http', 'https', 'ftp']) */
|
||
protocols ?: string[];
|
||
/** 需要顶级域名(默认 true) */
|
||
requireTld ?: boolean;
|
||
/** 需要协议头(默认 false) */
|
||
requireProtocol ?: boolean;
|
||
/** 需要主机地址(默认 true) */
|
||
requireHost ?: boolean;
|
||
/** 需要端口号(默认 false) */
|
||
requirePort ?: boolean;
|
||
/** 需要有效协议(默认 true) */
|
||
requireValidProtocol ?: boolean;
|
||
/** 允许下划线(默认 false) */
|
||
allowUnderscores ?: boolean;
|
||
/** 允许结尾点号(默认 false) */
|
||
allowTrailingDot ?: boolean;
|
||
/** 允许协议相对地址(默认 false) */
|
||
allowProtocolRelativeUrls ?: boolean;
|
||
/** 允许片段标识(默认 true) */
|
||
allowFragments ?: boolean;
|
||
/** 允许查询参数(默认 true) */
|
||
allowQueryComponents ?: boolean;
|
||
/** 禁用认证信息(默认 false) */
|
||
disallowAuth ?: boolean;
|
||
/** 验证长度(默认 true) */
|
||
validateLength ?: boolean;
|
||
/** 最大允许长度(默认 2084) */
|
||
maxAllowedLength ?: number;
|
||
/** 白名单主机列表 */
|
||
hostWhitelist ?: Array<string | RegExp>;
|
||
/** 黑名单主机列表 */
|
||
hostBlacklist ?: Array<string | RegExp>;
|
||
}
|
||
|
||
export function checkHost(host : string, matches : any[]) : boolean {
|
||
for (let i = 0; i < matches.length; i++) {
|
||
let match = matches[i];
|
||
if (host == match || (isRegExp(match) && (match as RegExp).test(host))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 辅助函数
|
||
function isValidPort(port : number | null) : boolean {
|
||
return port != null && !isNaN(port) && port > 0 && port <= 65535;
|
||
}
|
||
|
||
function validateHost(host : string, options : IsURLOptions | null, isIPv6 : boolean) : boolean {
|
||
if (isIPv6) return isIP(host, 6);
|
||
return isIP(host) || isValidDomain(host, {
|
||
requireTld: options?.requireTld ?? true,
|
||
allowUnderscore: options?.allowUnderscores ?? true,
|
||
allowTrailingDot: options?.allowTrailingDot ?? false
|
||
} as IsValidDomainOptions);
|
||
}
|
||
|
||
|
||
|
||
|
||
/** 匹配 IPv6 地址的正则表达式 */
|
||
const WRAPPED_IPV6_REGEX = /^\[([^\]]+)\](?::([0-9]+))?$/;
|
||
|
||
/**
|
||
* 验证字符串是否为有效的 URL
|
||
* @param url - 需要验证的字符串
|
||
* @param options - 配置选项
|
||
* @returns 是否为有效 URL
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* isURL('https://example.com'); // true
|
||
* isURL('user:pass@example.com', { disallowAuth: true }); // false
|
||
* ```
|
||
*/
|
||
export function isURL(url : string | null, options : IsURLOptions | null = null) : boolean {
|
||
// assertString(url);
|
||
|
||
// 1. 基础格式校验
|
||
if (url == null || url == '' || url.length == 0 || /[\s<>]/.test(url) || url.startsWith('mailto:')) {
|
||
return false;
|
||
}
|
||
// 合并配置选项
|
||
let protocols = options?.protocols ?? ['http', 'https', 'ftp']
|
||
// let requireTld = options?.requireTld ?? true
|
||
let requireProtocol = options?.requireProtocol ?? false
|
||
let requireHost = options?.requireHost ?? true
|
||
let requirePort = options?.requirePort ?? false
|
||
let requireValidProtocol = options?.requireValidProtocol ?? true
|
||
// let allowUnderscores = options?.allowUnderscores ?? false
|
||
// let allowTrailingDot = options?.allowTrailingDot ?? false
|
||
let allowProtocolRelativeUrls = options?.allowProtocolRelativeUrls ?? false
|
||
let allowFragments = options?.allowFragments ?? true
|
||
let allowQueryComponents = options?.allowQueryComponents ?? true
|
||
let validateLength = options?.validateLength ?? true
|
||
let maxAllowedLength = options?.maxAllowedLength ?? 2084
|
||
let hostWhitelist = options?.hostWhitelist
|
||
let hostBlacklist = options?.hostBlacklist
|
||
let disallowAuth = options?.disallowAuth ?? false
|
||
|
||
|
||
// 2. 长度校验
|
||
if (validateLength && url!.length > maxAllowedLength) {
|
||
return false;
|
||
}
|
||
// 3. 片段和查询参数校验
|
||
if (!allowFragments && url.includes('#')) return false;
|
||
if (!allowQueryComponents && (url.includes('?') || url.includes('&'))) return false;
|
||
|
||
// 处理 URL 组成部分
|
||
const [urlWithoutFragment] = url.split('#');
|
||
const [baseUrl] = urlWithoutFragment.split('?');
|
||
// 4. 协议处理
|
||
const protocolParts = baseUrl.split('://');
|
||
let protocol:string;
|
||
let remainingUrl = baseUrl;
|
||
|
||
if (protocolParts.length > 1) {
|
||
protocol = protocolParts.shift()!.toLowerCase();
|
||
if (requireValidProtocol && !protocols!.includes(protocol)) {
|
||
return false;
|
||
}
|
||
remainingUrl = protocolParts.join('://');
|
||
} else if (requireProtocol) {
|
||
return false;
|
||
} else if (baseUrl.startsWith('//')) {
|
||
if (!allowProtocolRelativeUrls) return false;
|
||
remainingUrl = baseUrl.slice(2);
|
||
}
|
||
|
||
if (remainingUrl == '') return false;
|
||
|
||
// 5. 处理主机部分
|
||
const [hostPart] = remainingUrl.split('/', 1);
|
||
const authParts = hostPart.split('@');
|
||
|
||
// 认证信息校验
|
||
if (authParts.length > 1) {
|
||
if (disallowAuth || authParts[0] == '') return false;
|
||
const auth = authParts.shift()!;
|
||
if (auth.split(':').length > 2) return false;
|
||
const [user, password] = auth.split(':');
|
||
if (user == '' && password == '') return false;
|
||
}
|
||
|
||
const hostname = authParts.join('@');
|
||
|
||
// 6. 解析主机和端口
|
||
type HostInfo = {
|
||
host ?: string;
|
||
ipv6 ?: string;
|
||
port ?: number;
|
||
};
|
||
|
||
const hostInfo : HostInfo = {};
|
||
const ipv6Match = hostname.match(WRAPPED_IPV6_REGEX);
|
||
if (ipv6Match != null) {
|
||
hostInfo.ipv6 = ipv6Match.length > 1 ? ipv6Match[1] : null;
|
||
const portStr = ipv6Match.length > 2 ? ipv6Match[2] : null;
|
||
if (portStr != null) {
|
||
hostInfo.port = parseInt(portStr);
|
||
if (!isValidPort(hostInfo.port)) return false;
|
||
}
|
||
} else {
|
||
const [host, ...portParts] = hostname.split(':');
|
||
hostInfo.host = host;
|
||
if (portParts.length > 0) {
|
||
const portStr = portParts.join(':');
|
||
hostInfo.port = parseInt(portStr);
|
||
if (!isValidPort(hostInfo.port)) return false;
|
||
}
|
||
}
|
||
|
||
// 7. 端口校验
|
||
if (requirePort && hostInfo.port == null) return false;
|
||
// 8. 主机验证逻辑
|
||
const finalHost = hostInfo.host ?? hostInfo.ipv6;
|
||
if (finalHost == null) return requireHost ? false : true;
|
||
// 白名单/黑名单检查
|
||
if (hostWhitelist != null && !checkHost(finalHost!, hostWhitelist!)) return false;
|
||
if (hostBlacklist != null && checkHost(finalHost!, hostBlacklist!)) return false;
|
||
|
||
// 9. 综合校验
|
||
return validateHost(
|
||
finalHost,
|
||
options,
|
||
!(hostInfo.ipv6 == null || hostInfo.ipv6 == '')
|
||
);
|
||
} |