Javascript Local Storage

Es gibt eine große Zahl gruseliger APIs (also: „Kram, den ein Programm tun kann“) in modernem Javascript, von Sensor APIs (zur Abfrage von Beschleunigung und Orientierung des Geräts und des Umgebungslichts) bis Websocket (mit dem weitgehend beliebige Server dauernd mit dem Client reden können, und das auch noch „im Hintergrund“ in Web Workers) – gruselig ist das, weil in aktuellen und nicht weiter modifizierten Browsern jede Webseite dieses Zeug nutzen kann, wobei eingestandenermaßen ein paar besonders dramatische APIs (Mikrofon und Kamera z.B.) noch durch Rückfragen abgesichert sind. Oh: Webseiten können das nicht nur nutzen, viel zu viele brauchen Javascript und gehen nicht, wenn mensch es abdreht.

Der breite Zugriff auf fast alles von Javascript aus ist der Grund, warum ich des Öfteren ziemlich unwillig auf „please enable Javascript“-Banner (und noch mehr auf ihre Abwesenheit trotz Abhängigkeit von Javascript) reagiere. Javascript erlauben bedeutet spätestens seit XMLHTTPRequest (mit dem Javascript an der NutzerIn vorbei mit dem Ursprungsserver kommunizieren konnte; kam in der ersten Hälfte der Nullerjahre) und CORS (was XMLHTTPRequest und jetzt fetch auf beliebige Server ausweitete, solange die kooperieren; im Firefox tauchte das 2009 auf) einen recht weitgehenden Zugriff auf die eigene Maschine zu erlauben, und das noch ganz ohne Spectre und Freunde – die übrigens ohne ubiquitäres Javascript auf privaten Rechnern weitgehend bedeutungslos wären.

Eine API, die ziemlich gruselig ist, weil sie Webseiten ein unendlich langes Gedächtnis in eurem Browser gibt, ist Local Storage; damit können Webseiten ernsthafte Datenmengen in eurem Browser speichern und sie wiederfinden, wenn sie das nächste Mal Javascript ausführen dürfen. Dass das zunächst lokal gespeichert wird, hilft nicht viel – per Websocket oder zur Not einem fetch mit Payload ist der Kram auch (wieder) auf jedem Server, der bereit ist, sich das anzuhören. Wohlgemerkt: ohne dass NutzerInnen irgendwas mitbekommen.

Wenn ihr mein Gruseln nachfühlen wollt, könnt ihr hier mal Javascript einschalten (ihr lasst mich doch sonst kein Javascript ausführen, oder?). Ihr bekommt dann unter diesem Absatz ein Eingabefeld, in das ihr Kram tippen könnt. Wenn ihr einen hinreichend modernen Browser habt (technisch: er muss das storage-Signal schicken; Firefox 78 tut das z.B., Webkit 4.0.37 nicht), könnt ihr eure Nachricht in der Javascript-Warnung am Fuß der Seite sehen, schockierenderweise auch in anderen Fenstern, die ihr auf blog.tfiu.de offen habt. Auf allen halbwegs aktuellen Großbrowsern erscheint der Text jedenfalls nach dem nächsten Reload. Und bleibt auch da. Aufauf, schreibt eurem künftigen Selbst eine Nachricht in den Fuß dieser Seite:

Gut. Ihr habt HTML aus. Dann ist hier auch keine Textbox.

Nennt mich paranoid, aber lustig ist das nicht.

Und so ärgern mich noch viel mehr als Seiten, die Javascript fordern, Seiten, die ohne Local Storage leer bleiben oder sonst irgendwie undurchschaubar kaputt gehen.

Was tun?

Für die großen Browser gibt es allerlei Erweiterungen, die das Management von Local Storage erlauben; im Firefox sehe ich gerade Forget Me Not oder StoragErazor (was gleich noch die IndexedDB-API mit abdeckt, die ähnlich schrecklich ist, aber noch nicht viel genutzt wird); soweit ich erkennen kann, erlaubt das unverzichtbare noscript nicht, Javascript ohne Local Storage laufen zu lassen.

Für meinen Haupt-Browser, den Luakit (verdammt, schon wieder ein Link von hier in github rein), habe ich eine kleine Erweiterung geschrieben, mit der ich mit der Tastenkombination ,tq schnell local storage ein- und ausschalten kann; auf die Weise kann ich normalerweise ohne Local Storage browsen, wenn dann aber mal wirklich eine Seite kaputt ist (gitlab ist da derzeit so ein ganz schlechtes Beispiel), braucht es nur ein paar Anschläge. Eine Anzeige in der Fußleiste (q/Q für Storage aus/an) kommt auch mit:

-- Web storage control

-- @module webstorage_control
-- @copyright Public Domain

local _M = {}

local window = require("window")
local theme = require("theme")
local settings = require("settings")
local modes = require("modes")
local add_binds = modes.add_binds

function update_webstorage_disp(w)
    if settings.get_setting("webview.enable_html5_database") then
        w.sbar.r.webstorage_d.text = "Q"
    else
        w.sbar.r.webstorage_d.text = "q"
    end
end

function toggle_local_storage(w)
    local local_storage_enabled =
        settings.get_setting("webview.enable_html5_database")

    settings.set_setting("webview.enable_html5_database",
        not local_storage_enabled)
    settings.set_setting("webview.enable_html5_local_storage",
        not local_storage_enabled)
    update_webstorage_disp(w)
end

window.add_signal("init", function (w)
    local r = w.sbar.r
    r.webstorage_d = widget{type="label"}
    r.layout:pack(r.webstorage_d)
    r.layout:reorder(r.webstorage_d, 1)
    r.webstorage_d.font = theme.font
    update_webstorage_disp(w)
end)

