0%

Masken und Steuerframes · Übung

Ping, Pong und andere Steuerframes

Dein Codec liest und schreibt jetzt maskierte und unmaskierte in beide Richtungen, aber er behandelt jeden gleich. Ein Close, ein oder ein Pong geht genauso durch decode_frame wie eine Textnachricht. Diese drei sind anders: Sie sind Steuerframes, die die Verbindung selbst verwalten, statt die Nachrichten der Anwendung zu tragen, und RFC 6455 unterwirft sie strengeren Regeln.

Die obere Hälfte der Opcode-Tabelle

Kapitel 3 hat die sechzehn Opcodes in zwei Gruppen aufgeteilt. Die untere Hälfte, 0x0 bis 0x7, ist für Daten: Fortsetzung, Text, binär und fünf reservierte . Die obere Hälfte, 0x8 bis 0xF, ist für die Steuerung:

OpcodeFrame
0x8Close
0x9Ping
0xAPong
0xB bis 0xFreserviert für zukünftige Steuerframes

Jeder Opcode in der oberen Hälfte hat das Bit 0x8 gesetzt, und keiner in der unteren Hälfte, also ist “ist das ein Steuerframe?” ein einziger Vergleich: Der Opcode ist 8 oder größer. parse_header_bytes lehnt die reservierten Opcodes bereits ab, also ist ein Steuerframe in der Praxis ein Close, ein Ping oder ein Pong.

Drei Regeln für Steuerframes

RFC 6455, Abschnitt 5.5, gibt Steuerframes drei Regeln, die Daten-Frames nicht haben:

  1. Höchstens 125 Bytes Payload. Ein Steuerframe verwendet also immer die 7-Bit-Länge; die Markierungen 126 und 127 kommen in einem nie vor.

  2. Nie fragmentiert. Ein Steuerframe hat immer FIN gesetzt. Lektion 5 zeigt, was Fragmente sind; ein Steuerframe ist nie eines.

  3. Zwischen Fragmenten erlaubt. Ein Steuerframe darf mitten in einer Nachricht ankommen, die auf mehrere Frames verteilt ist. Lektion 5 zeigt, warum das wichtig ist.

Die ersten beiden lassen sich allein anhand der ersten beiden Bytes prüfen. Byte 0 enthält FIN und den Opcode, und Byte 1 enthält die 7-Bit-Länge, die nur dann über 125 liegt, wenn sie eine der Markierungen ist. Also kann decode_frame einen Steuerframe, der eine der beiden Regeln verletzt, direkt nach parse_header_bytes ablehnen, bevor es mehr liest. Wie bei der Größengrenze in Kapitel 3 bedeutet die Ablehnung anhand des Headers, dass eine Gegenseite dein Programm nicht auf eine Payload warten oder eine Payload halten lassen kann, die es ohnehin ablehnen wird. Der Encoder folgt denselben Regeln, also kann dein eigener Code nie versehentlich einen solchen Frame bauen.

Ein Frame beginnt mit 89 7e 00 c8. Was tut der Decoder?

Ping und Pong

Ein Ping fragt die andere Seite: “Bist du noch da?”. Die andere Seite antwortet mit einem Pong, der genau dieselbe Payload trägt, die Anwendungsdaten des Pings (Abschnitte 5.5.2 und 5.5.3). Der RFC sagt, dass der Empfänger “as soon as is practical” antworten soll, also so bald wie praktikabel; er verspricht keine bestimmte Zeit. Abschnitt 5.7 druckt einen Ping ab, der “Hello” trägt, und den maskierten Pong, den ein Client mit dem Schlüssel 37 fa 21 3d zurücksendet, die erste und die letzte Zeile unten. Die mittlere Zeile steht nicht im RFC: Sie ist der unmaskierte Pong, den ein Server senden würde, nach denselben Regeln berechnet:

89 05 48 65 6c 6c 6f
8a 05 48 65 6c 6c 6f
8a 85 37 fa 21 3d 7f 9f 4d 51 58

Jede Seite darf einen Ping senden, und die Payload wählt der Sender, oft ist sie leer. Der RFC erlaubt auch einen Pong, nach dem niemand gefragt hat, einen unaufgeforderten Pong, der als einseitiges “Ich bin noch da” funktioniert und keine Antwort braucht. Kapitel 8 verwendet Pings, um eine Gegenseite zu bemerken, die verstummt ist; hier beantwortest du sie nur.

Dein Client empfängt einen Pong, nach dem er nie gefragt hat. Was sollte er tun?

Wer den Pong maskiert

