diff --git a/examples/with-amp/nuxt.config.js b/examples/with-amp/nuxt.config.js index f45837119e..6ae265293c 100644 --- a/examples/with-amp/nuxt.config.js +++ b/examples/with-amp/nuxt.config.js @@ -31,7 +31,7 @@ module.exports = { page.html = modifyHtml(page.html) }, // This hook is called before rendering the html to the browser - 'render:route': (url, page) => { + 'render:route': (url, page, { req, res }) => { page.html = modifyHtml(page.html) } } diff --git a/lib/builder/webpack/client.config.js b/lib/builder/webpack/client.config.js index d045553741..4d4be07f98 100644 --- a/lib/builder/webpack/client.config.js +++ b/lib/builder/webpack/client.config.js @@ -92,7 +92,7 @@ module.exports = function webpackClientConfig() { new webpack.DefinePlugin( Object.assign(env, { 'process.env.NODE_ENV': JSON.stringify( - env.NODE_ENV || (this.options.dev ? 'development' : 'production') + this.options.env.NODE_ENV || (this.options.dev ? 'development' : 'production') ), 'process.env.VUE_ENV': JSON.stringify('client'), 'process.mode': JSON.stringify(this.options.mode), diff --git a/lib/builder/webpack/server.config.js b/lib/builder/webpack/server.config.js index 66faf84d58..e92939f81f 100644 --- a/lib/builder/webpack/server.config.js +++ b/lib/builder/webpack/server.config.js @@ -47,7 +47,7 @@ module.exports = function webpackServerConfig() { new webpack.DefinePlugin( Object.assign(env, { 'process.env.NODE_ENV': JSON.stringify( - env.NODE_ENV || (this.options.dev ? 'development' : 'production') + this.options.env.NODE_ENV || (this.options.dev ? 'development' : 'production') ), 'process.env.VUE_ENV': JSON.stringify('server'), 'process.mode': JSON.stringify(this.options.mode), diff --git a/lib/common/options.js b/lib/common/options.js index 6c5fab4a6b..cfd9c3b142 100755 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -313,7 +313,9 @@ Options.defaults = { push: false, shouldPush: null }, - static: {}, + static: { + prefix: true + }, gzip: { threshold: 0 }, @@ -323,7 +325,8 @@ Options.defaults = { csp: { enabled: false, hashAlgorithm: 'sha256', - allowedSources: [] + allowedSources: undefined, + policies: undefined } }, watchers: { diff --git a/lib/core/middleware/nuxt.js b/lib/core/middleware/nuxt.js index bc80a99bcf..7a7b7ed07e 100644 --- a/lib/core/middleware/nuxt.js +++ b/lib/core/middleware/nuxt.js @@ -10,7 +10,7 @@ module.exports = async function nuxtMiddleware(req, res, next) { res.statusCode = 200 try { const result = await this.renderRoute(req.url, context) - await this.nuxt.callHook('render:route', req.url, result) + await this.nuxt.callHook('render:route', req.url, result, context) const { html, cspScriptSrcHashes, @@ -68,12 +68,31 @@ module.exports = async function nuxtMiddleware(req, res, next) { } if (this.options.render.csp && this.options.render.csp.enabled) { - const allowedSources = cspScriptSrcHashes.concat(this.options.render.csp.allowedSources) + const allowedSources = this.options.render.csp.allowedSources + const policies = this.options.render.csp.policies ? {...this.options.render.csp.policies} : null + let cspStr = `script-src 'self' ${(cspScriptSrcHashes).join(' ')}` + if (Array.isArray(allowedSources)) { + // For compatible section + cspStr = `script-src 'self' ${cspScriptSrcHashes.concat(allowedSources).join(' ')}` + } else if (typeof policies === 'object' && policies !== null && !Array.isArray(policies)) { + // Set default policy if necessary + if (!policies['script-src'] || !Array.isArray(policies['script-src'])) { + policies['script-src'] = [`'self'`].concat(cspScriptSrcHashes) + } else { + policies['script-src'] = cspScriptSrcHashes.concat(policies['script-src']) + if (!policies['script-src'].includes(`'self'`)) { + policies['script-src'] = [`'self'`].concat(policies['script-src']) + } + } - res.setHeader( - 'Content-Security-Policy', - `script-src 'self' ${(allowedSources).join(' ')}` - ) + // Make content-security-policy string + let cspArr = [] + Object.keys(policies).forEach((k) => { + cspArr.push(`${k} ${policies[k].join(' ')}`) + }) + cspStr = cspArr.join('; ') + } + res.setHeader('Content-Security-Policy', cspStr) } // Send response diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 8e994b3976..f876e3d9f7 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -255,6 +255,8 @@ module.exports = class Renderer { this.options.render.static ) ) + staticMiddleware.prefix = this.options.render.static.prefix + this.useMiddleware(staticMiddleware) // Serve .nuxt/dist/ files only for production // For dev they will be served with devMiddleware diff --git a/test/basic.ssr.csp.test.js b/test/basic.ssr.csp.test.js new file mode 100644 index 0000000000..1ae4f359e6 --- /dev/null +++ b/test/basic.ssr.csp.test.js @@ -0,0 +1,122 @@ +import test from 'ava' +import { resolve } from 'path' +import rp from 'request-promise-native' +import { Nuxt, Builder } from '..' +import { interceptLog } from './helpers/console' + +const port = 4005 +const url = route => 'http://localhost:' + port + route + +// Init nuxt.js and create server listening on localhost:4005 +const startCSPTestServer = async (t, csp) => { + const options = { + rootDir: resolve(__dirname, 'fixtures/basic'), + buildDir: '.nuxt-ssr', + dev: false, + head: { + titleTemplate(titleChunk) { + return titleChunk ? `${titleChunk} - Nuxt.js` : 'Nuxt.js' + } + }, + build: { stats: false }, + render: { csp } + } + + let nuxt = null + const logSpy = await interceptLog(async () => { + nuxt = new Nuxt(options) + const builder = await new Builder(nuxt) + await builder.build() + await nuxt.listen(port, '0.0.0.0') + }) + + t.true(logSpy.calledWithMatch('DONE')) + t.true(logSpy.calledWithMatch('OPEN')) + + return nuxt +} + +test.serial('Not contain Content-Security-Policy header, when csp.enabled is not set', async t => { + const nuxt = await startCSPTestServer(t, {}) + const { headers } = await rp(url('/stateless'), { + resolveWithFullResponse: true + }) + + t.is(headers['content-security-policy'], undefined) + + await nuxt.close() +}) + +test.serial('Contain Content-Security-Policy header, when csp.enabled is only set', async t => { + const cspOption = { + enabled: true + } + + const nuxt = await startCSPTestServer(t, cspOption) + const { headers } = await rp(url('/stateless'), { + resolveWithFullResponse: true + }) + + t.regex(headers['content-security-policy'], /^script-src 'self' 'sha256-.*'$/) + + await nuxt.close() +}) + +test.serial('Contain Content-Security-Policy header, when csp.allowedSources set', async t => { + const cspOption = { + enabled: true, + allowedSources: ['https://example.com', 'https://example.io'] + } + + const nuxt = await startCSPTestServer(t, cspOption) + const { headers } = await rp(url('/stateless'), { + resolveWithFullResponse: true + }) + + t.regex(headers['content-security-policy'], /^script-src 'self' 'sha256-.*'/) + t.true(headers['content-security-policy'].includes('https://example.com')) + t.true(headers['content-security-policy'].includes('https://example.io')) + + await nuxt.close() +}) + +test.serial('Contain Content-Security-Policy header, when csp.policies set', async t => { + const cspOption = { + enabled: true, + policies: { + 'default-src': [`'none'`], + 'script-src': ['https://example.com', 'https://example.io'] + } + } + + const nuxt = await startCSPTestServer(t, cspOption) + const { headers } = await rp(url('/stateless'), { + resolveWithFullResponse: true + }) + + t.regex(headers['content-security-policy'], /default-src 'none'/) + t.regex(headers['content-security-policy'], /script-src 'self' 'sha256-.*'/) + t.true(headers['content-security-policy'].includes('https://example.com')) + t.true(headers['content-security-policy'].includes('https://example.io')) + + await nuxt.close() +}) + +test.serial('Contain Content-Security-Policy header, when csp.policies.script-src is not set', async t => { + const cspOption = { + enabled: true, + policies: { + 'default-src': [`'none'`] + } + } + + const nuxt = await startCSPTestServer(t, cspOption) + const { headers } = await rp(url('/stateless'), { + resolveWithFullResponse: true + }) + + t.regex(headers['content-security-policy'], /default-src 'none'/) + t.regex(headers['content-security-policy'], /script-src 'self' 'sha256-.*'/) + + await nuxt.close() +}) diff --git a/test/basic.ssr.test.js b/test/basic.ssr.test.js index 45d0d230fc..11e877f038 100755 --- a/test/basic.ssr.test.js +++ b/test/basic.ssr.test.js @@ -9,7 +9,7 @@ const url = route => 'http://localhost:' + port + route let nuxt = null -// Init nuxt.js and create server listening on localhost:4003 +// Init nuxt.js and create server listening on localhost:4004 test.serial('Init Nuxt.js', async t => { const options = { rootDir: resolve(__dirname, 'fixtures/basic'), @@ -22,12 +22,6 @@ test.serial('Init Nuxt.js', async t => { }, build: { stats: false - }, - render: { - csp: { - enabled: true, - allowedSources: ['https://example.com', 'https://example.io'] - } } } @@ -253,16 +247,6 @@ test('ETag Header', async t => { t.is(error.statusCode, 304) }) -test('Content-Security-Policy Header', async t => { - const { headers } = await rp(url('/stateless'), { - resolveWithFullResponse: true - }) - // Verify functionality - t.regex(headers['content-security-policy'], /script-src 'self' 'sha256-.*'/) - t.true(headers['content-security-policy'].includes('https://example.com')) - t.true(headers['content-security-policy'].includes('https://example.io')) -}) - test('/_nuxt/server-bundle.json should return 404', async t => { const err = await t.throws( rp(url('/_nuxt/server-bundle.json'), { resolveWithFullResponse: true })