nginx.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. const _ = require('lodash');
  2. const fs = require('fs');
  3. const logger = require('../logger').nginx;
  4. const utils = require('../lib/utils');
  5. const error = require('../lib/error');
  6. const { Liquid } = require('liquidjs');
  7. const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
  8. const internalNginx = {
  9. /**
  10. * This will:
  11. * - test the nginx config first to make sure it's OK
  12. * - create / recreate the config for the host
  13. * - test again
  14. * - IF OK: update the meta with online status
  15. * - IF BAD: update the meta with offline status and remove the config entirely
  16. * - then reload nginx
  17. *
  18. * @param {Object|String} model
  19. * @param {String} host_type
  20. * @param {Object} host
  21. * @returns {Promise}
  22. */
  23. configure: (model, host_type, host) => {
  24. let combined_meta = {};
  25. return internalNginx.test()
  26. .then(() => {
  27. // Nginx is OK
  28. // We're deleting this config regardless.
  29. return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all
  30. })
  31. .then(() => {
  32. return internalNginx.generateConfig(host_type, host);
  33. })
  34. .then(() => {
  35. // Test nginx again and update meta with result
  36. return internalNginx.test()
  37. .then(() => {
  38. // nginx is ok
  39. combined_meta = _.assign({}, host.meta, {
  40. nginx_online: true,
  41. nginx_err: null
  42. });
  43. return model
  44. .query()
  45. .where('id', host.id)
  46. .patch({
  47. meta: combined_meta
  48. });
  49. })
  50. .catch((err) => {
  51. // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
  52. // It will always look like this:
  53. // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
  54. let valid_lines = [];
  55. let err_lines = err.message.split('\n');
  56. err_lines.map(function (line) {
  57. if (line.indexOf('/var/log/nginx/error.log') === -1) {
  58. valid_lines.push(line);
  59. }
  60. });
  61. if (debug_mode) {
  62. logger.error('Nginx test failed:', valid_lines.join('\n'));
  63. }
  64. // config is bad, update meta and delete config
  65. combined_meta = _.assign({}, host.meta, {
  66. nginx_online: false,
  67. nginx_err: valid_lines.join('\n')
  68. });
  69. return model
  70. .query()
  71. .where('id', host.id)
  72. .patch({
  73. meta: combined_meta
  74. })
  75. .then(() => {
  76. return internalNginx.deleteConfig(host_type, host, true);
  77. });
  78. });
  79. })
  80. .then(() => {
  81. return internalNginx.reload();
  82. })
  83. .then(() => {
  84. return combined_meta;
  85. });
  86. },
  87. /**
  88. * @returns {Promise}
  89. */
  90. test: () => {
  91. if (debug_mode) {
  92. logger.info('Testing Nginx configuration');
  93. }
  94. return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
  95. },
  96. /**
  97. * @returns {Promise}
  98. */
  99. reload: () => {
  100. return internalNginx.test()
  101. .then(() => {
  102. logger.info('Reloading Nginx');
  103. return utils.exec('/usr/sbin/nginx -s reload');
  104. });
  105. },
  106. /**
  107. * @param {String} host_type
  108. * @param {Integer} host_id
  109. * @returns {String}
  110. */
  111. getConfigName: (host_type, host_id) => {
  112. host_type = host_type.replace(new RegExp('-', 'g'), '_');
  113. if (host_type === 'default') {
  114. return '/data/nginx/default_host/site.conf';
  115. }
  116. return '/data/nginx/' + host_type + '/' + host_id + '.conf';
  117. },
  118. /**
  119. * Generates custom locations
  120. * @param {Object} host
  121. * @returns {Promise}
  122. */
  123. renderLocations: (host) => {
  124. // logger.info('host = ' + JSON.stringify(host, null, 2));
  125. return new Promise((resolve, reject) => {
  126. let template;
  127. try {
  128. template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
  129. } catch (err) {
  130. reject(new error.ConfigurationError(err.message));
  131. return;
  132. }
  133. let renderer = new Liquid({
  134. root: __dirname + '/../templates/'
  135. });
  136. let renderedLocations = '';
  137. const locationRendering = async () => {
  138. for (let i = 0; i < host.locations.length; i++) {
  139. let locationCopy = Object.assign({}, {access_list_id : host.access_list_id}, {certificate_id : host.certificate_id},
  140. {ssl_forced : host.ssl_forced}, {caching_enabled : host.caching_enabled},
  141. {block_exploits : host.block_exploits}, {allow_websocket_upgrade : host.allow_websocket_upgrade},
  142. {http2_support : host.http2_support}, {hsts_enabled : host.hsts_enabled},
  143. {hsts_subdomains : host.hsts_subdomains}, {access_list : host.access_list},
  144. {certificate : host.certificate}, host.locations[i]);
  145. if (locationCopy.forward_host.indexOf('/') > -1) {
  146. const splitted = locationCopy.forward_host.split('/');
  147. locationCopy.forward_host = splitted.shift();
  148. locationCopy.forward_path = `/${splitted.join('/')}`;
  149. }
  150. // logger.info('locationCopy = ' + JSON.stringify(locationCopy, null, 2));
  151. // eslint-disable-next-line
  152. renderedLocations += await renderer.parseAndRender(template, locationCopy);
  153. }
  154. };
  155. locationRendering().then(() => resolve(renderedLocations));
  156. });
  157. },
  158. /**
  159. * @param {String} host_type
  160. * @param {Object} host
  161. * @returns {Promise}
  162. */
  163. generateConfig: (host_type, host) => {
  164. host_type = host_type.replace(new RegExp('-', 'g'), '_');
  165. if (debug_mode) {
  166. logger.info('Generating ' + host_type + ' Config:', host);
  167. }
  168. // logger.info('host = ' + JSON.stringify(host, null, 2));
  169. let renderEngine = new Liquid({
  170. root: __dirname + '/../templates/'
  171. });
  172. return new Promise((resolve, reject) => {
  173. let template = null;
  174. let filename = internalNginx.getConfigName(host_type, host.id);
  175. try {
  176. template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
  177. } catch (err) {
  178. reject(new error.ConfigurationError(err.message));
  179. return;
  180. }
  181. let locationsPromise;
  182. let origLocations;
  183. // Manipulate the data a bit before sending it to the template
  184. if (host_type !== 'default') {
  185. host.use_default_location = true;
  186. if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
  187. host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
  188. }
  189. }
  190. if (host.locations) {
  191. // logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
  192. origLocations = [].concat(host.locations);
  193. locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
  194. host.locations = renderedLocations;
  195. });
  196. // Allow someone who is using / custom location path to use it, and skip the default / location
  197. _.map(host.locations, (location) => {
  198. if (location.path === '/') {
  199. host.use_default_location = false;
  200. }
  201. });
  202. } else {
  203. locationsPromise = Promise.resolve();
  204. }
  205. // Set the IPv6 setting for the host
  206. host.ipv6 = internalNginx.ipv6Enabled();
  207. locationsPromise.then(() => {
  208. renderEngine
  209. .parseAndRender(template, host)
  210. .then((config_text) => {
  211. fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
  212. if (debug_mode) {
  213. logger.success('Wrote config:', filename, config_text);
  214. }
  215. // Restore locations array
  216. host.locations = origLocations;
  217. resolve(true);
  218. })
  219. .catch((err) => {
  220. if (debug_mode) {
  221. logger.warn('Could not write ' + filename + ':', err.message);
  222. }
  223. reject(new error.ConfigurationError(err.message));
  224. });
  225. });
  226. });
  227. },
  228. /**
  229. * This generates a temporary nginx config listening on port 80 for the domain names listed
  230. * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt
  231. * when requesting a certificate without having a hostname set up already.
  232. *
  233. * @param {Object} certificate
  234. * @returns {Promise}
  235. */
  236. generateLetsEncryptRequestConfig: (certificate) => {
  237. if (debug_mode) {
  238. logger.info('Generating LetsEncrypt Request Config:', certificate);
  239. }
  240. let renderEngine = new Liquid({
  241. root: __dirname + '/../templates/'
  242. });
  243. return new Promise((resolve, reject) => {
  244. let template = null;
  245. let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
  246. try {
  247. template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
  248. } catch (err) {
  249. reject(new error.ConfigurationError(err.message));
  250. return;
  251. }
  252. certificate.ipv6 = internalNginx.ipv6Enabled();
  253. renderEngine
  254. .parseAndRender(template, certificate)
  255. .then((config_text) => {
  256. fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
  257. if (debug_mode) {
  258. logger.success('Wrote config:', filename, config_text);
  259. }
  260. resolve(true);
  261. })
  262. .catch((err) => {
  263. if (debug_mode) {
  264. logger.warn('Could not write ' + filename + ':', err.message);
  265. }
  266. reject(new error.ConfigurationError(err.message));
  267. });
  268. });
  269. },
  270. /**
  271. * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`
  272. *
  273. * @param {Object} certificate
  274. * @param {Boolean} [throw_errors]
  275. * @returns {Promise}
  276. */
  277. deleteLetsEncryptRequestConfig: (certificate, throw_errors) => {
  278. return new Promise((resolve, reject) => {
  279. try {
  280. let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
  281. if (debug_mode) {
  282. logger.warn('Deleting nginx config: ' + config_file);
  283. }
  284. fs.unlinkSync(config_file);
  285. } catch (err) {
  286. if (debug_mode) {
  287. logger.warn('Could not delete config:', err.message);
  288. }
  289. if (throw_errors) {
  290. reject(err);
  291. }
  292. }
  293. resolve();
  294. });
  295. },
  296. /**
  297. * @param {String} host_type
  298. * @param {Object} [host]
  299. * @param {Boolean} [throw_errors]
  300. * @returns {Promise}
  301. */
  302. deleteConfig: (host_type, host, throw_errors) => {
  303. host_type = host_type.replace(new RegExp('-', 'g'), '_');
  304. return new Promise((resolve, reject) => {
  305. try {
  306. let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
  307. if (debug_mode) {
  308. logger.warn('Deleting nginx config: ' + config_file);
  309. }
  310. fs.unlinkSync(config_file);
  311. } catch (err) {
  312. if (debug_mode) {
  313. logger.warn('Could not delete config:', err.message);
  314. }
  315. if (throw_errors) {
  316. reject(err);
  317. }
  318. }
  319. resolve();
  320. });
  321. },
  322. /**
  323. * @param {String} host_type
  324. * @param {Array} hosts
  325. * @returns {Promise}
  326. */
  327. bulkGenerateConfigs: (host_type, hosts) => {
  328. let promises = [];
  329. hosts.map(function (host) {
  330. promises.push(internalNginx.generateConfig(host_type, host));
  331. });
  332. return Promise.all(promises);
  333. },
  334. /**
  335. * @param {String} host_type
  336. * @param {Array} hosts
  337. * @param {Boolean} [throw_errors]
  338. * @returns {Promise}
  339. */
  340. bulkDeleteConfigs: (host_type, hosts, throw_errors) => {
  341. let promises = [];
  342. hosts.map(function (host) {
  343. promises.push(internalNginx.deleteConfig(host_type, host, throw_errors));
  344. });
  345. return Promise.all(promises);
  346. },
  347. /**
  348. * @param {string} config
  349. * @returns {boolean}
  350. */
  351. advancedConfigHasDefaultLocation: function (config) {
  352. return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
  353. },
  354. /**
  355. * @returns {boolean}
  356. */
  357. ipv6Enabled: function () {
  358. if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
  359. const disabled = process.env.DISABLE_IPV6.toLowerCase();
  360. return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
  361. }
  362. return true;
  363. }
  364. };
  365. module.exports = internalNginx;