const { logError, logWarn } = require('./logger'); const { parseMinutesToMs, parseSecondsToMs } = require('./utils'); /** * Validates and normalizes configuration from environment variables * @returns {object} - Validated configuration object */ function validateConfig() { const errors = []; const warnings = []; const config = {}; // Storage directory config.STORAGE_DIR = process.env.STORAGE_DIR || './my-storage'; // Domains file config.DOMAINS_FILE = process.env.DOMAINS_FILE || './cache/domains.json'; // Local DNS file config.LOCAL_DNS_FILE = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json'; // Holesail files config.HOLESAIL_SERVERS_FILE = process.env.HOLESAIL_SERVERS_FILE || './cache/holesail_servers.json'; config.HOLESAIL_CLIENTS_FILE = process.env.HOLESAIL_CLIENTS_FILE || './cache/holesail_clients.json'; // Selector cache file config.SELECTOR_CACHE_FILE = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json'; // Topic seed config.TOPIC_SEED = process.env.TOPIC_SEED || 'p2ns-dns'; // Certificates directory config.CERTS_DIR = process.env.CERTS_DIR || './certs'; // Internal port const internalPort = parseInt(process.env.INTERNAL_PORT || '8080', 10); if (isNaN(internalPort) || internalPort < 1 || internalPort > 65535) { errors.push(`INTERNAL_PORT must be between 1 and 65535, got: ${process.env.INTERNAL_PORT}`); } else { config.INTERNAL_PORT = internalPort; } // Subnet base (legacy, for backward compatibility) config.SUBNET_BASE = process.env.SUBNET_BASE || '192.168.3'; // Initial IP index (legacy, for backward compatibility) const initialIPIndex = parseInt(process.env.INITIAL_IP_INDEX || '2', 10); if (isNaN(initialIPIndex) || initialIPIndex < 1 || initialIPIndex > 254) { warnings.push(`INITIAL_IP_INDEX should be between 1 and 254, got: ${process.env.INITIAL_IP_INDEX}`); } else { config.INITIAL_IP_INDEX = initialIPIndex; } // Subnets configuration (multiple subnets support) let subnets = []; if (process.env.SUBNETS) { try { subnets = JSON.parse(process.env.SUBNETS); if (!Array.isArray(subnets)) { errors.push('SUBNETS must be a JSON array'); } else { // Validate each subnet entry subnets.forEach((subnet, index) => { if (!subnet || typeof subnet !== 'object') { errors.push(`SUBNETS[${index}]: must be an object`); return; } // Validate base IP (should be network address) if (!subnet.base || typeof subnet.base !== 'string') { errors.push(`SUBNETS[${index}]: base is required and must be a string`); } else if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(subnet.base)) { errors.push(`SUBNETS[${index}]: base must be a valid IPv4 address (got: ${subnet.base})`); } // Validate CIDR (1-32) const cidr = parseInt(subnet.cidr, 10); if (isNaN(cidr) || cidr < 1 || cidr > 32) { errors.push(`SUBNETS[${index}]: cidr must be between 1 and 32 (got: ${subnet.cidr})`); } else { subnet.cidr = cidr; } // Validate startIndex (1-254, or up to subnet size) const startIndex = parseInt(subnet.startIndex || config.INITIAL_IP_INDEX, 10); const maxIPs = Math.pow(2, 32 - subnet.cidr) - 2; // Subtract network and broadcast if (isNaN(startIndex) || startIndex < 1 || startIndex > Math.min(254, maxIPs)) { warnings.push(`SUBNETS[${index}]: startIndex should be between 1 and ${Math.min(254, maxIPs)} (got: ${startIndex})`); } else { subnet.startIndex = startIndex; } // Name is optional, default to "Subnet N" if (!subnet.name || typeof subnet.name !== 'string') { subnet.name = `Subnet ${index + 1}`; } }); if (subnets.length === 0) { warnings.push('SUBNETS array is empty, will fall back to SUBNET_BASE'); } } } catch (err) { errors.push(`Failed to parse SUBNETS JSON: ${err.message}`); } } // If no subnets configured, create default from SUBNET_BASE for backward compatibility if (subnets.length === 0) { const baseParts = config.SUBNET_BASE.split('.'); if (baseParts.length === 3) { subnets = [{ base: `${config.SUBNET_BASE}.0`, cidr: 24, startIndex: config.INITIAL_IP_INDEX, name: 'Default Subnet' }]; } } config.SUBNETS = subnets; // Public DNS server(s) - supports comma-separated list const publicDnsServerEnv = process.env.PUBLIC_DNS_SERVER || '1.1.1.1'; config.PUBLIC_DNS_SERVER = publicDnsServerEnv; // Parse and validate comma-separated list const dnsServers = publicDnsServerEnv.split(',').map(s => s.trim()).filter(s => s.length > 0); const invalidServers = []; dnsServers.forEach((server, index) => { if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(server)) { invalidServers.push(`server ${index + 1} (${server})`); } }); if (invalidServers.length > 0) { warnings.push(`PUBLIC_DNS_SERVER contains invalid IP addresses: ${invalidServers.join(', ')}`); } if (dnsServers.length === 0) { warnings.push('PUBLIC_DNS_SERVER is empty, using default 1.1.1.1'); config.PUBLIC_DNS_SERVER = '1.1.1.1'; } // Holesail timeout (in minutes, converted to milliseconds) try { config.HOLESAIL_TIMEOUT = parseMinutesToMs(process.env.HOLESAIL_TIMEOUT || '5'); if (config.HOLESAIL_TIMEOUT < 0) { warnings.push(`HOLESAIL_TIMEOUT should be a positive number, got: ${process.env.HOLESAIL_TIMEOUT}`); } } catch (err) { warnings.push(`HOLESAIL_TIMEOUT parse error: ${err.message}, using default 5 minutes`); config.HOLESAIL_TIMEOUT = parseMinutesToMs('5'); } // Full persistence config.FULL_PERSISTENCE = process.env.FULL_PERSISTENCE === 'true'; // Port check timeout (in seconds, converted to milliseconds) try { config.PORT_CHECK_TIMEOUT = parseSecondsToMs(process.env.PORT_CHECK_TIMEOUT || '2'); if (config.PORT_CHECK_TIMEOUT < 0) { warnings.push(`PORT_CHECK_TIMEOUT should be a positive number, got: ${process.env.PORT_CHECK_TIMEOUT}`); } } catch (err) { warnings.push(`PORT_CHECK_TIMEOUT parse error: ${err.message}, using default 2 seconds`); config.PORT_CHECK_TIMEOUT = parseSecondsToMs('2'); } // Log level const logLevel = parseInt(process.env.LOG_LEVEL || '0', 10); if (isNaN(logLevel) || logLevel < 0 || logLevel > 3) { warnings.push(`LOG_LEVEL should be between 0 and 3, got: ${process.env.LOG_LEVEL}`); } else { config.LOG_LEVEL = logLevel; } // DNS port const dnsPort = parseInt(process.env.DNS_PORT || '53', 10); if (isNaN(dnsPort) || dnsPort < 1 || dnsPort > 65535) { errors.push(`DNS_PORT must be between 1 and 65535, got: ${process.env.DNS_PORT}`); } else { config.DNS_PORT = dnsPort; } // HTTPS port const httpsPort = parseInt(process.env.HTTPS_PORT || '443', 10); if (isNaN(httpsPort) || httpsPort < 1 || httpsPort > 65535) { errors.push(`HTTPS_PORT must be between 1 and 65535, got: ${process.env.HTTPS_PORT}`); } else { config.HTTPS_PORT = httpsPort; } // HTTP port const httpPort = parseInt(process.env.HTTP_PORT || '80', 10); if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) { errors.push(`HTTP_PORT must be between 1 and 65535, got: ${process.env.HTTP_PORT}`); } else { config.HTTP_PORT = httpPort; } // Feature flags config.DISABLE_DNS_SERVER = process.env.DISABLE_DNS_SERVER === 'true'; config.DISABLE_PROXY_SERVER = process.env.DISABLE_PROXY_SERVER === 'true'; config.DISABLE_VIRTUAL_INTERFACES = process.env.DISABLE_VIRTUAL_INTERFACES === 'true'; config.ALLOW_ANY_WRITER_INVITES = process.env.ALLOW_ANY_WRITER_INVITES === 'true'; // Consensus configuration const consensusQuorumThreshold = parseFloat(process.env.CONSENSUS_QUORUM_THRESHOLD || '0.5'); if (isNaN(consensusQuorumThreshold) || consensusQuorumThreshold < 0 || consensusQuorumThreshold > 1) { warnings.push(`CONSENSUS_QUORUM_THRESHOLD should be between 0 and 1, got: ${process.env.CONSENSUS_QUORUM_THRESHOLD}`); } else { config.CONSENSUS_QUORUM_THRESHOLD = consensusQuorumThreshold; } const consensusMinVotes = parseInt(process.env.CONSENSUS_MIN_VOTES || '2', 10); if (isNaN(consensusMinVotes) || consensusMinVotes < 1) { warnings.push(`CONSENSUS_MIN_VOTES should be at least 1, got: ${process.env.CONSENSUS_MIN_VOTES}`); } else { config.CONSENSUS_MIN_VOTES = consensusMinVotes; } const consensusTieBreaker = process.env.CONSENSUS_TIE_BREAKER || 'timestamp'; if (!['timestamp', 'claimant_age', 'lexicographic'].includes(consensusTieBreaker)) { warnings.push(`CONSENSUS_TIE_BREAKER should be one of: timestamp, claimant_age, lexicographic. Got: ${consensusTieBreaker}`); } else { config.CONSENSUS_TIE_BREAKER = consensusTieBreaker; } config.CONSENSUS_VOTE_VALIDATION = process.env.CONSENSUS_VOTE_VALIDATION !== 'false'; config.CONSENSUS_IMMEDIATE_UPDATE = process.env.CONSENSUS_IMMEDIATE_UPDATE !== 'false'; // Log errors and warnings if (errors.length > 0) { errors.forEach(error => logError('Config', error)); throw new Error(`Configuration validation failed: ${errors.join('; ')}`); } if (warnings.length > 0) { warnings.forEach(warning => logWarn('Config', warning)); } return config; } module.exports = { validateConfig };