You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 04:11:42 +03:00 
			
		
		
		
	Merge pull request #1343 from ssrahul96/develop
Added support to download Let's Encrypt Certificate
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 *
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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'))
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user