const fs = require('fs').promises; const dns = require('dns').promises; const state = require('../../../infrastructure/state'); const { logError, logWarn, logInfo } = require('../../../infrastructure/logger'); const { saveSelectorCache } = require('../cache'); const { broadcast } = require('../websocket'); const { getConsensusState, getLocalClaimHash, getAllEntries } = require('../../../core/core'); const { getPersistentPublicKey } = require('../../../infrastructure/utils'); const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json'; async function handleLocalDnsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; if (method === 'GET' && urlPath === '/api/local-dns') { try { let records = state.localDnsRecords || []; let conflicts = []; const domains = new Set([...state.domainsWithBoth, ...state.versionPreferences.keys()]); for (const domain of domains) { let publicIP = state.publicIpForDomain[domain]; if (!publicIP && state.versionPreferences.has(domain)) { try { const ips = await dns.resolve4(domain); publicIP = ips[0] || 'N/A'; state.publicIpForDomain[domain] = publicIP; } catch (err) { logWarn('Admin', `Failed to resolve public IP for ${domain}: ${err.message}`); publicIP = 'N/A'; } } conflicts.push({ domain, version: state.versionPreferences.get(domain) || 'p2p', publicIP: publicIP || 'N/A' }); } records = records.map((rec, index) => ({ ...rec, index })); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ records, conflicts })); } catch (err) { logError('Admin', `Failed to fetch local DNS and conflicts: ${err.message}`); res.writeHead(500); res.end(JSON.stringify({ error: 'Failed to fetch local DNS and conflicts' })); } return true; } if (method === 'GET' && urlPath === '/api/selector-cache') { try { const preferences = Object.fromEntries(state.versionPreferences); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(preferences)); } catch (err) { logError('Admin', `Failed to fetch selector cache: ${err.message}`); res.writeHead(500); res.end(JSON.stringify({ error: 'Failed to fetch selector cache' })); } return true; } if (method === 'POST' && urlPath === '/api/add-local-dns') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const record = JSON.parse(body); if (!record.name || !record.type || !record.ttl || isNaN(record.ttl)) { throw new Error('Missing or invalid required fields: name, type, ttl'); } record.class = record.class || 'IN'; let records = state.localDnsRecords || []; records.push(record); await fs.writeFile(localDnsFile, JSON.stringify(records, null, 2)); state.localDnsRecords = records; broadcast({ type: 'update-local-dns' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { logError('Admin', `Failed to add local DNS record: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } if (method === 'POST' && urlPath === '/api/update-local-dns') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { index, record } = JSON.parse(body); if (!record.name || !record.type || !record.ttl || isNaN(record.ttl)) { throw new Error('Missing or invalid required fields: name, type, ttl'); } let records = state.localDnsRecords || []; if (index >= 0 && index < records.length) { record.class = record.class || 'IN'; records[index] = record; await fs.writeFile(localDnsFile, JSON.stringify(records, null, 2)); state.localDnsRecords = records; broadcast({ type: 'update-local-dns' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } else { res.writeHead(400); res.end('Invalid index'); } } catch (err) { logError('Admin', `Failed to update local DNS record: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } if (method === 'POST' && urlPath === '/api/delete-local-dns') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { index } = JSON.parse(body); let records = state.localDnsRecords || []; if (index >= 0 && index < records.length) { records.splice(index, 1); await fs.writeFile(localDnsFile, JSON.stringify(records, null, 2)); state.localDnsRecords = records; broadcast({ type: 'update-local-dns' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } else { res.writeHead(400); res.end('Invalid index'); } } catch (err) { logError('Admin', `Failed to delete local DNS record: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } if (method === 'POST' && urlPath === '/api/update-version-preference') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { domain, version } = JSON.parse(body); if (version !== 'p2p' && version !== 'public') { res.writeHead(400); res.end('Invalid version, must be "p2p" or "public"'); return; } state.versionPreferences.set(domain, version); await saveSelectorCache(); broadcast({ type: 'update-local-dns' }); logInfo('Admin', `Updated version preference for ${domain} to ${version}`); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { logError('Admin', `Failed to update version preference: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } if (method === 'GET' && urlPath === '/api/p2p-domain-conflicts') { try { const localWriter = getPersistentPublicKey(); if (!localWriter) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ conflicts: [] })); return true; } const allEntries = await getAllEntries(state.dnsPass, false); // Don't use cache for fresh conflict detection const domainClaimants = new Map(); // Collect all claimants for each domain for (const entry of allEntries) { if (entry.key.startsWith('claim:')) { const parts = entry.key.split(':'); if (parts.length === 3) { const domain = parts[1]; const claimant = parts[2]; if (!domainClaimants.has(domain)) domainClaimants.set(domain, new Set()); domainClaimants.get(domain).add(claimant); } } } const conflicts = []; for (const [domain, claimants] of domainClaimants) { // Check if user has a local claim if (claimants.has(localWriter)) { const consensusState = await getConsensusState(domain); // Check if consensus is resolved but user is not the resolved claimant if (consensusState.status === 'resolved' && consensusState.resolvedClaimant !== localWriter) { const localHash = await getLocalClaimHash(domain, localWriter); conflicts.push({ domain, localHash, resolvedHash: consensusState.hash, resolvedClaimant: consensusState.resolvedClaimant, localClaimant: localWriter, consensusStatus: consensusState.status, hashPreference: state.hashPreferences.get(domain) || 'resolved' // Default to resolved }); } } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ conflicts })); } catch (err) { logError('Admin', `Failed to fetch P2P domain conflicts: ${err.message}`); res.writeHead(500); res.end(JSON.stringify({ error: 'Failed to fetch P2P domain conflicts' })); } return true; } if (method === 'POST' && urlPath === '/api/update-hash-preference') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { domain, preference } = JSON.parse(body); if (preference !== 'local' && preference !== 'resolved') { res.writeHead(400); res.end('Invalid preference, must be "local" or "resolved"'); return; } state.hashPreferences.set(domain, preference); await saveSelectorCache(); broadcast({ type: 'update-local-dns' }); logInfo('Admin', `Updated hash preference for ${domain} to ${preference}`); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { logError('Admin', `Failed to update hash preference: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } if (method === 'POST' && urlPath === '/api/clear-dns-cache') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { domain } = JSON.parse(body); const { clearDNSCacheForDomain } = require('../../../networking/dns'); clearDNSCacheForDomain(domain); logInfo('Admin', `Cleared DNS cache for domain ${domain}`); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { logError('Admin', `Failed to clear DNS cache: ${err.message}`); res.writeHead(500); res.end(err.message); } }); return true; } return false; } module.exports = { handleLocalDnsRoutes };