0%

Caching en voorwaardelijke verzoeken · oefening

Een vingerafdruk voor een body

Een client die de boekenlijst van gisteren heeft bewaard, wil één ding weten: is de die de server nu zou sturen dezelfde als de lijst die hij heeft? De directe manier om dat uit te zoeken, is de twee bodies vergelijken, en daarvoor is de body van de server nodig, en dat is precies de download die de client probeerde te vermijden. Wat de twee kanten nodig hebben, is iets veel kleiners dat voor de bytes staat: een die hetzelfde is zodra de bytes hetzelfde zijn, en anders zodra er één byte verschilt. Dan kan de client die waarde sturen, en kan de server die vergelijken met de waarde van wat hij nu zou sturen.

Die waarde is een vingerafdruk van de body, en Python berekent er een met een uit de standaardbibliotheek.

De module hashlib

hashlib maakt van willekeurige bytes een digest: een korte waarde van vaste grootte, berekend uit elke byte. hashlib.sha256(body) leest de bytes in body en geeft een hash- terug, en het object geeft je de digest in twee vormen:

  • .hexdigest() geeft een str van 64 hexadecimale tekens terug, de cijfers 0 tot en met 9 en de letters a tot en met f. Zulke tekst kan zoals hij is in een header.

  • .digest() geeft dezelfde waarde terug als 32 ruwe bytes. Elke byte wordt in de andere vorm als twee hex-tekens geschreven, en daarom worden 32 bytes 64 tekens.

SHA-256 is de naam van het algoritme. Wat de grootte van de invoer ook is, een lege body of een hele bibliotheek, de digest heeft dezelfde grootte. Twee eigenschappen maken hem hier nuttig. Dezelfde bytes geven altijd dezelfde digest, op elke machine en in elke run van Python. En een wijziging van één byte geeft een digest die zichtbaar niets gemeen heeft met de oude. De digest identificeert alleen de bytes: niets in zijn 64 tekens laat iemand de body terughalen. Hier zijn twee bodies die één spatie verschillen:

import hashlib

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

Een -parser leest beide bodies als dezelfde data. De digests verschillen toch, omdat een digest bytes ziet, geen betekenis. Dat is het juiste gedrag voor een vingerafdruk van een antwoord: de client heeft bytes, en de vraag is of de server dezelfde bytes zou sturen.

hashlib.sha256 neemt bytes, nooit een str. De bodies van je server zijn al bytes, gebouwd door json_response met .encode("utf-8"), dus ze kunnen er direct in.

Wat drukt dit af?

import hashlib

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

Een tag staat tussen aanhalingstekens

In heet deze vingerafdruk een , verstuurd in een header met de naam ETag. De waarde heeft een vaste vorm (RFC 9110, sectie 8.8.3): de tekens van de tag tussen dubbele aanhalingstekens, en de aanhalingstekens horen bij de waarde. Een server waarvan de digest e72c...f6af is, stuurt:

etag: "e72c4626ea5e221c238f01d50feff3d7007992fb3447610eaee3ee661310f6af"

Een tag die precies zo is geschreven, is een sterke tag: hij belooft dat twee antwoorden met dezelfde tag dezelfde bytes hebben. Een tag kan ook met W/ beginnen, zoals in W/"e72c...". Dat markeert een zwakke tag, die minder belooft: de twee antwoorden betekenen hetzelfde, hoewel hun bytes kunnen verschillen, bijvoorbeeld een pagina waarvan de enige wijziging een tijdstempel in een commentaar is. De tag van je server komt uit de exacte bytes, dus hij is sterk, en hij stuurt nooit W/. In les 4 leert je server W/ te lezen als een client het terugstuurt.

Welke is een sterke ETag, geschreven als headerwaarde?

Niet Pythons hash()

Python heeft ook een ingebouwde hash(), en die is hier verleidelijk. Die geeft een geheel getal terug voor een waarde, en gelijke waarden in één programma krijgen gelijke getallen. Maar voor str en bytes verandert het getal van de ene run van Python naar de volgende. Voer python -c 'print(hash(b"abc"))' twee keer uit in je en de twee getallen verschillen, tenzij Python is gestart met de omgevingsvariabele PYTHONHASHSEED op een vaste waarde, wat het getal vastzet. Dat is bewust: het maakt het moeilijker voor wie dan ook om invoer te bouwen die de van Python vertraagt.

Een tag moet langer meegaan dan één run. Een client kan een tag uren bewaren terwijl de server herstart, en een oude tag moet nog steeds dezelfde bytes aanduiden als hij terugkomt. Een SHA-256-digest doet dat, omdat die van niets anders afhangt dan de bytes.

Twee GET /books-antwoorden met een uur ertussen hebben byte voor byte identieke bodies. Hoe zit het met hun tags?

Jouw opdracht

Schrijf in reading_server.py make_etag(body). Die neemt de bytes van de body en geeft de sterke tag terug als str: een dubbel aanhalingsteken, de SHA-256-hexdigest van de body, en een dubbel aanhalingsteken. reading_server.py importeert hashlib al. Eén regel is genoeg.

Nog niets roept make_etag aan; les 3 zet de tag op elk antwoord. Je kunt hem nu al uitproberen. Start Python in de terminal, in de projectmap:

python

Importeer daarna je module en roep de aan op twee bodies die één byte verschillen:

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

De buitenste enkele aanhalingstekens zijn hoe Python een str toont; de dubbele aanhalingstekens erbinnen horen bij de tag. Eén toegevoegde byte geeft een totaal andere tag, en elke tag is 66 tekens met zijn aanhalingstekens. Typ exit() om Python te verlaten, en klik daarna op Check my work.

Dezelfde gereedschappen komen terug in een latere cursus in deze reeks, WebSockets with Python, die gepland is. De openingshandshake daarvan hasht een sleutel met hashlib en stuurt de digest()-bytes in , de codering die je in hoofdstuk 7 leerde. Hier stuurt de volgende les je tag mee met elk antwoord dat je server op een leesactie geeft.

Opdracht

Schrijf make_etag(body) in reading_server.py.

  • body is de body als bytes.

  • Geef een str terug: een dubbel aanhalingsteken, hashlib.sha256(body).hexdigest(), en een dubbel aanhalingsteken. De aanhalingstekens horen bij de van de tag.

  • De tag is sterk: geen W/ ervoor.

Probeer het uit in de : start python, doe daarna import reading_server en roep reading_server.make_etag(b"Kindred") aan.

Klik daarna op Check my work. Die roept make_etag aan met bodies die je nog niet hebt gezien: een lege body, een -boek met niet-ASCII-tekens, en 5000 bytes die geen tekst zijn. Hij controleert dat elk resultaat een str is die met een dubbel aanhalingsteken begint en eindigt en de SHA-256-hexdigest van precies die bytes bevat (hex-cijfers in hoofdletters worden geaccepteerd), dat gelijke bytes gelijke tags geven, en dat het veranderen van één byte of het toevoegen van één spatie de tag verandert. Niets uit je terminal wordt beoordeeld.