WIP: complete control of new passthrough host type

This commit is contained in:
chaptergy
2021-10-10 23:49:57 +02:00
parent 5b1f0cead1
commit 5a2548c89d
19 changed files with 126 additions and 85 deletions

@ -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

@ -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);
}
});
}

@ -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 {

@ -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())
});

@ -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');
});
};

@ -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: {

@ -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",

@ -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 () {

@ -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;
}

@ -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"

@ -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]

@ -21,7 +21,7 @@
<div class="col-sm-4 col-md-4">
<div class="form-group">
<label class="form-label"><%- i18n('ssl-passthrough-hosts', 'forwarding-port') %> <span class="form-required">*</span></label>
<input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" value="<%- forwarding_port %>" required>
<input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 443" value="<%- forwarding_port %>" required>
</div>
</div>
</div>

@ -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;

@ -10,6 +10,9 @@
</div>
</div>
<div class="card-body no-padding min-100">
<div id="ssl-passthrough-disabled-info">
Disabled
</div>
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">

@ -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 () {

@ -20,6 +20,10 @@
<a href="/nginx/stream" class="dropdown-item "><%- i18n('streams', 'title') %></a>
<% } %>
<% if (canShow('ssl_passthrough_hosts')) { %>
<a href="/nginx/ssl-passthrough" class="dropdown-item "><%- i18n('ssl-passthrough-hosts', 'title') %></a>
<% } %>
<% if (canShow('dead_hosts')) { %>
<a href="/nginx/404" class="dropdown-item "><%- i18n('dead-hosts', 'title') %></a>
<% } %>

@ -31,9 +31,9 @@
</div>
<%
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, '_');
%>
<div class="col-sm-12 col-md-12">
<div class="form-group">

@ -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'
});
}

@ -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",