CORS · oefening
Beantwoord de preflight
De uitwisseling aan het eind van les 3 heeft drie beslissingen aan de kant van de browser en één aan de kant van de server. De browser beslist of hij vraagt, stelt de vraag op en leest het antwoord. De server beantwoordt de vraag. In deze les schrijf je ze alle vier: drie mini_browser.py, en één functie en twee takken in reading_server.py.
Trek de grens
needs_preflight(method, headers) geeft True terug als de browser eerst moet vragen. headers is een SIMPLE_METHODS, SIMPLE_HEADERS en SIMPLE_CONTENT_TYPES.
Een pagina mag een headernaam met willekeurige hoofdletters schrijven, dus vergelijk
name.lower()metSIMPLE_HEADERS.Een content type kan een
dragen, zoals in text/plain; charset=utf-8. Wat telt, is de tekst vóór de;, gestript en in kleine letters.
Authorization gaat dus met elke Content-Type: application/json ook. Accept: application/json niet: Accept staat op de safelist, en alleen bij een content type wordt naar de inhoud gekeken. Test eerst de methode, loop daarna de headers langs, en geef False terug als niets over de grens ging.
Vraag
preflight_request(url, page_origin, method, headers) geeft de vraag terug als een verzoek-dict:
{"method": "OPTIONS", "url": url, "headers": asked, "body": b""}
asked bevat "origin" met de "access-control-request-method" met de methode. Als het verzoek headers buiten de safelist heeft, bevat hij ook "access-control-request-headers": hun namen, samengevoegd met een komma. De meegeleverde unsafe_header_names(headers) geeft die namen in kleine letters en gesorteerd terug, door je needs_preflight naar elke header afzonderlijk te vragen. Als die
Bouw asked uit niets anders dan de vier Authorization van het echte verzoek. Alleen de naam ervan.
De frontend wil PUT /books/2 met Authorization en Content-Type: application/json. Wat bevat Access-Control-Request-Headers van de preflight?
Lees het antwoord
preflight_allows(response, page_origin, method, headers) geeft True terug als het antwoord het echte verzoek laat gaan. Hij doet vier tests, in deze volgorde:
Het antwoord moet de pagina toestemming geven zoals elk ander antwoord:
may_read(response, page_origin, credentials=False).De status moet tussen 200 en 299 liggen.
De methode moet een lid zijn van
access-control-allow-methods, tenzij het GET, HEAD of POST is, of de lijst*bevat. De vergelijking is exact.Elke naam in
unsafe_header_names(headers)moet een lid zijn vanaccess-control-allow-headers, of de lijst bevat*en de naam is nietauthorization. Headernamen worden zonder onderscheid in hoofdletters vergeleken, dus zet elk lid eerst om naar kleine letters.
De meegeleverde list_members(text) splitst zo’n door komma’s gescheiden
Elke test kan op zichzelf nee zeggen. Een antwoord met perfecte lijsten en status 404 is een nee. Dat geldt ook voor een 204 die een andere origin noemt, of geen origin. Geef False terug bij de eerste test die mislukt, en True pas aan het eind.
Een * in een van beide lijsten dekt wat niet genoemd wordt, met één * dekt nooit authorization. Die header moet bij naam genoemd worden.
De derde test is met een reden exact. Een browser schrijft zes methodenamen voor je in hoofdletters, DELETE, GET, HEAD, OPTIONS, POST en PUT, en verder geen. Een pagina die om patch vraagt, stuurt dus patch, en een antwoord dat PATCH toestaat, dekt dat niet.
Een antwoord op een preflight heeft alle juiste headers en status 404. Wat doet de browser?
Een antwoord op een preflight zegt Access-Control-Allow-Headers: *. Het echte verzoek draagt Authorization. Is dat toegestaan?
Antwoord op de server
preflight_response() geeft het antwoord terug: status 204, een lege body, en de twee lijsten, overgenomen uit de meegeleverde constanten CORS_METHODS en CORS_HEADERS.
Waar hij wordt aangeroepen, doet er meer toe dan wat hij bevat. Beide boektakken van route() beginnen met token_claims() en een weigering, dus vandaag krijgt een preflight 401. OPTIONS moet als eerste komen, voordat een van die twee wordt bereikt:
elif segments == ["books"]:
if method == "OPTIONS":
response = preflight_response()
else:
claims = token_claims(request, state)
response = collection_refusal(method, claims)
if response is None:
De tak "/books/{id}" krijgt dezelfde vorm rond token_claims() en item_refusal().
Het antwoord op de preflight heeft nog zijn access-control-allow-origin nodig, en het antwoord op het echte verzoek dat volgt ook. Eén regel als laatste regel van elk van de twee takken geeft ze allebei:
response = add_headers(response, cors_headers(request, state["cors_origins"]))
Voor een origin buiten de set antwoordt je server nog steeds 204 met beide lijsten en vary. Alleen access-control-allow-origin ontbreekt. Dat is genoeg: de eerste test van de browser mislukt, en het echte verzoek wordt nooit verstuurd.
OPTIONS beantwoorden zonder token geeft niets weg. Het antwoord zegt welke methoden en headers er bestaan, en het echte verzoek heeft nog steeds zijn token nodig.
Je opdracht
Schrijf de drie functies die voor les 4 gemarkeerd zijn in mini_browser.py, en preflight_response en de twee takken in reading_server.py.
Schrijf daarna test_a_delete_is_asked_about_first in tests/test_browser.py. Bouw met send = steps.send_to(state) de vraag voor een DELETE van steps.OWN + "/books/1" met een header Authorization vanaf steps.APP, verstuur die, en assert dat preflight_allows True zegt. Doe hetzelfde vanaf steps.OTHER en assert False. De test roept je drie functies rechtstreeks aan, omdat visit() nog één functie uit les 5 nodig heeft voordat hij een verzoek naar een andere origin kan dragen.
python -m pytest -q
python mini_browser.py
Het eerste commando eindigt met 37 passed. De derde tabel van het tweede heeft nu antwoorden:
request asks first asks about
GET False nothing is asked
POST with Content-Type: application/x-www-form-urlencoded False nothing is asked
POST with Content-Type: application/json True content-type
GET with Authorization: Bearer a-token True authorization
DELETE True no header
Twee dingen blijven open. Een browser mag een antwoord op een preflight een tijd onthouden, die een header Access-Control-Max-Age kan verlengen. Het model onthoudt niets, en je server zet die header niet. En het
Druk daarna op Check my work.
Opdracht
Schrijf de drie preflightregels van het model, het antwoord van de server op een preflight, en één test.
In mini_browser.py:
needs_preflight(method, headers)geeftTrueterug alsmethodniet inSIMPLE_METHODSzit, als de vorm in kleine letters van een headernaam niet inSIMPLE_HEADERSzit, of als het deel vóór de;van een headercontent-type, gestript en in kleine letters, niet inSIMPLE_CONTENT_TYPESzit. Anders geeft hijFalseterug.preflight_request(url, page_origin, method, headers)geeft{"method": "OPTIONS", "url": url, "headers": asked, "body": b""}terug.askedbevat"origin"(devan de pagina), "access-control-request-method"(de), en, als unsafe_header_names(headers)niet leeg is,"access-control-request-headers"met die namen samengevoegd met een komma. Hij bevat verder niets: geenen geen headerwaarde van het echte verzoek. preflight_allows(response, page_origin, method, headers)geeftFalseterug, tenzij alle vier gelden:may_read(response, page_origin, credentials=False); een status tussen 200 en 299; de methode is GET, HEAD of POST, of hij is een lid vanaccess-control-allow-methodsprecies zoals geschreven, of diebevat *; en elke naam inunsafe_header_names(headers)is een lid vanaccess-control-allow-headers, vergeleken zonder onderscheid in hoofdletters, of die lijst bevat*en de naam is nietauthorization.
In reading_server.py:
preflight_response()geeft status 204 terug, een lege body,access-control-allow-methodsmet deCORS_METHODS, enaccess-control-allow-headersmet de waardeCORS_HEADERS.Beantwoord in de takken
"/books"en"/books/{id}"vanroute()OPTIONSmetpreflight_response()voordattoken_claims()en de weigering worden bereikt.Als laatste regel van elk van die twee takken:
response = add_headers(response, cors_headers(request, state["cors_origins"])).
Schrijf in tests/test_browser.py test_a_delete_is_asked_about_first. Met state = reading_server.sample_state(), send = steps.send_to(state), url = steps.OWN + "/books/1" en headers = {"Authorization": "Bearer a-token"}: bouw mini_browser.preflight_request(url, steps.APP, "DELETE", headers), verstuur die, en assert dat mini_browser.preflight_allows(answer, steps.APP, "DELETE", headers) True is. Doe hetzelfde voor steps.OTHER en assert False.
Voer python -m pytest -q en python mini_browser.py uit.
Druk daarna op Check my work. Dat roept needs_preflight aan met negentien verzoeken die je niet hebt gezien, in zes groepen: andere methoden, de drie eenvoudige methoden alleen, headernamen buiten de safelist, de namen op de safelist, preflight_request opstelt voor een PUT met een token en JSON, en voor een DELETE zonder headers. Het roept preflight_allows aan met eigen antwoorden: statussen van 199 tot 404, een ontbrekende of andere origin, een ontbrekende methode of een methode in kleine letters, een ster in elke lijst, en headernamen met andere hoofdletters. Het stuurt OPTIONS naar beide boekroutes zonder cookie en zonder token, vanaf de origin van de frontend en vanaf twee andere, en naar /login. Het leest een boek met een token vanaf twee origins. Een eigen browsermodel stuurt daarna een DELETE met een token door je server, één keer vanaf elke kant. Als laatste voert het je test één keer bij naam uit. Niets uit je