0%

Um einen Protokollwechsel bitten · Übung

Die Hälfte des Clients schreiben

Dein Server kann eine Anfrage zerlegen, entscheiden und antworten. Noch sendet ihm niemand eine Anfrage. Diese Lektion schreibt die andere Seite: den Client, der einen Schlüssel erzeugt, die Upgrade-Anfrage schreibt und die Antwort des Servers prüft, bevor er glaubt, dass das Protokoll gewechselt hat.

Ein neuer Schlüssel

Der Schlüssel des Clients besteht aus 16 zufälligen Bytes in , neu für jede Anfrage. Pythons os.urandom(16) gibt 16 Bytes aus der Quelle unvorhersehbarer Zufälligkeit des zurück, und das ist die richtige Quelle für , die ein anderes Programm nicht erraten können darf. base64.b64encode macht daraus die 24 Zeichen eines Schlüssels, und .decode("ascii") macht daraus Text.

Ein Test kann einen zufälligen Schlüssel nicht prüfen, weil er ihn nicht im Voraus kennen kann. Deshalb nimmt make_client_key die Quelle der Zufälligkeit als , mit der echten als Standardwert:

def make_client_key(random_bytes=os.urandom):
    ...

Rufe darin random_bytes(16) auf. Im normalen Gebrauch ruft das os.urandom(16) auf. Ein Test übergibt stattdessen eine andere , eine, die bekannte Bytes zurückgibt, und weiß dann genau, welcher Schlüssel herauskommt. Das ist dieselbe Idee wie der Parameter connect=, den du in Networks with Python kennengelernt hast: eine Funktion, die als Wert übergeben wird, damit ein Test seine eigene einsetzen kann.

import base64


def fixed(n):
    return bytes(range(n))


key = base64.b64encode(fixed(16)).decode("ascii")

Mit dieser Funktion ist der Schlüssel bei jedem Lauf derselbe: das base64 der Bytes 0 bis 15. Ein Test ruft make_client_key(random_bytes=fixed) auf. Er übergibt fixed, die Funktion selbst, ohne Klammern, und make_client_key entscheidet, wann sie aufgerufen wird und womit.

Warum nimmt make_client_key den Parameter random_bytes?

Die Anfrage

build_upgrade_request(host, path, key) schreibt die Anfrage, die du in Lektion 1 gelesen hast, ohne die beiden optionalen Zeilen:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Das ist die Anfrage für Host, Pfad und Schlüssel des RFC; deine Funktion setzt ihre eigenen drei an deren Stelle. host ist Text wie server.example.com oder ein Name mit wie 127.0.0.1:50123, wenn der Server nicht auf dem Standardport läuft, und er kommt unverändert in die Host-Zeile. path kommt unverändert in die Anfragezeile, samt Query. Jede Zeile endet mit \r\n, eine Leerzeile beendet den Kopf, und die Anfrage hat keinen Body. Dein Client sendet keine Origin-Zeile, weil er kein Browser ist, und keine Sec-WebSocket-Protocol-Zeile, weil er nach keinem Subprotokoll fragt. Gib die Anfrage mit "ascii" kodiert zurück.

Die Antwort prüfen

Der Server antwortet mit einem Kopf, den read_head ohne sein abschließendes \r\n\r\n zurückgibt. check_accept_response(data, key) entscheidet, ob dieser Kopf Ja bedeutet. Sie gibt nichts zurück, wenn das so ist, und löst mit einer Meldung aus, die den Grund nennt, wenn nicht.

Beginne mit der Statuszeile, geschnitten mit split(" ", 2) wie in with Python, Lektion 1.4. Eine Zeile wie HTTP/1.1 allein hat überhaupt keinen Code; prüfe, wie viele Teile du bekommen hast, bevor du den zweiten liest, und löse ValueError aus, wenn der Code fehlt.

Jeder andere Status als 101 bedeutet für diesen Client, dass der gescheitert ist. RFC 6455 sagt, dass ein Client jeden anderen Status “per HTTP procedures” behandelt, also nach den Verfahren von HTTP: Eine 401 könnte zu einer Anmeldung führen, eine 3xx zu einer . Dein Client folgt keinen Weiterleitungen und sendet keine Zugangsdaten, also ist für ihn jeder andere Status, eine 426 oder eine 200 oder eine 100 Continue, ein gescheiterter Handshake. Vergleiche den Code genau mit "101"; ein anderer 1xx-Status ist keine Umstellung.

