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 letsencryptStaging = process.env.NODE_ENV !== 'production';
 | 
				
			||||||
const letsencryptConfig  = '/etc/letsencrypt.ini';
 | 
					const letsencryptConfig  = '/etc/letsencrypt.ini';
 | 
				
			||||||
const certbotCommand     = 'certbot';
 | 
					const certbotCommand     = 'certbot';
 | 
				
			||||||
 | 
					const archiver           = require('archiver');
 | 
				
			||||||
 | 
					const path               = require('path');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function omissions() {
 | 
					function omissions() {
 | 
				
			||||||
	return ['is_deleted'];
 | 
						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 {Access}  access
 | 
				
			||||||
	 * @param {Object}  data
 | 
						 * @param {Object}  data
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
	"main": "js/index.js",
 | 
						"main": "js/index.js",
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
		"ajv": "^6.12.0",
 | 
							"ajv": "^6.12.0",
 | 
				
			||||||
 | 
							"archiver": "^5.3.0",
 | 
				
			||||||
		"batchflow": "^0.4.0",
 | 
							"batchflow": "^0.4.0",
 | 
				
			||||||
		"bcrypt": "^5.0.0",
 | 
							"bcrypt": "^5.0.0",
 | 
				
			||||||
		"body-parser": "^1.19.0",
 | 
							"body-parser": "^1.19.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -209,6 +209,35 @@ router
 | 
				
			|||||||
			.catch(next);
 | 
								.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
 | 
					 * 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 = {
 | 
					module.exports = {
 | 
				
			||||||
    status: function () {
 | 
					    status: function () {
 | 
				
			||||||
        return fetch('get', '');
 | 
					        return fetch('get', '');
 | 
				
			||||||
@@ -638,6 +683,14 @@ module.exports = {
 | 
				
			|||||||
             */
 | 
					             */
 | 
				
			||||||
            renew: function (id, timeout = 180000) {
 | 
					            renew: function (id, timeout = 180000) {
 | 
				
			||||||
                return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout});
 | 
					                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>
 | 
					            <span class="dropdown-header"><%- i18n('audit-log', 'certificate') %> #<%- id %></span>
 | 
				
			||||||
            <% if (provider === 'letsencrypt') { %>
 | 
					            <% 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="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>
 | 
					                <div class="dropdown-divider"></div>
 | 
				
			||||||
            <% } %>
 | 
					            <% } %>
 | 
				
			||||||
            <a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
 | 
					            <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: {
 | 
					    ui: {
 | 
				
			||||||
        host_link: '.host-link',
 | 
					        host_link: '.host-link',
 | 
				
			||||||
        renew:     'a.renew',
 | 
					        renew:     'a.renew',
 | 
				
			||||||
        delete:    'a.delete'
 | 
					        delete:    'a.delete',
 | 
				
			||||||
 | 
					        download:  'a.download'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    events: {
 | 
					    events: {
 | 
				
			||||||
@@ -29,6 +30,11 @@ module.exports = Mn.View.extend({
 | 
				
			|||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            let win = window.open($(e.currentTarget).attr('rel'), '_blank');
 | 
					            let win = window.open($(e.currentTarget).attr('rel'), '_blank');
 | 
				
			||||||
            win.focus();
 | 
					            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-certificate-key": "Certificate Key",
 | 
				
			||||||
      "other-intermediate-certificate": "Intermediate Certificate",
 | 
					      "other-intermediate-certificate": "Intermediate Certificate",
 | 
				
			||||||
      "force-renew": "Renew Now",
 | 
					      "force-renew": "Renew Now",
 | 
				
			||||||
 | 
					      "download": "Download",
 | 
				
			||||||
      "renew-title": "Renew Let'sEncrypt Certificate"
 | 
					      "renew-title": "Renew Let'sEncrypt Certificate"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "access-lists": {
 | 
					    "access-lists": {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user