216 lines
6.1 KiB
JavaScript
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
|
|
};
|
|
|