0%

Vragen om van protocol te wisselen · oefening

De helft van de client schrijven

Je server kan een verzoek parsen, beslissen en antwoorden. Nog niets stuurt hem een verzoek. Deze les schrijft de andere kant: de client die een sleutel maakt, het upgradeverzoek schrijft, en het antwoord van de server controleert voordat hij gelooft dat het protocol is omgeschakeld.

Een nieuwe sleutel

De sleutel van de client is 16 willekeurige bytes in , nieuw voor elk verzoek. Python’s os.urandom(16) geeft 16 bytes terug uit de bron van onvoorspelbare willekeur van het , en dat is de juiste bron voor die een ander programma niet mag kunnen raden. base64.b64encode maakt er de 24 tekens van een sleutel van, en .decode("ascii") maakt daar tekst van.

Een test kan een willekeurige sleutel niet controleren, omdat hij die niet van tevoren kan weten. Daarom neemt make_client_key de bron van willekeur aan als , met de echte als standaardwaarde:

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

Roep daarbinnen random_bytes(16) aan. Bij normaal gebruik roept dat os.urandom(16) aan. Een test geeft in plaats daarvan een andere mee, een die bekende bytes teruggeeft, en weet dan precies welke sleutel eruit komt. Dit is hetzelfde idee als de connect=-parameter die je in Networks with Python tegenkwam: een functie die als waarde wordt meegegeven, zodat een test er zijn eigen functie voor in de plaats kan zetten.

import base64


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


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

Met die functie is de sleutel bij elke run hetzelfde: de base64 van de bytes 0 tot en met 15. Een test roept make_client_key(random_bytes=fixed) aan. Hij geeft fixed mee, de functie zelf, zonder haakjes, en make_client_key beslist wanneer hij haar aanroept en met wat.

Waarom neemt make_client_key random_bytes aan als parameter?

Het verzoek

build_upgrade_request(host, path, key) schrijft het verzoek dat je in les 1 las, zonder de twee optionele regels:

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

Dat is het verzoek voor de host, het pad en de sleutel van de RFC; je functie zet haar eigen drie op hun plaats. host is tekst zoals server.example.com, of een naam en een zoals 127.0.0.1:50123 als de server niet op de standaardpoort zit, en gaat zoals gegeven in de Host-regel. path gaat zoals gegeven in de verzoekregel, query en al. Elke regel eindigt op \r\n, een lege regel beëindigt de kop, en het verzoek heeft geen body. Je client stuurt geen Origin-regel, omdat hij geen browser is, en geen Sec-WebSocket-Protocol-regel, omdat hij om geen subprotocol vraagt. Geef het verzoek gecodeerd met "ascii" terug.

Het antwoord controleren

De server antwoordt met een kop, die read_head teruggeeft zonder de laatste \r\n\r\n. check_accept_response(data, key) beslist of die kop ja betekent. Het geeft niets terug als dat zo is, en werpt op met een melding die zegt waarom als dat niet zo is.

Begin met de statusregel, geknipt met split(" ", 2) zoals in with Python les 1.4. Een regel zoals alleen HTTP/1.1 heeft helemaal geen code; controleer hoeveel delen je kreeg voordat je het tweede leest, en werp ValueError op als de code ontbreekt.

Elke andere status dan 101 betekent voor deze client dat de mislukt is. RFC 6455 zegt dat een client elke andere status behandelt “volgens de HTTP-procedures”: een 401 kan tot inloggen leiden, een 3xx tot een . Je client volgt geen doorverwijzingen en stuurt geen inloggegevens, dus voor hem is elke andere status, een 426 of een 200 of een 100 Continue, een mislukte handshake. Vergelijk de code precies met "101"; een andere 1xx-status is geen omschakeling.

De woorden na de code, de reason phrase, zijn voor mensen. Een server mag 101 Web Socket Protocol Handshake schrijven in plaats van 101 Switching Protocols, en de handshake is net zo goed. Beslis op de code en de headers, nooit op de reason phrase.

