0%

Um einen Protokollwechsel bitten · Übung

Das Upgrade annehmen oder ablehnen

Dein Server kann eine Anfrage lesen und den Accept- für ihren Schlüssel berechnen. Jetzt muss er entscheiden: auf umstellen oder ablehnen. Diese Lektion schreibt diese Entscheidung, check_upgrade(request), die das aus Lektion 2 nimmt und die Bytes der ganzen Antwort zurückgibt.

Das Ja

Für die Beispielanfrage des RFC lautet die Antwort des Servers so, jede Zeile endet mit \r\n:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Der Status 101 Switching Protocols gehört zur Klasse 1xx, die du in with Python kennengelernt hast. Die meisten 1xx- sagen, dass noch mehr HTTP kommt; dieser sagt, dass das, was als Nächstes kommt, kein HTTP mehr ist. Upgrade: websocket und Connection: Upgrade wiederholen, worum der Client gebeten hat, jetzt als Zustimmung des Servers. Sec-WebSocket-Accept trägt den Wert aus Lektion 3. Dann kommt die Leerzeile, und sonst nichts: keine Content-Length, kein Body.

Nach dieser Leerzeile ist HTTP auf dieser Verbindung beendet. Die Bytes, die folgen, in beide Richtungen, sind WebSocket-, und das nächste Kapitel liest sie.

Was darf der Server direkt nach der Leerzeile senden, die seine 101-Antwort beendet?

Dein Server sendet in seiner 101 nie Sec-WebSocket-Protocol oder Sec-WebSocket-Extensions, weil dieser Kurs keins von beiden aushandelt.

Tokens, keine exakten Werte

Bevor er Ja sagt, prüft der Server, dass der Client wirklich gefragt hat. Der Upgrade-Header muss das Token websocket enthalten, und Connection muss das Token upgrade enthalten, beides ohne Rücksicht auf Groß- und Kleinschreibung. Lektion 1 hat gezeigt, warum ein exakter Vergleich scheitert: Connection: keep-alive, Upgrade bittet genauso um das Upgrade wie Connection: Upgrade.

has_token(value, token) beantwortet die Frage für jeden durch Kommas getrennten Header-Wert. Schneide den Wert an jedem Komma, entferne bei jedem Stück die Leerzeichen drumherum, wandle es in Kleinbuchstaben um und vergleiche es mit dem Token, ebenfalls in Kleinbuchstaben:

Was gibt das aus?

value = "keep-alive, Upgrade"
for item in value.split(","):
    print(item.strip().lower())

Es ist verlockend, stattdessen "upgrade" in value.lower() zu schreiben. Das findet upgrade aber auch in upgrades und in jedem längeren Wort, das es enthält. Der Vergleich ganzer Tokens findet nur das Token, also ist has_token("upgrades", "upgrade") gleich False.

Den Schlüssel prüfen

Ein Schlüssel muss das von 16 Bytes sein. base64.b64decode sagt es dir, wenn du ein hinzufügst. Standardmäßig überspringt es kommentarlos Zeichen, die nicht zum base64-Alphabet gehören, also dekodiert ein Schlüssel mit vier ! vor dem des RFC, !!!!dGhlIHNhbXBsZSBub25jZQ==, trotzdem zu den 16 Bytes der Beispiel-Nonce. Mit validate=True lehnt es diesen Schlüssel ab und löst binascii.Error aus. Fehlende Auffüllung löst in beiden Fällen denselben Fehler aus.

binascii.Error ist eine Art von , also fängt except ValueError: um das Dekodieren herum ihn ab. Prüfe nach einem erfolgreichen Dekodieren die Länge mit len: dGhl ist gültiges base64, dekodiert aber nur zu 3 Bytes.

Denk an den doppelten Schlüssel aus Lektion 2. Zwei Sec-WebSocket-Key-Zeilen kommen als ein Wert mit ", " in der Mitte an, und ein Komma und ein Leerzeichen sind keine base64-Zeichen, also lehnt diese Prüfung auch diese Anfrage ab.

Zwei Arten, Nein zu sagen

RFC 6455 erlaubt einem Server, einen mit einem gewöhnlichen HTTP-Fehler abzulehnen, und dieser Kurs verwendet genau zwei.

400 Bad Request beantwortet jede Anfrage, die fehlerhaft oder unvollständig ist. check_upgrade gibt eine 400 zurück, wenn:

  1. die nicht GET ist;

  2. die Version nicht genau HTTP/1.1 ist;

  3. es keinen host-Header gibt;

  4. upgrade kein websocket-Token hat oder fehlt;

  5. connection kein upgrade-Token hat oder fehlt;

  6. sec-websocket-key oder sec-websocket-version fehlt;

  7. der Schlüssel kein striktes base64 ist;

  8. der Schlüssel nicht zu 16 Bytes dekodiert.

Der RFC erlaubt jede Version ab HTTP/1.1, aber keine spätere HTTP-Version verwendet diesen Handshake, also akzeptiert dein Server genau HTTP/1.1.

426 Upgrade Required beantwortet eine Anfrage, die vollständig und wohlgeformt ist, aber nach einer anderen WebSocket-Version als 13 fragt. Sie trägt einen Sec-WebSocket-Version: 13-Header, der dem Client sagt, welche Version dieser Server spricht, sodass ein Client, der mehrere Versionen kennt, es mit dieser erneut versuchen kann. Prüfe die Version zuletzt, nachdem jede 400-Regel bestanden ist. Verwechsle sie nicht mit 505 HTTP Version Not Supported, die sich auf die HTTP-Version in der Anfragezeile bezieht, nicht auf die WebSocket-Version. RFC 6455 schlägt hier 426 vor und verlangt nur den Sec-WebSocket-Version-Header, also ist 426 die Wahl dieses Kurses: Sie nennt den Grund der Ablehnung.

