Stranger Than Usual

Linguistics is a portmanteau of Linguini and Breadsticks.

Quelle

Support für Drogenhändlerringe

Vorhin hat einer meiner Kollegen einen schönen Fall einer "Blind Idiot" Translation entdeckt:

Screenshot einer Website. Die Überschrift lautet: „Support für Drogenhändlerringe“. Darüber steht in einer grauen Box: „Diese Seite wurde von der Cloud Translation API übersetzt.“

Vor ein paar Jahren hat mit Google „pet peeve“ noch mit „Haustier ärgern“ übersetzt. Aber ich dachte, die wären mittlerweile weiter. Interessanterweise ist die Version bei archive.org aus dem Januar 2023 noch mit „DTO-Unterstützung“ korrekt übersetzt. Die aktuelle Version führt zum dem Screenshot oben. Ich habe die Seite auch mal archiviert, wenn das gefixt wird.

Tipp: nehmt immer die Doku in der Originalsprache, wenn möglich. Auch wenn die nicht maschinell übersetzt wurde. Es gibt im Zweifelsfall einfach mehr Literatur zu dem Thema in der Originalsprache.

Content-Security-Policy

Den Content-Security-Policy-HTTP-Header gibt es jetzt schon seit über einer Dekade. Was macht dieser Header? Er bietet eine Möglichkeit, für Betreiber von Webservern bestimmte Sicherheitsregeln einzuführen, die dann vom Browser (aka User-Agent) der Benutzer umgesetzt werden. Das ist eigentlich ganz schön, denn man muss sich als Webseitenbetreiber sowieso darauf verlassen, dass der Browser im Interesse des Benutzers arbeitet.

Die Kernfunktion der Content-Secuity-Policy (kurz „CSP“) ist hierbei zu kontrollieren, von welchen Quellen die Webseit Resourcen welcher Art beziehen darf. So kann man zum Beispiel angeben, dass CSS-Dateien nur von der Domain der Website selbst geladen werden dürfen, dass Bilder von der Website selbst oder von einem bestimmten CDN-Server kommen können und so weiter. Es gibt noch ein paar andere wichtige Funktionen, aber fokussieren wir uns erst einmal auf die Resourcen.

Exkurs: XSS-Attacken

Die gefährlichste Resourcen sind Scripte. Javascript ist eine Turing-Vollständige Progammiersprache und wenn eine Angreiferin es schafft, ein eigenes Script auf einer fremden Seite auszuführen, kann sie ziemlich viel anrichten, zum Beispiel Aktionen im Namen des Benutzers ausführen. Auch hier gibt es Sicherheitsmaßnahmen in Browsern, um den Schaden zu begrenzen, gefährlich ist es aber trotzdem. Solche Attacken nennt man Cross-Site-Scripting, oder XSS (fragt nicht, wo das X herkommt, ich habe keine Ahnung. Vermutlich ist es da, um Verwechslungen mit CSS zu verhindern).

XSS-Lücken passieren, wenn es Benutzern ermöglicht wird, eigenen Inhalt ungefiltert oder schlecht gefiltert auf einer fremden Website unterzubringen. Und dass Benutzer eigenen Inhalt auf fremden Websites unterbringen ist seit Jahrzehnten im Internet gang und gäbe. Nehmen wir als Beispiel mal einen Kommentar, den ich in einem Blog über eine Kommentarfunktion hinterlasse: Wenn die Seite meinen Kommentartext 1:1 übernimmt, und in meinem Kommentartext ist HTML-Code, dann wird dieser HTML-Code für alle gerendert, die sich diesen Kommentar durchlesen (oder auch nur die Seite laden, auf der er lesbar ist).

Wenn der HTML-Code jetzt ein Script enthält, wird auch dieser Code ausgeführt. Schon das Anzeigen beliebigen HTML-COdes in Kommentarn will man verhindern, aber ein Script ist, wie oben erklärt, eine Katastrophe. Deswegen ist es Standard, und wird auch von allen gängigen Frameworks und HTML-Templates unterstützt, die Benutzereingaben eben nicht 1:1 in die Seite zu schreiben, sondern spezielle HTML-Zeichen vorher zu escapen, also sie durch Platzhalter zu ersetzen, die dann nur für die Anzeige als die Zeichen dargestellt werden, die der Benutzer eigentlich eingegeben hat.

Wie man mit einer CSP XSS-Attacken verhindert

Escapen mag Standard sein, aber manchmal gibt es Bugs oder man stellt sich blöd an und man hat doch eine XSS-Lücke. Dafür (unter anderem) wurde der CSP-Header erfunden. Dort kann man wie gesagt angeben, woher Scripte stammen dürfen. Es gibt viele Optionen, das zu konfigurieren, doch kurz gesagt: Man gibt eine Liste von Domains an. Wenn die Scripte nicht auf dieser Liste stehen, werden sie nicht ausgeführt. Darunter fällt auch, dass sie nicht im HTML-Code direkt stehen dürfen. Will man das erlauben, muss man eine spezielle Anweisung 'unsafe-inline' hinzufügen. Will man auch erlauben, dass Scripte nach eigenem Gutdünken Text als Script ausführen sollen, muss man die Anweisung 'unsafe-eval' hinzufügen.

Die Namen dieser Spezialanweisung sollten aufhorchen lassen: dort steht ganz klar drin: Das ist nicht sicher. Wenn man diese Anweisungen in die CSP macht, hebelt man damit ihre Schutzwirkung fast komplett auf.

Meine Erfahrung mit CSPs

Ich habe vor etwa sieben Jahren in meiner damaligen Firma einen kleinen Vortrag über den CSP-Header gehalten und auch einen kleinen Webserver zum Demonstrieren geschrieben. Ich habe das damals gemacht, weil ich Aufmerksamkeit auf diesen damals noch nicht ganz alten Header lenken wollte, der eine zusätzliche Absicherung gegen verschiedene Attacken bringen kann.

Ich habe auch versucht, diesen Header in die Projekte einzubringen, in denen ich tätig war. Das war schwierig. Denn inline-Javascript wird oft verwendet. Es gibt zwar Wege, auch spezifisches Inline-Javascript zuzulassen, das ist aber bei bestehendem Code fast so aufwändig, wie das Javascript einfach in separat geladene Dateien auszulagern. Immerhin habe ich es geschafft, in einem Projekt einen CSP-Header in den Checkout einzubauen, also dort, wo ein Kauf abgeschlossen und bezahlt wird. Das ist ein sehr sicherheitskritischer Teil.

So zumindest die Theorie. In der Praxis haben wir den Header dann nie wirklich scharf schalten können, weil wir unbedingt einen Haufen Scripte von Drittparteien einbinden mussten, von irgendwelchen Sicherheitsbadges bis hin zum Google Tag Manager, in dem selber noch weitere Scripte geladen wurden.

Das wäre eigentlich auch kein Problem gewesen. Denn das sind Scripte von Drittparteien, die werden von Servern geladen die wir einfach in die Liste erlaubter Quellen angeben können. Richtig? Falsch. Denn diese Scripte wiederum fügen inline-Javascript in die Seite ein und erwarten dann, dass der ausgeführt wird. Oder wollen gleich direkt ein eval machen. Oder jemand aus der Marketingabteilung beschwert sich, dass das neue Script, das per Google-Tag-Manager eingebunden wurde, nicht läuft. Und schwupps – war die CSP wieder weg.

So ähnlich ist mir das auch in den beiden Projekten danach passiert. Zumindest das letzte davon hatte keine Entschuldigung, weil wir quasi auf der grünen Wiese angefangen haben. Wir hätten das von vornherein richtig machen können. Meine Rufe verhallten aber, weil es immer gerade etwas Wichtigeres zu tun gab.

Meine Hypothese war damals schon: IT-Sicherheit ist uns, als Gesellschaft, nicht wichtig genug.

Rein subjektiv: Wie sieht die Lage heute aus?

Dieses Blog hat eine strenge CSP. Ist aber auch unwichtig, weil das Blog hier statisch gerendert wird, ich die einzige Person bin, die Inhalt einfügt. Es gibt in der Website keine Kommentarfunktion, keine Kommentare von Lesern, die automatisch dargestellt werden. Es gibt auch keine sensiblen Informationen, die man mit boshaften Scripten heraustragen könnte oder Aktionen, die man durchführen könnte. Die CSP hier ist streng, aber überflüssig.

