123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- // @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 == '')
- );
- }
|