0%

Anfragen von Hand schreiben · Übung

Header, die der Server empfangen hat

Jede Anfrage, die du gesendet hast, trägt eine , und du hast sie nicht geschrieben: HOST_LINE, Host: practice-api:8080, mitgeliefert in Kapitel 1 mit dem Versprechen, dass dieses Kapitel sie erklärt. In dieser Lektion baust du diese Zeile aus der Adresse, schreibst alle anderen Headerzeilen, um die ein Aufrufer bittet, lehnst die ab, die die Anfrage kaputtmachen würden, und sendest einen Header an die Route /v1/headers von practice-api, die meldet, was sie empfangen hat.

Warum es Host gibt

Wenn sich dein Client mit practice-api auf 8080 verbindet, wird der Name zuerst zu einer aufgelöst, wie in Networks with Python. Die Verbindung selbst trägt nur diese Adresse und den Port. Der Name, von dem du ausgegangen bist, ist weg, wenn das erste Byte ankommt.

Das ist wichtig, weil eine Adresse oft für viele Namen antwortet. Ein einzelner Server kann books.test, shop.test und news.test gleichzeitig beherbergen, die alle zur selben IP-Adresse aufgelöst werden und alle auf Port 80 liegen. Wenn eine Anfrage ankommt, muss der Server wissen, welchen davon der Client gemeint hat, und nichts in der Verbindung sagt es. Die Zeile Host sagt es: Die Anfrage nennt den Server, für den sie bestimmt ist, und der Server wählt die Website, die antwortet.

Deshalb verlangt /1.1, dass jede Anfrage genau eine Zeile Host trägt, und ein Server muss eine Anfrage ohne sie mit 400 Bad Request ablehnen. Der Webserver vor practice-api sollte genau das tun, mit einer kurzen HTML-Seite statt . Wenn eine Anfrage ohne Host doch einmal das übliche 200 bekommt, hat sie den Dienst direkt erreicht, der das Fehlen toleriert; HTTP/1.1 verlangt die Zeile trotzdem, und dein Client sendet sie immer.

Der von Host übernimmt Host und Port der , genau so, wie die URL sie geschrieben hat:

URLZeile Host
http://practice-api:8080/v1/headersHost: practice-api:8080
http://books.test/shelvesHost: books.test
http://books.test:80/shelvesHost: books.test:80

Eine URL, die keinen Port nennt, bekommt eine Zeile Host ohne Port, obwohl sich der Client mit Port 80 verbindet. Deshalb hat Lektion 2 das None von urlsplit behalten: Dein host_header(host, port) versteht port=None als “die URL hat keinen Port genannt” und schreibt nur den Host. Weil 80 der Standardport für http ist, nennen Host: books.test und Host: books.test:80 denselben Server, und Server akzeptieren beides; dein host_header schreibt den Port immer dann, wenn es einen bekommt.

Die Verbindung hat practice-api auf Port 8080 schon erreicht. Warum sendet die Anfrage trotzdem Host: practice-api:8080?

Header-Syntax

Jede Headerzeile hat dieselbe Form: ein Name, ein Doppelpunkt, dann der Wert, mit \r\n am Ende. Ein Leerzeichen nach dem Doppelpunkt ist üblich, und dein Client schreibt immer eines:

X-Practice-Name: http-with-python

