0%

Caching und bedingte Anfragen · Übung

Ein Fingerabdruck für einen Body

Ein Client, der die Bücherliste von gestern aufbewahrt hat, will eine Sache wissen: Ist die , die der Server jetzt senden würde, dieselbe wie die, die er hat? Der direkte Weg, das herauszufinden, ist, die beiden Bodys zu vergleichen, und dafür braucht es den Body des Servers, also genau den Download, den der Client vermeiden wollte. Was beide Seiten brauchen, ist etwas viel Kleineres, das für die Bytes steht: ein , der immer gleich ist, wenn die Bytes gleich sind, und verschieden, sobald sich irgendein Byte unterscheidet. Dann kann der Client diesen Wert senden, und der Server kann ihn mit dem Wert dessen vergleichen, was er jetzt senden würde.

Dieser Wert ist ein Fingerabdruck des Bodys, und Python berechnet einen mit einem aus der Standardbibliothek.

Das Modul hashlib

hashlib macht aus beliebigen Bytes einen Digest: einen kurzen Wert fester Größe, berechnet aus jedem Byte. hashlib.sha256(body) liest die Bytes in body und gibt ein Hash- zurück, und das Objekt gibt dir seinen Digest in zwei Formen:

  • .hexdigest() gibt einen str aus 64 hexadezimalen Zeichen zurück, den Ziffern 0 bis 9 und den Buchstaben a bis f. Solcher Text kann so, wie er ist, in einen Header.

  • .digest() gibt denselben Wert als 32 rohe Bytes zurück. Jedes Byte wird in der anderen Form als zwei Hex-Zeichen geschrieben, und deshalb werden aus 32 Bytes 64 Zeichen.

SHA-256 ist der Name des Algorithmus. Egal wie groß die Eingabe ist, ein leerer Body oder eine ganze Bibliothek, der Digest hat dieselbe Größe. Zwei Eigenschaften machen ihn hier nützlich. Dieselben Bytes ergeben immer denselben Digest, auf jedem Rechner und in jedem Python-Lauf. Und eine Änderung eines einzigen Bytes ergibt einen Digest, der mit dem alten sichtbar nichts gemeinsam hat. Der Digest identifiziert die Bytes nur: Nichts in seinen 64 Zeichen lässt jemanden den Body wiederherstellen. Hier sind zwei Bodys, die sich um ein Leerzeichen unterscheiden:

import hashlib

for body in [b'{"books": []}', b'{"books": [ ]}']:
    print(hashlib.sha256(body).hexdigest())
e72c4626ea5e221c238f01d50feff3d7007992fb3447610eaee3ee661310f6af
2d0190243f5b7e60660f91e4aace542f2e2a0de3c2dc923a01d05263c7333624

Ein -Parser liest beide Bodys als dieselben Daten. Die Digests unterscheiden sich trotzdem, weil ein Digest Bytes sieht, keine Bedeutung. Das ist das richtige Verhalten für einen Fingerabdruck einer Antwort: Der Client hat Bytes, und die Frage ist, ob der Server dieselben Bytes senden würde.

hashlib.sha256 nimmt Bytes, nie einen str. Die Bodys deines Servers sind bereits Bytes, gebaut von json_response mit .encode("utf-8"), also können sie direkt hinein.

Was gibt das aus?

import hashlib

print(len(hashlib.sha256(b"x").hexdigest()), len(hashlib.sha256(b"x").digest()))

Ein Tag steht in Anführungszeichen

In heißt dieser Fingerabdruck und wird in einem Header namens ETag gesendet. Sein Wert hat eine feste Form (RFC 9110, Abschnitt 8.8.3): die Zeichen des Tags zwischen doppelten Anführungszeichen, und die Anführungszeichen gehören zum Wert. Ein Server, dessen Digest e72c...f6af ist, sendet:

etag: "e72c4626ea5e221c238f01d50feff3d7007992fb3447610eaee3ee661310f6af"

Ein genau so geschriebenes Tag ist ein starkes Tag: Es verspricht, dass zwei Antworten mit demselben Tag dieselben Bytes haben. Ein Tag kann auch mit W/ beginnen, wie in W/"e72c...". Das kennzeichnet ein schwaches Tag, das weniger verspricht: Die beiden Antworten bedeuten dasselbe, auch wenn sich ihre Bytes unterscheiden können, zum Beispiel bei einer Seite, deren einzige Änderung ein Zeitstempel in einem Kommentar ist. Das Tag deines Servers kommt aus den genauen Bytes, also ist es stark, und er sendet nie W/. In Lektion 4 lernt dein Server, W/ zu lesen, wenn ein Client es zurücksendet.

