0%

Toestand en identiteit · oefening

Houd een sessie bij op de server

Met een kan een client een herinnering van het ene verzoek naar het volgende meenemen. Deze les geeft je server iets wat het waard is om te onthouden: een login. Kijk eerst wat de server vandaag doet met de verzoeken die een login nodig heeft. Voer het scenario session van het hulpprogramma uit:

python pl_http.py session

Het stuurt vier verzoeken: GET /me, dan POST /login, dan opnieuw GET /me met de cookie gekopieerd uit de regel set-cookie van het login-antwoord, als die er is, en als laatste GET /me met cookie: session=not-a-real-id. Vandaag is elk antwoord HTTP/1.0 404 Not Found met {"error": "nothing is at this path"}, omdat geen van beide paden al bestaat.

Kijk naar de POST /login die het hulpprogramma stuurt:

client -> server  b'POST /login HTTP/1.1\r\nHost: books.test\r\nContent-Length: 0\r\n\r\n'  (61 bytes)

Hij heeft geen body, en zegt toch Content-Length: 0. Dat is de regel uit hoofdstuk 5, les 1: je read_body beantwoordt elke POST zonder lengte met 411 voordat handle zelfs maar naar het pad kijkt, dus een POST die niets te sturen heeft, zegt dat.

Bewaar de feiten, deel een sleutel uit

Het verleidelijke ontwerp zet de gebruiker in de cookie: set-cookie: user=guest. Dat werkt tot een client in plaats daarvan cookie: user=ana stuurt. Een cookie is tekst die de client bewaart en terugstuurt, en de client kan die naar believen veranderen, dus niets erin is als feit te vertrouwen.

Daarom blijven de feiten op de server. Als iemand inlogt, verzint de server een nieuwe willekeurige id, slaat hij onder die id in state["sessions"] op wie er inlogde, en geeft hij de client alleen de id in een cookie. Als er een verzoek met die cookie terugkomt, zoekt de server de id op:

client:  cookie: session=K7f...Qw
                         |
                         v
server:  state["sessions"]
           "K7f...Qw": {"user": "ana"}
           "p2Z...8c": {"user": "ben"}

De id wijst naar één rij, en de rij zegt wie de gebruiker is. De cookie veranderen levert een client niets op, tenzij hij de id van een andere rij raadt. Dat record op de server is een sessie, en de id is de sessie-id. sample_state() geeft daar nu een lege "sessions" voor terug, naast de gebruikers en tokens die latere lessen gebruiken.

Waar houdt deze server bij wie er is ingelogd?

Een id die niemand kan raden

Zolang een sessie leeft, werkt de id als een wachtwoord: iedereen die cookie: session=<that id> stuurt, is voor de server die gebruiker. De id moet dus onmogelijk te raden zijn. De secrets van Python maakt precies hiervoor:

Try it

secrets.token_urlsafe(16) neemt 16 willekeurige bytes van het en schrijft ze als 22 tekens van letters, cijfers, - en _. Zestien bytes is 128 willekeurige bits, veel te veel waarden om een voor een te proberen. De module random die je misschien kent van spellen en simulaties, is hier het verkeerde gereedschap: die is gebouwd om snel en herhaalbaar te zijn, niet onvoorspelbaar. Gebruik random voor simulaties en secrets voor alles wat een aanvaller niet mag raden.

Waarom nummer je sessies niet 1, 2, 3?

start_session(state, user) doet dat werk: één aanroep van secrets.token_urlsafe(16), geschreven via de module als secrets.token_urlsafe, daarna {"user": user} opgeslagen onder de nieuwe id in state["sessions"], en de id teruggegeven.

De cookie schrijven en lezen

session_cookie(session_id, *, max_age=SESSION_AGE) geeft de waarde van de header set-cookie terug: session=<id>; HttpOnly; Max-Age=<max_age>; Path=/. Je kunt hem bouwen met http.cookies.SimpleCookie, zoals in les 1, door httponly, path en max-age te zetten en OutputString() terug te geven, of met een . De volgorde van de attributen doet er niet toe. Een id uit token_urlsafe hoeft niet tussen aanhalingstekens, dus beide manieren schrijven dezelfde tekst. SESSION_AGE is 3600, een uur, en met het keyword- kan les 7 0 meegeven.

session_user(request, state) leest hem terug. request["headers"].get("cookie", []) is een van de cookie-waarden van het verzoek, meestal één. Voor elke waarde geeft http.cookies.SimpleCookie(value).get("session") de cookie session, of None als de regel er geen heeft. Zoek de .value ervan op in state["sessions"] met .get, en geef de opgeslagen gebruiker terug, of None als geen enkele waarde een levende sessie aanduidt.

Wat drukt dit af?

import http.cookies

c = http.cookies.SimpleCookie("theme=dark; session=abc")
print(c["session"].value, len(c))