Ein Pong ist ein Frame wie jeder andere, also gilt die Richtungsregel auch für ihn. Der Pong eines Clients ist maskiert, wie jeder Client-Frame, und der Pong eines Servers nicht. Deshalb nimmt reply_to_ping(frame, *, mask_key=None) einen mask_key und gibt ihn an encode_frame weiter: Ein Server ruft es ohne Schlüssel auf, und ein Client ruft es mit einem neuen auf. Das frame ist das , das decode_frame zurückgibt, also ist die Payload des Pongs frame["payload"], und ein Frame, dessen Opcode kein Ping ist, hat nichts, worauf man antworten könnte.

Was gibt das aus?

print(0x89 & 0x0F, (0x8A & 0x0F) >= 8)

Deine Aufgabe

In ws_frames.py:

  1. Schreibe is_control(opcode): True für einen Opcode von 8 oder mehr, darunter False.

  2. Schreibe reply_to_ping(frame, *, mask_key=None): Löse aus, außer wenn frame["opcode"] gleich OP_PING ist, und gib dann encode_frame(OP_PONG, frame["payload"], mask_key=mask_key) zurück.

  3. Löse in decode_frame direkt nach der Richtungsregel ValueError aus für einen Steuerframe, dessen 7-Bit-Länge über 125 liegt oder dessen FIN-Bit nicht gesetzt ist. Die Prüfung muss kommen, bevor die Payload gelesen wird.

  4. Löse in encode_frame ValueError aus für einen Steuerframe mit mehr als 125 Bytes Payload oder mit fin=False.

Daten-Frames behalten ihre alten Regeln: Ein Text-Frame mit 126 Bytes oder einer mit nicht gesetztem FIN wird weiterhin kodiert und dekodiert.

Klicke auf Run. Der Abschnitt zu Lektion 3 dekodiert den Ping des RFC so, wie ein Client ihn liest, und beantwortet ihn dann so, wie ein Client es muss, mit deinem reply_to_ping und dem Schlüssel des RFC. Das Ergebnis ist der maskierte Pong des RFC:

Lesson 3: the client's pong for the server's ping
8a 85 37 fa 21 3d 7f 9f 4d 51 58
Lesson 4: close 1000 and back
  not written yet

Klicke dann auf Submit. Die Prüfung ruft is_control für alle sechzehn Opcodes auf. Sie dekodiert Pings und Pongs mit bis zu 125 Bytes, maskiert und unmaskiert, am Stück, ein Byte und jeweils zwei Bytes auf einmal geliefert, und erwartet ValueError für einen Ping oder Pong, der mehr ankündigt, und für einen Ping oder Close mit nicht gesetztem FIN, ohne Lesevorgang für die Payload. Sie verlangt von encode_frame Steuerframes, die die Regeln verletzen, beantwortet Pings, die du nicht gesehen hast, mit und ohne Schlüssel, und erwartet ValueError von reply_to_ping für einen Pong, einen Text-Frame und einen Close. Der Close-Frame ist der einzige Steuerframe, dessen Payload etwas bedeutet; die nächste Lektion liest ihn.

Aufgabe

In ws_frames.py:

  1. Schreibe is_control(opcode): True für einen von 8 oder mehr (Close, , Pong und die reservierten Steuer-Opcodes), darunter False.

  2. Schreibe reply_to_ping(frame, *, mask_key=None): frame ist ein , wie decode_frame es zurückgibt. Löse aus, außer wenn sein Opcode OP_PING ist; gib andernfalls encode_frame(OP_PONG, frame["payload"], mask_key=mask_key) zurück.

  3. Löse in decode_frame ValueError aus für einen Steuerframe, dessen 7-Bit-Länge über 125 liegt oder dessen FIN-Bit nicht gesetzt ist, bevor seine Payload gelesen wird.

  4. Löse in encode_frame ValueError aus für einen Steuerframe mit mehr als 125 Payload-Bytes oder fin=False.

Klicke auf Run: Der Abschnitt zu Lektion 3 sollte 8a 85 37 fa 21 3d 7f 9f 4d 51 58 ausgeben. Klicke dann auf Submit. Die Prüfung ruft is_control für jeden Opcode von 0 bis 15 auf; dekodiert Pings und Pongs mit bis zu 125 Bytes, maskiert und unmaskiert, am Stück, ein Byte und jeweils zwei Bytes auf einmal; erwartet ValueError ohne Lesevorgang für die Payload bei einem Ping oder Pong, der mehr als 125 Bytes ankündigt, und bei einem Ping oder Close mit nicht gesetztem FIN; erwartet, dass Text- mit 126 Bytes oder mit nicht gesetztem FIN weiterhin funktionieren; erwartet ValueError von encode_frame für einen 126 Byte langen Ping oder Pong und einen Close mit fin=False; und beantwortet den Ping des RFC, einen leeren Ping und einen unbekannten 125 Byte langen Ping mit und ohne Schlüssel, wobei sie ValueError für einen Pong, einen Text-Frame oder einen Close erwartet.