aboutsummaryrefslogtreecommitdiff
path: root/deploy
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-16 06:40:07 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-16 06:40:07 +0200
commit2d33bf7cdf9160a2ba2d44194a264c3660831978 (patch)
tree2ad93e3c06f1528e25a3b69bd8a9805589e2d512 /deploy
parent74a84c56047f3463f9086fcffad5c62f402e047e (diff)
Die Web-Version wird öffentlich betreibbar
Die Browserfassung war für den öffentlichen Betrieb gesperrt, und zwar ausdrücklich: Impressum und Datenschutzerklärung trugen TODO-Blöcke statt Angaben, der Quelltextverweis im Fuß zeigte auf https://TODO-QUELLCODE-URL-EINTRAGEN, und der README führte beides als „vor dem produktiven Betrieb zwingend zu erledigen“. Beide Sperren fallen; die Auslieferung wird zugleich auf das tatsächliche Ziel eingerichtet — einen Unterpfad einer bestehenden Domain (/aendggner/), nicht einen eigenen Server. Impressum und Datenschutz führen nun die wirklichen Angaben: § 5 DDG (nicht mehr § 5 TMG) samt § 18 Abs. 2 MStV, Verantwortlicher nach Art. 4 Nr. 7 DSGVO, Rechtsgrundlage Art. 6 Abs. 1 lit. f für die Zugriffsprotokolle, deren Frist mit vierzehn Tagen benannt ist, sowie die Betroffenenrechte samt Aufsichtsbehörde. Die Aussage über die Dateien der Nutzer:innen bleibt, wie sie war; sie trifft zu, weil die Verarbeitung den Rechner nicht verlässt. Der Quelltext wird beigelegt statt verlinkt. deploy/webpaket.sh tritt in der Phase „package“ hinter den Übersetzer und macht aus dessen Ergebnis ein auslieferbares Verzeichnis: Es wirft den .wat-Zwischenschritt fort (368 MB, der bislang von Hand zu übergehen war), legt den Quelltext der gebauten Fassung als aendggner-quelltext.tar.gz nebst Fassungskennung daneben und komprimiert nach .gz und .br vor. Ein veränderter Arbeitsbaum bricht den Bau ab: Der beigelegte Quelltext wäre alsdann nicht der ausgelieferte, und die Auflage des § 13 AGPLv3 wäre gerade verfehlt. Der Fuß verweist primär auf den Tarball, kompakt daneben auf Gerrit als fortlaufende Quelle. Die nginx-Vorlage wird vom server-Block zum location-Schnipsel. Vier Punkte sind dabei mehr als Umschrift: * Die Dateien werden über root statt über alias zugeordnet. In einer Präfix-Location löst try_files gegen den root auf, nicht gegen den alias, und lieferte sonst 404 für alles. * Die Haltefrist von sieben Tagen entfällt zugunsten der Revalidierung. Die Dateinamen tragen keine Fassungskennung; ein Wiederkehrer mit altem app.js und neuem .wasm bekäme sonst eine Mischfassung, die es nie gegeben hat. Ein 304 für das unveränderte Modul kostet nichts. * Vorkomprimiertes wird ausgeliefert (gzip_static/brotli_static), statt 24 MB je Abruf neu zu packen — brotli drückt sie auf 7,1 MB. * Hinzu treten die Sicherheitskopfzeilen samt Inhaltsrichtlinie. Zwei ihrer Freigaben sind unvermeidlich, beide in der Vorlage begründet: 'wasm-unsafe-eval' für die Instanziierung des Moduls (echtes eval kommt in der erzeugten aendggner.js nicht vor), und 'unsafe-inline' für Stile, weil die Synopse als blob:-Dokument die Richtlinie der erzeugenden Seite erbt, ihr Stylesheet aber eingebettet trägt. Die Notwendigkeit ist gemessen, nicht vermutet: ohne die Freigabe meldet das Synopse-Dokument null aktive Stylesheets und Standardschrift, mit ihr eines mit dreiundzwanzig Regeln. Dazu drei Kleinigkeiten, die erst unter einem Unterpfad auffallen: ein Hinweis für abgeschaltetes JavaScript (bislang blieb das Formular wortlos wirkungslos), ein relativ verwiesenes favicon.svg (der Abruf der Domainwurzel ginge ins Leere) sowie Kurzbeschreibung und kanonische Adresse. Eine robots.txt unterbleibt bewusst: Unter einem Unterpfad ist sie wirkungslos, maßgeblich ist allein die der Domainwurzel; die Vorlage vermerkt, was dort einzutragen wäre. Geprüft ist beides, Bau und Lauf: 304 Prüfungen laufen durch, -Pwasm package hinterlässt ein Verzeichnis ohne .wat und mit vollständigem Tarball, und der IfSG-Fall ergibt im Browser unter der Inhaltsrichtlinie 48 angewandte Befehle, 27 zur manuellen Prüfung und 21 geänderte Normen — dasselbe wie die Befehlszeile, ohne eine einzige Beanstandung in der Konsole. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I004490ddaa7c424a60fb0ebac63fc9fc098736e3
Diffstat (limited to 'deploy')
-rw-r--r--deploy/nginx-aendggner.conf101
-rwxr-xr-xdeploy/webpaket.sh73
2 files changed, 140 insertions, 34 deletions
diff --git a/deploy/nginx-aendggner.conf b/deploy/nginx-aendggner.conf
index 46eacb5..78677b5 100644
--- a/deploy/nginx-aendggner.conf
+++ b/deploy/nginx-aendggner.conf
@@ -1,47 +1,80 @@
-# Vorlage für die Auslieferung der ÄndGgner-Browserfassung. Es gibt keinen
-# Anwendungsprozess mehr: Der Server liefert nur statische Dateien aus
-# (Inhalt von target/web/), gerechnet wird im Browser.
+# Vorlage für die Auslieferung der ÄndGgner-Browserfassung unter
+# https://matthias.benkard.de/aendggner/.
#
-# Vor dem Einsatz anpassen: server_name, ssl_certificate(_key) (z. B. via certbot), root.
+# Es gibt keinen Anwendungsprozess: Der Server liefert nur statische Dateien aus
+# (Inhalt von target/web/), gerechnet wird im Browser. Weil der Dienst unter einem
+# Unterpfad einer bestehenden Domain läuft, ist dies kein eigener server-Block,
+# sondern ein Schnipsel zum Einfügen in den vorhandenen — Zertifikat, HTTP→HTTPS
+# und Domainname bleiben, wie sie sind:
+#
+# server { ... include /etc/nginx/snippets/aendggner.conf; ... }
+#
+# Ausgerollt wird nach /var/www/aendggner (siehe README.adoc, Abschnitt „Ausrollen“).
-server {
- listen 80;
- listen [::]:80;
- server_name aendggner.example.org;
- return 301 https://$host$request_uri;
+# Ohne diesen Schritt liefe /aendggner ins Leere, und alle relativen Verweise der
+# Seite gingen von / statt von /aendggner/ aus.
+location = /aendggner {
+ return 301 /aendggner/;
}
-server {
- listen 443 ssl http2;
- listen [::]:443 ssl http2;
- server_name aendggner.example.org;
-
- ssl_certificate /etc/letsencrypt/live/aendggner.example.org/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/aendggner.example.org/privkey.pem;
-
- # Inhalt von target/web/ — ohne aendggner.js.wat, das nur ein Zwischenschritt
- # des Übersetzers ist.
- root /var/www/aendggner;
+location /aendggner/ {
+ # Bewusst „root /var/www“ (mit dem Verzeichnis /var/www/aendggner) statt
+ # „alias /var/www/aendggner/“: In einer Präfix-Location löst try_files $uri
+ # gegen den root auf, nicht gegen den alias, und liefert dann 404 für alles.
+ root /var/www;
index index.html;
- # Ohne diesen Typ lehnt der Browser die Streaming-Instanziierung des Moduls ab.
+ try_files $uri $uri/ =404;
+
+ # Ohne den ersten Typ lehnt der Browser die Streaming-Instanziierung des Moduls
+ # ab. Ein types-Block in einer location ersetzt die Zuordnung vollständig, statt
+ # sie zu ergänzen — deshalb stehen hier alle vorkommenden Endungen.
types {
application/wasm wasm;
+ text/html html;
+ text/css css;
+ text/javascript js;
+ image/svg+xml svg;
+ text/plain txt;
+ application/gzip gz;
}
+ default_type application/octet-stream;
- gzip on;
- gzip_types application/wasm application/javascript text/css text/html;
- gzip_min_length 1024;
+ # Das Wasm-Modul ist rund 24 MB groß. deploy/webpaket.sh legt beim Bauen .gz
+ # und .br daneben; ausgeliefert wird die fertige Datei, statt sie je Abruf neu
+ # zu packen. gzip_static bringen die üblichen Distributionspakete mit;
+ # brotli_static setzt das Modul ngx_brotli voraus — fehlt es, die Zeile
+ # streichen, gzip_static genügt.
+ gzip_static on;
+ brotli_static on;
- # Das Wasm-Modul ist einige Megabyte groß und ändert sich nur mit einer neuen
- # Fassung; vorkomprimierte .gz/.br daneben zu legen lohnt sich (ngx_brotli bzw.
- # gzip_static).
- location ~ \.(wasm|js|css)$ {
- expires 7d;
- add_header Cache-Control "public";
- }
+ # Kein „expires 7d“: Die Dateinamen tragen keine Fassungskennung. Ein Browser
+ # mit altem app.js und neuem .wasm (oder umgekehrt) bekäme eine Mischfassung,
+ # die es nie gegeben hat. Stattdessen Revalidierung bei jedem Abruf; für das
+ # unveränderte Modul kostet das ein 304 ohne Rumpf.
+ add_header Cache-Control "no-cache" always;
- location / {
- try_files $uri $uri/ =404;
- }
+ # add_header in einer location verdrängt sämtliche vom server-Block geerbten
+ # Header. Alles, was hier gelten soll, muss deshalb hier stehen — auch ein
+ # dort gesetztes HSTS.
+ # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
+
+ # 'wasm-unsafe-eval' ist für die Instanziierung des Moduls nötig; echtes eval
+ # oder new Function kommt in der erzeugten aendggner.js nicht vor.
+ #
+ # style-src braucht 'unsafe-inline': Die fertige Synopse wird als blob:-Dokument
+ # geöffnet, und ein blob:-Dokument erbt die CSP des Dokuments, das es erzeugt
+ # hat. Der HtmlRenderer bettet sein Stylesheet als <style> ein — ohne die
+ # Freigabe erschiene die Synopse unformatiert. Skripte bleiben streng.
+ add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer" always;
+ add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), usb=()" always;
}
+
+# Eine robots.txt unter /aendggner/ wäre wirkungslos: Maßgeblich ist allein die im
+# Wurzelverzeichnis der Domain. Wer die Seite von der Indizierung ausnehmen will,
+# trage dort ein:
+#
+# User-agent: *
+# Disallow: /aendggner/
diff --git a/deploy/webpaket.sh b/deploy/webpaket.sh
new file mode 100755
index 0000000..2003134
--- /dev/null
+++ b/deploy/webpaket.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+# Macht aus dem Übersetzerergebnis ein auslieferbares Verzeichnis. Läuft in der Phase
+# „package“ des Profils -Pwasm, unmittelbar nach native-image.
+#
+# 1. Der Textzwischenschritt aendggner.js.wat (mehrere hundert Megabyte) wird entfernt.
+# 2. Der Quelltext der gebauten Fassung wird als Tarball beigelegt — AGPLv3 §13 verlangt
+# beim Netzwerkbetrieb einen Quellcode-Zugang für die Nutzer:innen, und der Footer der
+# Startseite verweist darauf.
+# 3. Die großen Dateien werden vorkomprimiert, damit nginx sie mit gzip_static bzw.
+# brotli_static ausliefern kann, statt 24 MB je Abruf neu zu packen.
+#
+# Aufruf: deploy/webpaket.sh <ausgabeverzeichnis> (Standard: target/web)
+
+set -eu
+
+ziel="${1:-target/web}"
+quelle="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
+
+if [ ! -d "$ziel" ]; then
+ echo "webpaket: $ziel gibt es nicht — erst -Pwasm package laufen lassen." >&2
+ exit 1
+fi
+
+# 1. Zwischenschritt fort.
+rm -f "$ziel/aendggner.js.wat"
+
+# 2. Quelltext der gebauten Fassung.
+#
+# Ein veränderter Arbeitsbaum bricht den Bau ab: Der angebotene Quelltext muss der
+# ausgelieferten Fassung entsprechen, sonst ist die Auflage aus §13 verfehlt. Wer nur
+# ausprobieren will, setzt QUELLTEXT_UNGEPRUEFT=1.
+if ! git -C "$quelle" rev-parse --git-dir >/dev/null 2>&1; then
+ echo "webpaket: $quelle ist kein Git-Arbeitsbaum; ohne ihn lässt sich der Quelltext" >&2
+ echo " der gebauten Fassung nicht beilegen (AGPLv3 §13)." >&2
+ exit 1
+fi
+
+if [ -n "$(git -C "$quelle" status --porcelain)" ] && [ "${QUELLTEXT_UNGEPRUEFT:-}" != 1 ]; then
+ echo "webpaket: Der Arbeitsbaum trägt uneingecheckte Änderungen. Der beigelegte Quelltext" >&2
+ echo " wäre dann nicht der gebaute. Erst einchecken — oder für einen Probelauf" >&2
+ echo " QUELLTEXT_UNGEPRUEFT=1 setzen (dann nicht ausliefern)." >&2
+ exit 1
+fi
+
+fassung="$(git -C "$quelle" rev-parse HEAD)"
+git -C "$quelle" archive --format=tar.gz --prefix=aendggner/ HEAD \
+ -o "$(CDPATH='' cd -- "$ziel" && pwd)/aendggner-quelltext.tar.gz"
+
+{
+ echo "ÄndGgner — Quelltext der ausgelieferten Fassung"
+ echo
+ echo "Commit: $fassung"
+ echo "Gebaut: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+ echo "Vollständig in aendggner-quelltext.tar.gz; Bauanleitung darin in README.adoc."
+ echo "Fortlaufend: https://gerrit.benkard.de/plugins/gitiles/aendggner"
+} > "$ziel/quelltext-fassung.txt"
+
+# 3. Vorkompression. Der Tarball ist bereits gepackt und bleibt außen vor.
+for datei in "$ziel"/*.wasm "$ziel"/*.js "$ziel"/*.css "$ziel"/*.html "$ziel"/*.svg \
+ "$ziel"/*.txt; do
+ [ -f "$datei" ] || continue
+ gzip -9 -f -k -- "$datei"
+ if command -v brotli >/dev/null 2>&1; then
+ brotli -f -- "$datei"
+ fi
+done
+
+if ! command -v brotli >/dev/null 2>&1; then
+ echo "webpaket: brotli nicht gefunden — nur .gz angelegt." >&2
+fi
+
+echo "webpaket: $ziel ist auslieferbar (Fassung $(git -C "$quelle" rev-parse --short HEAD))."