0%

Um einen Protokollwechsel bitten · Übung

Die Upgrade-Anfrage zerlegen

Die letzte Lektion hat die Upgrade-Anfrage des RFC mit dem Auge gelesen. Dein Server muss sie mit Code lesen: die Bytes eines Anfragekopfs in ein mit der , dem Pfad, der Version und den Headern verwandeln. Einen Antwortkopf hast du in with Python auf diese Weise zerlegt. Eine Anfrage unterscheidet sich in zwei Punkten: Sie beginnt mit einer Anfragezeile statt mit einer Statuszeile, und dieser Kurs speichert jeden Header als einen statt als .

Das Projekt

Ab dieser Lektion liegt der Code des Kapitels in einem Projekt auf den Servern von Python Land, und seine Dateien bleiben von Lektion zu Lektion erhalten. Der Projekteditor zeigt zwei Dateien:

  • ws_handshake.py ist die Datei, in der du arbeitest. Sie enthält die Konstanten GUID und MAX_HEAD, zwei fertige und einen Platzhalter für jede Funktion, die dieses Kapitel verlangt, dessen Docstring die Lektion nennt, die sie schreibt.

  • handshake_demo.py ist das Programm, das der Button Run startet. Es ruft die Funktion jeder Lektion mit dem Beispiel des RFC auf und gibt das Ergebnis aus, oder not written yet bei einem Platzhalter. Bis Lektion 6 öffnet es keine Verbindung.

Die fertigen Funktionen kennst du. make_reader(sock) umhüllt einen mit einem Empfangspuffer, wie in Networks with Python, und read_head(reader) ist deine Funktion aus HTTP with Python, Lektion 1.3: Sie gibt die Bytes vor der Leerzeile zurück, ohne das \r\n\r\n, und behält alles, was danach kommt, im Puffer. Das ist der Kopf, den dein Parser erhält.

Von Bytes zu Zeilen

Die ersten Schritte sind die aus HTTP with Python, Lektion 1.4. Dekodiere die Bytes mit "iso-8859-1", was jedes Byte in ein Zeichen verwandelt und nie fehlschlägt, und teile den Text bei "\r\n" auf. Die erste Zeile ist die Anfragezeile; der Rest sind Header-Zeilen.

Schneide die Anfragezeile mit split(" ", 2) an einzelnen Leerzeichen in drei Teile: Methode, Pfad und Version. Die Anzahl 2 bedeutet höchstens zwei Schnitte, also bleibt eine Version, hinter der noch etwas steht, wie in GET /chat HTTP/1.1 extra, im dritten Teil, wo du es sehen und ablehnen kannst.

Header in ein Dict

Eine Header-Zeile besteht aus einem Namen, einem Doppelpunkt und einem . Schneide sie nur am ersten Doppelpunkt, weil ein Wert eigene Doppelpunkte enthalten kann: Ein Host-Header mit , etwa Host: example.com:8080, hat einen in seinem Wert.

Was gibt das aus?

print("Host: example.com:8080".split(":", 1))

Der Wert behält das Leerzeichen, das auf den Doppelpunkt folgte, und er kann auch an seinem Ende Leerzeichen haben, also wende .strip() darauf an. Header-Namen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, also speichere jeden Namen mit .lower(). Lass die Schreibung der Werte in Ruhe: Upgrade: WebSocket behält seine Großbuchstaben im Dictionary, und Lektion 4 vergleicht Tokens beim Prüfen ohne Rücksicht auf Groß- und Kleinschreibung.

parse_headers(lines) erledigt das für eine Liste von Header-Zeilen. parse_upgrade_request(data) dekodiert und teilt den Kopf, schneidet die Anfragezeile, übergibt die anderen Zeilen an parse_headers und gibt ein Dictionary zurück:

{"method": ..., "path": ..., "version": ..., "headers": {...}}

Eine Anfrage ganz ohne Header-Zeilen ergibt "headers": {}, also gibt parse_headers([]) ein leeres Dictionary zurück.

Ein Name, der zweimal vorkommt

HTTP erlaubt einem Sender, einen Header auf mehrere Zeilen mit demselben Namen zu verteilen. Connection: keep-alive in einer Zeile und Connection: Upgrade in einer anderen bedeuten dasselbe wie Connection: keep-alive, Upgrade. RFC 9110 sagt, dass ein Empfänger sie so zusammenfassen darf: die Werte in der Reihenfolge ihres Eintreffens, verbunden durch ein Komma und ein Leerzeichen.

In HTTP with Python hat dein Antwort-Parser für jeden Namen eine Liste von Werten geführt. Dieser Kurs speichert stattdessen einen einzelnen String und verbindet die Werte eines wiederholten Namens mit ", ", weil jeder Header, den der liest, ohnehin eine durch Kommas getrennte Liste von Tokens ist. Eine spätere Zeile darf nie eine frühere überschreiben: Das Upgrade-Token in der zweiten Connection-Zeile ist das, auf das es ankommt.

Eine Anfrage hat Connection: keep-alive und, zwei Zeilen später, Connection: Upgrade. Was sollte headers["connection"] enthalten?

