From 5b1f0cead1e763dcde527cd23c54854cefd87417 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Sun, 10 Oct 2021 23:49:07 +0200 Subject: [PATCH 1/6] WIP: started adding new host type ssl passthrough --- backend/internal/host.js | 31 +- backend/internal/nginx.js | 75 +++- backend/internal/ssl-passthrough-host.js | 381 ++++++++++++++++++ backend/internal/user.js | 17 +- .../access/ssl_passthrough_hosts-create.json | 23 ++ .../access/ssl_passthrough_hosts-delete.json | 23 ++ .../lib/access/ssl_passthrough_hosts-get.json | 23 ++ .../access/ssl_passthrough_hosts-list.json | 23 ++ .../access/ssl_passthrough_hosts-update.json | 23 ++ .../20211010141200_ssl_passthrough_host.js | 46 +++ backend/models/ssl_passthrough_host.js | 56 +++ backend/routes/api/main.js | 15 +- .../routes/api/nginx/ssl_passthrough_hosts.js | 196 +++++++++ .../endpoints/ssl-passthrough-hosts.json | 208 ++++++++++ backend/schema/index.json | 3 + backend/setup.js | 37 +- backend/templates/ssl_passthrough_host.conf | 39 ++ docker/docker-compose.dev.yml | 2 + docker/rootfs/etc/nginx/nginx.conf | 1 + docker/rootfs/etc/services.d/nginx/run | 1 + frontend/js/app/api.js | 61 +++ frontend/js/app/controller.js | 40 ++ .../js/app/nginx/ssl-passthrough/delete.ejs | 19 + .../js/app/nginx/ssl-passthrough/delete.js | 32 ++ .../js/app/nginx/ssl-passthrough/form.ejs | 34 ++ frontend/js/app/nginx/ssl-passthrough/form.js | 77 ++++ .../app/nginx/ssl-passthrough/list/item.ejs | 43 ++ .../js/app/nginx/ssl-passthrough/list/item.js | 54 +++ .../app/nginx/ssl-passthrough/list/main.ejs | 12 + .../js/app/nginx/ssl-passthrough/list/main.js | 32 ++ .../js/app/nginx/ssl-passthrough/main.ejs | 20 + frontend/js/app/nginx/ssl-passthrough/main.js | 81 ++++ frontend/js/app/router.js | 23 +- frontend/js/i18n/messages.json | 14 + frontend/js/models/ssl-passthrough-host.js | 27 ++ 35 files changed, 1743 insertions(+), 49 deletions(-) create mode 100644 backend/internal/ssl-passthrough-host.js create mode 100644 backend/lib/access/ssl_passthrough_hosts-create.json create mode 100644 backend/lib/access/ssl_passthrough_hosts-delete.json create mode 100644 backend/lib/access/ssl_passthrough_hosts-get.json create mode 100644 backend/lib/access/ssl_passthrough_hosts-list.json create mode 100644 backend/lib/access/ssl_passthrough_hosts-update.json create mode 100644 backend/migrations/20211010141200_ssl_passthrough_host.js create mode 100644 backend/models/ssl_passthrough_host.js create mode 100644 backend/routes/api/nginx/ssl_passthrough_hosts.js create mode 100644 backend/schema/endpoints/ssl-passthrough-hosts.json create mode 100644 backend/templates/ssl_passthrough_host.conf create mode 100644 frontend/js/app/nginx/ssl-passthrough/delete.ejs create mode 100644 frontend/js/app/nginx/ssl-passthrough/delete.js create mode 100644 frontend/js/app/nginx/ssl-passthrough/form.ejs create mode 100644 frontend/js/app/nginx/ssl-passthrough/form.js create mode 100644 frontend/js/app/nginx/ssl-passthrough/list/item.ejs create mode 100644 frontend/js/app/nginx/ssl-passthrough/list/item.js create mode 100644 frontend/js/app/nginx/ssl-passthrough/list/main.ejs create mode 100644 frontend/js/app/nginx/ssl-passthrough/list/main.js create mode 100644 frontend/js/app/nginx/ssl-passthrough/main.ejs create mode 100644 frontend/js/app/nginx/ssl-passthrough/main.js create mode 100644 frontend/js/models/ssl-passthrough-host.js diff --git a/backend/internal/host.js b/backend/internal/host.js index 58e1d09a4..f37b943d7 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -1,7 +1,8 @@ -const _ = require('lodash'); -const proxyHostModel = require('../models/proxy_host'); -const redirectionHostModel = require('../models/redirection_host'); -const deadHostModel = require('../models/dead_host'); +const _ = require('lodash'); +const proxyHostModel = require('../models/proxy_host'); +const redirectionHostModel = require('../models/redirection_host'); +const deadHostModel = require('../models/dead_host'); +const sslPassthroughHostModel = require('../models/ssl_passthrough_host'); const internalHost = { @@ -81,6 +82,9 @@ const internalHost = { .query() .where('is_deleted', 0), deadHostModel + .query() + .where('is_deleted', 0), + sslPassthroughHostModel .query() .where('is_deleted', 0) ]; @@ -112,6 +116,12 @@ const internalHost = { response_object.total_count += response_object.dead_hosts.length; } + if (promises_results[3]) { + // SSL Passthrough Hosts + response_object.ssl_passthrough_hosts = internalHost._getHostsWithDomains(promises_results[3], domain_names); + response_object.total_count += response_object.ssl_passthrough_hosts.length; + } + return response_object; }); }, @@ -137,7 +147,11 @@ const internalHost = { deadHostModel .query() .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%') + .andWhere('domain_names', 'like', '%' + hostname + '%'), + sslPassthroughHostModel + .query() + .where('is_deleted', 0) + .andWhere('domain_name', '=', hostname), ]; return Promise.all(promises) @@ -165,6 +179,13 @@ const internalHost = { } } + if (promises_results[3]) { + // SSL Passthrough Hosts + if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[3], ignore_type === 'ssl_passthrough' && ignore_id ? ignore_id : 0)) { + is_taken = true; + } + } + return { hostname: hostname, is_taken: is_taken diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 52bdd66dc..9215df9a4 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -1,10 +1,11 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); -const { Liquid } = require('liquidjs'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; +const _ = require('lodash'); +const fs = require('fs'); +const logger = require('../logger').nginx; +const utils = require('../lib/utils'); +const error = require('../lib/error'); +const { Liquid } = require('liquidjs'); +const passthroughHostModel = require('../models/ssl_passthrough_host'); +const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; const internalNginx = { @@ -44,12 +45,21 @@ const internalNginx = { nginx_err: null }); + if(host_type === 'ssl_passthrough_host'){ + return passthroughHostModel + .query() + .patch({ + meta: combined_meta + }); + } + return model .query() .where('id', host.id) .patch({ meta: combined_meta }); + }) .catch((err) => { // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. @@ -125,6 +135,8 @@ const internalNginx = { if (host_type === 'default') { return '/data/nginx/default_host/site.conf'; + } else if (host_type === 'ssl_passthrough_host') { + return '/data/nginx/ssl_passthrough_host/hosts.conf'; } return '/data/nginx/' + host_type + '/' + host_id + '.conf'; @@ -199,7 +211,7 @@ const internalNginx = { root: __dirname + '/../templates/' }); - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { let template = null; let filename = internalNginx.getConfigName(host_type, host.id); @@ -214,7 +226,25 @@ const internalNginx = { let origLocations; // Manipulate the data a bit before sending it to the template - if (host_type !== 'default') { + if (host_type === 'ssl_passthrough_host') { + if(internalNginx.sslPassthroughEnabled()){ + const allHosts = await passthroughHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']); + 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); + }), + } + } else { + internalNginx.deleteConfig(host_type, host) + } + + } else if (host_type !== 'default') { host.use_default_location = true; if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); @@ -429,6 +459,33 @@ const internalNginx = { } return true; + }, + + /** + * @returns {boolean} + */ + sslPassthroughEnabled: function () { + if (typeof process.env.ENABLE_SSL_PASSTHROUGH !== 'undefined') { + const enabled = process.env.ENABLE_SSL_PASSTHROUGH.toLowerCase(); + return (enabled === 'on' || enabled === 'true' || enabled === '1' || enabled === 'yes'); + } + + return true; + }, + + /** + * Helper function to add brackets to an IP if it is IPv6 + * @returns {string} + */ + addIpv6Brackets: function (ip) { + // Only run check if ipv6 is enabled + if (internalNginx.ipv6Enabled()) { + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/gi; + if(ipv6Regex.test(ip)){ + return `[${ip}]` + } + } + return ip; } }; diff --git a/backend/internal/ssl-passthrough-host.js b/backend/internal/ssl-passthrough-host.js new file mode 100644 index 000000000..a4f0d57de --- /dev/null +++ b/backend/internal/ssl-passthrough-host.js @@ -0,0 +1,381 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const passthroughHostModel = require('../models/ssl_passthrough_host'); +const internalHost = require('./host'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); + +function omissions () { + return ['is_deleted']; +} + +const internalPassthroughHost = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + 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'); + } + }); + }); + }).then((/*access_data*/) => { + data.owner_user_id = access.token.getUserId(1); + + if (typeof data.meta === 'undefined') { + data.meta = {}; + } + + return passthroughHostModel + .query() + .omit(omissions()) + .insertAndFetch(data); + }) + .then((row) => { + // Configure nginx + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}) + .then(() => { + return internalPassthroughHost.get(access, {id: row.id, expand: ['owner']}); + }); + }) + .then((row) => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'ssl_passthrough_host', + object_id: row.id, + meta: data + }) + .then(() => { + return row; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @return {Promise} + */ + 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'); + } + }); + }); + } + }).then((/*access_data*/) => { + return internalPassthroughHost.get(access, {id: data.id}); + }) + .then((row) => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('SSL Passthrough Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + + return passthroughHostModel + .query() + .omit(omissions()) + .patchAndFetchById(row.id, data) + .then(() => { + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}) + .then(() => { + return internalPassthroughHost.get(access, {id: row.id, expand: ['owner']}); + }); + }) + .then((saved_row) => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'ssl_passthrough_host', + object_id: row.id, + meta: data + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + if (typeof data === 'undefined') { + data = {}; + } + + return access.can('ssl_passthrough_hosts:get', data.id) + .then((access_data) => { + let query = passthroughHostModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowEager('[owner]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + query.omit(data.omit); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.eager('[' + data.expand.join(', ') + ']'); + } + + return query; + }) + .then((row) => { + if (row) { + return _.omit(row, omissions()); + } else { + throw new error.ItemNotFoundError(data.id); + } + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access.can('ssl_passthrough_hosts:delete', data.id) + .then(() => { + return internalPassthroughHost.get(access, {id: data.id}); + }) + .then((row) => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } + + return passthroughHostModel + .query() + .where('id', row.id) + .patch({ + is_deleted: 1 + }) + .then(() => { + // Update Nginx Config + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}) + .then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'deleted', + object_type: 'ssl_passthrough_host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + enable: (access, data) => { + return access.can('ssl_passthrough_hosts:update', data.id) + .then(() => { + return internalPassthroughHost.get(access, { + id: data.id, + expand: ['owner'] + }); + }) + .then((row) => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } else if (row.enabled) { + throw new error.ValidationError('Host is already enabled'); + } + + row.enabled = 1; + + return passthroughHostModel + .query() + .where('id', row.id) + .patch({ + enabled: 1 + }) + .then(() => { + // Configure nginx + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'enabled', + object_type: 'ssl_passthrough_host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + disable: (access, data) => { + return access.can('ssl_passthrough_hosts:update', data.id) + .then(() => { + return internalPassthroughHost.get(access, {id: data.id}); + }) + .then((row) => { + if (!row) { + throw new error.ItemNotFoundError(data.id); + } else if (!row.enabled) { + throw new error.ValidationError('Host is already disabled'); + } + + row.enabled = 0; + + return passthroughHostModel + .query() + .where('id', row.id) + .patch({ + enabled: 0 + }) + .then(() => { + // Update Nginx Config + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}) + .then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'disabled', + object_type: 'ssl_passthrough_host', + object_id: row.id, + meta: _.omit(row, omissions()) + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All SSL Passthrough Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('ssl_passthrough_hosts:list') + .then((access_data) => { + let query = passthroughHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']) + .allowEager('[owner]') + .orderBy('domain_name', 'ASC'); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof search_query === 'string') { + query.where(function () { + this.where('domain_name', 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.eager('[' + expand.join(', ') + ']'); + } + + return query; + }); + }, + + /** + * Report use + * + * @param {Number} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + let query = passthroughHostModel + .query() + .count('id as count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then((row) => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalPassthroughHost; diff --git a/backend/internal/user.js b/backend/internal/user.js index 2e2d8abf6..7ad4c4ead 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -62,14 +62,15 @@ const internalUser = { return userPermissionModel .query() .insert({ - user_id: user.id, - visibility: is_admin ? 'all' : 'user', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage' + user_id: user.id, + visibility: is_admin ? 'all' : 'user', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + ssl_passthrough_hosts: 'manage', + streams: 'manage', + access_lists: 'manage', + certificates: 'manage' }) .then(() => { return internalUser.get(access, {id: user.id, expand: ['permissions']}); diff --git a/backend/lib/access/ssl_passthrough_hosts-create.json b/backend/lib/access/ssl_passthrough_hosts-create.json new file mode 100644 index 000000000..63f278b84 --- /dev/null +++ b/backend/lib/access/ssl_passthrough_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_ssl_passthrough_hosts", "roles"], + "properties": { + "permission_ssl_passthrough_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/ssl_passthrough_hosts-delete.json b/backend/lib/access/ssl_passthrough_hosts-delete.json new file mode 100644 index 000000000..63f278b84 --- /dev/null +++ b/backend/lib/access/ssl_passthrough_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_ssl_passthrough_hosts", "roles"], + "properties": { + "permission_ssl_passthrough_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/ssl_passthrough_hosts-get.json b/backend/lib/access/ssl_passthrough_hosts-get.json new file mode 100644 index 000000000..ab0ec15e6 --- /dev/null +++ b/backend/lib/access/ssl_passthrough_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_ssl_passthrough_hosts", "roles"], + "properties": { + "permission_ssl_passthrough_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/ssl_passthrough_hosts-list.json b/backend/lib/access/ssl_passthrough_hosts-list.json new file mode 100644 index 000000000..ab0ec15e6 --- /dev/null +++ b/backend/lib/access/ssl_passthrough_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_ssl_passthrough_hosts", "roles"], + "properties": { + "permission_ssl_passthrough_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/ssl_passthrough_hosts-update.json b/backend/lib/access/ssl_passthrough_hosts-update.json new file mode 100644 index 000000000..63f278b84 --- /dev/null +++ b/backend/lib/access/ssl_passthrough_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_ssl_passthrough_hosts", "roles"], + "properties": { + "permission_ssl_passthrough_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/migrations/20211010141200_ssl_passthrough_host.js b/backend/migrations/20211010141200_ssl_passthrough_host.js new file mode 100644 index 000000000..9a92442b7 --- /dev/null +++ b/backend/migrations/20211010141200_ssl_passthrough_host.js @@ -0,0 +1,46 @@ +const migrate_name = 'ssl_passthrough_host'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.createTable('ssl_passthrough_host', (table) => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + 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.integer('forwarding_port').notNull().unsigned(); + table.json('meta').notNull(); + }) + .then(() => { + logger.info('[' + migrate_name + '] Table created'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.dropTable('stream') + .then(function () { + logger.info('[' + migrate_name + '] Table altered'); + }); +}; diff --git a/backend/models/ssl_passthrough_host.js b/backend/models/ssl_passthrough_host.js new file mode 100644 index 000000000..658754606 --- /dev/null +++ b/backend/models/ssl_passthrough_host.js @@ -0,0 +1,56 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const Model = require('objection').Model; +const User = require('./user'); +const now = require('./now_helper'); + +Model.knex(db); + +class SslPassthrougHost extends Model { + $beforeInsert () { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === 'undefined') { + this.meta = {}; + } + } + + $beforeUpdate () { + this.modified_on = now(); + } + + static get name () { + return 'SslPassthrougHost'; + } + + static get tableName () { + return 'ssl_passthrough_host'; + } + + static get jsonAttributes () { + return ['meta']; + } + + static get relationMappings () { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: 'ssl_passthrough_host.owner_user_id', + to: 'user.id' + }, + modify: function (qb) { + qb.where('user.is_deleted', 0); + qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); + } + } + }; + } +} + +module.exports = SslPassthrougHost; diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 33cbbc21f..48b095a86 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -1,6 +1,7 @@ -const express = require('express'); -const pjson = require('../../package.json'); -const error = require('../../lib/error'); +const express = require('express'); +const pjson = require('../../package.json'); +const error = require('../../lib/error'); +const internalNginx = require('../../internal/nginx'); let router = express.Router({ caseSensitive: true, @@ -34,10 +35,18 @@ router.use('/settings', require('./settings')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); +router.use('/nginx/ssl-passthrough-hosts', require('./nginx/ssl_passthrough_hosts')); router.use('/nginx/streams', require('./nginx/streams')); router.use('/nginx/access-lists', require('./nginx/access_lists')); router.use('/nginx/certificates', require('./nginx/certificates')); +router.get('/ssl-passthrough-enabled', (req, res/*, next*/) => { + res.status(200).send({ + status: 'OK', + ssl_passthrough_enabled: internalNginx.sslPassthroughEnabled() + }); +}); + /** * API 404 for all other routes * diff --git a/backend/routes/api/nginx/ssl_passthrough_hosts.js b/backend/routes/api/nginx/ssl_passthrough_hosts.js new file mode 100644 index 000000000..5eb75f710 --- /dev/null +++ b/backend/routes/api/nginx/ssl_passthrough_hosts.js @@ -0,0 +1,196 @@ +const express = require('express'); +const validator = require('../../../lib/validator'); +const jwtdecode = require('../../../lib/express/jwt-decode'); +const internalSslPassthrough = require('../../../internal/ssl-passthrough-host'); +const apiValidator = require('../../../lib/validator/api'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/ssl-passthrough-hosts + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/ssl-passthrough-hosts + * + * Retrieve all ssl passthrough hosts + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'definitions#/definitions/expand' + }, + query: { + $ref: 'definitions#/definitions/query' + } + } + }, { + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), + query: (typeof req.query.query === 'string' ? req.query.query : null) + }) + .then((data) => { + return internalSslPassthrough.getAll(res.locals.access, data.expand, data.query); + }) + .then((rows) => { + res.status(200) + .send(rows); + }) + .catch(next); + }) + + /** + * POST /api/nginx/ssl-passthrough-hosts + * + * Create a new ssl passthrough host + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/ssl-passthrough-hosts#/links/1/schema'}, req.body) + .then((payload) => { + return internalSslPassthrough.create(res.locals.access, payload); + }) + .then((result) => { + res.status(201) + .send(result); + }) + .catch(next); + }); + +/** + * Specific ssl passthrough host + * + * /api/nginx/ssl-passthrough-hosts/123 + */ +router + .route('/:ssl_passthrough_host_id') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/ssl-passthrough-hosts/123 + * + * Retrieve a specific ssl passthrough host + */ + .get((req, res, next) => { + validator({ + required: ['ssl_passthrough_host_id'], + additionalProperties: false, + properties: { + host_id: { + $ref: 'definitions#/definitions/id' + }, + expand: { + $ref: 'definitions#/definitions/expand' + } + } + }, { + host_id: req.params.host_id, + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + }) + .then((data) => { + return internalSslPassthrough.get(res.locals.access, { + id: parseInt(data.host_id, 10), + expand: data.expand + }); + }) + .then((row) => { + res.status(200) + .send(row); + }) + .catch(next); + }) + + /** + * PUT /api/nginx/ssl-passthrough-hosts/123 + * + * Update an existing ssl passthrough host + */ + .put((req, res, next) => { + apiValidator({$ref: 'endpoints/ssl-passthrough-hosts#/links/2/schema'}, req.body) + .then((payload) => { + payload.id = parseInt(req.params.host_id, 10); + return internalSslPassthrough.update(res.locals.access, payload); + }) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }) + + /** + * DELETE /api/nginx/ssl-passthrough-hosts/123 + * + * Delete an ssl passthrough host + */ + .delete((req, res, next) => { + internalSslPassthrough.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +/** + * Enable ssl passthrough host + * + * /api/nginx/ssl-passthrough-hosts/123/enable + */ +router + .route('/:host_id/enable') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/ssl-passthrough-hosts/123/enable + */ + .post((req, res, next) => { + internalSslPassthrough.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +/** + * Disable ssl passthrough host + * + * /api/nginx/ssl-passthrough-hosts/123/disable + */ +router + .route('/:host_id/disable') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/ssl-passthrough-hosts/123/disable + */ + .post((req, res, next) => { + internalSslPassthrough.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/schema/endpoints/ssl-passthrough-hosts.json b/backend/schema/endpoints/ssl-passthrough-hosts.json new file mode 100644 index 000000000..12306d08d --- /dev/null +++ b/backend/schema/endpoints/ssl-passthrough-hosts.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "endpoints/ssl-passthough-hosts", + "title": "SSL Passthrough Hosts", + "description": "Endpoints relating to SSL Passthrough Hosts", + "stability": "stable", + "type": "object", + "definitions": { + "id": { + "$ref": "../definitions.json#/definitions/id" + }, + "created_on": { + "$ref": "../definitions.json#/definitions/created_on" + }, + "modified_on": { + "$ref": "../definitions.json#/definitions/modified_on" + }, + "domain_name": { + "$ref": "../definitions.json#/definitions/domain_name" + }, + "forwarding_host": { + "anyOf": [ + { + "$ref": "../definitions.json#/definitions/domain_name" + }, + { + "type": "string", + "format": "ipv4" + }, + { + "type": "string", + "format": "ipv6" + } + ] + }, + "forwarding_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "enabled": { + "$ref": "../definitions.json#/definitions/enabled" + }, + "meta": { + "type": "object" + } + }, + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "created_on": { + "$ref": "#/definitions/created_on" + }, + "modified_on": { + "$ref": "#/definitions/modified_on" + }, + "domain_name": { + "$ref": "#/definitions/domain_name" + }, + "forwarding_host": { + "$ref": "#/definitions/forwarding_host" + }, + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" + }, + "enabled": { + "$ref": "#/definitions/enabled" + }, + "meta": { + "$ref": "#/definitions/meta" + } + }, + "links": [ + { + "title": "List", + "description": "Returns a list of SSL Passthrough Hosts", + "href": "/nginx/ssl-passthrough-hosts", + "access": "private", + "method": "GET", + "rel": "self", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "array", + "items": { + "$ref": "#/properties" + } + } + }, + { + "title": "Create", + "description": "Creates a new SSL Passthrough Host", + "href": "/nginx/ssl-passthrough-hosts", + "access": "private", + "method": "POST", + "rel": "create", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain_name", + "forwarding_host", + "forwarding_port" + ], + "properties": { + "domain_name": { + "$ref": "#/definitions/domain_name" + }, + "forwarding_host": { + "$ref": "#/definitions/forwarding_host" + }, + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Update", + "description": "Updates a existing SSL Passthrough Host", + "href": "/nginx/ssl-passthrough-hosts/{definitions.identity.example}", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "domain_name": { + "$ref": "#/definitions/domain_name" + }, + "forwarding_host": { + "$ref": "#/definitions/forwarding_host" + }, + "forwarding_port": { + "$ref": "#/definitions/forwarding_port" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, + "targetSchema": { + "properties": { + "$ref": "#/properties" + } + } + }, + { + "title": "Delete", + "description": "Deletes a existing SSL Passthrough Host", + "href": "/nginx/ssl-passthrough-hosts/{definitions.identity.example}", + "access": "private", + "method": "DELETE", + "rel": "delete", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + }, + { + "title": "Enable", + "description": "Enables a existing SSL Passthrough Host", + "href": "/nginx/ssl-passthrough-hosts/{definitions.identity.example}/enable", + "access": "private", + "method": "POST", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + }, + { + "title": "Disable", + "description": "Disables a existing SSL Passthrough Host", + "href": "/nginx/ssl-passthrough-hosts/{definitions.identity.example}/disable", + "access": "private", + "method": "POST", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "targetSchema": { + "type": "boolean" + } + } + ] +} diff --git a/backend/schema/index.json b/backend/schema/index.json index 6e7d1c8af..58eb91394 100644 --- a/backend/schema/index.json +++ b/backend/schema/index.json @@ -26,6 +26,9 @@ "dead-hosts": { "$ref": "endpoints/dead-hosts.json" }, + "ssl-passthrough-hosts": { + "$ref": "endpoints/ssl-passthrough-hosts.json" + }, "streams": { "$ref": "endpoints/streams.json" }, diff --git a/backend/setup.js b/backend/setup.js index 4d614baf5..4a2f9489b 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -1,15 +1,17 @@ -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); -const logger = require('./logger').setup; -const certificateModel = require('./models/certificate'); -const userModel = require('./models/user'); -const userPermissionModel = require('./models/user_permission'); -const utils = require('./lib/utils'); -const authModel = require('./models/auth'); -const settingModel = require('./models/setting'); -const dns_plugins = require('./global/certbot-dns-plugins'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; +const fs = require('fs'); +const NodeRSA = require('node-rsa'); +const config = require('config'); +const logger = require('./logger').setup; +const certificateModel = require('./models/certificate'); +const userModel = require('./models/user'); +const userPermissionModel = require('./models/user_permission'); +const utils = require('./lib/utils'); +const authModel = require('./models/auth'); +const settingModel = require('./models/setting'); +const passthroughHostModel = require('./models/ssl_passthrough_host'); +const dns_plugins = require('./global/certbot-dns-plugins'); +const internalNginx = require('./internal/nginx'); +const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; /** * Creates a new JWT RSA Keypair if not alread set on the config @@ -222,10 +224,19 @@ const setupLogrotation = () => { return runLogrotate(); }; +/** + * Makes sure the ssl passthrough option is reflected in the nginx config + * @returns {Promise} + */ +const setupSslPassthrough = () => { + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}); +}; + module.exports = function () { return setupJwt() .then(setupDefaultUser) .then(setupDefaultSettings) .then(setupCertbotPlugins) - .then(setupLogrotation); + .then(setupLogrotation) + .then(setupSslPassthrough); }; diff --git a/backend/templates/ssl_passthrough_host.conf b/backend/templates/ssl_passthrough_host.conf new file mode 100644 index 000000000..9ee872d41 --- /dev/null +++ b/backend/templates/ssl_passthrough_host.conf @@ -0,0 +1,39 @@ +# ------------------------------------------------------------ +# SSL Passthrough hosts +# ------------------------------------------------------------ + +map $ssl_preread_server_name $name { +{% for host in all_passthrough_hosts %} +{% if enabled %} + {{ host.domain_name }} ssl_passthrough_{{ host.escaped_name }} +{% endif %} +{% endfor %} + default https_default_backend; +} + +{% for host in all_passthrough_hosts %} +{% if enabled %} +upstream ssl_passthrough_{{ host.escaped_name }} { + server {{host.forwarding_host}}:{{host.forwarding_port}}; +} +{% endif %} +{% endfor %} + +upstream https_default_backend { + server 127.0.0.1:443; +} + +server { + listen 444; +{% if ipv6 -%} + listen [::]:444; +{% else -%} + #listen [::]:444; +{% endif %} + + proxy_pass $name; + ssl_preread on; + + # 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 99f262f12..4914cd101 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -11,6 +11,7 @@ services: - 3080:80 - 3081:81 - 3443:443 + - 3444:444 networks: - nginx_proxy_manager environment: @@ -22,6 +23,7 @@ services: DB_MYSQL_USER: "npm" DB_MYSQL_PASSWORD: "npm" DB_MYSQL_NAME: "npm" + ENABLE_SSL_PASSTHROUGH: "true" # DB_SQLITE_FILE: "/data/database.sqlite" # DISABLE_IPV6: "true" volumes: diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 4d5ee9017..4b9fef5ec 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -85,6 +85,7 @@ http { stream { # Files generated by NPM + include /data/nginx/ssl_passthrough_host/hosts.conf; include /data/nginx/stream/*.conf; # Custom diff --git a/docker/rootfs/etc/services.d/nginx/run b/docker/rootfs/etc/services.d/nginx/run index fe6ea44b3..bc5d23f11 100755 --- a/docker/rootfs/etc/services.d/nginx/run +++ b/docker/rootfs/etc/services.d/nginx/run @@ -12,6 +12,7 @@ mkdir -p /tmp/nginx/body \ /data/nginx/default_www \ /data/nginx/proxy_host \ /data/nginx/redirection_host \ + /data/nginx/ssl_passthrough_host \ /data/nginx/stream \ /data/nginx/dead_host \ /data/nginx/temp \ diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index 2511a7895..af47a1334 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -515,6 +515,67 @@ module.exports = { } }, + SslPassthroughHosts: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('nginx/ssl-passthrough-hosts', expand, query); + }, + + /** + * @param {Object} data + */ + create: function (data) { + return fetch('post', 'nginx/ssl-passthrough-hosts', data); + }, + + /** + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + update: function (data) { + let id = data.id; + delete data.id; + return fetch('put', 'nginx/ssl-passthrough-hosts/' + id, data); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + delete: function (id) { + return fetch('delete', 'nginx/ssl-passthrough-hosts/' + id); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + get: function (id) { + return fetch('get', 'nginx/ssl-passthrough-hosts/' + id); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + enable: function (id) { + return fetch('post', 'nginx/ssl-passthrough-hosts/' + id + '/enable'); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + disable: function (id) { + return fetch('post', 'nginx/ssl-passthrough-hosts/' + id + '/disable'); + } + }, + DeadHosts: { /** * @param {Array} [expand] diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js index 902659be9..abac53ad4 100644 --- a/frontend/js/app/controller.js +++ b/frontend/js/app/controller.js @@ -221,6 +221,46 @@ module.exports = { } }, + /** + * Nginx SSL Passthrough Hosts + */ + showNginxSslPassthrough: function () { + if (Cache.User.isAdmin() || Cache.User.canView('ssl_passthrough_hosts')) { + let controller = this; + + require(['./main', './nginx/ssl-passthrough/main'], (App, View) => { + controller.navigate('/nginx/ssl-passthrough'); + App.UI.showAppContent(new View()); + }); + } + }, + + /** + * SSL Passthrough Hosts Form + * + * @param [model] + */ + showNginxSslPassthroughForm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('ssl_passthrough_hosts')) { + require(['./main', './nginx/ssl-passthrough/form'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + + /** + * SSL Passthrough Hosts Delete Confirm + * + * @param model + */ + showNginxSslPassthroughConfirm: function (model) { + if (Cache.User.isAdmin() || Cache.User.canManage('ssl_passthrough_hosts')) { + require(['./main', './nginx/ssl-passthrough/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Nginx Dead Hosts */ diff --git a/frontend/js/app/nginx/ssl-passthrough/delete.ejs b/frontend/js/app/nginx/ssl-passthrough/delete.ejs new file mode 100644 index 000000000..d4ffdd072 --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/delete.ejs @@ -0,0 +1,19 @@ + diff --git a/frontend/js/app/nginx/ssl-passthrough/delete.js b/frontend/js/app/nginx/ssl-passthrough/delete.js new file mode 100644 index 000000000..26cf1920f --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/delete.js @@ -0,0 +1,32 @@ +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./delete.ejs'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + App.Api.Nginx.SslPassthroughHosts.delete(this.model.get('id')) + .then(() => { + App.Controller.showNginxSslPassthrough(); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + } +}); diff --git a/frontend/js/app/nginx/ssl-passthrough/form.ejs b/frontend/js/app/nginx/ssl-passthrough/form.ejs new file mode 100644 index 000000000..312000283 --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/form.ejs @@ -0,0 +1,34 @@ + diff --git a/frontend/js/app/nginx/ssl-passthrough/form.js b/frontend/js/app/nginx/ssl-passthrough/form.js new file mode 100644 index 000000000..ffaf27558 --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/form.js @@ -0,0 +1,77 @@ +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const SslPassthroughModel = require('../../../models/ssl-passthrough-host'); +const template = require('./form.ejs'); + +require('jquery-serializejson'); +require('jquery-mask-plugin'); +require('selectize'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + 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' + }, + + events: { + 'change @ui.switches': function () { + this.ui.type_error.hide(); + }, + + 'click @ui.save': function (e) { + e.preventDefault(); + + if (!this.ui.form[0].checkValidity()) { + $('').hide().appendTo(this.ui.form).click().remove(); + return; + } + + let view = this; + 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; + let is_new = true; + + if (this.model.get('id')) { + // edit + is_new = false; + method = App.Api.Nginx.SslPassthroughHosts.update; + data.id = this.model.get('id'); + } + + this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + method(data) + .then(result => { + view.model.set(result); + + App.UI.closeModal(function () { + if (is_new) { + App.Controller.showNginxSslPassthrough(); + } + }); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + }, + + initialize: function (options) { + if (typeof options.model === 'undefined' || !options.model) { + this.model = new SslPassthroughModel.Model(); + } + } +}); diff --git a/frontend/js/app/nginx/ssl-passthrough/list/item.ejs b/frontend/js/app/nginx/ssl-passthrough/list/item.ejs new file mode 100644 index 000000000..12c9c374e --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/list/item.ejs @@ -0,0 +1,43 @@ + +
+ +
+ + +
+ <%- domain_name %> +
+
+ <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> +
+ + +
<%- forwarding_host %>:<%- forwarding_port %>
+ + + <% + var o = isOnline(); + if (!enabled) { %> + <%- i18n('str', 'disabled') %> + <% } else if (o === true) { %> + <%- i18n('str', 'online') %> + <% } else if (o === false) { %> + <%- i18n('str', 'offline') %> + <% } else { %> + <%- i18n('str', 'unknown') %> + <% } %> + +<% if (canManage) { %> + + + +<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/ssl-passthrough/list/item.js b/frontend/js/app/nginx/ssl-passthrough/list/item.js new file mode 100644 index 000000000..b04500b7b --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/list/item.js @@ -0,0 +1,54 @@ +const Mn = require('backbone.marionette'); +const App = require('../../../main'); +const template = require('./item.ejs'); + +module.exports = Mn.View.extend({ + template: template, + tagName: 'tr', + + ui: { + able: 'a.able', + edit: 'a.edit', + delete: 'a.delete' + }, + + events: { + 'click @ui.able': function (e) { + e.preventDefault(); + let id = this.model.get('id'); + App.Api.Nginx.SslPassthroughHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) + .then(() => { + return App.Api.Nginx.SslPassthroughHosts.get(id) + .then(row => { + this.model.set(row); + }); + }); + }, + + 'click @ui.edit': function (e) { + e.preventDefault(); + App.Controller.showNginxSslPassthroughForm(this.model); + }, + + 'click @ui.delete': function (e) { + e.preventDefault(); + App.Controller.showNginxSslPassthroughDeleteConfirm(this.model); + } + }, + + templateContext: { + canManage: App.Cache.User.canManage('ssl_passthrough_hosts'), + + isOnline: function () { + return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; + }, + + getOfflineError: function () { + return this.meta.nginx_err || ''; + } + }, + + initialize: function () { + this.listenTo(this.model, 'change', this.render); + } +}); diff --git a/frontend/js/app/nginx/ssl-passthrough/list/main.ejs b/frontend/js/app/nginx/ssl-passthrough/list/main.ejs new file mode 100644 index 000000000..79268f06d --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/list/main.ejs @@ -0,0 +1,12 @@ + +   + <%- i18n('ssl-passthrough-hosts', 'domain-name') %> + <%- i18n('str', 'destination') %> + <%- i18n('str', 'status') %> + <% if (canManage) { %> +   + <% } %> + + + + diff --git a/frontend/js/app/nginx/ssl-passthrough/list/main.js b/frontend/js/app/nginx/ssl-passthrough/list/main.js new file mode 100644 index 000000000..0c82d9f6b --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/list/main.js @@ -0,0 +1,32 @@ +const Mn = require('backbone.marionette'); +const App = require('../../../main'); +const ItemView = require('./item'); +const template = require('./main.ejs'); + +const TableBody = Mn.CollectionView.extend({ + tagName: 'tbody', + childView: ItemView +}); + +module.exports = Mn.View.extend({ + tagName: 'table', + className: 'table table-hover table-outline table-vcenter card-table', + template: template, + + regions: { + body: { + el: 'tbody', + replaceElement: true + } + }, + + templateContext: { + canManage: App.Cache.User.canManage('ssl_passthrough_hosts') + }, + + onRender: function () { + this.showChildView('body', new TableBody({ + collection: this.collection + })); + } +}); diff --git a/frontend/js/app/nginx/ssl-passthrough/main.ejs b/frontend/js/app/nginx/ssl-passthrough/main.ejs new file mode 100644 index 000000000..cf29c2d7d --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/main.ejs @@ -0,0 +1,20 @@ +
+
+
+

<%- i18n('ssl-passthrough-hosts', 'title') %>

+
+ + <% if (showAddButton) { %> + <%- i18n('ssl-passthrough-hosts', 'add') %> + <% } %> +
+
+
+
+
+
+ +
+
+
+
diff --git a/frontend/js/app/nginx/ssl-passthrough/main.js b/frontend/js/app/nginx/ssl-passthrough/main.js new file mode 100644 index 000000000..c4419206b --- /dev/null +++ b/frontend/js/app/nginx/ssl-passthrough/main.js @@ -0,0 +1,81 @@ +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const SslPassthroughModel = require('../../../models/ssl-passthrough-host'); +const ListView = require('./list/main'); +const ErrorView = require('../../error/main'); +const EmptyView = require('../../empty/main'); +const template = require('./main.ejs'); + +module.exports = Mn.View.extend({ + id: 'nginx-ssl-passthrough', + template: template, + + ui: { + list_region: '.list-region', + add: '.add-item', + help: '.help', + dimmer: '.dimmer' + }, + + regions: { + list_region: '@ui.list_region' + }, + + events: { + 'click @ui.add': function (e) { + e.preventDefault(); + App.Controller.showNginxSslPassthroughForm(); + }, + + 'click @ui.help': function (e) { + e.preventDefault(); + App.Controller.showHelp(App.i18n('ssl-passthrough-hosts', 'help-title'), App.i18n('ssl-passthrough-hosts', 'help-content')); + } + }, + + templateContext: { + showAddButton: App.Cache.User.canManage('ssl_passthrough_hosts') + }, + + onRender: function () { + let view = this; + + App.Api.Nginx.SslPassthroughHosts.getAll(['owner']) + .then(response => { + if (!view.isDestroyed()) { + if (response && response.length) { + view.showChildView('list_region', new ListView({ + collection: new SslPassthroughModel.Collection(response) + })); + } else { + let manage = App.Cache.User.canManage('ssl_passthrough_hosts'); + + 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, + btn_color: 'blue', + permission: 'ssl-passthrough-hosts', + action: function () { + App.Controller.showNginxSslPassthroughForm(); + } + })); + } + } + }) + .catch(err => { + view.showChildView('list_region', new ErrorView({ + code: err.code, + message: err.message, + retry: function () { + App.Controller.showNginxSslPassthrough(); + } + })); + + console.error(err); + }) + .then(() => { + view.ui.dimmer.removeClass('active'); + }); + } +}); diff --git a/frontend/js/app/router.js b/frontend/js/app/router.js index a036bfc57..5407277bc 100644 --- a/frontend/js/app/router.js +++ b/frontend/js/app/router.js @@ -4,16 +4,17 @@ const Controller = require('./controller'); module.exports = AppRouter.default.extend({ controller: Controller, appRoutes: { - users: 'showUsers', - logout: 'logout', - 'nginx/proxy': 'showNginxProxy', - 'nginx/redirection': 'showNginxRedirection', - 'nginx/404': 'showNginxDead', - 'nginx/stream': 'showNginxStream', - 'nginx/access': 'showNginxAccess', - 'nginx/certificates': 'showNginxCertificates', - 'audit-log': 'showAuditLog', - 'settings': 'showSettings', - '*default': 'showDashboard' + users: 'showUsers', + logout: 'logout', + 'nginx/proxy': 'showNginxProxy', + 'nginx/redirection': 'showNginxRedirection', + 'nginx/404': 'showNginxDead', + 'nginx/ssl-passthrough': 'showNginxSslPassthrough', + 'nginx/stream': 'showNginxStream', + 'nginx/access': 'showNginxAccess', + 'nginx/certificates': 'showNginxCertificates', + 'audit-log': 'showAuditLog', + 'settings': 'showSettings', + '*default': 'showDashboard' } }); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 1e00ef7ca..94a7f2801 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -115,6 +115,19 @@ "processing-info": "Processing... This might take a few minutes.", "passphrase-protection-support-info": "Key files protected with a passphrase are not supported." }, + "ssl-passthrough-hosts": { + "title": "SSL Passthrough Hosts", + "empty": "There are no SSL Passthrough Hosts", + "add": "Add SSL Passthrough Hosts", + "form-title": "{id, select, undefined{New} other{Edit}} SSL Passthrough Host", + "domain-name": "Domain Name", + "forwarding-host": "Forward Host", + "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." + }, "proxy-hosts": { "title": "Proxy Hosts", "empty": "There are no Proxy Hosts", @@ -248,6 +261,7 @@ "proxy-host": "Proxy Host", "redirection-host": "Redirection Host", "dead-host": "404 Host", + "ssl-passthrough-host": "SSL Passthrough Host", "stream": "Stream", "user": "User", "certificate": "Certificate", diff --git a/frontend/js/models/ssl-passthrough-host.js b/frontend/js/models/ssl-passthrough-host.js new file mode 100644 index 000000000..e41cb6193 --- /dev/null +++ b/frontend/js/models/ssl-passthrough-host.js @@ -0,0 +1,27 @@ +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function () { + return { + id: undefined, + created_on: null, + modified_on: null, + domain_name: null, + forwarding_host: null, + forwarding_port: null, + enabled: true, + meta: {}, + // The following are expansions: + owner: null + }; + } +}); + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model: model + }) +}; From 5a2548c89df7e6ccfc869cbd49f9a9f8a0a91166 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Sun, 10 Oct 2021 23:49:57 +0200 Subject: [PATCH 2/6] 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 8f4890c3d..bbeb1c02d 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 f37b943d7..9c91b4169 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 9215df9a4..2c0c5f42f 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 a4f0d57de..a6b3f1576 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 9a92442b7..325386861 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 5eb75f710..dfa2eac30 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 12306d08d..5c206023d 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 4a2f9489b..769ca0600 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 9ee872d41..6dd2b34b5 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 4914cd101..4d2e3a1b7 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 af47a1334..d689acd3b 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 312000283..6ba358651 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 ffaf27558..4fea26f1c 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 cf29c2d7d..24adfbfd9 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 c4419206b..af86d91fd 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 671b4e3be..ae45fe570 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 b61617960..592c104bd 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 af8049ce8..b03d2db62 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 94a7f2801..a77471971 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", From 02d3093d88e828f0014399feadcad1ee1b1ba80c Mon Sep 17 00:00:00 2001 From: chaptergy Date: Tue, 12 Oct 2021 15:25:46 +0200 Subject: [PATCH 3/6] Finalizes SSL Passthrough hosts --- backend/internal/nginx.js | 53 ++++++++++++++++--- backend/routes/api/main.js | 2 +- backend/setup.js | 2 +- docker/rootfs/etc/nginx/nginx.conf | 2 +- docs/advanced-config/README.md | 25 +++++++++ frontend/js/app/controller.js | 2 +- .../js/app/nginx/ssl-passthrough/form.ejs | 2 +- frontend/js/app/nginx/ssl-passthrough/form.js | 8 +-- .../app/nginx/ssl-passthrough/list/main.ejs | 2 +- .../js/app/nginx/ssl-passthrough/main.ejs | 8 +-- frontend/js/app/nginx/ssl-passthrough/main.js | 3 +- frontend/js/i18n/messages.json | 2 +- frontend/scss/custom.scss | 9 ++++ 13 files changed, 95 insertions(+), 25 deletions(-) diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 2c0c5f42f..0bf31ac20 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -25,6 +25,7 @@ const internalNginx = { */ configure: (model, host_type, host) => { let combined_meta = {}; + const sslPassthroughEnabled = internalNginx.sslPassthroughEnabled(); return internalNginx.test() .then(() => { @@ -33,7 +34,25 @@ const internalNginx = { return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all }) .then(() => { - return internalNginx.generateConfig(host_type, host); + if(host_type === 'ssl_passthrough_host' && !sslPassthroughEnabled){ + // ssl passthrough is disabled + const meta = { + nginx_online: false, + nginx_err: 'SSL passthrough is not enabled in environment' + }; + + return passthroughHostModel + .query() + .where('is_deleted', 0) + .andWhere('enabled', 1) + .patch({ + meta + }).then(() => { + return internalNginx.deleteConfig('ssl_passthrough_host', host, false); + }); + } else { + return internalNginx.generateConfig(host_type, host); + } }) .then(() => { // Test nginx again and update meta with result @@ -46,11 +65,17 @@ const internalNginx = { }); if(host_type === 'ssl_passthrough_host'){ - return passthroughHostModel - .query() - .patch({ - meta: combined_meta - }); + // If passthrough is disabled we have already marked the hosts as offline + if (sslPassthroughEnabled) { + return passthroughHostModel + .query() + .where('is_deleted', 0) + .andWhere('enabled', 1) + .patch({ + meta: combined_meta + }); + } + return Promise.resolve(); } return model @@ -84,6 +109,18 @@ const internalNginx = { nginx_err: valid_lines.join('\n') }); + if(host_type === 'ssl_passthrough_host'){ + return passthroughHostModel + .query() + .where('is_deleted', 0) + .andWhere('enabled', 1) + .patch({ + meta: combined_meta + }).then(() => { + return internalNginx.deleteConfig('ssl_passthrough_host', host, true); + }); + } + return model .query() .where('id', host.id) @@ -241,7 +278,7 @@ const internalNginx = { }), } } else { - internalNginx.deleteConfig(host_type, host) + internalNginx.deleteConfig(host_type, host, false) } } else if (host_type !== 'default') { @@ -470,7 +507,7 @@ const internalNginx = { return (enabled === 'on' || enabled === 'true' || enabled === '1' || enabled === 'yes'); } - return true; + return false; }, /** diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 48b095a86..3ccf93988 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -42,7 +42,7 @@ router.use('/nginx/certificates', require('./nginx/certificates')); router.get('/ssl-passthrough-enabled', (req, res/*, next*/) => { res.status(200).send({ - status: 'OK', + status: 'OK', ssl_passthrough_enabled: internalNginx.sslPassthroughEnabled() }); }); diff --git a/backend/setup.js b/backend/setup.js index 12fec3dc8..f0eb8f89a 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -230,7 +230,7 @@ const setupLogrotation = () => { * @returns {Promise} */ const setupSslPassthrough = () => { - return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}).then(() => internalNginx.reload()); + return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {}); }; module.exports = function () { diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 4b9fef5ec..b56682d35 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -85,7 +85,7 @@ http { stream { # Files generated by NPM - include /data/nginx/ssl_passthrough_host/hosts.conf; + include /data/nginx/ssl_passthrough_host/hosts[.]conf; include /data/nginx/stream/*.conf; # Custom diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index c7b51a846..b0595c31d 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -172,3 +172,28 @@ value by specifying it as a Docker environment variable. The default if not spec X_FRAME_OPTIONS: "sameorigin" ... ``` + +## SSL Passthrough + +SSL Passthrough will allow you to proxy a server without [SSL termination](https://en.wikipedia.org/wiki/TLS_termination_proxy). This means the SSL encryption of the server will be passed right through the proxy, retaining the original certificate. + +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](https://en.wikipedia.org/wiki/Server_Name_Indication) to know where to send this network 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 a HTTPS requests. + +Due 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. + +To enable SSL Passthrough on your npm instance you need to do two things: add the environment variable `ENABLE_SSL_PASSTHROUGH` with the value `"true"`, and expose port 444 instead of 443 to the outside as port 443. + +```yml +version: '3' +services: + app: + ... + ports: + - '80:80' + - '81:81' + - '443:444' # Expose internal port 444 instead of 443 as SSL port + environment: + ... + ENABLE_SSL_PASSTHROUGH: "true" # Enable SSL Passthrough + ... +``` \ No newline at end of file diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js index abac53ad4..72accc019 100644 --- a/frontend/js/app/controller.js +++ b/frontend/js/app/controller.js @@ -253,7 +253,7 @@ module.exports = { * * @param model */ - showNginxSslPassthroughConfirm: function (model) { + showNginxSslPassthroughDeleteConfirm: function (model) { if (Cache.User.isAdmin() || Cache.User.canManage('ssl_passthrough_hosts')) { require(['./main', './nginx/ssl-passthrough/delete'], function (App, View) { App.UI.showModalDialog(new View({model: model})); diff --git a/frontend/js/app/nginx/ssl-passthrough/form.ejs b/frontend/js/app/nginx/ssl-passthrough/form.ejs index 6ba358651..30a26a54b 100644 --- a/frontend/js/app/nginx/ssl-passthrough/form.ejs +++ b/frontend/js/app/nginx/ssl-passthrough/form.ejs @@ -8,7 +8,7 @@
- +
diff --git a/frontend/js/app/nginx/ssl-passthrough/form.js b/frontend/js/app/nginx/ssl-passthrough/form.js index 4fea26f1c..522c3703c 100644 --- a/frontend/js/app/nginx/ssl-passthrough/form.js +++ b/frontend/js/app/nginx/ssl-passthrough/form.js @@ -12,11 +12,11 @@ module.exports = Mn.View.extend({ className: 'modal-dialog', ui: { - form: 'form', + form: 'form', forwarding_host: 'input[name="forwarding_host"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' }, events: { diff --git a/frontend/js/app/nginx/ssl-passthrough/list/main.ejs b/frontend/js/app/nginx/ssl-passthrough/list/main.ejs index 79268f06d..80c7d1732 100644 --- a/frontend/js/app/nginx/ssl-passthrough/list/main.ejs +++ b/frontend/js/app/nginx/ssl-passthrough/list/main.ejs @@ -1,6 +1,6 @@   - <%- i18n('ssl-passthrough-hosts', 'domain-name') %> + <%- i18n('all-hosts', 'domain-name') %> <%- i18n('str', 'destination') %> <%- i18n('str', 'status') %> <% if (canManage) { %> diff --git a/frontend/js/app/nginx/ssl-passthrough/main.ejs b/frontend/js/app/nginx/ssl-passthrough/main.ejs index 24adfbfd9..5fd4429c0 100644 --- a/frontend/js/app/nginx/ssl-passthrough/main.ejs +++ b/frontend/js/app/nginx/ssl-passthrough/main.ejs @@ -1,17 +1,17 @@
-
+

<%- i18n('ssl-passthrough-hosts', 'title') %>

-
- Disabled +
+ <%= i18n('ssl-passthrough-hosts', 'is-disabled-warning', {url: 'https://nginxproxymanager.com/advanced-config/#ssl-passthrough'}) %>
diff --git a/frontend/js/app/nginx/ssl-passthrough/main.js b/frontend/js/app/nginx/ssl-passthrough/main.js index af86d91fd..0ceb08be9 100644 --- a/frontend/js/app/nginx/ssl-passthrough/main.js +++ b/frontend/js/app/nginx/ssl-passthrough/main.js @@ -43,7 +43,6 @@ module.exports = Mn.View.extend({ 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 { @@ -65,7 +64,7 @@ module.exports = Mn.View.extend({ 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, - btn_color: 'blue', + btn_color: 'dark', permission: 'ssl-passthrough-hosts', action: function () { App.Controller.showNginxSslPassthroughForm(); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index a77471971..eda9087de 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -121,11 +121,11 @@ "empty": "There are no SSL Passthrough Hosts", "add": "Add SSL Passthrough Hosts", "form-title": "{id, select, undefined{New} other{Edit}} SSL Passthrough Host", - "domain-name": "Domain Name", "forwarding-host": "Forward Host", "forwarding-port": "Forward Port", "delete": "Delete SSL Passthrough Host", "delete-confirm": "Are you sure you want to delete this SSL Passthrough Host?", + "is-disabled-warning": "SSL Passthrough Hosts are not enabled in the environment. Please see the docs for more information.", "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." }, diff --git a/frontend/scss/custom.scss b/frontend/scss/custom.scss index 4037dcf6c..284798f59 100644 --- a/frontend/scss/custom.scss +++ b/frontend/scss/custom.scss @@ -12,6 +12,15 @@ a:hover { color: darken($primary-color, 10%); } +.alert-danger a { + color: #6b1110; + text-decoration: underline; +} + +a:hover { + color: darken(#6b1110, 10%); +} + .dropdown-header { padding-left: 1rem; } From f650137c8488df3c0980b027892747bc85012ed1 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Tue, 12 Oct 2021 15:42:22 +0200 Subject: [PATCH 4/6] Fixes eslint errors --- backend/internal/host.js | 4 +- backend/internal/nginx.js | 173 +++++++++--------- .../20211010141200_ssl_passthrough_host.js | 36 ++-- backend/routes/api/main.js | 2 +- .../routes/api/nginx/ssl_passthrough_hosts.js | 2 +- 5 files changed, 107 insertions(+), 110 deletions(-) diff --git a/backend/internal/host.js b/backend/internal/host.js index 9c91b4169..354b382b7 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -118,8 +118,8 @@ const internalHost = { if (promises_results[3]) { // SSL Passthrough Hosts - response_object.ssl_passthrough_hosts = internalHost._getHostsWithDomains(promises_results[3], domain_names); - response_object.total_count += response_object.ssl_passthrough_hosts.length; + response_object.ssl_passthrough_hosts = internalHost._getHostsWithDomains(promises_results[3], domain_names); + response_object.total_count += response_object.ssl_passthrough_hosts.length; } return response_object; diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 0bf31ac20..98225c190 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -1,11 +1,11 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); -const { Liquid } = require('liquidjs'); -const passthroughHostModel = require('../models/ssl_passthrough_host'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; +const _ = require('lodash'); +const fs = require('fs'); +const logger = require('../logger').nginx; +const utils = require('../lib/utils'); +const error = require('../lib/error'); +const { Liquid } = require('liquidjs'); +const passthroughHostModel = require('../models/ssl_passthrough_host'); +const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; const internalNginx = { @@ -24,7 +24,7 @@ const internalNginx = { * @returns {Promise} */ configure: (model, host_type, host) => { - let combined_meta = {}; + let combined_meta = {}; const sslPassthroughEnabled = internalNginx.sslPassthroughEnabled(); return internalNginx.test() @@ -34,7 +34,7 @@ const internalNginx = { return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all }) .then(() => { - if(host_type === 'ssl_passthrough_host' && !sslPassthroughEnabled){ + if (host_type === 'ssl_passthrough_host' && !sslPassthroughEnabled){ // ssl passthrough is disabled const meta = { nginx_online: false, @@ -64,7 +64,7 @@ const internalNginx = { nginx_err: null }); - if(host_type === 'ssl_passthrough_host'){ + if (host_type === 'ssl_passthrough_host'){ // If passthrough is disabled we have already marked the hosts as offline if (sslPassthroughEnabled) { return passthroughHostModel @@ -109,7 +109,7 @@ const internalNginx = { nginx_err: valid_lines.join('\n') }); - if(host_type === 'ssl_passthrough_host'){ + if (host_type === 'ssl_passthrough_host'){ return passthroughHostModel .query() .where('is_deleted', 0) @@ -235,7 +235,7 @@ const internalNginx = { * @param {Object} host * @returns {Promise} */ - generateConfig: (host_type, host) => { + generateConfig: async (host_type, host) => { host_type = host_type.replace(new RegExp('-', 'g'), '_'); if (debug_mode) { @@ -248,90 +248,87 @@ const internalNginx = { root: __dirname + '/../templates/' }); - return new Promise(async (resolve, reject) => { - let template = null; - let filename = internalNginx.getConfigName(host_type, host.id); - - try { - template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } + let template = null; + let filename = internalNginx.getConfigName(host_type, host.id); - let locationsPromise; - let origLocations; + try { + template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); + } catch (err) { + throw new error.ConfigurationError(err.message); + } - // Manipulate the data a bit before sending it to the template - if (host_type === 'ssl_passthrough_host') { - if(internalNginx.sslPassthroughEnabled()){ - const allHosts = await passthroughHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']); - host = { - all_passthrough_hosts: allHosts.map((host) => { - // Replace dots in domain - host.forwarding_host = internalNginx.addIpv6Brackets(host.forwarding_host); - return host; - }), - } - } else { - internalNginx.deleteConfig(host_type, host, false) - } - - } else if (host_type !== 'default') { - host.use_default_location = true; - if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { - host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); - } + let locationsPromise; + let origLocations; + + // Manipulate the data a bit before sending it to the template + if (host_type === 'ssl_passthrough_host') { + if (internalNginx.sslPassthroughEnabled()){ + const allHosts = await passthroughHostModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .omit(['is_deleted']); + host = { + all_passthrough_hosts: allHosts.map((host) => { + // Replace dots in domain + host.forwarding_host = internalNginx.addIpv6Brackets(host.forwarding_host); + return host; + }), + }; + } else { + internalNginx.deleteConfig(host_type, host, false); } + + } else if (host_type !== 'default') { + host.use_default_location = true; + if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { + host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + } + } - if (host.locations) { - //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); - origLocations = [].concat(host.locations); - locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { - host.locations = renderedLocations; - }); + if (host.locations) { + //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); + origLocations = [].concat(host.locations); + locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { + host.locations = renderedLocations; + }); - // Allow someone who is using / custom location path to use it, and skip the default / location - _.map(host.locations, (location) => { - if (location.path === '/') { - host.use_default_location = false; - } - }); + // Allow someone who is using / custom location path to use it, and skip the default / location + _.map(host.locations, (location) => { + if (location.path === '/') { + host.use_default_location = false; + } + }); - } else { - locationsPromise = Promise.resolve(); - } + } else { + locationsPromise = Promise.resolve(); + } - // Set the IPv6 setting for the host - host.ipv6 = internalNginx.ipv6Enabled(); + // Set the IPv6 setting for the host + host.ipv6 = internalNginx.ipv6Enabled(); - locationsPromise.then(() => { - renderEngine - .parseAndRender(template, host) - .then((config_text) => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + return locationsPromise.then(() => { + renderEngine + .parseAndRender(template, host) + .then((config_text) => { + fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } + if (debug_mode) { + logger.success('Wrote config:', filename, config_text); + } - // Restore locations array - host.locations = origLocations; + // Restore locations array + host.locations = origLocations; - resolve(true); - }) - .catch((err) => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } + return true; + }) + .catch((err) => { + if (debug_mode) { + logger.warn('Could not write ' + filename + ':', err.message); + } - reject(new error.ConfigurationError(err.message)); - }); - }); + throw new error.ConfigurationError(err.message); + }); }); }, @@ -514,12 +511,12 @@ const internalNginx = { * Helper function to add brackets to an IP if it is IPv6 * @returns {string} */ - addIpv6Brackets: function (ip) { + addIpv6Brackets: function (ip) { // Only run check if ipv6 is enabled if (internalNginx.ipv6Enabled()) { const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/gi; - if(ipv6Regex.test(ip)){ - return `[${ip}]` + if (ipv6Regex.test(ip)){ + return `[${ip}]`; } } return ip; diff --git a/backend/migrations/20211010141200_ssl_passthrough_host.js b/backend/migrations/20211010141200_ssl_passthrough_host.js index 325386861..1d19813dc 100644 --- a/backend/migrations/20211010141200_ssl_passthrough_host.js +++ b/backend/migrations/20211010141200_ssl_passthrough_host.js @@ -27,23 +27,23 @@ exports.up = function (knex/*, Promise*/) { }).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'); - }); - }) - ; + .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'); + }); + }) + ; }; /** @@ -59,7 +59,7 @@ exports.down = function (knex/*, Promise*/) { 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 and permissions updated'); diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 3ccf93988..209d4770b 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -42,7 +42,7 @@ router.use('/nginx/certificates', require('./nginx/certificates')); router.get('/ssl-passthrough-enabled', (req, res/*, next*/) => { res.status(200).send({ - status: 'OK', + status: 'OK', ssl_passthrough_enabled: internalNginx.sslPassthroughEnabled() }); }); diff --git a/backend/routes/api/nginx/ssl_passthrough_hosts.js b/backend/routes/api/nginx/ssl_passthrough_hosts.js index dfa2eac30..2c7f090eb 100644 --- a/backend/routes/api/nginx/ssl_passthrough_hosts.js +++ b/backend/routes/api/nginx/ssl_passthrough_hosts.js @@ -98,7 +98,7 @@ router } }, { host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) + expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) .then((data) => { return internalSslPassthrough.get(res.locals.access, { From 6e82161987ee3a2f396327c8afe2f36b3d63e7f9 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Tue, 12 Oct 2021 15:55:28 +0200 Subject: [PATCH 5/6] Adds comments to docker compose dev --- docker/docker-compose.dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 4d2e3a1b7..dff37f7e1 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -10,8 +10,8 @@ services: ports: - 3080:80 - 3081:81 - - 3443:443 - - 3444:444 + - 3443:443 # Ususally you would only have this one + - 3444:444 # This is to test ssl passthrough networks: - nginx_proxy_manager environment: From 70163a66fbe524856df64b83252e3b26244ac0d0 Mon Sep 17 00:00:00 2001 From: Julian Reinhardt Date: Mon, 25 Oct 2021 13:08:35 +0200 Subject: [PATCH 6/6] Fixes migration --- .../20211010141200_ssl_passthrough_host.js | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/backend/migrations/20211010141200_ssl_passthrough_host.js b/backend/migrations/20211010141200_ssl_passthrough_host.js index 1d19813dc..769afbc14 100644 --- a/backend/migrations/20211010141200_ssl_passthrough_host.js +++ b/backend/migrations/20211010141200_ssl_passthrough_host.js @@ -10,10 +10,10 @@ const logger = require('../logger').migrate; * @param {Promise} Promise * @returns {Promise} */ -exports.up = function (knex/*, Promise*/) { +exports.up = async function (knex/*, Promise*/) { logger.info('[' + migrate_name + '] Migrating Up...'); - return knex.schema.createTable('ssl_passthrough_host', (table) => { + await knex.schema.createTable('ssl_passthrough_host', (table) => { table.increments().primary(); table.dateTime('created_on').notNull(); table.dateTime('modified_on').notNull(); @@ -24,26 +24,44 @@ exports.up = function (knex/*, Promise*/) { 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(() => { - 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'); - }); - }) - ; + }); + + logger.info('[' + migrate_name + '] Table created'); + + // Remove unique constraint so name can be used for new table + await knex.schema.alterTable('user_permission', (table) => { + table.dropUnique('user_id'); + }); + + await knex.schema.renameTable('user_permission', 'user_permission_old'); + + // We need to recreate the table since sqlite does not support altering columns + await knex.schema.createTable('user_permission', (table) => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('user_id').notNull().unsigned(); + table.string('visibility').notNull(); + table.string('proxy_hosts').notNull(); + table.string('redirection_hosts').notNull(); + table.string('dead_hosts').notNull(); + table.string('streams').notNull(); + table.string('ssl_passthrough_hosts').notNull(); + table.string('access_lists').notNull(); + table.string('certificates').notNull(); + table.unique('user_id'); + }); + + await knex('user_permission_old').select('*', 'streams as ssl_passthrough_hosts').then((data) => { + if (data.length) { + return knex('user_permission').insert(data); + } + return Promise.resolve(); + }); + + await knex.schema.dropTableIfExists('user_permission_old'); + + logger.info('[' + migrate_name + '] permissions updated'); }; /**