diff --git a/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf index fe821fa..16657c4 100644 --- a/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf +++ b/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf @@ -7,6 +7,3 @@ ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; ssl_prefer_server_ciphers on; - -# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) -add_header Strict-Transport-Security max-age=15768000; diff --git a/src/backend/internal/dead-host.js b/src/backend/internal/dead-host.js index 9ad5b1f..d4c36a7 100644 --- a/src/backend/internal/dead-host.js +++ b/src/backend/internal/dead-host.js @@ -47,6 +47,7 @@ const internalDeadHost = { .then(() => { // At this point the domains should have been checked data.owner_user_id = access.token.getUserId(1); + data = internalHost.cleanSslHstsData(data); return deadHostModel .query() @@ -89,11 +90,11 @@ const internalDeadHost = { // Add to audit log return internalAuditLog.add(access, { - action: 'created', - object_type: 'dead-host', - object_id: row.id, - meta: data - }) + action: 'created', + object_type: 'dead-host', + object_id: row.id, + meta: data + }) .then(() => { return row; }); @@ -144,9 +145,9 @@ const internalDeadHost = { if (create_certificate) { return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) + 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; @@ -162,7 +163,9 @@ const internalDeadHost = { // 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); + }, data); + + data = internalHost.cleanSslHstsData(data, row); return deadHostModel .query() @@ -171,11 +174,11 @@ const internalDeadHost = { .then(saved_row => { // Add to audit log return internalAuditLog.add(access, { - action: 'updated', - object_type: 'dead-host', - object_id: row.id, - meta: data - }) + action: 'updated', + object_type: 'dead-host', + object_id: row.id, + meta: data + }) .then(() => { return _.omit(saved_row, omissions()); }); @@ -183,15 +186,15 @@ const internalDeadHost = { }) .then(() => { return internalDeadHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) + id: data.id, + expand: ['owner', 'certificate'] + }) .then(row => { // Configure nginx return internalNginx.configure(deadHostModel, 'dead_host', row) .then(new_meta => { row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); + row = internalHost.cleanRowCertificateMeta(row); return _.omit(row, omissions()); }); }); diff --git a/src/backend/internal/host.js b/src/backend/internal/host.js index 90c993f..3cffcd0 100644 --- a/src/backend/internal/host.js +++ b/src/backend/internal/host.js @@ -1,11 +1,40 @@ -'use strict'; - +const _ = require('lodash'); const proxyHostModel = require('../models/proxy_host'); const redirectionHostModel = require('../models/redirection_host'); const deadHostModel = require('../models/dead_host'); const internalHost = { + /** + * Makes sure that the ssl_* and hsts_* fields play nicely together. + * ie: if there is no cert, then force_ssl is off. + * if force_ssl is off, then hsts_enabled is definitely off. + * + * @param {object} data + * @param {object} [existing_data] + * @returns {object} + */ + cleanSslHstsData: function (data, existing_data) { + existing_data = existing_data === undefined ? {} : existing_data; + + let combined_data = _.assign({}, existing_data, data); + + if (!combined_data.certificate_id) { + combined_data.ssl_forced = false; + combined_data.http2_support = false; + } + + if (!combined_data.ssl_forced) { + combined_data.hsts_enabled = false; + } + + if (!combined_data.hsts_enabled) { + combined_data.hsts_subdomains = false; + } + + return combined_data; + }, + /** * used by the getAll functions of hosts, this removes the certificate meta if present * diff --git a/src/backend/internal/proxy-host.js b/src/backend/internal/proxy-host.js index 9a945d6..882d6dd 100644 --- a/src/backend/internal/proxy-host.js +++ b/src/backend/internal/proxy-host.js @@ -1,5 +1,3 @@ -'use strict'; - const _ = require('lodash'); const error = require('../lib/error'); const proxyHostModel = require('../models/proxy_host'); @@ -47,6 +45,7 @@ const internalProxyHost = { .then(() => { // At this point the domains should have been checked data.owner_user_id = access.token.getUserId(1); + data = internalHost.cleanSslHstsData(data); return proxyHostModel .query() @@ -90,11 +89,11 @@ const internalProxyHost = { // Add to audit log return internalAuditLog.add(access, { - action: 'created', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) + action: 'created', + object_type: 'proxy-host', + object_id: row.id, + meta: data + }) .then(() => { return row; }); @@ -109,7 +108,7 @@ const internalProxyHost = { */ update: (access, data) => { let create_certificate = data.certificate_id === 'new'; - +console.log('PH UPDATE:', data); if (create_certificate) { delete data.certificate_id; } @@ -145,9 +144,9 @@ const internalProxyHost = { if (create_certificate) { return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) + 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; @@ -165,6 +164,8 @@ const internalProxyHost = { domain_names: row.domain_names }, data); + data = internalHost.cleanSslHstsData(data, row); + return proxyHostModel .query() .where({id: data.id}) @@ -172,11 +173,11 @@ const internalProxyHost = { .then(saved_row => { // Add to audit log return internalAuditLog.add(access, { - action: 'updated', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) + action: 'updated', + object_type: 'proxy-host', + object_id: row.id, + meta: data + }) .then(() => { return _.omit(saved_row, omissions()); }); @@ -184,9 +185,9 @@ const internalProxyHost = { }) .then(() => { return internalProxyHost.get(access, { - id: data.id, - expand: ['owner', 'certificate', 'access_list'] - }) + id: data.id, + expand: ['owner', 'certificate', 'access_list'] + }) .then(row => { // Configure nginx return internalNginx.configure(proxyHostModel, 'proxy_host', row) diff --git a/src/backend/internal/redirection-host.js b/src/backend/internal/redirection-host.js index 9850426..7f2dea5 100644 --- a/src/backend/internal/redirection-host.js +++ b/src/backend/internal/redirection-host.js @@ -47,6 +47,7 @@ const internalRedirectionHost = { .then(() => { // At this point the domains should have been checked data.owner_user_id = access.token.getUserId(1); + data = internalHost.cleanSslHstsData(data); return redirectionHostModel .query() @@ -89,11 +90,11 @@ const internalRedirectionHost = { // Add to audit log return internalAuditLog.add(access, { - action: 'created', - object_type: 'redirection-host', - object_id: row.id, - meta: data - }) + action: 'created', + object_type: 'redirection-host', + object_id: row.id, + meta: data + }) .then(() => { return row; }); @@ -144,9 +145,9 @@ const internalRedirectionHost = { if (create_certificate) { return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) + 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; @@ -162,7 +163,9 @@ const internalRedirectionHost = { // 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); + }, data); + + data = internalHost.cleanSslHstsData(data, row); return redirectionHostModel .query() @@ -171,11 +174,11 @@ const internalRedirectionHost = { .then(saved_row => { // Add to audit log return internalAuditLog.add(access, { - action: 'updated', - object_type: 'redirection-host', - object_id: row.id, - meta: data - }) + action: 'updated', + object_type: 'redirection-host', + object_id: row.id, + meta: data + }) .then(() => { return _.omit(saved_row, omissions()); }); @@ -183,15 +186,15 @@ const internalRedirectionHost = { }) .then(() => { return internalRedirectionHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) + id: data.id, + expand: ['owner', 'certificate'] + }) .then(row => { // Configure nginx return internalNginx.configure(redirectionHostModel, 'redirection_host', row) .then(new_meta => { row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); + row = internalHost.cleanRowCertificateMeta(row); return _.omit(row, omissions()); }); }); diff --git a/src/backend/migrations/20190218060101_hsts.js b/src/backend/migrations/20190218060101_hsts.js new file mode 100644 index 0000000..752d332 --- /dev/null +++ b/src/backend/migrations/20190218060101_hsts.js @@ -0,0 +1,53 @@ +'use strict'; + +const migrate_name = 'hsts'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); + proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + + return knex.schema.table('redirection_host', function (redirection_host) { + redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); + redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] redirection_host Table altered'); + + return knex.schema.table('dead_host', function (dead_host) { + dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); + dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] dead_host Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex, Promise) { + logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); + return Promise.resolve(true); +}; diff --git a/src/backend/schema/definitions.json b/src/backend/schema/definitions.json index 6654811..eaf5595 100644 --- a/src/backend/schema/definitions.json +++ b/src/backend/schema/definitions.json @@ -187,6 +187,16 @@ "example": false, "type": "boolean" }, + "hsts_enabled": { + "description": "Is HSTS Enabled", + "example": false, + "type": "boolean" + }, + "hsts_subdomains": { + "description": "Is HSTS applicable to all subdomains", + "example": false, + "type": "boolean" + }, "ssl_provider": { "type": "string", "pattern": "^(letsencrypt|other)$" diff --git a/src/backend/schema/endpoints/dead-hosts.json b/src/backend/schema/endpoints/dead-hosts.json index 0fb4d17..0c73c3b 100644 --- a/src/backend/schema/endpoints/dead-hosts.json +++ b/src/backend/schema/endpoints/dead-hosts.json @@ -24,6 +24,12 @@ "ssl_forced": { "$ref": "../definitions.json#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "../definitions.json#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../definitions.json#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "../definitions.json#/definitions/http2_support" }, @@ -56,6 +62,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -113,6 +125,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -153,6 +171,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json index 9e033da..df7cb11 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -38,6 +38,12 @@ "ssl_forced": { "$ref": "../definitions.json#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "../definitions.json#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../definitions.json#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "../definitions.json#/definitions/http2_support" }, @@ -93,6 +99,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -174,6 +186,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -238,6 +256,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, diff --git a/src/backend/schema/endpoints/redirection-hosts.json b/src/backend/schema/endpoints/redirection-hosts.json index 804ab9d..1295fa4 100644 --- a/src/backend/schema/endpoints/redirection-hosts.json +++ b/src/backend/schema/endpoints/redirection-hosts.json @@ -32,6 +32,12 @@ "ssl_forced": { "$ref": "../definitions.json#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "../definitions.json#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../definitions.json#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "../definitions.json#/definitions/http2_support" }, @@ -73,6 +79,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_subdomains" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -140,6 +152,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, @@ -189,6 +207,12 @@ "ssl_forced": { "$ref": "#/definitions/ssl_forced" }, + "hsts_enabled": { + "$ref": "#/definitions/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "#/definitions/hsts_enabled" + }, "http2_support": { "$ref": "#/definitions/http2_support" }, diff --git a/src/backend/templates/_hsts.conf b/src/backend/templates/_hsts.conf new file mode 100644 index 0000000..cd8ec18 --- /dev/null +++ b/src/backend/templates/_hsts.conf @@ -0,0 +1,8 @@ +{% if certificate and certificate_id > 0 -%} +{% if ssl_forced == 1 or ssl_forced == true %} +{% if hsts_enabled == 1 or hsts_enabled == true %} + # HSTS (ngx_http_headers_module is required) (31536000 seconds = 1 year) + add_header Strict-Transport-Security "max-age=31536000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always; +{% endif %} +{% endif %} +{% endif %} \ No newline at end of file diff --git a/src/backend/templates/dead_host.conf b/src/backend/templates/dead_host.conf index d80536d..8d3534a 100644 --- a/src/backend/templates/dead_host.conf +++ b/src/backend/templates/dead_host.conf @@ -4,11 +4,16 @@ server { {% include "_listen.conf" %} {% include "_certificates.conf" %} +{% include "_hsts.conf" %} access_log /data/logs/dead_host-{{ id }}.log standard; {{ advanced_config }} - return 404; + location / { +{% include "_forced_ssl.conf" %} +{% include "_hsts.conf" %} + return 404; + } } {% endif %} diff --git a/src/backend/templates/proxy_host.conf b/src/backend/templates/proxy_host.conf index 3f3b80b..52e7058 100644 --- a/src/backend/templates/proxy_host.conf +++ b/src/backend/templates/proxy_host.conf @@ -10,6 +10,7 @@ server { {% include "_certificates.conf" %} {% include "_assets.conf" %} {% include "_exploits.conf" %} +{% include "_hsts.conf" %} access_log /data/logs/proxy_host-{{ id }}.log proxy; @@ -23,6 +24,7 @@ server { {%- endif %} {% include "_forced_ssl.conf" %} +{% include "_hsts.conf" %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} proxy_set_header Upgrade $http_upgrade; diff --git a/src/backend/templates/redirection_host.conf b/src/backend/templates/redirection_host.conf index f405a09..7f55e91 100644 --- a/src/backend/templates/redirection_host.conf +++ b/src/backend/templates/redirection_host.conf @@ -6,15 +6,15 @@ server { {% include "_certificates.conf" %} {% include "_assets.conf" %} {% include "_exploits.conf" %} +{% include "_hsts.conf" %} access_log /data/logs/redirection_host-{{ id }}.log standard; {{ advanced_config }} - # TODO: Preserve Path Option - location / { {% include "_forced_ssl.conf" %} +{% include "_hsts.conf" %} {% if preserve_path == 1 or preserve_path == true %} return 301 $scheme://{{ forward_domain_name }}$request_uri; diff --git a/src/frontend/js/app/nginx/dead/form.ejs b/src/frontend/js/app/nginx/dead/form.ejs index 4d2c4b5..f94d2cc 100644 --- a/src/frontend/js/app/nginx/dead/form.ejs +++ b/src/frontend/js/app/nginx/dead/form.ejs @@ -54,6 +54,24 @@ +
+
+ +
+
+
+
+ +
+
diff --git a/src/frontend/js/app/nginx/dead/form.js b/src/frontend/js/app/nginx/dead/form.js index 881778e..905fab7 100644 --- a/src/frontend/js/app/nginx/dead/form.js +++ b/src/frontend/js/app/nginx/dead/form.js @@ -22,6 +22,8 @@ module.exports = Mn.View.extend({ save: 'button.save', certificate_select: 'select[name="certificate_id"]', ssl_forced: 'input[name="ssl_forced"]', + hsts_enabled: 'input[name="hsts_enabled"]', + hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', letsencrypt: '.letsencrypt' }, @@ -36,11 +38,44 @@ module.exports = Mn.View.extend({ } let enabled = id === 'new' || parseInt(id, 10) > 0; - this.ui.ssl_forced.add(this.ui.http2_support) + + let inputs = this.ui.ssl_forced.add(this.ui.http2_support); + inputs .prop('disabled', !enabled) .parents('.form-group') .css('opacity', enabled ? 1 : 0.5); - this.ui.http2_support.prop('disabled', !enabled); + + if (!enabled) { + inputs.prop('checked', false); + } + + inputs.trigger('change'); + }, + + 'change @ui.ssl_forced': function () { + let checked = this.ui.ssl_forced.prop('checked'); + this.ui.hsts_enabled + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_enabled.prop('checked', false); + } + + this.ui.hsts_enabled.trigger('change'); + }, + + 'change @ui.hsts_enabled': function () { + let checked = this.ui.hsts_enabled.prop('checked'); + this.ui.hsts_subdomains + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_subdomains.prop('checked', false); + } }, 'click @ui.save': function (e) { @@ -55,13 +90,10 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); // Manipulate - if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') { - data.ssl_forced = true; - } - - if (typeof data.http2_support !== 'undefined') { - data.http2_support = !!data.http2_support; - } + data.hsts_enabled = !!data.hsts_enabled; + data.hsts_subdomains = !!data.hsts_subdomains; + data.http2_support = !!data.http2_support; + data.ssl_forced = !!data.ssl_forced; if (typeof data.domain_names === 'string' && data.domain_names) { data.domain_names = data.domain_names.split(','); diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index 1cc13b5..0962916 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -110,6 +110,24 @@
+
+
+ +
+
+
+
+ +
+
diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index c69c5a8..fcc394d 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -25,6 +25,8 @@ module.exports = Mn.View.extend({ certificate_select: 'select[name="certificate_id"]', access_list_select: 'select[name="access_list_id"]', ssl_forced: 'input[name="ssl_forced"]', + hsts_enabled: 'input[name="hsts_enabled"]', + hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', forward_scheme: 'select[name="forward_scheme"]', letsencrypt: '.letsencrypt' @@ -40,10 +42,44 @@ module.exports = Mn.View.extend({ } let enabled = id === 'new' || parseInt(id, 10) > 0; - this.ui.ssl_forced.add(this.ui.http2_support) + + let inputs = this.ui.ssl_forced.add(this.ui.http2_support); + inputs .prop('disabled', !enabled) .parents('.form-group') .css('opacity', enabled ? 1 : 0.5); + + if (!enabled) { + inputs.prop('checked', false); + } + + inputs.trigger('change'); + }, + + 'change @ui.ssl_forced': function () { + let checked = this.ui.ssl_forced.prop('checked'); + this.ui.hsts_enabled + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_enabled.prop('checked', false); + } + + this.ui.hsts_enabled.trigger('change'); + }, + + 'change @ui.hsts_enabled': function () { + let checked = this.ui.hsts_enabled.prop('checked'); + this.ui.hsts_subdomains + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_subdomains.prop('checked', false); + } }, 'click @ui.save': function (e) { @@ -63,14 +99,9 @@ module.exports = Mn.View.extend({ data.caching_enabled = !!data.caching_enabled; data.allow_websocket_upgrade = !!data.allow_websocket_upgrade; data.http2_support = !!data.http2_support; - - if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') { - data.ssl_forced = true; - } - - if (typeof data.http2_support !== 'undefined') { - data.http2_support = !!data.http2_support; - } + data.hsts_enabled = !!data.hsts_enabled; + data.hsts_subdomains = !!data.hsts_subdomains; + data.ssl_forced = !!data.ssl_forced; if (typeof data.domain_names === 'string' && data.domain_names) { data.domain_names = data.domain_names.split(','); @@ -132,6 +163,9 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; + this.ui.ssl_forced.trigger('change'); + this.ui.hsts_enabled.trigger('change'); + // Domain names this.ui.domain_names.selectize({ delimiter: ',', diff --git a/src/frontend/js/app/nginx/redirection/form.ejs b/src/frontend/js/app/nginx/redirection/form.ejs index d3f8da8..7cdb8a3 100644 --- a/src/frontend/js/app/nginx/redirection/form.ejs +++ b/src/frontend/js/app/nginx/redirection/form.ejs @@ -78,6 +78,24 @@
+
+
+ +
+
+
+
+ +
+
diff --git a/src/frontend/js/app/nginx/redirection/form.js b/src/frontend/js/app/nginx/redirection/form.js index 182ebf6..61e8401 100644 --- a/src/frontend/js/app/nginx/redirection/form.js +++ b/src/frontend/js/app/nginx/redirection/form.js @@ -22,6 +22,8 @@ module.exports = Mn.View.extend({ save: 'button.save', certificate_select: 'select[name="certificate_id"]', ssl_forced: 'input[name="ssl_forced"]', + hsts_enabled: 'input[name="hsts_enabled"]', + hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', letsencrypt: '.letsencrypt' }, @@ -36,11 +38,44 @@ module.exports = Mn.View.extend({ } let enabled = id === 'new' || parseInt(id, 10) > 0; - this.ui.ssl_forced.add(this.ui.http2_support) + + let inputs = this.ui.ssl_forced.add(this.ui.http2_support); + inputs .prop('disabled', !enabled) .parents('.form-group') .css('opacity', enabled ? 1 : 0.5); - this.ui.http2_support.prop('disabled', !enabled); + + if (!enabled) { + inputs.prop('checked', false); + } + + inputs.trigger('change'); + }, + + 'change @ui.ssl_forced': function () { + let checked = this.ui.ssl_forced.prop('checked'); + this.ui.hsts_enabled + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_enabled.prop('checked', false); + } + + this.ui.hsts_enabled.trigger('change'); + }, + + 'change @ui.hsts_enabled': function () { + let checked = this.ui.hsts_enabled.prop('checked'); + this.ui.hsts_subdomains + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + if (!checked) { + this.ui.hsts_subdomains.prop('checked', false); + } }, 'click @ui.save': function (e) { @@ -55,16 +90,12 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); // Manipulate - data.block_exploits = !!data.block_exploits; - data.preserve_path = !!data.preserve_path; - - if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') { - data.ssl_forced = true; - } - - if (typeof data.http2_support !== 'undefined') { - data.http2_support = !!data.http2_support; - } + data.block_exploits = !!data.block_exploits; + data.preserve_path = !!data.preserve_path; + data.http2_support = !!data.http2_support; + data.hsts_enabled = !!data.hsts_enabled; + data.hsts_subdomains = !!data.hsts_subdomains; + data.ssl_forced = !!data.ssl_forced; if (typeof data.domain_names === 'string' && data.domain_names) { data.domain_names = data.domain_names.split(','); diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index dffcec5..8c0dcdf 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -79,7 +79,9 @@ "no-ssl": "This host will not use HTTPS", "advanced": "Advanced", "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", - "advanced-config": "Custom Nginx Configuration" + "advanced-config": "Custom Nginx Configuration", + "hsts-enabled": "HSTS Enabled", + "hsts-subdomains": "HSTS Subdomains" }, "ssl": { "letsencrypt": "Let's Encrypt", diff --git a/src/frontend/js/models/dead-host.js b/src/frontend/js/models/dead-host.js index 770c997..eaacf1b 100644 --- a/src/frontend/js/models/dead-host.js +++ b/src/frontend/js/models/dead-host.js @@ -14,6 +14,8 @@ const model = Backbone.Model.extend({ certificate_id: 0, ssl_forced: false, http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, enabled: true, meta: {}, advanced_config: '', diff --git a/src/frontend/js/models/proxy-host.js b/src/frontend/js/models/proxy-host.js index 72c0737..e169fa0 100644 --- a/src/frontend/js/models/proxy-host.js +++ b/src/frontend/js/models/proxy-host.js @@ -17,6 +17,8 @@ const model = Backbone.Model.extend({ access_list_id: 0, certificate_id: 0, ssl_forced: false, + hsts_enabled: false, + hsts_subdomains: false, caching_enabled: false, allow_websocket_upgrade: false, block_exploits: false, diff --git a/src/frontend/js/models/redirection-host.js b/src/frontend/js/models/redirection-host.js index e007fa8..cb31e6f 100644 --- a/src/frontend/js/models/redirection-host.js +++ b/src/frontend/js/models/redirection-host.js @@ -15,6 +15,8 @@ const model = Backbone.Model.extend({ preserve_path: true, certificate_id: 0, ssl_forced: false, + hsts_enabled: false, + hsts_subdomains: false, block_exploits: false, http2_support: false, advanced_config: '',