Beide Ablehnungen sind 4xx-Statuscodes, die Klasse, die sagt, dass der Fehler bei der Anfrage des Clients liegt. refuse(status) baut beide: die Statuszeile, bei 426 den Versions-Header, dann Content-Length: 0 und Connection: close, dann die Leerzeile. Content-Length: 0 sagt, dass kein Body folgt, und Connection: close sagt, dass der Server die Verbindung nach dieser Antwort schließt. Die beiden Antworten sind:

HTTP/1.1 400 Bad Request
Content-Length: 0
Connection: close

HTTP/1.1 426 Upgrade Required
Sec-WebSocket-Version: 13
Content-Length: 0
Connection: close

Ein echter Server kann auch mit anderen Statuscodes ablehnen. Das eigene Beispiel des RFC für eine nicht passende Version antwortet mit 400 Bad Request und einem Sec-WebSocket-Version-Header, und ein Server, der prüft, woher eine Verbindung kommt, könnte mit 403 Forbidden antworten, wie es Kapitel 9 tut. Diese beiden decken alles ab, was dein Server jetzt prüft.

Eine Anfrage ist vollständig und gültig, bis auf Sec-WebSocket-Version: 8. Was gibt check_upgrade zurück?

RFC 6455 sagt nicht, in welcher Reihenfolge ein Server diese Regeln prüfen muss, also könnte eine Anfrage mit zwei Fehlern berechtigterweise jeden der beiden Statuscodes bekommen. Dein Server prüft in der Reihenfolge der oben, die Version zuletzt.

Deine Aufgabe

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

  1. has_token(value, token): True, wenn irgendein durch Kommas getrenntes Element von value, ohne Leerzeichen drumherum und in Kleinbuchstaben, gleich token in Kleinbuchstaben ist.

  2. refuse(status): die vollständige 400- oder 426-Antwort von oben, als Bytes.

  3. check_upgrade(request): eine 400 oder eine 426 nach den Regeln oben, sonst die 101-Antwort mit accept_value des Schlüssels, als Bytes. Verwende headers.get(name, "") für einen Header, der fehlen könnte.

Klicke auf Run. Der Abschnitt zu Lektion 4 gibt die 101-Antwort für die Anfrage des RFC als Bytes-Wert aus, mit allen \r\n:

Lesson 4: check_upgrade
b'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n'

Diese Zeile stammt aus einem lokalen Lauf der fertigen Lektion. Wenn deine übereinstimmt, klicke auf Submit. Die Serverseite des Handshakes ist fertig; die nächste Lektion schreibt den Client, der fragt.

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. has_token(value, token) gibt True zurück, wenn irgendein Element des durch Kommas getrennten value, ohne Leerzeichen drumherum und in Kleinbuchstaben, gleich token in Kleinbuchstaben ist, und sonst False. Ein Token innerhalb eines längeren Worts zählt nicht: has_token("upgrades", "upgrade") ist False.

  2. refuse(status) gibt Bytes zurück: bei 400 HTTP/1.1 400 Bad Request; bei 426 HTTP/1.1 426 Upgrade Required, gefolgt von Sec-WebSocket-Version: 13; dann Content-Length: 0, Connection: close und die Leerzeile. Jede Zeile endet mit \r\n.

  3. check_upgrade(request) nimmt ein in der Form, die parse_upgrade_request zurückgibt, und gibt Bytes zurück. Es gibt refuse(400) zurück, wenn die nicht GET ist, die Version nicht genau HTTP/1.1 ist, host fehlt, upgrade kein websocket-Token hat, connection kein upgrade-Token hat, der Schlüssel oder der Versions-Header fehlt oder der Schlüssel kein striktes (base64.b64decode(key, validate=True)) von genau 16 Bytes ist. Andernfalls gibt es refuse(426) zurück, wenn die Version nicht 13 ist, und die 101-Antwort, wenn sie es ist: HTTP/1.1 101 Switching Protocols, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Accept: und accept_value(key), dann die Leerzeile und nichts danach.

Lass die Funktionen für die Lektionen 5 und 6, wie sie sind. Klicke auf Run, um die 101-Antwort für die Anfrage des RFC zu sehen.

Klicke dann auf Submit. Die Prüfung ruft has_token mit auf, die du nicht gesehen hast, und check_upgrade mit Anfrage-Dictionaries, die sie selbst baut, also hängt sie nicht von deinem Parser ab: die Anfrage des RFC, eine gültige Anfrage mit einem Schlüssel, den du nicht gesehen hast, und den Tokens in anderer Schreibweise, zehn Anfragen, die eine 400 bekommen müssen, jede mit genau einem Fehler, und eine, die eine 426 bekommen muss. Sie liest jede Antwort so, wie ein Client es tun würde: die Statuszeile, die Header nach ihrem Namen in Kleinbuchstaben, die Leerzeile am Ende und entweder keinen Body oder einen Body, der so lang ist wie seine Content-Length. Die Reihenfolge der Header spielt keine Rolle, und eine Ablehnung darf zusätzliche Header oder einen kurzen Body mit der richtigen Content-Length tragen; eine 101 darf weder einen Body noch einen Protokoll- oder Extensions-Header tragen. Das Importieren von ws_handshake.py darf nichts ausgeben.