Meine Nextcloud-Instanz hat eine komplizierte CSP. Es werden zwar inline-Scripte erlaubt, aber es wird mithilfe von nonce-Anweisungen kontrolliert, dass nur gewollte Scripte ausgeführt werden.

Aber schauen wir uns doch mal wichtigere Seiten an. Wikipedia zum Beispiel hat keine CSP. Hmm. Aber gut, da kann sowieso jeder drin herumschreiben, was brauchen die schon an Sicherheit? Außerdem ist die Software alt. Aber sicher sind Bankenwebsites da besser aufgestellt, oder? Wie sieht es denn zum Beispiel mit der Sparkasse Essen aus?

script-src 'self' blob: https://morris-server.de:8801 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' data: blob: https://api.sparkassen-mediacenter.de https://sparkassen-mediacenter.de https://cdn.sparkassen-mediacenter.de

Oh-oh. Was sehen meine müden Augen da? 'unsafe-inline' und 'unsafe-eval' unter script-src? Kein nonce, kein hash, fast kein Schutz mehr. Aber das ist ja alles Subjektiv, oder? Was sagen denn ordentlich angelegte Studien dazu?

Studien zur CSP-Nutzung

Da gibt es eine Studie aus 2020: Complex Security Policy? A Longitudinal Analysis of Deployed Content Security Policies. Da haben sie die Nutzung des CSP-Headers seit 2012 (also in etwa seit den Anfängen dieses Headers) auf 10000 populären Seiten verfolgt. Ergebnis: Die meisten Websites, wenn sie überhaupt eine CSP haben, setzen auf 'unsafe-inline' und 'unsafe-eval'. Ein paar andere Funktionen der CSP, die ich hier nicht erwähnt habe (hauptsächlich sicherzustellen, dass alle Resourcen per TLS geladen werden), werden immerhin mehr genutzt.

Die Autoren schreiben:

The insights gathered from our survey indicate that CSP has earned a bad reputation due to its complexity in content restriction, resulting in developers shying away from any part of CSP.

Ich muss hier sagen: eine gute CSP muss nicht komplex sein. Es sind die fucking-Überkomplexen Webseiten, die so viel Bullshit an Javascript laden, dass es unmöglich wird, eine effektive CSP einzusetzen.

In der Studie sind auch noch ein paar andere Studien referenziert, die mit anderen Schwerpunkten zu einem ähnlichen Ergebnis gekommen sind (mindestens eine davon hat sich über eine Milliarde Webseiten angeschaut).

Also was tun?

Glücklicherweise ist eine CSP nicht notwendig, um eine sichere Website zu haben. Es ist nicht so schwierig, keine XSS-Lücken einzubauen. Die Realität zeigt aber, dass es immer wieder passiert, und da wäre eine CSP wirklich hilfreich, um dir den Arsch zu retten.

Wo kann man Anfangen? Wie auch in der Studie erwähnt: In neuen Projekten ist das ganz einfach: Als erstes die CSP festlegen. Dann die Website entwickeln. So merkt man sofort, wenn man mit dem was man tut in Konflikt mit der CSP gerät. Meist kann man die Probleme ausräumen, ohne die CSP zu ändern. Wenn nicht, ändert man die CSP, indem man zum Beispiel eine weitere Quelle hinzufügt, oder, wenn es wirklich schlimm wird, ein nonce oder einen Hashwert (das würde ich aber nicht empfehlen, das macht alles nur komplexer).

In bestehenden Projekten ist das schwieriger. Dort gibt es mitunter Inline-Javascript auf eine riesiege Codebase verteilt. Hier hilft eigentlich nur, bei jeder Änderung, die sowieso gemacht werden muss, ein bisschen weniger inline-Javascript als vorher zurückzulassen. Vielleicht auch die CSP zuerst nur für besonders sicherheitsrelevante Bereiche setzen und nach und nach ausweiten. Nonces verwenden, um schnell größere Codeteile CSP-kompatibel zu machen. Nextcloud zum Beispiel hat das geschafft (oder ist zumindest auf dem Weg dorthin), wie ich anhand einiger geschlossener Tickets und meiner aktuellen Installation sehen konnte.

In beiden Fällen gilt: man muss denen mit dem Geld klarmachen, dass es wichtig ist, eine effektive CSP auf dem Server zu haben. Am besten mal bei denen vorbeischauen, wenn es mal wieder ein IT-Sicherheitsvorfall Schlagzeilen macht. Nach dem Motto: Das könnten wir sein.

Ebenfalls in beiden Fällen: Überprüft, ob ihr die Javascripte, die von externen Servern geladen werden, wirklich braucht. Das ist nicht nur wichtig für die CSP, es ist auch so ein Sicherheitsproblem für eure Seite. Und wenn ihr sie einbindet: lasst nicht zu, dass sie euch ein 'unsafe-inline' oder 'unsafe-eval' aufzwingen. Denkt darüber nach: selbst wenn ihr denen vertraut: Wenn sie unfähig sind, ihren Code ohne inline-JS oder eval zu schreiben, dann kann es mit ihren Fähigkeiten, sicheren Code zu schreiben auch nicht so weit her sein. Wollt ihr denen dann wirklich erlauben, Code auf eurer Seite auszuführen?

KittenMoji

Letztes Wochenende habe ich auf Mastodon einen Tweet gesehen, wo jemand KittenMoji erwähnt hat. KittenMoji ist eine Byte-zu-Text-Codierung. Ähnlich wie bei der Hexadezimalschreibweise oder Base64 können damit Binärdaten als Text encodiert werden.

KittenMoji verwendet, wie der Name schon nahelegt, Emojis zur Codierung. Alle verwendeten Emojis belegen, als UTF-8 codiert, jeweils vier Bytes, also 32 Bit. KittenMoji ist eine Base256-Codierung, also ein Byte wird auf genau ein Emoji abgebildet. Im Gegensatz also zu Base64, das jeweils 6 Bits mit 8 Bits encodiert oder Hexadezimalschreibweise, die jeweils 8 Bit mit 16 Bits encodiert, werden bei KittenMoji jeweils 8 Bits mit 32 Bits encodiert.

KittenMoji ist also extrem ineffizient und es gibt eigentlich keinen Grund, es zu verwenden. Ich habe ein Rust-Crate zur De-und Encodierung von KittenMoji geschrieben.

Das kittenmoji-crate

Irgendwie funktioniert mein Gehirn am besten wenn das, was ich programmiere, komplett unwichtig ist. Ich habe das crate dann auch auf crates.io, der offiziellen Rust-Paketplattform veröffentlicht. Einfach mal um zu schauen, wie das funktioniert. Wenn ich einen dummen Fehler mache: kein Problem, ist ja kein wichtiges Paket. Falls ich irgendwann einmal ein wichtiges Paket veröffentliche, weiß ich dann schon, wie das läuft.

Das kittenmoji-crate besteht im Prinzip aus sechs Funktionen: drei zum Encodieren, drei zum Decodieren. Jeweils eine Funktion, die einen byte-slice bzw. string-slice en- bzw. decodiert, eine Funktion, die einen byte-Iterator entgegennimmt und einen char-Iterator zurückgibt (fürs Encodieren, entsprechend umgekehrt fürs Decodieren) und jeweils eine Funktion, die einen bytestream liest und ihn entsprechend de- oder encodiert.

Letzteres wird in zwei Beispielprogrammen demonstriert, encode und decode, die jeweils von stdin lesen und nach stdout schreiben und die Daten dazwischen en- oder decodieren. Das funktioniert mit beliebig großen Dateien, der interne Buffer hat eine konstante Größe, der genutzte Arbeitsspeicher hält sich also in Grenzen und ist nicht von der Größe der Eingabedaten abhängig.

Optimierung des Encoders

Wer das Blog hier schon eine Weile verfolgt weiß, dass ich gelegentlich einfach mal ganz nutzlose Programme optimiere. Zum Beispiel das zeilenweise Einlesen einer Datei. Oder einen primitiven DGA-Detektor. Oder einen Brainfuckinterpreter. Mein erstes Veröffentlichtes Rust-Crate? Natürlich!

