瀏覽代碼

Add SSL tab to stream UI

jbowring 1 年之前
父節點
當前提交
3dbc70faa6
共有 4 個文件被更改,包括 336 次插入52 次删除
  1. 176 37
      frontend/js/app/nginx/stream/form.ejs
  2. 154 13
      frontend/js/app/nginx/stream/form.js
  3. 2 1
      frontend/js/i18n/messages.json
  4. 4 1
      frontend/js/models/stream.js

+ 176 - 37
frontend/js/app/nginx/stream/form.ejs

@@ -3,48 +3,187 @@
         <h5 class="modal-title"><%- i18n('streams', 'form-title', {id: id}) %></h5>
         <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
     </div>
-    <div class="modal-body">
+    <div class="modal-body has-tabs">
+        <div class="alert alert-danger mb-0 rounded-0" id="le-error-info" role="alert"></div>
         <form>
-            <div class="row">
-                <div class="col-sm-12 col-md-12">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
-                        <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
+                <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
+            </ul>
+            <div class="tab-content">
+                <!-- Details -->
+                <div role="tabpanel" class="tab-pane active" id="details">
+                    <div class="row">
+                        <div class="col-sm-12 col-md-12">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'incoming-port') %> <span class="form-required">*</span></label>
+                                <input name="incoming_port" type="number" class="form-control text-monospace" placeholder="eg: 8080" min="1" max="65535" value="<%- incoming_port %>" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-8 col-md-8">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
+                                <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-4 col-md-4">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
+                                <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12">
+                            <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
+                        </div>
                     </div>
                 </div>
-                <div class="col-sm-8 col-md-8">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'forwarding-host') %><span class="form-required">*</span></label>
-                        <input type="text" name="forwarding_host" class="form-control text-monospace" placeholder="example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" value="<%- forwarding_host %>" autocomplete="off" maxlength="255" required>
+
+                <!-- SSL -->
+                <div role="tabpanel" class="tab-pane" id="ssl-options">
+                    <div class="row">
+                        <div class="col-sm-12 col-md-12">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('streams', 'ssl-certificate') %></label>
+                                <select name="certificate_id" class="form-control custom-select" placeholder="<%- i18n('all-hosts', 'none') %>">
+                                    <option selected value="0" data-data="{&quot;id&quot;:0}" <%- certificate_id ? '' : 'selected' %>><%- i18n('all-hosts', 'none') %></option>
+                                    <option selected value="new" data-data="{&quot;id&quot;:&quot;new&quot;}"><%- i18n('all-hosts', 'new-cert') %></option>
+                                </select>
+                            </div>
+                        </div>
+
+                        <!-- DNS challenge -->
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('all-hosts', 'domain-names') %> <span class="form-required">*</span></label>
+                                <input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>">
+                            </div>
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input
+                                            type="checkbox"
+                                            class="custom-switch-input"
+                                            name="meta[dns_challenge]"
+                                            value="1"
+                                            checked
+                                            disabled
+                                    >
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%= i18n('ssl', 'dns-challenge') %></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <fieldset class="form-fieldset dns-challenge">
+                                <div class="text-red mb-4"><i class="fe fe-alert-triangle"></i> <%= i18n('ssl', 'certbot-warning') %></div>
+
+                                <!-- Certbot DNS plugin selection -->
+                                <div class="row">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group">
+                                            <label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label>
+                                            <select
+                                                    name="meta[dns_provider]"
+                                                    id="dns_provider"
+                                                    class="form-control custom-select"
+                                            >
+                                                <option
+                                                        value=""
+                                                        disabled
+                                                        hidden
+                                                        <%- getDnsProvider() === null ? 'selected' : '' %>
+                                                >Please Choose...</option>
+                                                <% _.each(dns_plugins, function(plugin_info, plugin_name){ %>
+                                                    <option
+                                                            value="<%- plugin_name %>"
+                                                            <%- getDnsProvider() === plugin_name ? 'selected' : '' %>
+                                                    ><%- plugin_info.name %></option>
+                                                <% }); %>
+                                            </select>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- Certbot credentials file content -->
+                                <div class="row credentials-file-content">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group">
+                                            <label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label>
+                                            <textarea
+                                                    name="meta[dns_provider_credentials]"
+                                                    class="form-control text-monospace"
+                                                    id="dns_provider_credentials"
+                                            ><%- getDnsProviderCredentials() %></textarea>
+                                            <div class="text-secondary small">
+                                                <i class="fe fe-info"></i>
+                                                <%= i18n('ssl', 'credentials-file-content-info') %>
+                                            </div>
+                                            <div class="text-red small">
+                                                <i class="fe fe-alert-triangle"></i>
+                                                <%= i18n('ssl', 'stored-as-plaintext-info') %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- DNS propagation delay -->
+                                <div class="row">
+                                    <div class="col-sm-12 col-md-12">
+                                        <div class="form-group mb-0">
+                                            <label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label>
+                                            <input
+                                                    type="number"
+                                                    min="0"
+                                                    name="meta[propagation_seconds]"
+                                                    class="form-control"
+                                                    id="propagation_seconds"
+                                                    value="<%- getPropagationSeconds() %>"
+                                            >
+                                            <div class="text-secondary small">
+                                                <i class="fe fe-info"></i>
+                                                <%= i18n('ssl', 'propagation-seconds-info') %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </fieldset>
+                        </div>
+
+                        <!-- Lets encrypt -->
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="form-label"><%- i18n('ssl', 'letsencrypt-email') %> <span class="form-required">*</span></label>
+                                <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required disabled>
+                            </div>
+                        </div>
+                        <div class="col-sm-12 col-md-12 letsencrypt">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="meta[letsencrypt_agree]" value="1" required disabled>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%= i18n('ssl', 'letsencrypt-agree', {url: 'https://letsencrypt.org/repository/'}) %> <span class="form-required">*</span></span>
+                                </label>
+                            </div>
+                        </div>
                     </div>
                 </div>
