Bläddra i källkod

Custom SSL Validation endpoint

Jamie Curnow 7 år sedan
förälder
incheckning
c8592503e3

+ 1 - 1
src/backend/index.js

@@ -43,6 +43,6 @@ function appStart () {
 try {
     appStart();
 } catch (err) {
-    logger.error(err.message);
+    logger.error(err.message, err);
     process.exit(1);
 }

+ 62 - 7
src/backend/internal/certificate.js

@@ -1,10 +1,13 @@
 'use strict';
 
+const fs               = require('fs');
 const _                = require('lodash');
 const error            = require('../lib/error');
 const certificateModel = require('../models/certificate');
 const internalAuditLog = require('./audit-log');
 const internalHost     = require('./host');
+const tempWrite        = require('temp-write');
+const utils            = require('../lib/utils');
 
 function omissions () {
     return ['is_deleted'];
@@ -200,7 +203,8 @@ const internalCertificate = {
     },
 
     /**
-     * Validates that the certs provided are good
+     * Validates that the certs provided are good.
+     * This is probably a horrible way to do this.
      *
      * @param   {Access}  access
      * @param   {Object}  data
@@ -208,7 +212,8 @@ const internalCertificate = {
      * @returns {Promise}
      */
     validate: (access, data) => {
-        return new Promise((resolve, reject) => {
+        return new Promise(resolve => {
+            // Put file contents into an object
             let files = {};
             _.map(data.files, (file, name) => {
                 if (internalHost.allowed_ssl_files.indexOf(name) !== -1) {
@@ -219,12 +224,62 @@ const internalCertificate = {
             resolve(files);
         })
             .then(files => {
+                // For each file, create a temp file and write the contents to it
+                // Then test it depending on the file type
+                let promises = [];
+                _.map(files, (content, type) => {
+                    promises.push(tempWrite(content, '/tmp')
+                        .then(filepath => {
+                            if (type === 'certificate_key') {
+                                return utils.exec('openssl rsa -in ' + filepath + ' -check')
+                                    .then(result => {
+                                        return {tmp: filepath, result: result.split("\n").shift()};
+                                    }).catch(err => {
+                                        return {tmp: filepath, result: false, err: new error.ValidationError('Certificate Key is not valid')};
+                                    });
+
+                            } else if (type === 'certificate') {
+                                return utils.exec('openssl x509 -in ' + filepath + ' -text -noout')
+                                    .then(result => {
+                                        return {tmp: filepath, result: result};
+                                    }).catch(err => {
+                                        return {tmp: filepath, result: false, err: new error.ValidationError('Certificate is not valid')};
+                                    });
+                            } else {
+                                return {tmp: filepath, result: false};
+                            }
+                        })
+                        .then(file_result => {
+                            // Remove temp files
+                            fs.unlinkSync(file_result.tmp);
+                            delete file_result.tmp;
+
+                            return {[type]: file_result};
+                        })
+                    );
+                });
 
-                // TODO: validate using openssl
-                // files.certificate
-                // files.certificate_key
-
-                return true;
+                // With the results, delete the temp files for security mainly.
+                // If there was an error with any of them, wait until we've done the deleting
+                // before throwing it.
+                return Promise.all(promises)
+                    .then(files => {
+                        let data = {};
+                        let err = null;
+
+                        _.each(files, file => {
+                            data = _.assign({}, data, file);
+                            if (typeof file.err !== 'undefined' && file.err) {
+                                err = file.err;
+                            }
+                        });
+
+                        if (err) {
+                            throw err;
+                        }
+
+                        return data;
+                    });
             });
     },
 

+ 1 - 1
src/backend/internal/host.js

@@ -8,7 +8,7 @@ const deadHostModel        = require('../models/dead_host');
 
 const internalHost = {
 
-    allowed_ssl_files: ['certificate', 'certificate_key'],
+    allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'],
 
     /**
      * Internal use only, checks to see if the domain is already taken by any other record

+ 1 - 1
src/backend/routes/api/nginx/certificates.js

@@ -163,7 +163,7 @@ router
      * POST /api/nginx/certificates/123/upload
      *
      * Upload certificates
-     */validate
+     */
     .post((req, res, next) => {
         if (!req.files) {
             res.status(400)

+ 8 - 0
src/frontend/js/app/api.js

@@ -535,6 +535,14 @@ module.exports = {
              */
             upload: function (id, form_data) {
                 return FileUpload('nginx/certificates/' + id + '/upload', form_data);
+            },
+
+            /**
+             * @param  {FormData} form_data
+             * @params {Promise}
+             */
+            validate: function (form_data) {
+                return FileUpload('nginx/certificates/validate', form_data);
             }
         }
     },

+ 14 - 4
src/frontend/js/app/nginx/certificates/form.ejs

@@ -39,22 +39,32 @@
                     </div>
                     <div class="col-sm-12 col-md-12 other-ssl">
                         <div class="form-group">
-                            <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div>
+                            <div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div>
                             <div class="custom-file">
-                                <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate" required>
+                                <input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required>
                                 <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
                             </div>
                         </div>
                     </div>
                     <div class="col-sm-12 col-md-12 other-ssl">
                         <div class="form-group">
-                            <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div>
+                            <div class="form-label"><%- i18n('certificates', 'other-certificate') %><span class="form-required">*</span></div>
                             <div class="custom-file">
-                                <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key" required>
+                                <input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate">
                                 <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
                             </div>
                         </div>
                     </div>
+                    <div class="col-sm-12 col-md-12 other-ssl">
+                        <div class="form-group">
+                            <div class="form-label"><%- i18n('certificates', 'other-intermediate-certificate') %></div>
+                            <div class="custom-file">
+                                <input type="file" class="custom-file-input" name="meta[other_intermediate_certificate]" id="other_intermediate_certificate">
+                                <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
+                            </div>
+                        </div>
+                    </div>
+
                 <% } %>
             </div>
         </form>

+ 49 - 39
src/frontend/js/app/nginx/certificates/form.js

@@ -15,13 +15,14 @@ module.exports = Mn.View.extend({
     max_file_size: 5120,
 
     ui: {
-        form:                      'form',
-        domain_names:              'input[name="domain_names"]',
-        buttons:                   '.modal-footer button',
-        cancel:                    'button.cancel',
-        save:                      'button.save',
-        other_ssl_certificate:     '#other_ssl_certificate',
-        other_ssl_certificate_key: '#other_ssl_certificate_key'
+        form:                           'form',
+        domain_names:                   'input[name="domain_names"]',
+        buttons:                        '.modal-footer button',
+        cancel:                         'button.cancel',
+        save:                           'button.save',
+        other_certificate:              '#other_certificate',
+        other_certificate_key:          '#other_certificate_key',
+        other_intermediate_certificate: '#other_intermediate_certificate'
     },
 
     events: {
@@ -33,8 +34,8 @@ module.exports = Mn.View.extend({
                 return;
             }
 
-            let view = this;
-            let data = this.ui.form.serializeJSON();
+            let view      = this;
+            let data      = this.ui.form.serializeJSON();
             data.provider = this.model.get('provider');
 
             // Manipulate
@@ -46,55 +47,66 @@ module.exports = Mn.View.extend({
                 data.domain_names = data.domain_names.split(',');
             }
 
-            let method    = App.Api.Nginx.Certificates.create;
-            let is_new    = true;
             let ssl_files = [];
 
-            if (this.model.get('id')) {
-                // edit
-                is_new  = false;
-                method  = App.Api.Nginx.Certificates.update;
-                data.id = this.model.get('id');
-            }
-
             // check files are attached
             if (this.model.get('provider') === 'other' && !this.model.hasSslFiles()) {
-                if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) {
-                    alert('certificate file is not attached');
+                if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) {
+                    alert('Certificate file is not attached');
                     return;
                 } else {
-                    if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) {
-                        alert('certificate file is too large (> 5kb)');
+                    if (this.ui.other_certificate[0].files[0].size > this.max_file_size) {
+                        alert('Certificate file is too large (> 5kb)');
                         return;
                     }
-                    ssl_files.push({name: 'certificate', file: this.ui.other_ssl_certificate[0].files[0]});
+                    ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]});
                 }
 
-                if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) {
-                    alert('certificate key file is not attached');
+                if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) {
+                    alert('Certificate key file is not attached');
                     return;
                 } else {
-                    if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) {
-                        alert('certificate key file is too large (> 5kb)');
+                    if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) {
+                        alert('Certificate key file is too large (> 5kb)');
+                        return;
+                    }
+                    ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]});
+                }
+
+                if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) {
+                    if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) {
+                        alert('Intermediate Certificate file is too large (> 5kb)');
                         return;
                     }
-                    ssl_files.push({name: 'certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]});
+                    ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]});
                 }
             }
 
             this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
-            method(data)
+
+            // compile file data
+            let form_data = new FormData();
+            if (view.model.get('provider') && ssl_files.length) {
+                ssl_files.map(function (file) {
+                    form_data.append(file.name, file.file);
+                });
+            }
+
+            new Promise(resolve => {
+                if (view.model.get('provider') === 'other') {
+                    resolve(App.Api.Nginx.Certificates.validate(form_data));
+                } else {
+                    resolve();
+                }
+            })
+                .then(() => {
+                    return App.Api.Nginx.Certificates.create(data);
+                })
                 .then(result => {
                     view.model.set(result);
 
                     // Now upload the certs if we need to
-                    if (ssl_files.length) {
-                        let form_data = new FormData();
-
-                        ssl_files.map(function (file) {
-                            form_data.append(file.name, file.file);
-                        });
-
+                    if (view.model.get('provider') === 'other') {
                         return App.Api.Nginx.Certificates.upload(view.model.get('id'), form_data)
                             .then(result => {
                                 view.model.set('meta', _.assign({}, view.model.get('meta'), result));
@@ -103,9 +115,7 @@ module.exports = Mn.View.extend({
                 })
                 .then(() => {
                     App.UI.closeModal(function () {
-                        if (is_new) {
-                            App.Controller.showNginxCertificates();
-                        }
+                        App.Controller.showNginxCertificates();
                     });
                 })
                 .catch(err => {

+ 4 - 3
src/frontend/js/i18n/messages.json

@@ -66,8 +66,6 @@
       "force-ssl": "Force SSL",
       "domain-names": "Domain Names",
       "cert-provider": "Certificate Provider",
-      "other-certificate": "Certificate",
-      "other-certificate-key": "Certificate Key",
       "block-exploits": "Block Common Exploits",
       "caching-enabled": "Cache Assets"
     },
@@ -141,7 +139,10 @@
       "delete": "Delete SSL Certificate",
       "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.",
       "help-title": "SSL Certificates",
-      "help-content": "TODO"
+      "help-content": "TODO",
+      "other-certificate": "Certificate",
+      "other-certificate-key": "Certificate Key",
+      "other-intermediate-certificate": "Intermediate Certificate"
     },
     "access-lists": {
       "title": "Access Lists",