UNPKG

ajt-validator

Version:

Validation library for JavaScript and TypeScript

242 lines (241 loc) 10.2 kB
"use strict"; /** * URL Validator module * A comprehensive tool for validating, parsing, and analyzing URLs */ Object.defineProperty(exports, "__esModule", { value: true }); exports.URLValidator = void 0; /** * Class for validating and parsing URLs */ class URLValidator { /** * Constructor for URLValidator * @param options - Configuration options */ constructor(options = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; this.lastError = null; // Default configuration with type safety this.options = { allowedProtocols: (_a = options.allowedProtocols) !== null && _a !== void 0 ? _a : ['http:', 'https:'], allowedDomains: (_b = options.allowedDomains) !== null && _b !== void 0 ? _b : null, allowSubdomains: (_c = options.allowSubdomains) !== null && _c !== void 0 ? _c : true, allowIPAddresses: (_d = options.allowIPAddresses) !== null && _d !== void 0 ? _d : true, allowAuth: (_e = options.allowAuth) !== null && _e !== void 0 ? _e : true, allowFragment: (_f = options.allowFragment) !== null && _f !== void 0 ? _f : true, requirePath: (_g = options.requirePath) !== null && _g !== void 0 ? _g : false, pathPattern: (_h = options.pathPattern) !== null && _h !== void 0 ? _h : null, requiredQueryParams: (_j = options.requiredQueryParams) !== null && _j !== void 0 ? _j : [], allowedQueryParams: (_k = options.allowedQueryParams) !== null && _k !== void 0 ? _k : null, disallowPorts: (_l = options.disallowPorts) !== null && _l !== void 0 ? _l : [], errorMessage: (_m = options.errorMessage) !== null && _m !== void 0 ? _m : 'Invalid URL format' }; } /** * Validate a URL string against configured rules * @param url - The URL to validate * @returns Whether the URL is valid */ validate(url) { // Reset error message this.lastError = null; // Check if URL is empty if (!url || typeof url !== 'string' || url.trim() === '') { this.lastError = 'URL cannot be empty'; return false; } try { // Use built-in URL parser first for basic validation const parsedUrl = new URL(url); // Validate protocol if (this.options.allowedProtocols.length > 0 && !this.options.allowedProtocols.includes(parsedUrl.protocol)) { this.lastError = `Protocol must be one of: ${this.options.allowedProtocols.join(', ')}`; return false; } // Validate domain if (this.options.allowedDomains && this.options.allowedDomains.length > 0) { const hostname = parsedUrl.hostname; // Check if it's an IP address const isIpAddress = this._isIpAddress(hostname); if (isIpAddress) { if (!this.options.allowIPAddresses) { this.lastError = 'IP addresses are not allowed'; return false; } } else { // Domain validation let isDomainValid = false; for (const domain of this.options.allowedDomains) { if (hostname === domain) { isDomainValid = true; break; } if (this.options.allowSubdomains && hostname.endsWith('.' + domain)) { isDomainValid = true; break; } } if (!isDomainValid) { this.lastError = 'Domain is not in the list of allowed domains'; return false; } } } // Validate port if (this.options.disallowPorts.length > 0 && parsedUrl.port && this.options.disallowPorts.includes(parseInt(parsedUrl.port, 10))) { this.lastError = `Port ${parsedUrl.port} is not allowed`; return false; } // Validate auth if (!this.options.allowAuth && (parsedUrl.username || parsedUrl.password)) { this.lastError = 'Authentication credentials in URLs are not allowed'; return false; } // Validate fragment if (!this.options.allowFragment && parsedUrl.hash) { this.lastError = 'URL fragments are not allowed'; return false; } // Validate path const path = parsedUrl.pathname; if (this.options.requirePath && (path === '/' || path === '')) { this.lastError = 'URL path is required'; return false; } if (this.options.pathPattern && path !== '/' && path !== '' && !this.options.pathPattern.test(path)) { this.lastError = 'URL path does not match required format'; return false; } // Validate query parameters const queryParams = this._parseQueryParams(parsedUrl.search); // Check required query parameters if (this.options.requiredQueryParams.length > 0) { const queryKeys = queryParams.map(([key]) => key); for (const requiredParam of this.options.requiredQueryParams) { if (!queryKeys.includes(requiredParam)) { this.lastError = `Required query parameter '${requiredParam}' is missing`; return false; } } } // Check allowed query parameters if (this.options.allowedQueryParams && this.options.allowedQueryParams.length > 0) { for (const [key] of queryParams) { if (!this.options.allowedQueryParams.includes(key)) { this.lastError = `Query parameter '${key}' is not allowed`; return false; } } } // If we reached here, the URL is valid return true; } catch (error) { // URL parsing failed (malformed URL) this.lastError = error instanceof Error ? error.message : 'Invalid URL format'; return false; } } /** * Get the error message from the last validation * @returns The error message or null if no error */ getErrorMessage() { return this.lastError || this.options.errorMessage; } /** * Parse a URL into its components * @param url - The URL to parse * @returns Parsed URL components or null if invalid */ parseURL(url) { try { const parsedUrl = new URL(url); // Check for IP address const isIPAddress = this._isIpAddress(parsedUrl.hostname); // Parse query parameters const queryParams = this._parseQueryParams(parsedUrl.search); // Default port numbers for common protocols const defaultPorts = { 'http:': 80, 'https:': 443, 'ftp:': 21, 'ftps:': 990 }; // Return structured object with URL components return { protocol: parsedUrl.protocol, username: parsedUrl.username, password: parsedUrl.password, hostname: parsedUrl.hostname, port: parsedUrl.port ? parseInt(parsedUrl.port, 10) : defaultPorts[parsedUrl.protocol] || null, path: parsedUrl.pathname, query: parsedUrl.search, fragment: parsedUrl.hash, isIPAddress, queryParams }; } catch (error) { return null; } } /** * Normalize a URL * @param url - URL to normalize * @returns Normalized URL */ normalizeURL(url) { try { // Basic normalization using URL constructor return new URL(url).toString(); } catch (error) { return url; } } /** * Parse query string into array of key-value pairs * @param queryString - The query string to parse * @returns Array of key-value pairs */ _parseQueryParams(queryString) { if (!queryString || queryString === '?') { return []; } // Remove leading ? if present const query = queryString.startsWith('?') ? queryString.substring(1) : queryString; return query .split('&') .map(param => { const [key, value] = param.split('='); return [key, value === undefined ? '' : decodeURIComponent(value)]; }); } /** * Check if a string is an IP address * @param str - String to check * @returns Whether the string is an IP address */ _isIpAddress(str) { // IPv4 pattern const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // Simple IPv6 pattern (not exhaustive) const ipv6Pattern = /^\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)]$/; return ipv4Pattern.test(str) || ipv6Pattern.test(str); } } exports.URLValidator = URLValidator; // Export for compatibility exports.default = { URLValidator };