Преглед изворни кода

Merge pull request #1343 from ssrahul96/develop

Added support to download Let's Encrypt Certificate
jc21 пре 4 година
родитељ
комит
1626c8edd1

+ 67 - 0
backend/internal/certificate.js

@@ -13,6 +13,8 @@ const internalHost       = require('./host');
 const letsencryptStaging = process.env.NODE_ENV !== 'production';
 const letsencryptConfig  = '/etc/letsencrypt.ini';
 const certbotCommand     = 'certbot';
+const archiver           = require('archiver');
+const path               = require('path');
 
 function omissions() {
 	return ['is_deleted'];
@@ -335,6 +337,71 @@ const internalCertificate = {
 			});
 	},
 
+	/**
+	 * @param   {Access}  access
+	 * @param   {Object}  data
+	 * @param   {Number}  data.id
+	 * @returns {Promise}
+	 */
+	download: (access, data) => {
+		return new Promise((resolve, reject) => {
+			access.can('certificates:get', data)
+				.then(() => {
+					return internalCertificate.get(access, data);
+				})
+				.then((certificate) => {
+					if (certificate.provider === 'letsencrypt') {
+						const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id;
+
+						if (!fs.existsSync(zipDirectory)) {
+							throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists');
+						}
+
+						let certFiles      = fs.readdirSync(zipDirectory)
+							.filter((fn) => fn.endsWith('.pem'))
+							.map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
+						const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`;
+						const opName       = '/tmp/' + downloadName;
+						internalCertificate.zipFiles(certFiles, opName)
+							.then(() => {
+								logger.debug('zip completed : ', opName);
+								const resp = {
+									fileName: opName
+								};
+								resolve(resp);
+							}).catch((err) => reject(err));
+					} else {
+						throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded');
+					}
+				}).catch((err) => reject(err));
+		});
+	},
+
+	/**
+	* @param   {String}  source
+	* @param   {String}  out
+	* @returns {Promise}
+	*/
+	zipFiles(source, out) {
+		const archive = archiver('zip', { zlib: { level: 9 } });
+		const stream  = fs.createWriteStream(out);
+	
+		return new Promise((resolve, reject) => {
+			source
+				.map((fl) => {
+					let fileName = path.basename(fl);
+					logger.debug(fl, 'added to certificate zip');
+					archive.file(fl, { name: fileName });
+				});
+			archive
+				.on('error', (err) => reject(err))
+				.pipe(stream);
+	
+			stream.on('close', () => resolve());
+			archive.finalize();
+		});
+	},
+
 	/**
 	 * @param {Access}  access
 	 * @param {Object}  data

+ 1 - 0
backend/package.json

@@ -5,6 +5,7 @@
 	"main": "js/index.js",
 	"dependencies": {
 		"ajv": "^6.12.0",
+		"archiver": "^5.3.0",
 		"batchflow": "^0.4.0",
 		"bcrypt": "^5.0.0",
 		"body-parser": "^1.19.0",

+ 29 - 0
backend/routes/api/nginx/certificates.js

@@ -209,6 +209,35 @@ router
 			.catch(next);
 	});
 
+
+/**
+ * Download LE Certs
+ *
+ * /api/nginx/certificates/123/download
+ */
+router
+	.route('/:certificate_id/download')
+	.options((req, res) => {
+		res.sendStatus(204);
+	})
+	.all(jwtdecode())
+
+	/**
+	 * GET /api/nginx/certificates/123/download
+	 *
+	 * Renew certificate
+	 */
+	.get((req, res, next) => {
+		internalCertificate.download(res.locals.access, {
+			id: parseInt(req.params.certificate_id, 10)
+		})
+			.then((result) => {
+				res.status(200)
+					.download(result.fileName);
+			})
+			.catch(next);
+	});
+
 /**
  * Validate Certs before saving
  *

+ 53 - 0
frontend/js/app/api.js

@@ -152,6 +152,51 @@ function FileUpload(path, fd) {
     });
 }
 
+//ref : https://codepen.io/chrisdpratt/pen/RKxJNo
+function DownloadFile(verb, path, filename) {
+    return new Promise(function (resolve, reject) {
+        let api_url = '/api/';
+        let url = api_url + path;
+        let token = Tokens.getTopToken();
+
+        $.ajax({
+            url: url,
+            type: verb,
+            crossDomain: true,
+            xhrFields: {
+                withCredentials: true,
+                responseType: 'blob'
+            },
+
+            beforeSend: function (xhr) {
+                xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
+            },
+
+            success: function (data) {
+                var a = document.createElement('a');
+                var url = window.URL.createObjectURL(data);
+                a.href = url;
+                a.download = filename;
+                document.body.append(a);
+                a.click();
+                a.remove();
+                window.URL.revokeObjectURL(url);
+            },
+
+            error: function (xhr, status, error_thrown) {
+                let code = 400;
+
+                if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') {
+                    error_thrown = xhr.responseJSON.error.message;
+                    code = xhr.responseJSON.error.code || 500;
+                }
+
+                reject(new ApiError(error_thrown, xhr.responseText, code));
+            }
+        });
+    });
+}
+
 module.exports = {
     status: function () {
         return fetch('get', '');
@@ -638,6 +683,14 @@ module.exports = {
              */
             renew: function (id, timeout = 180000) {
                 return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout});
+            },
+
+            /**
+             * @param   {Number}  id
+             * @returns {Promise}
+             */
+            download: function (id) {
+                return DownloadFile('get', "nginx/certificates/" + id + "/download", "certificate.zip")
             }
         }
     },

+ 1 - 0
frontend/js/app/nginx/certificates/list/item.ejs

@@ -41,6 +41,7 @@
             <span class="dropdown-header"><%- i18n('audit-log', 'certificate') %> #<%- id %></span>
             <% if (provider === 'letsencrypt') { %>
                 <a href="#" class="renew dropdown-item"><i class="dropdown-icon fe fe-refresh-cw"></i> <%- i18n('certificates', 'force-renew') %></a>
+                <a href="#" class="download dropdown-item"><i class="dropdown-icon fe fe-download"></i> <%- i18n('certificates', 'download') %></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>

+ 7 - 1
frontend/js/app/nginx/certificates/list/item.js

@@ -11,7 +11,8 @@ module.exports = Mn.View.extend({
     ui: {
         host_link: '.host-link',
         renew:     'a.renew',
-        delete:    'a.delete'
+        delete:    'a.delete',
+        download:  'a.download'
     },
 
     events: {
@@ -29,6 +30,11 @@ module.exports = Mn.View.extend({
             e.preventDefault();
             let win = window.open($(e.currentTarget).attr('rel'), '_blank');
             win.focus();
+        },
+        
+        'click @ui.download': function (e) {
+            e.preventDefault();
+            App.Api.Nginx.Certificates.download(this.model.get('id'))
         }
     },
 

+ 1 - 0
frontend/js/i18n/messages.json

@@ -188,6 +188,7 @@
       "other-certificate-key": "Certificate Key",
       "other-intermediate-certificate": "Intermediate Certificate",
       "force-renew": "Renew Now",
+      "download": "Download",
       "renew-title": "Renew Let'sEncrypt Certificate"
     },
     "access-lists": {