Skip to content

Commit 1626c8e

Browse files
authored
Merge pull request #1343 from ssrahul96/develop
Added support to download Let's Encrypt Certificate
2 parents ca3370a + ca6561b commit 1626c8e

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

backend/internal/certificate.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const internalHost = require('./host');
1313
const letsencryptStaging = process.env.NODE_ENV !== 'production';
1414
const letsencryptConfig = '/etc/letsencrypt.ini';
1515
const certbotCommand = 'certbot';
16+
const archiver = require('archiver');
17+
const path = require('path');
1618

1719
function omissions() {
1820
return ['is_deleted'];
@@ -335,6 +337,71 @@ const internalCertificate = {
335337
});
336338
},
337339

340+
/**
341+
* @param {Access} access
342+
* @param {Object} data
343+
* @param {Number} data.id
344+
* @returns {Promise}
345+
*/
346+
download: (access, data) => {
347+
return new Promise((resolve, reject) => {
348+
access.can('certificates:get', data)
349+
.then(() => {
350+
return internalCertificate.get(access, data);
351+
})
352+
.then((certificate) => {
353+
if (certificate.provider === 'letsencrypt') {
354+
const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id;
355+
356+
if (!fs.existsSync(zipDirectory)) {
357+
throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists');
358+
}
359+
360+
let certFiles = fs.readdirSync(zipDirectory)
361+
.filter((fn) => fn.endsWith('.pem'))
362+
.map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
363+
const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`;
364+
const opName = '/tmp/' + downloadName;
365+
internalCertificate.zipFiles(certFiles, opName)
366+
.then(() => {
367+
logger.debug('zip completed : ', opName);
368+
const resp = {
369+
fileName: opName
370+
};
371+
resolve(resp);
372+
}).catch((err) => reject(err));
373+
} else {
374+
throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded');
375+
}
376+
}).catch((err) => reject(err));
377+
});
378+
},
379+
380+
/**
381+
* @param {String} source
382+
* @param {String} out
383+
* @returns {Promise}
384+
*/
385+
zipFiles(source, out) {
386+
const archive = archiver('zip', { zlib: { level: 9 } });
387+
const stream = fs.createWriteStream(out);
388+
389+
return new Promise((resolve, reject) => {
390+
source
391+
.map((fl) => {
392+
let fileName = path.basename(fl);
393+
logger.debug(fl, 'added to certificate zip');
394+
archive.file(fl, { name: fileName });
395+
});
396+
archive
397+
.on('error', (err) => reject(err))
398+
.pipe(stream);
399+
400+
stream.on('close', () => resolve());
401+
archive.finalize();
402+
});
403+
},
404+
338405
/**
339406
* @param {Access} access
340407
* @param {Object} data

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"main": "js/index.js",
66
"dependencies": {
77
"ajv": "^6.12.0",
8+
"archiver": "^5.3.0",
89
"batchflow": "^0.4.0",
910
"bcrypt": "^5.0.0",
1011
"body-parser": "^1.19.0",

backend/routes/api/nginx/certificates.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,35 @@ router
209209
.catch(next);
210210
});
211211

212+
213+
/**
214+
* Download LE Certs
215+
*
216+
* /api/nginx/certificates/123/download
217+
*/
218+
router
219+
.route('/:certificate_id/download')
220+
.options((req, res) => {
221+
res.sendStatus(204);
222+
})
223+
.all(jwtdecode())
224+
225+
/**
226+
* GET /api/nginx/certificates/123/download
227+
*
228+
* Renew certificate
229+
*/
230+
.get((req, res, next) => {
231+
internalCertificate.download(res.locals.access, {
232+
id: parseInt(req.params.certificate_id, 10)
233+
})
234+
.then((result) => {
235+
res.status(200)
236+
.download(result.fileName);
237+
})
238+
.catch(next);
239+
});
240+
212241
/**
213242
* Validate Certs before saving
214243
*

frontend/js/app/api.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,51 @@ function FileUpload(path, fd) {
152152
});
153153
}
154154

155+
//ref : https://codepen.io/chrisdpratt/pen/RKxJNo
156+
function DownloadFile(verb, path, filename) {
157+
return new Promise(function (resolve, reject) {
158+
let api_url = '/api/';
159+
let url = api_url + path;
160+
let token = Tokens.getTopToken();
161+
162+
$.ajax({
163+
url: url,
164+
type: verb,
165+
crossDomain: true,
166+
xhrFields: {
167+
withCredentials: true,
168+
responseType: 'blob'
169+
},
170+
171+
beforeSend: function (xhr) {
172+
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
173+
},
174+
175+
success: function (data) {
176+
var a = document.createElement('a');
177+
var url = window.URL.createObjectURL(data);
178+
a.href = url;
179+
a.download = filename;
180+
document.body.append(a);
181+
a.click();
182+
a.remove();
183+
window.URL.revokeObjectURL(url);
184+
},
185+
186+
error: function (xhr, status, error_thrown) {
187+
let code = 400;
188+
189+
if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') {
190+
error_thrown = xhr.responseJSON.error.message;
191+
code = xhr.responseJSON.error.code || 500;
192+
}
193+
194+
reject(new ApiError(error_thrown, xhr.responseText, code));
195+
}
196+
});
197+
});
198+
}
199+
155200
module.exports = {
156201
status: function () {
157202
return fetch('get', '');
@@ -638,6 +683,14 @@ module.exports = {
638683
*/
639684
renew: function (id, timeout = 180000) {
640685
return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout});
686+
},
687+
688+
/**
689+
* @param {Number} id
690+
* @returns {Promise}
691+
*/
692+
download: function (id) {
693+
return DownloadFile('get', "nginx/certificates/" + id + "/download", "certificate.zip")
641694
}
642695
}
643696
},

frontend/js/app/nginx/certificates/list/item.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<span class="dropdown-header"><%- i18n('audit-log', 'certificate') %> #<%- id %></span>
4242
<% if (provider === 'letsencrypt') { %>
4343
<a href="#" class="renew dropdown-item"><i class="dropdown-icon fe fe-refresh-cw"></i> <%- i18n('certificates', 'force-renew') %></a>
44+
<a href="#" class="download dropdown-item"><i class="dropdown-icon fe fe-download"></i> <%- i18n('certificates', 'download') %></a>
4445
<div class="dropdown-divider"></div>
4546
<% } %>
4647
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>

frontend/js/app/nginx/certificates/list/item.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ module.exports = Mn.View.extend({
1111
ui: {
1212
host_link: '.host-link',
1313
renew: 'a.renew',
14-
delete: 'a.delete'
14+
delete: 'a.delete',
15+
download: 'a.download'
1516
},
1617

1718
events: {
@@ -29,6 +30,11 @@ module.exports = Mn.View.extend({
2930
e.preventDefault();
3031
let win = window.open($(e.currentTarget).attr('rel'), '_blank');
3132
win.focus();
33+
},
34+
35+
'click @ui.download': function (e) {
36+
e.preventDefault();
37+
App.Api.Nginx.Certificates.download(this.model.get('id'))
3238
}
3339
},
3440

frontend/js/i18n/messages.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
"other-certificate-key": "Certificate Key",
189189
"other-intermediate-certificate": "Intermediate Certificate",
190190
"force-renew": "Renew Now",
191+
"download": "Download",
191192
"renew-title": "Renew Let'sEncrypt Certificate"
192193
},
193194
"access-lists": {

0 commit comments

Comments
 (0)