add_binds("normal", {
    { "^,tq$", "Enable/disable web local storage", toggle_local_storage},
})

return _M

-- vim: et:sw=4:ts=8:sts=4:tw=80

Wer auch den luakit verwendet, kann das nach .config/luakit/webstorage_control.lua packen und dann:

require("webstorage_control")

an geeigneter Stelle (z.B. in .config/luakit/rc.lua) unterbringen. Wenn dermaleinst so viele Seiten ohne Local Storage kaputtgehen wie derzeit ohne Javascript, müsste das wahrscheinlich eher wie in der noscript-Erweiterung automatisiert werden.

Auch wenn ich mal Local Storage erlaube, will ich natürlich nicht, dass der Kram persistent bleibt. Deshalb habe ich noch folgendes kleine Python-Programm geschrieben:

#!/usr/bin/python
"""
Clear luakit web local storage and cookies

There's a whitelist applied to both.
"""


import fnmatch
import glob
import os
import re
import sqlite3


class Whitelist:
    """A fnmatch-based whitelist.

    Test as in "domain in whitelist".  It's not fast, though.
    """
    def __init__(self,
            src_path=os.path.expanduser("~/.config/luakit/cookie.whitelist")):
        with open(src_path) as f:
            self.patterns = [s.strip() for s in f.read().split("\n")]

    def __contains__(self, domain):
        for pattern in self.patterns:
            if fnmatch.fnmatch(domain, pattern):
                return True
        return False


def clear_cookies(whitelist):
    """removes cookies from domains not in whitelist from luakit's
    cookies db.
    """
    conn = sqlite3.connect(
        os.path.expanduser("~/.local/share/luakit/cookies.db"))

    try:
        all_hosts = list(r[0]
            for r in conn.execute("select distinct host from moz_cookies"))
        for host in all_hosts:
            if host in whitelist:
                continue
            conn.execute("delete from moz_cookies where host=?",
                (host,))
    finally:
        conn.commit()
        conn.execute("VACUUM")
        conn.close()


def try_unlink(f_name):
    """removes f_name if it exists.
    """
    if os.path.exists(f_name):
        os.unlink(f_name)


def clear_local_storage(whitelist):
    """removes luakit's local storage files unless their source
    domains are whitelisted for cookies.
    """
    for f_name in glob.glob(os.path.expanduser(
            "~/.local/share/luakit/local_storage/*.localstorage")):
        mat = re.match("https?_(.*?)_\d+.localstorage",
            os.path.basename(f_name))
        if not mat:
            print("{}???".format(f_name))
            continue

        if mat.group(1) in whitelist:
            continue

        try_unlink(f_name)
        try_unlink(f_name+"-shm")
        try_unlink(f_name+"-wal")


def main():
    whitelist = Whitelist()
    clear_cookies(whitelist)
    clear_local_storage(whitelist)


if __name__=="__main__":
    main()

Das Programm liest eine Liste von Shell-Patterns (also etwas wie *.xkcd.com) aus einer Datei ~/.config/luakit/cookie.whitelist und löscht dann alle Cookies und Local Storage-Einträge im Luakit, die nicht von Servern kommen, die in dieser Ausnahmeliste erwähnt sind. Das Ganze läuft bei mir jeden Morgen aus meiner Crontab:

01 09 * * * ~/mybin/clear_luakit_cookies.py

Aber: Besser wärs, das wäre alles nicht da. In einem Browser, also etwas, mit dem mensch Webseiten anschauen will, hat eine API wie Local Storage mit Persistenz und Signalisierung eigentlich nichts verloren.

Oh: Der Javascript-Quellcode für den ganzen Spaß mit euren Notizen in der Fußzeile ist erschreckend klein. Für den Fall, dass ich das mal anders schreibe und das so nicht mehr im Seiten-Quellcode zu finden sein wird, hier zunächst der Code für das Eingabefeld oben:

<div id="textarea-container">
  <p><em>Gut.  Ihr habt HTML aus.  Dann ist hier auch keine Textbox.
  </em></p>
</div>

<script deferred="deferred">
  function setUp() {
    document.querySelector("#textarea-container").innerHTML =
      `<textarea id="to-store" style="width:100%; height:6cm"
      placeholder="Tippt kram, den ihr im Fuß der Seite sehen wollt."
      ></textarea>`;
    let textSource = document.querySelector("#to-store");
    if (!window.localStorage) {
      textSource.value = "Ah, gut.  Du hast local storage aus.  Dann\n"
        +"geht das hier nicht.  Wie gesagt, das ist gut.";
       return;
    }

    if (window.localStorage.savedText) {
      textSource.value = window.localStorage.getItem("savedText");
    }

    textSource.addEventListener("input", () => {
      window.localStorage.setItem("savedText", textSource.value);
    });

    window.addEventListener("storage", (ev) => {
      textSource.value = ev.newValue;
    });
  }

  setUp();
</script>

Und dann noch der fürs Management der Fußzeile (der sitzt jetzt gerade im head-Element):

if (window.localStorage) {
  target.innerHTML +=
    `<div id="ls-warning"><p><strong>Schlimmer noch:</strong>
    Dein Browser lässt mich
    auch local storage machen.  Wenn du dir in
    <a href="/javascript-local-storage.html">diesem Artikel</a>
    eine Nachricht hinterlässt, bekommst du sie bei deinem nächsten
    Besuch unten zu sehen; du kannst da sogar dein eigenes
    HTML und Javascript unterbringen und ausführen lassen.</p>
    <p id="user-message"/></div>`;

  if (window.localStorage.savedText) {
    document.querySelector("#user-message").innerHTML =
      window.localStorage.savedText;
  }

  window.addEventListener("storage", (ev) => {
    document.querySelector("#user-message").innerHTML = ev.newValue;
  });
}
Kategorie: edv