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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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