CORS · Übung
Beantworte den Preflight
Der Austausch am Ende von Lektion 3 hat drei Entscheidungen beim Browser und eine beim Server. Der Browser entscheidet, ob er fragt, baut die Frage und liest die Antwort. Der Server beantwortet die Frage. In dieser Lektion schreibst du alle vier: drei mini_browser.py und eine Funktion und zwei Zweige in reading_server.py.
Zieh die Linie
needs_preflight(method, headers) gibt True zurück, wenn der Browser zuerst fragen muss. headers ist ein SIMPLE_METHODS, SIMPLE_HEADERS und SIMPLE_CONTENT_TYPES.
Eine Seite darf einen Header-Namen in beliebiger Groß- und Kleinschreibung schreiben, also vergleiche
name.lower()mitSIMPLE_HEADERS.Ein Content-Type kann einen
tragen, wie in text/plain; charset=utf-8. Was zählt, ist der Text vor dem;, ohne umgebende Leerzeichen und in Kleinbuchstaben.
Also überschreitet Authorization die Linie mit jeder Content-Type: application/json ebenfalls. Accept: application/json nicht: Accept steht auf der Safelist, und nur in einen Content-Type wird hineingesehen. Prüfe zuerst die Methode, geh dann die Header durch und gib False zurück, wenn nichts die Linie überschritten hat.
Frag
preflight_request(url, page_origin, method, headers) gibt die Frage als Anfrage-Dictionary zurück:
{"method": "OPTIONS", "url": url, "headers": asked, "body": b""}
asked enthält "origin" mit dem "access-control-request-method" mit der Methode. Wenn die Anfrage Header außerhalb der Safelist hat, enthält es außerdem "access-control-request-headers": ihre Namen, mit einem Komma verbunden. Das mitgelieferte unsafe_header_names(headers) gibt diese Namen in Kleinbuchstaben und sortiert zurück, indem es dein needs_preflight zu jedem Header einzeln befragt. Wenn diese
Baue asked aus nichts als den vier Authorization der echten Anfrage auch nicht. Nur sein Name.
Das Frontend möchte PUT /books/2 mit Authorization und Content-Type: application/json. Was enthält Access-Control-Request-Headers des Preflights?
Lies die Antwort
preflight_allows(response, page_origin, method, headers) gibt True zurück, wenn die Antwort die echte Anfrage gehen lässt. Es macht vier Tests, in dieser Reihenfolge:
Die Antwort muss der Seite eine Erlaubnis geben wie jede andere Antwort:
may_read(response, page_origin, credentials=False).Ihr Status muss zwischen 200 und 299 liegen.
Die Methode muss ein Element von
access-control-allow-methodssein, außer sie ist GET, HEAD oder POST, oder die Liste enthält*. Der Vergleich ist exakt.Jeder Name in
unsafe_header_names(headers)muss ein Element vonaccess-control-allow-headerssein, oder die Liste enthält*und der Name ist nichtauthorization. Header-Namen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, also wandle jedes Element zuerst in Kleinbuchstaben um.
Das mitgelieferte list_members(text) zerlegt einen solchen kommagetrennten
Jeder Test kann allein Nein sagen. Eine Antwort mit perfekten Listen und dem Status 404 ist ein Nein. Genauso eine 204, die einen anderen Origin nennt oder keinen. Gib beim ersten Test, der scheitert, False zurück, und True nur am Ende.
Ein * in einer der beiden Listen deckt ab, was nicht genannt ist, mit einer * deckt nie authorization ab. Dieser Header muss genannt werden.
Der dritte Test ist aus einem Grund exakt. Ein Browser schreibt sechs Methodennamen für dich in Großbuchstaben, DELETE, GET, HEAD, OPTIONS, POST und PUT, und keine anderen. Eine Seite, die nach patch fragt, sendet also patch, und eine Antwort, die PATCH erlaubt, deckt das nicht ab.
Eine Preflight-Antwort hat alle richtigen Header und den Status 404. Was macht der Browser?
Eine Preflight-Antwort sagt Access-Control-Allow-Headers: *. Die echte Anfrage trägt Authorization. Ist sie erlaubt?
Antworte auf dem Server
preflight_response() gibt die Antwort zurück: Status 204, einen leeren Body und die beiden Listen, entnommen aus den mitgelieferten Konstanten CORS_METHODS und CORS_HEADERS.
Wo sie aufgerufen wird, ist wichtiger als das, was sie enthält. Beide Buch-Zweige von route() beginnen mit token_claims() und einer Ablehnung, also bekommt ein Preflight heute eine 401. OPTIONS muss zuerst kommen, bevor eines davon erreicht wird:
elif segments == ["books"]:
if method == "OPTIONS":
response = preflight_response()
else:
claims = token_claims(request, state)
response = collection_refusal(method, claims)
if response is None:
Der Zweig "/books/{id}" bekommt dieselbe Form um token_claims() und item_refusal().
Die Preflight-Antwort braucht noch ihr access-control-allow-origin, und die Antwort auf die echte Anfrage, die folgt, ebenso. Eine Zeile als letzte Zeile jedes der beiden Zweige gibt beiden eines:
response = add_headers(response, cors_headers(request, state["cors_origins"]))
Für einen Origin außerhalb des Sets antwortet dein Server trotzdem mit 204, beiden Listen und vary. Nur access-control-allow-origin fehlt. Das genügt: Der erste Test des Browsers scheitert, und die echte Anfrage wird nie gesendet.
OPTIONS ohne Token zu beantworten gibt nichts preis. Die Antwort sagt, welche Methoden und Header es gibt, und die echte Anfrage braucht weiterhin ihr Token.
Deine Aufgabe
Schreibe die drei Funktionen, die in mini_browser.py für Lektion 4 markiert sind, und preflight_response und die beiden Zweige in reading_server.py.
Schreibe dann test_a_delete_is_asked_about_first in tests/test_browser.py. Baue mit send = steps.send_to(state) die Frage für ein DELETE von steps.OWN + "/books/1" mit einem Header Authorization von steps.APP, sende sie und sichere zu, dass preflight_allows True sagt. Mach dasselbe von steps.OTHER aus und sichere False zu. Der Test ruft deine drei Funktionen direkt auf, weil visit() noch eine Funktion aus Lektion 5 braucht, bevor es eine Anfrage an einen anderen Origin tragen kann.
python -m pytest -q
python mini_browser.py
Der erste endet mit 37 passed. Die dritte Tabelle des zweiten hat jetzt Antworten:
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
Zwei Dinge bleiben offen. Ein Browser darf sich eine Preflight-Antwort eine Zeit lang merken, die ein Header Access-Control-Max-Age verlängern kann. Das Modell merkt sich nichts, und dein Server setzt diesen Header nicht. Und die
Klicke dann auf Check my work.
Aufgabe
Schreibe die drei Preflight-Regeln des Modells, die Antwort des Servers auf einen Preflight und einen Test.
In mini_browser.py:
needs_preflight(method, headers)gibtTruezurück, wennmethodnicht inSIMPLE_METHODSist, wenn die Kleinbuchstabenform eines Header-Namens nicht inSIMPLE_HEADERSist oder wenn der Teil eines Headerscontent-typevor dem;, ohne umgebende Leerzeichen und in Kleinbuchstaben, nicht inSIMPLE_CONTENT_TYPESist. Sonst gibt esFalsezurück.preflight_request(url, page_origin, method, headers)gibt{"method": "OPTIONS", "url": url, "headers": asked, "body": b""}zurück.askedenthält"origin"(dender Seite), "access-control-request-method"(die) und, wenn unsafe_header_names(headers)nicht leer ist,"access-control-request-headers"mit diesen Namen, durch ein Komma verbunden. Es enthält nichts anderes: keinund keinen Header- der echten Anfrage. preflight_allows(response, page_origin, method, headers)gibtFalsezurück, außer alle vier gelten:may_read(response, page_origin, credentials=False); ein Status zwischen 200 und 299; die Methode ist GET, HEAD oder POST, oder sie ist ein Element vonaccess-control-allow-methods, genau wie geschrieben, oder dieseenthält *; und jeder Name inunsafe_header_names(headers)ist ein Element vonaccess-control-allow-headers, ohne Rücksicht auf Groß- und Kleinschreibung verglichen, oder diese Liste enthält*und der Name ist nichtauthorization.
In reading_server.py:
preflight_response()gibt Status 204 zurück, einen leeren Body,access-control-allow-methodsaufCORS_METHODSgesetzt undaccess-control-allow-headersaufCORS_HEADERSgesetzt.Beantworte in den Zweigen
"/books"und"/books/{id}"vonroute()OPTIONSmitpreflight_response(), bevortoken_claims()und die Ablehnung erreicht werden.Als letzte Zeile jedes dieser beiden Zweige:
response = add_headers(response, cors_headers(request, state["cors_origins"])).
Schreibe in tests/test_browser.py den Test test_a_delete_is_asked_about_first. Mit state = reading_server.sample_state(), send = steps.send_to(state), url = steps.OWN + "/books/1" und headers = {"Authorization": "Bearer a-token"}: Baue mini_browser.preflight_request(url, steps.APP, "DELETE", headers), sende es und sichere zu, dass mini_browser.preflight_allows(answer, steps.APP, "DELETE", headers) True ist. Mach dasselbe für steps.OTHER und sichere False zu.
Führe python -m pytest -q und python mini_browser.py aus.
Klicke dann auf Check my work. Es ruft needs_preflight mit neunzehn Anfragen auf, die du noch nicht gesehen hast, in sechs Gruppen: andere Methoden, die drei einfachen Methoden allein, Header-Namen außerhalb der Safelist, die Namen auf der Safelist, preflight_request für ein PUT mit einem Token und JSON baut, und für ein DELETE ohne Header. Es ruft preflight_allows mit eigenen Antworten auf: Status von 199 bis 404, ein fehlender oder anderer Origin, eine fehlende oder kleingeschriebene Methode, ein Stern in jeder Liste und Header-Namen in anderer Groß- und Kleinschreibung. Es sendet OPTIONS an beide Buch-Routen ohne Cookie und ohne Token, vom Origin des Frontends und von zwei anderen, und an /login. Es liest ein Buch mit einem Token von zwei Origins. Ein eigenes Browsermodell sendet dann ein DELETE mit einem Token durch deinen Server, einmal von jedem der beiden Origins. Zuletzt führt es deinen Test einmal mit Namen aus. Nichts aus deinem