Custom SSL Validation endpoint

This commit is contained in:
Jamie Curnow 2018-08-08 16:58:21 +10:00
parent 1b68869e6b
commit c8592503e3
8 changed files with 139 additions and 55 deletions

View File

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

View File

@ -1,10 +1,13 @@
'use strict'; 'use strict';
const fs = require('fs');
const _ = require('lodash'); const _ = require('lodash');
const error = require('../lib/error'); const error = require('../lib/error');
const certificateModel = require('../models/certificate'); const certificateModel = require('../models/certificate');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalHost = require('./host'); const internalHost = require('./host');
const tempWrite = require('temp-write');
const utils = require('../lib/utils');
function omissions () { function omissions () {
return ['is_deleted']; 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 {Access} access
* @param {Object} data * @param {Object} data
@ -208,7 +212,8 @@ const internalCertificate = {
* @returns {Promise} * @returns {Promise}
*/ */
validate: (access, data) => { validate: (access, data) => {
return new Promise((resolve, reject) => { return new Promise(resolve => {
// Put file contents into an object
let files = {}; let files = {};
_.map(data.files, (file, name) => { _.map(data.files, (file, name) => {
if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { if (internalHost.allowed_ssl_files.indexOf(name) !== -1) {
@ -219,12 +224,62 @@ const internalCertificate = {
resolve(files); resolve(files);
}) })
.then(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')};
});
// TODO: validate using openssl } else if (type === 'certificate') {
// files.certificate return utils.exec('openssl x509 -in ' + filepath + ' -text -noout')
// files.certificate_key .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 true; return {[type]: file_result};
})
);
});
// 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;
});
}); });
}, },

View File

@ -8,7 +8,7 @@ const deadHostModel = require('../models/dead_host');
const internalHost = { 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 * Internal use only, checks to see if the domain is already taken by any other record

View File

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

View File

@ -535,6 +535,14 @@ module.exports = {
*/ */
upload: function (id, form_data) { upload: function (id, form_data) {
return FileUpload('nginx/certificates/' + id + '/upload', 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);
} }
} }
}, },

View File

@ -39,22 +39,32 @@
</div> </div>
<div class="col-sm-12 col-md-12 other-ssl"> <div class="col-sm-12 col-md-12 other-ssl">
<div class="form-group"> <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"> <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> <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-12 other-ssl"> <div class="col-sm-12 col-md-12 other-ssl">
<div class="form-group"> <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"> <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> <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
</div> </div>
</div> </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> </div>
</form> </form>

View File

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

View File

@ -66,8 +66,6 @@
"force-ssl": "Force SSL", "force-ssl": "Force SSL",
"domain-names": "Domain Names", "domain-names": "Domain Names",
"cert-provider": "Certificate Provider", "cert-provider": "Certificate Provider",
"other-certificate": "Certificate",
"other-certificate-key": "Certificate Key",
"block-exploits": "Block Common Exploits", "block-exploits": "Block Common Exploits",
"caching-enabled": "Cache Assets" "caching-enabled": "Cache Assets"
}, },
@ -141,7 +139,10 @@
"delete": "Delete SSL Certificate", "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.", "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-title": "SSL Certificates",
"help-content": "TODO" "help-content": "TODO",
"other-certificate": "Certificate",
"other-certificate-key": "Certificate Key",
"other-intermediate-certificate": "Intermediate Certificate"
}, },
"access-lists": { "access-lists": {
"title": "Access Lists", "title": "Access Lists",