Wie mag er verbinding maken? · oefening
Controleer een token vóór 101
Een scorebijhouderspagina kan haar token alleen in de querystring meesturen, een Python-client kan een Bearer-header sturen, en vandaag leest de server geen van beide. Deze les schrijft de beslissing: mag deze client scoren, gegeven het pad en de headers van een
Vóór 101, of nooit
De handshake is een 101 Switching Protocols is het laatste HTTP-bericht ervan. Daarna spreekt de verbinding
Je hebt eerder upgrades geweigerd. In hoofdstuk 2 beantwoordde check_upgrade een misvormde upgrade met 400 en een verkeerde versie met 426, in plaats van 101. Een ontbrekend token is hetzelfde soort antwoord: een status, bepaald op basis van de verzoekregel en de headers, verstuurd vóór enig frame.
Waarom moet het token gecontroleerd worden voordat de server 101 antwoordt?
Waar het token staat
De Authorization-partition(" "), het schema bearer in hoofd- of kleine letters, en de rest gestript.
Wat drukt dit af?
print("bearer tok".partition(" "))
Het token komt terug met een spatie ervoor, en daarom wordt de rest gestript voordat hij wordt vergeleken.
Een header kan meer dan één keer in een verzoek voorkomen, en het Headers-get werpt MultipleValuesError op als een naam herhaald wordt, dus lees de header met headers.get_all("authorization"), dat een
Zonder enige Authorization-header kijk je in de query. parse_qs, uit dezelfde urllib.parse als urlsplit, maakt van een querystring een +:
from urllib.parse import parse_qs, urlsplit
print(urlsplit("/score?token=abc%2Fdef&x=1").path)
print(parse_qs("token=abc%2Fdef&token=2"))
print(parse_qs("token="))
print(parse_qs("token=a+b"))
De eerste regel drukt /score af: het pad zonder query. De andere drukken {'token': ['abc/def', '2']} af, met %2F gedecodeerd tot / en beide waarden bewaard in de lijst; {}, omdat een lege waarde wordt weggelaten; en {'token': ['a b']}, met + gedecodeerd tot een spatie.
De query moet dus precies één token-waarde bevatten: een lege token= laat er geen over, en twee waarden worden geweigerd net als twee headers. find_token(path, headers) geeft het gevonden token terug of None, volgens deze regels:
| Wat het verzoek draagt | Wat beslist |
|---|---|
precies één Authorization-header van de vorm Bearer <token> | het token van die header |
elke andere Authorization-header: een ander schema, geen token, of twee headers | niemand: None, en de query wordt niet gelezen |
geen Authorization-header | de ene token-waarde in de query, anders None |
De middelste rij doet ertoe. Een slechte header valt niet terug op de query: een client die een inloggegeven stuurde, wordt beoordeeld op dat inloggegeven.
Vergelijk zonder tijd te lekken
token == expected geeft het juiste antwoord, maar == op twee hmac.compare_digest(a, b) doet er even lang over, welk deel van de waarden ook overeenkomt. (Timing kan nog steeds de lengte van de waarden verraden, maar niet welke tekens klopten.) De cursus gebruikt dat hulpmiddel in plaats van te redeneren over wanneer de aanval zou werken.
Eén detail: compare_digest weigert str-waarden met tekens buiten ASCII.
import hmac
hmac.compare_digest("café", "cafe")
Een token uit de query kan zo’n string zijn nadat parse_qs het heeft gedecodeerd (caf%C3%A9 wordt café), en een headerwaarde ook. Vergelijk daarom bytes: hmac.compare_digest(token.encode("utf-8"), expected.encode("utf-8")) werkt voor elke tekst. Roep het aan via de module, als hmac.compare_digest, met de regel import hmac die al bovenaan scoreboard_server.py staat.
Waarom hmac.compare_digest in plaats van == voor het token?
401, en waarom niet 403
HTTP with Python trok de grens: 401 als de server niet weet wie je bent, 403 als hij het weet en toch nee zegt. Het scorebord heeft één token en één recht: scoren. Een client zonder token, of met een verkeerd token, is iemand die de server niet kan identificeren, dus elke weigering op /score is 401. Een 403 kom je in les 4 tegen, van een andere controle. /watch heeft geen token nodig, dus check_request(path, headers, expected) vergelijkt eerst urlsplit(path).path met "/score" en geeft None terug voor al het andere; zo kan een querystring het pad niet verbergen.
Een scorebijhouder stuurt het verkeerde token. Waarom hier 401 en niet 403?
Het tokenbestand
Het verwachte token staat in token.txt in je werkruimte. Het bevat een verzonnen oefentoken, geen geheim: een echt token staat nooit in een broncodeboom, waar elke kopie van de code het meedraagt. Echte services laden het uit de omgeving of uit een geheimenopslag. read_token(path) opent het bestand met encoding="utf-8" in een with-blok en geeft de tekst terug, ontdaan van de newline en eventuele spaties eromheen.
Je opdracht
Schrijf in scoreboard_server.py read_token, find_token en check_request zoals hun docstrings en deze les beschrijven. Nog niets roept check_request aan tijdens een handshake.
Klik op Run. De sectie van les 2 bouwt vijf verzoeken met de Headers van de bibliotheek en drukt voor elk ervan af: het pad zonder query, of er een Authorization-header werd gestuurd, of de query een token bevatte, en wat check_request antwoordde. Ze drukt nooit een token af. Een afgemaakte versie, op de site uitgevoerd, drukte af:
Lesson 2: who may score?
/watch, no Authorization header, a token in the query: False -> None
/score, no Authorization header, a token in the query: False -> 401
/score, an Authorization header, a token in the query: False -> None
/score, no Authorization header, a token in the query: True -> None
/score, an Authorization header, a token in the query: False -> 401
De kijker komt er zonder iets door. De scorebijhouder zonder iets krijgt 401. Het derde en vierde verzoek dragen het verwachte token van de demo, een in een header en een in de query. Het laatste stuurt een gok in een header. Klik daarna op Submit.
De volgende les geeft check_request aan de bibliotheek, zodat er een 401 uitgaat in plaats van een 101.
Opdracht
In scoreboard_server.py:
Schrijf
read_token(path): open het bestand oppathmetencoding="utf-8"in eenwith-blok en geef de tekst terug met.strip().Schrijf
find_token(path, headers). De docstring geeft de regel: “If any Authorization header is present, decide from the headers alone: the token of exactly one Bearer header, else None (a bad or repeated header never falls back to the query). Only with no Authorization header, return the one token query value, else None.” Kort gezegd: is er een Authorization-header, dan beslissen alleen de headers (het token van precies één Bearer-header, anders None, zonder terug te vallen op de query); alleen zonder Authorization-header geef je de ene querywaarde token terug, anders None. Lees de header metheaders.get_all("authorization"), splits deervan met partition(" "), vergelijk het schema na.lower()met"bearer", en strip het token; een leeg token telt als geen token. Lees de query metparse_qs(urlsplit(path).query).get("token"), wat eenvan precies één waarde moet zijn. Schrijf
check_request(path, headers, expected): geefNoneterug tenzijurlsplit(path).pathgelijk is aan"/score"; geef401terug alsfind_tokengeen token vindt of alshmac.compare_digest(token.encode("utf-8"), expected.encode("utf-8"))onwaar is; geef andersNoneterug. Houd de regelimport hmacen roepcompare_digestaan via de.
Klik op Run: de sectie van les 2 drukt None af voor /watch, 401 voor /score zonder iets, None voor het verwachte token in een header en in de query, en 401 voor een gok.
Klik daarna op Submit. De controle gebruikt een verwacht token dat je nog niet hebt gezien. Ze stuurt verzoeken die het dragen in een Bearer-header (met het schema en de headernaam in andere hoofd- en kleine letters, en extra spaties), in de query, alleen of naast andere waarden, en verzoeken die 401 moeten krijgen: geen token, een verkeerd token, het token met één teken extra, alleen het einde van het token, een ander schema, Bearer met niets erachter, twee Authorization-headers, een slechte header naast het juiste querytoken, twee token-waarden, een lege, en tokens met tekst buiten ASCII, die geen /watch wordt doorgelaten, wat de query ook bevat. Om te zien dat compare_digest de beslissing neemt, vervangt de controle scoreboard_server.hmac door een stand-in waarvan compare_digest een vast antwoord geeft, dus een regel from hmac import wordt geweigerd; elke aanroep moet twee waarden van één type meegeven, en de gevallen buiten ASCII hebben dat type nodig als bytes. read_token leest token.txt en een tweede bestand dat de controle ernaast schrijft. Er mag niets worden afgedrukt.