-                <div class="col-sm-4 col-md-4">
-                    <div class="form-group">
-                        <label class="form-label"><%- i18n('streams', 'forwarding-port') %> <span class="form-required">*</span></label>
-                        <input name="forwarding_port" type="number" class="form-control text-monospace" placeholder="eg: 80" min="1" max="65535" value="<%- forwarding_port %>" required>
-                    </div>
-                </div>
-                <div class="col-sm-6 col-md-6">
-                    <div class="form-group">
-                        <label class="custom-switch">
-                            <input type="checkbox" class="custom-switch-input" name="tcp_forwarding" value="1"<%- tcp_forwarding ? ' checked' : '' %>>
-                            <span class="custom-switch-indicator"></span>
-                            <span class="custom-switch-description"><%- i18n('streams', 'tcp-forwarding') %></span>
-                        </label>
-                    </div>
-                </div>
-                <div class="col-sm-6 col-md-6">
-                    <div class="form-group">
-                        <label class="custom-switch">
-                            <input type="checkbox" class="custom-switch-input" name="udp_forwarding" value="1"<%- udp_forwarding ? ' checked' : '' %>>
-                            <span class="custom-switch-indicator"></span>
-                            <span class="custom-switch-description"><%- i18n('streams', 'udp-forwarding') %></span>
-                        </label>
-                    </div>
-                </div>
-                <div class="col-sm-12 col-md-12">
-                    <div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
-                </div>
             </div>
         </form>
     </div>

+ 154 - 13
frontend/js/app/nginx/stream/form.js

@@ -1,24 +1,38 @@
-const Mn          = require('backbone.marionette');
-const App         = require('../../main');
-const StreamModel = require('../../../models/stream');
-const template    = require('./form.ejs');
+const Mn            = require('backbone.marionette');
+const App           = require('../../main');
+const StreamModel   = require('../../../models/stream');
+const template      = require('./form.ejs');
+const dns_providers = require('../../../../../global/certbot-dns-plugins');
 
 require('jquery-serializejson');
 require('jquery-mask-plugin');
 require('selectize');
