Skip to content

Commit c859250

Browse files
author
Jamie Curnow
committed
Custom SSL Validation endpoint
1 parent 1b68869 commit c859250

File tree

8 files changed

+140
-56
lines changed

8 files changed

+140
-56
lines changed

src/backend/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ function appStart () {
4343
try {
4444
appStart();
4545
} catch (err) {
46-
logger.error(err.message);
46+
logger.error(err.message, err);
4747
process.exit(1);
4848
}

src/backend/internal/certificate.js

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
'use strict';
22

3+
const fs = require('fs');
34
const _ = require('lodash');
45
const error = require('../lib/error');
56
const certificateModel = require('../models/certificate');
67
const internalAuditLog = require('./audit-log');
78
const internalHost = require('./host');
9+
const tempWrite = require('temp-write');
10+
const utils = require('../lib/utils');
811

912
function omissions () {
1013
return ['is_deleted'];
@@ -200,15 +203,17 @@ const internalCertificate = {
200203
},
201204

202205
/**
203-
* Validates that the certs provided are good
206+
* Validates that the certs provided are good.
207+
* This is probably a horrible way to do this.
204208
*
205209
* @param {Access} access
206210
* @param {Object} data
207211
* @param {Object} data.files
208212
* @returns {Promise}
209213
*/
210214
validate: (access, data) => {
211-
return new Promise((resolve, reject) => {
215+
return new Promise(resolve => {
216+
// Put file contents into an object
212217
let files = {};
213218
_.map(data.files, (file, name) => {
214219
if (internalHost.allowed_ssl_files.indexOf(name) !== -1) {
@@ -219,12 +224,62 @@ const internalCertificate = {
219224
resolve(files);
220225
})
221226
.then(files => {
227+
// For each file, create a temp file and write the contents to it
228+
// Then test it depending on the file type
229+
let promises = [];
230+
_.map(files, (content, type) => {
231+
promises.push(tempWrite(content, '/tmp')
232+
.then(filepath => {
233+
if (type === 'certificate_key') {
234+
return utils.exec('openssl rsa -in ' + filepath + ' -check')
235+
.then(result => {
236+
return {tmp: filepath, result: result.split("\n").shift()};
237+
}).catch(err => {
238+
return {tmp: filepath, result: false, err: new error.ValidationError('Certificate Key is not valid')};
239+
});
240+
241+
} else if (type === 'certificate') {
242+
return utils.exec('openssl x509 -in ' + filepath + ' -text -noout')
243+
.then(result => {
244+
return {tmp: filepath, result: result};
245+
}).catch(err => {
246+
return {tmp: filepath, result: false, err: new error.ValidationError('Certificate is not valid')};
247+
});
248+
} else {
249+
return {tmp: filepath, result: false};
250+
}
251+
})
252+
.then(file_result => {
253+
// Remove temp files
254+
fs.unlinkSync(file_result.tmp);
255+
delete file_result.tmp;
256+
257+
return {[type]: file_result};
258+
})
259+
);
260+
});
222261

223-
// TODO: validate using openssl
224-
// files.certificate
225-
// files.certificate_key
226-
227-
return true;
262+
// With the results, delete the temp files for security mainly.
263+
// If there was an error with any of them, wait until we've done the deleting
264+
// before throwing it.
265+
return Promise.all(promises)
266+
.then(files => {
267+
let data = {};
268+
let err = null;
269+
270+
_.each(files, file => {
271+
data = _.assign({}, data, file);
272+
if (typeof file.err !== 'undefined' && file.err) {
273+
err = file.err;
274+
}
275+
});
276+
277+
if (err) {
278+
throw err;
279+
}
280+
281+
return data;
282+
});
228283
});
229284
},
230285

src/backend/internal/host.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const deadHostModel = require('../models/dead_host');
88

99
const internalHost = {
1010

11-
allowed_ssl_files: ['certificate', 'certificate_key'],
11+
allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'],
1212

1313
/**
1414
* Internal use only, checks to see if the ___domain is already taken by any other record

src/backend/routes/api/nginx/certificates.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ router
163163
* POST /api/nginx/certificates/123/upload
164164
*
165165
* Upload certificates
166-
*/validate
166+
*/
167167
.post((req, res, next) => {
168168
if (!req.files) {
169169
res.status(400)

src/frontend/js/app/api.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,14 @@ module.exports = {
535535
*/
536536
upload: function (id, form_data) {
537537
return FileUpload('nginx/certificates/' + id + '/upload', form_data);
538+
},
539+
540+
/**
541+
* @param {FormData} form_data
542+
* @params {Promise}
543+
*/
544+
validate: function (form_data) {
545+
return FileUpload('nginx/certificates/validate', form_data);
538546
}
539547
}
540548
},

src/frontend/js/app/nginx/certificates/form.ejs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,32 @@
3939
</div>
4040
<div class="col-sm-12 col-md-12 other-ssl">
4141
<div class="form-group">
42-
<div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div>
42+
<div class="form-label"><%- i18n('certificates', 'other-certificate-key') %><span class="form-required">*</span></div>
4343
<div class="custom-file">
44-
<input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate" required>
44+
<input type="file" class="custom-file-input" name="meta[other_certificate_key]" id="other_certificate_key" required>
4545
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
4646
</div>
4747
</div>
4848
</div>
4949
<div class="col-sm-12 col-md-12 other-ssl">
5050
<div class="form-group">
51-
<div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div>
51+
<div class="form-label"><%- i18n('certificates', 'other-certificate') %><span class="form-required">*</span></div>
5252
<div class="custom-file">
53-
<input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key" required>
53+
<input type="file" class="custom-file-input" name="meta[other_certificate]" id="other_certificate">
5454
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
5555
</div>
5656
</div>
5757
</div>
58+
<div class="col-sm-12 col-md-12 other-ssl">
59+
<div class="form-group">
60+
<div class="form-label"><%- i18n('certificates', 'other-intermediate-certificate') %></div>
61+
<div class="custom-file">
62+
<input type="file" class="custom-file-input" name="meta[other_intermediate_certificate]" id="other_intermediate_certificate">
63+
<label class="custom-file-label"><%- i18n('str', 'choose-file') %></label>
64+
</div>
65+
</div>
66+
</div>
67+
5868
<% } %>
5969
</div>
6070
</form>

src/frontend/js/app/nginx/certificates/form.js

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ module.exports = Mn.View.extend({
1515
max_file_size: 5120,
1616

1717
ui: {
18-
form: 'form',
19-
domain_names: 'input[name="domain_names"]',
20-
buttons: '.modal-footer button',
21-
cancel: 'button.cancel',
22-
save: 'button.save',
23-
other_ssl_certificate: '#other_ssl_certificate',
24-
other_ssl_certificate_key: '#other_ssl_certificate_key'
18+
form: 'form',
19+
domain_names: 'input[name="domain_names"]',
20+
buttons: '.modal-footer button',
21+
cancel: 'button.cancel',
22+
save: 'button.save',
23+
other_certificate: '#other_certificate',
24+
other_certificate_key: '#other_certificate_key',
25+
other_intermediate_certificate: '#other_intermediate_certificate'
2526
},
2627

2728
events: {
@@ -33,8 +34,8 @@ module.exports = Mn.View.extend({
3334
return;
3435
}
3536

36-
let view = this;
37-
let data = this.ui.form.serializeJSON();
37+
let view = this;
38+
let data = this.ui.form.serializeJSON();
3839
data.provider = this.model.get('provider');
3940

4041
// Manipulate
@@ -46,55 +47,66 @@ module.exports = Mn.View.extend({
4647
data.domain_names = data.domain_names.split(',');
4748
}
4849

49-
let method = App.Api.Nginx.Certificates.create;
50-
let is_new = true;
5150
let ssl_files = [];
5251

53-
if (this.model.get('id')) {
54-
// edit
55-
is_new = false;
56-
method = App.Api.Nginx.Certificates.update;
57-
data.id = this.model.get('id');
58-
}
59-
6052
// check files are attached
6153
if (this.model.get('provider') === 'other' && !this.model.hasSslFiles()) {
62-
if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) {
63-
alert('certificate file is not attached');
54+
if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) {
55+
alert('Certificate file is not attached');
6456
return;
6557
} else {
66-
if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) {
67-
alert('certificate file is too large (> 5kb)');
58+
if (this.ui.other_certificate[0].files[0].size > this.max_file_size) {
59+
alert('Certificate file is too large (> 5kb)');
6860
return;
6961
}
70-
ssl_files.push({name: 'certificate', file: this.ui.other_ssl_certificate[0].files[0]});
62+
ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]});
7163
}
7264

73-
if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) {
74-
alert('certificate key file is not attached');
65+
if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) {
66+
alert('Certificate key file is not attached');
7567
return;
7668
} else {
77-
if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) {
78-
alert('certificate key file is too large (> 5kb)');
69+
if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) {
70+
alert('Certificate key file is too large (> 5kb)');
71+
return;
72+
}
73+
ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]});
74+
}
75+
76+
if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) {
77+
if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) {
78+
alert('Intermediate Certificate file is too large (> 5kb)');
7979
return;
8080
}
81-
ssl_files.push({name: 'certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]});
81+
ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]});
8282
}
8383
}
8484

8585
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
86-
method(data)
86+
87+
// compile file data
88+
let form_data = new FormData();
89+
if (view.model.get('provider') && ssl_files.length) {
90+
ssl_files.map(function (file) {
91+
form_data.append(file.name, file.file);
92+
});
93+
}
94+
95+
new Promise(resolve => {
96+
if (view.model.get('provider') === 'other') {
97+
resolve(App.Api.Nginx.Certificates.validate(form_data));
98+
} else {
99+
resolve();
100+
}
101+
})
102+
.then(() => {
103+
return App.Api.Nginx.Certificates.create(data);
104+
})
87105
.then(result => {
88106
view.model.set(result);
89107

90108
// Now upload the certs if we need to
91-
if (ssl_files.length) {
92-
let form_data = new FormData();
93-
94-
ssl_files.map(function (file) {
95-
form_data.append(file.name, file.file);
96-
});
97-
109+
if (view.model.get('provider') === 'other') {
98110
return App.Api.Nginx.Certificates.upload(view.model.get('id'), form_data)
99111
.then(result => {
100112
view.model.set('meta', _.assign({}, view.model.get('meta'), result));
@@ -103,9 +115,7 @@ module.exports = Mn.View.extend({
103115
})
104116
.then(() => {
105117
App.UI.closeModal(function () {
106-
if (is_new) {
107-
App.Controller.showNginxCertificates();
108-
}
118+
App.Controller.showNginxCertificates();
109119
});
110120
})
111121
.catch(err => {

src/frontend/js/i18n/messages.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@
6666
"force-ssl": "Force SSL",
6767
"___domain-names": "Domain Names",
6868
"cert-provider": "Certificate Provider",
69-
"other-certificate": "Certificate",
70-
"other-certificate-key": "Certificate Key",
7169
"block-exploits": "Block Common Exploits",
7270
"caching-enabled": "Cache Assets"
7371
},
@@ -141,7 +139,10 @@
141139
"delete": "Delete SSL Certificate",
142140
"delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.",
143141
"help-title": "SSL Certificates",
144-
"help-content": "TODO"
142+
"help-content": "TODO",
143+
"other-certificate": "Certificate",
144+
"other-certificate-key": "Certificate Key",
145+
"other-intermediate-certificate": "Intermediate Certificate"
145146
},
146147
"access-lists": {
147148
"title": "Access Lists",

0 commit comments

Comments
 (0)