Added ability to force renew a LE cert, and also fix revoking certs

This commit is contained in:
Jamie Curnow 2019-05-08 15:25:48 +10:00
parent feaa0e51bd
commit 4d5adefa41
7 changed files with 141 additions and 59 deletions

View File

@ -1,5 +1,3 @@
'use strict';
const fs = require('fs'); const fs = require('fs');
const _ = require('lodash'); const _ = require('lodash');
const logger = require('../logger').ssl; const logger = require('../logger').ssl;
@ -9,7 +7,7 @@ const internalAuditLog = require('./audit-log');
const tempWrite = require('temp-write'); const tempWrite = require('temp-write');
const utils = require('../lib/utils'); const utils = require('../lib/utils');
const moment = require('moment'); 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 internalNginx = require('./nginx');
const internalHost = require('./host'); const internalHost = require('./host');
const certbot_command = '/usr/bin/certbot'; const certbot_command = '/usr/bin/certbot';
@ -21,7 +19,7 @@ function omissions () {
const internalCertificate = { const internalCertificate = {
allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'], 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: null,
interval_processing: false, interval_processing: false,
@ -205,7 +203,7 @@ const internalCertificate = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Integer} data.id * @param {Number} data.id
* @param {String} [data.email] * @param {String} [data.email]
* @param {String} [data.name] * @param {String} [data.name]
* @return {Promise} * @return {Promise}
@ -251,7 +249,7 @@ const internalCertificate = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Integer} data.id * @param {Number} data.id
* @param {Array} [data.expand] * @param {Array} [data.expand]
* @param {Array} [data.omit] * @param {Array} [data.omit]
* @return {Promise} * @return {Promise}
@ -297,7 +295,7 @@ const internalCertificate = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Integer} data.id * @param {Number} data.id
* @param {String} [data.reason] * @param {String} [data.reason]
* @returns {Promise} * @returns {Promise}
*/ */
@ -381,7 +379,7 @@ const internalCertificate = {
/** /**
* Report use * Report use
* *
* @param {Integer} user_id * @param {Number} user_id
* @param {String} visibility * @param {String} visibility
* @returns {Promise} * @returns {Promise}
*/ */
@ -522,7 +520,7 @@ const internalCertificate = {
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} data * @param {Object} data
* @param {Integer} data.id * @param {Number} data.id
* @param {Object} data.files * @param {Object} data.files
* @returns {Promise} * @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 * @param {Object} certificate the certificate row
* @returns {Promise} * @returns {Promise}
@ -762,17 +790,29 @@ const internalCertificate = {
revokeLetsEncryptSsl: (certificate, throw_errors) => { revokeLetsEncryptSsl: (certificate, throw_errors) => {
logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); 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) { if (debug_mode) {
logger.info('Command:', cmd); logger.info('Command:', revoke_cmd);
} }
return utils.exec(cmd) return utils.exec(revoke_cmd)
.then(result => { .then((result) => {
logger.info(result); logger.info(result);
return 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 => { .catch(err => {
if (debug_mode) { if (debug_mode) {
logger.error(err.message); logger.error(err.message);
@ -796,7 +836,7 @@ const internalCertificate = {
/** /**
* @param {Object} in_use_result * @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.proxy_hosts
* @param {Array} in_use_result.redirection_hosts * @param {Array} in_use_result.redirection_hosts
* @param {Array} in_use_result.dead_hosts * @param {Array} in_use_result.dead_hosts
@ -826,7 +866,7 @@ const internalCertificate = {
/** /**
* @param {Object} in_use_result * @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.proxy_hosts
* @param {Array} in_use_result.redirection_hosts * @param {Array} in_use_result.redirection_hosts
* @param {Array} in_use_result.dead_hosts * @param {Array} in_use_result.dead_hosts

View File

@ -1,5 +1,3 @@
'use strict';
const express = require('express'); const express = require('express');
const validator = require('../../../lib/validator'); const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode'); const jwtdecode = require('../../../lib/express/jwt-decode');
@ -94,13 +92,13 @@ router
certificate_id: { certificate_id: {
$ref: 'definitions#/definitions/id' $ref: 'definitions#/definitions/id'
}, },
expand: { expand: {
$ref: 'definitions#/definitions/expand' $ref: 'definitions#/definitions/expand'
} }
} }
}, { }, {
certificate_id: req.params.certificate_id, 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 => { .then(data => {
return internalCertificate.get(res.locals.access, { 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 * Validate Certs before saving
* *

View File

@ -1,5 +1,3 @@
'use strict';
const $ = require('jquery'); const $ = require('jquery');
const _ = require('underscore'); const _ = require('underscore');
const Tokens = require('./tokens'); const Tokens = require('./tokens');
@ -11,8 +9,8 @@ const Tokens = require('./tokens');
* @constructor * @constructor
*/ */
const ApiError = function (message, debug, code) { const ApiError = function (message, debug, code) {
let temp = Error.call(this, message); let temp = Error.call(this, message);
temp.name = this.name = 'ApiError'; temp.name = this.name = 'ApiError';
this.stack = temp.stack; this.stack = temp.stack;
this.message = temp.message; this.message = temp.message;
this.debug = debug; this.debug = debug;
@ -35,7 +33,7 @@ ApiError.prototype = Object.create(Error.prototype, {
* @param {Object} [options] * @param {Object} [options]
* @returns {Promise} * @returns {Promise}
*/ */
function fetch (verb, path, data, options) { function fetch(verb, path, data, options) {
options = options || {}; options = options || {};
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@ -55,7 +53,7 @@ function fetch (verb, path, data, options) {
contentType: options.contentType || 'application/json; charset=UTF-8', contentType: options.contentType || 'application/json; charset=UTF-8',
processData: options.processData || true, processData: options.processData || true,
crossDomain: true, crossDomain: true,
timeout: options.timeout ? options.timeout : 15000, timeout: options.timeout ? options.timeout : 30000,
xhrFields: { xhrFields: {
withCredentials: true withCredentials: true
}, },
@ -99,7 +97,7 @@ function fetch (verb, path, data, options) {
* @param {Array} expand * @param {Array} expand
* @returns {String} * @returns {String}
*/ */
function makeExpansionString (expand) { function makeExpansionString(expand) {
let items = []; let items = [];
_.forEach(expand, function (exp) { _.forEach(expand, function (exp) {
items.push(encodeURIComponent(exp)); items.push(encodeURIComponent(exp));
@ -114,7 +112,7 @@ function makeExpansionString (expand) {
* @param {String} [query] * @param {String} [query]
* @returns {Promise} * @returns {Promise}
*/ */
function getAllObjects (path, expand, query) { function getAllObjects(path, expand, query) {
let params = []; let params = [];
if (typeof expand === 'object' && expand !== null && expand.length) { 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('&') : '')); return fetch('get', path + (params.length ? '?' + params.join('&') : ''));
} }
/** function FileUpload(path, fd) {
* @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) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
let token = Tokens.getTopToken(); let token = Tokens.getTopToken();
@ -214,7 +199,7 @@ module.exports = {
Users: { Users: {
/** /**
* @param {Integer|String} user_id * @param {Number|String} user_id
* @param {Array} [expand] * @param {Array} [expand]
* @returns {Promise} * @returns {Promise}
*/ */
@ -639,6 +624,14 @@ module.exports = {
*/ */
validate: function (form_data) { validate: function (form_data) {
return FileUpload('nginx/certificates/validate', form_data); return FileUpload('nginx/certificates/validate', form_data);
},
/**
* @param {Number} id
* @returns {Promise}
*/
renew: function (id) {
return fetch('post', 'nginx/certificates/' + id + '/renew');
} }
} }
}, },

View File

@ -5,16 +5,23 @@
</td> </td>
<td> <td>
<div> <div>
<% if (provider === 'letsencrypt') { %> <%
<% domain_names.map(function(host) { if (provider === 'letsencrypt') {
%> domain_names.map(function(host) {
<span class="tag"><%- host %></span> if (host.indexOf('*') === -1) {
<% %>
<span class="tag host-link hover-pink" rel="https://<%- host %>"><%- host %></span>
<%
} else {
%>
<span class="tag"><%- host %></span>
<%
}
}); });
%> } else {
<% } else { %> %><%- nice_name %><%
<%- nice_name %> }
<% } %> %>
</div> </div>
<div class="small text-muted"> <div class="small text-muted">
<%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %>
@ -31,6 +38,10 @@
<div class="item-action dropdown"> <div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a> <a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<% if (provider === 'letsencrypt') { %>
<a href="#" class="renew dropdown-item"><i class="dropdown-icon fe fe-refresh-cw"></i> <%- i18n('certificates', 'force-renew') %></a>
<div class="dropdown-divider"></div>
<% } %>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a> <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
</div> </div>
</div> </div>

View File

@ -1,5 +1,3 @@
'use strict';
const Mn = require('backbone.marionette'); const Mn = require('backbone.marionette');
const moment = require('moment'); const moment = require('moment');
const App = require('../../../main'); const App = require('../../../main');
@ -10,13 +8,26 @@ module.exports = Mn.View.extend({
tagName: 'tr', tagName: 'tr',
ui: { ui: {
delete: 'a.delete' host_link: '.host-link',
renew: 'a.renew',
delete: 'a.delete'
}, },
events: { events: {
'click @ui.renew': function (e) {
e.preventDefault();
App.Controller.showNginxCertificateRenew(this.model);
},
'click @ui.delete': function (e) { 'click @ui.delete': function (e) {
e.preventDefault(); e.preventDefault();
App.Controller.showNginxCertificateDeleteConfirm(this.model); App.Controller.showNginxCertificateDeleteConfirm(this.model);
},
'click @ui.host_link': function (e) {
e.preventDefault();
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
win.focus();
} }
}, },

View File

@ -1,5 +1,3 @@
'use strict';
const Mn = require('backbone.marionette'); const Mn = require('backbone.marionette');
const App = require('../../../main'); const App = require('../../../main');
const ItemView = require('./item'); const ItemView = require('./item');

View File

@ -32,7 +32,8 @@
"offline": "Offline", "offline": "Offline",
"unknown": "Unknown", "unknown": "Unknown",
"expires": "Expires", "expires": "Expires",
"value": "Value" "value": "Value",
"please-wait": "Please wait..."
}, },
"login": { "login": {
"title": "Login to your account" "title": "Login to your account"
@ -115,7 +116,7 @@
"access-list": "Access List", "access-list": "Access List",
"allow-websocket-upgrade": "Websockets Support", "allow-websocket-upgrade": "Websockets Support",
"ignore-invalid-upstream-ssl": "Ignore Invalid SSL", "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": { "redirection-hosts": {
"title": "Redirection Hosts", "title": "Redirection Hosts",
@ -169,7 +170,9 @@
"help-content": "TODO", "help-content": "TODO",
"other-certificate": "Certificate", "other-certificate": "Certificate",
"other-certificate-key": "Certificate Key", "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": { "access-lists": {
"title": "Access Lists", "title": "Access Lists",