From ec9eb0dd6078ad3e892bac5437f30c723f5b0982 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Sat, 2 Dec 2023 19:13:47 -0500 Subject: [PATCH] Refactor and integrate ddns resolution with nginx module Refactored ddns resolver so that no patching is done. nginx.js will automatically resolve ddns addresses if needed. Added dedicated logger scope for ddns resovler. --- backend/index.js | 2 +- backend/internal/nginx.js | 40 ++++++++- .../lib/{ddns_resolver => }/ddns_resolver.js | 85 ++----------------- backend/lib/utils.js | 32 +++++++ backend/logger.js | 3 +- 5 files changed, 80 insertions(+), 82 deletions(-) rename backend/lib/{ddns_resolver => }/ddns_resolver.js (74%) diff --git a/backend/index.js b/backend/index.js index 82f9be5f..129cf060 100644 --- a/backend/index.js +++ b/backend/index.js @@ -9,7 +9,7 @@ async function appStart () { const apiValidator = require('./lib/validator/api'); const internalCertificate = require('./internal/certificate'); const internalIpRanges = require('./internal/ip_ranges'); - const ddnsResolver = require('./lib/ddns_resolver/ddns_resolver'); + const ddnsResolver = require('./lib/ddns_resolver'); return migrate.latest() .then(setup) diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 77933e73..2f5dc376 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -4,6 +4,7 @@ const logger = require('../logger').nginx; const config = require('../lib/config'); const utils = require('../lib/utils'); const error = require('../lib/error'); +const ddnsResolver = require('../lib/ddns_resolver'); const internalNginx = { @@ -131,6 +132,31 @@ const internalNginx = { return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf'; }, + /** + * Resolves any ddns addresses that need to be resolved for clients in the host's access list. + * @param {Object} host + * @returns {Promise} + */ + resolveDDNSAddresses: (host) => { + const promises = []; + if (typeof host.access_list !== 'undefined' && typeof host.access_list.clients !== 'undefined') { + for (const client of host.access_list.clients) { + if (ddnsResolver.requiresResolution(client.address)) { + const p = ddnsResolver.resolveAddress(client.address) + .then((resolvedIP) => { + client.address = `${resolvedIP}; # ${client.address}`; + return Promise.resolve(); + }); + promises.push(p); + } + } + } + if (promises.length) { + return Promise.all(promises); + } + return Promise.resolve(); + }, + /** * Generates custom locations * @param {Object} host @@ -201,6 +227,12 @@ const internalNginx = { return; } + // Resolve ddns addresses if needed + let resolverPromise = Promise.resolve(); + if (host_type === 'proxy_host') { + resolverPromise = internalNginx.resolveDDNSAddresses(host); + } + let locationsPromise; let origLocations; @@ -215,8 +247,10 @@ const internalNginx = { if (host.locations) { //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); origLocations = [].concat(host.locations); - locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { - host.locations = renderedLocations; + locationsPromise = resolverPromise.then(() => { + return internalNginx.renderLocations(host).then((renderedLocations) => { + host.locations = renderedLocations; + }); }); // Allow someone who is using / custom location path to use it, and skip the default / location @@ -227,7 +261,7 @@ const internalNginx = { }); } else { - locationsPromise = Promise.resolve(); + locationsPromise = resolverPromise; } // Set the IPv6 setting for the host diff --git a/backend/lib/ddns_resolver/ddns_resolver.js b/backend/lib/ddns_resolver.js similarity index 74% rename from backend/lib/ddns_resolver/ddns_resolver.js rename to backend/lib/ddns_resolver.js index fc759133..b339ca47 100644 --- a/backend/lib/ddns_resolver/ddns_resolver.js +++ b/backend/lib/ddns_resolver.js @@ -1,41 +1,7 @@ -const error = require('../error') -const logger = require('../../logger').global; -const internalAccessList = require('../../internal/access-list'); -const internalNginx = require('../../internal/nginx'); -const spawn = require('child_process').spawn; - -const cmdHelper = { - /** - * Run the given command. Safer than using exec since args are passed as a list instead of in shell mode as a single string. - * @param {string} cmd The command to run - * @param {string} args The args to pass to the command - * @returns Promise that resolves to stdout or an object with error code and stderr if there's an error - */ - run: (cmd, args) => { - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const proc = spawn(cmd, args); - proc.stdout.on('data', (data) => { - stdout += data; - }); - proc.stderr.on('data', (data) => { - stderr += data; - }); - - proc.on('close', (exitCode) => { - if (!exitCode) { - resolve(stdout.trim()); - } else { - reject({ - exitCode: exitCode, - stderr: stderr - }); - } - }); - }); - } -}; +const error = require('./error') +const logger = require('../logger').ddns; +const internalAccessList = require('../internal/access-list'); +const utils = require('./utils'); const ddnsResolver = { /** @@ -99,15 +65,13 @@ const ddnsResolver = { /** Private **/ // Properties _initialized: false, - _updateIntervalMs: 1000 * 60 * 60, // 1 hr default (overriden with $DDNS_UPDATE_INTERVAL env var) + _updateIntervalMs: 60 * 60 * 1000, // 1 hr default (overriden with $DDNS_UPDATE_INTERVAL env var) /** * cache mapping host to (ip address, last updated time) */ _cache: new Map(), _interval: null, // reference to created interval id _processingDDNSUpdate: false, - - _originalGenerateConfig: null, // Used for patching config generation to resolve hosts // Methods @@ -126,16 +90,6 @@ const ddnsResolver = { logger.warn(`[DDNS] invalid value for update interval: '${process.env.DDNS_UPDATE_INTERVAL}'`); } } - - // Patch nginx config generation if needed (check env var) - if (typeof process.env.DDNS_UPDATE_PATCH !== 'undefined') { - const enabled = Number(process.env.DDNS_UPDATE_PATCH.toLowerCase()); - if (!isNaN(enabled) && enabled) { - logger.info('Patching nginx config generation'); - ddnsResolver._originalGenerateConfig = internalNginx.generateConfig; - internalNginx.generateConfig = ddnsResolver._patchedGenerateConfig; - } - } ddnsResolver._initialized = true; }, @@ -146,7 +100,7 @@ const ddnsResolver = { */ _queryHost: (host) => { logger.info('Looking up IP for ', host); - return cmdHelper.run('getent', ['hosts', host]) + return utils.execSafe('getent', ['hosts', host]) .then((result) => { if (result.length < 8) { logger.error('IP lookup returned invalid output: ', result); @@ -162,35 +116,12 @@ const ddnsResolver = { }); }, - _patchedGenerateConfig: (host_type, host) => { - const promises = []; - if (host_type === 'proxy_host') { - if (typeof host.access_list !== 'undefined' && typeof host.access_list.clients !== 'undefined') { - for (const client of host.access_list.clients) { - if (ddnsResolver.requiresResolution(client.address)) { - const p = ddnsResolver.resolveAddress(client.address) - .then((resolvedIP) => { - client.address = `${resolvedIP}; # ${client.address}`; - return Promise.resolve(); - }); - promises.push(p); - } - } - } - } - if (promises.length) { - return Promise.all(promises) - .then(() => { - return ddnsResolver._originalGenerateConfig(host_type, host); - }); - } - return ddnsResolver._originalGenerateConfig(host_type, host); - }, - /** * Triggered by a timer, will check for and update ddns hosts in access list clients */ _checkForDDNSUpdates: () => { + const internalNginx = require('../internal/nginx'); // Prevent circular import + logger.info('Checking for DDNS updates...'); if (!ddnsResolver._processingDDNSUpdate) { ddnsResolver._processingDDNSUpdate = true; diff --git a/backend/lib/utils.js b/backend/lib/utils.js index bcdb3341..188a8cbe 100644 --- a/backend/lib/utils.js +++ b/backend/lib/utils.js @@ -4,6 +4,7 @@ const execFile = require('child_process').execFile; const { Liquid } = require('liquidjs'); const logger = require('../logger').global; const error = require('./error'); +const spawn = require('child_process').spawn; module.exports = { @@ -26,6 +27,37 @@ module.exports = { return stdout; }, + /** + * Run the given command. Safer than using exec since args are passed as a list instead of in shell mode as a single string. + * @param {string} cmd The command to run + * @param {string} args The args to pass to the command + * @returns Promise that resolves to stdout or an object with error code and stderr if there's an error + */ + execSafe: (cmd, args) => { + return new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const proc = spawn(cmd, args); + proc.stdout.on('data', (data) => { + stdout += data; + }); + proc.stderr.on('data', (data) => { + stderr += data; + }); + + proc.on('close', (exitCode) => { + if (!exitCode) { + resolve(stdout.trim()); + } else { + reject({ + exitCode: exitCode, + stderr: stderr + }); + } + }); + }); + }, + /** * @param {String} cmd * @param {Array} args diff --git a/backend/logger.js b/backend/logger.js index 0ebb07c5..78553f51 100644 --- a/backend/logger.js +++ b/backend/logger.js @@ -10,5 +10,6 @@ module.exports = { certbot: new Signale({scope: 'Certbot '}), import: new Signale({scope: 'Importer '}), setup: new Signale({scope: 'Setup '}), - ip_ranges: new Signale({scope: 'IP Ranges'}) + ip_ranges: new Signale({scope: 'IP Ranges'}), + ddns: new Signale({scope: 'DDNS '}) };