Files
p2ns/includes/admin/admin-backend/routes/domains.js
T
2025-12-17 20:49:34 -05:00

273 lines
11 KiB
JavaScript

const fs = require('fs').promises;
const state = require('../../../infrastructure/state');
const { getAllEntries, getHashForDomain, doAutoVotes, getConsensusState, invalidateEntriesCache } = require('../../../core/core');
const { addDomain } = require('../../../core/domains');
const { validateDomainAddition, validateDomainRemoval } = require('../../../infrastructure/validation');
const { atomicDomainCleanup } = require('../../../core/domain_cleanup');
const { createInterfaceForDomain } = require('../../../networking/virtual_interfaces');
const { logDebug, logError, logInfo } = require('../../../infrastructure/logger');
const { trackRequest } = require('../../../maintenance/metrics');
const { createErrorResponse } = require('../../../infrastructure/error_handler');
const { broadcast } = require('../websocket');
const { getPersistentPublicKey } = require('../../../infrastructure/utils');
const domainsFile = process.env.DOMAINS_FILE || './cache/domains.json';
async function handleDomainsRoutes(req, res) {
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
const method = req.method;
const url = new URL(req.url, `https://${req.headers.host}`);
if (method === 'GET' && urlPath === '/api/resolved-domains') {
try {
const allEntries = await getAllEntries();
const domainClaimants = new Map();
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 localWriter = getPersistentPublicKey();
const domains = new Set(domainClaimants.keys());
const resolved = [];
for (const domain of domains) {
const hash = await getHashForDomain(domain) || 'none';
const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false;
let isOwner = false;
let consensusState = null;
try {
consensusState = await getConsensusState(domain);
isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false;
} catch (err) {
logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`);
}
resolved.push({ domain, hash, isLocal, isOwner, consensusState });
}
let internalDomains = ['p2ns.admin'];
try {
const { getInternalDomains } = require('../../../plugins/plugin-handler');
internalDomains = await getInternalDomains();
} catch (err) {
// Fallback if plugin system not available
}
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain))));
} catch (err) {
logError('Admin', `Failed to fetch resolved domains: ${err.message}`);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Failed to fetch domains' }));
}
return true;
}
if (method === 'POST' && urlPath === '/api/add-domain') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
try {
const data = JSON.parse(body);
const validation = validateDomainAddition(data);
if (!validation.valid) {
res.writeHead(400);
res.end(validation.error || 'Invalid input');
return;
}
// Extract SSL flag - handle both boolean true and string "true"
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
await addDomain(validation.domain, validation.hash, ssl);
await doAutoVotes();
let domains = [];
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
if (!Array.isArray(parsed)) {
logError('Admin', `Domains file does not contain an array, resetting to empty array`);
domains = [];
} else {
domains = parsed;
}
}
const existingIndex = domains.findIndex(d => d.domain === validation.domain);
if (existingIndex !== -1) {
domains[existingIndex].hash = validation.hash;
domains[existingIndex].ssl = ssl; // Update SSL flag
} else {
domains.push({ domain: validation.domain, hash: validation.hash, ssl: ssl });
}
await fs.writeFile(domainsFile, JSON.stringify(domains, null, 2));
if (!state.domainToIPMap.has(validation.domain)) {
await createInterfaceForDomain(validation.domain);
logDebug('Admin', `Assigned IP to ${validation.domain}: ${state.domainToIPMap.get(validation.domain)}`);
}
broadcast({ type: 'update-database' });
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
} catch (err) {
trackRequest('/api/add-domain', false);
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
}
});
return true;
}
if (method === 'POST' && urlPath === '/api/remove-domain') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
try {
const data = JSON.parse(body);
const validation = validateDomainRemoval(data);
if (!validation.valid) {
res.writeHead(400);
res.end(validation.error || 'Invalid input');
return;
}
const domain = validation.domain;
// Authorization check: verify peer has claim and is resolved claimant
const localWriter = getPersistentPublicKey();
if (!localWriter) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Peer not initialized');
return;
}
// Check if peer has a claim for this domain
const allEntries = await getAllEntries();
const claimKey = `claim:${domain}:${localWriter}`;
const hasClaim = allEntries.some(entry => entry.key === claimKey);
if (!hasClaim) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Only domains you have claims and resolutions for can be deleted');
return;
}
// Check if peer is the resolved claimant
const consensusState = await getConsensusState(domain);
if (consensusState.resolvedClaimant !== localWriter) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Only domains you have claims and resolutions for can be deleted');
return;
}
await atomicDomainCleanup(domain);
if (state.sendRemovalRequest) {
state.sendRemovalRequest(domain);
}
trackRequest('/api/remove-domain', true);
broadcast({ type: 'update-database' });
broadcast({ type: 'update-holesail-clients' });
broadcast({ type: 'update-local-dns' });
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
} catch (err) {
trackRequest('/api/remove-domain', false);
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
}
});
return true;
}
// POST /api/reset-claims-and-entries - Reset all claims and entries, request peers to provide domains
if (method === 'POST' && urlPath === '/api/reset-claims-and-entries') {
try {
trackRequest('/api/reset-claims-and-entries', true);
logInfo('Admin', 'Resetting all claims and entries, requesting peers to provide domains');
const pass = state.dnsPass;
if (!pass) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'DNS pass not initialized' }));
return true;
}
// Get all entries
const allEntries = await getAllEntries(pass, false); // Don't use cache
// Filter for claims and votes
const entriesToRemove = allEntries.filter(entry =>
entry.key.startsWith('claim:') || entry.key.startsWith('vote:')
);
logInfo('Admin', `Removing ${entriesToRemove.length} claim and vote entries`);
// Remove all claim and vote entries
let removedCount = 0;
let errorCount = 0;
for (const entry of entriesToRemove) {
try {
await pass.remove(entry.key);
removedCount++;
} catch (err) {
logError('Admin', `Error removing entry ${entry.key}: ${err.message}`);
errorCount++;
}
}
// Invalidate cache
invalidateEntriesCache();
// Send message to all peers requesting them to provide their domains
let peersNotified = 0;
if (state.peerChannels && state.peerChannels.size > 0) {
for (const channels of state.peerChannels.values()) {
try {
if (channels.requestMessage && channels.requestChannel && !channels.requestChannel.destroyed) {
channels.requestMessage.send('request_domains');
peersNotified++;
}
} catch (err) {
logError('Admin', `Error sending request_domains message to peer: ${err.message}`);
}
}
logInfo('Admin', `Sent request_domains message to ${peersNotified} peer(s)`);
}
// Broadcast updates
broadcast({ type: 'update-database' });
broadcast({ type: 'update-stats' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Claims and entries reset successfully',
removed: removedCount,
errors: errorCount,
peersNotified: peersNotified
}));
return true;
} catch (err) {
logError('Admin', `Failed to reset claims and entries: ${err.message}`);
trackRequest('/api/reset-claims-and-entries', false);
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
return true;
}
}
return false;
}
module.exports = { handleDomainsRoutes };