diff --git a/index.js b/index.js
index 2bb927d..0750c5f 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,7 @@ const {
appendSourceList,
buildPolicyString,
calculateConfig,
+ isIndexHtmlForTesting,
readConfig
} = require('./lib/utils');
@@ -68,18 +69,29 @@ module.exports = {
// hook may be called more than once, but we only need to calculate once
if (!this._config) {
let { app, project } = this;
- let ownConfig = readConfig(project, environment);
let ui = project.ui;
+ let ownConfig = readConfig(project, environment);
let config = calculateConfig(environment, ownConfig, runConfig, ui);
- // add static test nonce if build includes tests
+ this._config = config;
+ this._policyString = buildPolicyString(config.policy);
+
+ // generate config for test environment if app includes tests
// Note: app is not defined for CLI commands
if (app && app.tests) {
- appendSourceList(config.policy, 'script-src', `'nonce-${STATIC_TEST_NONCE}'`);
- }
+ let ownConfigForTest = readConfig(project, 'test');
+ let runConfigForTest = project.config('test');
+ let configForTest = calculateConfig('test', ownConfigForTest, runConfigForTest, ui);
- this._config = config;
- this._policyString = buildPolicyString(config.policy);
+ // add static nonce required for tests
+ appendSourceList(configForTest.policy, 'script-src', `'nonce-${STATIC_TEST_NONCE}'`);
+
+ // testem requires frame-src to run
+ configForTest.policy['frame-src'] = ["'self'"];
+
+ this._configForTest = configForTest;
+ this._policyStringForTest = buildPolicyString(configForTest.policy);
+ }
}
// CSP header should only be set in FastBoot if
@@ -120,6 +132,7 @@ module.exports = {
// support live reload, executing tests in development enviroment via
// `http://localhost:4200/tests` and reporting CSP violations on CLI.
let policyObject = this._config.policy;
+ let policyObjectForTest = this._configForTest.policy;
// live reload requires some addition CSP directives
if (options.liveReload) {
@@ -128,6 +141,12 @@ module.exports = {
port: options.liveReloadPort,
ssl: options.ssl
});
+
+ allowLiveReload(policyObjectForTest, {
+ hostname: options.liveReloadHost,
+ port: options.liveReloadPort,
+ ssl: options.ssl
+ });
}
// add report URI to policy object and allow it as connection source
@@ -135,15 +154,22 @@ module.exports = {
let ecHost = options.host || 'localhost';
let ecProtocol = options.ssl ? 'https://' : 'http://';
let ecOrigin = ecProtocol + ecHost + ':' + options.port;
+
appendSourceList(policyObject, 'connect-src', ecOrigin);
+ appendSourceList(policyObjectForTest, 'connect-src', ecOrigin);
+
policyObject['report-uri'] = ecOrigin + REPORT_PATH;
+ policyObjectForTest['report-uri'] = policyObject['report-uri'];
}
this._policyString = buildPolicyString(policyObject);
+ this._policyStringForTest = buildPolicyString(policyObjectForTest);
app.use((req, res, next) => {
- let header = this._config.reportOnly ? CSP_HEADER_REPORT_ONLY : CSP_HEADER;
- let policyString = this._policyString;
+ let isRequestForTests = req.originalUrl.startsWith('/tests');
+ let config = isRequestForTests ? this._configForTest : this._config;
+ let policyString = isRequestForTests ? this._policyStringForTest : this._policyString;
+ let header = config.reportOnly ? CSP_HEADER_REPORT_ONLY : CSP_HEADER;
// clear existing headers before setting ours
res.removeHeader(CSP_HEADER);
@@ -175,21 +201,34 @@ module.exports = {
}
// inject CSP meta tag
- if (type === 'head' && this._config.delivery.indexOf('meta') !== -1) {
+ if (
+ // if addon is configured to deliver CSP by meta tag
+ ( type === 'head' && this._config.delivery.indexOf('meta') !== -1 ) ||
+ // ensure it's injected in tests/index.html to ensure consistent test results
+ type === 'test-head'
+ ) {
+ // skip head slot for tests/index.html to prevent including the CSP meta tag twice
+ if (type === 'head' && isIndexHtmlForTesting(existingContent)) {
+ return;
+ }
+
+ let config = type === 'head' ? this._config : this._configForTest;
+ let policyString = type === 'head' ? this._policyString : this._policyStringForTest;
+
this.ui.writeWarnLine(
'Content Security Policy does not support report only mode if delivered via meta element. ' +
"Either set `ENV['ember-cli-content-security-policy'].reportOnly` to `false` or remove `'meta'` " +
"from `ENV['ember-cli-content-security-policy'].delivery`.",
- this._config.reportOnly
+ config.reportOnly
);
- unsupportedDirectives(this._config.policy).forEach(function(name) {
+ unsupportedDirectives(config.policy).forEach(function(name) {
let msg = 'CSP delivered via meta does not support `' + name + '`, ' +
'per the W3C recommendation.';
console.log(chalk.yellow(msg)); // eslint-disable-line no-console
});
- return ``;
+ return ``;
}
// inject event listener needed for test support
diff --git a/lib/utils.js b/lib/utils.js
index a5d73f3..dff0a6d 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -2,6 +2,7 @@
'use strict';
+const assert = require('assert');
const fs = require('fs');
const path = require('path');
@@ -82,11 +83,6 @@ function calculateConfig(environment, ownConfig, runConfig, ui) {
reportOnly: true,
};
- // testem requires frame-src to run
- if (environment === 'test') {
- config.policy['frame-src'] = CSP_SELF;
- }
-
ui.writeWarnLine(
'Configuring ember-cli-content-security-policy using `contentSecurityPolicy`, ' +
'`contentSecurityPolicyHeader` and `contentSecurityPolicyMeta` keys in `config/environment.js` ' +
@@ -111,14 +107,6 @@ function calculateConfig(environment, ownConfig, runConfig, ui) {
// apply configuration
Object.assign(config, ownConfig);
- // test environment may not use express server and therefore requires CSP
- // delivery through meta element. Otherwise tests may fail if run via
- // `http://localhost:4200/tests` or `ember test --server` but not for
- // `ember test`.
- if (environment === 'test' && config.failTests && !config.delivery.includes('meta')) {
- config.delivery.push('meta');
- }
-
return config;
}
@@ -166,9 +154,48 @@ function appendSourceList(policyObject, directiveName, sourceList) {
policyObject[directiveName].push(sourceList);
}
+
+/**
+ * Determines based on `existingContent` passed to `contentFor` hook, if the hook
+ * is run for `index.html` or `tests/index.html`.
+ *
+ * When running this addon only meta tag for runtime configuration is injected in
+ * existingContent for sure. The runtime configuration has different value for
+ * `environment` property between index.html and tests/index.html.
+ *
+ * @param {string[]} isIndexHtmlForTesting
+ * @return {boolean}
+ */
+function isIndexHtmlForTesting(existingContent) {
+ let encodedRunTimeConfig;
+ let configRegExp = //;
+ for (let content of existingContent) {
+ let matches = content.match(configRegExp);
+
+ if (matches && matches.length >= 1) {
+ encodedRunTimeConfig = matches[1];
+ }
+ }
+ assert(
+ encodedRunTimeConfig,
+ 'Run time configuration is required in order to determine if contentFor hook is run for ' +
+ 'but seems to be missing in existing content.'
+ );
+
+ let runTimeConfig
+ try {
+ runTimeConfig = JSON.parse(decodeURIComponent(encodedRunTimeConfig));
+ } catch(error) {
+ throw new Error(`Could not decode runtime configuration cause of ${error}`);
+ }
+
+ return runTimeConfig.environment === 'test';
+}
+
module.exports = {
appendSourceList,
buildPolicyString,
calculateConfig,
+ isIndexHtmlForTesting,
readConfig
};
diff --git a/node-tests/e2e/deliver-test.js b/node-tests/e2e/deliver-test.js
index b3cf955..f77fae6 100644
--- a/node-tests/e2e/deliver-test.js
+++ b/node-tests/e2e/deliver-test.js
@@ -3,11 +3,11 @@ const denodeify = require('denodeify');
const request = denodeify(require('request'));
const AddonTestApp = require('ember-cli-addon-tests').AddonTestApp;
const {
+ CSP_META_TAG_REG_EXP,
removeConfig,
setConfig
} = require('../utils');
-const CSP_META_TAG_REG_EXP = //i;
describe('e2e: delivers CSP as configured', function() {
this.timeout(300000);
diff --git a/node-tests/e2e/test-support-test.js b/node-tests/e2e/test-support-test.js
index 3656258..ea35bb3 100644
--- a/node-tests/e2e/test-support-test.js
+++ b/node-tests/e2e/test-support-test.js
@@ -1,7 +1,11 @@
const expect = require('chai').expect;
const AddonTestApp = require('ember-cli-addon-tests').AddonTestApp;
const fs = require('fs-extra');
+const denodeify = require('denodeify');
+const request = denodeify(require('request'));
const {
+ CSP_META_TAG_REG_EXP,
+ getConfigPath,
removeConfig,
setConfig
} = require('../utils');
@@ -60,6 +64,112 @@ describe('e2e: provides test support', function() {
}
});
+ it('ensures CSP is applied in tests regradless if executed with development server or not', async function() {
+ await setConfig(app, {
+ delivery: ['header'],
+ });
+
+ await app.runEmberCommand('build');
+
+ let testsIndexHtml = await fs.readFile(app.filePath('dist/tests/index.html'), 'utf8');
+ let indexHtml = await fs.readFile(app.filePath('dist/index.html'), 'utf8');
+ expect(testsIndexHtml).to.match(CSP_META_TAG_REG_EXP);
+ expect(indexHtml).to.not.match(CSP_META_TAG_REG_EXP);
+ });
+
+ describe('it uses CSP configuration for test environment if running tests', function() {
+ before(async function() {
+ // setConfig utility does not support configuration depending on environment
+ // need to write the file manually
+ let configuration = `
+ module.exports = function(environment) {
+ return {
+ delivery: ['header', 'meta'],
+ policy: {
+ 'default-src': environment === 'test' ? ["'none'"] : ["'self'"]
+ },
+ reportOnly: false
+ };
+ };
+ `;
+ await fs.writeFile(getConfigPath(app), configuration);
+
+ await app.startServer();
+ });
+
+ after(async function() {
+ await app.stopServer();
+ });
+
+ it('uses CSP configuration for test environment for meta tag in tests/index.html', async function() {
+ let testsIndexHtml = await fs.readFile(app.filePath('dist/tests/index.html'), 'utf8');
+ let indexHtml = await fs.readFile(app.filePath('dist/index.html'), 'utf8');
+
+ let [,cspInTestsIndexHtml] = testsIndexHtml.match(CSP_META_TAG_REG_EXP);
+ let [,cspInIndexHtml] = indexHtml.match(CSP_META_TAG_REG_EXP);
+
+ expect(cspInTestsIndexHtml).to.include("default-src 'none';");
+ expect(cspInIndexHtml).to.include("default-src 'self';");
+ });
+
+ it('uses CSP configuration for test environment for CSP header serving tests/', async function() {
+ let responseForTests = await request({
+ url: 'http://localhost:49741/tests',
+ headers: {
+ 'Accept': 'text/html'
+ }
+ });
+ let responseForApp = await request({
+ url: 'http://localhost:49741',
+ headers: {
+ 'Accept': 'text/html'
+ }
+ });
+
+ let cspForTests = responseForTests.headers['content-security-policy'];
+ let cspForApp = responseForApp.headers['content-security-policy'];
+
+ expect(cspForTests).to.include("default-src 'none';");
+ expect(cspForApp).to.include("default-src 'self';");
+ });
+ });
+
+ describe('includes frame-src required by testem', function() {
+ before(async function() {
+ await setConfig(app, {
+ delivery: ['header', 'meta'],
+ reportOnly: false,
+ });
+
+ await app.startServer();
+ });
+
+ after(async function() {
+ await app.stopServer();
+
+ await removeConfig(app);
+ });
+
+ it('includes frame-src required by testem in CSP delivered by meta tag', async function() {
+ let testsIndexHtml = await fs.readFile(app.filePath('dist/tests/index.html'), 'utf8');
+ let [,cspInTestsIndexHtml] = testsIndexHtml.match(CSP_META_TAG_REG_EXP);
+
+ expect(cspInTestsIndexHtml).to.include("frame-src 'self';");
+ });
+
+ it('includes frame-src required by testem in CSP delivered by HTTP header', async function() {
+ let responseForTests = await request({
+ url: 'http://localhost:49741/tests',
+ headers: {
+ 'Accept': 'text/html'
+ }
+ });
+ let cspForTests = responseForTests.headers['content-security-policy'];
+
+ expect(cspForTests).to.include("frame-src 'self';");
+ });
+ });
+
it('does not cause tests failures if addon is disabled', async function() {
await setConfig(app, {
enabled: false,
diff --git a/node-tests/utils.js b/node-tests/utils.js
index 9476bf1..dc7de63 100644
--- a/node-tests/utils.js
+++ b/node-tests/utils.js
@@ -1,5 +1,7 @@
const fs = require('fs-extra');
+const CSP_META_TAG_REG_EXP = //i;
+
function getConfigPath(app) {
return app.filePath('config/content-security-policy.js');
}
@@ -22,6 +24,8 @@ async function removeConfig(app) {
}
module.exports = {
+ CSP_META_TAG_REG_EXP,
+ getConfigPath,
removeConfig,
setConfig,
};