Der Name ist ein Wort: Am ersten Doppelpunkt endet er, also kann er keinen Doppelpunkt enthalten, und auch kein Leerzeichen, keinen Tabulator und keinen Zeilenumbruch. Die vollständige Regel von HTTP für Namen ist noch strenger und verbietet auch Zeichen wie /, (, , und @; dein Client lehnt die ab, die die Anfrage kaputtmachen würden. Der Wert ist der Rest der Zeile und darf Leerzeichen und Doppelpunkte enthalten; er darf nie \r oder \n enthalten, weil diese die Zeile beenden.

Wenn ein Feld mehrere Werte hat, schreibt der Client eine Zeile pro Wert, jedes Mal mit demselben Namen. Das ist die Form von parse_head aus Kapitel 1, andersherum gelesen. Dort wurden die Zeilen X-Trace: a und x-trace: b zu {"x-trace": ["a", "b"]}; hier bekommt ein Aufrufer, der {"X-Tag": ["a", "b"]} übergibt, zwei Zeilen:

X-Tag: a
X-Tag: b

Vom zu den Zeilen zu kommen sind zwei , eine über die Namen und eine über die Werte jedes Namens:

Try it

Ein Aufrufer, der einen einzelnen übergibt, {"Accept": "application/json"}, meint eine Zeile. format_headers akzeptiert beide Formen: Ein Wert, der ein str ist, wird wie eine mit einem Element behandelt, sodass die innere Schleife nie Buchstabe für Buchstabe durch einen String läuft.

Ein Zeilenumbruch in einem Wert

Headerwerte kommen oft von woanders: der Name eines Benutzers, ein Titel, ein Wert aus einer Datei. Angenommen, einer davon enthält einen Zeilenumbruch:

Try it

Der Aufrufer meinte einen Header, X-Note, mit einem seltsamen Wert. Der Server liest zwei: X-Note: hello und X-Admin: yes, eine Zeile, die niemand absichtlich geschrieben hat. Das ist Header-Injection (header injection). Wenn irgendein Teil eines Werts von jemand anderem gewählt werden kann, kann diese Person jeden beliebigen Header hinzufügen oder den Kopf mit einer Leerzeile vorzeitig beenden und einen Body beginnen. Die Lösung ist einfach und streng: Lehne jeden Namen und jeden Wert ab, der \r oder \n enthält, und löse aus, bevor irgendetwas gesendet wird.

Ein Aufrufer übergibt {"X-Note": "hello\r\nX-Admin: yes"}, und ein Client schreibt es ohne Prüfung. Was liest der Server?

Deine Aufgabe

Vier Teile von wire.py gehören zu dieser Lektion, in dieser Reihenfolge.

  1. host_header(host, port) gibt die Zeile Host zurück, mit \r\n am Ende: Host: practice-api:8080\r\n für ("practice-api", 8080) und Host: books.test\r\n für ("books.test", None).

  2. format_headers(headers) gibt einen str mit einer Zeile Name: value\r\n pro Wert zurück. Lauf wie im Beispiel oben in einer Schleife über die Namen und ihre Werte. Wenn ein Wert ein str ist, was du mit isinstance(values, str) prüfen kannst, behandle ihn wie eine Liste mit einem Element. Löse ValueError aus für einen leeren Namen; für einen Namen, der ein Leerzeichen, einen Doppelpunkt, einen Tabulator, \r oder \n enthält; und für einen Wert, der \r oder \n enthält. Ein leeres Dict ergibt "".

  3. Ändere make_request zu make_request(method, target, headers=None). Baue den Kopf aus der Anfragezeile, dann host_header(ADDRESS[0], ADDRESS[1]) anstelle von HOST_LINE, dann format_headers(headers), wenn headers nicht None ist, dann die Leerzeile. Host kommt zuerst, wie HTTP es empfiehlt. Entferne HOST_LINE oben aus der Datei, damit nichts versehentlich eine zweite Zeile Host hinzufügen kann.

  4. Gib fetch einen headers=None nach dem *, neben connect und timeout, und reiche ihn an make_request weiter.

Lies zuerst deine eigenen Bytes. Im :

python -c "import wire; print(wire.make_request('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS))"
b'GET /v1/headers HTTP/1.1\r\nHost: practice-api:8080\r\nX-Practice-Name: http-with-python\r\n\r\n'

Das sind genau die Bytes, die dein Client sendet: die Anfragezeile, deine aus ADDRESS gebaute Zeile Host, der Übungs-Header und die Leerzeile. Prüfe jede davon. Die Live-Läufe in diesem Kapitel übergeben headers=wire.PRACTICE_HEADERS, wie README.md sagt; python wire.py allein sendet nur Host.

Was der Server empfangen hat

Sende jetzt die Anfrage. /v1/headers antwortet mit dem Header X-Practice-Name, den es empfangen hat, und mit nichts sonst: Es meldet weder Host noch irgendeinen anderen Header. practice-api steht außerdem hinter einem kleinen Webserver, der jede Anfrage an ihn weiterreicht, und dieser vorgeschaltete Server ist so eingerichtet, dass er seine eigene Zeile Host für den Dienst schreibt; selbst eine Route, die Host meldet, würde also nicht deine zeigen. Für Host und jede andere Zeile sind deine eigenen Anfrage-Bytes oben der Beleg dafür, was du gesendet hast. Für X-Practice-Name ist die Antwort des Servers ein zweiter Beleg:

python -c "import wire; print(wire.fetch('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS)['body'])"

Du solltest {"header":"X-Practice-Name","value":"http-with-python"} sehen: Die Route spiegelt diesen einen Header zurück, egal in welcher Schreibweise du seinen Namen gesendet hast, und keinen anderen. Wenn der Body anders aussieht, vergleiche ihn mit deinen Anfrage-Bytes oben; das ist, was du gesendet hast.

Der Body wird als Bytes ausgegeben, also endet er mit \n, dem Zeilenumbruch, den practice-api nach jedem JSON-Body setzt.

Sende zum Schluss zwei Werte für den Übungs-Header. format_headers schreibt sie als zwei Zeilen, und practice-api lehnt einen wiederholten Übungs-Header ab:

python -c "import wire; r = wire.fetch('GET', '/v1/headers', headers={'X-Practice-Name': ['a', 'b']}); print(r['status'], r['body'])"

Du solltest 400 mit der Meldung "X-Practice-Name must not be repeated" sehen: Der Server hat zwei Zeilen mit demselben Namen gesehen, und genau das ist ein wiederholtes Feld auf der Leitung. Wenn du etwas anderes siehst, enthalten deine Anfrage-Bytes trotzdem die zwei Zeilen; vergleiche sie mit der Antwort. Nichts in diesem Lauf wird geprüft; er ist zum Anschauen da.

Drücke dann Check my work.

Wenn eine Prüfung fehlschlägt, bitte Monty, die fehlschlagende Meldung mit deiner Schleife in format_headers zu vergleichen und zu sagen, gegen welche Regel sie verstößt, ohne den Code zu schreiben.

Dein Client schreibt jetzt jede Zeile einer Anfrage selbst, aber fetch sendet immer noch nur an practice-api, und nur ein Ziel, das du getrennt gebaut hast. Die nächste Lektion setzt die Teile in einer zusammen, die jede , jede URL, beliebige Header und einen Body nimmt.

Aufgabe

Arbeite in wire.py in dieser Reihenfolge:

  1. host_header(host, port) gibt "Host: <host>:<port>\r\n" zurück, oder "Host: <host>\r\n", wenn port gleich None ist.

  2. format_headers(headers) gibt einen str mit einer Zeile "Name: value\r\n" pro zurück, wobei ein Wert ein str oder eine von str ist. Die löst aus für einen leeren Namen, einen Namen, der ein Leerzeichen, einen Doppelpunkt, einen Tabulator, \r oder \n enthält, und einen Wert, der \r oder \n enthält. Ein leeres ergibt "".

  3. make_request(method, target, headers=None) gibt die Anfragezeile zurück, dann host_header(ADDRESS[0], ADDRESS[1]) anstelle von HOST_LINE, dann format_headers(headers), wenn headers nicht None ist, dann die Leerzeile, kodiert als ASCII. Entferne HOST_LINE, damit die Anfrage genau eine Zeile Host hat.

  4. fetch bekommt headers=None nach dem * und reicht es an make_request weiter.

Gib im wire.make_request('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS) aus und lies jede Zeile, hole dann /v1/headers mit dem Übungs-Header ab, und noch einmal mit zwei Werten dafür.

Drücke dann Check my work. Die Prüfung ruft host_header mit Hosts und auf, die du nicht gesehen hast, mit und ohne Port; format_headers mit Namen und Werten, die du nicht gesehen hast, darunter ein wiederholtes Feld, ein Wert mit Doppelpunkten und neun unsichere Namen und Werte, von denen jeder ValueError auslösen muss; und make_request mit und ohne Header, einmal mit ADDRESS auf einen anderen Host und Port geändert, sodass die Zeile Host aus ADDRESS kommen muss. Sie ruft fetch mit Headern über eine Ersatz-Funktion für connect auf und liest die Anfrage, die es gesendet hat, während die Antwort in Stücken ankommt, an jeder Position geschnitten, auf einer Verbindung, die entweder schließt oder offen bleibt. Die Reihenfolge zwischen Headern mit verschiedenen Namen wird nicht geprüft. Sie nimmt nie Kontakt mit practice-api auf und liest dein Terminal nicht.