0%

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 in 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 der Header, die die Seite gesetzt hat, von Namen auf Text. Es ist die Funktion, die du in Lektion 3 gelesen hast, mit zwei zusätzlichen Details, und das Projekt dieses Kapitels liefert die drei Sets mit: SIMPLE_METHODS, SIMPLE_HEADERS und SIMPLE_CONTENT_TYPES.

  • Eine Seite darf einen Header-Namen in beliebiger Groß- und Kleinschreibung schreiben, also vergleiche name.lower() mit SIMPLE_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 , und 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 der Seite und "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 leer ist, wird der Header weggelassen.

Baue asked aus nichts als den vier . Kein kommt hinein, und der Header 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:

  1. Die Antwort muss der Seite eine Erlaubnis geben wie jede andere Antwort: may_read(response, page_origin, credentials=False).

  2. Ihr Status muss zwischen 200 und 299 liegen.

  3. Die Methode muss ein Element von access-control-allow-methods sein, außer sie ist GET, HEAD oder POST, oder die Liste enthält *. Der Vergleich ist exakt.

  4. Jeder Name in unsafe_header_names(headers) muss ein Element von access-control-allow-headers sein, oder die Liste enthält * und der Name ist nicht authorization. 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 in seine Elemente und gibt für einen fehlenden Header eine leere Liste.

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 : Ein * 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 -Antwort sitzt jetzt auf den gewöhnlichen Antworten der beiden Buch-Zweige, während eine 304 sie noch nicht mitnimmt. Lektion 6 behebt das.

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) gibt True zurück, wenn method nicht in SIMPLE_METHODS ist, wenn die Kleinbuchstabenform eines Header-Namens nicht in SIMPLE_HEADERS ist oder wenn der Teil eines Headers content-type vor dem ;, ohne umgebende Leerzeichen und in Kleinbuchstaben, nicht in SIMPLE_CONTENT_TYPES ist. Sonst gibt es False zurück.

  • preflight_request(url, page_origin, method, headers) gibt {"method": "OPTIONS", "url": url, "headers": asked, "body": b""} zurück. asked enthält "origin" (den der 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: kein und keinen Header- der echten Anfrage.

  • preflight_allows(response, page_origin, method, headers) gibt False zurü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 von access-control-allow-methods, genau wie geschrieben, oder diese enthält *; und jeder Name in unsafe_header_names(headers) ist ein Element von access-control-allow-headers, ohne Rücksicht auf Groß- und Kleinschreibung verglichen, oder diese Liste enthält * und der Name ist nicht authorization.

In reading_server.py:

  • preflight_response() gibt Status 204 zurück, einen leeren Body, access-control-allow-methods auf CORS_METHODS gesetzt und access-control-allow-headers auf CORS_HEADERS gesetzt.

  • Beantworte in den Zweigen "/books" und "/books/{id}" von route() OPTIONS mit preflight_response(), bevor token_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, und die drei Formular- in gemischter Groß- und Kleinschreibung mit einem . Es liest die Frage, die 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 wird bewertet.