Skip to content

Commit

Permalink
set CSP header in FastBoot
Browse files Browse the repository at this point in the history
  • Loading branch information
jelhan committed Sep 28, 2019
1 parent 7e5e2bf commit e5fe84a
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 42 deletions.
34 changes: 34 additions & 0 deletions addon/instance-initializers/content-security-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assert } from '@ember/debug';

// reads addon config stored in meta element
function readAddonConfig(appInstance) {
let config = appInstance.resolveRegistration('config:environment');
let addonConfig = config['ember-cli-content-security-policy'];

// TODO: do not require policy to be stored in config object
// if already available through CSP meta element
assert(
'Required configuration is available at run-time',
addonConfig.hasOwnProperty('reportOnly') && addonConfig.hasOwnProperty('policy')
);

return config['ember-cli-content-security-policy'];
}

export function initialize(appInstance) {
let fastboot = appInstance.lookup('service:fastboot');

if (!fastboot || !fastboot.get('isFastBoot')) {
// nothing to do if application does not run in FastBoot or
// does not even have a FastBoot service
return;
}

let { policy, reportOnly } = readAddonConfig(appInstance);
let header = reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
fastboot.get('response.headers').set(header, policy);
}

export default {
initialize
};
1 change: 1 addition & 0 deletions fastboot/instance-initializers/content-security-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, initialize } from 'ember-cli-content-security-policy/instance-initializers/content-security-policy';
83 changes: 52 additions & 31 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,55 @@ let allowLiveReload = function(policyObject, liveReloadConfig) {
module.exports = {
name: require('./package').name,

// Configuration is only available by public API in `app` passed to some hook.
// We calculate configuration in `config` 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.
//
// The same applies to policy string generation. It's also calculated in `config`
// hook and reused in both others. But this one might be overriden in `serverMiddleware`
// hook to support live reload. This is safe because `serverMiddleware` hook is executed
// before `contentFor` hook.
//
// Only a small subset of the configuration is required at run time in order to support
// FastBoot. This one is returned here as default configuration in order to make it
// available at run time.
config: function(environment, runConfig) {
// calculate configuration and policy string
// 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 config = calculateConfig(environment, ownConfig, runConfig, ui);

// add static test nonce if build includes tests
// Note: app is not defined for CLI commands
if (app && app.tests) {
appendSourceList(config.policy, 'script-src', `'nonce-${STATIC_TEST_NONCE}'`);
}

this._config = config;
this._policyString = buildPolicyString(config.policy);
}

// provide configuration needed at run-time for FastBoot support (if needed)
// TODO: only inject if application uses FastBoot
if (!this._config.enabled || !this._config.delivery.includes('header')) {
return {};
}

return {
'ember-cli-content-security-policy': {
policy: this._policyString,
reportOnly: this._config.reportOnly,
},
};
},

serverMiddleware: function({ app, options }) {
// Configuration is not changeable at run-time. Therefore it's safe to not
// register the express middleware at all if addon is disabled and
Expand Down Expand Up @@ -115,6 +164,7 @@ module.exports = {
return;
}

// inject CSP meta tag
if (type === 'head' && this._config.delivery.indexOf(DELIVERY_META) !== -1) {
this.ui.writeWarnLine(
'Content Security Policy does not support report only mode if delivered via meta element. ' +
Expand All @@ -132,6 +182,7 @@ module.exports = {
return `<meta http-equiv="${CSP_HEADER}" content="${this._policyString}">`;
}

// inject event listener needed for test support
if (type === 'test-body' && this._config.failTests) {
let qunitDependency = (new VersionChecker(this)).for('qunit');
if (qunitDependency.exists() && qunitDependency.lt('2.9.2')) {
Expand All @@ -157,8 +208,8 @@ module.exports = {
`;
}

// Add nonce to <script> tag inserted by Ember CLI to assert that test file was loaded.
if (type === 'test-body-footer') {
// Add nonce to <script> tag inserted by ember-cli to assert that test file was loaded.
existingContent.forEach((entry, index) => {
if (/<script>\s*Ember.assert\(.*EmberENV.TESTS_FILE_LOADED\);\s*<\/script>/.test(entry)) {
existingContent[index] = entry.replace('<script>', '<script nonce="' + STATIC_TEST_NONCE + '">');
Expand All @@ -171,36 +222,6 @@ module.exports = {
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.
//
// The same applies to policy string generation. It's also calculated in `included`
// hook and reused in both others. But this one might be overriden in `serverMiddleware`
// hook to support live reload. This is safe because `serverMiddleware` hook is executed
// before `contentFor` hook.
included: function(app) {
this._super.included.apply(this, arguments);

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;
let config = calculateConfig(environment, ownConfig, runConfig, ui);

// add static test nonce if build includes tests
if (app.tests) {
appendSourceList(config.policy, 'script-src', `'nonce-${STATIC_TEST_NONCE}'`);
}

this._config = config;
this._policyString = buildPolicyString(config.policy);
},

// holds configuration for this addon
_config: null,

Expand Down
122 changes: 122 additions & 0 deletions node-tests/e2e/fastboot-support-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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 {
removeConfig,
setConfig
} = require('../utils');

describe('e2e: fastboot integration', function() {
this.timeout(300000);

let app;
let serverProcess;
let serverPromise;

// code to start and stop fastboot app server is highly inspired by ember-cli-addon-tests
// https://github.com/tomdale/ember-cli-addon-tests/blob/master/lib/commands/start-server.js
function startServer() {
return new Promise((resolve, reject) => {
serverPromise = app.run('node', 'server.js', {
onOutput(output, child) {
// detect start of fastboot app server
if (output.includes('HTTP server started')) {
serverProcess = child;
resolve();
}
},
}).catch(reject);
});
}

before(async function() {
app = new AddonTestApp();

await app.create('default', {
noFixtures: true,
skipNpm: true,
});

await app.editPackageJSON(pkg => {
pkg.devDependencies['ember-cli-fastboot'] = "*";
pkg.devDependencies['fastboot-app-server'] = "*";
});

await app.run('npm', 'install');

// Quick Start instructions of FastBoot App Server
// https://github.com/ember-fastboot/fastboot-app-server
await fs.writeFile(app.filePath('server.js'),
`
const FastBootAppServer = require('fastboot-app-server');
let server = new FastBootAppServer({
distPath: 'dist',
port: 49742,
});
server.start();
`
);
});

afterEach(async function() {
// stop fastboot app server
if (process.platform === 'win32') {
serverProcess.send({ kill: true });
} else {
serverProcess.kill('SIGINT');
}

// wait until sever terminated
await serverPromise;

await removeConfig(app);
});

it('sets CSP header if served via FastBoot', async function() {
await app.runEmberCommand('build');
await startServer();

let response = await request({
url: 'http://localhost:49742',
headers: {
'Accept': 'text/html'
},
});

expect(response.headers).to.include.key('content-security-policy-report-only');
});

it('does not set CSP header if disabled', async function() {
await setConfig(app, { enabled: false });
await app.runEmberCommand('build');
await startServer();

let response = await request({
url: 'http://localhost:49742',
headers: {
'Accept': 'text/html'
},
});

expect(response.headers).to.not.include.key('content-security-policy-report-only');
});

it('does not set CSP header if delivery does not include header', async function() {
await setConfig(app, { delivery: ['meta'] });
await app.runEmberCommand('build');
await startServer();

let response = await request({
url: 'http://localhost:49742',
headers: {
'Accept': 'text/html'
},
});

expect(response.headers).to.not.include.key('content-security-policy-report-only');
});
});
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
"start": "ember serve",
"test": "ember test",
"test:all": "ember try:each",
"test:node": "mocha node-tests/**"
"test:node": "for i in node-tests/*/*; do mocha $i; done"
},
"dependencies": {
"body-parser": "^1.17.0",
"chalk": "^2.0.0",
"ember-cli-babel": "^7.1.2",
"ember-cli-version-checker": "^3.1.3"
},
"devDependencies": {
Expand All @@ -33,9 +34,9 @@
"denodeify": "^1.2.1",
"ember-cli": "~3.7.1",
"ember-cli-addon-tests": "^0.11.1",
"ember-cli-babel": "^7.1.2",
"ember-cli-dependency-checker": "^3.0.0",
"ember-cli-eslint": "^4.2.3",
"ember-cli-fastboot": "^2.2.1",
"ember-cli-htmlbars": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^1.0.3",
"ember-cli-inject-live-reload": "^1.8.2",
Expand Down
Loading

0 comments on commit e5fe84a

Please sign in to comment.