Merge e4c10131f06ae364242cd547f1375b8bc7aba63b into 805968aac613fb3719b5fe8a78cdb2d7c7a7f026

This commit is contained in:
Joel Bowring 2024-12-16 22:04:52 -06:00 committed by GitHub
commit 8173dfbedd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 571 additions and 153 deletions

View File

@ -4,6 +4,8 @@ const utils = require('../lib/utils');
const streamModel = require('../models/stream');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const internalHost = require('./host');
function omissions () {
return ['is_deleted'];
@ -17,6 +19,12 @@ const internalStream = {
* @returns {Promise}
*/
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('streams:create', data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
@ -26,16 +34,44 @@ const internalStream = {
data.meta = {};
}
// streams aren't routed by domain name so don't store domain names in the DB
let data_no_domains = structuredClone(data);
delete data_no_domains.domain_names;
return streamModel
.query()
.insertAndFetch(data)
.insertAndFetch(data_no_domains)
.then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, 'stream', row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
return row;
});
})
.then((row) => {
@ -59,6 +95,12 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('streams:update', data.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
@ -70,16 +112,32 @@ const internalStream = {
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
return streamModel
.query()
.patchAndFetchById(row.id, data)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
@ -92,6 +150,17 @@ const internalStream = {
return saved_row;
});
});
})
.then(() => {
return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']})
.then((row) => {
return internalNginx.configure(streamModel, 'stream', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
});
},
@ -114,7 +183,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.first();
if (access_data.permission_visibility !== 'all') {
@ -131,6 +200,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
row = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
@ -196,14 +266,14 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ['owner']
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
throw new error.ValidationError('Stream is already enabled');
}
row.enabled = 1;
@ -249,7 +319,7 @@ const internalStream = {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
throw new error.ValidationError('Stream is already disabled');
}
row.enabled = 0;
@ -297,7 +367,7 @@ const internalStream = {
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.allowGraph('[owner,certificate]')
.orderBy('incoming_port', 'ASC');
if (access_data.permission_visibility !== 'all') {
@ -316,6 +386,13 @@ const internalStream = {
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},

View File

@ -0,0 +1,38 @@
const migrate_name = 'stream_ssl';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
exports.up = function (knex) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('stream', (table) => {
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema.table('stream', (table) => {
table.dropColumn('certificate_id');
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
});
};

View File

@ -6,6 +6,7 @@ const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
const Certificate = require('./certificate');
Model.knex(db);
@ -64,6 +65,17 @@ class Stream extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: 'stream.certificate_id',
to: 'certificate.id'
},
modify: function (qb) {
qb.where('certificate.is_deleted', 0);
}
}
};
}

View File

@ -52,6 +52,22 @@
"udp_forwarding": {
"type": "boolean"
},
"domain_names": {
"$ref": "../common.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"certificate": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./certificate-object.json"
}
]
},
"enabled": {
"$ref": "../common.json#/properties/enabled"
},

View File

@ -32,6 +32,12 @@
"udp_forwarding": {
"$ref": "../../../components/stream-object.json#/properties/udp_forwarding"
},
"domain_names": {
"$ref": "../../../components/stream-object.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
}
@ -73,7 +79,9 @@
"nickname": "Admin",
"avatar": "",
"roles": ["admin"]
}
},
"certificate_id": 0,
"certificate": null
}
}
},

View File

