Anfragen von Hand schreiben · Übung
Header, die der Server empfangen hat
Jede Anfrage, die du gesendet hast, trägt eine 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
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 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 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 Host übernimmt Host und Port der
| URL | Zeile Host |
|---|---|
http://practice-api:8080/v1/headers | Host: practice-api:8080 |
http://books.test/shelves | Host: books.test |
http://books.test:80/shelves | Host: 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
Ein Aufrufer, der einen einzelnen {"Accept": "application/json"}, meint eine Zeile. format_headers akzeptiert beide Formen: Ein Wert, der ein str ist, wird wie eine
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:
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
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.
host_header(host, port)gibt die ZeileHostzurück, mit\r\nam Ende:Host: practice-api:8080\r\nfür("practice-api", 8080)undHost: books.test\r\nfür("books.test", None).format_headers(headers)gibt einenstrmit einer ZeileName: value\r\npro Wert zurück. Lauf wie im Beispiel oben in einer Schleife über die Namen und ihre Werte. Wenn ein Wert einstrist, was du mitisinstance(values, str)prüfen kannst, behandle ihn wie eine Liste mit einem Element. LöseValueErroraus für einen leeren Namen; für einen Namen, der ein Leerzeichen, einen Doppelpunkt, einen Tabulator,\roder\nenthält; und für einen Wert, der\roder\nenthält. Ein leeres Dict ergibt"".Ändere
make_requestzumake_request(method, target, headers=None). Baue den Kopf aus der Anfragezeile, dannhost_header(ADDRESS[0], ADDRESS[1])anstelle vonHOST_LINE, dannformat_headers(headers), wennheadersnichtNoneist, dann die Leerzeile.Hostkommt zuerst, wie HTTP es empfiehlt. EntferneHOST_LINEoben aus der Datei, damit nichts versehentlich eine zweite ZeileHosthinzufügen kann.Gib
fetcheinenheaders=Nonenach dem*, nebenconnectundtimeout, und reiche ihn anmake_requestweiter.
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
Aufgabe
Arbeite in wire.py in dieser Reihenfolge:
host_header(host, port)gibt"Host: <host>:<port>\r\n"zurück, oder"Host: <host>\r\n", wennportgleichNoneist.format_headers(headers)gibt einenstrmit einer Zeile"Name: value\r\n"prozurück, wobei ein Wert ein stroder einevon strist. Dielöst aus für einen leeren Namen, einen Namen, der ein Leerzeichen, einen Doppelpunkt, einen Tabulator, \roder\nenthält, und einen Wert, der\roder\nenthält. Ein leeresergibt "".make_request(method, target, headers=None)gibt die Anfragezeile zurück, dannhost_header(ADDRESS[0], ADDRESS[1])anstelle vonHOST_LINE, dannformat_headers(headers), wennheadersnichtNoneist, dann die Leerzeile, kodiert als ASCII. EntferneHOST_LINE, damit die Anfrage genau eine ZeileHosthat.fetchbekommtheaders=Nonenach dem*und reicht es anmake_requestweiter.
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 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.