diff --git a/.gitignore b/.gitignore index b665061e..2c4a7637 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ assets/*.css /mastodon mastodon.log assets/robots.txt +/inline-script-checksum.json diff --git a/bin/build-inline-script.js b/bin/build-inline-script.js new file mode 100644 index 00000000..3942e6d2 --- /dev/null +++ b/bin/build-inline-script.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const crypto = require('crypto') +const fs = require('fs') +const pify = require('pify') +const readFile = pify(fs.readFile.bind(fs)) +const writeFile = pify(fs.writeFile.bind(fs)) +const path = require('path') + +async function main () { + let headScriptFilepath = path.join(__dirname, '../inline-script.js') + let headScript = await readFile(headScriptFilepath, 'utf8') + headScript = `(function () {'use strict'; ${headScript}})()` + + let checksum = crypto.createHash('sha256').update(headScript).digest('base64') + + let checksumFilepath = path.join(__dirname, '../inline-script-checksum.json') + await writeFile(checksumFilepath, JSON.stringify({checksum}), 'utf8') + + let html2xxFilepath = path.join(__dirname, '../templates/2xx.html') + let html2xxFile = await readFile(html2xxFilepath, 'utf8') + html2xxFile = html2xxFile.replace( + /[\s\S]+/, + '' + ) + await writeFile(html2xxFilepath, html2xxFile, 'utf8') +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/docs/Theming.md b/docs/Theming.md index 9073d627..318fb748 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -40,7 +40,7 @@ const themes = [ export { themes } ``` -Add your theme in `templates/2xx.html`. +Add your theme in `inline-script.js`. ```js window.__themeColors = { 'default': "royalblue", diff --git a/inline-script.js b/inline-script.js new file mode 100644 index 00000000..b35def29 --- /dev/null +++ b/inline-script.js @@ -0,0 +1,36 @@ +// For perf reasons, this script is run inline to quickly set certain styles. +// To allow CSP to work correctly, we also calculate a sha256 hash during +// the build process and write it to inline-script-checksum.json. +window.__themeColors = { + 'default': 'royalblue', + scarlet: '#e04e41', + seafoam: '#177380', + hotpants: 'hotpink', + oaken: 'saddlebrown', + majesty: 'blueviolet', + gecko: '#4ab92f', + ozark: '#5263af', + cobalt: '#08439b', + sorcery: '#ae91e8', + offline: '#999999' +} +if (localStorage.store_currentInstance && localStorage.store_instanceThemes) { + let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str) + let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)] + if (theme !== 'default') { + document.body.classList.add(`theme-${theme}`) + let link = document.createElement('link') + link.rel = 'stylesheet' + link.href = `/theme-${theme}.css` + document.head.appendChild(link) + if (window.__themeColors[theme]) { + document.getElementById('theThemeColor').content = window.__themeColors[theme] + } + } +} +if (!localStorage.store_currentInstance) { + // if not logged in, show all these 'hidden-from-ssr' elements + let style = document.createElement('style') + style.textContent = '.hidden-from-ssr { opacity: 1 !important; }' + document.head.appendChild(style) +} diff --git a/package-lock.json b/package-lock.json index a60dabda..c7f33ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1563,6 +1563,11 @@ } } }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, "caniuse-api": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", @@ -2046,6 +2051,11 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" }, + "content-security-policy-builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", + "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -2398,6 +2408,11 @@ } } }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -4863,6 +4878,18 @@ "sntp": "1.0.9" } }, + "helmet-csp": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.0.tgz", + "integrity": "sha512-IGIAkWnxjRbgMXFA2/kmDqSIrIaSfZ6vhMHlSHw7jm7Gm9nVVXqwJ2B1YEpYrJsLrqY+w2Bbimk7snux9+sZAw==", + "requires": { + "camelize": "1.0.0", + "content-security-policy-builder": "2.0.0", + "dasherize": "2.0.0", + "lodash.reduce": "4.6.0", + "platform": "1.3.5" + } + }, "highlight-es": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.1.tgz", @@ -5783,6 +5810,11 @@ "lodash.keys": "3.1.2" } }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -7038,6 +7070,11 @@ } } }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" + }, "pluralize": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", diff --git a/package.json b/package.json index 842e65f4..76d57273 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,15 @@ "version": "0.1.6", "scripts": { "lint": "standard", - "dev": "run-s build-svg serve-dev", + "dev": "run-s build-svg build-inline-script serve-dev", "serve-dev": "run-p --race build-sass-watch serve", "serve": "node server.js", - "build": "cross-env NODE_ENV=production run-s globalize-css build-sass build-svg sapper-build deglobalize-css", + "build": "cross-env NODE_ENV=production run-s globalize-css build-sass build-svg build-inline-script sapper-build deglobalize-css", "sapper-build": "cross-env NODE_ENV=production sapper build", "start": "cross-env NODE_ENV=production node server.js", "build-and-start": "run-s build start", "build-svg": "node ./bin/build-svg.js", + "build-inline-script": "node ./bin/build-inline-script.js", "build-sass": "node ./bin/build-sass.js", "build-sass-watch": "node ./bin/build-sass.js --watch", "run-mastodon": "node -r esm ./bin/run-mastodon", @@ -48,6 +49,7 @@ "font-awesome-svg-png": "^1.2.2", "form-data": "^2.3.2", "glob": "^7.1.2", + "helmet-csp": "^2.7.0", "indexeddb-getall-shim": "^1.3.1", "intersection-observer": "^0.5.0", "lodash": "^4.17.5", @@ -140,6 +142,7 @@ "package.json", "package-lock.json", "server.js", + "inline-script.js", "webpack.client.config.js", "webpack.server.config.js" ] diff --git a/server.js b/server.js index 425759d9..fccaccab 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,9 @@ const compression = require('compression') const sapper = require('sapper') const serveStatic = require('serve-static') const app = express() +const csp = require('helmet-csp') + +const headScriptChecksum = require('./inline-script-checksum').checksum const { PORT = 4002 } = process.env @@ -15,6 +18,17 @@ global.fetch = (url, opts) => { app.use(compression({ threshold: 0 })) +app.use(csp({ + directives: { + scriptSrc: [`'self'`, `'sha256-${headScriptChecksum}'`], + workerSrc: [`'self'`], + styleSrc: [`'self'`, `'unsafe-inline'`], + frameSrc: [`'none'`], + objectSrc: [`'none'`], + manifestSrc: [`'self'`] + } +})) + app.use(serveStatic('assets', { setHeaders: (res) => { res.setHeader('Cache-Control', 'public,max-age=600') diff --git a/templates/2xx.html b/templates/2xx.html index d0873e63..0d6d3b13 100644 --- a/templates/2xx.html +++ b/templates/2xx.html @@ -40,42 +40,44 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o %sapper.head%
- + } +} +if (!localStorage.store_currentInstance) { + // if not logged in, show all these 'hidden-from-ssr' elements + let style = document.createElement('style') + style.textContent = '.hidden-from-ssr { opacity: 1 !important; }' + document.head.appendChild(style) +} +})()