From 5a2548c89df7e6ccfc869cbd49f9a9f8a0a91166 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Sun, 10 Oct 2021 23:49:57 +0200 Subject: [PATCH] WIP: complete control of new passthrough host type --- backend/app.js | 10 ++-- backend/internal/host.js | 11 +++- backend/internal/nginx.js | 2 +- backend/internal/ssl-passthrough-host.js | 52 +++++++------------ .../20211010141200_ssl_passthrough_host.js | 33 +++++++++--- .../routes/api/nginx/ssl_passthrough_hosts.js | 4 +- .../endpoints/ssl-passthrough-hosts.json | 2 +- backend/setup.js | 19 +++---- backend/templates/ssl_passthrough_host.conf | 10 ++-- docker/docker-compose.dev.yml | 4 +- frontend/js/app/api.js | 9 ++++ .../js/app/nginx/ssl-passthrough/form.ejs | 2 +- frontend/js/app/nginx/ssl-passthrough/form.js | 3 -- .../js/app/nginx/ssl-passthrough/main.ejs | 3 ++ frontend/js/app/nginx/ssl-passthrough/main.js | 21 ++++++-- frontend/js/app/ui/menu/main.ejs | 4 ++ frontend/js/app/user/permissions.ejs | 4 +- frontend/js/app/user/permissions.js | 13 ++--- frontend/js/i18n/messages.json | 5 +- 19 files changed, 126 insertions(+), 85 deletions(-) diff --git a/backend/app.js b/backend/app.js index 8f4890c..bbeb1c0 100644 --- a/backend/app.js +++ b/backend/app.js @@ -74,12 +74,10 @@ app.use(function (err, req, res, next) { } // Not every error is worth logging - but this is good for now until it gets annoying. - if (typeof err.stack !== 'undefined' && err.stack) { - if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { - log.debug(err.stack); - } else if (typeof err.public == 'undefined' || !err.public) { - log.warn(err.message); - } + if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { + log.debug(err); + } else if (typeof err.stack !== 'undefined' && err.stack && (typeof err.public == 'undefined' || !err.public)) { + log.warn(err.message); } res diff --git a/backend/internal/host.js b/backend/internal/host.js index f37b943..9c91b41 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -206,14 +206,21 @@ const internalHost = { if (existing_rows && existing_rows.length) { existing_rows.map(function (existing_row) { - existing_row.domain_names.map(function (existing_hostname) { + + function checkHostname(existing_hostname) { // Does this domain match? if (existing_hostname.toLowerCase() === hostname.toLowerCase()) { if (!ignore_id || ignore_id !== existing_row.id) { is_taken = true; } } - }); + } + + if (existing_row.domain_names) { + existing_row.domain_names.map(checkHostname); + } else if (existing_row.domain_name) { + checkHostname(existing_row.domain_name); + } }); } diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 9215df9..2c0c5f4 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -236,8 +236,8 @@ const internalNginx = { host = { all_passthrough_hosts: allHosts.map((host) => { // Replace dots in domain - host.escaped_name = host.domain_name.replace(/\./, '_'); host.forwarding_host = internalNginx.addIpv6Brackets(host.forwarding_host); + return host; }), } } else { diff --git a/backend/internal/ssl-passthrough-host.js b/backend/internal/ssl-passthrough-host.js index a4f0d57..a6b3f15 100644 --- a/backend/internal/ssl-passthrough-host.js +++ b/backend/internal/ssl-passthrough-host.js @@ -19,20 +19,12 @@ const internalPassthroughHost = { create: (access, data) => { return access.can('ssl_passthrough_hosts:create', data) .then(() => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); + // Get the domain name and check it against existing records + return internalHost.isHostnameTaken(data.domain_name) + .then((result) => { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } }); }).then((/*access_data*/) => { data.owner_user_id = access.token.getUserId(1); @@ -57,7 +49,7 @@ const internalPassthroughHost = { // Add to audit log return internalAuditLog.add(access, { action: 'created', - object_type: 'ssl_passthrough_host', + object_type: 'ssl-passthrough-host', object_id: row.id, meta: data }) @@ -76,21 +68,13 @@ const internalPassthroughHost = { update: (access, data) => { return access.can('ssl_passthrough_hosts:update', data.id) .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'ssl_passthrough', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); + // Get the domain name and check it against existing records + if (typeof data.domain_name !== 'undefined') { + return internalHost.isHostnameTaken(data.domain_name, 'ssl_passthrough', data.id) + .then((result) => { + if (result.is_taken) { + throw new error.ValidationError(result.hostname + ' is already in use'); + } }); } }).then((/*access_data*/) => { @@ -116,7 +100,7 @@ const internalPassthroughHost = { // Add to audit log return internalAuditLog.add(access, { action: 'updated', - object_type: 'ssl_passthrough_host', + object_type: 'ssl-passthrough-host', object_id: row.id, meta: data }) @@ -207,7 +191,7 @@ const internalPassthroughHost = { // Add to audit log return internalAuditLog.add(access, { action: 'deleted', - object_type: 'ssl_passthrough_host', + object_type: 'ssl-passthrough-host', object_id: row.id, meta: _.omit(row, omissions()) }); @@ -256,7 +240,7 @@ const internalPassthroughHost = { // Add to audit log return internalAuditLog.add(access, { action: 'enabled', - object_type: 'ssl_passthrough_host', + object_type: 'ssl-passthrough-host', object_id: row.id, meta: _.omit(row, omissions()) }); @@ -305,7 +289,7 @@ const internalPassthroughHost = { // Add to audit log return internalAuditLog.add(access, { action: 'disabled', - object_type: 'ssl_passthrough_host', + object_type: 'ssl-passthrough-host', object_id: row.id, meta: _.omit(row, omissions()) }); diff --git a/backend/migrations/20211010141200_ssl_passthrough_host.js b/backend/migrations/20211010141200_ssl_passthrough_host.js index 9a92442..3253868 100644 --- a/backend/migrations/20211010141200_ssl_passthrough_host.js +++ b/backend/migrations/20211010141200_ssl_passthrough_host.js @@ -20,13 +20,30 @@ exports.up = function (knex/*, Promise*/) { table.integer('owner_user_id').notNull().unsigned(); table.integer('is_deleted').notNull().unsigned().defaultTo(0); table.string('domain_name').notNull(); - table.string('forward_ip').notNull(); + table.string('forwarding_host').notNull(); table.integer('forwarding_port').notNull().unsigned(); + table.integer('enabled').notNull().unsigned().defaultTo(1); table.json('meta').notNull(); + }).then(() => { + logger.info('[' + migrate_name + '] Table created'); }) - .then(() => { - logger.info('[' + migrate_name + '] Table created'); - }); + .then(() => { + return knex.schema.table('user_permission', (table) => { + table.string('ssl_passthrough_hosts').notNull(); + }) + .then(() => { + return knex('user_permission').update('ssl_passthrough_hosts', knex.ref('streams')); + }) + .then(() => { + return knex.schema.alterTable('user_permission', (table) => { + table.string('ssl_passthrough_hosts').notNullable().alter(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] permissions updated'); + }); + }) + ; }; /** @@ -39,8 +56,12 @@ exports.up = function (knex/*, Promise*/) { exports.down = function (knex/*, Promise*/) { logger.info('[' + migrate_name + '] Migrating Down...'); - return knex.schema.dropTable('stream') + return knex.schema.dropTable('stream').then(() => { + return knex.schema.table('user_permission', (table) => { + table.dropColumn('ssl_passthrough_hosts'); + }) + }) .then(function () { - logger.info('[' + migrate_name + '] Table altered'); + logger.info('[' + migrate_name + '] Table altered and permissions updated'); }); }; diff --git a/backend/routes/api/nginx/ssl_passthrough_hosts.js b/backend/routes/api/nginx/ssl_passthrough_hosts.js index 5eb75f7..dfa2eac 100644 --- a/backend/routes/api/nginx/ssl_passthrough_hosts.js +++ b/backend/routes/api/nginx/ssl_passthrough_hosts.js @@ -73,7 +73,7 @@ router * /api/nginx/ssl-passthrough-hosts/123 */ router - .route('/:ssl_passthrough_host_id') + .route('/:host_id') .options((req, res) => { res.sendStatus(204); }) @@ -86,7 +86,7 @@ router */ .get((req, res, next) => { validator({ - required: ['ssl_passthrough_host_id'], + required: ['host_id'], additionalProperties: false, properties: { host_id: { diff --git a/backend/schema/endpoints/ssl-passthrough-hosts.json b/backend/schema/endpoints/ssl-passthrough-hosts.json index 12306d0..5c20602 100644 --- a/backend/schema/endpoints/ssl-passthrough-hosts.json +++ b/backend/schema/endpoints/ssl-passthrough-hosts.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/ssl-passthough-hosts", + "$id": "endpoints/ssl-passthrough-hosts", "title": "SSL Passthrough Hosts", "description": "Endpoints relating to SSL Passthrough Hosts", "stability": "stable", diff --git a/backend/setup.js b/backend/setup.js index 4a2f948..769ca06 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -107,14 +107,15 @@ const setupDefaultUser = () => { }) .then(() => { return userPermissionModel.query().insert({ - user_id: user.id, - visibility: 'all', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage', + user_id: user.id, + visibility: 'all', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + ssl_passthrough_hosts: 'manage', + streams: 'manage', + access_lists: 'manage', + certificates: 'manage', }); }); }) @@ -229,7 +230,7 @@ const setupLogrotation = () => { * @returns {Promise} */ const setupSslPassthrough = () => { - return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}); + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}).then(() => internalNginx.reload()); }; module.exports = function () { diff --git a/backend/templates/ssl_passthrough_host.conf b/backend/templates/ssl_passthrough_host.conf index 9ee872d..6dd2b34 100644 --- a/backend/templates/ssl_passthrough_host.conf +++ b/backend/templates/ssl_passthrough_host.conf @@ -4,16 +4,16 @@ map $ssl_preread_server_name $name { {% for host in all_passthrough_hosts %} -{% if enabled %} - {{ host.domain_name }} ssl_passthrough_{{ host.escaped_name }} +{% if host.enabled %} + {{ host.domain_name }} ssl_passthrough_{{ host.domain_name }}; {% endif %} {% endfor %} default https_default_backend; } {% for host in all_passthrough_hosts %} -{% if enabled %} -upstream ssl_passthrough_{{ host.escaped_name }} { +{% if host.enabled %} +upstream ssl_passthrough_{{ host.domain_name }} { server {{host.forwarding_host}}:{{host.forwarding_port}}; } {% endif %} @@ -34,6 +34,8 @@ server { proxy_pass $name; ssl_preread on; + error_log /data/logs/ssl-passthrough-hosts_error.log warn; + # Custom include /data/nginx/custom/server_ssl_passthrough[.]conf; } \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 4914cd1..4d2e3a1 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -23,7 +23,7 @@ services: DB_MYSQL_USER: "npm" DB_MYSQL_PASSWORD: "npm" DB_MYSQL_NAME: "npm" - ENABLE_SSL_PASSTHROUGH: "true" + # ENABLE_SSL_PASSTHROUGH: "true" # DB_SQLITE_FILE: "/data/database.sqlite" # DISABLE_IPV6: "true" volumes: @@ -41,6 +41,8 @@ services: container_name: npm_db networks: - nginx_proxy_manager + ports: + - 33306:3306 environment: MYSQL_ROOT_PASSWORD: "npm" MYSQL_DATABASE: "npm" diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index af47a13..d689acd 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -516,6 +516,15 @@ module.exports = { }, SslPassthroughHosts: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getFeatureEnabled: function () { + return fetch('get', 'ssl-passthrough-enabled'); + }, + /** * @param {Array} [expand] * @param {String} [query] diff --git a/frontend/js/app/nginx/ssl-passthrough/form.ejs b/frontend/js/app/nginx/ssl-passthrough/form.ejs index 3120002..6ba3586 100644 --- a/frontend/js/app/nginx/ssl-passthrough/form.ejs +++ b/frontend/js/app/nginx/ssl-passthrough/form.ejs @@ -21,7 +21,7 @@
- +
diff --git a/frontend/js/app/nginx/ssl-passthrough/form.js b/frontend/js/app/nginx/ssl-passthrough/form.js index ffaf275..4fea26f 100644 --- a/frontend/js/app/nginx/ssl-passthrough/form.js +++ b/frontend/js/app/nginx/ssl-passthrough/form.js @@ -14,9 +14,7 @@ module.exports = Mn.View.extend({ ui: { form: 'form', forwarding_host: 'input[name="forwarding_host"]', - type_error: '.forward-type-error', buttons: '.modal-footer button', - switches: '.custom-switch-input', cancel: 'button.cancel', save: 'button.save' }, @@ -38,7 +36,6 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); // Manipulate - data.incoming_port = parseInt(data.incoming_port, 10); data.forwarding_port = parseInt(data.forwarding_port, 10); let method = App.Api.Nginx.SslPassthroughHosts.create; diff --git a/frontend/js/app/nginx/ssl-passthrough/main.ejs b/frontend/js/app/nginx/ssl-passthrough/main.ejs index cf29c2d..24adfbf 100644 --- a/frontend/js/app/nginx/ssl-passthrough/main.ejs +++ b/frontend/js/app/nginx/ssl-passthrough/main.ejs @@ -10,6 +10,9 @@
+
+ Disabled +
diff --git a/frontend/js/app/nginx/ssl-passthrough/main.js b/frontend/js/app/nginx/ssl-passthrough/main.js index c441920..af86d91 100644 --- a/frontend/js/app/nginx/ssl-passthrough/main.js +++ b/frontend/js/app/nginx/ssl-passthrough/main.js @@ -11,10 +11,11 @@ module.exports = Mn.View.extend({ template: template, ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' + list_region: '.list-region', + add: '.add-item', + help: '.help', + dimmer: '.dimmer', + disabled_info: '#ssl-passthrough-disabled-info' }, regions: { @@ -39,6 +40,16 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; + view.ui.disabled_info.hide(); + + App.Api.Nginx.SslPassthroughHosts.getFeatureEnabled().then((response) => { + console.debug(response) + if (response.ssl_passthrough_enabled === false) { + view.ui.disabled_info.show(); + } else { + view.ui.disabled_info.hide(); + } + }); App.Api.Nginx.SslPassthroughHosts.getAll(['owner']) .then(response => { @@ -53,7 +64,7 @@ module.exports = Mn.View.extend({ view.showChildView('list_region', new EmptyView({ title: App.i18n('ssl-passthrough-hosts', 'empty'), subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('ssl_passthrough_hosts', 'add') : null, + link: manage ? App.i18n('ssl-passthrough-hosts', 'add') : null, btn_color: 'blue', permission: 'ssl-passthrough-hosts', action: function () { diff --git a/frontend/js/app/ui/menu/main.ejs b/frontend/js/app/ui/menu/main.ejs index 671b4e3..ae45fe5 100644 --- a/frontend/js/app/ui/menu/main.ejs +++ b/frontend/js/app/ui/menu/main.ejs @@ -20,6 +20,10 @@ <%- i18n('streams', 'title') %> <% } %> + <% if (canShow('ssl_passthrough_hosts')) { %> + <%- i18n('ssl-passthrough-hosts', 'title') %> + <% } %> + <% if (canShow('dead_hosts')) { %> <%- i18n('dead-hosts', 'title') %> <% } %> diff --git a/frontend/js/app/user/permissions.ejs b/frontend/js/app/user/permissions.ejs index b616179..592c104 100644 --- a/frontend/js/app/user/permissions.ejs +++ b/frontend/js/app/user/permissions.ejs @@ -31,9 +31,9 @@
<% - var list = ['proxy-hosts', 'redirection-hosts', 'dead-hosts', 'streams', 'access-lists', 'certificates']; + var list = ['proxy-hosts', 'redirection-hosts', 'dead-hosts', 'streams', 'ssl-passthrough-hosts', 'access-lists', 'certificates']; list.map(function(item) { - var perm = item.replace('-', '_'); + var perm = item.replace(/-/g, '_'); %>
diff --git a/frontend/js/app/user/permissions.js b/frontend/js/app/user/permissions.js index af8049c..b03d2db 100644 --- a/frontend/js/app/user/permissions.js +++ b/frontend/js/app/user/permissions.js @@ -29,12 +29,13 @@ module.exports = Mn.View.extend({ if (view.model.isAdmin()) { // Force some attributes for admin data = _.assign({}, data, { - access_lists: 'manage', - dead_hosts: 'manage', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - streams: 'manage', - certificates: 'manage' + access_lists: 'manage', + dead_hosts: 'manage', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + ssl_passthrough_hosts: 'manage', + streams: 'manage', + certificates: 'manage' }); } diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 94a7f28..a774719 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -72,6 +72,7 @@ "enable-ssl": "Enable SSL", "force-ssl": "Force SSL", "http2-support": "HTTP/2 Support", + "domain-name": "Domain Name", "domain-names": "Domain Names", "cert-provider": "Certificate Provider", "block-exploits": "Block Common Exploits", @@ -125,8 +126,8 @@ "forwarding-port": "Forward Port", "delete": "Delete SSL Passthrough Host", "delete-confirm": "Are you sure you want to delete this SSL Passthrough Host?", - "help-title": "What is a SSL Passthrough Host?", - "help-content": "An SSL Passthrough Host will allow you to proxy a server without SSL termination. This means the SSL encryption of the server will be passed right through the proxy, retaining the upstream certificates.\nThough this also means the proxy does not know anything about the traffic, and it just relies on an SSL feature called Server Name Indication, to know where to send this packet. This also means, if the client does not provide this additional information, accessing the site through the proxy won't be possible. However most modern browsers include this information in HTTP requests.\n\nHowever using SSL Passthrough comes with a performance penalty, since all hosts (including normal proxy hosts) now have to pass through this additional step of checking the destination. If you do not need your service to be available on port 443, it is recommended to use a stream host instead." + "help-title": "What is an SSL Passthrough Host?", + "help-content": "An SSL Passthrough Host will allow you to proxy a server without SSL termination. This means the SSL encryption of the server will be passed right through the proxy, retaining the upstream certificate.\n Because of the SSL encryption the proxy does not know anything about the traffic, and it just relies on an SSL feature called Server Name Indication to know where to send this packet. This also means if the client does not provide this additional information, accessing the site through the proxy won't be possible. But most modern browsers include this information in HTTP requests.\n\nDue to nginx constraints using SSL Passthrough comes with a performance penalty for other hosts, since all hosts (including normal proxy hosts) now have to pass through this additional step and basically being proxied twice. If you want to retain the upstream SSL certificate but do not need your service to be available on port 443, it is recommended to use a stream host instead." }, "proxy-hosts": { "title": "Proxy Hosts",