Преглед на файлове

Added Stream forwarding support

Jamie Curnow преди 7 години
родител
ревизия
b57d1e5a66

+ 1 - 1
manager/package.json

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

+ 42 - 19
manager/src/backend/internal/host.js

@@ -44,19 +44,31 @@ const internalHost = {
      */
     create: payload => {
         return new Promise((resolve, reject) => {
-            // Enforce lowercase hostnames
-            payload.hostname = payload.hostname.toLowerCase();
+            let existing_host = false;
 
-            // 1. Check that the hostname doesn't already exist
-            let existing_host = db.hosts.findOne({hostname: payload.hostname});
+            if (payload.type === 'stream') {
+                // Check that the incoming port doesn't already exist
+                existing_host = db.hosts.findOne({incoming_port: payload.incoming_port});
+
+                if (payload.incoming_port === 80 || payload.incoming_port === 81 || payload.incoming_port === 443) {
+                    reject(new error.ConfigurationError('Port ' + payload.incoming_port + ' is reserved'));
+                    return;
+                }
+
+            } else {
+                payload.hostname = payload.hostname.toLowerCase();
+
+                // Check that the hostname doesn't already exist
+                existing_host = db.hosts.findOne({hostname: payload.hostname});
+            }
 
             if (existing_host) {
                 reject(new error.ValidationError('Hostname already exists'));
             } else {
-                // 2. Add host to db
+                // Add host to db
                 let host = db.hosts.save(payload);
 
-                // 3. Fire the config generation for this host
+                // Fire the config generation for this host
                 internalHost.configure(host, true)
                     .then((/*result*/) => {
                         resolve(host);
@@ -98,10 +110,16 @@ const internalHost = {
                 }
 
                 // Check that the hostname doesn't already exist
-                let other_host = db.hosts.findOne({hostname: payload.hostname});
+                let other_host = false;
+
+                if (typeof payload.incoming_port !== 'undefined') {
+                    other_host = db.hosts.findOne({incoming_port: payload.incoming_port});
+                } else {
+                    other_host = db.hosts.findOne({hostname: payload.hostname});
+                }
 
                 if (other_host && other_host._id !== id) {
-                    reject(new error.ValidationError('Hostname already exists'));
+                    reject(new error.ValidationError((other_host.type === 'stream' ? 'Source Stream Port' : 'Hostname') + ' already exists'));
                 } else {
                     // 2. Update host
                     db.hosts.update({_id: id}, payload, {multi: false, upsert: false});
@@ -126,17 +144,22 @@ const internalHost = {
                 return data;
             })
             .then(data => {
-                if (
-                    (data.original.ssl && !data.updated.ssl) ||                             // ssl was enabled and is now disabled
-                    (data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host
-                ) {
-                    // SSL was turned off or hostname for ssl has changed so we should remove certs for the original
-                    return internalSsl.deleteCerts(data.original)
-                        .then(() => {
-                            db.hosts.update({_id: data.updated._id}, {ssl_expires: 0}, {multi: false, upsert: false});
-                            data.updated.ssl_expires = 0;
-                            return data;
-                        });
+                if (data.updated.type !== 'stream') {
+                    if (
+                        (data.original.ssl && !data.updated.ssl) ||                             // ssl was enabled and is now disabled
+                        (data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host
+                    ) {
+                        // SSL was turned off or hostname for ssl has changed so we should remove certs for the original
+                        return internalSsl.deleteCerts(data.original)
+                            .then(() => {
+                                db.hosts.update({_id: data.updated._id}, {ssl_expires: 0}, {
+                                    multi:  false,
+                                    upsert: false
+                                });
+                                data.updated.ssl_expires = 0;
+                                return data;
+                            });
+                    }
                 }
 
                 return data;

+ 4 - 0
manager/src/backend/internal/nginx.js

@@ -32,6 +32,10 @@ const internalNginx = {
      * @returns {String}
      */
     getConfigName: host => {
+        if (host.type === 'stream') {
+            return '/config/nginx/stream/' + host.incoming_port + '.conf';
+        }
+
         return '/config/nginx/' + host.hostname + '.conf';
     },
 

+ 31 - 3
manager/src/backend/schema/endpoints/hosts.json

@@ -12,7 +12,7 @@
     },
     "type": {
       "type": "string",
-      "pattern": "^(proxy|redirection|404)$"
+      "pattern": "^(proxy|redirection|404|stream)$"
     },
     "hostname": {
       "$ref": "../definitions.json#/definitions/hostname"
@@ -59,6 +59,17 @@
     "access_list": {
       "type": "object",
       "readonly": true
+    },
+    "incoming_port": {
+      "type": "integer",
+      "minumum": 1,
+      "maxiumum": 65535
+    },
+    "protocols": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
     }
   },
   "links": [
@@ -86,8 +97,7 @@
       "schema": {
         "type": "object",
         "required": [
-          "type",
-          "hostname"
+          "type"
         ],
         "properties": {
           "type": {
@@ -125,6 +135,12 @@
           },
           "access_list_id": {
             "$ref": "#/definitions/access_list_id"
+          },
+          "incoming_port": {
+            "$ref": "#/definitions/incoming_port"
+          },
+          "protocols": {
+            "$ref": "#/definitions/protocols"
           }
         }
       },
@@ -181,6 +197,12 @@
           },
           "access_list_id": {
             "$ref": "#/definitions/access_list_id"
+          },
+          "incoming_port": {
+            "$ref": "#/definitions/incoming_port"
+          },
+          "protocols": {
+            "$ref": "#/definitions/protocols"
           }
         }
       },
@@ -247,6 +269,12 @@
     },
     "advanced": {
       "$ref": "#/definitions/advanced"
+    },
+    "incoming_port": {
+      "$ref": "#/definitions/incoming_port"
+    },
+    "protocols": {
+      "$ref": "#/definitions/protocols"
     }
   }
 }

+ 11 - 0
manager/src/backend/templates/stream.conf.ejs

@@ -0,0 +1,11 @@
+# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %>
+<%
+protocols.forEach(function (protocol) {
+%>
+server {
+    listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>;
+    proxy_pass <%- forward_server %>:<%- forward_port %>;
+}
+<%
+});
+%>

+ 11 - 0
manager/src/frontend/js/app/controller.js

@@ -86,6 +86,17 @@ module.exports = {
         });
     },
 
+    /**
+     * Show Stream Host Form
+     *
+     * @param model
+     */
+    showStreamHostForm: function (model) {
+        require(['./main', './host/stream_form'], function (App, View) {
+            App.UI.showModalDialog(new View({model: model}));
+        });
+    },
+
     /**
      * Show Delete Host Confirmation
      *

+ 2 - 1
manager/src/frontend/js/app/dashboard/main.ejs

@@ -1,6 +1,6 @@
 <table class="table table-condensed table-striped">
     <thead>
-        <th>Hostname</th>
+        <th>Source</th>
         <th>Destination</th>
         <th>SSL</th>
         <th>Access List</th>
@@ -13,6 +13,7 @@
                     <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>
+                    <li><a href="#" class="new-stream">Stream Host</a></li>
                 </ul>
             </div>
         </th>

+ 7 - 1
manager/src/frontend/js/app/dashboard/main.js

@@ -28,7 +28,8 @@ module.exports = Mn.View.extend({
     ui: {
         new_proxy:       'th .new-proxy',
         new_redirection: 'th .new-redirection',
-        new_404:         'th .new-404'
+        new_404:         'th .new-404',
+        new_stream:      'th .new-stream'
     },
 
     events: {
@@ -45,6 +46,11 @@ module.exports = Mn.View.extend({
         'click @ui.new_404': function (e) {
             e.preventDefault();
             Controller.show404HostForm(new HostModel.Model);
+        },
+
+        'click @ui.new_stream': function (e) {
+            e.preventDefault();
+            Controller.showStreamHostForm(new HostModel.Model);
         }
     },
 

+ 26 - 11
manager/src/frontend/js/app/dashboard/row.ejs

@@ -1,7 +1,14 @@
-<td><a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a></td>
+<td>
+    <% if (type === 'stream') { %>
+        <%- incoming_port %>
+        <%- protocols.join(', ').toUpperCase() %>
+    <% } else { %>
+        <a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a>
+    <% } %>
+</td>
 <td>
     <span class="monospace">
-        <% if (type === 'proxy') { %>
+        <% if (type === 'proxy' || type === 'stream') { %>
             <%- forward_server %>:<%- forward_port %>
         <% } else if (type === 'redirection') { %>
             <%- forward_host %>
@@ -11,19 +18,27 @@
     </span>
 </td>
 <td>
-    <% if (ssl && force_ssl) { %>
-        Forced
-    <% } else if (ssl) { %>
-        Enabled
+    <% if (type === 'stream') { %>
+        -
     <% } else { %>
-        No
+        <% if (ssl && force_ssl) { %>
+            Forced
+        <% } else if (ssl) { %>
+            Enabled
+        <% } else { %>
+            No
+        <% } %>
     <% } %>
 </td>
 <td>
-    <% if (access_list) { %>
-        <a href="#" class="access_list"><%- access_list.name %></a>
+    <% if (type === 'stream') { %>
+    -
     <% } else { %>
-        <em>None</em>
+        <% if (access_list) { %>
+            <a href="#" class="access_list"><%- access_list.name %></a>
+        <% } else { %>
+            <em>None</em>
+        <% } %>
     <% } %>
 </td>
 <td class="text-right">
@@ -31,7 +46,7 @@
         <button type="button" class="btn btn-default btn-xs renew" title="Renew SSL"><i class="fa fa-shield" aria-hidden="true"></i></button>
     <% } %>
     <button type="button" class="btn btn-default btn-xs reconfigure" title="Reconfigure Nginx"><i class="fa fa-refresh" aria-hidden="true"></i></button>
-    <button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"><i class="fa fa-code" aria-hidden="true"></i></button>
+    <button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"<%- type === 'stream' ? ' disabled' : '' %>><i class="fa fa-code" aria-hidden="true"></i></button>
     <button type="button" class="btn btn-warning btn-xs edit" title="Edit"><i class="fa fa-pencil" aria-hidden="true"></i></button>
     <button type="button" class="btn btn-danger btn-xs delete" title="Delete"><i class="fa fa-times" aria-hidden="true"></i></button>
 </td>

+ 3 - 0
manager/src/frontend/js/app/dashboard/row.js

@@ -32,6 +32,9 @@ module.exports = Mn.View.extend({
                 case '404':
                     Controller.show404HostForm(this.model);
                     break;
+                case 'stream':
+                    Controller.showStreamHostForm(this.model);
+                    break;
             }
         },
 

+ 55 - 0
manager/src/frontend/js/app/host/stream_form.ejs

@@ -0,0 +1,55 @@
+<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<% } %> Stream Host</h4>
+            </div>
+            <div class="modal-body">
+                <div class="alert alert-warning" role="alert">
+                    A Stream Host will forward a TCP/UDP connection directly to a another server on your network. <strong>There is no authentication.</strong>
+                    Note you will also have to open the incoming port in your docker configuration for this to work.
+                    <br>
+                    <br>
+                    You will not be able to use port <strong>80</strong>, <strong>81</strong> or <strong>443</strong> or any other previously configured Stream Host incoming port.
+                </div>
+                <div class="form-group">
+                    <label class="col-sm-4 control-label">Incoming Port</label>
+                    <div class="col-sm-8">
+                        <input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="incoming_port" value="<%- incoming_port ? incoming_port : '' %>" required>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-sm-4 control-label">Forwarding IP</label>
+                    <div class="col-sm-8">
+                        <input type="text" class="form-control" placeholder="192.168.0.1" name="forward_server" value="<%- forward_server %>" required>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-sm-4 control-label">Forwarding Port</label>
+                    <div class="col-sm-8">
+                        <input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="forward_port" value="<%- typeof _id === 'undefined' ? '' : forward_port %>" required>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="col-sm-offset-4 col-sm-8">
+                        <div class="checkbox">
+                            <label>
+                                <input type="checkbox" name="protocols[]" value="tcp"<%- typeof _id === 'undefined' || hasStreamProtocol('tcp') ? ' checked' : '' %>> TCP Forwarding
+                            </label>
+                        </div>
+                        <div class="checkbox">
+                            <label>
+                                <input type="checkbox" name="protocols[]" value="udp"<%- hasStreamProtocol('udp') ? ' checked' : '' %>> UDP Forwarding
+                            </label>
+                        </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>

+ 63 - 0
manager/src/frontend/js/app/host/stream_form.js

@@ -0,0 +1,63 @@
+'use strict';
+
+import Mn from 'backbone.marionette';
+
+const _          = require('lodash');
+const template   = require('./stream_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'
+    },
+
+    events: {
+        'submit @ui.form': function (e) {
+            e.preventDefault();
+            let data = _.extend({}, this.ui.form.serializeJSON());
+
+            data.type = 'stream';
+
+            // Ports are integers
+            data.incoming_port = parseInt(data.incoming_port, 10);
+            data.forward_port  = parseInt(data.forward_port, 10);
+
+            if (typeof data.protocols === 'undefined' || !data.protocols.length) {
+                alert('You must select one or more Protocols');
+                return;
+            }
+
+            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');
+                });
+        }
+    },
+
+    templateContext: {
+        hasStreamProtocol: function (protocol) {
+            return this.protocols.indexOf(protocol) !== -1;
+        }
+    }
+});

+ 3 - 1
manager/src/frontend/js/models/host.js

@@ -20,7 +20,9 @@ const model = Backbone.Model.extend({
             letsencrypt_email: '',
             accept_tos:        false,
             access_list_id:    '',
-            advanced:          ''
+            advanced:          '',
+            incoming_port:     0,
+            protocols:         []
         };
     }
 });

+ 4 - 0
rootfs/etc/nginx/nginx.conf

@@ -53,3 +53,7 @@ http {
   include /etc/nginx/conf.d/*.conf;
   include /config/nginx/*.conf;
 }
+
+stream {
+    include /config/nginx/stream/*.conf;
+}

+ 1 - 1
rootfs/etc/services.d/nginx/run

@@ -1,5 +1,5 @@
 #!/usr/bin/with-contenv bash
 
-mkdir -p /tmp/nginx /config/{nginx,logs,access} /var/lib/nginx/cache/{public,private}
+mkdir -p /tmp/nginx /config/{nginx,logs,access} /config/nginx/stream /var/lib/nginx/cache/{public,private}
 chown root /tmp/nginx
 exec nginx