Fixes #25
This commit is contained in:
Nolan Lawson 2018-04-14 15:50:16 -07:00 committed by GitHub
parent 6230c2703e
commit 283bc78b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 163 additions and 38 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ assets/*.css
/mastodon /mastodon
mastodon.log mastodon.log
assets/robots.txt assets/robots.txt
/inline-script-checksum.json

View File

@ -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(
/<!-- insert inline script here -->[\s\S]+<!-- end insert inline script here -->/,
'<!-- insert inline script here --><script>' + headScript + '</script><!-- end insert inline script here -->'
)
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
}
main().catch(err => {
console.error(err)
process.exit(1)
})

View File

@ -40,7 +40,7 @@ const themes = [
export { themes } export { themes }
``` ```
Add your theme in `templates/2xx.html`. Add your theme in `inline-script.js`.
```js ```js
window.__themeColors = { window.__themeColors = {
'default': "royalblue", 'default': "royalblue",

36
inline-script.js Normal file
View File

@ -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)
}

37
package-lock.json generated
View File

@ -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": { "caniuse-api": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", "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", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" "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": { "content-type": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "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": { "date-now": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@ -4863,6 +4878,18 @@
"sntp": "1.0.9" "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": { "highlight-es": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.1.tgz", "resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.1.tgz",
@ -5783,6 +5810,11 @@
"lodash.keys": "3.1.2" "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": { "lodash.uniq": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "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": { "pluralize": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz",

View File

@ -4,14 +4,15 @@
"version": "0.1.6", "version": "0.1.6",
"scripts": { "scripts": {
"lint": "standard", "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-dev": "run-p --race build-sass-watch serve",
"serve": "node server.js", "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", "sapper-build": "cross-env NODE_ENV=production sapper build",
"start": "cross-env NODE_ENV=production node server.js", "start": "cross-env NODE_ENV=production node server.js",
"build-and-start": "run-s build start", "build-and-start": "run-s build start",
"build-svg": "node ./bin/build-svg.js", "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": "node ./bin/build-sass.js",
"build-sass-watch": "node ./bin/build-sass.js --watch", "build-sass-watch": "node ./bin/build-sass.js --watch",
"run-mastodon": "node -r esm ./bin/run-mastodon", "run-mastodon": "node -r esm ./bin/run-mastodon",
@ -48,6 +49,7 @@
"font-awesome-svg-png": "^1.2.2", "font-awesome-svg-png": "^1.2.2",
"form-data": "^2.3.2", "form-data": "^2.3.2",
"glob": "^7.1.2", "glob": "^7.1.2",
"helmet-csp": "^2.7.0",
"indexeddb-getall-shim": "^1.3.1", "indexeddb-getall-shim": "^1.3.1",
"intersection-observer": "^0.5.0", "intersection-observer": "^0.5.0",
"lodash": "^4.17.5", "lodash": "^4.17.5",
@ -140,6 +142,7 @@
"package.json", "package.json",
"package-lock.json", "package-lock.json",
"server.js", "server.js",
"inline-script.js",
"webpack.client.config.js", "webpack.client.config.js",
"webpack.server.config.js" "webpack.server.config.js"
] ]

View File

@ -3,6 +3,9 @@ const compression = require('compression')
const sapper = require('sapper') const sapper = require('sapper')
const serveStatic = require('serve-static') const serveStatic = require('serve-static')
const app = express() const app = express()
const csp = require('helmet-csp')
const headScriptChecksum = require('./inline-script-checksum').checksum
const { PORT = 4002 } = process.env const { PORT = 4002 } = process.env
@ -15,6 +18,17 @@ global.fetch = (url, opts) => {
app.use(compression({ threshold: 0 })) 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', { app.use(serveStatic('assets', {
setHeaders: (res) => { setHeaders: (res) => {
res.setHeader('Cache-Control', 'public,max-age=600') res.setHeader('Cache-Control', 'public,max-age=600')

View File

@ -40,42 +40,44 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o
%sapper.head% %sapper.head%
</head> </head>
<body> <body>
<script> <!-- auto-generated w/ build-inline-script.js -->
<!-- load theme on startup (handled outside of Sapper/Svelte) --> <!-- insert inline script here --><script>(function () {'use strict'; // For perf reasons, this script is run inline to quickly set certain styles.
window.__themeColors = { // To allow CSP to work correctly, we also calculate a sha256 hash during
'default': "royalblue", // the build process and write it to inline-script-checksum.json.
scarlet: "#e04e41", window.__themeColors = {
seafoam: "#177380", 'default': 'royalblue',
hotpants: "hotpink", scarlet: '#e04e41',
oaken: "saddlebrown", seafoam: '#177380',
majesty: "blueviolet", hotpants: 'hotpink',
gecko: "#4ab92f", oaken: 'saddlebrown',
ozark: "#5263af", majesty: 'blueviolet',
cobalt: "#08439b", gecko: '#4ab92f',
sorcery: "#ae91e8", ozark: '#5263af',
offline: "#999999" 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 && localStorage.store_instanceThemes) { }
let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str) }
let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)] if (!localStorage.store_currentInstance) {
if (theme !== 'default') { // if not logged in, show all these 'hidden-from-ssr' elements
document.body.classList.add(`theme-${theme}`) let style = document.createElement('style')
let link = document.createElement('link') style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
link.rel = 'stylesheet' document.head.appendChild(style)
link.href = `/theme-${theme}.css` }
document.head.appendChild(link) })()</script><!-- end insert inline script here -->
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)
}
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;"> <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
<!-- auto-generated w/ build-svg.js --> <!-- auto-generated w/ build-svg.js -->
<!-- insert svg here --><svg xmlns="http://www.w3.org/2000/svg" style="display:none;"> <!-- insert svg here --><svg xmlns="http://www.w3.org/2000/svg" style="display:none;">