@ -29,56 +29,29 @@
"additionalProperties": false,
"minProperties": 1,
"properties": {
"incoming_port": {
"$ref": "../../../../components/stream-object.json#/properties/incoming_port"
},
"forwarding_host": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_host"
},
"forwarding_port": {
"$ref": "../../../../components/stream-object.json#/properties/forwarding_port"
},
"tcp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding"
},
"udp_forwarding": {
"$ref": "../../../../components/stream-object.json#/properties/udp_forwarding"
},
"domain_names": {
"$ref": "../../../../components/proxy-host-object.json#/properties/domain_names"
},
"forward_scheme": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme"
},
"forward_host": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_host"
},
"forward_port": {
"$ref": "../../../../components/proxy-host-object.json#/properties/forward_port"
"$ref": "../../../../components/stream-object.json#/properties/domain_names"
},
"certificate_id": {
"$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id"
},
"ssl_forced": {
"$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced"
},
"hsts_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled"
},
"hsts_subdomains": {
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
},
"http2_support": {
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
},
"block_exploits": {
"$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits"
},
"caching_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
},
"allow_websocket_upgrade": {
"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
},
"access_list_id": {
"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
},
"advanced_config": {
"$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config"
},
"enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/enabled"
"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
},
"meta": {
"$ref": "../../../../components/proxy-host-object.json#/properties/meta"
},
"locations": {
"$ref": "../../../../components/proxy-host-object.json#/properties/locations"
"$ref": "../../../../components/stream-object.json#/properties/meta"
}
}
}
@ -97,25 +70,16 @@
"created_on": "2024-10-08T23:23:03.000Z",
"modified_on": "2024-10-08T23:26:37.000Z",
"owner_user_id": 1,
"domain_names": ["test.example.com"],
"forward_host": "192.168.0.10",
"forward_port": 8989,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": false,
"advanced_config": "",
"incoming_port": 9090,
"forwarding_host": "router.internal",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"owner": {
"id": 1,
"created_on": "2024-10-07T22:43:55.000Z",

View File

@ -2,6 +2,7 @@
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;

View File

@ -0,0 +1,13 @@
{% if certificate and certificate_id > 0 %}
{% if certificate.provider == "letsencrypt" %}
# Let's Encrypt SSL
include conf.d/include/ssl-cache-stream.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem;
{%- else %}
# Custom SSL
ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem;
ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem;
{%- endif -%}
{%- endif -%}

View File

@ -5,12 +5,10 @@
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
listen {{ incoming_port }};
{% if ipv6 -%}
listen [::]:{{ incoming_port }};
{% else -%}
#listen [::]:{{ incoming_port }};
{% endif %}
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{%- include "_certificates_stream.conf" %}
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
@ -19,14 +17,12 @@ server {
include /data/nginx/custom/server_stream_tcp[.]conf;
}
{% endif %}
{% if udp_forwarding == 1 or udp_forwarding == true %}
{% if udp_forwarding == 1 or udp_forwarding == true -%}
server {
listen {{ incoming_port }} udp;
{% if ipv6 -%}
listen [::]:{{ incoming_port }} udp;
{% else -%}
#listen [::]:{{ incoming_port }} udp;
{% endif %}
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
# Custom

View File

@ -0,0 +1,2 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL_stream:50m;

View File

@ -0,0 +1,2 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;

View File

@ -1,6 +1,3 @@
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

View File

@ -3,8 +3,16 @@
<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>
<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">
@ -46,6 +54,137 @@
<div class="forward-type-error invalid-feedback"><%- i18n('streams', 'forward-type-error') %></div>
</div>
</div>
</div>
<!-- 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>
</form>
</div>
<div class="modal-footer">

View File

@ -2,10 +2,14 @@ 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,
@ -18,7 +22,17 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button',
switches: '.custom-switch-input',
cancel: 'button.cancel',
save: 'button.save'
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);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
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) {

View File

@ -16,7 +16,10 @@
</td>
<td>
<div>
<% if (tcp_forwarding) { %>
<% if (certificate) { %>
<span class="tag"><%- i18n('streams', 'tcp+ssl') %></span>
<% }
else if (tcp_forwarding) { %>
<span class="tag"><%- i18n('streams', 'tcp') %></span>
<% }
if (udp_forwarding) { %>
@ -24,6 +27,9 @@
<% } %>
</div>
</td>
<td>
<div><%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('all-hosts', 'none') %></div>
</td>
<td>
<%
var o = isOnline();

View File

@ -3,6 +3,7 @@
<th><%- i18n('streams', 'incoming-port') %></th>
<th><%- i18n('str', 'destination') %></th>
<th><%- i18n('streams', 'protocol') %></th>
<th><%- i18n('str', 'ssl') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>

View File

@ -88,7 +88,7 @@ module.exports = Mn.View.extend({
onRender: function () {
let view = this;
view.fetch(['owner'])
view.fetch(['owner', 'certificate'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@ -179,7 +179,9 @@
"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",
"tcp+ssl": "TCP+SSL"
},
"certificates": {
"title": "SSL Certificates",

View File

@ -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
};
}
});