diff --git a/README.md b/README.md index 2518d3d4..3b2b2159 100644 --- a/README.md +++ b/README.md @@ -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 + diff --git a/manager/package.json b/manager/package.json index 30c157e9..4f3bedc3 100644 --- a/manager/package.json +++ b/manager/package.json @@ -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": { diff --git a/manager/src/backend/internal/host.js b/manager/src/backend/internal/host.js index ac5da925..1cdb667d 100644 --- a/manager/src/backend/internal/host.js +++ b/manager/src/backend/internal/host.js @@ -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); }) diff --git a/manager/src/backend/internal/nginx.js b/manager/src/backend/internal/nginx.js index 736eab5e..1aa40688 100644 --- a/manager/src/backend/internal/nginx.js +++ b/manager/src/backend/internal/nginx.js @@ -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); diff --git a/manager/src/backend/schema/endpoints/hosts.json b/manager/src/backend/schema/endpoints/hosts.json index 12f5a83c..b74b59fa 100644 --- a/manager/src/backend/schema/endpoints/hosts.json +++ b/manager/src/backend/schema/endpoints/hosts.json @@ -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" }, diff --git a/manager/src/backend/templates/404.conf.ejs b/manager/src/backend/templates/404.conf.ejs new file mode 100644 index 00000000..d136541a --- /dev/null +++ b/manager/src/backend/templates/404.conf.ejs @@ -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; +} diff --git a/manager/src/backend/templates/host.conf.ejs b/manager/src/backend/templates/proxy.conf.ejs similarity index 100% rename from manager/src/backend/templates/host.conf.ejs rename to manager/src/backend/templates/proxy.conf.ejs diff --git a/manager/src/backend/templates/redirection.conf.ejs b/manager/src/backend/templates/redirection.conf.ejs new file mode 100644 index 00000000..1c4f91b5 --- /dev/null +++ b/manager/src/backend/templates/redirection.conf.ejs @@ -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; +} diff --git a/manager/src/frontend/js/app/controller.js b/manager/src/frontend/js/app/controller.js index 3ecc626b..789fd951 100644 --- a/manager/src/frontend/js/app/controller.js +++ b/manager/src/frontend/js/app/controller.js @@ -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})); }); }, diff --git a/manager/src/frontend/js/app/dashboard/empty.ejs b/manager/src/frontend/js/app/dashboard/empty.ejs index 1fabb660..f45451ab 100644 --- a/manager/src/frontend/js/app/dashboard/empty.ejs +++ b/manager/src/frontend/js/app/dashboard/empty.ejs @@ -1,5 +1,9 @@

It looks like there are no hosts configured.

-

+

+ + + +

diff --git a/manager/src/frontend/js/app/dashboard/empty.js b/manager/src/frontend/js/app/dashboard/empty.js index 6cd18cd2..48d6b527 100644 --- a/manager/src/frontend/js/app/dashboard/empty.js +++ b/manager/src/frontend/js/app/dashboard/empty.js @@ -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); } } }); diff --git a/manager/src/frontend/js/app/dashboard/main.ejs b/manager/src/frontend/js/app/dashboard/main.ejs index 618cd954..6759348b 100644 --- a/manager/src/frontend/js/app/dashboard/main.ejs +++ b/manager/src/frontend/js/app/dashboard/main.ejs @@ -1,10 +1,21 @@ - + - + diff --git a/manager/src/frontend/js/app/dashboard/main.js b/manager/src/frontend/js/app/dashboard/main.js index 9867fb98..8d1709f6 100644 --- a/manager/src/frontend/js/app/dashboard/main.js +++ b/manager/src/frontend/js/app/dashboard/main.js @@ -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); } }, diff --git a/manager/src/frontend/js/app/dashboard/row.ejs b/manager/src/frontend/js/app/dashboard/row.ejs index 91182a9f..6943c806 100644 --- a/manager/src/frontend/js/app/dashboard/row.ejs +++ b/manager/src/frontend/js/app/dashboard/row.ejs @@ -1,5 +1,15 @@ - +
HostnameForwardDestination SSL Access List +
+ + +
+
<%- hostname %><%- forward_server %>:<%- forward_port %> + + <% if (type === 'proxy') { %> + <%- forward_server %>:<%- forward_port %> + <% } else if (type === 'redirection') { %> + <%- forward_host %> + <% } else if (type === '404') { %> + 404 + <% } %> + + <% if (ssl && force_ssl) { %> Forced diff --git a/manager/src/frontend/js/app/dashboard/row.js b/manager/src/frontend/js/app/dashboard/row.js index b0c31b06..23eab1d0 100644 --- a/manager/src/frontend/js/app/dashboard/row.js +++ b/manager/src/frontend/js/app/dashboard/row.js @@ -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) { diff --git a/manager/src/frontend/js/app/host/404_form.ejs b/manager/src/frontend/js/app/host/404_form.ejs new file mode 100644 index 00000000..373a66dd --- /dev/null +++ b/manager/src/frontend/js/app/host/404_form.ejs @@ -0,0 +1,50 @@ + diff --git a/manager/src/frontend/js/app/host/404_form.js b/manager/src/frontend/js/app/host/404_form.js new file mode 100644 index 00000000..8a6b3c07 --- /dev/null +++ b/manager/src/frontend/js/app/host/404_form.js @@ -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'); + }); + } + } +}); diff --git a/manager/src/frontend/js/app/host/form.ejs b/manager/src/frontend/js/app/host/proxy_form.ejs similarity index 93% rename from manager/src/frontend/js/app/host/form.ejs rename to manager/src/frontend/js/app/host/proxy_form.ejs index aec356f9..f977d41e 100644 --- a/manager/src/frontend/js/app/host/form.ejs +++ b/manager/src/frontend/js/app/host/proxy_form.ejs @@ -2,7 +2,7 @@