diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js index 7a30e60ecb5595ccecfdb863010107800fc94c06..180b25be34408a687ce5e9d69788117d64900220 100644 --- a/spec/file_caching_test.js +++ b/spec/file_caching_test.js @@ -56,6 +56,7 @@ describe('File caching service', () => { expect(response.statusCode).toBe(200) expect(response.headers['content-type']).toBe('application/javascript; charset=utf-8') expect(response.text).toBe('this is example') + expect(response.headers['content-security-policy']).toContain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=') const response2 = await request(app).get('/test.txt') expect(response2.statusCode).toBe(200) expect(response2.headers['content-type']).toBe('text/plain; charset=utf-8') @@ -66,6 +67,7 @@ describe('File caching service', () => { const response = await request(app).get('/main.css') expect(response.statusCode).toBe(200) expect(response.headers['content-type']).toBe('text/css; charset=utf-8') + expect(response.headers['content-security-policy']).toContain('sha256-YjRiYWRlYTVhYmM5ZTZkNjE2ZGM4YjcwZWRlNzUxMmU0YjgxY2UxMWExOTI2ZjM1NzM1M2Y2MWJjNmUwMmZjMwo=') }) it('serves / as index.html', async () => { diff --git a/src/createApp.js b/src/createApp.js index ffdaa7800eec5ef5f2f64fefb328d266b9c020d3..5fab09652ee2578647440cfb4485dc094e5bae5b 100644 --- a/src/createApp.js +++ b/src/createApp.js @@ -38,7 +38,19 @@ export function createApp () { // Application-level middleware app.use(httpLogger) - app.use(helmet()) + app.use((req, res, next) => { + const { sha256Sum } = fileCache.get(req.path) + res.locals.sha256Sum = sha256Sum + next() + }) + app.use(helmet({ + contentSecurityPolicy: { + useDefaults: true, + directives: { + defaultSrc: ["'self'", (req, res) => res.locals.sha256Sum ? `'sha256-${res.locals.sha256Sum}'` : ''] + } + } + })) app.use('/healthy', health.LivenessEndpoint(healthCheck)) app.use('/ready', health.ReadinessEndpoint(healthCheck)) app.use(metricsMiddleware) diff --git a/src/files.js b/src/files.js index 9d2c45b3bd9f5253b26f94ac00d7e889ab535bf0..b059dc3c83f0dfbb9a739012d8f4e11aaea608e6 100644 --- a/src/files.js +++ b/src/files.js @@ -1,6 +1,11 @@ import fetch from 'node-fetch' +import crypto from 'crypto' class FileCache { + constructor () { + this._cache = {} + } + async warmUp (manifests, deps) { const cache = Object.fromEntries(await (async function () { const files = Object.keys(deps) @@ -20,9 +25,12 @@ class FileCache { } const response = await fetch(new URL(file, manifest.meta.baseUrl)) if (!response.ok) return null + const content = await response.buffer() + const sha256Sum = crypto.createHash('sha256').update(content).digest('base64') return [file, { 'content-type': response.headers.get('content-type'), - content: await response.buffer() + sha256Sum, + content }] } catch (e) { console.error(e) } }))).filter(data => Array.isArray(data) && data.length === 2))