diff --git a/index.js b/index.js index 2cd24b3..b556312 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, config) { - let { hostname, port, ssl } = config.liveReload; +let allowLiveReload = function(policyObject, liveReloadConfig) { + let { hostname, port, ssl } = liveReloadConfig; ['localhost', '0.0.0.0', hostname].filter(Boolean).forEach(function(hostname) { let protocol = ssl ? 'wss://' : 'ws://'; @@ -85,29 +85,19 @@ let allowLiveReload = function(policyObject, config) { module.exports = { name: require('./package').name, - serverMiddleware: function(config) { - 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) => { + serverMiddleware: function({ app, options }) { + // Calculate livereload settings and cache it to be reused in `contentFor` hook. + // Can't do that one in another hook cause it depends on middleware options, + // which are only available in `serverMiddleware` hook. + if (options.liveReload) { + this._liveReload = { + host: options.liveReloadHost, + port: options.liveReloadPort, + ssl: options.ssl + } + } + + app.use((req, res, next) => { if (!this._config.enabled) { next(); return; @@ -123,15 +113,15 @@ module.exports = { appendSourceList(policyObject, 'script-src', "'nonce-" + STATIC_TEST_NONCE + "'"); } - if (this._config.liveReload.enabled) { - allowLiveReload(policyObject, this._config); + if (this._liveReload) { + allowLiveReload(policyObject, this._liveReload); } // 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 = this._config.liveReload.host || 'localhost'; - let ecProtocol = this._config.liveReload.ssl ? 'https://' : 'http://'; - let ecOrigin = ecProtocol + ecHost + ':' + this._config.liveReload.port; + let ecHost = options.host || 'localhost'; + let ecProtocol = options.ssl ? 'https://' : 'http://'; + let ecOrigin = ecProtocol + ecHost + ':' + options.port; appendSourceList(policyObject, 'connect-src', ecOrigin); policyObject['report-uri'] = ecOrigin + REPORT_PATH; } @@ -157,9 +147,9 @@ module.exports = { }); let bodyParser = require('body-parser'); - 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) { + 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) { // 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' }); @@ -185,8 +175,8 @@ module.exports = { appendSourceList(policyObject, 'script-src', "'nonce-" + STATIC_TEST_NONCE + "'"); } - if (this._config.liveReload.enabled) { - allowLiveReload(policyObject, this._config); + if (this._liveReload) { + allowLiveReload(policyObject, this._liveReload); } // clone policy object cause config should not be mutated @@ -219,13 +209,34 @@ 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 prevents us from needing to + // calculate the config more than once. We can't do this in `contentFor` hook cause + // that one is executed after `serverMiddleware` and can't do it in `serverMiddleware` + // hook cause that one is only executed on `ember serve` but not on `ember build` or + // `ember test`. We can't do it in `init` hook cause app is not available by then. + included: function(app) { + let environment = app.env; + let ownConfig = readConfig(app.project, environment); // config/content-security-policy.js + let runConfig = app.project.config(); // config/environment.js + let ui = app.project.ui; + + this._config = calculateConfig(environment, ownConfig, runConfig, ui); + }, + + // holds configuration for this addon + _config: null, + + // holds live reload configuration if express server is used and live reload is enabled + _liveReload: null, }; -function calculateConfig(environment, ownConfig, liveReloadConfig, runConfig, ui) { +function calculateConfig(environment, ownConfig, runConfig, ui) { let config = { delivery: [DELIVERY_HEADER], enabled: true, - liveReload: liveReloadConfig, policy: { 'default-src': [CSP_NONE], 'script-src': [CSP_SELF], diff --git a/node-tests/e2e/deliver-test.js b/node-tests/e2e/deliver-test.js index 804c320..46d23d1 100644 --- a/node-tests/e2e/deliver-test.js +++ b/node-tests/e2e/deliver-test.js @@ -141,8 +141,12 @@ describe('e2e: delivers CSP as configured', function() { }); }); - describe('supports livereaload', function() { - it('adds CSP directives required by livereload', async function() { + describe('supports live reload', function() { + it('adds CSP directives required by live reload', async function() { + await setConfig(app, { + delivery: ['header', 'meta'], + }); + await app.startServer(); let response = await request({ @@ -152,11 +156,14 @@ describe('e2e: delivers CSP as configured', function() { } }); - 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/); + let cspInHeader = response.headers['content-security-policy-report-only']; + let cspInMetaElement = response.body.match(CSP_META_TAG_REG_EXP)[1]; + [cspInHeader, cspInMetaElement].forEach((csp) => { + 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/); + }); }); }); }); diff --git a/node-tests/unit/config-test.js b/node-tests/unit/config-test.js index 7c10088..7659022 100644 --- a/node-tests/unit/config-test.js +++ b/node-tests/unit/config-test.js @@ -11,17 +11,17 @@ describe('unit: configuration', function() { }); it('is enabled by default', function() { - let config = calculateConfig('development', {}, {}, {}, UIMock); + let config = calculateConfig('development', {}, {}, UIMock); expect(config.enabled).to.be.true; }); it('delivers CSP by HTTP header by default', function() { - let config = calculateConfig('development', {}, {}, {}, UIMock); + let config = calculateConfig('development', {}, {}, UIMock); expect(config.delivery).to.deep.equal(['header']); }); it('defaults to report only mode', function() { - let config = calculateConfig('development', {}, {}, {}, UIMock); + let config = calculateConfig('development', {}, {}, UIMock); expect(config.reportOnly).to.be.true; }); @@ -35,7 +35,6 @@ describe('unit: configuration', function() { } }, {}, - {}, UIMock ); expect(config.policy).to.deep.equal({ @@ -49,7 +48,6 @@ describe('unit: configuration', function() { let config = calculateConfig( 'development', {}, - {}, { contentSecurityPolicy: { 'default-src': ["'self'"], @@ -73,7 +71,6 @@ describe('unit: configuration', function() { let config = calculateConfig( 'development', {}, - {}, { contentSecurityPolicyMeta: true }, UIMock ); @@ -82,7 +79,6 @@ describe('unit: configuration', function() { config = calculateConfig( 'development', {}, - {}, { contentSecurityPolicyMeta: false }, UIMock ); @@ -93,7 +89,6 @@ describe('unit: configuration', function() { let config = calculateConfig( 'development', {}, - {}, { contentSecurityPolicyHeader: 'Content-Security-Policy-Report-Only' }, UIMock ); @@ -102,7 +97,6 @@ describe('unit: configuration', function() { config = calculateConfig( 'development', {}, - {}, { contentSecurityPolicyHeader: 'Content-Security-Policy' }, UIMock );