/** * 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', 'DNS token refresh error', { error: error.message }); return { success: false, error: error.message }; } } /** * Try to refresh the DNS token using per-server (dns..) 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', 'Credential manager error', { error: err.message }); } 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', 'Credential manager error', { server: targetServer, error: err.message }); } 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 };