From 1454fd6b083318f1b73f2f333cc06224d5e4a24f Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Tue, 6 Aug 2019 00:27:27 +0200 Subject: [PATCH] fixes livereload --- index.js | 54 +++++++++++++++++++--------------- node-tests/e2e/deliver-test.js | 50 ++++++++++++++++++++++++++++++- node-tests/unit/config-test.js | 12 ++------ 3 files changed, 82 insertions(+), 34 deletions(-) diff --git a/index.js b/index.js index 752548a..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) { - let { hostname, port, ssl } = this._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,9 +85,17 @@ let allowLiveReload = function(policyObject) { module.exports = { name: require('./package').name, - serverMiddleware: function(config) { - let app = config.app; - let options = config.options; + 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) { @@ -105,8 +113,8 @@ module.exports = { appendSourceList(policyObject, 'script-src', "'nonce-" + STATIC_TEST_NONCE + "'"); } - if (this._config.liveReload.enabled) { - allowLiveReload(policyObject, options); + if (this._liveReload) { + allowLiveReload(policyObject, this._liveReload); } // only needed for headers, since report-uri cannot be specified in meta tag @@ -167,8 +175,8 @@ module.exports = { appendSourceList(policyObject, 'script-src', "'nonce-" + STATIC_TEST_NONCE + "'"); } - if (this._config.liveReload.enabled) { - allowLiveReload(policyObject); + if (this._liveReload) { + allowLiveReload(policyObject, this._liveReload); } // clone policy object cause config should not be mutated @@ -204,22 +212,28 @@ module.exports = { // 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. + // 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 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); + 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, buildConfig, runConfig, ui) { +function calculateConfig(environment, ownConfig, runConfig, ui) { let config = { delivery: [DELIVERY_HEADER], enabled: true, @@ -261,14 +275,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..858717d 100644 --- a/node-tests/e2e/deliver-test.js +++ b/node-tests/e2e/deliver-test.js @@ -6,13 +6,27 @@ const fs = require('fs-extra'); const CSP_META_TAG_REG_EXP = //i; +function getConfigPath(app) { + return app.filePath('config/content-security-policy.js'); +} + async function setConfig(app, config) { - let file = app.filePath('config/content-security-policy.js'); + let file = getConfigPath(app); let content = `module.exports = function() { return ${JSON.stringify(config)}; }`; await fs.writeFile(file, content); } +async function removeConfig(app) { + let file = getConfigPath(app); + + if (!fs.existsSync(file)) { + return; + } + + await fs.remove(file); +} + describe('e2e: delivers CSP as configured', function() { this.timeout(300000); @@ -24,6 +38,10 @@ describe('e2e: delivers CSP as configured', function() { await app.create('default', { noFixtures: true }); }); + afterEach(async function() { + await removeConfig(app); + }); + // Server isn't shutdown successfully if `app.startServer()` and `app.stopServer()` // are not wrapped inside a describe block. Therefore all tests after the first one // fail with a "Port 49741 is already in use" error. @@ -122,4 +140,34 @@ describe('e2e: delivers CSP as configured', function() { expect(response.body).to.not.match(CSP_META_TAG_REG_EXP); }); }); + + describe('supports live reload', function() { + afterEach(async function() { + await app.stopServer(); + }); + + it('adds CSP directives required by live reload', async function() { + await setConfig(app, { + delivery: ['header', 'meta'], + }); + + await app.startServer(); + + let response = await request({ + url: 'http://localhost:49741', + headers: { + 'Accept': 'text/html' + } + }); + + 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 );