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 8f4890c3..bbeb1c02 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 f37b943d..9c91b416 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 9215df9a..2c0c5f42 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 a4f0d57d..a6b3f157 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 9a92442b..32538686 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 5eb75f71..dfa2eac3 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 12306d08..5c206023 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 4a2f9489..769ca060 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 9ee872d4..6dd2b34b 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 4914cd10..4d2e3a1b 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 af47a133..d689acd3 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 31200028..6ba35865 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 ffaf2755..4fea26f1 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 cf29c2d7..24adfbfd 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 c4419206..af86d91f 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 671b4e3b..ae45fe57 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 b6161796..592c104b 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 af8049ce..b03d2db6 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 94a7f280..a7747197 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",