init
This commit is contained in:
198
uni_modules/lime-shared/isURL/index.ts
Normal file
198
uni_modules/lime-shared/isURL/index.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
// @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 == '')
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user