diff --git a/index.js b/index.js index 752548a..2cd24b3 100644 --- a/index.js +++ b/index.js @@ -71,8 +71,8 @@ let appendSourceList = function(policyObject, name, sourceList) { }; // appends directives needed for Ember CLI live reload feature to policy object -let allowLiveReload = function(policyObject) { - let { hostname, port, ssl } = this._config.liveReload; +let allowLiveReload = function(policyObject, config) { + let { hostname, port, ssl } = config.liveReload; ['localhost', '0.0.0.0', hostname].filter(Boolean).forEach(function(hostname) { let protocol = ssl ? 'wss://' : 'ws://'; @@ -86,10 +86,28 @@ module.exports = { name: require('./package').name, serverMiddleware: function(config) { - let app = config.app; - let options = config.options; - - app.use((req, res, next) => { + let expressApp = config.app; + + let emberApp = this.app; + let environment = emberApp.env; + let ownConfig = readConfig(emberApp.project, environment); // config/content-security-policy.js + let middlewareOptions = config.options; + let liveReloadConfig = { + enabled: middlewareOptions.liveReload, + hostname: middlewareOptions.liveReloadHost, + port: middlewareOptions.liveReloadPort, + ssl: middlewareOptions.ssl + }; + let runConfig = emberApp.project.config(); // config/environment.js + let ui = emberApp.project.ui; + + // Not all information needed to calculate configuration is available by public API + // in all hooks. Especially `contentFor` hook is missing most informations that are + // required. Therefore configuration is calculated in `serverMiddleware` hook, cached + // and reused in `contentFor` hook, which is executed later. + this._config = calculateConfig(environment, ownConfig, liveReloadConfig, runConfig, ui); + + expressApp.use((req, res, next) => { if (!this._config.enabled) { next(); return; @@ -106,14 +124,14 @@ module.exports = { } if (this._config.liveReload.enabled) { - allowLiveReload(policyObject, options); + allowLiveReload(policyObject, this._config); } // only needed for headers, since report-uri cannot be specified in meta tag if (header.indexOf('Report-Only') !== -1 && !('report-uri' in policyObject)) { - let ecHost = options.host || 'localhost'; - let ecProtocol = options.ssl ? 'https://' : 'http://'; - let ecOrigin = ecProtocol + ecHost + ':' + options.port; + let ecHost = this._config.liveReload.host || 'localhost'; + let ecProtocol = this._config.liveReload.ssl ? 'https://' : 'http://'; + let ecOrigin = ecProtocol + ecHost + ':' + this._config.liveReload.port; appendSourceList(policyObject, 'connect-src', ecOrigin); policyObject['report-uri'] = ecOrigin + REPORT_PATH; } @@ -139,9 +157,9 @@ module.exports = { }); let bodyParser = require('body-parser'); - app.use(REPORT_PATH, bodyParser.json({ type: 'application/csp-report' })); - app.use(REPORT_PATH, bodyParser.json({ type: 'application/json' })); - app.use(REPORT_PATH, function(req, res, _next) { + expressApp.use(REPORT_PATH, bodyParser.json({ type: 'application/csp-report' })); + expressApp.use(REPORT_PATH, bodyParser.json({ type: 'application/json' })); + expressApp.use(REPORT_PATH, function(req, res, _next) { // eslint-disable-next-line no-console console.log(chalk.red('Content Security Policy violation:') + '\n\n' + JSON.stringify(req.body, null, 2)); res.send({ status:'ok' }); @@ -168,7 +186,7 @@ module.exports = { } if (this._config.liveReload.enabled) { - allowLiveReload(policyObject); + allowLiveReload(policyObject, this._config); } // clone policy object cause config should not be mutated @@ -201,28 +219,13 @@ module.exports = { includedCommands: function() { return require('./lib/commands'); }, - - // Configuration is only available by public API in `app` passed to `included` hook. - // We calculate configuration in `included` hook and use it in `serverMiddleware` - // and `contentFor` hooks, which are executed later. This is necessary cause Ember CLI - // does not provide a public API to read build time configuation (`ember-cli-build.js`) - // yet. `this._findHost(this).options` seems to be the only reliable way to get it in - // these hooks but is private API. - included: function(app) { - let environment = app.env; - let ownConfig = readConfig(app.project, environment); // config/content-security-policy.js - let buildConfig = app.options || {}; // build-time configuration including livereload and ssl options - let runConfig = app.project.config(); // config/environment.js - let ui = app.project.ui; - - this._config = calculateConfig(environment, ownConfig, buildConfig, runConfig, ui); - }, }; -function calculateConfig(environment, ownConfig, buildConfig, runConfig, ui) { +function calculateConfig(environment, ownConfig, liveReloadConfig, runConfig, ui) { let config = { delivery: [DELIVERY_HEADER], enabled: true, + liveReload: liveReloadConfig, policy: { 'default-src': [CSP_NONE], 'script-src': [CSP_SELF], @@ -261,14 +264,6 @@ function calculateConfig(environment, ownConfig, buildConfig, runConfig, ui) { config.reportOnly = runConfig.contentSecurityPolicyHeader !== CSP_HEADER; } - // live reload configuration is required to allow the hosts used by it - config.liveReload = { - enabled: buildConfig.liveReload, - host: buildConfig.liveReloadHost, - port: buildConfig.liveReloadPort, - ssl: buildConfig.ssl - } - // apply configuration Object.assign(config, ownConfig); diff --git a/node-tests/e2e/deliver-test.js b/node-tests/e2e/deliver-test.js index 39524cb..56ca567 100644 --- a/node-tests/e2e/deliver-test.js +++ b/node-tests/e2e/deliver-test.js @@ -122,4 +122,23 @@ describe('e2e: delivers CSP as configured', function() { expect(response.body).to.not.match(CSP_META_TAG_REG_EXP); }); }); + + describe('supports livereaload', function() { + it('adds CSP directives required by livereload', async function() { + await app.startServer(); + + let response = await request({ + url: 'http://localhost:49741', + headers: { + 'Accept': 'text/html' + } + }); + + let csp = response.headers['content-security-policy-report-only']; + expect(csp).to.match(/connect-src [^;]* ws:\/\/0.0.0.0:49741/); + expect(csp).to.match(/connect-src [^;]* ws:\/\/localhost:49741/); + expect(csp).to.match(/script-src [^;]* 0.0.0.0:49741/); + expect(csp).to.match(/script-src [^;]* localhost:49741/); + }); + }); });