浏览代码

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

Jamie Curnow 6 年之前
父节点
当前提交
4d5adefa41

+ 55 - 15
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

+ 30 - 4
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
  *

+ 16 - 23
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');
             }
         }
     },

+ 20 - 9
src/frontend/js/app/nginx/certificates/list/item.ejs

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

+ 14 - 3
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();
         }
     },
 

+ 0 - 2
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');

+ 6 - 3
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",