Added support for redirection and 404 hosts

This commit is contained in:
Jamie Curnow
2018-01-04 16:18:48 +10:00
parent 61820840e0
commit 64de096565
22 changed files with 453 additions and 31 deletions

View File

@ -1,6 +1,6 @@
{
"name": "nginx-proxy-manager",
"version": "1.0.0",
"version": "1.0.1",
"description": "Nginx proxt with built in Web based management",
"main": "src/backend/index.js",
"dependencies": {

View File

@ -155,15 +155,14 @@ const internalHost = {
*
* @param {Object} host
* @param {Boolean} [reload_nginx]
* @param {Boolean} [force_ssl_renew]
* @returns {Promise}
*/
configure: (host, reload_nginx, force_ssl_renew) => {
configure: (host, reload_nginx) => {
return new Promise((resolve/*, reject*/) => {
resolve(internalNginx.deleteConfig(host));
})
.then(() => {
if (host.ssl && (force_ssl_renew || !internalSsl.hasValidSslCerts(host))) {
if (host.ssl && !internalSsl.hasValidSslCerts(host)) {
return internalSsl.configureSsl(host);
}
})
@ -248,7 +247,7 @@ const internalHost = {
reject(new error.ValidationError('Host does not have SSL enabled'));
} else {
// 3. Fire the ssl and config generation for this host, forcing ssl
internalHost.configure(host, true, true)
internalSsl.renewSsl(host)
.then((/*result*/) => {
resolve(host);
})

View File

@ -45,7 +45,11 @@ const internalNginx = {
let filename = internalNginx.getConfigName(host);
try {
template = fs.readFileSync(__dirname + '/../templates/host.conf.ejs', {encoding: 'utf8'});
if (typeof host.type === 'undefined' || !host.type) {
host.type = 'proxy';
}
template = fs.readFileSync(__dirname + '/../templates/' + host.type + '.conf.ejs', {encoding: 'utf8'});
let config_text = ejs.render(template, host);
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
resolve(true);

View File

@ -10,6 +10,10 @@
"type": "string",
"readonly": true
},
"type": {
"type": "string",
"pattern": "^(proxy|redirection|404)$"
},
"hostname": {
"$ref": "../definitions.json#/definitions/hostname"
},
@ -17,6 +21,9 @@
"type": "string",
"format": "ipv4"
},
"forward_host": {
"type": "string"
},
"forward_port": {
"type": "integer",
"minumum": 1,
@ -79,14 +86,19 @@
"schema": {
"type": "object",
"required": [
"hostname",
"forward_server",
"forward_port"
"type",
"hostname"
],
"properties": {
"type": {
"$ref": "#/definitions/type"
},
"hostname": {
"$ref": "#/definitions/hostname"
},
"forward_host": {
"$ref": "#/definitions/forward_host"
},
"forward_server": {
"$ref": "#/definitions/forward_server"
},
@ -137,6 +149,9 @@
"hostname": {
"$ref": "#/definitions/hostname"
},
"forward_host": {
"$ref": "#/definitions/forward_host"
},
"forward_server": {
"$ref": "#/definitions/forward_server"
},
@ -188,9 +203,15 @@
"_id": {
"$ref": "#/definitions/_id"
},
"type": {
"$ref": "#/definitions/type"
},
"hostname": {
"$ref": "#/definitions/hostname"
},
"forward_host": {
"$ref": "#/definitions/forward_host"
},
"forward_server": {
"$ref": "#/definitions/forward_server"
},

View File

@ -0,0 +1,19 @@
# <%- hostname %>
server {
listen 80;
<%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %>
server_name <%- hostname %>;
access_log /config/logs/<%- hostname %>.log proxy;
<% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
return 404;
}

View File

@ -0,0 +1,22 @@
# <%- hostname %>
server {
listen 80;
<%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %>
server_name <%- hostname %>;
access_log /config/logs/<%- hostname %>.log proxy;
<%- typeof asset_caching !== 'undefined' && asset_caching ? 'include conf.d/include/assets.conf;' : '' %>
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
<% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
return 301 $scheme://<%- forward_host %>$request_uri;
}

View File

@ -54,12 +54,34 @@ module.exports = {
},
/**
* Show Host Form
* Show Proxy Host Form
*
* @param model
*/
showHostForm: function (model) {
require(['./main', './host/form'], function (App, View) {
showProxyHostForm: function (model) {
require(['./main', './host/proxy_form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
},
/**
* Show Redirection Host Form
*
* @param model
*/
showRedirectionHostForm: function (model) {
require(['./main', './host/redirection_form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
},
/**
* Show 404 Host Form
*
* @param model
*/
show404HostForm: function (model) {
require(['./main', './host/404_form'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
},

View File

@ -1,5 +1,9 @@
<td colspan="10" class="text-center">
<br><br>
<p>It looks like there are no hosts configured.</p>
<p><button type="button" class="btn btn-sm btn-success">Create your first Host</button></p>
<p>
<button type="button" class="btn btn-sm btn-success proxy">Create Proxy Host</button>
<button type="button" class="btn btn-sm btn-success redirection">Create Redirection Host</button>
<button type="button" class="btn btn-sm btn-success 404">Create 404 Host</button>
</p>
</td>

View File

@ -11,13 +11,25 @@ module.exports = Mn.View.extend({
tagName: 'tr',
ui: {
create: 'button'
proxy: 'button.proxy',
redirection: 'button.redirection',
'404': 'button.404'
},
events: {
'click @ui.create': function (e) {
'click @ui.proxy': function (e) {
e.preventDefault();
Controller.showHostForm(new HostModel.Model);
Controller.showProxyHostForm(new HostModel.Model);
},
'click @ui.redirection': function (e) {
e.preventDefault();
Controller.showRedirectionHostForm(new HostModel.Model);
},
'click @ui.404': function (e) {
e.preventDefault();
Controller.show404HostForm(new HostModel.Model);
}
}
});

View File

@ -1,10 +1,21 @@
<table class="table table-condensed table-striped">
<thead>
<th>Hostname</th>
<th>Forward</th>
<th>Destination</th>
<th>SSL</th>
<th>Access List</th>
<th class="text-right"><button type="button" class="btn btn-xs btn-info">Create Host</button></th>
<th class="text-right">
<div class="btn-group">
<button type="button" class="btn btn-xs btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Create Host <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="#" class="new-proxy">Proxy Host</a></li>
<li><a href="#" class="new-redirection">Redirection Host</a></li>
<li><a href="#" class="new-404">404 Host</a></li>
</ul>
</div>
</th>
</thead>
<tbody>
<!-- items -->

View File

@ -26,13 +26,25 @@ module.exports = Mn.View.extend({
},
ui: {
'create': 'th button'
new_proxy: 'th .new-proxy',
new_redirection: 'th .new-redirection',
new_404: 'th .new-404'
},
events: {
'click @ui.create': function (e) {
'click @ui.new_proxy': function (e) {
e.preventDefault();
Controller.showHostForm(new HostModel.Model);
Controller.showProxyHostForm(new HostModel.Model);
},
'click @ui.new_redirection': function (e) {
e.preventDefault();
Controller.showRedirectionHostForm(new HostModel.Model);
},
'click @ui.new_404': function (e) {
e.preventDefault();
Controller.show404HostForm(new HostModel.Model);
}
},

View File

@ -1,5 +1,15 @@
<td><a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a></td>
<td><span class="monospace"><%- forward_server %>:<%- forward_port %></span></td>
<td>
<span class="monospace">
<% if (type === 'proxy') { %>
<%- forward_server %>:<%- forward_port %>
<% } else if (type === 'redirection') { %>
<%- forward_host %>
<% } else if (type === '404') { %>
404
<% } %>
</span>
</td>
<td>
<% if (ssl && force_ssl) { %>
Forced

View File

@ -22,7 +22,17 @@ module.exports = Mn.View.extend({
events: {
'click @ui.edit': function (e) {
e.preventDefault();
Controller.showHostForm(this.model);
switch (this.model.get('type')) {
case 'proxy':
Controller.showProxyHostForm(this.model);
break;
case 'redirection':
Controller.showRedirectionHostForm(this.model);
break;
case '404':
Controller.show404HostForm(this.model);
break;
}
},
'click @ui.delete': function (e) {

View File

@ -0,0 +1,50 @@
<div class="modal-dialog">
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> 404 Host</h4>
</div>
<div class="modal-body">
<p>A 404 host will simply return a 404 not found page for any hits to any path on the domain.</p>
<div class="form-group">
<label class="col-sm-4 control-label">Hostname</label>
<div class="col-sm-8">
<input type="text" class="form-control" placeholder="myhost.example.com" name="hostname" value="<%- hostname %>" required>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="ssl" value="true"<%- ssl ? ' checked' : '' %>> Enable SSL with Letsencrypt
</label>
</div>
</div>
</div>
<div class="ssl_options"<%= ssl ? '' : ' style="display: none;"' %>>
<div class="form-group">
<label class="col-sm-4 control-label">Letsencrypt Email</label>
<div class="col-sm-8">
<input type="email" class="form-control" placeholder="" name="letsencrypt_email" value="<%- letsencrypt_email %>"<%- ssl ? ' required' : '' %>>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="accept_tos" value="true"<%- ssl && typeof _id !== 'undefined' ? ' checked' : '' %><%- ssl ? ' required' : '' %>> I accept the <a href="https://letsencrypt.org/repository/" target="_blank">Letsencrypt Terms of Service</a>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success save">Save</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,78 @@
'use strict';
import Mn from 'backbone.marionette';
const _ = require('lodash');
const template = require('./404_form.ejs');
const Controller = require('../controller');
const Api = require('../api');
const App = require('../main');
require('jquery-serializejson');
module.exports = Mn.View.extend({
template: template,
ui: {
form: 'form',
buttons: 'form button',
ssl_options: '.ssl_options',
ssl: 'input[name="ssl"]',
letsencrypt_email: 'input[name="letsencrypt_email"]',
accept_tos: 'input[name="accept_tos"]'
},
events: {
'change @ui.ssl': function (e) {
let inputs = this.ui.letsencrypt_email.add(this.ui.accept_tos);
if (this.ui.ssl.prop('checked')) {
this.ui.ssl_options.show();
inputs.prop('required', true);
} else {
this.ui.ssl_options.hide();
inputs.prop('required', false);
}
},
'submit @ui.form': function (e) {
e.preventDefault();
let data = _.extend({}, this.ui.form.serializeJSON());
// Change text true's to bools
_.map(data, function (val, key) {
if (val === 'true') {
data[key] = true;
}
});
// This is a 404 host
data.type = '404';
// accept_tos is not required for backend
delete data.accept_tos;
if (!data.ssl) {
delete data.letsencrypt_email;
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
let method = Api.Hosts.create;
if (this.model.get('_id')) {
// edit
method = Api.Hosts.update;
data._id = this.model.get('_id');
}
method(data)
.then((/*result*/) => {
App.UI.closeModal();
Controller.showDashboard();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -2,7 +2,7 @@
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> Host</h4>
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> Proxy Host</h4>
</div>
<div class="modal-body">
<div class="form-group">
@ -62,7 +62,7 @@
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="accept_tos" value="true"<%- accept_tos ? ' checked' : '' %><%- ssl ? ' required' : '' %>> I accept the <a href="https://letsencrypt.org/repository/" target="_blank">Letsencrypt Terms of Service</a>
<input type="checkbox" name="accept_tos" value="true"<%- ssl && typeof _id !== 'undefined' ? ' checked' : '' %><%- ssl ? ' required' : '' %>> I accept the <a href="https://letsencrypt.org/repository/" target="_blank">Letsencrypt Terms of Service</a>
</label>
</div>
<div class="checkbox">

View File

@ -3,7 +3,7 @@
import Mn from 'backbone.marionette';
const _ = require('lodash');
const template = require('./form.ejs');
const template = require('./proxy_form.ejs');
const Controller = require('../controller');
const Api = require('../api');
const App = require('../main');
@ -46,6 +46,8 @@ module.exports = Mn.View.extend({
}
});
data.type = 'proxy';
// Port is integer
data.forward_port = parseInt(data.forward_port, 10);

View File

@ -0,0 +1,62 @@
<div class="modal-dialog">
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> Redirection Host</h4>
</div>
<div class="modal-body">
<p>A redirection host will forward browser requests on this hostname to the new hostname while keeping the same path.</p>
<div class="form-group">
<label class="col-sm-4 control-label">Hostname</label>
<div class="col-sm-8">
<input type="text" class="form-control" placeholder="myhost.example.com" name="hostname" value="<%- hostname %>" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Forwarding Hostname</label>
<div class="col-sm-8">
<input type="text" class="form-control" placeholder="mynewhost.example.com" name="forward_host" value="<%- forward_host %>" required>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="block_exploits" value="true"<%- block_exploits ? ' checked' : '' %>> Block Common Exploits
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="ssl" value="true"<%- ssl ? ' checked' : '' %>> Enable SSL with Letsencrypt
</label>
</div>
</div>
</div>
<div class="ssl_options"<%= ssl ? '' : ' style="display: none;"' %>>
<div class="form-group">
<label class="col-sm-4 control-label">Letsencrypt Email</label>
<div class="col-sm-8">
<input type="email" class="form-control" placeholder="" name="letsencrypt_email" value="<%- letsencrypt_email %>"<%- ssl ? ' required' : '' %>>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label>
<input type="checkbox" name="accept_tos" value="true"<%- ssl && typeof _id !== 'undefined' ? ' checked' : '' %><%- ssl ? ' required' : '' %>> I accept the <a href="https://letsencrypt.org/repository/" target="_blank">Letsencrypt Terms of Service</a>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success save">Save</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,78 @@
'use strict';
import Mn from 'backbone.marionette';
const _ = require('lodash');
const template = require('./redirection_form.ejs');
const Controller = require('../controller');
const Api = require('../api');
const App = require('../main');
require('jquery-serializejson');
module.exports = Mn.View.extend({
template: template,
ui: {
form: 'form',
buttons: 'form button',
ssl_options: '.ssl_options',
ssl: 'input[name="ssl"]',
letsencrypt_email: 'input[name="letsencrypt_email"]',
accept_tos: 'input[name="accept_tos"]'
},
events: {
'change @ui.ssl': function (e) {
let inputs = this.ui.letsencrypt_email.add(this.ui.accept_tos);
if (this.ui.ssl.prop('checked')) {
this.ui.ssl_options.show();
inputs.prop('required', true);
} else {
this.ui.ssl_options.hide();
inputs.prop('required', false);
}
},
'submit @ui.form': function (e) {
e.preventDefault();
let data = _.extend({}, this.ui.form.serializeJSON());
// Change text true's to bools
_.map(data, function (val, key) {
if (val === 'true') {
data[key] = true;
}
});
data.type = 'redirection';
// accept_tos is not required for backend
delete data.accept_tos;
if (!data.ssl) {
delete data.letsencrypt_email;
delete data.force_ssl;
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
let method = Api.Hosts.create;
if (this.model.get('_id')) {
// edit
method = Api.Hosts.update;
data._id = this.model.get('_id');
}
method(data)
.then((/*result*/) => {
App.UI.closeModal();
Controller.showDashboard();
})
.catch((err) => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -7,8 +7,10 @@ const model = Backbone.Model.extend({
defaults: function () {
return {
type: 'proxy',
hostname: '',
forward_server: '',
forward_host: '',
forward_port: 80,
asset_caching: false,
block_exploits: true,