index.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // @ts-nocheck
  2. import { isValidDomain, type IsValidDomainOptions } from '../isValidDomain';
  3. import { isIP } from '../isIP';
  4. import { isRegExp } from '../isRegExp';
  5. // import {merge} from '../merge';
  6. /** URL 验证配置选项 */
  7. export type IsURLOptions = {
  8. /** 允许的协议列表(默认 ['http', 'https', 'ftp']) */
  9. protocols ?: string[];
  10. /** 需要顶级域名(默认 true) */
  11. requireTld ?: boolean;
  12. /** 需要协议头(默认 false) */
  13. requireProtocol ?: boolean;
  14. /** 需要主机地址(默认 true) */
  15. requireHost ?: boolean;
  16. /** 需要端口号(默认 false) */
  17. requirePort ?: boolean;
  18. /** 需要有效协议(默认 true) */
  19. requireValidProtocol ?: boolean;
  20. /** 允许下划线(默认 false) */
  21. allowUnderscores ?: boolean;
  22. /** 允许结尾点号(默认 false) */
  23. allowTrailingDot ?: boolean;
  24. /** 允许协议相对地址(默认 false) */
  25. allowProtocolRelativeUrls ?: boolean;
  26. /** 允许片段标识(默认 true) */
  27. allowFragments ?: boolean;
  28. /** 允许查询参数(默认 true) */
  29. allowQueryComponents ?: boolean;
  30. /** 禁用认证信息(默认 false) */
  31. disallowAuth ?: boolean;
  32. /** 验证长度(默认 true) */
  33. validateLength ?: boolean;
  34. /** 最大允许长度(默认 2084) */
  35. maxAllowedLength ?: number;
  36. /** 白名单主机列表 */
  37. hostWhitelist ?: Array<string | RegExp>;
  38. /** 黑名单主机列表 */
  39. hostBlacklist ?: Array<string | RegExp>;
  40. }
  41. export function checkHost(host : string, matches : any[]) : boolean {
  42. for (let i = 0; i < matches.length; i++) {
  43. let match = matches[i];
  44. if (host == match || (isRegExp(match) && (match as RegExp).test(host))) {
  45. return true;
  46. }
  47. }
  48. return false;
  49. }
  50. // 辅助函数
  51. function isValidPort(port : number | null) : boolean {
  52. return port != null && !isNaN(port) && port > 0 && port <= 65535;
  53. }
  54. function validateHost(host : string, options : IsURLOptions | null, isIPv6 : boolean) : boolean {
  55. if (isIPv6) return isIP(host, 6);
  56. return isIP(host) || isValidDomain(host, {
  57. requireTld: options?.requireTld ?? true,
  58. allowUnderscore: options?.allowUnderscores ?? true,
  59. allowTrailingDot: options?.allowTrailingDot ?? false
  60. } as IsValidDomainOptions);
  61. }
  62. /** 匹配 IPv6 地址的正则表达式 */
  63. const WRAPPED_IPV6_REGEX = /^\[([^\]]+)\](?::([0-9]+))?$/;
  64. /**
  65. * 验证字符串是否为有效的 URL
  66. * @param url - 需要验证的字符串
  67. * @param options - 配置选项
  68. * @returns 是否为有效 URL
  69. *
  70. * @example
  71. * ```typescript
  72. * isURL('https://example.com'); // true
  73. * isURL('user:pass@example.com', { disallowAuth: true }); // false
  74. * ```
  75. */
  76. export function isURL(url : string | null, options : IsURLOptions | null = null) : boolean {
  77. // assertString(url);
  78. // 1. 基础格式校验
  79. if (url == null || url == '' || url.length == 0 || /[\s<>]/.test(url) || url.startsWith('mailto:')) {
  80. return false;
  81. }
  82. // 合并配置选项
  83. let protocols = options?.protocols ?? ['http', 'https', 'ftp']
  84. // let requireTld = options?.requireTld ?? true
  85. let requireProtocol = options?.requireProtocol ?? false
  86. let requireHost = options?.requireHost ?? true
  87. let requirePort = options?.requirePort ?? false
  88. let requireValidProtocol = options?.requireValidProtocol ?? true
  89. // let allowUnderscores = options?.allowUnderscores ?? false
  90. // let allowTrailingDot = options?.allowTrailingDot ?? false
  91. let allowProtocolRelativeUrls = options?.allowProtocolRelativeUrls ?? false
  92. let allowFragments = options?.allowFragments ?? true
  93. let allowQueryComponents = options?.allowQueryComponents ?? true
  94. let validateLength = options?.validateLength ?? true
  95. let maxAllowedLength = options?.maxAllowedLength ?? 2084
  96. let hostWhitelist = options?.hostWhitelist
  97. let hostBlacklist = options?.hostBlacklist
  98. let disallowAuth = options?.disallowAuth ?? false
  99. // 2. 长度校验
  100. if (validateLength && url!.length > maxAllowedLength) {
  101. return false;
  102. }
  103. // 3. 片段和查询参数校验
  104. if (!allowFragments && url.includes('#')) return false;
  105. if (!allowQueryComponents && (url.includes('?') || url.includes('&'))) return false;
  106. // 处理 URL 组成部分
  107. const [urlWithoutFragment] = url.split('#');
  108. const [baseUrl] = urlWithoutFragment.split('?');
  109. // 4. 协议处理
  110. const protocolParts = baseUrl.split('://');
  111. let protocol:string;
  112. let remainingUrl = baseUrl;
  113. if (protocolParts.length > 1) {
  114. protocol = protocolParts.shift()!.toLowerCase();
  115. if (requireValidProtocol && !protocols!.includes(protocol)) {
  116. return false;
  117. }
  118. remainingUrl = protocolParts.join('://');
  119. } else if (requireProtocol) {
  120. return false;
  121. } else if (baseUrl.startsWith('//')) {
  122. if (!allowProtocolRelativeUrls) return false;
  123. remainingUrl = baseUrl.slice(2);
  124. }
  125. if (remainingUrl == '') return false;
  126. // 5. 处理主机部分
  127. const [hostPart] = remainingUrl.split('/', 1);
  128. const authParts = hostPart.split('@');
  129. // 认证信息校验
  130. if (authParts.length > 1) {
  131. if (disallowAuth || authParts[0] == '') return false;
  132. const auth = authParts.shift()!;
  133. if (auth.split(':').length > 2) return false;
  134. const [user, password] = auth.split(':');
  135. if (user == '' && password == '') return false;
  136. }
  137. const hostname = authParts.join('@');
  138. // 6. 解析主机和端口
  139. type HostInfo = {
  140. host ?: string;
  141. ipv6 ?: string;
  142. port ?: number;
  143. };
  144. const hostInfo : HostInfo = {};
  145. const ipv6Match = hostname.match(WRAPPED_IPV6_REGEX);
  146. if (ipv6Match != null) {
  147. hostInfo.ipv6 = ipv6Match.length > 1 ? ipv6Match[1] : null;
  148. const portStr = ipv6Match.length > 2 ? ipv6Match[2] : null;
  149. if (portStr != null) {
  150. hostInfo.port = parseInt(portStr);
  151. if (!isValidPort(hostInfo.port)) return false;
  152. }
  153. } else {
  154. const [host, ...portParts] = hostname.split(':');
  155. hostInfo.host = host;
  156. if (portParts.length > 0) {
  157. const portStr = portParts.join(':');
  158. hostInfo.port = parseInt(portStr);
  159. if (!isValidPort(hostInfo.port)) return false;
  160. }
  161. }
  162. // 7. 端口校验
  163. if (requirePort && hostInfo.port == null) return false;
  164. // 8. 主机验证逻辑
  165. const finalHost = hostInfo.host ?? hostInfo.ipv6;
  166. if (finalHost == null) return requireHost ? false : true;
  167. // 白名单/黑名单检查
  168. if (hostWhitelist != null && !checkHost(finalHost!, hostWhitelist!)) return false;
  169. if (hostBlacklist != null && checkHost(finalHost!, hostBlacklist!)) return false;
  170. // 9. 综合校验
  171. return validateHost(
  172. finalHost,
  173. options,
  174. !(hostInfo.ipv6 == null || hostInfo.ipv6 == '')
  175. );
  176. }