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

248 lines
9.4 KiB
JavaScript

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 };