ix: Prevent claim overwriting and improve domain removal safety
Fix critical bugs in domain claim management to ensure data integrity: - Domain addition: Check for existing claims before adding to prevent duplicates - Update user's own claim if it exists instead of creating duplicates - Skip update if hash and SSL flag unchanged (idempotent) - Never touch other users' claims when updating own claim - Domain removal: Add strict validation to prevent removing other users' claims - Only perform full cleanup (atomicDomainCleanup) when user is confirmed resolved claimant - Require consensus status 'resolved' AND resolvedClaimant matches localWriter - Improved logging to show which removal path is taken - Enhanced removeOwnClaimAndVotes with explicit vote key parsing - Hash preferences cleanup: Fix module path resolution error - Correct require path from admin-backend/cache.js (../../core/core) - Add lazy require pattern with error handling to prevent crashes - Ensure cleanup runs after consensus updates and domain removals
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const state = require('../../infrastructure/state');
|
const state = require('../../infrastructure/state');
|
||||||
const { logDebug, logError, logInfo } = require('../../infrastructure/logger');
|
const { logDebug, logError, logInfo, logWarn } = require('../../infrastructure/logger');
|
||||||
|
|
||||||
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
|
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
|
||||||
const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json';
|
const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json';
|
||||||
@@ -196,6 +196,86 @@ async function savePeerHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function cleanupHashPreferences() {
|
||||||
|
try {
|
||||||
|
// Lazy require to avoid circular dependencies
|
||||||
|
let getConsensusState, getLocalClaimHash, getAllEntries, getPersistentPublicKey;
|
||||||
|
try {
|
||||||
|
const coreModule = require('../../core/core');
|
||||||
|
getConsensusState = coreModule.getConsensusState;
|
||||||
|
getLocalClaimHash = coreModule.getLocalClaimHash;
|
||||||
|
getAllEntries = coreModule.getAllEntries;
|
||||||
|
const utilsModule = require('../../infrastructure/utils');
|
||||||
|
getPersistentPublicKey = utilsModule.getPersistentPublicKey;
|
||||||
|
} catch (requireErr) {
|
||||||
|
logError('Admin', `Failed to require core modules for hash preferences cleanup: ${requireErr.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localWriter = getPersistentPublicKey();
|
||||||
|
if (!localWriter) {
|
||||||
|
logDebug('Admin', 'No local writer available for hash preferences cleanup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEntries = await getAllEntries(state.dnsPass, false); // Use fresh data, bypass cache
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each hash preference to see if it's still valid
|
||||||
|
const domainsToRemove = [];
|
||||||
|
for (const [domain, preference] of state.hashPreferences) {
|
||||||
|
// Check if user still has a local claim for this domain
|
||||||
|
if (!domainClaimants.has(domain) || !domainClaimants.get(domain).has(localWriter)) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: user no longer has local claim`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if consensus is still resolved to a different claimant
|
||||||
|
const consensusState = await getConsensusState(domain);
|
||||||
|
if (consensusState.status !== 'resolved' || consensusState.resolvedClaimant === localWriter) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: conflict no longer exists (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant})`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify local hash still exists
|
||||||
|
const localHash = await getLocalClaimHash(domain, localWriter);
|
||||||
|
if (!localHash) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: local claim hash not found`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove invalid preferences
|
||||||
|
if (domainsToRemove.length > 0) {
|
||||||
|
for (const domain of domainsToRemove) {
|
||||||
|
state.hashPreferences.delete(domain);
|
||||||
|
}
|
||||||
|
await saveSelectorCache();
|
||||||
|
logInfo('Admin', `Cleaned up ${domainsToRemove.length} invalid hash preference(s) from selector_cache.json`);
|
||||||
|
} else {
|
||||||
|
logDebug('Admin', 'All hash preferences are valid');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError('Admin', `Failed to cleanup hash preferences: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
loadSelectorCache();
|
loadSelectorCache();
|
||||||
loadLocalDnsRecords();
|
loadLocalDnsRecords();
|
||||||
@@ -209,6 +289,7 @@ module.exports = {
|
|||||||
loadPeerMetrics,
|
loadPeerMetrics,
|
||||||
savePeerMetrics,
|
savePeerMetrics,
|
||||||
loadPeerHistory,
|
loadPeerHistory,
|
||||||
savePeerHistory
|
savePeerHistory,
|
||||||
|
cleanupHashPreferences
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
|
|
||||||
if (method === 'GET' && urlPath === '/api/resolved-domains') {
|
if (method === 'GET' && urlPath === '/api/resolved-domains') {
|
||||||
try {
|
try {
|
||||||
const allEntries = await getAllEntries();
|
// Use fresh data (bypass cache) to ensure latest consensus state
|
||||||
|
const allEntries = await getAllEntries(state.dnsPass, false);
|
||||||
const domainClaimants = new Map();
|
const domainClaimants = new Map();
|
||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
if (entry.key.startsWith('claim:')) {
|
if (entry.key.startsWith('claim:')) {
|
||||||
@@ -101,7 +102,23 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
// Extract SSL flag - handle both boolean true and string "true"
|
// Extract SSL flag - handle both boolean true and string "true"
|
||||||
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
|
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
|
||||||
await addDomain(validation.domain, validation.hash, ssl);
|
await addDomain(validation.domain, validation.hash, ssl);
|
||||||
|
|
||||||
|
// Invalidate cache to ensure fresh consensus check
|
||||||
|
invalidateEntriesCache();
|
||||||
|
|
||||||
|
// Recalculate consensus for the new domain
|
||||||
await doAutoVotes();
|
await doAutoVotes();
|
||||||
|
|
||||||
|
// Small delay to allow consensus to settle
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
// Trigger consensus recalculation request to notify peers
|
||||||
|
if (state.sendConsensusRequest) {
|
||||||
|
setTimeout(() => {
|
||||||
|
state.sendConsensusRequest(validation.domain);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
let domains = [];
|
let domains = [];
|
||||||
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
||||||
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
||||||
@@ -123,7 +140,11 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
// Note: IP assignment now happens automatically after consensus resolution
|
// Note: IP assignment now happens automatically after consensus resolution
|
||||||
// See assignAllIPs() function for automatic IP assignment logic
|
// See assignAllIPs() function for automatic IP assignment logic
|
||||||
logDebug('Admin', `Domain ${validation.domain} added - IP will be assigned automatically after consensus resolution`);
|
logDebug('Admin', `Domain ${validation.domain} added - IP will be assigned automatically after consensus resolution`);
|
||||||
|
|
||||||
|
// Broadcast multiple update types to ensure frontend refreshes
|
||||||
broadcast({ type: 'update-database' });
|
broadcast({ type: 'update-database' });
|
||||||
|
broadcast({ type: 'update-local-dns' }); // Also update local DNS tab to show conflicts
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
res.end('OK');
|
res.end('OK');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -171,18 +192,40 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
|
|
||||||
// Check if peer is the resolved claimant
|
// Check if peer is the resolved claimant
|
||||||
const consensusState = await getConsensusState(domain);
|
const consensusState = await getConsensusState(domain);
|
||||||
const isResolvedClaimant = consensusState.resolvedClaimant === localWriter;
|
|
||||||
|
// Only do full cleanup if:
|
||||||
|
// 1. Consensus is resolved
|
||||||
|
// 2. User is the resolved claimant
|
||||||
|
// 3. Resolved claimant is not null/undefined
|
||||||
|
const isResolvedClaimant = consensusState.status === 'resolved' &&
|
||||||
|
consensusState.resolvedClaimant &&
|
||||||
|
consensusState.resolvedClaimant === localWriter;
|
||||||
|
|
||||||
if (isResolvedClaimant) {
|
if (isResolvedClaimant) {
|
||||||
// Full removal: user is the resolved claimant
|
// Full removal: user is the resolved claimant
|
||||||
|
logInfo('Admin', `User is resolved claimant for ${domain}, performing full cleanup`);
|
||||||
await atomicDomainCleanup(domain);
|
await atomicDomainCleanup(domain);
|
||||||
} else {
|
} else {
|
||||||
// Partial removal: user has claim but isn't the resolved claimant
|
// Partial removal: user has claim but isn't the resolved claimant
|
||||||
|
// This includes cases where:
|
||||||
|
// - Consensus is not resolved
|
||||||
|
// - User is not the resolved claimant (conflict scenario)
|
||||||
|
// - Resolved claimant is null/undefined
|
||||||
|
logInfo('Admin', `User is NOT resolved claimant for ${domain} (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant}), removing only own claim and votes`);
|
||||||
await removeOwnClaimAndVotes(domain, localWriter);
|
await removeOwnClaimAndVotes(domain, localWriter);
|
||||||
}
|
}
|
||||||
if (state.sendRemovalRequest) {
|
if (state.sendRemovalRequest) {
|
||||||
state.sendRemovalRequest(domain);
|
state.sendRemovalRequest(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup hash preferences if domain was removed
|
||||||
|
try {
|
||||||
|
const { cleanupHashPreferences } = require('../cache');
|
||||||
|
await cleanupHashPreferences();
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('Admin', `Failed to cleanup hash preferences after domain removal: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
trackRequest('/api/remove-domain', true);
|
trackRequest('/api/remove-domain', true);
|
||||||
broadcast({ type: 'update-database' });
|
broadcast({ type: 'update-database' });
|
||||||
broadcast({ type: 'update-holesail-clients' });
|
broadcast({ type: 'update-holesail-clients' });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ window.updateMap = {
|
|||||||
'update-peers': ['peers'],
|
'update-peers': ['peers'],
|
||||||
'update-certs': ['certs'],
|
'update-certs': ['certs'],
|
||||||
'update-interfaces': ['interfaces'],
|
'update-interfaces': ['interfaces'],
|
||||||
'update-local-dns': ['local-dns', 'dns-conflicts'],
|
'update-local-dns': ['local-dns', 'dns-conflicts', 'p2p-domain-conflicts'],
|
||||||
'update-holesail': [],
|
'update-holesail': [],
|
||||||
'update-holesail-clients': [],
|
'update-holesail-clients': [],
|
||||||
'update-settings': ['settings'],
|
'update-settings': ['settings'],
|
||||||
|
|||||||
+83
-2
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const state = require('../infrastructure/state');
|
const state = require('../infrastructure/state');
|
||||||
const { logDebug, logError, logInfo } = require('../infrastructure/logger');
|
const { logDebug, logError, logInfo, logWarn } = require('../infrastructure/logger');
|
||||||
|
|
||||||
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
|
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
|
||||||
const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json';
|
const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json';
|
||||||
@@ -74,6 +74,86 @@ async function loadLocalDnsRecords() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function cleanupHashPreferences() {
|
||||||
|
try {
|
||||||
|
// Lazy require to avoid circular dependencies
|
||||||
|
let getConsensusState, getLocalClaimHash, getAllEntries, getPersistentPublicKey;
|
||||||
|
try {
|
||||||
|
const coreModule = require('../core/core');
|
||||||
|
getConsensusState = coreModule.getConsensusState;
|
||||||
|
getLocalClaimHash = coreModule.getLocalClaimHash;
|
||||||
|
getAllEntries = coreModule.getAllEntries;
|
||||||
|
const utilsModule = require('../infrastructure/utils');
|
||||||
|
getPersistentPublicKey = utilsModule.getPersistentPublicKey;
|
||||||
|
} catch (requireErr) {
|
||||||
|
logError('Admin', `Failed to require core modules for hash preferences cleanup: ${requireErr.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localWriter = getPersistentPublicKey();
|
||||||
|
if (!localWriter) {
|
||||||
|
logDebug('Admin', 'No local writer available for hash preferences cleanup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEntries = await getAllEntries(state.dnsPass, false); // Use fresh data, bypass cache
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each hash preference to see if it's still valid
|
||||||
|
const domainsToRemove = [];
|
||||||
|
for (const [domain, preference] of state.hashPreferences) {
|
||||||
|
// Check if user still has a local claim for this domain
|
||||||
|
if (!domainClaimants.has(domain) || !domainClaimants.get(domain).has(localWriter)) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: user no longer has local claim`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if consensus is still resolved to a different claimant
|
||||||
|
const consensusState = await getConsensusState(domain);
|
||||||
|
if (consensusState.status !== 'resolved' || consensusState.resolvedClaimant === localWriter) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: conflict no longer exists (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant})`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify local hash still exists
|
||||||
|
const localHash = await getLocalClaimHash(domain, localWriter);
|
||||||
|
if (!localHash) {
|
||||||
|
logInfo('Admin', `Removing hash preference for ${domain}: local claim hash not found`);
|
||||||
|
domainsToRemove.push(domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove invalid preferences
|
||||||
|
if (domainsToRemove.length > 0) {
|
||||||
|
for (const domain of domainsToRemove) {
|
||||||
|
state.hashPreferences.delete(domain);
|
||||||
|
}
|
||||||
|
await saveSelectorCache();
|
||||||
|
logInfo('Admin', `Cleaned up ${domainsToRemove.length} invalid hash preference(s) from selector_cache.json`);
|
||||||
|
} else {
|
||||||
|
logDebug('Admin', 'All hash preferences are valid');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError('Admin', `Failed to cleanup hash preferences: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
loadSelectorCache();
|
loadSelectorCache();
|
||||||
loadLocalDnsRecords();
|
loadLocalDnsRecords();
|
||||||
@@ -81,6 +161,7 @@ loadLocalDnsRecords();
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
loadSelectorCache,
|
loadSelectorCache,
|
||||||
saveSelectorCache,
|
saveSelectorCache,
|
||||||
loadLocalDnsRecords
|
loadLocalDnsRecords,
|
||||||
|
cleanupHashPreferences
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const state = require('../../infrastructure/state');
|
const state = require('../../infrastructure/state');
|
||||||
const { getAllEntries, getHashForDomain, doAutoVotes, getConsensusState, removeOwnClaimAndVotes } = require('../../core/core');
|
const { getAllEntries, getHashForDomain, doAutoVotes, getConsensusState, removeOwnClaimAndVotes, invalidateEntriesCache } = require('../../core/core');
|
||||||
const { addDomain } = require('../../core/domains');
|
const { addDomain } = require('../../core/domains');
|
||||||
const { validateDomainAddition, validateDomainRemoval } = require('../../infrastructure/validation');
|
const { validateDomainAddition, validateDomainRemoval } = require('../../infrastructure/validation');
|
||||||
const { atomicDomainCleanup } = require('../../core/domain_cleanup');
|
const { atomicDomainCleanup } = require('../../core/domain_cleanup');
|
||||||
@@ -20,7 +20,8 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
|
|
||||||
if (method === 'GET' && urlPath === '/api/resolved-domains') {
|
if (method === 'GET' && urlPath === '/api/resolved-domains') {
|
||||||
try {
|
try {
|
||||||
const allEntries = await getAllEntries();
|
// Use fresh data (bypass cache) to ensure latest consensus state
|
||||||
|
const allEntries = await getAllEntries(state.dnsPass, false);
|
||||||
const domainClaimants = new Map();
|
const domainClaimants = new Map();
|
||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
if (entry.key.startsWith('claim:')) {
|
if (entry.key.startsWith('claim:')) {
|
||||||
@@ -101,7 +102,23 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
// Extract SSL flag - handle both boolean true and string "true"
|
// Extract SSL flag - handle both boolean true and string "true"
|
||||||
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
|
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
|
||||||
await addDomain(validation.domain, validation.hash, ssl);
|
await addDomain(validation.domain, validation.hash, ssl);
|
||||||
|
|
||||||
|
// Invalidate cache to ensure fresh consensus check
|
||||||
|
invalidateEntriesCache();
|
||||||
|
|
||||||
|
// Recalculate consensus for the new domain
|
||||||
await doAutoVotes();
|
await doAutoVotes();
|
||||||
|
|
||||||
|
// Small delay to allow consensus to settle
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
// Trigger consensus recalculation request to notify peers
|
||||||
|
if (state.sendConsensusRequest) {
|
||||||
|
setTimeout(() => {
|
||||||
|
state.sendConsensusRequest(validation.domain);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
let domains = [];
|
let domains = [];
|
||||||
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
||||||
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
||||||
@@ -123,7 +140,11 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
// Note: IP assignment now happens automatically after consensus resolution
|
// Note: IP assignment now happens automatically after consensus resolution
|
||||||
// See assignAllIPs() function for automatic IP assignment logic
|
// See assignAllIPs() function for automatic IP assignment logic
|
||||||
logDebug('Admin', `Domain ${validation.domain} added - IP will be assigned automatically after consensus resolution`);
|
logDebug('Admin', `Domain ${validation.domain} added - IP will be assigned automatically after consensus resolution`);
|
||||||
|
|
||||||
|
// Broadcast multiple update types to ensure frontend refreshes
|
||||||
broadcast({ type: 'update-database' });
|
broadcast({ type: 'update-database' });
|
||||||
|
broadcast({ type: 'update-local-dns' }); // Also update local DNS tab to show conflicts
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
res.end('OK');
|
res.end('OK');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -171,18 +192,40 @@ async function handleDomainsRoutes(req, res) {
|
|||||||
|
|
||||||
// Check if peer is the resolved claimant
|
// Check if peer is the resolved claimant
|
||||||
const consensusState = await getConsensusState(domain);
|
const consensusState = await getConsensusState(domain);
|
||||||
const isResolvedClaimant = consensusState.resolvedClaimant === localWriter;
|
|
||||||
|
// Only do full cleanup if:
|
||||||
|
// 1. Consensus is resolved
|
||||||
|
// 2. User is the resolved claimant
|
||||||
|
// 3. Resolved claimant is not null/undefined
|
||||||
|
const isResolvedClaimant = consensusState.status === 'resolved' &&
|
||||||
|
consensusState.resolvedClaimant &&
|
||||||
|
consensusState.resolvedClaimant === localWriter;
|
||||||
|
|
||||||
if (isResolvedClaimant) {
|
if (isResolvedClaimant) {
|
||||||
// Full removal: user is the resolved claimant
|
// Full removal: user is the resolved claimant
|
||||||
|
logInfo('Admin', `User is resolved claimant for ${domain}, performing full cleanup`);
|
||||||
await atomicDomainCleanup(domain);
|
await atomicDomainCleanup(domain);
|
||||||
} else {
|
} else {
|
||||||
// Partial removal: user has claim but isn't the resolved claimant
|
// Partial removal: user has claim but isn't the resolved claimant
|
||||||
|
// This includes cases where:
|
||||||
|
// - Consensus is not resolved
|
||||||
|
// - User is not the resolved claimant (conflict scenario)
|
||||||
|
// - Resolved claimant is null/undefined
|
||||||
|
logInfo('Admin', `User is NOT resolved claimant for ${domain} (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant}), removing only own claim and votes`);
|
||||||
await removeOwnClaimAndVotes(domain, localWriter);
|
await removeOwnClaimAndVotes(domain, localWriter);
|
||||||
}
|
}
|
||||||
if (state.sendRemovalRequest) {
|
if (state.sendRemovalRequest) {
|
||||||
state.sendRemovalRequest(domain);
|
state.sendRemovalRequest(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup hash preferences if domain was removed
|
||||||
|
try {
|
||||||
|
const { cleanupHashPreferences } = require('../cache');
|
||||||
|
await cleanupHashPreferences();
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('Admin', `Failed to cleanup hash preferences after domain removal: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
trackRequest('/api/remove-domain', true);
|
trackRequest('/api/remove-domain', true);
|
||||||
broadcast({ type: 'update-database' });
|
broadcast({ type: 'update-database' });
|
||||||
broadcast({ type: 'update-holesail-clients' });
|
broadcast({ type: 'update-holesail-clients' });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ window.updateMap = {
|
|||||||
'update-peers': ['peers'],
|
'update-peers': ['peers'],
|
||||||
'update-certs': ['certs'],
|
'update-certs': ['certs'],
|
||||||
'update-interfaces': ['interfaces'],
|
'update-interfaces': ['interfaces'],
|
||||||
'update-local-dns': ['local-dns', 'dns-conflicts'],
|
'update-local-dns': ['local-dns', 'dns-conflicts', 'p2p-domain-conflicts'],
|
||||||
'update-holesail': [],
|
'update-holesail': [],
|
||||||
'update-holesail-clients': [],
|
'update-holesail-clients': [],
|
||||||
'update-settings': ['settings'],
|
'update-settings': ['settings'],
|
||||||
|
|||||||
+28
-9
@@ -566,6 +566,14 @@ async function doAutoVotes() {
|
|||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
await autoVoteForDomain(domain, allEntries);
|
await autoVoteForDomain(domain, allEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup hash preferences after consensus is updated
|
||||||
|
try {
|
||||||
|
const { cleanupHashPreferences } = require('../admin/cache');
|
||||||
|
await cleanupHashPreferences();
|
||||||
|
} catch (err) {
|
||||||
|
logWarn('Core', `Failed to cleanup hash preferences after auto-votes: ${err.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function voteForDomain(domain, claimant, allEntries = null) {
|
async function voteForDomain(domain, claimant, allEntries = null) {
|
||||||
@@ -688,6 +696,7 @@ async function removeDomain(domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove only the user's own claim and all votes cast by them
|
// Remove only the user's own claim and all votes cast by them
|
||||||
|
// IMPORTANT: This function must NEVER remove other users' claims or votes
|
||||||
async function removeOwnClaimAndVotes(domain, localWriter) {
|
async function removeOwnClaimAndVotes(domain, localWriter) {
|
||||||
const pass = state.dnsPass;
|
const pass = state.dnsPass;
|
||||||
if (!pass) {
|
if (!pass) {
|
||||||
@@ -700,27 +709,37 @@ async function removeOwnClaimAndVotes(domain, localWriter) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const allEntries = await getAllEntries(pass, false); // Don't use cache for removal
|
const allEntries = await getAllEntries(pass, false); // Don't use cache for removal
|
||||||
let removed = 0;
|
let removedClaims = 0;
|
||||||
|
let removedVotes = 0;
|
||||||
|
|
||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
// Remove the user's claim record
|
// Remove ONLY the user's own claim record
|
||||||
|
// Format: claim:domain:claimant
|
||||||
if (entry.key === `claim:${domain}:${localWriter}`) {
|
if (entry.key === `claim:${domain}:${localWriter}`) {
|
||||||
await pass.remove(entry.key);
|
await pass.remove(entry.key);
|
||||||
logDebug('Core', `Removed claim ${entry.key}`);
|
logInfo('Core', `Removed own claim: ${entry.key}`);
|
||||||
removed++;
|
removedClaims++;
|
||||||
}
|
}
|
||||||
// Remove all votes cast BY the user (votes where localWriter is the voter)
|
// Remove ONLY votes cast BY the user (votes where localWriter is the voter)
|
||||||
else if (entry.key.startsWith(`vote:${domain}:`) && entry.key.endsWith(`:${localWriter}`)) {
|
// Format: vote:domain:claimant:voter
|
||||||
|
// We check that it ends with :localWriter to ensure we only remove votes cast BY this user
|
||||||
|
else if (entry.key.startsWith(`vote:${domain}:`)) {
|
||||||
|
const parts = entry.key.split(':');
|
||||||
|
if (parts.length === 4 && parts[3] === localWriter) {
|
||||||
|
// This is a vote cast BY the user (voter is localWriter)
|
||||||
await pass.remove(entry.key);
|
await pass.remove(entry.key);
|
||||||
logDebug('Core', `Removed vote ${entry.key} cast by user`);
|
logInfo('Core', `Removed vote cast by user: ${entry.key}`);
|
||||||
removed++;
|
removedVotes++;
|
||||||
|
}
|
||||||
|
// Explicitly skip votes cast FOR the user's claim (where parts[2] === localWriter but parts[3] !== localWriter)
|
||||||
|
// These are votes cast BY OTHER USERS for the user's claim, and must NOT be removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateEntriesCache(); // Invalidate cache after removal
|
invalidateEntriesCache(); // Invalidate cache after removal
|
||||||
consensusStateCache.delete(domain); // Remove from consensus cache
|
consensusStateCache.delete(domain); // Remove from consensus cache
|
||||||
trackDomainEvent('remove_claim', domain);
|
trackDomainEvent('remove_claim', domain);
|
||||||
logInfo('Core', `Removed own claim and ${removed-1} vote(s) cast by user for domain ${domain}`);
|
logInfo('Core', `Removed ${removedClaims} own claim(s) and ${removedVotes} vote(s) cast by user for domain ${domain}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('Core', `Error removing own claim for domain ${domain}: ${err.message}`);
|
logError('Core', `Error removing own claim for domain ${domain}: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const state = require('../infrastructure/state');
|
const state = require('../infrastructure/state');
|
||||||
const { logDebug, logInfo } = require('../infrastructure/logger');
|
const { logDebug, logInfo } = require('../infrastructure/logger');
|
||||||
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache } = require('./core');
|
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache, parseClaimValue } = require('./core');
|
||||||
const { trackDomainEvent } = require('../maintenance/metrics');
|
const { trackDomainEvent } = require('../maintenance/metrics');
|
||||||
const { getPersistentPublicKey } = require('../infrastructure/utils');
|
const { getPersistentPublicKey } = require('../infrastructure/utils');
|
||||||
// Initialize reserved IPs set if not already present
|
// Initialize reserved IPs set if not already present
|
||||||
@@ -20,17 +20,48 @@ async function addDomain(domain, hash, ssl = false) {
|
|||||||
// Ensure ssl is a boolean
|
// Ensure ssl is a boolean
|
||||||
const sslValue = Boolean(ssl);
|
const sslValue = Boolean(ssl);
|
||||||
|
|
||||||
|
// Check if user already has a claim for this domain
|
||||||
|
const allEntries = await getAllEntries(pass, false); // Use fresh data, bypass cache
|
||||||
|
let existingClaim = null;
|
||||||
|
for (const entry of allEntries) {
|
||||||
|
if (entry.key === claimKey) {
|
||||||
|
existingClaim = entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store claim with timestamp and SSL flag in JSON format for new claims
|
// Store claim with timestamp and SSL flag in JSON format for new claims
|
||||||
// Format: { hash: "...", timestamp: 1234567890, ssl: true/false }
|
// Format: { hash: "...", timestamp: 1234567890, ssl: true/false }
|
||||||
const claimValue = JSON.stringify({ hash, timestamp, ssl: sslValue });
|
const claimValue = JSON.stringify({ hash, timestamp, ssl: sslValue });
|
||||||
|
|
||||||
logDebug('Domains', `Adding domain ${domain} with hash ${hash} and SSL=${sslValue} as claim ${claimKey} at timestamp ${timestamp}`);
|
if (existingClaim) {
|
||||||
|
// User already has a claim - update it instead of creating duplicate
|
||||||
|
const parsed = parseClaimValue(existingClaim.value);
|
||||||
|
|
||||||
|
// Check if anything actually changed
|
||||||
|
if (parsed.hash === hash && parsed.ssl === sslValue) {
|
||||||
|
logDebug('Domains', `Claim for ${domain} already exists with same hash and SSL flag, skipping update`);
|
||||||
|
return; // No changes needed
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo('Domains', `Updating existing claim for ${domain} (hash: ${parsed.hash} -> ${hash}, SSL: ${parsed.ssl} -> ${sslValue})`);
|
||||||
|
|
||||||
|
// Remove old claim and add updated one
|
||||||
|
await pass.remove(claimKey);
|
||||||
|
await pass.add(claimKey, claimValue);
|
||||||
|
|
||||||
|
logInfo('Domains', `Updated claim ${claimKey} with new hash ${hash} and SSL=${sslValue}`);
|
||||||
|
} else {
|
||||||
|
// New claim - add it
|
||||||
|
logDebug('Domains', `Adding new domain ${domain} with hash ${hash} and SSL=${sslValue} as claim ${claimKey} at timestamp ${timestamp}`);
|
||||||
await pass.add(claimKey, claimValue);
|
await pass.add(claimKey, claimValue);
|
||||||
|
|
||||||
const verified = await pass.get(claimKey);
|
const verified = await pass.get(claimKey);
|
||||||
logDebug('Domains', `Verified add for ${claimKey}: ${verified ? verified.toString('utf8') : 'null'}`);
|
logDebug('Domains', `Verified add for ${claimKey}: ${verified ? verified.toString('utf8') : 'null'}`);
|
||||||
logInfo('Domains', `Domain ${domain} added as claim ${claimKey} with timestamp ${timestamp}`);
|
logInfo('Domains', `Domain ${domain} added as claim ${claimKey} with timestamp ${timestamp}`);
|
||||||
invalidateEntriesCache(); // Invalidate cache after adding
|
}
|
||||||
|
|
||||||
|
invalidateEntriesCache(); // Invalidate cache after adding/updating
|
||||||
trackDomainEvent('add', domain);
|
trackDomainEvent('add', domain);
|
||||||
|
|
||||||
// Immediately auto-vote after adding (if immediate updates enabled)
|
// Immediately auto-vote after adding (if immediate updates enabled)
|
||||||
|
|||||||
Reference in New Issue
Block a user