Die Wörter nach dem Code, die Reason-Phrase, sind für Menschen. Ein Server darf 101 Web Socket Protocol Handshake statt 101 Switching Protocols schreiben, und der Handshake ist genauso gut. Entscheide anhand des Codes und der Header, nie anhand der Reason-Phrase.

Die Antwort ist HTTP/1.1 426 Upgrade Required. Was tut check_accept_response?

Ein Server antwortet mit HTTP/1.1 101 Web Socket Protocol Handshake und korrekten Headern. Ist das ein Erfolg?

Lies bei einer 101 die Header mit deinem parse_headers und prüfe drei davon, die Gegenseite dessen, was du in Lektion 4 geprüft hast:

  1. upgrade hat das Token websocket, per has_token, also besteht WebSocket und websockets nicht;

  2. connection hat das Token upgrade, per has_token;

  3. sec-websocket-accept ist genau gleich accept_value(key) für den Schlüssel, den dieser Client gesendet hat.

Der letzte Vergleich beachtet absichtlich Groß- und Kleinschreibung. In base64 sind a und A verschiedene Zeichen, die für verschiedene Bits stehen, also ist ein Accept-Wert, der sich nur in der Schreibung unterscheidet, ein anderer Wert, und er bedeutet, dass der Server die Antwort auf diesen Schlüssel nicht berechnet hat.

Deine Aufgabe

Schreibe in ws_handshake.py diese drei Funktionen, in dieser Reihenfolge:

  1. make_client_key(random_bytes=os.urandom): Rufe random_bytes(16) einmal auf und gib das base64 des Ergebnisses als str zurück.

  2. build_upgrade_request(host, path, key): die Anfrage von oben, als Bytes.

  3. check_accept_response(data, key): Löse ValueError aus, außer wenn data ein 101-Kopf mit den beiden Tokens und dem richtigen Accept-Wert ist.

Lass serve_handshake und client_handshake für Lektion 6. Klicke auf Run. Der Abschnitt zu Lektion 5 gibt die Anfrage für Host, Pfad und Schlüssel des RFC als Bytes aus:

Lesson 5: build_upgrade_request
b'GET /chat HTTP/1.1\r\nHost: server.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n'

Diese Zeile stammt aus einem lokalen Lauf der fertigen Lektion. Wenn deine übereinstimmt, klicke auf Submit.

Du hast jetzt beide Hälften des Handshakes, jede für sich geprüft. Im Projekt, das dieses Kapitel abschließt, treffen sie sich über eine echte Verbindung.

Aufgabe

Schreibe drei 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. make_client_key(random_bytes=os.urandom) ruft random_bytes(16) einmal auf und gibt das dieser Bytes als str zurück. Rufe den auf, nie direkt os.urandom, damit ein Aufrufer seine eigene Funktion übergeben kann.

  2. build_upgrade_request(host, path, key) gibt Bytes zurück: GET, path und HTTP/1.1 in der Anfragezeile, dann Host: mit host unverändert, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key: mit key, Sec-WebSocket-Version: 13 und die Leerzeile. Jede Zeile endet mit \r\n. Sende keine Origin- und keine Sec-WebSocket-Protocol-Zeile.

  3. check_accept_response(data, key) nimmt einen Antwortkopf, so wie read_head ihn zurückgibt, ohne das abschließende \r\n\r\n. Dekodiere ihn mit "iso-8859-1" und schneide die Statuszeile mit split(" ", 2). Löse aus, wenn die Zeile keinen Code hat oder der Code nicht genau 101 ist. Lies dann die Header mit parse_headers und löse ValueError aus, außer wenn upgrade das Token websocket hat, connection das Token upgrade hat und sec-websocket-accept genau gleich accept_value(key) ist. Gib nichts zurück.

Lass serve_handshake und client_handshake für Lektion 6. Klicke auf Run, um die Anfrage für das Beispiel des RFC zu sehen.

Klicke dann auf Submit. Die Prüfung übergibt make_client_key eigene Funktionen, die bekannte Bytes zurückgeben, und zeichnet auf, wonach sie gefragt wurden. Sie zerlegt die Anfragen, die build_upgrade_request für Hosts und Pfade schreibt, die du nicht gesehen hast, also spielt die Reihenfolge der Header keine Rolle. Sie ruft check_accept_response mit 101-Köpfen auf, die bestehen müssen, einer davon mit einer ungewöhnlichen Reason-Phrase und Header-Schreibung, und mit Köpfen, die ValueError auslösen müssen: andere , fehlende oder falsche Tokens und Accept-, die nicht zum Schlüssel gehören. Das Importieren von ws_handshake.py darf nichts ausgeben.