Het antwoord is HTTP/1.1 426 Upgrade Required. Wat doet check_accept_response?

Een server antwoordt HTTP/1.1 101 Web Socket Protocol Handshake met de juiste headers. Is dat een succes?

Lees bij een 101 de headers met je parse_headers en controleer er drie, de serverkant van wat je in les 4 controleerde:

  1. upgrade heeft het token websocket, via has_token, dus WebSocket komt erdoor en websockets niet;

  2. connection heeft het token upgrade, via has_token;

  3. sec-websocket-accept is precies gelijk aan accept_value(key) voor de sleutel die deze client stuurde.

De laatste vergelijking let met opzet op hoofdletters. In base64 zijn a en A verschillende tekens die voor verschillende bits staan, dus een acceptwaarde die alleen in hoofdletters verschilt, is een andere waarde, en betekent dat de server het antwoord op deze sleutel niet heeft berekend.

Je opdracht

Schrijf in ws_handshake.py deze drie functies, in deze volgorde:

  1. make_client_key(random_bytes=os.urandom): roep random_bytes(16) één keer aan en geef de base64 van het resultaat terug als str.

  2. build_upgrade_request(host, path, key): het verzoek hierboven, als bytes.

  3. check_accept_response(data, key): werp ValueError op tenzij data een 101-kop is met de twee tokens en de juiste acceptwaarde.

Laat serve_handshake en client_handshake voor les 6. Druk op Run. De sectie van les 5 drukt het verzoek voor de host, het pad en de sleutel van de RFC af, als bytes:

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'

Die regel komt uit een lokale run van de afgewerkte les. Als die van jou overeenkomt, druk je op Submit.

Je hebt nu beide helften van de handshake, elk apart gecontroleerd. In het project dat dit hoofdstuk afsluit, ontmoeten ze elkaar over een echte verbinding.

Opdracht

Schrijf drie in ws_handshake.py, in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt, dus je krijgt ze één voor één.

  1. make_client_key(random_bytes=os.urandom) roept random_bytes(16) één keer aan en geeft de van die bytes terug als str. Roep de aan, nooit rechtstreeks os.urandom, zodat een aanroeper zijn eigen functie kan meegeven.

  2. build_upgrade_request(host, path, key) geeft bytes terug: GET, path en HTTP/1.1 op de verzoekregel, daarna Host: met host zoals gegeven, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key: met key, Sec-WebSocket-Version: 13, en de lege regel. Elke regel eindigt op \r\n. Stuur geen Origin- en geen Sec-WebSocket-Protocol-regel.

  3. check_accept_response(data, key) neemt een antwoordkop aan zoals read_head hem teruggeeft, zonder de laatste \r\n\r\n. Decodeer hem met "iso-8859-1" en knip de statusregel met split(" ", 2). Werp op als de regel geen code heeft of de code niet precies 101 is. Lees daarna de headers met parse_headers en werp ValueError op tenzij upgrade het token websocket heeft, connection het token upgrade heeft, en sec-websocket-accept precies gelijk is aan accept_value(key). Geef niets terug.

Laat serve_handshake en client_handshake voor les 6. Druk op Run om het verzoek voor het voorbeeld van de RFC te zien.

Druk daarna op Submit. De controle geeft make_client_key eigen functies mee die bekende bytes teruggeven en noteert waarmee ze werden aangeroepen. Ze parset de verzoeken die build_upgrade_request schrijft voor hosts en paden die je nog niet hebt gezien, dus de volgorde van de headers maakt niet uit. Ze roept check_accept_response aan op 101-koppen die moeten slagen, een ervan met een ongebruikelijke reason phrase en ongebruikelijke hoofdletters in de headers, en op koppen die ValueError moeten opwerpen: andere statussen, ontbrekende of verkeerde tokens, en acceptwaarden die niet bij de sleutel horen. Het importeren van ws_handshake.py mag niets afdrukken.