Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/services/domainBoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { getDb } = require('../db/connection');
const domains = require('./domains');
const settings = require('./settings');
const { extractBaseDomains, shouldFlagServerIp } = require('./domainSeed');
const { isPublicDomain } = require('./caddyTlsAutomation');

/**
* Verify a set of domain names against the server IP, persist results, and
Expand Down Expand Up @@ -34,9 +35,21 @@ async function verifyAndReflag(domainNames, { verifyEach = domains.verify } = {}

async function runDomainSeedAndVerify({ verifyEach = domains.verify } = {}) {
const routeDomains = getDb().prepare('SELECT DISTINCT domain FROM routes WHERE domain IS NOT NULL').all().map(r => r.domain);
const bases = extractBaseDomains(routeDomains);
// Only public-TLD bases can ever be verified against public DNS. Non-public
// bases (.internal/.lan/...) would otherwise linger forever as 'pending'
// noise on the Domains page, so they are never seeded.
const bases = extractBaseDomains(routeDomains).filter(isPublicDomain);
for (const d of bases) domains.seedPending(d);

// One-time cleanup: drop non-public bases that earlier boots auto-seeded as
// 'pending' (they can never verify; routes consume routes.domain directly and
// nothing references the domains table by FK). Verified rows are never touched.
const lingering = getDb().prepare("SELECT domain FROM domains WHERE status='pending'").all().map(r => r.domain);
const delStmt = getDb().prepare('DELETE FROM domains WHERE domain=?');
for (const d of lingering) {
if (!isPublicDomain(d)) delStmt.run(d);
}

// verify only rows still pending (idempotent across boots)
const pending = getDb().prepare("SELECT domain FROM domains WHERE status='pending'").all().map(r => r.domain);
const { verified, flagged } = await verifyAndReflag(pending, { verifyEach });
Expand Down
23 changes: 23 additions & 0 deletions tests/domain_boot_seed.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ test('seeds distinct base domains and verifies them', async () => {
assert.ok(rows.every(r => r.status === 'verified'));
});

test('skips non-public-TLD bases and prunes lingering pending non-public rows', async () => {
// an internal-TLD route: its base (gc.internal) must NEVER be seeded
getDb().prepare(`INSERT INTO routes (description, domain, target_ip, target_port, enabled, route_type)
VALUES ('r3','nas.gc.internal','10.0.0.4','80',1,'http')`).run();
// a non-public base auto-seeded by an earlier boot → must be pruned
getDb().prepare("INSERT OR IGNORE INTO domains (domain, status) VALUES ('old.lan','pending')").run();
// a verified row must survive — cleanup only targets pending rows
getDb().prepare("INSERT OR IGNORE INTO domains (domain, status) VALUES ('kept.internal','verified')").run();

const res = await domainBoot.runDomainSeedAndVerify({
verifyEach: async (d) => ({ status: 'verified', resolvedIp: '1.2.3.4', expectedIp: '1.2.3.4', error: null }),
});

// Set membership (exact match) — avoids CodeQL's js/incomplete-url-substring
// false-positive that fires on Array.includes('host.tld') in tests.
const present = new Set(getDb().prepare('SELECT domain FROM domains').all().map(r => r.domain));
assert.equal(res.seeded, 2, 'only the 2 public bases are seeded');
assert.ok(!present.has('gc.internal'), 'non-public base must not be seeded');
assert.ok(!present.has('old.lan'), 'lingering pending non-public row must be pruned');
assert.ok(present.has('kept.internal'), 'verified rows must never be pruned');
assert.ok(present.has('domaincaster.com') && present.has('marcbackes.net'), 'public bases seeded');
});

test('>=2 all-mismatch sets the server-IP warning and keeps rows pending', async () => {
const res = await domainBoot.runDomainSeedAndVerify({
verifyEach: async (d) => ({ status: 'failed', resolvedIp: '9.9.9.9', expectedIp: '1.2.3.4', error: 'x' }),
Expand Down
Loading