Files
p2ns/includes/infrastructure/validation.js
T
2025-12-17 20:05:50 -05:00

216 lines
6.1 KiB
JavaScript

const { logError } = require('./logger');
// Domain name validation regex (RFC 1035) - supports punycode (xn--)
const DOMAIN_REGEX = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
// Holesail hash validation (starts with hs://, allows alphanumeric and hyphens)
const HOLESAIL_HASH_REGEX = /^hs:\/\/[a-zA-Z0-9\-]+$/;
// IP address validation regex
const IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
/**
* Validates a domain name
* @param {string} domain - Domain name to validate
* @returns {boolean} - True if valid
*/
function validateDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// Allow internal domains
if (domain === 'localhost' || domain.startsWith('127.0.0.1')) {
return true;
}
// Check basic domain format
if (domain.length > 253) {
return false;
}
// Allow punycode domains (xn-- prefix)
// Punycode labels start with xn-- followed by ASCII characters
if (domain.includes('xn--')) {
// Check if it's a valid punycode domain format
// Can be: xn--label, label.xn--label, or label.xn--label.tld
const punycodeLabelPattern = /^xn--[a-zA-Z0-9\-]{1,59}$/;
const punycodeMultiLabelPattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*xn--[a-zA-Z0-9\-]{1,59}(\.([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z]{2,})?$/;
// Single punycode label (like xn--um8h)
if (punycodeLabelPattern.test(domain)) {
return true;
}
// Multi-label with punycode
if (punycodeMultiLabelPattern.test(domain)) {
return true;
}
// Fallback to standard regex in case punycode is in middle labels
return DOMAIN_REGEX.test(domain);
}
return DOMAIN_REGEX.test(domain);
}
/**
* Validates a Holesail hash
* @param {string} hash - Hash to validate
* @returns {boolean} - True if valid
*/
function validateHolesailHash(hash) {
if (!hash || typeof hash !== 'string') {
return false;
}
return HOLESAIL_HASH_REGEX.test(hash);
}
/**
* Validates a port number
* @param {number|string} port - Port to validate
* @returns {boolean} - True if valid
*/
function validatePort(port) {
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
if (isNaN(portNum)) {
return false;
}
return portNum >= 1 && portNum <= 65535;
}
/**
* Validates an IP address
* @param {string} ip - IP address to validate
* @returns {boolean} - True if valid
*/
function validateIP(ip) {
if (!ip || typeof ip !== 'string') {
return false;
}
if (!IP_REGEX.test(ip)) {
return false;
}
const parts = ip.split('.');
return parts.every(part => {
const num = parseInt(part, 10);
return num >= 0 && num <= 255;
});
}
/**
* Validates a file path (basic validation to prevent directory traversal)
* @param {string} path - Path to validate
* @returns {boolean} - True if valid
*/
function validateFilePath(path) {
if (!path || typeof path !== 'string') {
return false;
}
// Prevent directory traversal
if (path.includes('..') || path.includes('//')) {
return false;
}
return true;
}
/**
* Sanitizes input by trimming and removing null bytes
* @param {string} input - Input to sanitize
* @returns {string} - Sanitized input
*/
function sanitizeInput(input) {
if (typeof input !== 'string') {
return input;
}
return input.trim().replace(/\0/g, '');
}
/**
* Validates domain removal data
* @param {object} data - Data object with domain property
* @returns {object} - { valid: boolean, error?: string }
*/
function validateDomainRemoval(data) {
if (!data || typeof data !== 'object') {
return { valid: false, error: 'Invalid data format' };
}
if (!data.domain) {
return { valid: false, error: 'Domain is required' };
}
const domain = sanitizeInput(data.domain);
if (!validateDomain(domain)) {
return { valid: false, error: 'Invalid domain format' };
}
return { valid: true, domain };
}
/**
* Validates domain addition data
* @param {object} data - Data object with domain and hash properties
* @returns {object} - { valid: boolean, error?: string, domain?: string, hash?: string }
*/
function validateDomainAddition(data) {
if (!data || typeof data !== 'object') {
return { valid: false, error: 'Invalid data format' };
}
if (!data.domain) {
return { valid: false, error: 'Domain is required' };
}
if (!data.hash) {
return { valid: false, error: 'Hash is required' };
}
const domain = sanitizeInput(data.domain);
const hash = sanitizeInput(data.hash);
if (!validateDomain(domain)) {
return { valid: false, error: 'Invalid domain format' };
}
if (!validateHolesailHash(hash)) {
return { valid: false, error: 'Invalid Holesail hash format' };
}
return { valid: true, domain, hash };
}
/**
* Validates Holesail client creation data
* @param {object} data - Data object with domain, key, port, protocol
* @returns {object} - { valid: boolean, error?: string, ... }
*/
function validateHolesailClient(data) {
if (!data || typeof data !== 'object') {
return { valid: false, error: 'Invalid data format' };
}
if (!data.domain) {
return { valid: false, error: 'Domain is required' };
}
if (!data.key) {
return { valid: false, error: 'Holesail key is required' };
}
if (!data.port) {
return { valid: false, error: 'Port is required' };
}
const domain = sanitizeInput(data.domain);
const key = sanitizeInput(data.key);
const protocol = sanitizeInput(data.protocol || 'tcp');
if (!validateDomain(domain)) {
return { valid: false, error: 'Invalid domain format' };
}
if (!validateHolesailHash(key)) {
return { valid: false, error: 'Invalid Holesail key format' };
}
if (!validatePort(data.port)) {
return { valid: false, error: 'Invalid port number' };
}
if (protocol !== 'tcp' && protocol !== 'udp') {
return { valid: false, error: 'Protocol must be tcp or udp' };
}
return { valid: true, domain, key, port: parseInt(data.port, 10), protocol };
}
module.exports = {
validateDomain,
validateHolesailHash,
validatePort,
validateIP,
validateFilePath,
sanitizeInput,
validateDomainRemoval,
validateDomainAddition,
validateHolesailClient
};