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 os.urandom(16) gibt 16 Bytes aus der Quelle unvorhersehbarer Zufälligkeit des 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
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 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 host ist Text wie server.example.com oder ein Name mit 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
Beginne mit der Statuszeile, geschnitten mit split(" ", 2) wie in 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 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:
upgradehat das Tokenwebsocket, perhas_token, also bestehtWebSocketundwebsocketsnicht;connectionhat das Tokenupgrade, perhas_token;sec-websocket-acceptist genau gleichaccept_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:
make_client_key(random_bytes=os.urandom): Ruferandom_bytes(16)einmal auf und gib das base64 des Ergebnisses alsstrzurück.build_upgrade_request(host, path, key): die Anfrage von oben, als Bytes.check_accept_response(data, key): LöseValueErroraus, außer wenndataein 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 ws_handshake.py, in dieser Reihenfolge. Submit nennt die erste, die noch NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.
make_client_key(random_bytes=os.urandom)ruftrandom_bytes(16)einmal auf und gibt dasdieser Bytes als strzurück. Rufe denauf, nie direkt os.urandom, damit ein Aufrufer seine eigene Funktion übergeben kann.build_upgrade_request(host, path, key)gibt Bytes zurück:GET,pathundHTTP/1.1in der Anfragezeile, dannHost:mithostunverändert,Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-Key:mitkey,Sec-WebSocket-Version: 13und die Leerzeile. Jede Zeile endet mit\r\n. Sende keineOrigin- und keineSec-WebSocket-Protocol-Zeile.check_accept_response(data, key)nimmt einen Antwortkopf, so wieread_headihn zurückgibt, ohne das abschließende\r\n\r\n. Dekodiere ihn mit"iso-8859-1"und schneide die Statuszeile mitsplit(" ", 2). Löseaus, wenn die Zeile keinen Code hat oder der Code nicht genau 101ist. Lies dann die Header mitparse_headersund löseValueErroraus, außer wennupgradedas Tokenwebsockethat,connectiondas Tokenupgradehat undsec-websocket-acceptgenau gleichaccept_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 ws_handshake.py darf nichts ausgeben.