mirror of
https://github.com/jc21/nginx-proxy-manager.git
synced 2024-08-30 18:22:48 +00:00
ec9eb0dd60
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.
240 lines
9.4 KiB
JavaScript
240 lines
9.4 KiB
JavaScript
const error = require('./error')
|
|
const logger = require('../logger').ddns;
|
|
const internalAccessList = require('../internal/access-list');
|
|
const utils = require('./utils');
|
|
|
|
const ddnsResolver = {
|
|
/**
|
|
* Starts a timer to periodically check for ddns updates
|
|
*/
|
|
initTimer: () => {
|
|
ddnsResolver._initialize();
|
|
ddnsResolver._interval = setInterval(ddnsResolver._checkForDDNSUpdates, ddnsResolver._updateIntervalMs);
|
|
logger.info(`DDNS Update Timer initialized (interval: ${Math.floor(ddnsResolver._updateIntervalMs / 1000)}s)`);
|
|
// Trigger a run so that initial cache is populated and hosts can be updated - delay by 10s to give server time to boot up
|
|
setTimeout(ddnsResolver._checkForDDNSUpdates, 10 * 1000);
|
|
},
|
|
|
|
/**
|
|
* Checks whether the address requires resolution (i.e. starts with ddns:)
|
|
* @param {String} address
|
|
* @returns {boolean}
|
|
*/
|
|
requiresResolution: (address) => {
|
|
if (typeof address !== 'undefined' && address && address.toLowerCase().startsWith('ddns:')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Resolves the given address to its IP
|
|
* @param {String} address
|
|
* @param {boolean} forceUpdate: whether to force resolution instead of using the cached value
|
|
*/
|
|
resolveAddress: (address, forceUpdate=false) => {
|
|
if (!forceUpdate && ddnsResolver._cache.has(address)) {
|
|
// Check if it is still valid
|
|
const value = ddnsResolver._cache.get(address);
|
|
const ip = value[0];
|
|
const lastUpdated = value[1];
|
|
const nowSeconds = Date.now();
|
|
const delta = nowSeconds - lastUpdated;
|
|
if (delta < ddnsResolver._updateIntervalMs) {
|
|
return Promise.resolve(ip);
|
|
}
|
|
}
|
|
ddnsResolver._cache.delete(address);
|
|
// Reach here only if cache value doesn't exist or needs to be updated
|
|
let host = address.toLowerCase();
|
|
if (host.startsWith('ddns:')) {
|
|
host = host.substring(5);
|
|
}
|
|
return ddnsResolver._queryHost(host)
|
|
.then((resolvedIP) => {
|
|
ddnsResolver._cache.set(address, [resolvedIP, Date.now()]);
|
|
return resolvedIP;
|
|
})
|
|
.catch((_error) => {
|
|
// return input address in case of failure
|
|
return address;
|
|
});
|
|
},
|
|
|
|
|
|
/** Private **/
|
|
// Properties
|
|
_initialized: false,
|
|
_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,
|
|
|
|
// Methods
|
|
|
|
_initialize: () => {
|
|
if (ddnsResolver._initialized) {
|
|
return;
|
|
}
|
|
// Init the resolver
|
|
// Read and set custom update interval from env if needed
|
|
if (typeof process.env.DDNS_UPDATE_INTERVAL !== 'undefined') {
|
|
const interval = Number(process.env.DDNS_UPDATE_INTERVAL.toLowerCase());
|
|
if (!isNaN(interval)) {
|
|
// Interval value from env is in seconds. Set min to 60s.
|
|
ddnsResolver._updateIntervalMs = Math.max(interval * 1000, 60 * 1000);
|
|
} else {
|
|
logger.warn(`[DDNS] invalid value for update interval: '${process.env.DDNS_UPDATE_INTERVAL}'`);
|
|
}
|
|
}
|
|
ddnsResolver._initialized = true;
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {String} host
|
|
* @returns {Promise}
|
|
*/
|
|
_queryHost: (host) => {
|
|
logger.info('Looking up IP for ', host);
|
|
return utils.execSafe('getent', ['hosts', host])
|
|
.then((result) => {
|
|
if (result.length < 8) {
|
|
logger.error('IP lookup returned invalid output: ', result);
|
|
throw error.ValidationError('Invalid output from getent hosts');
|
|
}
|
|
const out = result.split(/\s+/);
|
|
logger.info(`Resolved ${host} to ${out[0]}`);
|
|
return out[0];
|
|
},
|
|
(error) => {
|
|
logger.error('Error looking up IP for ' + host + ': ', error);
|
|
throw error;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
|
|
const updatedAddresses = new Map();
|
|
|
|
// Get all ddns hostnames in use
|
|
return ddnsResolver._getAccessLists()
|
|
.then((rows) => {
|
|
// Build map of used addresses that require resolution
|
|
const usedAddresses = new Map();
|
|
for (const row of rows) {
|
|
if (!row.proxy_host_count) {
|
|
// Ignore rows (access lists) that are not associated to any hosts
|
|
continue;
|
|
}
|
|
for (const client of row.clients) {
|
|
if (!ddnsResolver.requiresResolution(client.address)) {
|
|
continue;
|
|
}
|
|
if (!usedAddresses.has(client.address)) {
|
|
usedAddresses.set(client.address, [row]);
|
|
} else {
|
|
usedAddresses.get(client.address).push(row);
|
|
}
|
|
}
|
|
}
|
|
logger.info(`Found ${usedAddresses.size} address(es) in use.`);
|
|
// Remove unused addresses
|
|
const addressesToRemove = [];
|
|
for (const address of ddnsResolver._cache.keys()) {
|
|
if (!usedAddresses.has(address)) {
|
|
addressesToRemove.push(address);
|
|
}
|
|
}
|
|
addressesToRemove.forEach((address) => { ddnsResolver._cache.delete(address); });
|
|
|
|
const promises = [];
|
|
|
|
for (const [address, rows] of usedAddresses) {
|
|
let oldIP = '';
|
|
if (ddnsResolver._cache.has(address)) {
|
|
oldIP = ddnsResolver._cache.get(address)[0];
|
|
}
|
|
const p = ddnsResolver.resolveAddress(address, true)
|
|
.then((resolvedIP) => {
|
|
if (resolvedIP !== address && resolvedIP !== oldIP) {
|
|
// Mark this as an updated address
|
|
updatedAddresses.set(address, rows);
|
|
}
|
|
});
|
|
promises.push(p);
|
|
}
|
|
|
|
if (promises.length) {
|
|
return Promise.all(promises);
|
|
}
|
|
return Promise.resolve();
|
|
})
|
|
.then(() => {
|
|
logger.info(`${updatedAddresses.size} DDNS IP(s) updated.`);
|
|
const updatedRows = new Map();
|
|
const proxy_hosts = [];
|
|
for (const rows of updatedAddresses.values()) {
|
|
for (const row of rows) {
|
|
if (!updatedRows.has(row.id)) {
|
|
updatedRows.set(row.id, 1);
|
|
proxy_hosts.push(...row.proxy_hosts);
|
|
}
|
|
}
|
|
}
|
|
if (proxy_hosts.length) {
|
|
logger.info(`Updating ${proxy_hosts.length} proxy host(s) affected by DDNS changes`);
|
|
return internalNginx.bulkGenerateConfigs('proxy_host', proxy_hosts)
|
|
.then(internalNginx.reload);
|
|
}
|
|
return Promise.resolve();
|
|
})
|
|
.then(() => {
|
|
logger.info('Finished checking for DDNS updates');
|
|
ddnsResolver._processingDDNSUpdate = false;
|
|
});
|
|
} else {
|
|
logger.info('Skipping since previous DDNS update check is in progress');
|
|
}
|
|
},
|
|
|
|
_getAccessLists: () => {
|
|
const fakeAccess = {
|
|
can: (capabilityStr) => {
|
|
return Promise.resolve({
|
|
permission_visibility: 'all'
|
|
})
|
|
}
|
|
};
|
|
|
|
return internalAccessList.getAll(fakeAccess)
|
|
.then((rows) => {
|
|
const promises = [];
|
|
for (const row of rows) {
|
|
const p = internalAccessList.get(fakeAccess, {
|
|
id: row.id,
|
|
expand: ['owner', 'items', 'clients', 'proxy_hosts.[certificate,access_list.[clients,items]]']
|
|
}, true /* <- skip masking */);
|
|
promises.push(p);
|
|
}
|
|
if (promises.length) {
|
|
return Promise.all(promises);
|
|
}
|
|
return Promise.resolve([]);
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = ddnsResolver;
|