+const Helpers = require("../../../lib/helpers");
+const certListItemTemplate = require("../certificates-list-item.ejs");
+const i18n = require("../../i18n");
 
 module.exports = Mn.View.extend({
     template:  template,
     className: 'modal-dialog',
 
     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'
+        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',
+        le_error_info:            '#le-error-info',
+        certificate_select:       'select[name="certificate_id"]',
+        domain_names:             'input[name="domain_names"]',
+        dns_challenge_switch:     'input[name="meta[dns_challenge]"]',
+        dns_challenge_content:    '.dns-challenge',
+        dns_provider:             'select[name="meta[dns_provider]"]',
+        credentials_file_content: '.credentials-file-content',
+        dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
+        propagation_seconds:      'input[name="meta[propagation_seconds]"]',
+        letsencrypt:              '.letsencrypt'
     },
 
     events: {
@@ -48,6 +62,35 @@ module.exports = Mn.View.extend({
             data.tcp_forwarding  = !!data.tcp_forwarding;
             data.udp_forwarding  = !!data.udp_forwarding;
 
+            if (typeof data.meta === 'undefined') data.meta = {};
+            data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
+            data.meta.dns_challenge = true;
+
+            if (data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
+
+            if (typeof data.domain_names === 'string' && data.domain_names) {
+                data.domain_names = data.domain_names.split(',');
+            }
+
+            // Check for any domain names containing wildcards, which are not allowed with letsencrypt
+            if (data.certificate_id === 'new') {
+                let domain_err = false;
+                if (!data.meta.dns_challenge) {
+                    data.domain_names.map(function (name) {
+                        if (name.match(/\*/im)) {
+                            domain_err = true;
+                        }
+                    });
+                }
+
+                if (domain_err) {
+                    alert(i18n('ssl', 'no-wildcard-without-dns'));
+                    return;
+                }
+            } else {
+                data.certificate_id = parseInt(data.certificate_id, 10);
+            }
+
             let method = App.Api.Nginx.Streams.create;
             let is_new = true;
 
@@ -70,10 +113,108 @@ module.exports = Mn.View.extend({
                     });
                 })
                 .catch(err => {
-                    alert(err.message);
+                    let more_info = '';
+                    if (err.code === 500 && err.debug) {
+                        try {
+                            more_info = JSON.parse(err.debug).debug.stack.join("\n");
+                        } catch (e) {
+                        }
+                    }
+                    this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>` : ''}`;
+                    this.ui.le_error_info.show();
+                    this.ui.le_error_info[0].scrollIntoView();
                     this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
+                    this.ui.save.removeClass('btn-loading');
                 });
-        }
+        },
+
+        'change @ui.certificate_select': function () {
+            let id = this.ui.certificate_select.val();
+            if (id === 'new') {
+                this.ui.letsencrypt.show().find('input').prop('disabled', false);
+                this.ui.domain_names.prop('required', 'required');
+
+                this.ui.dns_challenge_switch
+                    .prop('disabled', true)
+                    .parents('.form-group')
+                    .css('opacity', 0.5);
+
+                this.ui.dns_provider.prop('required', 'required');
+                const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
+                if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
+                    this.ui.dns_provider_credentials.prop('required', 'required');
+                }
+                this.ui.dns_challenge_content.show();
+            } else {
+                this.ui.letsencrypt.hide().find('input').prop('disabled', true);
+            }
+        },
+
+        'change @ui.dns_provider': function () {
+            const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
+            if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
+                this.ui.dns_provider_credentials.prop('required', 'required');
+                this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
+                this.ui.credentials_file_content.show();
+            } else {
+                this.ui.dns_provider_credentials.prop('required', false);
+                this.ui.credentials_file_content.hide();
+            }
+        },
+    },
+
+    templateContext: {
+        getLetsencryptEmail: function () {
+            return App.Cache.User.get('email');
+        },
+        getDnsProvider: function () {
+            return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
+        },
+        getDnsProviderCredentials: function () {
+            return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
+        },
+        getPropagationSeconds: function () {
+            return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
+        },
+        dns_plugins: dns_providers,
+    },
+
+    onRender: function () {
+        let view = this;
+
+        // Certificates
+        this.ui.le_error_info.hide();
+        this.ui.dns_challenge_content.hide();
+        this.ui.credentials_file_content.hide();
+        this.ui.letsencrypt.hide();
+        this.ui.certificate_select.selectize({
+            valueField:       'id',
+            labelField:       'nice_name',
+            searchField:      ['nice_name', 'domain_names'],
+            create:           false,
+            preload:          true,
+            allowEmptyOption: true,
+            render:           {
+                option: function (item) {
+                    item.i18n         = App.i18n;
+                    item.formatDbDate = Helpers.formatDbDate;
+                    return certListItemTemplate(item);
+                }
+            },
+            load:             function (query, callback) {
+                App.Api.Nginx.Certificates.getAll()
+                    .then(rows => {
+                        callback(rows);
+                    })
+                    .catch(err => {
+                        console.error(err);
+                        callback();
+                    });
+            },
+            onLoad:           function () {
+                view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
+            }
+        });
     },
 
     initialize: function (options) {

+ 2 - 1
frontend/js/i18n/messages.json

@@ -179,7 +179,8 @@
       "delete-confirm": "Are you sure you want to delete this Stream?",
       "help-title": "What is a Stream?",
       "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy.",
-      "search": "Search Incoming Port…"
+      "search": "Search Incoming Port…",
+      "ssl-certificate": "SSL Certificate for TCP Forwarding"
     },
     "certificates": {
       "title": "SSL Certificates",

+ 4 - 1
frontend/js/models/stream.js

@@ -15,8 +15,11 @@ const model = Backbone.Model.extend({
             udp_forwarding:  false,
             enabled:         true,
             meta:            {},
+            certificate_id:  0,
+            domain_names:    [],
             // The following are expansions:
-            owner:           null
+            owner:           null,
+            certificate:     null
         };
     }
 });