Ich habe mich hier auf die Stream-Varianten fokussiert, weil die mutmaßlich die größten Datenmengen verarbeiten und dementsprechend am meisten von einer Optimierung profitieren. Zur Messung habe ich wieder einmal Hyperfine verwendet. Encodiert habe ich… ein CD-Image einer Windows 98 SE (hey, ich habe Anfang des Jahrtausends Geld dafür ausgegeben, dann soll sich diese CD auch mal nützlich machen!).

Die Datei ist knapp 600MiB groß (626524160 Byte, um genau zu sein). Der Code sah zu diesem Zeitpunkt so aus:

pub fn encode_stream(mut input: impl Read, mut output: impl Write) -> io::Result<()> {
    let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
    let mut n = input.read(&mut buf)?;
    let mut cbuf: [u8; 4] = [0; 4];
    while n > 0 {
        for c in encode(buf[..n].iter().copied()) {
            let s = c.encode_utf8(&mut cbuf);
            output.write_all(s.as_bytes())?;
        }
        n = input.read(&mut buf)?;
    }
    Ok(())
}

Es wird also in einer Schleife Daten gelesen, in einen Buffer auf dem Stack geschrieben und dann wird der oben erwähnte Iterator aufgerufen, der dann die Bytes mittels einer Lookup-Tabelle in Unicode-chars umwandelt, die dann als UTF-8 codiert und dann geschrieben werden. Wie schnell ist das?

$ hyperfine -w1 "cat ~/Dateien/CD-Images/Win98SE.iso | target/release/examples/encode  > /dev/null"
  Time (mean ± σ):      4.654 s ±  0.245 s    [User: 4.461 s, System: 0.462 s]
  Range (min … max):    4.359 s …  5.172 s    10 runs

Ich habe hier nach /dev/null geschrieben, weil ich die Schreibgeschwindigkeit meiner SSD jetzt nun wirklich nicht in die Messung einbeziehen wollte. Dennoch schwanken die Werte bei verschiedenen Messdurchläufen recht stark, also sind sie mit Vorsicht zu genießen. Ich habe das Experiment mehrere Male laufen lassen, die Werte schwanken immer im selben Bereich.

Schnell ist das jedenfalls nicht. Was könnte man also verbessern? Nun, zunächst ist mir aufgefallen, dass ich eine Menge kleiner Funktionsaufrufe habe. Der Iterator, das UTF-8-Codieren… alles für jedes einzelne Byte im Input. über 600 Millionen Bytes sind das dementsprechend viele Aufrufe. Kann man das vielleicht verkleinern? Aber natürlich. Erst einmal bin ich den Iterator losgeworden, und habe den Krams inline gemacht. Meine Lookup-Tabelle hat aber char-Werte zurückgegeben, den Aufruf von char::encode_utf8() hätte ich also trotzdem noch gebraucht. Also habe ich eine zweite Lookup-Tabelle erstellt, die stattdessen &str enthält, also kleine statische Strings, die praktischerweise schon UTF-8 codiert sind. Ergebnis:

  Time (mean ± σ):      2.519 s ±  0.070 s    [User: 2.317 s, System: 0.465 s]
  Range (min … max):    2.443 s …  2.682 s    10 runs

Diese Verbesserung kann sich sehen lassen. Aber es geht noch besser. Mit den &str-Werten ist es nämlich so: Das Programm muss zunächst auf das Array zugreifen, um sich an der richtigen Stelle eine Referenz auf den String zu holen, dieser Referenz folgen, dann eine Funktion aufrufen, um den String als byte-Slice zu interpretieren (zumindest dieser Funktionsaufruf sollte aber vom Compiler rausoptimiert werden) um dann die Daten zu schreiben. Zu viele Dereferenzieren. In vielen Fällen macht das nicht viel aus. Hier aber schon, weil es so oft gemacht wird. Außerdem muss bei den Slices mit viel mehr Daten gehandelt werden. Der Pointer? 8 Byte. Die Größenangabe? 8 Byte. Die Nutzdaten? 4 Byte.

Wie wäre es also, wenn ich eine Indirektion umgehe, und statische Arrays in die Lookuptabelle stecke? Der Encodierungscode sieht danach etwa so aus:

let mut buf: [u8; ENCODE_BUFFER_SIZE] = [0; ENCODE_BUFFER_SIZE];
let mut n = input.read(&mut buf)?;
while n > 0 {
    for b in &buf[..n] {
        output.write_all(&ALPHABET_BYTES[*b as usize])?;
    }
    n = input.read(&mut buf)?;
}
Ok(())

Und die Geschwindigkeit ist noch einmal deutlich verbessert worden:

  Time (mean ± σ):      1.379 s ±  0.023 s    [User: 1.191 s, System: 0.435 s]
  Range (min … max):    1.356 s …  1.420 s    10 runs

Ich könnte auch noch versuchen, die Anzahl der write()-Aufrufe zu verringern. Ich würde sowieso einen gebufferten Writer empfehlen, aber da muss ich halt trotzdem noch einen Haufen Funktionsaufrufe machen. Wenn ich die reduzieren kann, bin ich vielleicht trotz des Overheads, den ein zusätzlicher Buffer bringt, schneller:

let mut write_buf: [u8; ENCODE_BUFFER_SIZE * 4] = [0; ENCODE_BUFFER_SIZE * 4];
let mut n = input.read(&mut buf)?;
while n > 0 {
    for (i, b) in buf[..n].iter().enumerate() {
        let bytes = ALPHABET_BYTES[*b as usize];
        write_buf[i * 4] = bytes[0];
        write_buf[i * 4 + 1] = bytes[1];
        write_buf[i * 4 + 2] = bytes[2];
        write_buf[i * 4 + 3] = bytes[3];
    }
    output.write_all(&write_buf[..n * 4])?;
    n = input.read(&mut buf)?;
}

Das bringt tatsächlich noch einmal ein paar 100ms:

  Time (mean ± σ):     975.3 ms ±  67.0 ms    [User: 796.8 ms, System: 416.2 ms]
  Range (min … max):   924.5 ms … 1135.5 ms    10 runs

Geben wir uns damit zufrieden und schauen mal das Decodieren an.

Optimieren des Decoders

Das Größte Problem bei der Optimierung des Decoders ist: Im Gegensatz zum Encoder kann das Decodieren fehlschlagen. Encoding ist eindeutig, jedes Byte ist genau einem Emoji zugeordnet. Beim Decoden ist das anders. Als Input kommen vier-Byte-Häppchen in Frage, die entweder

  • ein gültiges KittenMoji-Emoji sind
  • gültiger UTF-8-Text, aber kein gültiges KittenMoji
  • kein gültiger UTF-8 Text sind.

Ich zeige den Code hier jetzt nicht direkt, der ist ein bisschen sperrig, aber man kann die alte Version hier im Git-Repo auf gitlab.com finden.

Nur eine kurze Zusammenfassung, was der Code macht:

  1. lese bytes in den Buffer
  2. versuche, die Bytes als UTF-8-String zu interpretieren
  3. wenn der String ungültig ist, weil am Ende bytes für einen vollständigen Codepoint fehlen (kann immer mal vorkommen, wenn man aus dem Stream liest): Nimm den kürzesten gültigen String und merk dir, wie viel im Buffer danach noch benötigt wird
  4. wenn der String ungültig ist, weil er wirklich ungültig ist: breche mit Fehler ab
  5. gehe durch alle Codepoints des strings
  6. Für jeden Codepoint: schlage in einer (vorher generierten) Hashmap nach, auf welches Byte er gemapped ist
  7. Wenn der Codepoint nicht gemapped ist: breche mit Fehler ab
  8. Wenn der Codepoint gemapped ist: schreibe das dazugehörige Byte in den Output
  9. Dann kümmere dich darum, dass die ggf. überstehenden Bytes am Ende an den Anfang des Buffers verschoben werden und lies die nächsten Bytes in den Buffer

