nginx.js 14 KB

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