| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 | const _          = require('lodash');const fs         = require('fs');const logger     = require('../logger').nginx;const utils      = require('../lib/utils');const error      = require('../lib/error');const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;const internalNginx = {	/**	 * This will:	 * - test the nginx config first to make sure it's OK	 * - create / recreate the config for the host	 * - test again	 * - IF OK:  update the meta with online status	 * - IF BAD: update the meta with offline status and remove the config entirely	 * - then reload nginx	 *	 * @param   {Object|String}  model	 * @param   {String}         host_type	 * @param   {Object}         host	 * @returns {Promise}	 */	configure: (model, host_type, host) => {		let combined_meta = {};		return internalNginx.test()			.then(() => {				// Nginx is OK				// We're deleting this config regardless.				// Don't throw errors, as the file may not exist at all				// Delete the .err file too				return internalNginx.deleteConfig(host_type, host, false, true);			})			.then(() => {				return internalNginx.generateConfig(host_type, host);			})			.then(() => {				// Test nginx again and update meta with result				return internalNginx.test()					.then(() => {						// nginx is ok						combined_meta = _.assign({}, host.meta, {							nginx_online: true,							nginx_err:    null						});						return model							.query()							.where('id', host.id)							.patch({								meta: combined_meta							});					})					.catch((err) => {						// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.						// It will always look like this:						//   nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)						let valid_lines = [];						let err_lines   = err.message.split('\n');						err_lines.map(function (line) {							if (line.indexOf('/var/log/nginx/error.log') === -1) {								valid_lines.push(line);							}						});						if (debug_mode) {							logger.error('Nginx test failed:', valid_lines.join('\n'));						}						// config is bad, update meta and delete config						combined_meta = _.assign({}, host.meta, {							nginx_online: false,							nginx_err:    valid_lines.join('\n')						});						return model							.query()							.where('id', host.id)							.patch({								meta: combined_meta							})							.then(() => {								internalNginx.renameConfigAsError(host_type, host)							})							.then(() => {								return internalNginx.deleteConfig(host_type, host, true);							});					});			})			.then(() => {				return internalNginx.reload();			})			.then(() => {				return combined_meta;			});	},	/**	 * @returns {Promise}	 */	test: () => {		if (debug_mode) {			logger.info('Testing Nginx configuration');		}		return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');	},	/**	 * @returns {Promise}	 */	reload: () => {		return internalNginx.test()			.then(() => {				logger.info('Reloading Nginx');				return utils.exec('/usr/sbin/nginx -s reload');			});	},	/**	 * @param   {String}  host_type	 * @param   {Integer} host_id	 * @returns {String}	 */	getConfigName: (host_type, host_id) => {		if (host_type === 'default') {			return '/data/nginx/default_host/site.conf';		}		return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';	},	/**	 * Generates custom locations	 * @param   {Object}  host	 * @returns {Promise}	 */	renderLocations: (host) => {		return new Promise((resolve, reject) => {			let template;			try {				template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});			} catch (err) {				reject(new error.ConfigurationError(err.message));				return;			}			const renderEngine = utils.getRenderEngine();			let renderedLocations = '';			const locationRendering = async () => {				for (let i = 0; i < host.locations.length; i++) {					let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id},						{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},						{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},						{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},						{certificate: host.certificate}, host.locations[i]);					if (locationCopy.forward_host.indexOf('/') > -1) {						const splitted = locationCopy.forward_host.split('/');						locationCopy.forward_host = splitted.shift();						locationCopy.forward_path = `/${splitted.join('/')}`;					}					// eslint-disable-next-line					renderedLocations += await renderEngine.parseAndRender(template, locationCopy);				}			};			locationRendering().then(() => resolve(renderedLocations));		});	},	/**	 * @param   {String}  host_type	 * @param   {Object}  host	 * @returns {Promise}	 */	generateConfig: (host_type, host) => {		const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);		if (debug_mode) {			logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2));		}		const renderEngine = utils.getRenderEngine();		return new Promise((resolve, reject) => {			let template = null;			let filename = internalNginx.getConfigName(nice_host_type, host.id);			try {				template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'});			} catch (err) {				reject(new error.ConfigurationError(err.message));				return;			}			let locationsPromise;			let origLocations;			// Manipulate the data a bit before sending it to the template			if (nice_host_type !== 'default') {				host.use_default_location = true;				if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {					host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);				}			}			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;				});				// Allow someone who is using / custom location path to use it, and skip the default / location				_.map(host.locations, (location) => {					if (location.path === '/') {						host.use_default_location = false;					}				});			} else {				locationsPromise = Promise.resolve();			}			// Set the IPv6 setting for the host			host.ipv6 = internalNginx.ipv6Enabled();			locationsPromise.then(() => {				renderEngine					.parseAndRender(template, host)					.then((config_text) => {						fs.writeFileSync(filename, config_text, {encoding: 'utf8'});						if (debug_mode) {							logger.success('Wrote config:', filename, config_text);						}						// Restore locations array						host.locations = origLocations;						resolve(true);					})					.catch((err) => {						if (debug_mode) {							logger.warn('Could not write ' + filename + ':', err.message);						}						reject(new error.ConfigurationError(err.message));					});			});		});	},	/**	 * This generates a temporary nginx config listening on port 80 for the domain names listed	 * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt	 * when requesting a certificate without having a hostname set up already.	 *	 * @param   {Object}  certificate	 * @returns {Promise}	 */	generateLetsEncryptRequestConfig: (certificate) => {		if (debug_mode) {			logger.info('Generating LetsEncrypt Request Config:', certificate);		}		const renderEngine = utils.getRenderEngine();		return new Promise((resolve, reject) => {			let template = null;			let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';			try {				template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});			} catch (err) {				reject(new error.ConfigurationError(err.message));				return;			}			certificate.ipv6 = internalNginx.ipv6Enabled();			renderEngine				.parseAndRender(template, certificate)				.then((config_text) => {					fs.writeFileSync(filename, config_text, {encoding: 'utf8'});					if (debug_mode) {						logger.success('Wrote config:', filename, config_text);					}					resolve(true);				})				.catch((err) => {					if (debug_mode) {						logger.warn('Could not write ' + filename + ':', err.message);					}					reject(new error.ConfigurationError(err.message));				});		});	},	/**	 * A simple wrapper around unlinkSync that writes to the logger	 *	 * @param   {String}  filename	 */	deleteFile: (filename) => {		logger.debug('Deleting file: ' + filename);		try {			fs.unlinkSync(filename);		} catch (err) {			logger.debug('Could not delete file:', JSON.stringify(err, null, 2));		};	},	/**	 *	 * @param   {String} host_type	 * @returns String	 */	getFileFriendlyHostType: (host_type) => {		return host_type.replace(new RegExp('-', 'g'), '_');	},	/**	 * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`	 *	 * @param   {Object}  certificate	 * @returns {Promise}	 */	deleteLetsEncryptRequestConfig: (certificate) => {		const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';		return new Promise((resolve/*, reject*/) => {			internalNginx.deleteFile(config_file);			resolve();		});	},	/**	 * @param   {String}  host_type	 * @param   {Object}  [host]	 * @param   {Boolean} [delete_err_file]	 * @returns {Promise}	 */	deleteConfig: (host_type, host, delete_err_file) => {		const config_file     = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);		const config_file_err = config_file + '.err';		return new Promise((resolve/*, reject*/) => {			internalNginx.deleteFile(config_file);			if (delete_err_file) {				internalNginx.deleteFile(config_file_err);			}			resolve();		});	},	/**	 * @param   {String}  host_type	 * @param   {Object}  [host]	 * @returns {Promise}	 */	renameConfigAsError: (host_type, host) => {		const config_file     = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);		const config_file_err = config_file + '.err';		return new Promise((resolve/*, reject*/) => {			fs.unlink(config_file, () => {				// ignore result, continue				fs.rename(config_file, config_file_err, () => {					// also ignore result, as this is a debugging informative file anyway					resolve();				});			});		});	},	/**	 * @param   {String}  host_type	 * @param   {Array}   hosts	 * @returns {Promise}	 */	bulkGenerateConfigs: (host_type, hosts) => {		let promises = [];		hosts.map(function (host) {			promises.push(internalNginx.generateConfig(host_type, host));		});		return Promise.all(promises);	},	/**	 * @param   {String}  host_type	 * @param   {Array}   hosts	 * @returns {Promise}	 */	bulkDeleteConfigs: (host_type, hosts) => {		let promises = [];		hosts.map(function (host) {			promises.push(internalNginx.deleteConfig(host_type, host, true));		});		return Promise.all(promises);	},	/**	 * @param   {string}  config	 * @returns {boolean}	 */	advancedConfigHasDefaultLocation: function (config) {		return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);	},	/**	 * @returns {boolean}	 */	ipv6Enabled: function () {		if (typeof process.env.DISABLE_IPV6 !== 'undefined') {			const disabled = process.env.DISABLE_IPV6.toLowerCase();			return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');		}		return true;	}};module.exports = internalNginx;
 |