From d947f819abcb5713086dc9b0d216e69d7dccb290 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Thu, 14 Feb 2019 19:39:24 -0800 Subject: [PATCH] fix: increase cache, use csp checksums over nonce (#988) attempt to address #985 --- package.json | 1 - src/server.js | 53 +++++++++-------------- src/server/sapperInlineScriptChecksums.js | 21 +++++++++ 3 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 src/server/sapperInlineScriptChecksums.js diff --git a/package.json b/package.json index 536c9531..a9492815 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "terser-webpack-plugin": "^1.2.2", "text-encoding": "^0.7.0", "tiny-queue": "^0.2.1", - "uuid": "^3.3.2", "web-animations-js": "^2.3.1", "webpack": "^4.29.3", "webpack-bundle-analyzer": "^3.0.4" diff --git a/src/server.js b/src/server.js index 2069b47c..1c540bcd 100644 --- a/src/server.js +++ b/src/server.js @@ -1,17 +1,16 @@ import * as sapper from '../__sapper__/server.js' -const express = require('express') -const compression = require('compression') -const serveStatic = require('serve-static') -const app = express() -const helmet = require('helmet') -const uuidv4 = require('uuid/v4') - -const headScriptChecksum = require('./inline-script/checksum') +import express from 'express' +import compression from 'compression' +import serveStatic from 'serve-static' +import helmet from 'helmet' +import fetch from 'node-fetch' +import inlineScriptChecksum from './inline-script/checksum' +import { sapperInlineScriptChecksums } from './server/sapperInlineScriptChecksums' const { PORT = 4002 } = process.env +const app = express() // this allows us to do e.g. `fetch('/_api/blog')` on the server -const fetch = require('node-fetch') global.fetch = (url, opts) => { if (url[0] === '/') { url = `http://localhost:${PORT}${url}` @@ -19,32 +18,23 @@ global.fetch = (url, opts) => { return fetch(url, opts) } -const debugPaths = ['/report.html', '/stats.json'] - -const debugOnly = (fn) => (req, res, next) => ( - !~debugPaths.indexOf(req.path) ? next() : fn(req, res, next) -) - -const nonDebugOnly = (fn) => (req, res, next) => ( - ~debugPaths.indexOf(req.path) ? next() : fn(req, res, next) -) - app.use(compression({ threshold: 0 })) -app.use((req, res, next) => { - res.locals.nonce = uuidv4() - next() -}) +// CSP only needs to apply to core HTML files, not debug files +// like report.html or the JS/CSS/JSON/image files +const coreHtmlFilesOnly = (fn) => (req, res, next) => { + let coreHtml = !/\.(js|css|json|png|svg|jpe?g|map)$/.test(req.path) && + !(/\/report.html/.test(req.path)) + return coreHtml ? fn(req, res, next) : next() +} -// report.html needs to have CSP disable because it has inline scripts -app.use(debugOnly(helmet())) -app.use(nonDebugOnly(helmet({ +app.use(coreHtmlFilesOnly(helmet({ contentSecurityPolicy: { directives: { scriptSrc: [ `'self'`, - `'sha256-${headScriptChecksum}'`, - (req, res) => `'nonce-${res.locals.nonce}'` + `'sha256-${inlineScriptChecksum}'`, + ...sapperInlineScriptChecksums.map(_ => `'sha256-${_}'`) ], workerSrc: [`'self'`], styleSrc: [`'self'`, `'unsafe-inline'`], @@ -60,13 +50,12 @@ app.use(nonDebugOnly(helmet({ app.use(serveStatic('static', { setHeaders: (res) => { - res.setHeader('Cache-Control', 'public,max-age=600') + res.setHeader('Cache-Control', 'public,max-age=3600') } })) -debugPaths.forEach(debugPath => { - app.use(debugPath, express.static(`__sapper__/build/client${debugPath}`)) -}) +app.use(express.static('__sapper__/build/client/report.html')) +app.use(express.static('__sapper__/build/client/stats.json')) app.use(sapper.middleware()) diff --git a/src/server/sapperInlineScriptChecksums.js b/src/server/sapperInlineScriptChecksums.js new file mode 100644 index 00000000..e4fa88df --- /dev/null +++ b/src/server/sapperInlineScriptChecksums.js @@ -0,0 +1,21 @@ +// TODO: this is a hacky solution to generating checksums for Sapper's inline scripts. +// I determined this by running `sapper export` and then checking all unique inline scripts. +// Really, this ought to be built into Sapper somehow. + +import crypto from 'crypto' + +let scripts = [ + `__SAPPER__={baseUrl:"",preloaded:[{},{}]};`, + `__SAPPER__={baseUrl:"",preloaded:[{}]};`, + `__SAPPER__={baseUrl:"",preloaded:[{},null,null,{}]};`, + `__SAPPER__={baseUrl:"",preloaded:[{},null,{}]};` +] + +if (process.env.NODE_ENV === 'production') { + // sapper adds service worker only in production + scripts = scripts.map(script => `${script}if('serviceWorker' in navigator)navigator.serviceWorker.register('/service-worker.js');`) +} + +export const sapperInlineScriptChecksums = scripts.map(script => { + return crypto.createHash('sha256').update(script).digest('base64') +})