Adversarial audit 2026-08-16 (GLM-5.3 delegate, 2 rounds, 141 tool calls):
P0-1: Dashboard WebSocket (/api/v1/ws) dead on EVERY boot since DC-076.
server.js passed module exports (DependencyManager class, {AutoRestartManager}
namespace, SSLMonitor class) instead of createApp()'s live instances — first
.on() threw ERR_INVALID_ARG_TYPE, catch swallowed it. Fix: app.locals.ctx
exposed in src/app.js; server.js passes all 8 real EventEmitter instances.
P0-2: error.log corrupted since 2026-07-14. errorMiddleware called
logError(FILE, SIZE, path, err, meta) — 5 args into a 3-arg wrapper —
logging 'Error: 5242880' garbage every ~60s and DISCARDING the real error
object. Fix: correct 3-arg call + legacy-shape guard in logErrorWrapper +
~74 log.error sites swept to pass real error objects (AST-verified scope-
safe 71/71, 29/29 modules load clean).
P0-3: auth-polling storm (stranded grade=B commit never landed in prod):
401/403 behind TOTP gate hammered /api/v1/services/status + SSE reconnect
every 2-8s, with misleading direct-probe fallback marking services 'up'.
Fix landed + B-round MEDIUM follow-up: TOTP re-auth success now clears
_dcAuthLost, resumes SSE (new _sseResume clears the latch), and refreshes.
Also: eslintignore static-sites/ (33→0 errors); nodemailer 8→9.0.5 and
sharp 0.33→0.35.3 (3 high CVEs killed; jest green on new majors);
dockerode@5/uuid deferred (semver-major, Docker API surface).
Verification: 80/80 suites, 1837/1837 tests; ESLint 0 errors/743 warnings;
node --check all changed files; bundles rebuilt + SW cache bumped.
Judges: Codex quota-dead until Aug 19 (verified live) — GLM adversarial
delegate per operator directive 2026-08-07. Round 1: 98-call mechanical
verification (timed out pre-verdict). Round 2 (this grade): B, one MEDIUM
(re-auth freeze) — fixed in this commit as prescribed.
357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
/**
|
|
* DNS context - Technitium DNS operations and token management
|
|
*
|
|
* DEPRECATED: This module is kept for backward compatibility.
|
|
* New code should use src/context/provider-dns.js which supports multiple providers.
|
|
*
|
|
* This module now delegates to the provider system internally.
|
|
*/
|
|
const { TIMEOUTS, SESSION_TTL, CADDY } = require('../utilities/constants');
|
|
const { createCache, CACHE_CONFIGS } = require('../utilities/cache-config');
|
|
const { createProviderDnsContext } = require('./provider-dns');
|
|
|
|
// DNS token management
|
|
let dnsToken = process.env.DNS_ADMIN_TOKEN || '';
|
|
let dnsTokenExpiry = null;
|
|
|
|
// Per-server token cache
|
|
const dnsServerTokens = createCache(CACHE_CONFIGS.dnsTokens);
|
|
|
|
/**
|
|
* Build full Technitium DNS API URL
|
|
*/
|
|
function buildDnsUrl(server, apiPath, params) {
|
|
const protocol = server.match(/^\d+\.\d+\.\d+\.\d+$/) ? 'http' : 'https';
|
|
const port = protocol === 'http' ? `:${CADDY.DEFAULT_DNS_PORT}` : '';
|
|
const qs = params instanceof URLSearchParams ? params.toString() : new URLSearchParams(params).toString();
|
|
return `${protocol}://${server}${port}${apiPath}?${qs}`;
|
|
}
|
|
|
|
/**
|
|
* Call a Technitium DNS API endpoint
|
|
*/
|
|
async function callDns(server, apiPath, params, fetchT, httpsAgent) {
|
|
const url = buildDnsUrl(server, apiPath, params);
|
|
const response = await fetchT(url, {
|
|
method: 'GET',
|
|
headers: { 'Accept': 'application/json' },
|
|
agent: httpsAgent
|
|
}, TIMEOUTS.HTTP_LONG);
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Refresh DNS token via login
|
|
*/
|
|
async function refreshDnsToken(username, password, server, fetchT, log) {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
user: username,
|
|
pass: password,
|
|
includeInfo: 'false'
|
|
});
|
|
|
|
const response = await fetchT(
|
|
`http://${server}:5380/api/user/login?${params.toString()}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
}
|
|
},
|
|
10000
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'ok' && result.token) {
|
|
dnsToken = result.token;
|
|
dnsTokenExpiry = new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString();
|
|
log.info('dns', 'DNS token refreshed', { expires: dnsTokenExpiry });
|
|
return { success: true, token: dnsToken };
|
|
}
|
|
|
|
return { success: false, error: result.errorMessage || 'Login failed' };
|
|
} catch (error) {
|
|
log.error('dns', error, null, { note: 'DNS token refresh error' });
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to refresh the DNS token using per-server (dns.<id>.<role>) credentials.
|
|
* Returns the refresh result on success, or null if no per-server credentials match.
|
|
*/
|
|
async function refreshWithPerServerCredentials(dnsId, serverIp, credentialManager, fetchT, log) {
|
|
for (const role of ['admin', 'readonly']) {
|
|
try {
|
|
const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`);
|
|
const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`);
|
|
if (username && password) {
|
|
return await refreshDnsToken(username, password, serverIp, fetchT, log);
|
|
}
|
|
} catch (err) {
|
|
log.error('dns', `Per-server ${role} credential error`, { dnsId, error: err.message });
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ensure we have a valid DNS token (auto-refresh if needed)
|
|
*/
|
|
async function tryCredentialPair(dnsId, role, primaryIp, siteConfig, credentialManager, fetchT, log) {
|
|
try {
|
|
const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`);
|
|
const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`);
|
|
if (username && password) {
|
|
return await refreshDnsToken(username, password, primaryIp, fetchT, log);
|
|
}
|
|
return null;
|
|
} catch (err) {
|
|
log.error('dns', `Per-server ${role} credential error`, { dnsId, error: err.message });
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function ensureValidDnsToken(siteConfig, credentialManager, fetchT, log) {
|
|
// Check if token is valid and not expired
|
|
if (dnsToken && dnsTokenExpiry && new Date() < new Date(dnsTokenExpiry)) {
|
|
return { success: true, token: dnsToken };
|
|
}
|
|
|
|
const primaryIp = siteConfig.dnsServerIp;
|
|
if (primaryIp) {
|
|
const dnsId = dnsIpToDnsId(primaryIp, siteConfig);
|
|
if (dnsId) {
|
|
for (const role of ['admin', 'readonly']) {
|
|
const result = await tryCredentialPair(dnsId, role, primaryIp, siteConfig, credentialManager, fetchT, log);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to global credentials
|
|
try {
|
|
const username = await credentialManager.retrieve('dns.username');
|
|
const password = await credentialManager.retrieve('dns.password');
|
|
const server = await credentialManager.retrieve('dns.server');
|
|
if (username && password) {
|
|
return await refreshDnsToken(username, password, server || primaryIp, fetchT, log);
|
|
}
|
|
} catch (err) {
|
|
log.error('dns', err, null, { note: 'Credential manager error' });
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'No DNS credentials configured. Please set up credentials via /api/dns/credentials'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Map DNS server IP to its ID
|
|
*/
|
|
function dnsIpToDnsId(serverIp, siteConfig) {
|
|
for (const [dnsId, info] of Object.entries(siteConfig.dnsServers || {})) {
|
|
if (info.ip === serverIp) return dnsId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a valid token for a specific DNS server
|
|
*/
|
|
async function getTokenForServer(targetServer, siteConfig, credentialManager, fetchT, log, role = 'readonly') {
|
|
const cacheKey = `${targetServer}:${role}`;
|
|
const cached = dnsServerTokens.get(cacheKey);
|
|
|
|
if (cached && cached.token && cached.expiry && new Date() < new Date(cached.expiry)) {
|
|
return { success: true, token: cached.token };
|
|
}
|
|
|
|
const serverPort = siteConfig.dnsServerPort || '5380';
|
|
|
|
async function authenticateToServer(username, password) {
|
|
const params = new URLSearchParams({
|
|
user: username,
|
|
pass: password,
|
|
includeInfo: 'false'
|
|
});
|
|
|
|
const response = await fetchT(
|
|
`http://${targetServer}:${serverPort}/api/user/login?${params.toString()}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
}
|
|
}
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'ok' && result.token) {
|
|
dnsServerTokens.set(cacheKey, {
|
|
token: result.token,
|
|
expiry: new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString()
|
|
});
|
|
log.info('dns', 'DNS token obtained for server', { server: targetServer, role });
|
|
return { success: true, token: result.token };
|
|
}
|
|
|
|
return { success: false, error: result.errorMessage || 'Login failed' };
|
|
}
|
|
|
|
const dnsId = dnsIpToDnsId(targetServer, siteConfig);
|
|
|
|
if (dnsId) {
|
|
try {
|
|
const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`);
|
|
const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`);
|
|
if (username && password) {
|
|
return await authenticateToServer(username, password);
|
|
}
|
|
} catch (err) {
|
|
log.error('dns', `Per-server ${role} credential error`, { dnsId, server: targetServer, error: err.message });
|
|
}
|
|
|
|
const fallbackRole = role === 'readonly' ? 'admin' : 'readonly';
|
|
try {
|
|
const username = await credentialManager.retrieve(`dns.${dnsId}.${fallbackRole}.username`);
|
|
const password = await credentialManager.retrieve(`dns.${dnsId}.${fallbackRole}.password`);
|
|
if (username && password) {
|
|
return await authenticateToServer(username, password);
|
|
}
|
|
} catch (err) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
try {
|
|
const username = await credentialManager.retrieve('dns.username');
|
|
const password = await credentialManager.retrieve('dns.password');
|
|
if (username && password) {
|
|
return await authenticateToServer(username, password);
|
|
}
|
|
} catch (err) {
|
|
log.error('dns', err, null, { note: 'Credential manager error', server: targetServer });
|
|
}
|
|
|
|
return { success: false, error: 'No DNS credentials configured' };
|
|
}
|
|
|
|
/**
|
|
* Require a DNS token (throw if unavailable)
|
|
*/
|
|
async function requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log) {
|
|
if (providedToken) return providedToken;
|
|
const result = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
|
if (result.success) return result.token;
|
|
const err = new Error('No valid DNS token available. ' + result.error);
|
|
err.statusCode = 401;
|
|
throw err;
|
|
}
|
|
|
|
/**
|
|
* Create DNS record
|
|
*/
|
|
async function createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log) {
|
|
const tokenResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
|
if (!tokenResult.success) {
|
|
throw new Error(`DNS token not available: ${tokenResult.error}`);
|
|
}
|
|
|
|
const domain = buildDomain(subdomain);
|
|
const zone = siteConfig.tld.replace(/^\./, '');
|
|
|
|
const dnsParams = {
|
|
token: dnsToken,
|
|
domain,
|
|
zone,
|
|
type: 'A',
|
|
ipAddress: ip,
|
|
ttl: '300',
|
|
overwrite: 'true'
|
|
};
|
|
|
|
const callDnsApi = () => callDns(siteConfig.dnsServerIp, '/api/zones/records/add', dnsParams, fetchT, httpsAgent);
|
|
|
|
try {
|
|
log.info('dns', 'Creating DNS record', { domain, ip });
|
|
const result = await callDnsApi();
|
|
|
|
if (result.status === 'ok') {
|
|
log.info('dns', 'DNS record created', { domain, ip });
|
|
return { success: true };
|
|
}
|
|
|
|
if (result.errorMessage && result.errorMessage.toLowerCase().includes('token')) {
|
|
log.info('dns', 'Token appears expired, attempting auto-refresh');
|
|
const refreshResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
|
if (!refreshResult.success) throw new Error(`Token refresh failed: ${refreshResult.error}`);
|
|
|
|
const retryResult = await callDnsApi();
|
|
if (retryResult.status === 'ok') {
|
|
log.info('dns', 'DNS record created after token refresh', { domain, ip });
|
|
return { success: true };
|
|
}
|
|
throw new Error(retryResult.errorMessage || 'Unknown error after token refresh');
|
|
}
|
|
|
|
throw new Error(result.errorMessage || 'Unknown error');
|
|
} catch (error) {
|
|
throw new Error(`Failed to create DNS record for ${domain}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function invalidateTokenForServer(serverIp) {
|
|
dnsServerTokens.delete(`${serverIp}:readonly`);
|
|
dnsServerTokens.delete(`${serverIp}:admin`);
|
|
}
|
|
|
|
function createDnsContext(siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log, DNS_CREDENTIALS_FILE) {
|
|
// Create the new provider-aware context
|
|
const providerCtx = createProviderDnsContext(siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log, DNS_CREDENTIALS_FILE);
|
|
|
|
// Legacy Technitium-specific wrappers (kept for backward compat)
|
|
const ensureToken = () => ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
|
const require = (providedToken) => requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log);
|
|
const getForServer = (server, role) => getTokenForServer(server, siteConfig, credentialManager, fetchT, log, role);
|
|
const refresh = (username, password, server) => refreshDnsToken(username, password, server, fetchT, log);
|
|
const create = (subdomain, ip) => createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log);
|
|
const call = (server, apiPath, params) => callDns(server, apiPath, params, fetchT, httpsAgent);
|
|
|
|
return {
|
|
// Legacy Technitium-specific interface (unchanged)
|
|
call,
|
|
buildUrl: buildDnsUrl,
|
|
requireToken: require,
|
|
ensureToken,
|
|
createRecord: create,
|
|
getToken: () => dnsToken,
|
|
setToken: (t) => { dnsToken = t; },
|
|
getTokenExpiry: () => dnsTokenExpiry,
|
|
setTokenExpiry: (e) => { dnsTokenExpiry = e; },
|
|
getTokenForServer: getForServer,
|
|
invalidateTokenForServer,
|
|
refresh,
|
|
credentialsFile: DNS_CREDENTIALS_FILE,
|
|
|
|
// Provider-aware methods (new)
|
|
getProviderId: providerCtx.getProviderId,
|
|
getActiveProvider: providerCtx.getActiveProvider,
|
|
getAvailableProviders: providerCtx.getAvailableProviders,
|
|
supportsCapability: providerCtx.supportsCapability,
|
|
|
|
// Universal DNS helpers (delegated to provider context)
|
|
universalCreateRecord: providerCtx.universalCreateRecord,
|
|
universalDeleteRecord: providerCtx.universalDeleteRecord,
|
|
universalResolveRecord: providerCtx.universalResolveRecord,
|
|
};
|
|
}
|
|
|
|
module.exports = { createDnsContext };
|