Da sind viele Stellen, die langsam sind. Insbesondere, in der tight loop: viele Anfragen an die HashMap (inkl. vieler Berechnungen von Hashes, viele Überprüfungen auf Fehler, viele write()-Operationen. Wie langsam ist es?

Benchmark 1: cat encoded | target/release/examples/decode > /dev/null
  Time (mean ± σ):     11.583 s ±  0.520 s    [User: 11.072 s, System: 1.645 s]
  Range (min … max):   10.952 s … 12.446 s    10 runs

Oh-oh. Über 12 Sekunden. Mehr als doppelt so viel wie der unoptimierte Encoder. Naja, probieren wir mal denselben Trick: Anstatt mit Strings zu arbeiten, arbeiten wir mal direkt auf den Bytes (den Code dieses Zwischenstandes habe ich leider verworfen, deswegen kann ich den hier nicht zeigen).

  Time (mean ± σ):     12.428 s ±  0.574 s    [User: 11.899 s, System: 1.686 s]
  Range (min … max):   11.569 s … 13.488 s    10 runs

WTF, es ist langsamer geworden? Probieren wir es noch mal. Immer noch langsamer… Und obwohl wir uns die String-Überprüfung und die Decodierung des UTF-8 gespart haben. Ok, Änderungen zurücknehmen. Wir schauen uns erst einmal andere Optionen zur Optimierung an. Was ist zum Beispiel, wenn wir den Iterator über Bord werfen und direkt, ohne weitere Funktionsaufrufe, in der Map nachschlagen?

  Time (mean ± σ):     11.018 s ±  0.229 s    [User: 10.484 s, System: 1.730 s]
  Range (min … max):   10.627 s … 11.274 s    10 runs

Minimal besser. Aber vermutlich nicht einmal statistisch signifikant besser (ich habe es nicht nachgerechnet). Aber Moment: vielleicht geht es ja schneller, wenn wir den Buffer vergrößern, in den read() schreibt? So von 128 auf 1024 Bytes?

  Time (mean ± σ):      9.673 s ±  0.180 s    [User: 9.142 s, System: 1.677 s]
  Range (min … max):    9.350 s …  9.884 s    10 runs

Besser. Aber da geht noch mehr. Machen wir das Gleiche wie beim Encodieren: Schreiben wir den Output erst einmal in einen Buffer, um write()-Aufrufe zu sparen:

  Time (mean ± σ):      9.056 s ±  0.433 s    [User: 8.521 s, System: 1.674 s]
  Range (min … max):    8.457 s …  9.648 s    10 runs

Mühsam nährt sich das Eichhörnchen. Also müssen wir uns wohl noch einmal anschauen, warum das direkt-auf-Bytes-Arbeiten hier nichts gebracht hat. Meiner Vermutung ist: Es ist die Hashberechnung. Es kann sein, dass die auf einen char einfach wesentlich effizienter zu berechnen ist als auf einem array. Aber char will ich hier nicht nehmen, dann haben wir wieder die UTF-8-Überprüfung, die wir eigentlich nicht benötigen. Wir haben ohnehin nur gültige Codepoints in der Hashmap, wenn eine Bytefolge nicht gefunden wird, dann ist sie ungültig, egal ob sie gültiges UTF-8 ist oder nicht.

Aber wir können die 4-Byte-Folge ja einfach in u32 umrechnen. Normalerweise muss man sich bei so etwas um die Byte-Order kümmern. Hier aber nicht. Die Byte-Order ist mir egal, solange Hashmap und Suchstring die gleiche haben. Wenn ich also auf beiden Seiten einfach die native Byte-Order nehme müsste es auf allen Maschinen funktionieren und ist nicht mehr Aufwand für die CPU als eine einfache Kopieroperation.

for (i, bytes) in buf[..len].chunks_exact(4).enumerate() {
    // using native byte order here is ok because the decode_map does also uses native byte
    // order.
    write_buf[i] = *decode_map.get(&u32::from_ne_bytes(bytes.try_into().expect("expected 4 byte slice to be transformed to 4 byte array"))).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            format!("unexpected byte sequence in stream: '{bytes:0x?}', either invalid utf-8 or non-kittenmoji code point"),
        )
    })?;
}

Und?

  Time (mean ± σ):      5.805 s ±  0.223 s    [User: 5.353 s, System: 1.444 s]
  Range (min … max):    5.491 s …  6.131 s    10 runs

Now we're talking! Natürlich haben wir immer noch den ganzen HashMap-Krams, der die Geschwindigkeit herunterzieht. Bevor jemand fragt: Ich habe es mit einer sortierten Liste und einer Binärsuche ausprobiert, das war deutlich langsamer. Vermutlich, weil es bei dieser Suche viele branches gibt. Die triviale Lösung, einfach die Liste zu durchsuchen ist hier noch langsamer.

Um die Fehlerbehandlung kommen wir auch so oder so nicht herum. Eine kleine Optimierung wäre noch, statt mit chunks_exact zu iterieren und die slices in arrays umzuwandeln (wieder mit Fehlerbehandlung, in diesem Fall aber das etwas effizientere expect, weil wir wissen, dass alle slices 4 bytes lang sind und der Fehlerfall nicht eintritt). Die Alternative array_chunks ist minimal besser, aber bisher nur in der nightly-Version verfügbar und nicht stabil.

Fazit

Ich habe es geschafft, das Encodieren und Decodieren für ein unglaublich ineffizientes Encoding viel effizienter zu machen. Und das besonders da, wo man es wirklich nicht verwenden will. Ein kleines Stückchen Binärdaten mit Emojis zu encodieren? Das sieht lustig aus, und jemand auf Mastodon hat darauf hingewiesen, dass z.B. Mastodon (mit Längenbeschränkung für toots) jedes Emoji als ein Zeichen zählt. Hier ist dieses Encoding also sogar effizienter als Base64, zumindest für den Benutzer.

KittenMoji wurde übrigens für Schlüsselencoding im Small Web erdacht (siehe die Dokumentation von kitten, ein Small Web development kit). Die Idee hinter dem Small Web ist es, wieder mehr selbstgehostete Websites zu, aber trotzdem den Community-Effekt von z.B. Facebook zu haben. Also ein dezentrales soziales Netzwerk, ohne die Nachteile von Facebook. Es soll einfach aufzusetzen sein, aber um ehrlich zu sein, ich bleibe lieber bei meinem guten altmodischen Blog, ohne zu viele fancy features.

Das Repo für meinen KittenMoji De-/Encoder findet ihr auf Gitlab.com. Ich betrachte das Crate soweit erst einmal als Feature-Complete, warte aber noch ein bisschen ab, bis ich die Version auf 1.0.0 setze, vielleicht fällt mir ja noch irgendetwas Wichtiges ein. Wer noch weitere Ideen zur Optimierung hat kann sie gerne ausprobieren, ich nehme Patches an. Wichtig ist, dass alles safe rust ist und keine Abhängigkeiten zum Crate hinzugefügt werden.

Wer KittenMoji in der eigenen Software verwenden möchte… Ich würde fast empfehlen: Kopiert die Quelldateien und fügt sie bei euch ein, schlagt euch nicht mit einer Abhängigkeit herum, die ich höchstwahrscheinlich nicht pflegen werde.

Drei neue oder alte Gründe zur Frustration

Das hier soll ein kurzer Rant werden um einfach mal Dampf abzulassen. Die Themen schweben teilweise seit Mitte letzter Woche im Raum, aber ich hatte nicht die Kraft, darüber zu schimpfen. Jetzt muss es aber langsam mal raus, sonst drehe ich komplett durch.

Merz macht Wahlmampf für die AfD

Da sei zu nächst die Meldung, dass Merz der Ampel ein Ultimatum bis Dienstag stellt. Diese Woche Dienstag, also vorgestern. Merz will Menschen direkt an der Grenze abweisen. „Asylbewerber“. Mit anderen Worten: KEINE HALBE WOCHE nach den FUCKING REKORDERGEBNISSEN FÜR DIE AfD IN LANDTAGSWAHLEN, wo alle so verzweifelt waren, wie eine Hasspartei wie die AfD es so weit bringen konnte MACHT MERZ WAHLKAMPF FÜR EBEN JENE HASSPARTEI. Auf dem Rücken der Schwächsten der Schwächsten. Auf derm Rücken der Leute, die nun definitiv ÜBERHAUPT NICHTS dafür können, was in unserem Land alles schief geht. Es ist wissenschaftlich belegt, dass das Aufgreifen rechter Narrative und Themen durch gemäßigte Parteien nicht die Wähler von der rechtsextremen Partei zu der gemäßigten Partei bringt, sondern umgekehrt, und in dem Zuge auch nebenbei noch rechte Narrative Salonfähig macht.