Das Verbinden hat noch eine Wirkung, auf die sich RFC 6455 verlässt. Eine Anfrage darf Sec-WebSocket-Key nur einmal tragen. Wenn ein Client ihn zweimal sendet, enthält dein Dictionary zwei Schlüssel, verbunden durch ", ", was kein gültiger Schlüssel ist, und die Prüfung aus Lektion 4 lehnt ihn ohne eigene Regel ab.

Was als fehlerhaft gilt

Dein Parser lehnt ab, was er nicht lesen kann, statt zu raten. Löse aus bei:

  • einer Anfragezeile, die sich an einzelnen Leerzeichen nicht in genau drei Teile schneiden lässt, etwa GET /chat, oder GET /chat HTTP/1.1 extra, wo der dritte Teil ein Leerzeichen enthält;

  • einer Version, die nicht mit HTTP/ beginnt, etwa FTP/1.0;

  • einer Header-Zeile ohne Doppelpunkt;

  • einer Header-Zeile, deren Name leer ist oder ein Leerzeichen enthält, etwa : x oder Bad Name: x.

Gib jedem ValueError eine Meldung, die die abgelehnte Zeile zitiert. Echte Server unterscheiden sich darin, was sie tolerieren; manche akzeptieren zwei Leerzeichen hintereinander in der Anfragezeile. Ein strenger Parser lehnt mit Begründung ab, und nichts von dem, was er akzeptiert, wurde geraten.

Deine Aufgabe

Schreibe in ws_handshake.py zuerst parse_headers(lines) und dann parse_upgrade_request(data), wie oben beschrieben. Lass die Funktionen für die Lektionen 3 bis 6, wie sie sind. Klicke dann auf Run. Mit deinem Parser gibt handshake_demo.py das Dictionary für die Anfrage des RFC aus, und die Abschnitte der späteren Lektionen sagen weiterhin not written yet:

Lesson 2: parse_upgrade_request
{'method': 'GET', 'path': '/chat', 'version': 'HTTP/1.1', 'headers': {'host': 'server.example.com', 'upgrade': 'websocket', 'connection': 'Upgrade', 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==', 'origin': 'http://example.com', 'sec-websocket-protocol': 'chat, superchat', 'sec-websocket-version': '13'}}
Lesson 3: accept_value
  not written yet
Lesson 4: check_upgrade
  not written yet
Lesson 5: build_upgrade_request
  not written yet
Lesson 6: run_loopback is not written yet

Diese Ausgabe stammt aus einem lokalen Lauf der fertigen Lektion. Ein Name mit Großbuchstaben oder ein Wert mit Leerzeichen drumherum deutet auf .lower() oder .strip() hin.

Wenn das Dictionary richtig aussieht, klicke auf Submit. Die Prüfung ruft deine beiden Funktionen mit Anfrageköpfen auf, die du nicht gesehen hast, mit ungewöhnlicher Schreibung, aufgefüllten Werten, einem wiederholten Namen und einem Doppelpunkt in einem Wert, und mit Köpfen, die abgelehnt werden müssen.

Noch sagt nichts im Dictionary, ob der Server die Anfrage annehmen sollte. Die nächste Lektion berechnet den einen Wert, ohne den die Antwort des Servers nicht auskommt.

Aufgabe

Schreibe zwei in ws_handshake.py, in dieser Reihenfolge. Submit nennt die erste, die noch NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.

  1. parse_headers(lines) nimmt eine von Header-Zeilen () und gibt ein zurück. Schneide jede Zeile nur an ihrem ersten Doppelpunkt; der Name kommt in Kleinbuchstaben hinein, der ohne die Leerzeichen drumherum. Wenn ein Name erneut vorkommt, verbinde den neuen Wert mit dem gespeicherten durch ", ", in Reihenfolge. Löse aus bei einer Zeile ohne Doppelpunkt und bei einem Namen, der leer ist oder ein Leerzeichen enthält. parse_headers([]) gibt {} zurück.

  2. parse_upgrade_request(data) nimmt den Kopf als Bytes, so wie read_head ihn zurückgibt: alles vor der Leerzeile, ohne das abschließende \r\n\r\n. Dekodiere ihn mit "iso-8859-1", teile ihn bei "\r\n" auf und schneide die erste Zeile mit split(" ", 2). Löse ValueError aus, außer wenn das genau drei Teile ergibt, der dritte kein Leerzeichen enthält und mit HTTP/ beginnt. Gib {"method": ..., "path": ..., "version": ..., "headers": parse_headers(the other lines)} zurück.

Lass die Funktionen für die Lektionen 3 bis 6, wie sie sind. Klicke auf Run, um dein Dictionary für die Anfrage des RFC zu sehen.

Klicke dann auf Submit. Die Prüfung ruft beide Funktionen direkt auf, ohne : mit der Anfrage des RFC, mit einem Anfragekopf, den du nicht gesehen hast, dessen Namen eine seltsame Schreibung haben, dessen Werte zusätzliche Leerzeichen tragen, mit einem wiederholten Connection-Header und einem Doppelpunkt in einem Wert, und mit Anfragezeilen und Header-Zeilen, die ValueError auslösen müssen. Sie prüft die Werte in deinem Dictionary, nicht die Reihenfolge seiner Schlüssel. Das Importieren von ws_handshake.py darf nichts ausgeben.