SimpleCookie heeft één scherpe kant. Als één stuk van een regel Cookie misvormd is, geeft hij helemaal geen cookies terug, zonder melding: session=abc; bad cookie here; x=1 geeft een leeg resultaat. Een kapotte cookie uit een ander deel van een site zou dan de sessie verbergen. Een zelfgeschreven parser zou vergevingsgezinder kunnen zijn; deze les vraagt daar niet om.

Wat /me antwoordt

GET /me antwoordt altijd met 200: {"user": "guest"} bij een levende sessie, {"user": null} bij geen cookie, een onbekende id of een beëindigde sessie. json.dumps schrijft Pythons None als null. Sommige API’s antwoorden hier in plaats daarvan met 401. Een 401 moet echter een manier van inloggen noemen die de client op die kan gebruiken, in een header WWW-Authenticate, en een cookiesessie heeft geen zo’n schema om te noemen. Daarom zegt deze server “niemand” met een 200. Les 5 brengt de eerste 401, op /login, waar wel een schema bestaat.

Jouw opdracht

In reading_server.py:

  1. Schrijf start_session(state, user).

  2. Schrijf session_cookie(session_id, *, max_age=SESSION_AGE).

  3. Schrijf session_user(request, state).

  4. Voeg in handle een tak toe voor het pad /login: een POST start een sessie voor "guest" en antwoordt met 200 met {"user": "guest"} en een header set-cookie van session_cookie; elke andere krijgt not_allowed("POST"). Voeg een tak toe voor /me: GET en HEAD antwoorden met 200 met {"user": ...} uit session_user; elke andere methode krijgt not_allowed("GET, HEAD").

Tot les 5 is elke login guest, wat er ook wordt meegestuurd: er zijn nog geen inloggegevens om te controleren.

Voer het scenario opnieuw uit:

python pl_http.py session

De eerste GET /me antwoordt nu {"user": null}. De login antwoordt met 200, en onder het antwoord drukt het hulpprogramma de regel set-cookie af: session=, dan 22 tekens die bij elke run veranderen, dan HttpOnly; Max-Age=3600; Path=/. Het derde verzoek draagt die cookie en krijgt {"user": "guest"}; de verzonnen id krijgt {"user": null}.

De sessies leven in de state-dict, dus ze duren zolang één run van het hulpprogramma. Max-Age vraagt de client de cookie na een uur te vergeten, maar niets op deze server beëindigt een sessie na verloop van tijd: hij leest geen klok. Les 7 beëindigt sessies op verzoek.

Klik daarna op Check my work. De server deelt nu een cookie uit en herkent die, maar het hulpprogramma moest de cookie met de hand naar het volgende verzoek kopiëren. In de volgende les doet een client dat zelf.

Opdracht

Houd in reading_server.py een loginsessie bij op de server.

  • start_session(state, user) maakt een nieuwe id met secrets.token_urlsafe(16), geschreven via de (import secrets staat al bovenaan; secrets.token_hex(16) wordt ook geaccepteerd, net als elke grootte van 16 bytes of meer), slaat {"user": user} eronder op in state["sessions"], en geeft de id terug.

  • session_cookie(session_id, *, max_age=SESSION_AGE) geeft de set-cookie- session=<id>; HttpOnly; Max-Age=<max_age>; Path=/ terug, met de attributen in willekeurige volgorde en zonder Secure, zoals les 1 uitlegde. Bouw hem met http.cookies.SimpleCookie of een .

  • session_user(request, state) leest de session uit de header cookie van het verzoek met http.cookies.SimpleCookie en geeft de gebruiker van die sessie terug, of None als er geen cookie is, geen cookie session, of geen sessie onder de waarde ervan.

  • In handle: POST /login start een sessie voor "guest" en antwoordt met 200 met {"user": "guest"} en een header set-cookie van session_cookie; elke andere op /login krijgt 405 met allow: POST. GET /me en HEAD /me antwoorden met 200 met {"user": name}, of {"user": null} zonder levende sessie; elke andere methode krijgt 405 met allow: GET, HEAD. Elke andere route houdt haar antwoord.

  • De sessie-id verschijnt nooit in de body van een antwoord.

Voer python pl_http.py session uit in de en lees de regel set-cookie en de drie antwoorden van /me.

Klik daarna op Check my work. Met reading_server.secrets vervangen door een vervanger die de grootte vastlegt waar je om vraagt en id’s teruggeeft die je nog niet hebt gezien, controleert hij dat start_session de sessie onder precies die id opslaat en dat de cookie en handle die gebruiken. Hij parset je cookie met http.cookies.SimpleCookie en controleert de waarde, HttpOnly, Path=/ en Max-Age, ook met max_age=0. Hij roept handle aan met een state die je nog niet hebt gezien: een login, /me met alleen de cookie, naast een andere cookie in beide volgordes, met een onbekende id, zonder cookie, en met een cookie met de naam sessionx, HEAD /me, en de 405’s. Als laatste start hij met de echte secrets 40 sessies op één state en op verse states en controleert hij dat elke id anders is en minstens 22 tekens lang. Niets uit je terminal wordt beoordeeld.