Fuck you, Merz. Du bist Teil des Problems. Wenn die AfD die Macht übernimmt, hast du kein Recht, dich darüber zu beklagen. Du hast ihnen geholfen. Aber SPD, Grüne, FDP und BSW waran auch mit von der Partie gegen Einwanderung zu hetzen. Ihr könnt euch die zweifelhaften Lorbeeren gerne teilen.

Schon wieder die Vorratsdatenspeicherung

Wenn du merkst, dass du ein totes Pferd reitest, geh zu einem Nekromanten. Dann hast du zwar ein stinkendes, untotes Pferd, aber vielleicht, nur vielleicht läuft es ja dieses Mal in die richtige Richung.

Die Vorratsdatenspeicherung ist nicht nur tot, sondern zwei Mal tot, gekippt vom Bundesverfassungsgericht und vom Europäischen Gerichtshof. Ihr Nutzen ist bestenfalls zweifelhaft, ihre Verfassungswidrigkeit von den höchsten Gerichten bestätigt.

Trotzdem will die SPD jetzt in einem Akt des blinden Aktionismus die Vorratsdatenspeicherung wieder einführen und dabei direkt auch noch den Koalitionsvertrag brechen.

Cookie-Banner

Das dritte Thema ist nur ein kleiner Aufreger, damit ich das Gefühl habe, das nicht alles furchtbar, sondern manches einfach nur ein bisschen scheiße ist. Ich habe in einem alten Artikel schon einmal meine Meinung zu den Cookiebannern dargestellt: sie sind nervig, aber nicht notwendig wenn man seinen Nutzern nicht hinterherspioniert. Sie dienen einzig und allein als Feigenblatt um behaupten zu können, die Nutzer hätten dem massiven Datensammeln zugestimmt.

Kleiner Tipp: Wenn eine Website behauptet, dass ihr „deine Privatsphäre sehr wichtig“ ist und im selben Atemzug die Zustimmung von dir haben will, deine aten mit mehr als 700 Partnern (keine Übertreibung) zu teilen, dann ist denen deine Privatsphäre nicht wichtig.

Die Bundesregierung will jetzt jedenfalls Cookie-Banner eindämmen. So weit klingt das erst einmal nicht schlecht. Die Dinger nerven ja schon. Ich hätte da einen Vorschlag: Es gibt schon diesen Do-Not-Track-HTTP-Header. Da kann man dem Browser sagen, man möchte nicht getracked werden, und dann teilt der Browser das jeder Website mit. Das Problem: keine Website hält sich dran.

Aber man könnte das ja gesetzlich festlegen: Der Header würde dann als explizite nicht-Einwilligung gelten, und somit das Cookie-Banner überflüssig machen.

Hier gibt es nur ein Problem: Die Webseitenbetreiber haben daran kein Interesse. Die wollen die Leute so lange nerven, bis sie der Datensammlung zustimmen. Und da es ja vereinzelt auch sinnvolle Dinge gibt, für die persönliche Daten verarbeitet werden können, wird es auch schwierig, das komplett zu verbieten.

Der Vorschlag der Bundesregierung geht sowieso nicht in diese Richtung. Leut deren Vorstellung soll nämlich eine Drittpartei die Zustimmungen verwalten.

  • Nachteile: hat dieselben Probleme wie oben beschrieben, außerdem hat jetzt einen Drittpartei Daten darüber, welche Seiten du besuchst. Und wenn dieser Dienst mal ausfällt, ist Land unter.
  • Vorteile: Äh… ich würde welche nennen, wenn ich nur welche wüsste.

Update

Ich habe es gerade noch einmal nachgeschlagen laut Artikel 21, Absatz 5 der DSGVO gilt:

Im Zusammenhang mit der Nutzung von Diensten der Informationsgesellschaft kann die betroffene Person ungeachtet der Richtlinie 2002/58/EG ihr Widerspruchsrecht mittels automatisierter Verfahren ausüben, bei denen technische Spezifikationen verwendet werden.

Das wäre zum Beispiel auch der Do-Not-Track-Header. Die Tatsache, dass ich den setze und trotzdem noch Cookiebanner angezeigt kriege ist der Beweis dafür, dass das nichts bringt.

Fazit

