SmartParks_visitore/uni_modules/lime-shared/isURL/index.ts

198 lines
6.3 KiB
TypeScript
Raw Permalink Normal View History

2025-08-21 11:23:54 +08:00
// @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 == '')
);
}