Welches ist ein starkes ETag, als Headerwert geschrieben?

Nicht Pythons hash()

Python hat auch ein eingebautes hash(), und das ist hier verlockend. Es gibt für einen Wert eine ganze Zahl zurück, und gleiche Werte in einem Programm bekommen gleiche Zahlen. Aber bei str und bytes ändert sich die Zahl von einem Python-Lauf zum nächsten. Führ python -c 'print(hash(b"abc"))' zweimal in deinem aus, und die beiden Zahlen unterscheiden sich, außer Python wurde mit der Umgebungsvariable PYTHONHASHSEED auf einen festen Wert gestartet, was die Zahl festlegt. Das ist Absicht: Es macht es schwerer, Eingaben zu bauen, die Pythons ausbremsen.

Ein Tag muss einen Lauf überdauern. Ein Client kann ein Tag stundenlang behalten, während der Server neu startet, und ein altes Tag muss, wenn es zurückkommt, immer noch dieselben Bytes benennen. Ein SHA-256-Digest tut das, weil er von nichts als den Bytes abhängt.

Zwei Antworten auf GET /books im Abstand von einer Stunde haben Byte für Byte identische Bodys. Was ist mit ihren Tags?

Deine Aufgabe

Schreib in reading_server.py make_etag(body). Es nimmt die Bytes des Bodys und gibt das starke Tag als str zurück: ein doppeltes Anführungszeichen, den SHA-256-Hex-Digest des Bodys und ein doppeltes Anführungszeichen. reading_server.py importiert hashlib bereits. Eine Zeile reicht.

Noch ruft nichts make_etag auf; Lektion 3 setzt sein Tag auf jede Antwort. Du kannst es jetzt ausprobieren. Starte Python im Terminal, im Projektordner:

python

Importiere dann dein Modul und ruf die mit zwei Bodys auf, die sich um ein Byte unterscheiden:

>>> import reading_server
>>> reading_server.make_etag(b"Kindred")
'"213c649df845274f0590aae4a97f84eb72a8935662c6fb10c7a63bda2e69549f"'
>>> reading_server.make_etag(b"Kindred!")
'"fc29d1d0143aae9c48c2ab38f15c87e99a4345712343322b8d0d8924a63ce456"'

Die äußeren einfachen Anführungszeichen sind die Art, wie Python einen str anzeigt; die doppelten Anführungszeichen darin sind Teil des Tags. Ein hinzugefügtes Byte ergibt ein völlig anderes Tag, und jedes Tag ist mit seinen Anführungszeichen 66 Zeichen lang. Gib exit() ein, um Python zu verlassen, und drück dann Check my work.

Dieselben Werkzeuge kommen in einem späteren Kurs dieser Reihe wieder, WebSockets with Python, der geplant ist. Sein eröffnender Handshake hasht einen Schlüssel mit hashlib und sendet die digest()-Bytes in , der Kodierung, die du in Kapitel 7 gelernt hast. Hier sendet die nächste Lektion dein Tag auf jeder Antwort, die dein Server auf einen Lesezugriff gibt.

Aufgabe

Schreib make_etag(body) in reading_server.py.

  • body ist der Body als Bytes.

  • Gib einen str zurück: ein doppeltes Anführungszeichen, hashlib.sha256(body).hexdigest() und ein doppeltes Anführungszeichen. Die Anführungszeichen sind Teil des Tag-.

  • Das Tag ist stark: kein W/ davor.

Probier es im aus: Starte python, dann import reading_server, und ruf reading_server.make_etag(b"Kindred") auf.

Drück dann Check my work. Es ruft make_etag mit Bodys auf, die du noch nicht gesehen hast: einem leeren Body, einem -Buch mit Zeichen außerhalb von ASCII und 5000 Bytes, die kein Text sind. Es prüft, dass jedes Ergebnis ein str ist, der mit einem doppelten Anführungszeichen beginnt und endet und den SHA-256-Hex-Digest genau dieser Bytes enthält (Hex-Ziffern in Großbuchstaben werden akzeptiert), dass gleiche Bytes gleiche Tags ergeben und dass die Änderung eines Bytes oder ein zusätzliches Leerzeichen das Tag ändert. Nichts aus deinem Terminal wird bewertet.