mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	SSL certificate upload support
This commit is contained in:
		| @@ -3,6 +3,7 @@ | ||||
| const path        = require('path'); | ||||
| const express     = require('express'); | ||||
| const bodyParser  = require('body-parser'); | ||||
| const fileUpload  = require('express-fileupload'); | ||||
| const compression = require('compression'); | ||||
| const log         = require('./logger').express; | ||||
|  | ||||
| @@ -10,6 +11,7 @@ const log         = require('./logger').express; | ||||
|  * App | ||||
|  */ | ||||
| const app = express(); | ||||
| app.use(fileUpload()); | ||||
| app.use(bodyParser.json()); | ||||
| app.use(bodyParser.urlencoded({extended: true})); | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,8 @@ const deadHostModel        = require('../models/dead_host'); | ||||
|  | ||||
| const internalHost = { | ||||
|  | ||||
|     allowed_ssl_files: ['other_certificate', 'other_certificate_key'], | ||||
|  | ||||
|     /** | ||||
|      * Internal use only, checks to see if the domain is already taken by any other record | ||||
|      * | ||||
| @@ -64,6 +66,21 @@ const internalHost = { | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Cleans the ssl keys from the meta object and sets them to "true" | ||||
|      * | ||||
|      * @param   {Object}  meta | ||||
|      * @returns {*} | ||||
|      */ | ||||
|     cleanMeta: function (meta) { | ||||
|         internalHost.allowed_ssl_files.map(key => { | ||||
|             if (typeof meta[key] !== 'undefined' && meta[key]) { | ||||
|                 meta[key] = true; | ||||
|             } | ||||
|         }); | ||||
|         return meta; | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * Private call only | ||||
|      * | ||||
|   | ||||
| @@ -96,6 +96,7 @@ const internalProxyHost = { | ||||
|                     .omit(omissions()) | ||||
|                     .patchAndFetchById(row.id, data) | ||||
|                     .then(saved_row => { | ||||
|                         saved_row.meta = internalHost.cleanMeta(saved_row.meta); | ||||
|                         return _.omit(saved_row, omissions()); | ||||
|                     }); | ||||
|             }); | ||||
| @@ -144,6 +145,7 @@ const internalProxyHost = { | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 if (row) { | ||||
|                     row.meta = internalHost.cleanMeta(row.meta); | ||||
|                     return _.omit(row, omissions()); | ||||
|                 } else { | ||||
|                     throw new error.ItemNotFoundError(data.id); | ||||
| @@ -180,6 +182,32 @@ const internalProxyHost = { | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param   {Access}  access | ||||
|      * @param   {Object}  data | ||||
|      * @param   {Integer} data.id | ||||
|      * @param   {Object}  data.files | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     setCerts: (access, data) => { | ||||
|         return internalProxyHost.get(access, {id: data.id}) | ||||
|             .then(row => { | ||||
|                 _.map(data.files, (file, name) => { | ||||
|                     if (internalHost.allowed_ssl_files.indexOf(name) !== -1) { | ||||
|                         row.meta[name] = file.data.toString(); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 return internalProxyHost.update(access, { | ||||
|                     id:   data.id, | ||||
|                     meta: row.meta | ||||
|                 }); | ||||
|             }) | ||||
|             .then(row => { | ||||
|                 return _.pick(row.meta, internalHost.allowed_ssl_files); | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * All Hosts | ||||
|      * | ||||
| @@ -215,6 +243,13 @@ const internalProxyHost = { | ||||
|                 } | ||||
|  | ||||
|                 return query; | ||||
|             }) | ||||
|             .then(rows => { | ||||
|                 rows.map(row => { | ||||
|                     row.meta = internalHost.cleanMeta(row.meta); | ||||
|                 }); | ||||
|  | ||||
|                 return rows; | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -234,6 +234,8 @@ module.exports = function (token_string) { | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         reloadObjects: this.loadObjects, | ||||
|  | ||||
|         /** | ||||
|          * | ||||
|          * @param {String}  permission | ||||
| @@ -248,7 +250,6 @@ module.exports = function (token_string) { | ||||
|                 return this.init() | ||||
|                     .then(() => { | ||||
|                         // Initialised, token decoded ok | ||||
|  | ||||
|                         return this.getObjectSchema(permission) | ||||
|                             .then(objectSchema => { | ||||
|                                 let data_schema = { | ||||
| @@ -275,9 +276,9 @@ module.exports = function (token_string) { | ||||
|  | ||||
|                                 permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json'); | ||||
|  | ||||
|                                 //logger.debug('objectSchema:', JSON.stringify(objectSchema, null, 2)); | ||||
|                                 //logger.debug('permissionSchema:', JSON.stringify(permissionSchema, null, 2)); | ||||
|                                 //logger.debug('data_schema:', JSON.stringify(data_schema, null, 2)); | ||||
|                                 // logger.info('objectSchema', JSON.stringify(objectSchema, null, 2)); | ||||
|                                 // logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2)); | ||||
|                                 // logger.info('data_schema', JSON.stringify(data_schema, null, 2)); | ||||
|  | ||||
|                                 let ajv = validator({ | ||||
|                                     verbose:      true, | ||||
| @@ -301,8 +302,9 @@ module.exports = function (token_string) { | ||||
|                             }); | ||||
|                     }) | ||||
|                     .catch(err => { | ||||
|                         logger.error(err.message); | ||||
|                         logger.error(err.errors); | ||||
|                         err.permission      = permission; | ||||
|                         err.permission_data = data; | ||||
|                         logger.error(permission, data, err.message); | ||||
|  | ||||
|                         throw new error.PermissionError('Permission Denied', err); | ||||
|                     }); | ||||
|   | ||||
| @@ -147,4 +147,38 @@ router | ||||
|             .catch(next); | ||||
|     }); | ||||
|  | ||||
| /** | ||||
|  * Specific proxy-host Certificates | ||||
|  * | ||||
|  * /api/nginx/proxy-hosts/123/certificates | ||||
|  */ | ||||
| router | ||||
|     .route('/:host_id/certificates') | ||||
|     .options((req, res) => { | ||||
|         res.sendStatus(204); | ||||
|     }) | ||||
|     .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes | ||||
|  | ||||
|     /** | ||||
|      * POST /api/nginx/proxy-hosts/123/certificates | ||||
|      * | ||||
|      * Upload certifications | ||||
|      */ | ||||
|     .post((req, res, next) => { | ||||
|         if (!req.files) { | ||||
|             res.status(400) | ||||
|                 .send({error: 'No files were uploaded'}); | ||||
|         } else { | ||||
|             internalProxyHost.setCerts(res.locals.access, { | ||||
|                 id:    parseInt(req.params.host_id, 10), | ||||
|                 files: req.files | ||||
|             }) | ||||
|                 .then(result => { | ||||
|                     res.status(200) | ||||
|                         .send(result); | ||||
|                 }) | ||||
|                 .catch(next); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| module.exports = router; | ||||
|   | ||||
| @@ -43,14 +43,19 @@ function fetch (verb, path, data, options) { | ||||
|         let url     = api_url + path; | ||||
|         let token   = Tokens.getTopToken(); | ||||
|  | ||||
|         if ((typeof options.contentType === 'undefined' || options.contentType.match(/json/im)) && typeof data === 'object') { | ||||
|             data = JSON.stringify(data); | ||||
|         } | ||||
|  | ||||
|         $.ajax({ | ||||
|             url:         url, | ||||
|             data:        typeof data === 'object' ? JSON.stringify(data) : data, | ||||
|             type:        verb, | ||||
|             dataType:    'json', | ||||
|             contentType: 'application/json; charset=UTF-8', | ||||
|             contentType: options.contentType || 'application/json; charset=UTF-8', | ||||
|             processData: options.processData || true, | ||||
|             crossDomain: true, | ||||
|             timeout:     (options.timeout ? options.timeout : 15000), | ||||
|             timeout:     options.timeout ? options.timeout : 15000, | ||||
|             xhrFields:   { | ||||
|                 withCredentials: true | ||||
|             }, | ||||
| @@ -123,6 +128,41 @@ function getAllObjects (path, expand, query) { | ||||
|     return fetch('get', path + (params.length ? '?' + params.join('&') : '')); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param   {String}  path | ||||
|  * @param   {FormData}  form_data | ||||
|  * @returns {Promise} | ||||
|  */ | ||||
| function upload (path, form_data) { | ||||
|     console.log('UPLOAD:', path, form_data); | ||||
|     return fetch('post', path, form_data, { | ||||
|         contentType: 'multipart/form-data', | ||||
|         processData: false | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function FileUpload (path, fd) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         let xhr   = new XMLHttpRequest(); | ||||
|         let token = Tokens.getTopToken(); | ||||
|  | ||||
|         xhr.open('POST', '/api/' + path); | ||||
|         xhr.overrideMimeType('text/plain'); | ||||
|         xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); | ||||
|         xhr.send(fd); | ||||
|  | ||||
|         xhr.onreadystatechange = function () { | ||||
|             if (this.readyState === XMLHttpRequest.DONE) { | ||||
|                 if (xhr.status !== 200 && xhr.status !== 201) { | ||||
|                     reject(new Error('Upload failed: ' + xhr.status)); | ||||
|                 } else { | ||||
|                     resolve(xhr.responseText); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     status: function () { | ||||
|         return fetch('get', ''); | ||||
| @@ -283,6 +323,15 @@ module.exports = { | ||||
|              */ | ||||
|             delete: function (id) { | ||||
|                 return fetch('delete', 'nginx/proxy-hosts/' + id); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param  {Integer}  id | ||||
|              * @param  {FormData} form_data | ||||
|              * @params {Promise} | ||||
|              */ | ||||
|             setCerts: function (id, form_data) { | ||||
|                 return FileUpload('nginx/proxy-hosts/' + id + '/certificates', form_data); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
| @@ -294,6 +343,41 @@ module.exports = { | ||||
|              */ | ||||
|             getAll: function (expand, query) { | ||||
|                 return getAllObjects('nginx/redirection-hosts', expand, query); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param {Object}  data | ||||
|              */ | ||||
|             create: function (data) { | ||||
|                 return fetch('post', 'nginx/redirection-hosts', data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Object}   data | ||||
|              * @param   {Integer}  data.id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             update: function (data) { | ||||
|                 let id = data.id; | ||||
|                 delete data.id; | ||||
|                 return fetch('put', 'nginx/redirection-hosts/' + id, data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Integer}  id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             delete: function (id) { | ||||
|                 return fetch('delete', 'nginx/redirection-hosts/' + id); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param  {Integer}  id | ||||
|              * @param  {FormData} form_data | ||||
|              * @params {Promise} | ||||
|              */ | ||||
|             setCerts: function (id, form_data) { | ||||
|                 return upload('nginx/redirection-hosts/' + id + '/certificates', form_data); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
| @@ -305,6 +389,32 @@ module.exports = { | ||||
|              */ | ||||
|             getAll: function (expand, query) { | ||||
|                 return getAllObjects('nginx/streams', expand, query); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param {Object}  data | ||||
|              */ | ||||
|             create: function (data) { | ||||
|                 return fetch('post', 'nginx/streams', data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Object}   data | ||||
|              * @param   {Integer}  data.id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             update: function (data) { | ||||
|                 let id = data.id; | ||||
|                 delete data.id; | ||||
|                 return fetch('put', 'nginx/streams/' + id, data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Integer}  id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             delete: function (id) { | ||||
|                 return fetch('delete', 'nginx/streams/' + id); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
| @@ -316,6 +426,41 @@ module.exports = { | ||||
|              */ | ||||
|             getAll: function (expand, query) { | ||||
|                 return getAllObjects('nginx/dead-hosts', expand, query); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param {Object}  data | ||||
|              */ | ||||
|             create: function (data) { | ||||
|                 return fetch('post', 'nginx/dead-hosts', data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Object}   data | ||||
|              * @param   {Integer}  data.id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             update: function (data) { | ||||
|                 let id = data.id; | ||||
|                 delete data.id; | ||||
|                 return fetch('put', 'nginx/dead-hosts/' + id, data); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param   {Integer}  id | ||||
|              * @returns {Promise} | ||||
|              */ | ||||
|             delete: function (id) { | ||||
|                 return fetch('delete', 'nginx/dead-hosts/' + id); | ||||
|             }, | ||||
|  | ||||
|             /** | ||||
|              * @param  {Integer}  id | ||||
|              * @param  {FormData} form_data | ||||
|              * @params {Promise} | ||||
|              */ | ||||
|             setCerts: function (id, form_data) { | ||||
|                 return upload('nginx/dead-hosts/' + id + '/certificates', form_data); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| @@ -328,6 +473,32 @@ module.exports = { | ||||
|          */ | ||||
|         getAll: function (expand, query) { | ||||
|             return getAllObjects('access-lists', expand, query); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * @param {Object}  data | ||||
|          */ | ||||
|         create: function (data) { | ||||
|             return fetch('post', 'access-lists', data); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * @param   {Object}   data | ||||
|          * @param   {Integer}  data.id | ||||
|          * @returns {Promise} | ||||
|          */ | ||||
|         update: function (data) { | ||||
|             let id = data.id; | ||||
|             delete data.id; | ||||
|             return fetch('put', 'access-lists/' + id, data); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * @param   {Integer}  id | ||||
|          * @returns {Promise} | ||||
|          */ | ||||
|         delete: function (id) { | ||||
|             return fetch('delete', 'access-lists/' + id); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -94,7 +94,7 @@ | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label"><%- i18n('all-hosts', 'other-certificate') %></div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate]" id="other_ssl_certificate"> | ||||
|                                     <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
| @@ -103,7 +103,7 @@ | ||||
|                             <div class="form-group"> | ||||
|                                 <div class="form-label"><%- i18n('all-hosts', 'other-certificate-key') %></div> | ||||
|                                 <div class="custom-file"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]"> | ||||
|                                     <input type="file" class="custom-file-input" name="meta[other_ssl_certificate_key]" id="other_ssl_certificate_key"> | ||||
|                                     <label class="custom-file-label"><%- i18n('str', 'choose-file') %></label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|   | ||||
| @@ -13,6 +13,7 @@ require('selectize'); | ||||
| module.exports = Mn.View.extend({ | ||||
|     template:  template, | ||||
|     className: 'modal-dialog', | ||||
|     max_file_size: 5120, | ||||
|  | ||||
|     ui: { | ||||
|         form:                      'form', | ||||
| @@ -24,6 +25,8 @@ module.exports = Mn.View.extend({ | ||||
|         ssl_enabled:               'input[name="ssl_enabled"]', | ||||
|         ssl_options:               '#ssl-options input', | ||||
|         ssl_provider:              'input[name="ssl_provider"]', | ||||
|         other_ssl_certificate:     '#other_ssl_certificate', | ||||
|         other_ssl_certificate_key: '#other_ssl_certificate_key', | ||||
|  | ||||
|         // SSL hiding and showing | ||||
|         all_ssl:         '.letsencrypt-ssl, .other-ssl', | ||||
| @@ -75,21 +78,71 @@ module.exports = Mn.View.extend({ | ||||
|                 data.domain_names = data.domain_names.split(','); | ||||
|             } | ||||
|  | ||||
|             // Process | ||||
|             this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); | ||||
|             let require_ssl_files = typeof data.ssl_enabled !== 'undefined' && data.ssl_enabled && typeof data.ssl_provider !== 'undefined' && data.ssl_provider === 'other'; | ||||
|             let ssl_files         = []; | ||||
|             let method            = App.Api.Nginx.ProxyHosts.create; | ||||
|             let is_new            = true; | ||||
|  | ||||
|             let must_require_ssl_files = require_ssl_files && !view.model.hasSslFiles('other'); | ||||
|  | ||||
|             if (this.model.get('id')) { | ||||
|                 // edit | ||||
|                 is_new  = false; | ||||
|                 method  = App.Api.Nginx.ProxyHosts.update; | ||||
|                 data.id = this.model.get('id'); | ||||
|             } | ||||
|  | ||||
|             // check files are attached | ||||
|             if (require_ssl_files) { | ||||
|                 if (!this.ui.other_ssl_certificate[0].files.length || !this.ui.other_ssl_certificate[0].files[0].size) { | ||||
|                     if (must_require_ssl_files) { | ||||
|                         alert('certificate file is not attached'); | ||||
|                         return; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (this.ui.other_ssl_certificate[0].files[0].size > this.max_file_size) { | ||||
|                         alert('certificate file is too large (> 5kb)'); | ||||
|                         return; | ||||
|                     } | ||||
|                     ssl_files.push({name: 'other_certificate', file: this.ui.other_ssl_certificate[0].files[0]}); | ||||
|                 } | ||||
|  | ||||
|                 if (!this.ui.other_ssl_certificate_key[0].files.length || !this.ui.other_ssl_certificate_key[0].files[0].size) { | ||||
|                     if (must_require_ssl_files) { | ||||
|                         alert('certificate key file is not attached'); | ||||
|                         return; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (this.ui.other_ssl_certificate_key[0].files[0].size > this.max_file_size) { | ||||
|                         alert('certificate key file is too large (> 5kb)'); | ||||
|                         return; | ||||
|                     } | ||||
|                     ssl_files.push({name: 'other_certificate_key', file: this.ui.other_ssl_certificate_key[0].files[0]}); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); | ||||
|             method(data) | ||||
|                 .then(result => { | ||||
|                     view.model.set(result); | ||||
|  | ||||
|                     // Now upload the certs if we need to | ||||
|                     if (ssl_files.length) { | ||||
|                         let form_data = new FormData(); | ||||
|  | ||||
|                         ssl_files.map(function (file) { | ||||
|                             form_data.append(file.name, file.file); | ||||
|                         }); | ||||
|  | ||||
|                         return App.Api.Nginx.ProxyHosts.setCerts(view.model.get('id'), form_data) | ||||
|                             .then(result => { | ||||
|                                 view.model.set('meta', _.assign({}, view.model.get('meta'), result)); | ||||
|                             }); | ||||
|                     } | ||||
|                 }) | ||||
|                 .then(() => { | ||||
|                     App.UI.closeModal(function () { | ||||
|                         if (method === App.Api.Nginx.ProxyHosts.create) { | ||||
|                         if (is_new) { | ||||
|                             App.Controller.showNginxProxy(); | ||||
|                         } | ||||
|                     }); | ||||
|   | ||||
| @@ -19,11 +19,20 @@ const model = Backbone.Model.extend({ | ||||
|             ssl_forced:      false, | ||||
|             caching_enabled: false, | ||||
|             block_exploits:  false, | ||||
|             meta:            [], | ||||
|             meta:            {}, | ||||
|             // The following are expansions: | ||||
|             owner:           null, | ||||
|             access_list:     null | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @param   {String}  type     'letsencrypt' or 'other' | ||||
|      * @returns {Boolean} | ||||
|      */ | ||||
|     hasSslFiles: function (type) { | ||||
|         let meta = this.get('meta'); | ||||
|         return typeof meta[type + '_certificate'] !== 'undefined' && meta[type + '_certificate'] && typeof meta[type + '_certificate_key'] !== 'undefined' && meta[type + '_certificate_key']; | ||||
|     } | ||||
| }); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user