ajt-validator
Version:
Validation library for JavaScript and TypeScript
242 lines (241 loc) • 10.2 kB
JavaScript
"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 };