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

@ -2,13 +2,13 @@
# Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-1.0.0-green.svg)
![Version](https://img.shields.io/badge/version-1.0.1-green.svg)
![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg)
![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg)
![Build Status](http://bamboo.jc21.com/plugins/servlet/wittified/build-status/AB-NPM)
This NPM comes as a pre-built docker image that enables you to easily forward to your websites
This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
@ -19,6 +19,10 @@ running at home or otherwise, including free SSL, without having to know too muc
- Secure your sites with SSL and optionally force SSL
- Secure your sites with Basic HTTP Authentication Access Lists
- Advanced Nginx config option for super users
- 3 domain uses:
- Proxy requests to upstream server
- Redirect requests to another domain
- Return immediate 404's
## Getting started
@ -84,7 +88,7 @@ I won't go in to too much detail here but here are the basics for someone new to
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS
4. Use the NPM here as your gateway to forward to your other web based services
4. Use the Nginx Proxy Manager here as your gateway to forward to your other web based services
## Screenshots
@ -98,8 +102,8 @@ I won't go in to too much detail here but here are the basics for someone new to
- Pass on human readable ssl cert errors to the ui
- Allow a host to be a redirection to another domain
- Allow a host to return immediate 404's
- UI: Allow column sorting on tables
- UI: Allow filtering hosts by types
- Advanced option to overwrite the default location block (or regex to do it automatically)
- Change the renew ssl process to use the letsencrypt renew procedure so as to avoid rate limits
- Add nice upstream error pages

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,