diff --git a/src/backend/internal/nginx.js b/src/backend/internal/nginx.js index 1e992992..89c43a54 100644 --- a/src/backend/internal/nginx.js +++ b/src/backend/internal/nginx.js @@ -130,6 +130,35 @@ const internalNginx = { return '/data/nginx/' + host_type + '/' + host_id + '.conf'; }, + /** + * Generates custom locations + * @param {Object} host + * @returns {Promise} + */ + renderLocations: (host) => { + return new Promise((resolve, reject) => { + let template; + + try { + template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'}); + } catch (err) { + reject(new error.ConfigurationError(err.message)); + return; + } + + let renderer = new Liquid(); + let renderedLocations = ''; + + const locationRendering = async () => { + for (let i = 0; i < host.locations.length; i++) { + renderedLocations += await renderer.parseAndRender(template, host.locations[i]); + } + } + + locationRendering().then(() => resolve(renderedLocations)); + }); + }, + /** * @param {String} host_type * @param {Object} host @@ -157,6 +186,9 @@ const internalNginx = { return; } + let locationsPromise; + let origLocations; + // Manipulate the data a bit before sending it to the template if (host_type !== 'default') { host.use_default_location = true; @@ -165,24 +197,38 @@ const internalNginx = { } } - renderEngine - .parseAndRender(template, host) - .then(config_text => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } - - resolve(true); - }) - .catch(err => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); + if (host.locations) { + origLocations = [].concat(host.locations); + locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { + host.locations = renderedLocations; }); + } else { + locationsPromise = Promise.resolve(); + } + + locationsPromise.then(() => { + renderEngine + .parseAndRender(template, host) + .then(config_text => { + fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); + + if (debug_mode) { + logger.success('Wrote config:', filename, config_text); + } + + // Restore locations array + host.locations = origLocations; + + resolve(true); + }) + .catch(err => { + if (debug_mode) { + logger.warn('Could not write ' + filename + ':', err.message); + } + + reject(new error.ConfigurationError(err.message)); + }); + }); }); }, diff --git a/src/backend/migrations/20190215115310_customlocations.js b/src/backend/migrations/20190215115310_customlocations.js new file mode 100644 index 00000000..5b55dd5e --- /dev/null +++ b/src/backend/migrations/20190215115310_customlocations.js @@ -0,0 +1,37 @@ +'use strict'; + +const migrate_name = 'custom_locations'; +const logger = require('../logger').migrate; + +/** + * Migrate + * Extends proxy_host table with locations field + * + * @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.json('locations'); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_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/models/proxy_host.js b/src/backend/models/proxy_host.js index a1217236..faa5d068 100644 --- a/src/backend/models/proxy_host.js +++ b/src/backend/models/proxy_host.js @@ -47,7 +47,7 @@ class ProxyHost extends Model { } static get jsonAttributes () { - return ['domain_names', 'meta']; + return ['domain_names', 'meta', 'locations']; } static get relationMappings () { diff --git a/src/backend/schema/endpoints/proxy-hosts.json b/src/backend/schema/endpoints/proxy-hosts.json index df7cb119..cd98355f 100644 --- a/src/backend/schema/endpoints/proxy-hosts.json +++ b/src/backend/schema/endpoints/proxy-hosts.json @@ -69,6 +69,41 @@ }, "meta": { "type": "object" + }, + "locations": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "required": [ + "forward_scheme", + "forward_host", + "forward_port", + "path" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": ["integer", "null"] + }, + "path": { + "type": "string", + "minLength": 1 + }, + "forward_scheme": { + "$ref": "#/definitions/forward_scheme" + }, + "forward_host": { + "$ref": "#/definitions/forward_host" + }, + "forward_port": { + "$ref": "#/definitions/forward_port" + }, + "advanced_config": { + "type": "string" + } + } + } } }, "properties": { @@ -128,6 +163,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } }, "links": [ @@ -215,6 +253,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } } }, @@ -285,6 +326,9 @@ }, "meta": { "$ref": "#/definitions/meta" + }, + "locations": { + "$ref": "#/definitions/locations" } } }, diff --git a/src/backend/templates/_location.conf b/src/backend/templates/_location.conf new file mode 100644 index 00000000..d31befe8 --- /dev/null +++ b/src/backend/templates/_location.conf @@ -0,0 +1,8 @@ + location {{ path }} { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}; + {{ advanced_config }} + } \ No newline at end of file diff --git a/src/backend/templates/proxy_host.conf b/src/backend/templates/proxy_host.conf index 95f850a9..fc58a43b 100644 --- a/src/backend/templates/proxy_host.conf +++ b/src/backend/templates/proxy_host.conf @@ -16,7 +16,10 @@ server { {{ advanced_config }} +{{ locations }} + {% if use_default_location %} + location / { {%- if access_list_id > 0 -%} # Access List diff --git a/src/frontend/js/app/nginx/proxy/form.ejs b/src/frontend/js/app/nginx/proxy/form.ejs index ef9b95b4..3f940f12 100644 --- a/src/frontend/js/app/nginx/proxy/form.ejs +++ b/src/frontend/js/app/nginx/proxy/form.ejs @@ -7,10 +7,22 @@
+ + +
+
+
+ +
+
+
+
+
diff --git a/src/frontend/js/app/nginx/proxy/form.js b/src/frontend/js/app/nginx/proxy/form.js index fcc394de..1e26bcf5 100644 --- a/src/frontend/js/app/nginx/proxy/form.js +++ b/src/frontend/js/app/nginx/proxy/form.js @@ -3,11 +3,14 @@ const Mn = require('backbone.marionette'); const App = require('../../main'); const ProxyHostModel = require('../../../models/proxy-host'); +const ProxyLocationModel = require('../../../models/proxy-host-location'); const template = require('./form.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs'); const accessListItemTemplate = require('./access-list-item.ejs'); +const CustomLocation = require('./location'); const Helpers = require('../../../lib/helpers'); + require('jquery-serializejson'); require('selectize'); @@ -15,6 +18,8 @@ module.exports = Mn.View.extend({ template: template, className: 'modal-dialog', + locationsCollection: new ProxyLocationModel.Collection(), + ui: { form: 'form', domain_names: 'input[name="domain_names"]', @@ -22,6 +27,8 @@ module.exports = Mn.View.extend({ buttons: '.modal-footer button', cancel: 'button.cancel', save: 'button.save', + add_location_btn: 'button.add_location', + locations_container:'.locations_container', certificate_select: 'select[name="certificate_id"]', access_list_select: 'select[name="access_list_id"]', ssl_forced: 'input[name="ssl_forced"]', @@ -32,6 +39,10 @@ module.exports = Mn.View.extend({ letsencrypt: '.letsencrypt' }, + regions: { + locations_regions: '@ui.locations_container' + }, + events: { 'change @ui.certificate_select': function () { let id = this.ui.certificate_select.val(); @@ -82,6 +93,13 @@ module.exports = Mn.View.extend({ } }, + 'click @ui.add_location_btn': function (e) { + e.preventDefault(); + + const model = new ProxyLocationModel.Model(); + this.locationsCollection.add(model); + }, + 'click @ui.save': function (e) { e.preventDefault(); @@ -93,6 +111,16 @@ module.exports = Mn.View.extend({ let view = this; let data = this.ui.form.serializeJSON(); + // Add locations + data.locations = []; + this.locationsCollection.models.forEach((location) => { + data.locations.push(location.toJSON()); + }); + + // Serialize collects path from custom locations + // This field must be removed from root object + delete data.path; + // Manipulate data.forward_port = parseInt(data.forward_port, 10); data.block_exploits = !!data.block_exploits; @@ -246,5 +274,20 @@ module.exports = Mn.View.extend({ if (typeof options.model === 'undefined' || !options.model) { this.model = new ProxyHostModel.Model(); } + + this.locationsCollection = new ProxyLocationModel.Collection(); + + // Custom locations + this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({ + collection: this.locationsCollection + })); + + // Check wether there are any location defined + if (options.model && Array.isArray(options.model.attributes.locations)) { + options.model.attributes.locations.forEach((location) => { + let m = new ProxyLocationModel.Model(location); + this.locationsCollection.add(m); + }); + } } }); diff --git a/src/frontend/js/app/nginx/proxy/location-item.ejs b/src/frontend/js/app/nginx/proxy/location-item.ejs new file mode 100644 index 00000000..66e97c4c --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/location-item.ejs @@ -0,0 +1,63 @@ +
+
+
+
+
+ +
+
+
+ + location + + +
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + <%- i18n('locations', 'delete') %> + +
+
\ No newline at end of file diff --git a/src/frontend/js/app/nginx/proxy/location.js b/src/frontend/js/app/nginx/proxy/location.js new file mode 100644 index 00000000..e9513a48 --- /dev/null +++ b/src/frontend/js/app/nginx/proxy/location.js @@ -0,0 +1,54 @@ +const locationItemTemplate = require('./location-item.ejs'); +const Mn = require('backbone.marionette'); +const App = require('../../main'); + +const LocationView = Mn.View.extend({ + template: locationItemTemplate, + className: 'location_block', + + ui: { + toggle: 'input[type="checkbox"]', + config: '.config', + delete: '.location-delete' + }, + + events: { + 'change @ui.toggle': function(el) { + if (el.target.checked) { + this.ui.config.show(); + } else { + this.ui.config.hide(); + } + }, + + 'change .model': function (e) { + const map = {}; + map[e.target.name] = e.target.value; + this.model.set(map); + }, + + 'click @ui.delete': function () { + this.model.destroy(); + } + }, + + onRender: function() { + $(this.ui.config).hide(); + }, + + templateContext: function() { + return { + i18n: App.i18n + } + } +}); + +const LocationCollectionView = Mn.CollectionView.extend({ + className: 'locations_container', + childView: LocationView +}); + +module.exports = { + LocationCollectionView, + LocationView +} \ No newline at end of file diff --git a/src/frontend/js/i18n/messages.json b/src/frontend/js/i18n/messages.json index f4695f75..c2da571e 100644 --- a/src/frontend/js/i18n/messages.json +++ b/src/frontend/js/i18n/messages.json @@ -82,7 +82,14 @@ "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", "advanced-config": "Custom Nginx Configuration", "hsts-enabled": "HSTS Enabled", - "hsts-subdomains": "HSTS Subdomains" + "hsts-subdomains": "HSTS Subdomains", + "locations": "Custom locations" + }, + "locations": { + "new_location": "Add location", + "path": "/path", + "location_label": "Define location", + "delete": "Delete" }, "ssl": { "letsencrypt": "Let's Encrypt", diff --git a/src/frontend/js/models/proxy-host-location.js b/src/frontend/js/models/proxy-host-location.js new file mode 100644 index 00000000..08459138 --- /dev/null +++ b/src/frontend/js/models/proxy-host-location.js @@ -0,0 +1,37 @@ +'use strict'; + +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function() { + return { + opened: false, + path: '', + advanced_config: '', + forward_scheme: 'http', + forward_host: '', + forward_port: '80' + } + }, + + toJSON() { + const r = Object.assign({}, this.attributes); + delete r.opened; + return r; + }, + + toggleVisibility: function () { + this.save({ + opened: !this.get('opened') + }); + } +}) + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model + }) +} \ No newline at end of file