const express = require('express'); const httpProxy = require('http-proxy'); const http = require('http'); const net = require('net'); const url = require('url'); const Holesail = require('holesail'); const Corestore = require('corestore'); const Hyperswarm = require('hyperswarm'); const Autopass = require('autopass'); const Protomux = require('protomux'); const c = require('compact-encoding'); const crypto = require('crypto'); const process = require('process'); const fs = require('fs'); const path = require('path'); const { parseMinutesToMs, parseSecondsToMs, secondsToMs, loadOrCreateKeypair, setupCache, getPersistentPublicKey, isSecureHolesailKey } = require('../includes/infrastructure/utils'); const { validateDomainDetailed } = require('../includes/infrastructure/validation'); // ============================================================================ // Configuration // ============================================================================ const config = { // Server configuration PROXY_PORT: parseInt(process.env.PROXY_PORT || '5577'), PROXY_HOST: process.env.PROXY_HOST || '0.0.0.0', // P2NS configuration TOPIC_SEED: process.env.TOPIC_SEED || 'p2ns-dns', STORAGE_DIR: process.env.STORAGE_DIR || './my-proxy-storage', // Holesail configuration HOLESAIL_TIMEOUT: parseMinutesToMs(process.env.HOLESAIL_TIMEOUT || '720'), // 12 hours = 720 minutes FULL_PERSISTENCE: process.env.FULL_PERSISTENCE === 'true', HTTP_PORT: parseInt(process.env.HTTP_PORT || '80'), DISABLE_PROXY_SERVER: process.env.DISABLE_PROXY_SERVER === 'true', MAX_HOLESAIL_CLIENTS: parseInt(process.env.MAX_HOLESAIL_CLIENTS || '100'), // Performance configuration CACHE_TTL: parseSecondsToMs(process.env.CACHE_TTL || '60'), // 60 seconds AUTO_VOTE_DEBOUNCE: parseSecondsToMs(process.env.AUTO_VOTE_DEBOUNCE || '60'), // 60 seconds // Security configuration RATE_LIMIT_WINDOW: parseSecondsToMs(process.env.RATE_LIMIT_WINDOW || '60'), // 1 minute = 60 seconds RATE_LIMIT_MAX_REQUESTS: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), RATE_LIMIT_MAX_PER_DOMAIN: parseInt(process.env.RATE_LIMIT_MAX_PER_DOMAIN || '20'), REQUEST_TIMEOUT: parseSecondsToMs(process.env.REQUEST_TIMEOUT || '30'), // 30 seconds // Logging DEBUG: process.env.DEBUG === 'true', // Domain validation MAX_DOMAIN_LENGTH: 253, MAX_DOMAIN_CACHE_SIZE: parseInt(process.env.MAX_DOMAIN_CACHE_SIZE || '5000', 10), MAX_METRIC_MAP_SIZE: parseInt(process.env.MAX_METRIC_MAP_SIZE || '1000', 10), DOMAIN_REGEX: /^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)*$/i }; // Validate configuration function validateConfig() { if (config.PROXY_PORT < 1 || config.PROXY_PORT > 65535) { throw new Error(`Invalid PROXY_PORT: ${config.PROXY_PORT}`); } if (config.HOLESAIL_TIMEOUT < 0) { throw new Error(`Invalid HOLESAIL_TIMEOUT: ${config.HOLESAIL_TIMEOUT}`); } if (config.MAX_HOLESAIL_CLIENTS < 1) { throw new Error(`Invalid MAX_HOLESAIL_CLIENTS: ${config.MAX_HOLESAIL_CLIENTS}`); } } validateConfig(); // ============================================================================ // Enhanced Logging with Request IDs // ============================================================================ let requestIdCounter = 0; function generateRequestId() { return `req-${Date.now()}-${++requestIdCounter}`; } function logWithContext(level, category, message, context = {}) { const timestamp = new Date().toISOString(); const contextStr = Object.keys(context).length > 0 ? ` [${Object.entries(context).map(([k, v]) => `${k}=${v}`).join(', ')}]` : ''; const logMessage = `[${timestamp}] [${level}] [${category}]${contextStr} ${message}`; if (level === 'ERROR') { console.error(logMessage); } else if (level === 'WARN') { console.warn(logMessage); } else if (level === 'DEBUG' && config.DEBUG) { console.debug(logMessage); } else if (level === 'INFO') { console.log(logMessage); } } const logInfo = (category, message, context) => logWithContext('INFO', category, message, context); const logError = (category, message, context) => logWithContext('ERROR', category, message, context); const logWarn = (category, message, context) => logWithContext('WARN', category, message, context); const logDebug = (category, message, context) => logWithContext('DEBUG', category, message, context); // ============================================================================ // Metrics Collection // ============================================================================ const metrics = { requests: { total: 0, success: 0, errors: 0, notFound: 0, timeouts: 0 }, activeConnections: 0, holesailClients: 0, responseTimes: [], errorsByType: new Map(), requestsByDomain: new Map(), lastReset: Date.now() }; function recordMetric(type, value = 1) { if (type === 'request') { metrics.requests.total += value; } else if (type === 'success') { metrics.requests.success += value; } else if (type === 'error') { metrics.requests.errors += value; } else if (type === 'notFound') { metrics.requests.notFound += value; } else if (type === 'timeout') { metrics.requests.timeouts += value; } else if (type === 'responseTime') { metrics.responseTimes.push(value); // Keep only last 1000 response times if (metrics.responseTimes.length > 1000) { metrics.responseTimes.shift(); } } } function evictOldestMapEntry(map, maxSize) { if (map.size <= maxSize) return; const firstKey = map.keys().next().value; if (firstKey !== undefined) { map.delete(firstKey); } } function recordError(type, message) { const count = metrics.errorsByType.get(type) || 0; metrics.errorsByType.set(type, count + 1); evictOldestMapEntry(metrics.errorsByType, config.MAX_METRIC_MAP_SIZE); recordMetric('error'); } function recordDomainRequest(domain) { const count = metrics.requestsByDomain.get(domain) || 0; metrics.requestsByDomain.set(domain, count + 1); evictOldestMapEntry(metrics.requestsByDomain, config.MAX_METRIC_MAP_SIZE); } function getMetrics() { const avgResponseTime = metrics.responseTimes.length > 0 ? metrics.responseTimes.reduce((a, b) => a + b, 0) / metrics.responseTimes.length : 0; return { ...metrics, avgResponseTime: Math.round(avgResponseTime), topDomains: Array.from(metrics.requestsByDomain.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([domain, count]) => ({ domain, count })), errorTypes: Array.from(metrics.errorsByType.entries()) .map(([type, count]) => ({ type, count })) }; } // ============================================================================ // Rate Limiting // ============================================================================ const rateLimitStore = { byIP: new Map(), byDomain: new Map() }; function checkRateLimit(ip, domain) { const now = Date.now(); // Per-IP rate limiting const ipKey = ip || 'unknown'; let ipLimits = rateLimitStore.byIP.get(ipKey); if (!ipLimits) { ipLimits = { count: 0, resetTime: now + config.RATE_LIMIT_WINDOW }; rateLimitStore.byIP.set(ipKey, ipLimits); } if (now > ipLimits.resetTime) { ipLimits.count = 0; ipLimits.resetTime = now + config.RATE_LIMIT_WINDOW; } if (ipLimits.count >= config.RATE_LIMIT_MAX_REQUESTS) { return { allowed: false, reason: 'IP rate limit exceeded' }; } ipLimits.count++; // Per-domain rate limiting if (domain) { let domainLimits = rateLimitStore.byDomain.get(domain); if (!domainLimits) { domainLimits = { count: 0, resetTime: now + config.RATE_LIMIT_WINDOW }; rateLimitStore.byDomain.set(domain, domainLimits); } if (now > domainLimits.resetTime) { domainLimits.count = 0; domainLimits.resetTime = now + config.RATE_LIMIT_WINDOW; } if (domainLimits.count >= config.RATE_LIMIT_MAX_PER_DOMAIN) { return { allowed: false, reason: 'Domain rate limit exceeded' }; } domainLimits.count++; } return { allowed: true }; } // Cleanup old rate limit entries periodically setInterval(() => { const now = Date.now(); for (const [key, limits] of rateLimitStore.byIP.entries()) { if (now > limits.resetTime + config.RATE_LIMIT_WINDOW) { rateLimitStore.byIP.delete(key); } } for (const [key, limits] of rateLimitStore.byDomain.entries()) { if (now > limits.resetTime + config.RATE_LIMIT_WINDOW) { rateLimitStore.byDomain.delete(key); } } }, config.RATE_LIMIT_WINDOW * 2); // ============================================================================ // Input Validation // ============================================================================ function validateDomain(domain) { return validateDomainDetailed(domain, { maxLength: config.MAX_DOMAIN_LENGTH }); } // ============================================================================ // Domain-to-Hash Caching // ============================================================================ const domainCache = new Map(); function getCachedHash(domain) { const cached = domainCache.get(domain); if (cached && Date.now() < cached.expires) { return cached.hash; } if (cached) { domainCache.delete(domain); } return null; } function setCachedHash(domain, hash) { if (domainCache.size >= config.MAX_DOMAIN_CACHE_SIZE) { const firstKey = domainCache.keys().next().value; if (firstKey !== undefined) { domainCache.delete(firstKey); } } domainCache.set(domain, { hash, expires: Date.now() + config.CACHE_TTL }); } function invalidateDomainCache(domain = null) { if (domain) { domainCache.delete(domain); } else { domainCache.clear(); } } // ============================================================================ // State Management // ============================================================================ const state = { connectedPeers: new Set(), peerChannels: new Map(), dnsPass: null, holesails: new Map(), starting: new Map(), tlsServers: new Map(), httpServers: new Map(), swarm: null, topic: null, store: null, core: null, keypair: null // Persistent keypair for consistent peer identity }; // ============================================================================ // Stubbed TLS Proxy (as per original design) // ============================================================================ const ca = { createTlsProxy: (domain, ip, port, log) => { const server = net.createServer(); // Placeholder log('Holesail', `Stubbed TLS proxy created for ${domain} on ${ip}:${port}`, { domain, ip, port }); return server; } }; // ============================================================================ // Port Management // ============================================================================ async function checkPortAvailability(ip, port) { return new Promise((resolve, reject) => { const server = net.createServer(); server.once('error', (err) => { server.close(); if (err.code === 'EADDRINUSE') reject(new Error(`Port ${port} on ${ip} is in use`)); else reject(err); }); server.once('listening', () => { server.close(); resolve(); }); server.listen(port, ip); }); } // ============================================================================ // Holesail Client Management (Improved with better locking) // ============================================================================ async function startHolesailClient(domain, hash, ip, persistent = false) { return new Promise((resolve, reject) => { // Check connection limit if (state.holesails.size >= config.MAX_HOLESAIL_CLIENTS) { reject(new Error(`Maximum Holesail clients limit reached: ${config.MAX_HOLESAIL_CLIENTS}`)); return; } const server = net.createServer(); server.listen(0, ip, async () => { const port = server.address().port; server.close(); const key = `${domain}:${port}`; // Check if already exists if (state.holesails.has(key)) { logDebug('Holesail', `Holesail client already exists for ${key}`, { domain, port }); resolve(port); return; } // Get or create start promise (improved locking) let startPromise = state.starting.get(key); if (!startPromise) { startPromise = (async () => { try { logInfo('Holesail', `Starting Holesail client for domain: ${domain}`, { domain, hash, ip, port }); const secure = isSecureHolesailKey(hash); logInfo('Holesail', `Creating Holesail client for ${domain} with secure=${secure} (key starts with: ${hash.substring(0, 10)}...)`); const holesail = new Holesail({ client: true, key: hash, port: port, host: ip, secure: secure, log: false }); await holesail.ready(); state.holesails.set(key, holesail); metrics.holesailClients = state.holesails.size; logInfo('Holesail', `Holesail client for ${key} connected`, { domain, ip, port }); if (!config.DISABLE_PROXY_SERVER) { const tlsServer = ca.createTlsProxy(domain, ip, port, logDebug); state.tlsServers.set(key, tlsServer); const httpPort = config.HTTP_PORT; if (port === httpPort) { try { await checkPortAvailability(ip, httpPort); const httpServer = http.createServer((req, res) => { const host = req.headers.host || domain; res.writeHead(301, { 'Location': `https://${host}${req.url}` }); res.end(); }); // Add error handler for HTTP server httpServer.on('error', (err) => { logError('Holesail', `HTTP redirect server error for ${key}`, { domain, error: err.message }); }); httpServer.listen(httpPort, ip, () => { logInfo('Holesail', `HTTP redirect server for ${key} listening`, { domain, ip, port: httpPort }); }); state.httpServers.set(key, httpServer); } catch (err) { logError('Holesail', `Failed to start HTTP redirect server for ${key}`, { domain, error: err.message }); } } } if (!persistent) { setTimeout(async () => { logInfo('Holesail', `Closing Holesail client for ${key}`, { domain }); try { await holesail.close(); state.holesails.delete(key); metrics.holesailClients = state.holesails.size; const tlsServer = state.tlsServers.get(key); if (tlsServer) { tlsServer.close(); state.tlsServers.delete(key); } const httpServer = state.httpServers.get(key); if (httpServer) { httpServer.close(); state.httpServers.delete(key); } } catch (err) { logError('Holesail', `Error closing Holesail client for ${key}`, { domain, error: err.message }); } }, config.HOLESAIL_TIMEOUT); } resolve(port); } catch (err) { logError('Holesail', `Error connecting Holesail client for ${key}`, { domain, error: err.message }); reject(err); } finally { state.starting.delete(key); } })(); state.starting.set(key, startPromise); } // Wait for the promise startPromise.then(resolve).catch(reject); }); server.on('error', (err) => { server.close(); logError('Holesail', `Failed to bind port for ${domain}`, { domain, ip, error: err.message }); reject(err); }); }); } // ============================================================================ // Core Functions (Optimized) // ============================================================================ let cachedEntries = null; let cachedEntriesTime = 0; const ENTRIES_CACHE_TTL = secondsToMs(5); // 5 seconds async function getAllEntries(pass = state.dnsPass, useCache = true) { const now = Date.now(); if (useCache && cachedEntries && (now - cachedEntriesTime) < ENTRIES_CACHE_TTL) { return cachedEntries; } const entries = []; try { const stream = pass.list(); for await (const entry of stream) { entries.push({ key: entry.key.toString('utf8'), value: entry.value.toString('utf8') }); } cachedEntries = entries; cachedEntriesTime = now; } catch (err) { logError('Core', `Error getting entries`, { error: err.message }); } return entries; } async function getHashForDomain(domain) { const cached = getCachedHash(domain); if (cached) { logDebug('Core', `Cache hit for domain: ${domain}`, { domain }); return cached; } try { const { getHashForDomain: coreGetHashForDomain } = require('../includes/core/core'); const hash = await coreGetHashForDomain(domain); if (hash) { setCachedHash(domain, hash); return hash; } } catch (err) { logDebug('Core', `Core consensus not available: ${err.message}`, { domain }); } return null; } async function getDomainSSLStatus(domain) { try { const { getDomainSSLStatus: coreGetDomainSSLStatus } = require('../includes/core/core'); return await coreGetDomainSSLStatus(domain); } catch (err) { logDebug('Core', `Core SSL status not available, defaulting to false: ${err.message}`, { domain }); return false; } } // ============================================================================ // Auto-Voting (Optimized) // ============================================================================ let lastVoteTime = 0; async function doAutoVotes() { const now = Date.now(); if (now - lastVoteTime < config.AUTO_VOTE_DEBOUNCE) return; lastVoteTime = now; const pass = state.dnsPass; if (!pass) return; logInfo('Core', 'Performing auto-votes check'); // Use cached entries to avoid multiple getAllEntries calls const allEntries = await getAllEntries(pass, true); const domains = new Set(); for (const entry of allEntries) { if (entry.key.startsWith('claim:')) { const parts = entry.key.split(':'); if (parts.length === 3) domains.add(parts[1]); } } for (const domain of domains) { await autoVoteForDomain(domain, allEntries); } } async function autoVoteForDomain(domain, allEntries) { // Try to use core auto-voting logic if available try { const { autoVoteForDomain: coreAutoVoteForDomain } = require('../includes/core/core'); await coreAutoVoteForDomain(domain, allEntries); return; } catch (err) { logDebug('Core', `Core auto-voting not available, using fallback: ${err.message}`, { domain }); } // Fallback to simplified logic if core not available const pass = state.dnsPass; if (!pass) return; const claims = {}; const myVotes = new Map(); const localWriter = getPersistentPublicKey(); if (!localWriter) { logWarn('Core', 'Cannot auto-vote: persistent public key not available', { domain }); return; } for (const entry of allEntries) { if (entry.key.startsWith(`claim:${domain}:`)) { const claimant = entry.key.slice(`claim:${domain}:`.length); // Parse claim value (may be JSON with timestamp or legacy format) try { const parsed = JSON.parse(entry.value); claims[claimant] = parsed.hash || entry.value; } catch (e) { claims[claimant] = entry.value; // Legacy format } } if (entry.key.startsWith(`vote:${domain}:`)) { const parts = entry.key.split(':'); if (parts.length === 4 && parts[3] === localWriter) { myVotes.set(parts[2], entry.key); } } } const claimsLength = Object.keys(claims).length; if (claimsLength === 0) return; const localHasClaim = claims.hasOwnProperty(localWriter); let intendedClaimant; if (localHasClaim) { intendedClaimant = localWriter; } else if (claimsLength === 1) { intendedClaimant = Object.keys(claims)[0]; } else { intendedClaimant = Object.keys(claims).sort((a, b) => a.localeCompare(b))[0]; } // Validate intended claimant exists if (!claims.hasOwnProperty(intendedClaimant)) { logWarn('Core', `Cannot vote for invalid claimant: ${intendedClaimant}`, { domain }); return; } for (const [votedClaimant, voteKey] of myVotes) { if (votedClaimant !== intendedClaimant) { await pass.remove(voteKey); logInfo('Core', `Removed vote for ${domain}`, { domain, claimant: votedClaimant }); } } if (!myVotes.has(intendedClaimant)) { const voteKey = `vote:${domain}:${intendedClaimant}:${localWriter}`; await pass.add(voteKey, claims[intendedClaimant]); logInfo('Core', `Auto-voted for ${domain}`, { domain, claimant: intendedClaimant }); } } // ============================================================================ // P2NS Setup (Fixed scope issues) // ============================================================================ async function setupP2NS() { const isMaster = false; const topic = crypto.createHash('sha256').update(config.TOPIC_SEED).digest(); state.topic = topic; logDebug('Main', `Generated topic`, { topic: topic.toString('hex') }); // Ensure cache directory exists await setupCache(); // Load persistent keypair early and store in state const keypair = await loadOrCreateKeypair(); state.keypair = keypair; logInfo('Main', 'Persistent keypair loaded'); const store = new Corestore(config.STORAGE_DIR); state.store = store; logInfo('Main', `Corestore initialized`, { storageDir: config.STORAGE_DIR }); // Use the persistent keypair from state // Configure Hyperswarm for better local network peer discovery const swarmOptions = { keyPair: state.keypair, maxPeers: parseInt(process.env.MAX_PEERS || '24', 10) }; const swarm = new Hyperswarm(swarmOptions); state.swarm = swarm; logInfo('Main', 'Hyperswarm instance created with persisted keypair'); logInfo('Main', `Hyperswarm configured: maxPeers=${swarmOptions.maxPeers}`); let dnsPass = null; let core = null; let processingInvite = false; let currentInvitePromise = null; let currentPairOperation = null; const failedInvitePeers = new Set(); const pendingInviteAcks = new Map(); const pendingInviteRequestHandlers = new Set(); const peerChannels = new Map(); const channelManager = require('../includes/plugins/channel-manager'); const coreRpc = require('../includes/core/core-rpc'); const { createCoreSwarmHandlers } = require('../includes/core/core-swarm-handlers'); function isProcessingInvite() { return processingInvite; } async function acquireInviteLock() { if (processingInvite && currentInvitePromise) { await currentInvitePromise; } processingInvite = true; } function releaseInviteLock() { processingInvite = false; currentInvitePromise = null; } function withTimeout(promise, timeoutMs, operationName) { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs); }) ]); } function setupDomainsWatcher() { // Proxy does not run full domain watcher } async function listDomains() { return []; } const proxySwarmHandlers = createCoreSwarmHandlers({ Autopass, store, core: { get writable() { return core?.writable; }, update: () => core?.update() }, state, isMaster, getDnsPass: () => dnsPass, setDnsPass: (p) => { dnsPass = p; state.dnsPass = p; core = p.base; state.core = core; }, connectedPeers: state.connectedPeers, peerChannels, failedInvitePeers, pendingInviteAcks, pendingInviteRequestHandlers, isProcessingInvite, acquireInviteLock, releaseInviteLock, withTimeout, getCurrentInvitePromise: () => currentInvitePromise, setCurrentInvitePromise: (p) => { currentInvitePromise = p; }, getCurrentPairOperation: () => currentPairOperation, setCurrentPairOperation: (p) => { currentPairOperation = p; }, setupDomainsWatcher, listDomains, doAutoVotes, setupListeners: () => setupListeners(core), coreRpc, channelManager, CORE_DOMAIN: coreRpc.CORE_DOMAIN }); coreRpc.registerCoreRpcHandlers({ onInviteRequest: proxySwarmHandlers.onInviteRequest, onInviteDeliver: proxySwarmHandlers.onInviteDeliver, onInviteAck: proxySwarmHandlers.onInviteAck, onInviteUnavailable: proxySwarmHandlers.onInviteUnavailable, onInviteQueued: proxySwarmHandlers.onInviteQueued, onInviteRelayRequest: proxySwarmHandlers.onInviteRelayRequest, onInviteRelayResponse: proxySwarmHandlers.onInviteRelayResponse, onCoreStatus: proxySwarmHandlers.onCoreStatus, onConsensusRemoveDomain: proxySwarmHandlers.onConsensusRemoveDomain, onConsensusRecalculate: proxySwarmHandlers.onConsensusRecalculate, onConsensusRemoveConflictClaim: proxySwarmHandlers.onConsensusRemoveConflictClaim }); channelManager.registerPluginProtocol(coreRpc.CORE_DOMAIN, 'request', { methods: {}, autoReconnect: true }); await swarm.flush(); logDebug('Main', 'Swarm flush completed'); swarm.join(topic, { server: true, client: true }); logInfo('Main', 'Joined Hyperswarm topic (server: true, client: true)'); // Set up connection handler with timeout handling for better local network peer support swarm.on('connection', async (conn, info) => { // Set socket timeout to prevent hanging connections (helps with local network peers) if (conn && conn.socket) { const socketTimeout = parseInt(process.env.SWARM_CONNECTION_TIMEOUT || '30000', 10); // 30 seconds default conn.socket.setTimeout(socketTimeout); conn.socket.on('timeout', () => { const peerId = conn.remotePublicKey ? conn.remotePublicKey.toString('hex').slice(0, 16) : 'unknown'; logWarn('Swarm', `Connection timeout for peer ${peerId}...`); if (!conn.destroyed) { conn.destroy(); } }); } // Validate remotePublicKey exists before using it if (!conn.remotePublicKey) { logWarn('Swarm', 'Connection established without remotePublicKey, closing connection'); conn.destroy(); return; } const peerId = conn.remotePublicKey.toString('hex'); logInfo('Swarm', `New connection established`, { peerId }); if (state.connectedPeers.has(peerId)) { logDebug('Swarm', `Peer already connected`, { peerId }); return; } state.connectedPeers.add(peerId); // Do not replicate the full Corestore on the p2ns topic — Autopass uses its own pairing/replication. logDebug('Swarm', 'Skipping p2ns-topic Corestore replication (Autopass handles dnsPass sync)'); const mux = Protomux.from(conn); peerChannels.set(peerId, { conn, mux }); channelManager.registerPeerConnection(peerId, conn, mux); channelManager.createPluginChannelsForPeer(peerId, conn, mux); if (!isMaster) { const maxRetries = 10; let retryCount = 0; const retryInterval = 5000; const sendInviteRequest = async () => { if (dnsPass || retryCount >= maxRetries || conn.destroyed) return; retryCount++; try { const res = await coreRpc.inviteRequest(peerId, 5000); if (res?.status === coreRpc.INVITE_STATUS.UNAVAILABLE) { failedInvitePeers.add(peerId); return; } } catch (err) { logDebug('Swarm', `RPC invite.request failed: ${err.message}`); } setTimeout(sendInviteRequest, retryInterval); }; sendInviteRequest(); } // Fixed: Use peerId instead of undefined 'key' conn.on('close', () => { state.connectedPeers.delete(peerId); peerChannels.delete(peerId); channelManager.unregisterPeerConnection(peerId); channelManager.handlePeerDisconnect(peerId); logDebug('Swarm', `Connection closed`, { peerId }); }); conn.on('error', (err) => { const errMsg = err.message || ''; const errCode = err.code || ''; // Handle common benign network errors - these are normal in P2P networks const benignErrors = [ 'connection reset by peer', 'ECONNRESET', 'EPIPE', 'broken pipe', 'socket hang up', 'ECONNABORTED', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ]; const isBenignError = benignErrors.some(benign => errMsg.toLowerCase().includes(benign.toLowerCase()) || errCode === benign ); if (isBenignError) { // Log as debug instead of error - these are normal network events logDebug('Swarm', `Connection reset/closed by peer`, { peerId, error: errMsg || errCode }); } else { // For unexpected errors, log as error logError('Swarm', `Connection error with peer`, { peerId, error: err.message }); } state.connectedPeers.delete(peerId); state.peerChannels.delete(peerId); // Clean up channels const channels = state.peerChannels.get(peerId); if (channels) { try { if (channels.inviteChannel && !channels.inviteChannel.destroyed) { channels.inviteChannel.close(); } if (channels.requestChannel && !channels.requestChannel.destroyed) { channels.requestChannel.close(); } } catch (cleanupErr) { logDebug('Swarm', `Error cleaning up channels`, { peerId, error: cleanupErr.message }); } } }); }); function setupListeners(core) { state.dnsPass.on('update', () => { // Invalidate cache on DNS updates invalidateDomainCache(); cachedEntries = null; doAutoVotes(); }); core.on('peer-add', (peer) => { core.update().catch(err => { logError('Swarm', `Error updating core after peer-add`, { error: err.message }); }); }); core.on('append', () => { // Invalidate cache on append invalidateDomainCache(); cachedEntries = null; }); } // Wait for P2NS connection with timeout const maxWaitTime = secondsToMs(60); // 60 seconds const startTime = Date.now(); while (!state.dnsPass) { if (Date.now() - startTime > maxWaitTime) { throw new Error('P2NS setup timeout: No DNS pass received within 60 seconds'); } await new Promise(resolve => setTimeout(resolve, 1000)); } logInfo('Main', 'P2NS setup complete as joiner'); } // ============================================================================ // Express Proxy Server // ============================================================================ async function main() { try { await setupP2NS(); } catch (err) { logError('Main', `P2NS setup failed`, { error: err.message }); throw err; } const app = express(); const proxy = httpProxy.createProxyServer({ changeOrigin: true, timeout: config.REQUEST_TIMEOUT }); const server = http.createServer(app); // Log Express and path-to-regexp versions for debugging const expressVersion = require('express/package.json').version; let pathToRegexpVersion = 'unknown'; try { const pathToRegexpPackageJson = JSON.parse(fs.readFileSync(path.join(require.resolve('path-to-regexp'), '../package.json'))); pathToRegexpVersion = pathToRegexpPackageJson.version; } catch (err) { logWarn('Proxy', `Failed to retrieve path-to-regexp version`, { error: err.message }); } logInfo('Proxy', `Using Express v${expressVersion}, path-to-regexp v${pathToRegexpVersion}`); // Handle proxy errors proxy.on('error', (err, req, res) => { const requestId = req.requestId || 'unknown'; logError('Proxy', `Proxy error`, { requestId, url: req.url, error: err.message }); recordError('proxy_error', err.message); if (res && !res.headersSent) { res.status(502).send('Bad Gateway: Proxy error'); } }); // Health check endpoint app.get('/health', (req, res) => { const health = { status: 'ok', timestamp: new Date().toISOString(), p2ns: { connected: !!state.dnsPass, peers: state.connectedPeers.size }, holesail: { active: state.holesails.size, max: config.MAX_HOLESAIL_CLIENTS }, metrics: getMetrics() }; if (!state.dnsPass) { health.status = 'degraded'; health.p2ns.error = 'DNS pass not initialized'; } res.json(health); }); // Metrics endpoint app.get('/metrics', (req, res) => { res.json(getMetrics()); }); // Rate limiting middleware const rateLimitMiddleware = (req, res, next) => { const ip = req.ip || req.connection.remoteAddress || 'unknown'; const domain = req.params?.domain; const limitCheck = checkRateLimit(ip, domain); if (!limitCheck.allowed) { logWarn('Proxy', `Rate limit exceeded`, { ip, domain, reason: limitCheck.reason }); recordError('rate_limit', limitCheck.reason); return res.status(429).json({ error: 'Too Many Requests', reason: limitCheck.reason }); } next(); }; // Request ID middleware app.use((req, res, next) => { req.requestId = generateRequestId(); req.startTime = Date.now(); next(); }); // Handle HTTP requests with error handling try { // Handle requests with subpaths using wildcard syntax: /:domain/*subpath logInfo('Proxy', 'Registering route: /:domain/*subpath'); app.all('/:domain/*subpath', rateLimitMiddleware, async (req, res) => { const requestId = req.requestId; const startTime = req.startTime; try { let domain = req.params.domain; if (domain) { domain = domain.toLowerCase(); } // Validate domain const validation = validateDomain(domain); if (!validation.valid) { logWarn('Proxy', `Invalid domain`, { requestId, domain, error: validation.error }); recordError('invalid_domain', validation.error); recordMetric('notFound'); return res.status(400).json({ error: 'Invalid domain', details: validation.error }); } const subpath = req.params.subpath ? req.params.subpath.join('/') : ''; logDebug('Proxy', `Processing request`, { requestId, domain, subpath: subpath || '(none)' }); // Reconstruct the URL with query parameters const queryString = req.originalUrl.split('?')[1]; req.url = '/' + subpath + (queryString ? '?' + queryString : ''); // Set request timeout const timeout = setTimeout(() => { if (!res.headersSent) { logWarn('Proxy', `Request timeout`, { requestId, domain }); recordError('timeout', 'Request timeout'); recordMetric('timeout'); res.status(504).json({ error: 'Gateway Timeout' }); } }, config.REQUEST_TIMEOUT); res.on('finish', () => { clearTimeout(timeout); const duration = Date.now() - startTime; recordMetric('responseTime', duration); recordMetric('request'); if (res.statusCode >= 200 && res.statusCode < 300) { recordMetric('success'); } else if (res.statusCode === 404) { recordMetric('notFound'); } else { recordMetric('error'); } recordDomainRequest(domain); logInfo('Proxy', `Request completed`, { requestId, domain, status: res.statusCode, duration }); }); const hash = await getHashForDomain(domain); if (!hash) { logWarn('Proxy', `No hash found for domain`, { requestId, domain }); recordError('domain_not_found', domain); recordMetric('notFound'); return res.status(404).json({ error: 'Domain not found or unable to resolve in P2NS' }); } const ip = '127.0.0.1'; const port = await startHolesailClient(domain, hash, ip, true); // Check if domain uses SSL/TLS const useSSL = await getDomainSSLStatus(domain); const protocol = useSSL ? 'https' : 'http'; proxy.web(req, res, { target: `${protocol}://${ip}:${port}` }); } catch (err) { logError('Proxy', `Error processing request`, { requestId, domain: req.params.domain, error: err.message }); recordError('request_error', err.message); if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error' }); } } }); // Handle requests to domain root: /:domain logInfo('Proxy', 'Registering route: /:domain'); app.all('/:domain', rateLimitMiddleware, async (req, res) => { const requestId = req.requestId; const startTime = req.startTime; try { let domain = req.params.domain; if (domain) { domain = domain.toLowerCase(); } // Validate domain const validation = validateDomain(domain); if (!validation.valid) { logWarn('Proxy', `Invalid domain`, { requestId, domain, error: validation.error }); recordError('invalid_domain', validation.error); recordMetric('notFound'); return res.status(400).json({ error: 'Invalid domain', details: validation.error }); } logDebug('Proxy', `Processing request`, { requestId, domain, subpath: '(none)' }); // Keep original URL with query parameters const queryString = req.originalUrl.split('?')[1]; req.url = '/' + (queryString ? '?' + queryString : ''); // Set request timeout const timeout = setTimeout(() => { if (!res.headersSent) { logWarn('Proxy', `Request timeout`, { requestId, domain }); recordError('timeout', 'Request timeout'); recordMetric('timeout'); res.status(504).json({ error: 'Gateway Timeout' }); } }, config.REQUEST_TIMEOUT); res.on('finish', () => { clearTimeout(timeout); const duration = Date.now() - startTime; recordMetric('responseTime', duration); recordMetric('request'); if (res.statusCode >= 200 && res.statusCode < 300) { recordMetric('success'); } else if (res.statusCode === 404) { recordMetric('notFound'); } else { recordMetric('error'); } recordDomainRequest(domain); logInfo('Proxy', `Request completed`, { requestId, domain, status: res.statusCode, duration }); }); const hash = await getHashForDomain(domain); if (!hash) { logWarn('Proxy', `No hash found for domain`, { requestId, domain }); recordError('domain_not_found', domain); recordMetric('notFound'); return res.status(404).json({ error: 'Domain not found or unable to resolve in P2NS' }); } const ip = '127.0.0.1'; const port = await startHolesailClient(domain, hash, ip, true); // Check if domain uses SSL/TLS const useSSL = await getDomainSSLStatus(domain); const protocol = useSSL ? 'https' : 'http'; proxy.web(req, res, { target: `${protocol}://${ip}:${port}` }); } catch (err) { logError('Proxy', `Error processing request`, { requestId, domain: req.params.domain, error: err.message }); recordError('request_error', err.message); if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error' }); } } }); } catch (err) { logError('Proxy', `Failed to register HTTP route`, { error: err.message }); throw err; } // Handle WebSocket upgrades with error handling server.on('upgrade', async (req, socket, head) => { const requestId = generateRequestId(); const startTime = Date.now(); try { const parsedUrl = url.parse(req.url); const match = parsedUrl.pathname.match(/^\/([^\/]+)(.*)$/); if (!match) { logWarn('Proxy', `Invalid WebSocket URL`, { requestId, url: req.url }); recordError('invalid_websocket_url', req.url); socket.destroy(); return; } let domain = match[1].toLowerCase(); // Validate domain const validation = validateDomain(domain); if (!validation.valid) { logWarn('Proxy', `Invalid domain for WebSocket`, { requestId, domain, error: validation.error }); recordError('invalid_domain', validation.error); socket.destroy(); return; } const subpath = match[2] || ''; logDebug('Proxy', `Processing WebSocket upgrade`, { requestId, domain, subpath }); req.url = subpath + (parsedUrl.search || ''); const hash = await getHashForDomain(domain); if (!hash) { logWarn('Proxy', `No hash found for domain`, { requestId, domain }); recordError('domain_not_found', domain); socket.destroy(); return; } const ip = '127.0.0.1'; const port = await startHolesailClient(domain, hash, ip, true); // Check if domain uses SSL/TLS for WebSocket const useSSL = await getDomainSSLStatus(domain); const protocol = useSSL ? 'wss' : 'ws'; proxy.ws(req, socket, head, { target: `${protocol}://${ip}:${port}` }); // Track WebSocket connection metrics.activeConnections++; socket.on('close', () => { metrics.activeConnections--; }); socket.on('error', (err) => { logError('Proxy', `WebSocket error`, { requestId, domain, error: err.message }); recordError('websocket_error', err.message); metrics.activeConnections--; }); } catch (err) { logError('Proxy', `Error processing WebSocket upgrade`, { requestId, error: err.message }); recordError('websocket_upgrade_error', err.message); if (!socket.destroyed) { socket.destroy(); } } }); // Start server try { server.listen(config.PROXY_PORT, config.PROXY_HOST, () => { logInfo('Proxy', `Express proxy server listening`, { port: config.PROXY_PORT, host: config.PROXY_HOST }); }); server.on('error', (err) => { logError('Proxy', `Server error`, { error: err.message }); recordError('server_error', err.message); }); } catch (err) { logError('Proxy', `Failed to start server`, { port: config.PROXY_PORT, error: err.message }); throw err; } // Cleanup (Fixed scope issues) let isShuttingDown = false; const cleanup = async () => { if (isShuttingDown) return; isShuttingDown = true; logInfo('Main', 'Shutting down gracefully...'); const timeout = setTimeout(() => { logError('Main', 'Shutdown timed out, forcing exit'); process.exit(1); }, 10000); // Close Holesail clients for (const [key, holesail] of state.holesails) { try { await holesail.close(); logInfo('Main', `Closed Holesail client`, { key }); } catch (err) { logError('Main', `Error closing Holesail client`, { key, error: err.message }); } } state.holesails.clear(); // Close TLS servers for (const [key, tlsServer] of state.tlsServers) { try { tlsServer.close(); logInfo('Main', `Closed TLS server`, { key }); } catch (err) { logError('Main', `Error closing TLS server`, { key, error: err.message }); } } state.tlsServers.clear(); // Close HTTP servers for (const [key, httpServer] of state.httpServers) { try { httpServer.close(); logInfo('Main', `Closed HTTP server`, { key }); } catch (err) { logError('Main', `Error closing HTTP server`, { key, error: err.message }); } } state.httpServers.clear(); // Close peer channels for (const [peerId, channels] of state.peerChannels) { try { if (channels.inviteChannel && !channels.inviteChannel.destroyed) { channels.inviteChannel.close(); } if (channels.requestChannel && !channels.requestChannel.destroyed) { channels.requestChannel.close(); } } catch (err) { logError('Main', `Error closing channels for peer`, { peerId, error: err.message }); } } state.peerChannels.clear(); // Close swarm (using state.swarm and state.topic) if (state.swarm && state.topic) { try { state.swarm.leave(state.topic); await state.swarm.flush(); await state.swarm.destroy(); logInfo('Main', 'Swarm destroyed'); } catch (err) { logError('Main', `Error destroying swarm`, { error: err.message }); } } // Close DNS pass (includes closing its internal Autobase core) if (state.dnsPass && typeof state.dnsPass.close === 'function') { try { await state.dnsPass.close(); logInfo('Main', 'DNS pass closed - Autobase core should be closed'); } catch (err) { logError('Main', `Error closing dnsPass`, { error: err.message }); } } // Verify dnsPass base core is closed (dnsPass.close() should handle this) if (state.core && typeof state.core.close === 'function' && !state.core.closed) { try { await state.core.close(); logInfo('Main', 'DNS pass base core closed explicitly'); } catch (err) { if (err.message.includes('Corestore is closed') || err.message.includes('already closed')) { logInfo('Main', 'DNS pass base core was already closed'); } else { logError('Main', `Error closing dnsPass base core`, { error: err.message }); } } } // Close corestore if (state.store && typeof state.store.close === 'function' && !state.store.closed) { try { await state.store.close(); logInfo('Main', 'Corestore closed'); } catch (err) { // Ignore errors if corestore was already closed if (err.message.includes('Corestore is closed') || err.message.includes('already closed')) { logInfo('Main', 'Corestore already closed, skipping cleanup'); } else { logError('Main', `Error closing corestore`, { error: err.message }); } } } else { logInfo('Main', 'Corestore not available or already closed'); } clearTimeout(timeout); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('SIGQUIT', cleanup); } main().catch(err => { logError('Main', `Fatal error`, { error: err.message }); process.exit(1); });