Fazit: Ich ziehe mich zurück, informiere mich nicht mehr darüber, was in der Welt so vorgeht, schreibe Programme die bunte Lichter machen und üq048wrhqaüosifj9´hq3zrhqü9wuefhqß9274gz=(&T=§)/WZHEFD)`

Karte oder bar? Ach warte…

Als ich heute zum Essen in die Mensa ging, begrüßten mich große Aushänge: Kartenzahlung sei leider nicht möglich, nur Zahlung per Studierendenausweis ginge noch. Grund: bundesweiter Kartenzahlungsausfall.

Glücklicherweise habe ich mittlerweile einen Promovierendenausweis, was aufgrund einiger bürokratischer Hürden recht lange gedauert hat. Man kann in der Mensa nämlich nicht mehr bar zahlen. Wäre das mit der Kartenzahlung in der ersten Hälfte des Jahres passiert, hätte ich heute hungrig bleiben müssen.

Grund: Die Mensa und die Cafeterien der RUB akzeptieren kein Bargeld mehr. Selbst wenn man nur ein belegtes Brötchen kaufen möchte muss das mit EC-Karte (ach warte, die haben das ja umbenannt, Girocard) oder mit einem vorher aufgeladenen Studierendenausweis ankommen. Es gibt nicht eine einzige Kasse, an der man noch bar zahlen kann.

Das hat dazu geführt, dass man heute einen Studentenausweis haben musste oder hungrig blieb. Im Ausland (Beispiel: Niederlande) werden wir Deutschen ja gerne belächelt, weil wir so am Bargeld festhalten. Aber hier ist mal ein Beispiel was passiert, wenn man Bargeld überhaupt nicht mehr verarbeiten kann.

Stra?e

Ich habe ein Paket bestellt. Der Händler hat es nebst meiner Adresse dem DPD anvertraut. Ich wohne in einer der zahlreichen Straßen, die auf „straße“ enden.

Jetzt kriege ich eine SMS von denen, dass mein Paket bald zugestellt werden soll und ich z.B. den Zustelltermin anpassen kann oder denen erlaube, es irgendwo abzustellen.

Was steht auf der Adresse? „[Soundso]stra?e“. Es ist 2024 und der Deutsche Paketdienst kann nicht mit ß umgehen? Beim Händler kann das Problem nicht liegen: das Label auf dem Paket hat eindeutig ein ß.

Im Ernst 2024. Textencoding ist ein gelöstes Problem!

Erinnerung: Infinite Screaming Generator

In Anbetracht der Wahlergebnisse der Landtagswahl Thüringen 2024 möchte ich gerne noch einmal auf den Infinite Screaming Generator hinweisen, den ich in einem Blogpost im Juni schon erwähnt habe (es gibt den auch als Mastodon-Bot).

Wenn die CDU nicht mit der AfD koalieren möchte, müssen sie meiner Rechnung nach mit dem BSW, den und den Linken koalieren. Für die AfD würde eine Koalition mit CDU oder dem BSW reichen (oder mit der Linken und der SPD, aber das kann ich mir nun wirklich nicht vorstellen).

Das BSW hat sich bisher durch ähnlich populistische Themen wie die AfD positioniert (insbesondere im Bereich Ausländerfeindlichkeit stehen sich die beiden nahe), haben sich aber klar gegen eine Koalition mit der AfD ausgeprochen. Die CDU hingegen…

Auf der einen Seite sprechen sich viele Politiker der CDU immer wieder gegen die AfD aus. Auf der anderen Seite haben einige CDU-Mitglieder immer wieder gezeigt, dass die Brandmauer gegen Rechts aus Stroh besteht. Und die CDU hat ja auch dadurch, dass sie Themen von der AfD immer wieder aufgegriffen hat, die AfD erst gesellschaftsfähig gemacht. Und eine Koalition mit der Linkspartei wird, soweit ich das einschätzen kann, in der CDU als genau so gefährlich angesehen wie eine Koalition mit der AfD.

Meine Hypothese: Innerhalb dieser Woche wird irgendein Thüringischer CDU-Politker etwas von „wir müssen pragmatisch denken“ und „wir müssen dem Wählerwillen folgen“ reden und eine Koalition mit der AfD „nicht kategorisch ausschließen“.

Dann wird es einen riesigen Shitstorm geben und dieser Politiker wird sagen, dass das nicht so gemeint war oder dass seine Worte aus dem Zusammenhang gerissen wurden.

Darüber, wie der kommende Landtag in Thüringen aussieht, mag ich nicht spekulieren. Ich weiß nur eins: Schön wird es nicht.

Straßenlärm in Witten

Seit Ende letzten Jahres wohne ich in Witten. Eigentlich ist meine Wohnung ganz gut gelegen. Aber die kleine Straße, die an dem Haus vorbeiführt hat Tempolimit 50. Dabei ist sie nun wirklich nicht so breit.

Und obwohl meine Wohnung der Straße nicht direkt zugewandt ist und sogar eine Haushälfte entfernt von ihr sitzt, ist der Straßenlärm nachts unerträglich laut. Das war im Winter kein Problem. Jetzt, im Sommer, habe ich aber praktisch immer die Tür zu meinem Schlafzimmer offen. Und meine Güte geht mir das auf die Nerven.

Und der normale Autolärm ist schon schlimm genug. Schlimmer wird es bei nasser Fahrbahn. Dann sind die Laufgeräusche deutlich schlimmer. Auch bin ich schön öfters beim kurz vor dem Einschlafen von einem LKW geweckt worden, der mit hoher Geschwindigkeit über einen kleinen Huckel fährt, den es wohl in der Straße geben muss und dann einen lauten KNALL macht.

Am Schlimmsten sind aber die Autos und Motorräder mit kaputtem Auspuff, die hier regelmäßig mitten in der Nacht mit gefühlt viel zu hoher Geschwindigkeit über die Straße brettern. Da machen die Laufgeräusche keinen Unterschied mehr, der Motor ist hier einfach lauter als bei einem normalen Auto, wenn man das Ohr direkt daran halten würde.

Nun ist natürlich die Frage: Warum ist auf dieser Straße überhaupt 50 erlaubt? Die ist nun wirklich nicht so breit. Die könnte auch gut 30 sein. Stellt sich heraus: Die Stadt Witten hat da schon einen Plan. So 20% eines Plans. Sie wollten auch Input von Bürgern der Stadt. Dummerweise haben sie dafür nicht genug Marketing gemacht, an mir ist das glatt vorübergegangen. Von Juni bis Juli konnte man Vorschläge einreichen. Ich habe diese Seite erst ende Juli entdeckt. Immerhin scheint meine Straße so oder so im Fokus zu liegen.

Ich schaue Mal, was daraus wird. Die wollen jetzt einen Plan erarbeiten. Der nächste Plan danach kommt erst 2019, also hoffe ich stark, dass dieser hier was taugt. Und einfach nur die Höchstgeschwindigkeit runtersetzen (wobei auch auch befürchte, dass die das nur nachts machen wrden) hilf halt nicht. Man muss den Fahrern auch das Gefühl vermitteln, dass man nicht so schnell fahren kann.

Man könnte zum Beispiel einen Radweg ergänzen. Momentan ist für Fahrräder explizit erlaubt, den Gehweg zu verwenden. Ein angehobener Radweg könnte die Straße so weit verkleinern, dass die Autos nicht mehr 50 fahren können. Und besser für den Radverkehr (und die Fußgänger) wäre das auch.

Die dreifache Hölle der Nextcloud-Konfiguration

Ich betreue auch eine kleine Nextcloud-Instanz. Eigentlich ganz schön, so kommt man hier und da um große Anbieter wie Google herum.

Nextcloud hat auf der Admin-Übersichtsseite eine nette Funktion, die einem sagt, welche Konfigurationen alle falsch aussehen. Aber wenn die so falsch sind, warum kann Nextcloud die nicht selber korrigieren? Das liegt daran, dass diese Konfigurationen außerhalb von Nextclouds Einflussbereich liegen.

So beschwert es sich über einige Fehler in der nginx-Konfiguration. Da fängt es schon an. Ich brauche eine ziemlich komplizierte nginx-Konfiguration, um nextcloud überhaupt zum Laufen zu kriegen. Ein „leite einfach alles an Nextcloud weiter und lass Nextcloud entscheiden, was es macht“ reicht nicht. Ich muss einen Haufen Routen konfigurieren, Sicherheitsheader, Umleitungen, das ganze Programm.

Gut, das muss ich für z.B. dieses Blog hier auch machen. In diesem Blog gibt es aber einen wesentlichen Unterschied: Es ist nur ein Haufen Dateien, da muss man nginx halt sagen, was es damit machen soll. Und das ist auch nicht so schwierig, das läuft wesentlich einfacher als die Konfiguration für Nextcloud. Man könnte fast sagen, Nextcloud ist keine abgeschlossene Software, sondern ist für Grundfunktionalitäten von nginx (oder apache, den ich aber nicht benutze) abhängig.

Warum ist das so? Ich weiß es nicht, aber ich habe eine Vermutung. Nextcloud ist in PHP geschrieben, und PHP hat so eine seltsame Abhängigkeit zur FastCGI-Schnittstelle eines Webservers. Das ist eigentlich auch keine Entschuldigung, eigentlich müsste Nextcloud trotzdem seinen eigenen Kram regeln können. Tut es aber nicht. Stattdessen verlangt es sogar, dass ich auch noch in der PHP-Konfiguration herumschraube, damit es ordentlich läuft. Da stehen dann so Sachen drin, wie viel Arbeitsspeicher es maximal verwenden darf, oder timeouts für Requests.

Man stelle sich das mal bei einer anderen Scriptsprache vor. Wenn ich zum Beispiel bei Python erst einmal in einer globalen Konfigurationsdatei festlegen müsste, dass es mehr als 128MiB Speicher verwenden darf. Aber damit hört es natürlich nicht auf. In der PHP-Konfiguration gibt es nicht eins, nicht zwei, sondern drei Verzeichnisse mit Konfigurationsdateien. Gut, apache2 wird es nicht sein, ich nutze ja nginx. cli wohl auch nicht, das wird vermutlich nur für Kommendizeilenprogramme verwendet. Also bleibt eigentlich nur fpm.

Nur leider bringt das nichts. Nextcloud bleibt bei seinen 128MiB Speicher. Selbst wenn ich die Limits auch in den anderen Dateien anpasse. Online findet man jede Menge zu den diversen Problemen, nichts davon hilfreich. Die Dokumentation (sogar in der Warnung verlinkt) geht auf diese Details überhaupt nicht ein.

Nun will ich ja nicht sagen, dass diese Einschränkungen nicht wichtig sind. Aus Sicherheitsgründen oder so. Aber wenn es dafür notwendig ist, diese Einstellungen über drei verschiedene Programme hinweg zu machen, während es grundsätzlich auch möglich wäre, das alles in einem Programm zu machen, dann ist beim Design der ganzen Sache irgendwo etwas schief gelaufen.

Ich habe jetzt mal wieder mehrere Stunden investiert und immer noch sind diese blöden Warnungen da, und niemand kann mir auch nur erklären, was die meisten davon eigentlich bedeuten. Also bleiben die jetzt erst einmal. Und das wirklich Nervige: eigentlich wollte ich nur herausfinden, warum seit einiger Zeit Bilder nicht mehr in nextcloud selber angezeigt, sondern mit immer direkt zum Download angeboten werden, wenn ich auf sie klicke.

Der Gegenangriff gegen Single Page Applications

Ein altes Thema in diesem Blog, häufig von Rants begleitet, ist der übermäßige Einsatz von Javascript auf Websites. Insbesondere wenn Grundfunktionen der Website nicht mehr gehen, wenn man kein Javascript aktiviert hat. So habe ich mich zum Beispiel 2010 auf meinem alten Blog darüber aufgeregt, dass bei einer oder mehreren Websites Grundfunktionen wie die Seitennavigation, der Login oder das Herunterladen von Dateien nur mit Javascript. Oder die Suchfunktion Oder, im selben Jahr, ein Mini-Rant, ohne in die Details zu gehen.

Als ich dann 2012 dieses Blog in Ruby on Rails umgesetzt habe, habe ich extra darauf geachtet, überhaupt kein Javascript zu verwenden. Damals waren alle Rails-Tutorials Javascript-lastig. Nicht extrem, man hätte die Seite noch benutzen können, aber es war da. Ich wollte das nicht. 2020, als ich die auf Rails basierende Software durch einen static site generator ersetzt habe, habe ich das beibehalten.

Dazwischen habe ich eine ganze Menge über Webentwicklung gelernt. 2015 habe ich angefangen, in Hamburg zu arbeiten. Ich habe am Backend und am Frontend gearbeitet. Und musste auch selber Javascript schreiben. Zuerst für eine Website, die für das meiste an Javascript einen Fallback hatte. Später dann an React-Anwendungen, die keinen Fallback hatten. Mea culpa. Ich war Teil des Problems geworden. Aber es war mir schlicht nicht möglich, meine Kollegen zu überzeugen, dass das eigentlich Wahnsinn ist. Obwohl mir das durchaus bewusst war, wie dieser Blogpost von 2017 zeigt.

Warum so viel Javascript?

Warum? Warum hat sich übermäßiges Javascript, insbesondere Single Page Apps (SPAs) so weit durchgesetzt? Hier hat jemand letztes Jahr mal zusammengefasst, wie sich das aus seiner Perspektive entwickelt hat. Ich kann den Artikel empfehlen, und auch ein paar andere Artikel auf der Seite, auf die ich später zurückkomme. In einer Sache bin ich aber anderer Meinung als der Autor. Der geht davon aus, dass der kontinuierliche Aufstieg von SPAs auf gezieltes Marketing wider besseren Wissens passiert ist.

Ich sehe das nicht ganz so von bösen Absichten durchdrungen. Ich bin einfach der Meinung, wir, als Entwickler, sind einer Art kollektivem Stockholm-Syndrom zum Opfer gefallen (ja, ich weiß, das Stockholm-Syndrom gibt es wahrscheinlich nicht wirklich. Aber es ist eine schöne Geschichte und eine passende Metapher). Wir stecken halt bis zum Hals im JS-Code mit allen Probleme, die das bringt, wir kommen nicht ohne weiteres mehr heraus, aber hey: schaut euch dieses Tool an, das alles viel einfacher macht!

Ich will Javascript nicht die Existenzberechtigung absprechen. Eine Landkartenseite wie OpenStreetMap? Ein Videokonferenztool? Ein Spiel? Klar, das braucht Javascript? Ein Webshop? Höchstens ein bisschen, zum Beispiel um im Checkout das Leben einfacher zu machen (und auch das sollte einen Fallback haben). Aber ein Blog? Eine Newsseite? Ein Webcomic? Eine Rezepte-Seite? All das braucht kein Javascript.

A New Hope

Deswegen hat es mich sehr gefreut, dass ich in der letzten Monaten immer wieder Artikel gegen übermäßigen Einsatz von Javascript gefunden habe. Und in der letzten Woche vier Artikel vom oben erwähnten Blog, wo der Autor ordentlich mit SPAs aufräumt. Insbesondere geht es da auch um öffentliche Websites in den USA (Kalifornien, um genau zu sein), wo Leute SNAP benefits beantragen können oder so (so eine Art Sozialhilfe). Dummerweise ist die offizielle Seite dazu etwa 25 fucking Megabyte groß, der größte Teil davon… natürlich Javascript. Auch in Kalifornien gibt es Gegenden mit nur langsamem Internetzugang, und 20 Megabyte JS sind für schwachbrüstige Geräte (Leute, die auf Sozialhilfe angewiesen sind haben i.d.R. keine high-end-Smartphones) auch ein ganzer Happen. Ergebnis: Die Seite braucht knapp 30 Sekunden um zu laden, eine vergleichbare Seite einer Drittpartei kommt auf vier Sekunden.

Mir machen diese Artikel Hoffnung, dass der Wind sich so langsam dreht. Nur ein bisschen Hoffnung, nicht viel. Vor knapp neun Jahren gab des den Talk The Website Obesity Crisis. Jetzt haben wir die oben genannte 25MiB-Seite und wie dieser aktuelle Artikel über Javascript-Bloat zeigt, ist die Situation insgesamt auch nicht viel besser.

Ach ja: Bei dem Bookshop, an dem ich entwickelt habe, haben wir stets versucht, das Javascript klein zu halten. Es ist nicht wirklich gelungen, weil wir einige sehr alte Browser unterstützen mussten, aber wir sind bei um die 200kiB geblieben. Es geht nicht in meinen Kopf herein, was man machen muss, um auf 25MiB zu kommen. Wirklich nicht.

Gründe gegen SPAs (oder überflüssiges Javascript auf nicht-SPAs)

Naja, jedenfalls ist das jetzt die Gelegenheit für mich, auch mal meinen Senf dazuzugeben und zu erläutern, welche Probleme man sich mit SPAs im Speziellen oder zu viel Javascript im Allgemeinen aufhalst.

Zuerst ist da natürlich die Größe, wie schon oben genannt. Trotz aller Beteuerungen, dass SPAs viel effizienter seien, weil sie für den Inhalt ja nur JSON nachladen müssen und nicht das ganze HTML immer und immer wieder, sind SPAs in der Regel so groß, dass man tausende Seiten anschauen muss, bevor sie sich lohnen.

Dann ist da die Archivierbarkeit. SPAs können zum Beispiel vom internet archive viel schlechter archiviert werden, weil sie dynamisch Daten nachladen. Das ist den meisten Betreibern natürlich egal. Was denen aber wichtig ist: sie wollen durch Suchmaschinen gefunden werden. Das funktioniert mittlerweile auch für SPAs, aber bei weitem nicht so gut wie für Websites, die ihren Inhalt direkt ausliefern.

Dinge, die der Browser schon kann und die in SPAs mühsam erneut implementiert werden müssen

Da wäre zunächst Routing: Normalerweise ist Routing einfach: Man schreibt ein <a>-Tag, verpasst ihm ein href-Attribut, fertig. Auf SPAs muss man aber zusätzlich noch:

  • alle Klicks auf (interne, und nur interne) Links abfangen, damit der Browser die Seite nicht neu lädt
  • die URL der Seite anpassen, ohne dass der Browser die Seite neu lädt
  • den Inhalt der Seite ändern
  • nach oben scrollen, damit die User den Anfang der Seite sehen und nicht die Position, wo man bisher war
  • oder gegebenenfalls zum richtigen Anker auf der Seite springen, falls die URL ein Fragment (#) hat
  • wenn die Seite neu geladen wird, in die URL schauen um den richtigen Inhalt anzuzeigen

Bevor jemand damit kommt, dass man diese Sachen sonst serverseitig machen muss: Erstens muss man die meisten davon nicht serverseitig machen, weil sie clientseitig laufen und das normalerweise der Browser erledigt. Zweitens muss man die Teile, die man doch serverseitig machen muss (unterschiedlichen Inhalt über unterschiedliche URL-Pfade ausliefern) trotzdem machen, weil man bei SPAs üblicherweise einen API-Server im Hintergrund hat, von dem die eigentlichen Inhalte kommen.

Das ist natürlich angenommen, man möchte es den Usern ermöglichen, eine normale Website-Erfahrung zu haben, wo u.a. auch die Navigation mit den zurück/vorwärts-Buttons des Browsers funktioniert, man zu Abschnitten innerhalb der Seite springen kann und Links auf einzelne Seiten hat, die man auch mit anderen teilen kann. Wenn man sich diese Mühe nicht macht, haben die User eine erheblich eingeschränkte Version einer normalen Website. In einem Fall haben mir Kollegen in einem anderen Team auch mitgeteilt, sie könnten keine direkten Links auf Seiten machen, weil sie dafür den Webserver so einstellen müssten, dass er für verschiedene URLs genau denselben Inhalt ausliefert. Nein, wirklich!

Besonders aufgefallen sind mir auch, wie schon oben verlinkt Links, die nicht an <a>-Tags hängen.

Andere Dinge, die man nachimplementieren muss:

  • Formulare (unterschätzt das nicht, da gibt es viele Fallstricke)
  • Requests an den Server im Allgemeinen
  • eventuell eine Ladeanzeige („spinner“)

Diese Liste ist nicht vollständig, es gibt noch mehr. Routing ist aber definitiv der wichtigste und umständlichste Punkt.

Komplexität

Komplexitätssucht ist eine Art Berufskrankheit unter Softwareentwicklern. Um Rich Hickey zu zitieren:

I think programmers have become inured to incidental complexity… when they encounter complexity, they consider it a challenge to overcome, rather than an obstacle to remove. Overcoming complexity isn’t work, it’s waste.

Manche Komplexität lässt sich nicht entfernen. Manche Komplexität würde sich entfernen lassen, ist es aber Wert, mit ihr umzugehen (ich, als Rust-Fan, sehe Rust in der zweiten Kategorie). Aber vieles an Komplexität, die wir uns antun, ist unnütz, nervig und schädlich.

SPAs sind ein schönes Beispiel dafür. Der ganze Krams, den ich oben über das Reimplementieren von Browserfunktionalitäten geschrieben habe? Unnütze Komplexität. Wo kommt in SPAs sonst noch vermeidbare Komplexität her? Hier nur eine kurze Auswahl:

  • manuell geschriebene HTTP-Requests und das Parsen der Ergebnisse (und sanity-checks, ob die Antworten das enthalten was man erwartet. Kann man auch lassen, aber dann hat man halt später Spaß mit komischen Fehlern wenn mal etwas nicht passt)
  • Behandeln von Sonderfällen für ältere Browser (polyfills)
  • Abhängigkeiten auf Bibliotheken von Dritten, insbesondere Frameworks wie React (jede Abhängigkeit bedeutet einen erhöhten Wartungsaufwand. Und man sollte besser alle Updates zeitnah mitnehmen, oder man kommt irgendwann an den Punkt, wo man ein neues Update braucht, es aber nicht installieren kann, weil man vorher mehrere Jahre Update nachholen muss)
  • Fehlerbehandlung

Dazu kommt Komplexität bei den Entwicklertools. Da wären zunächst die Entwicklertools selber. Man braucht

  • NPM (für die oben genannten Abhängigkeiten)
  • einen Linter (Javascript ist so furchtbar, dass das, im Gegensatz zu anderen Sprachen, mehr als nur ein nice-to-have ist)
  • etwas wie Webpack, um das Ergebnis irgendwie zusammenzufügen
  • einen Typescript-Compiler, wenn man Typsecript verwendet (was zu empfehlen ist, weil man damit Fehler vermeiden kann, die in so einer komplexen Umgebung schnell passieren)
  • etwas wie Babel, um Polyfills für veraltete Browser einzufügen, die man unbedingt noch supporten muss

Es ist ein bisschen so, als ob man beim Hausbau beim Fundament gepfuscht hat, das Haus sich schon während des Baus zur Seite neigt, man aber einfach weiter baut und drumherum jede Menge Gerüste und Stützen hinstellt, damit das Haus nicht zusammenbricht, anstatt von vornherein für ein vernünftiges Fundament zu sorgen. So ein bisschen wie der Schiefe Turm von Pisa, wo man noch Jahrhunderte nach dem Bau mit den Folgen zu kämpfen hat. Nur dass eine Webapp nicht zu einer beliebten Touristenattraktion wird.

Was sind die Nachteile von Komplexität?

  • Wartung: mehr Komplexität macht Wartung und Weiterentwicklung schwieriger und damit teurer
  • Stabilität: mehr Komplexität macht Software anfälliger für Fehler, jeder Fehler kann größere Auswirkungen haben. Bonuspunkte: sobald auch nur eine Javascript-Datei nicht geladen werden kann, blockiert oft die ganze Seite
  • Security: was für die Stabilität gilt, gilt auch für die Sicherheit: je unübersichtlicher, desto eher schleicht sich ein Fehler ein, der ernsthafte Konsequenzen hat
  • Performance: Mehr Komplexität bedeutet mehr Code, d.h. es muss mehr heruntergeladen werden und auch die Ausführung ist langsamer

Vermeintliche Lösungen

Eine beliebte Lösung für ein paar der Probleme, insbesondere das mit der Archivierbarkeit und der Suchmaschinenfreundlichkeit: Wir rendern die Seite serverseitig und lassen sie dann clientseitig als SPA laufen. Das geht schon fast in die richtige Richtung, wird aber häufig durch zwei Probleme heruntergezogen: Erstens kann man die Seite dann ohne Javascript trotzdem nicht nutzen (bzw. muss man trotzdem eine ordentliche Menge an Javascript laden, was den vermeintlichen Größenvorteil von SPAs endgültig zunichte macht), zweitens ist das für die Entwickler eine Menge Arbeit, weil sie die Seite gleich zwei Mal entwickeln müssen.

Das Ergebnis ist, dass so etwas meist eher halbherzig gemacht und für manche Seiten auf der Website komplett vergessen wird. Die Lösung dafür wiederum ist etwas, was die Marketingabteilung „isomorphic Javascript“ genannt hat. Da soll man dann denselben Code Server- und clientseitig laufen lassen können. Der Nachteil: noch mehr Komplexität.

Mal ganz abgesehen davon, dass Javascript eine furchtbare Sprache ist, die ich nicht auch noch serverseitig sehen will, gibt es einen ganzen Haufen von Dingen, die auf dem Server grundsätzlich anders laufen als im Client. Ich habe einmal mit Nuxt gearbeitet. Das ist quasi eine isomorphic Javascript-Version von vue. Der ganze Code bestand aus Ausnahmen, wo man dann doch darauf achten musste, wo man serverseitig und wo man clientseitig arbeitete. Man musste überall höllisch aufpassen, dass man nicht aus Versehen irgendwelche serverseitigen Secrets leaked. Das ist eine Sicherheitslücke, die nur darauf wartet, implementiert zu werden.

Alternativen

Denken wir mal konstruktiv: Was sollten wir denn machen? Erst einmal: Keine SPAs, macht klassische Websites wo immer möglich. Dann gibt es da das Konzept des „unobtrusive Javascript“: Javascript, dass Funktionen ergänzt bzw. die Benutzbarkeit erhöht, wobei es aber trotzdem noch möglich sein muss, die Seite ohne Javascript zu verwenden (dann gehen halt schlimmstenfalls ein paar Komfortfunktionen verloren).

Verallgemeinert ist das im Konzept Progressive Enhancement. Das läuft auf dasselbe hinaus, bezieht aber auch das Styling (CSS) mit ein. Die Idee: Eine Seite sollte komplett ohne Styling schon benutzbar sein (z.B. auch in einem textbasierten Browser wie Links). Das Styling kommt obendrauf, um die Seite schöner und vielleicht auch ein bisschen übersichtlicher bzw. lesbarer zu machen. Erst dann kommt das Javascript obendrauf, für Komfortfunktionen.

Dazu gibt es einen schönen Artikel der britischen Regierung. Als Website-Beispiel kann ich zum Beispiel dieses Blog hier nennen. Aber es gibt auch wichtige Websites, die das richtig machen: Wie ich letzten Monat festgestellt habe funktioniert zumindest das Lesen von Wikipedia-Artikeln sehr gut auch ohne CSS. Als Negativbeispiel kann man leider fast jede größere Website nennen.

Ausklang

So, das war wieder ein Artikel der deutlich länger geworden ist als ursprünglich geplant. Also spare ich mir den anderen, nur entfernt relevante Kram, den ich sonst noch im Kopf habe. Was können wir also mitnehmen?

  • SPAs sind für die meisten Anwendungsfälle eine Scheißidee
  • macht eure Websites nach dem Progressive Enhancement-Prinzip, nutzt Javascript nur, um Komfortfunktionen hinzuzufügen oder dann, wenn ihr etwas machen wollt, was wirklich nicht ohne Javascript geht
  • hoffen wir mal, dass die Tage der SPAs gezählt sind (aber ich glaube nicht wirklich daran)