forked from MapComplete/MapComplete
Security: add inline script with automatic hash
This commit is contained in:
parent
4852888b41
commit
5a6f5f064b
8 changed files with 89 additions and 49 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.33.1",
|
"version": "0.33.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.33.1",
|
"version": "0.33.5",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^3.8.0",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
"country-to-currency": "^1.0.10",
|
"country-to-currency": "^1.0.10",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"csv-parse": "^5.1.0",
|
"csv-parse": "^5.1.0",
|
||||||
"doctest-ts-improved": "^0.8.8",
|
"doctest-ts-improved": "^0.8.8",
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.0.5",
|
||||||
|
@ -5392,6 +5393,12 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||||
|
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
|
||||||
|
},
|
||||||
"node_modules/css-line-break": {
|
"node_modules/css-line-break": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
@ -17341,6 +17348,11 @@
|
||||||
"which": "^2.0.1"
|
"which": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
|
||||||
|
},
|
||||||
"css-line-break": {
|
"css-line-break": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
|
10
package.json
10
package.json
|
@ -8,7 +8,6 @@
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"config": {
|
"config": {
|
||||||
"#": "Various endpoints that are instance-specific. This is the default configuration, which is re-exported in 'Constants.ts'.",
|
|
||||||
"#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`",
|
"#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`",
|
||||||
"#oauth_credentials:comment": [
|
"#oauth_credentials:comment": [
|
||||||
"`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.",
|
"`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.",
|
||||||
|
@ -18,10 +17,10 @@
|
||||||
"Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`"
|
"Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`"
|
||||||
],
|
],
|
||||||
"oauth_credentials": {
|
"oauth_credentials": {
|
||||||
"#": "This client-id is registered by 'MapComplete' on osm.org",
|
"#": "This client-id is registered by 'MapComplete' on osm.org",
|
||||||
"oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk",
|
"oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk",
|
||||||
"oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg",
|
"oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg",
|
||||||
"url": "https://www.openstreetmap.org"
|
"url": "https://www.openstreetmap.org"
|
||||||
},
|
},
|
||||||
"api_keys": {
|
"api_keys": {
|
||||||
"#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version",
|
"#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version",
|
||||||
|
@ -108,6 +107,7 @@
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^3.8.0",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
"country-to-currency": "^1.0.10",
|
"country-to-currency": "^1.0.10",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"csv-parse": "^5.1.0",
|
"csv-parse": "^5.1.0",
|
||||||
"doctest-ts-improved": "^0.8.8",
|
"doctest-ts-improved": "^0.8.8",
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.0.5",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import SpecialVisualizations from "../src/UI/SpecialVisualizations"
|
||||||
import Constants from "../src/Models/Constants"
|
import Constants from "../src/Models/Constants"
|
||||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers"
|
import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers"
|
||||||
import { ImmutableStore } from "../src/Logic/UIEventSource"
|
import { ImmutableStore } from "../src/Logic/UIEventSource"
|
||||||
|
import * as crypto from "crypto"
|
||||||
|
|
||||||
const sharp = require("sharp")
|
const sharp = require("sharp")
|
||||||
const template = readFileSync("theme.html", "utf8")
|
const template = readFileSync("theme.html", "utf8")
|
||||||
|
@ -205,9 +206,14 @@ function asLangSpan(t: Translation, tag = "span"): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
let previousSrc: Set<string> = new Set<string>()
|
let previousSrc: Set<string> = new Set<string>()
|
||||||
function generateCsp(layout: LayoutConfig): string {
|
function generateCsp(
|
||||||
|
layout: LayoutConfig,
|
||||||
|
options: {
|
||||||
|
scriptSrcs: string[]
|
||||||
|
}
|
||||||
|
): string {
|
||||||
const apiUrls: string[] = [
|
const apiUrls: string[] = [
|
||||||
"self",
|
"'self'",
|
||||||
...Constants.defaultOverpassUrls,
|
...Constants.defaultOverpassUrls,
|
||||||
Constants.countryCoderEndpoint,
|
Constants.countryCoderEndpoint,
|
||||||
"https://api.openstreetmap.org",
|
"https://api.openstreetmap.org",
|
||||||
|
@ -248,9 +254,11 @@ function generateCsp(layout: LayoutConfig): string {
|
||||||
)
|
)
|
||||||
previousSrc = hosts
|
previousSrc = hosts
|
||||||
|
|
||||||
const csp = {
|
const csp: Record<string, string> = {
|
||||||
"default-src": "'self'",
|
"default-src": "'self'",
|
||||||
"script-src": "'self' https://gc.zgo.at/count.js",
|
"script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join(
|
||||||
|
" "
|
||||||
|
),
|
||||||
"img-src": "* data:", // maplibre depends on 'data:' to load
|
"img-src": "* data:", // maplibre depends on 'data:' to load
|
||||||
"connect-src": connectSrc.join(" "),
|
"connect-src": connectSrc.join(" "),
|
||||||
"report-to": "https://report.mapcomplete.org/csp",
|
"report-to": "https://report.mapcomplete.org/csp",
|
||||||
|
@ -267,6 +275,14 @@ function generateCsp(layout: LayoutConfig): string {
|
||||||
].join("\n")
|
].join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeOtherLanguages = readFileSync("./src/UI/RemoveOtherLanguages.js", "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.join("\n")
|
||||||
|
const removeOtherLanguagesHash = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(removeOtherLanguages)
|
||||||
|
.digest("base64")
|
||||||
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
|
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
|
||||||
Locale.language.setData(layout.language[0])
|
Locale.language.setData(layout.language[0])
|
||||||
const targetLanguage = layout.language[0]
|
const targetLanguage = layout.language[0]
|
||||||
|
@ -338,7 +354,10 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title })
|
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title })
|
||||||
|
const templateLines = template.split("\n")
|
||||||
|
const removeOtherLanguagesReference = templateLines.find(
|
||||||
|
(line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0
|
||||||
|
)
|
||||||
let output = template
|
let output = template
|
||||||
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
|
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
|
||||||
.replace(
|
.replace(
|
||||||
|
@ -346,7 +365,13 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
|
||||||
Translations.t.general.poweredByOsm.textFor(targetLanguage)
|
Translations.t.general.poweredByOsm.textFor(targetLanguage)
|
||||||
)
|
)
|
||||||
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
|
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
|
||||||
.replace(/<!-- CSP -->/, generateCsp(layout))
|
.replace(
|
||||||
|
/<!-- CSP -->/,
|
||||||
|
generateCsp(layout, {
|
||||||
|
scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.replace(removeOtherLanguagesReference, "<script>" + removeOtherLanguages + "</script>")
|
||||||
.replace(
|
.replace(
|
||||||
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
|
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
|
||||||
asLangSpan(layout.shortDescription)
|
asLangSpan(layout.shortDescription)
|
||||||
|
@ -357,7 +382,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
|
||||||
)
|
)
|
||||||
|
|
||||||
.replace(
|
.replace(
|
||||||
'<script src="./src/index.ts" type="module"></script>',
|
/.*\/src\/index\.ts.*/,
|
||||||
`<script type="module" src="./index_${layout.id}.ts"></script>`
|
`<script type="module" src="./index_${layout.id}.ts"></script>`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ hosted.mapcomplete.org {
|
||||||
countrycoder.mapcomplete.org {
|
countrycoder.mapcomplete.org {
|
||||||
root * tiles/
|
root * tiles/
|
||||||
file_server
|
file_server
|
||||||
|
header {
|
||||||
|
+Permissions-Policy "interest-cohort=()"
|
||||||
|
+Access-Control-Allow-Origin https://hosted.mapcomplete.org https://dev.mapcomplete.org https://mapcomplete.org
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ npm run test
|
||||||
npm run prepare-deploy &&
|
npm run prepare-deploy &&
|
||||||
mv config.json.bu config.json &&
|
mv config.json.bu config.json &&
|
||||||
zip dist.zip -r dist/* &&
|
zip dist.zip -r dist/* &&
|
||||||
scp -r dist.zip hetzner:/root/ &&
|
scp ./scripts/hetzner/config/* hetzner:/root/ &&
|
||||||
echo "Upload completed, deploying config and booting" &&
|
|
||||||
rsync -rzh --progress dist.zip hetzner:/root/ &&
|
rsync -rzh --progress dist.zip hetzner:/root/ &&
|
||||||
|
echo "Upload completed, deploying config and booting" &&
|
||||||
ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" &&
|
ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" &&
|
||||||
rm dist.zip
|
rm dist.zip
|
||||||
|
|
31
src/UI/RemoveOtherLanguages.js
Normal file
31
src/UI/RemoveOtherLanguages.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
let lang = (
|
||||||
|
(navigator.languages && navigator.languages[0]) ||
|
||||||
|
navigator.language ||
|
||||||
|
navigator["userLanguage"] ||
|
||||||
|
"en"
|
||||||
|
).substr(0, 2)
|
||||||
|
|
||||||
|
function filterLangs(maindiv) {
|
||||||
|
let foundLangs = 0
|
||||||
|
for (const child of Array.from(maindiv.children)) {
|
||||||
|
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
||||||
|
foundLangs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundLangs === 0) {
|
||||||
|
lang = "en"
|
||||||
|
}
|
||||||
|
for (const child of Array.from(maindiv.children)) {
|
||||||
|
const childLang = child.attributes.getNamedItem("lang")
|
||||||
|
if (childLang === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (childLang.value === lang) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
child.parentElement.removeChild(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterLangs(document.getElementById("descriptions-while-loading"))
|
||||||
|
filterLangs(document.getElementById("default-title"))
|
|
@ -1,32 +0,0 @@
|
||||||
export {}
|
|
||||||
let lang = (
|
|
||||||
(navigator.languages && navigator.languages[0]) ||
|
|
||||||
navigator.language ||
|
|
||||||
navigator["userLanguage"] ||
|
|
||||||
"en"
|
|
||||||
).substr(0, 2)
|
|
||||||
|
|
||||||
function filterLangs(maindiv: HTMLElement) {
|
|
||||||
let foundLangs = 0
|
|
||||||
for (const child of Array.from(maindiv.children)) {
|
|
||||||
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
|
||||||
foundLangs++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foundLangs === 0) {
|
|
||||||
lang = "en"
|
|
||||||
}
|
|
||||||
for (const child of Array.from(maindiv.children)) {
|
|
||||||
const childLang = child.attributes.getNamedItem("lang")
|
|
||||||
if (childLang === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (childLang.value === lang) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
child.parentElement.removeChild(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterLangs(document.getElementById("descriptions-while-loading"))
|
|
||||||
filterLangs(document.getElementById("default-title"))
|
|
|
@ -65,7 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
|
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
|
||||||
<script async src="./src/UI/RemoveOtherLanguages.ts" type="module"></script>
|
<script src="./src/UI/RemoveOtherLanguages.js"></script>
|
||||||
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
|
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
|
||||||
<script defer src="./src/index.ts" type="module"></script>
|
<script defer src="./src/index.ts" type="module"></script>
|
||||||
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
|
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
|
||||||
|
|
Loading…
Reference in a new issue