From 4d5adefa41da9b62a20683e6d22bc21c8d1d237d Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 8 May 2019 15:25:48 +1000 Subject: [PATCH] Added ability to force renew a LE cert, and also fix revoking certs --- src/backend/internal/certificate.js | 70 +++++++++++++++---- src/backend/routes/api/nginx/certificates.js | 34 +++++++-- src/frontend/js/app/api.js | 39 +++++------ .../js/app/nginx/certificates/list/item.ejs | 29 +++++--- .../js/app/nginx/certificates/list/item.js | 17 ++++- .../js/app/nginx/certificates/list/main.js | 2 - src/frontend/js/i18n/messages.json | 9 ++- 7 files changed, 141 insertions(+), 59 deletions(-) diff --git a/src/backend/internal/certificate.js b/src/backend/internal/certificate.js index 6b1bd1c..fbe5e25 100644 --- a/src/backend/internal/certificate.js +++ b/src/backend/internal/certificate.js @@ -1,5 +1,3 @@ -'use strict'; - const fs = require('fs'); const _ = require('lodash'); const logger = require('../logger').ssl; @@ -9,7 +7,7 @@ const internalAuditLog = require('./audit-log'); const tempWrite = require('temp-write'); const utils = require('../lib/utils'); const moment = require('moment'); -const debug_mode = process.env.NODE_ENV !== 'production'; +const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG ; const internalNginx = require('./nginx'); const internalHost = require('./host'); const certbot_command = '/usr/bin/certbot'; @@ -21,7 +19,7 @@ function omissions () { const internalCertificate = { allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'], - interval_timeout: 1000 * 60 * 60 * 12, // 12 hours + interval_timeout: 1000 * 60 * 60, // 1 hour interval: null, interval_processing: false, @@ -205,7 +203,7 @@ const internalCertificate = { /** * @param {Access} access * @param {Object} data - * @param {Integer} data.id + * @param {Number} data.id * @param {String} [data.email] * @param {String} [data.name] * @return {Promise} @@ -251,7 +249,7 @@ const internalCertificate = { /** * @param {Access} access * @param {Object} data - * @param {Integer} data.id + * @param {Number} data.id * @param {Array} [data.expand] * @param {Array} [data.omit] * @return {Promise} @@ -297,7 +295,7 @@ const internalCertificate = { /** * @param {Access} access * @param {Object} data - * @param {Integer} data.id + * @param {Number} data.id * @param {String} [data.reason] * @returns {Promise} */ @@ -381,7 +379,7 @@ const internalCertificate = { /** * Report use * - * @param {Integer} user_id + * @param {Number} user_id * @param {String} visibility * @returns {Promise} */ @@ -522,7 +520,7 @@ const internalCertificate = { /** * @param {Access} access * @param {Object} data - * @param {Integer} data.id + * @param {Number} data.id * @param {Object} data.files * @returns {Promise} */ @@ -734,6 +732,36 @@ const internalCertificate = { }); }, + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + renew: (access, data) => { + return access.can('certificates:update', data) + .then(() => { + return internalCertificate.get(access, data); + }) + .then((certificate) => { + if (certificate.provider === 'letsencrypt') { + return internalCertificate.renewLetsEncryptSsl(certificate) + .then(() => { + return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') + }) + .then(cert_info => { + return certificateModel + .query() + .patchAndFetchById(certificate.id, { + expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') + }); + }); + } else { + throw new error.ValidationError('Only Let\'sEncrypt certificates can be renewed'); + } + }) + }, + /** * @param {Object} certificate the certificate row * @returns {Promise} @@ -762,17 +790,29 @@ const internalCertificate = { revokeLetsEncryptSsl: (certificate, throw_errors) => { logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - let cmd = certbot_command + ' revoke --cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + (debug_mode ? '--staging' : ''); + let revoke_cmd = certbot_command + ' revoke --cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + (debug_mode ? '--staging' : ''); + let delete_cmd = certbot_command + ' delete --cert-name "npm-' + certificate.id + '" ' + (debug_mode ? '--staging' : ''); if (debug_mode) { - logger.info('Command:', cmd); + logger.info('Command:', revoke_cmd); } - return utils.exec(cmd) - .then(result => { + return utils.exec(revoke_cmd) + .then((result) => { logger.info(result); return result; }) + .then(() => { + if (debug_mode) { + logger.info('Command:', delete_cmd); + } + + return utils.exec(delete_cmd) + .then((result) => { + logger.info(result); + return result; + }) + }) .catch(err => { if (debug_mode) { logger.error(err.message); @@ -796,7 +836,7 @@ const internalCertificate = { /** * @param {Object} in_use_result - * @param {Integer} in_use_result.total_count + * @param {Number} in_use_result.total_count * @param {Array} in_use_result.proxy_hosts * @param {Array} in_use_result.redirection_hosts * @param {Array} in_use_result.dead_hosts @@ -826,7 +866,7 @@ const internalCertificate = { /** * @param {Object} in_use_result - * @param {Integer} in_use_result.total_count + * @param {Number} in_use_result.total_count * @param {Array} in_use_result.proxy_hosts * @param {Array} in_use_result.redirection_hosts * @param {Array} in_use_result.dead_hosts diff --git a/src/backend/routes/api/nginx/certificates.js b/src/backend/routes/api/nginx/certificates.js index 04fafdf..4c873bc 100644 --- a/src/backend/routes/api/nginx/certificates.js +++ b/src/backend/routes/api/nginx/certificates.js @@ -1,5 +1,3 @@ -'use strict'; - const express = require('express'); const validator = require('../../../lib/validator'); const jwtdecode = require('../../../lib/express/jwt-decode'); @@ -94,13 +92,13 @@ router certificate_id: { $ref: 'definitions#/definitions/id' }, - expand: { + expand: { $ref: 'definitions#/definitions/expand' } } }, { certificate_id: req.params.certificate_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) .then(data => { return internalCertificate.get(res.locals.access, { @@ -181,6 +179,34 @@ router } }); +/** + * Renew LE Certs + * + * /api/nginx/certificates/123/renew + */ +router + .route('/:certificate_id/renew') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/certificates/123/renew + * + * Renew certificate + */ + .post((req, res, next) => { + internalCertificate.renew(res.locals.access, { + id: parseInt(req.params.certificate_id, 10) + }) + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + /** * Validate Certs before saving * diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index c8d5719..74356f0 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -1,5 +1,3 @@ -'use strict'; - const $ = require('jquery'); const _ = require('underscore'); const Tokens = require('./tokens'); @@ -11,8 +9,8 @@ const Tokens = require('./tokens'); * @constructor */ const ApiError = function (message, debug, code) { - let temp = Error.call(this, message); - temp.name = this.name = 'ApiError'; + let temp = Error.call(this, message); + temp.name = this.name = 'ApiError'; this.stack = temp.stack; this.message = temp.message; this.debug = debug; @@ -35,7 +33,7 @@ ApiError.prototype = Object.create(Error.prototype, { * @param {Object} [options] * @returns {Promise} */ -function fetch (verb, path, data, options) { +function fetch(verb, path, data, options) { options = options || {}; return new Promise(function (resolve, reject) { @@ -55,7 +53,7 @@ function fetch (verb, path, data, options) { contentType: options.contentType || 'application/json; charset=UTF-8', processData: options.processData || true, crossDomain: true, - timeout: options.timeout ? options.timeout : 15000, + timeout: options.timeout ? options.timeout : 30000, xhrFields: { withCredentials: true }, @@ -99,7 +97,7 @@ function fetch (verb, path, data, options) { * @param {Array} expand * @returns {String} */ -function makeExpansionString (expand) { +function makeExpansionString(expand) { let items = []; _.forEach(expand, function (exp) { items.push(encodeURIComponent(exp)); @@ -114,7 +112,7 @@ function makeExpansionString (expand) { * @param {String} [query] * @returns {Promise} */ -function getAllObjects (path, expand, query) { +function getAllObjects(path, expand, query) { let params = []; if (typeof expand === 'object' && expand !== null && expand.length) { @@ -128,20 +126,7 @@ function getAllObjects (path, expand, query) { return fetch('get', path + (params.length ? '?' + params.join('&') : '')); } -/** - * @param {String} path - * @param {FormData} form_data - * @returns {Promise} - */ -function upload (path, form_data) { - console.log('UPLOAD:', path, form_data); - return fetch('post', path, form_data, { - contentType: 'multipart/form-data', - processData: false - }); -} - -function FileUpload (path, fd) { +function FileUpload(path, fd) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); let token = Tokens.getTopToken(); @@ -214,7 +199,7 @@ module.exports = { Users: { /** - * @param {Integer|String} user_id + * @param {Number|String} user_id * @param {Array} [expand] * @returns {Promise} */ @@ -639,6 +624,14 @@ module.exports = { */ validate: function (form_data) { return FileUpload('nginx/certificates/validate', form_data); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + renew: function (id) { + return fetch('post', 'nginx/certificates/' + id + '/renew'); } } }, diff --git a/src/frontend/js/app/nginx/certificates/list/item.ejs b/src/frontend/js/app/nginx/certificates/list/item.ejs index ad22fb5..dd79405 100644 --- a/src/frontend/js/app/nginx/certificates/list/item.ejs +++ b/src/frontend/js/app/nginx/certificates/list/item.ejs @@ -5,16 +5,23 @@
- <% if (provider === 'letsencrypt') { %> - <% domain_names.map(function(host) { - %> - <%- host %> - <% + <% + if (provider === 'letsencrypt') { + domain_names.map(function(host) { + if (host.indexOf('*') === -1) { + %> + <%- host %> + <% + } else { + %> + <%- host %> + <% + } }); - %> - <% } else { %> - <%- nice_name %> - <% } %> + } else { + %><%- nice_name %><% + } + %>
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> @@ -31,6 +38,10 @@ diff --git a/src/frontend/js/app/nginx/certificates/list/item.js b/src/frontend/js/app/nginx/certificates/list/item.js index d289747..6915227 100644 --- a/src/frontend/js/app/nginx/certificates/list/item.js +++ b/src/frontend/js/app/nginx/certificates/list/item.js @@ -1,5 +1,3 @@ -'use strict'; - const Mn = require('backbone.marionette'); const moment = require('moment'); const App = require('../../../main'); @@ -10,13 +8,26 @@ module.exports = Mn.View.extend({ tagName: 'tr', ui: { - delete: 'a.delete' + host_link: '.host-link', + renew: 'a.renew', + delete: 'a.delete' }, events: { + 'click @ui.renew': function (e) { + e.preventDefault(); + App.Controller.showNginxCertificateRenew(this.model); + }, + 'click @ui.delete': function (e) { e.preventDefault(); App.Controller.showNginxCertificateDeleteConfirm(this.model); + }, + + 'click @ui.host_link': function (e) { + e.preventDefault(); + let win = window.open($(e.currentTarget).attr('rel'), '_blank'); + win.focus(); } }, diff --git a/src/frontend/js/app/nginx/certificates/list/main.js b/src/frontend/js/app/nginx/certificates/list/main.js index 6472604..6bc7924 100644 --- a/src/frontend/js/app/nginx/certificates/list/main.js +++ b/src/frontend/js/app/nginx/certificates/list/main.js @@ -1,5 +1,3 @@ -'use strict'; - const Mn = require('backbone.marionette'); const App = require('../../../main'); const ItemView = require('./item'); diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index dd095b0..fff42c1 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -32,7 +32,8 @@ "offline": "Offline", "unknown": "Unknown", "expires": "Expires", - "value": "Value" + "value": "Value", + "please-wait": "Please wait..." }, "login": { "title": "Login to your account" @@ -115,7 +116,7 @@ "access-list": "Access List", "allow-websocket-upgrade": "Websockets Support", "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", - "cutom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding" + "custom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding" }, "redirection-hosts": { "title": "Redirection Hosts", @@ -169,7 +170,9 @@ "help-content": "TODO", "other-certificate": "Certificate", "other-certificate-key": "Certificate Key", - "other-intermediate-certificate": "Intermediate Certificate" + "other-intermediate-certificate": "Intermediate Certificate", + "force-renew": "Renew Now", + "renew-title": "Renew Let'sEncrypt Certificate" }, "access-lists": { "title": "Access Lists",