Skip to content

Commit

Permalink
livereload config is only available from middleware options
Browse files Browse the repository at this point in the history
  • Loading branch information
jelhan committed Aug 7, 2019
1 parent da41a5c commit f8f9e11
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 53 deletions.
85 changes: 48 additions & 37 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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://';
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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' });
Expand All @@ -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
Expand Down Expand Up @@ -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],
Expand Down
21 changes: 14 additions & 7 deletions node-tests/e2e/deliver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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/);
});
});
});
});
12 changes: 3 additions & 9 deletions node-tests/unit/config-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand All @@ -35,7 +35,6 @@ describe('unit: configuration', function() {
}
},
{},
{},
UIMock
);
expect(config.policy).to.deep.equal({
Expand All @@ -49,7 +48,6 @@ describe('unit: configuration', function() {
let config = calculateConfig(
'development',
{},
{},
{
contentSecurityPolicy: {
'default-src': ["'self'"],
Expand All @@ -73,7 +71,6 @@ describe('unit: configuration', function() {
let config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyMeta: true },
UIMock
);
Expand All @@ -82,7 +79,6 @@ describe('unit: configuration', function() {
config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyMeta: false },
UIMock
);
Expand All @@ -93,7 +89,6 @@ describe('unit: configuration', function() {
let config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyHeader: 'Content-Security-Policy-Report-Only' },
UIMock
);
Expand All @@ -102,7 +97,6 @@ describe('unit: configuration', function() {
config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyHeader: 'Content-Security-Policy' },
UIMock
);
Expand Down

0 comments on commit f8f9e11

Please sign in to comment.