0%

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 in 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 met de headers die de pagina zette, namen naar tekst. Het is de functie die je in les 3 las, met twee details erbij, en het project van dit hoofdstuk levert de drie sets: SIMPLE_METHODS, SIMPLE_HEADERS en SIMPLE_CONTENT_TYPES.

  • Een pagina mag een headernaam met willekeurige hoofdletters schrijven, dus vergelijk name.lower() met SIMPLE_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 over de grens, en 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 van de pagina en "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 leeg is, wordt de header weggelaten.

Bouw asked uit niets anders dan de vier . Er gaat geen in, en ook niet de header 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:

  1. Het antwoord moet de pagina toestemming geven zoals elk ander antwoord: may_read(response, page_origin, credentials=False).

  2. De status moet tussen 200 en 299 liggen.

  3. 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.

  4. Elke naam in unsafe_header_names(headers) moet een lid zijn van access-control-allow-headers, of de lijst bevat * en de naam is niet authorization. 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 in haar leden, en geeft een lege lijst voor een header die ontbreekt.

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 : een * 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 -antwoord zit nu op de gewone antwoorden van de twee boektakken, terwijl een 304 nog zonder vertrekt. Les 6 lost dat op.

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) geeft True terug als method niet in SIMPLE_METHODS zit, als de vorm in kleine letters van een headernaam niet in SIMPLE_HEADERS zit, of als het deel vóór de ; van een header content-type, gestript en in kleine letters, niet in SIMPLE_CONTENT_TYPES zit. Anders geeft hij False terug.

  • preflight_request(url, page_origin, method, headers) geeft {"method": "OPTIONS", "url": url, "headers": asked, "body": b""} terug. asked bevat "origin" (de van 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: geen en geen headerwaarde van het echte verzoek.

  • preflight_allows(response, page_origin, method, headers) geeft False terug, 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 van access-control-allow-methods precies zoals geschreven, of die bevat *; en elke naam in unsafe_header_names(headers) is een lid van access-control-allow-headers, vergeleken zonder onderscheid in hoofdletters, of die lijst bevat * en de naam is niet authorization.

In reading_server.py:

  • preflight_response() geeft status 204 terug, een lege body, access-control-allow-methods met de CORS_METHODS, en access-control-allow-headers met de waarde CORS_HEADERS.

  • Beantwoord in de takken "/books" en "/books/{id}" van route() OPTIONS met preflight_response() voordat token_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, , en de drie formuliertypes met gemengde hoofdletters en een . Het leest de vraag die 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 wordt beoordeeld.