0%

Vragen om van protocol te wisselen · oefening

De upgrade accepteren of weigeren

Je server kan een verzoek lezen en de acceptwaarde voor zijn sleutel berekenen. Nu moet hij beslissen: overschakelen naar , of weigeren. Deze les schrijft die beslissing, check_upgrade(request), die de uit les 2 aanneemt en de bytes van het hele antwoord teruggeeft.

Het ja

Voor het voorbeeldverzoek van de RFC is dit het antwoord van de server, met elke regel eindigend op \r\n:

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

De status 101 Switching Protocols hoort bij de klasse 1xx die je in with Python tegenkwam. De meeste 1xx-statussen zeggen dat er meer HTTP komt; deze zegt dat wat er nu komt geen HTTP meer is. Upgrade: websocket en Connection: Upgrade herhalen waar de client om vroeg, nu als instemming van de server. Sec-WebSocket-Accept draagt de uit les 3. Dan komt de lege regel, en verder niets: geen Content-Length, geen body.

Na die lege regel is HTTP op deze verbinding klaar. De bytes die volgen, in beide richtingen, zijn WebSocket-, en het volgende hoofdstuk leest ze.

Wat mag de server direct na de lege regel die zijn 101-antwoord beëindigt sturen?

Je server stuurt nooit Sec-WebSocket-Protocol of Sec-WebSocket-Extensions in zijn 101, omdat deze cursus over geen van beide onderhandelt.

Tokens, geen exacte waarden

Voordat hij ja zegt, controleert de server dat de client er echt om vroeg. De Upgrade-header moet het token websocket bevatten, en Connection moet het token upgrade bevatten, allebei zonder op hoofdletters te letten. Les 1 liet zien waarom een exacte vergelijking faalt: Connection: keep-alive, Upgrade vraagt net zo goed om de upgrade als Connection: Upgrade.

has_token(value, token) beantwoordt de vraag voor elke door komma’s gescheiden headerwaarde. Knip de waarde bij elke komma, strip elk stuk, zet het in kleine letters, en vergelijk het met het token, ook in kleine letters:

Wat drukt dit af?

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

Het is verleidelijk om in plaats daarvan "upgrade" in value.lower() te schrijven. Dat vindt upgrade ook in upgrades, en in elk langer woord dat het bevat. Hele tokens vergelijken vindt alleen het token, dus has_token("upgrades", "upgrade") is False.

De sleutel controleren

Een sleutel moet de van 16 bytes zijn. base64.b64decode vertelt het je, met één extra . Standaard slaat het tekens die niet bij het base64-alfabet horen zonder iets te zeggen over, dus een sleutel met vier ! voor die van de RFC, !!!!dGhlIHNhbXBsZSBub25jZQ==, decodeert nog steeds naar de 16 bytes van de sample nonce. Met validate=True weigert het die sleutel en werpt het binascii.Error op. Ontbrekende opvulling werpt hoe dan ook dezelfde fout op.

binascii.Error is een soort , dus except ValueError: rond het decoderen vangt hem. Controleer na een geslaagde decodering de lengte met len: dGhl is geldige base64 maar decodeert naar maar 3 bytes.

Denk aan de dubbele sleutel uit les 2. Twee Sec-WebSocket-Key-regels komen aan als één waarde met ", " in het midden, en een komma en een spatie zijn geen base64-tekens, dus deze controle weigert dat verzoek ook.

Twee manieren om nee te zeggen

RFC 6455 laat een server een weigeren met een gewone HTTP-fout, en deze cursus gebruikt er precies twee.

400 Bad Request beantwoordt elk verzoek dat misvormd of onvolledig is. check_upgrade geeft een 400 terug als:

  1. de niet GET is;

  2. de versie niet precies HTTP/1.1 is;

  3. er geen host-header is;

  4. upgrade geen websocket-token heeft, of ontbreekt;

  5. connection geen upgrade-token heeft, of ontbreekt;

  6. sec-websocket-key of sec-websocket-version ontbreekt;

  7. de sleutel geen strikte base64 is;

  8. de sleutel niet naar 16 bytes decodeert.

De RFC staat elke versie vanaf HTTP/1.1 toe, maar geen latere versie van HTTP gebruikt deze handshake, dus je server accepteert precies HTTP/1.1.

426 Upgrade Required beantwoordt een verzoek dat volledig en goed gevormd is, maar om een andere WebSocket-versie dan 13 vraagt. Het draagt een Sec-WebSocket-Version: 13-header, die de client vertelt welke versie deze server spreekt, zodat een client die meerdere versies kent het met die versie opnieuw kan proberen. Controleer de versie als laatste, nadat elke 400-regel is geslaagd. Verwar het niet met 505 HTTP Version Not Supported, dat over de HTTP-versie in de verzoekregel gaat, niet over de WebSocket-versie. RFC 6455 stelt hier 426 voor en eist alleen de Sec-WebSocket-Version-header, dus 426 is de keuze van deze cursus: het noemt de reden voor de weigering.

Beide weigeringen zijn 4xx-statussen, de klasse die zegt dat het verzoek van de client de fout is. refuse(status) bouwt ze allebei: de statusregel, voor 426 de versieheader, dan Content-Length: 0 en Connection: close, en dan de lege regel. Content-Length: 0 zegt dat er geen body volgt, en Connection: close zegt dat de server de verbinding na dit antwoord sluit. De twee antwoorden zijn:

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

Een echte server kan ook met andere statussen weigeren. Het eigen voorbeeld van de RFC voor een versieverschil antwoordt 400 Bad Request met een Sec-WebSocket-Version-header, en een server die controleert waar een verbinding vandaan komt, zou 403 Forbidden kunnen antwoorden, zoals hoofdstuk 9 doet. Deze twee dekken alles wat je server nu controleert.

Een verzoek is volledig en geldig, behalve Sec-WebSocket-Version: 8. Wat geeft check_upgrade terug?

RFC 6455 zegt niet in welke volgorde een server deze regels moet controleren, dus een verzoek met twee fouten kan terecht elk van beide statussen krijgen. Je server controleert in de volgorde van de hierboven, versie als laatste.

Je opdracht

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

  1. has_token(value, token): True als een door komma’s gescheiden item van value, gestript en in kleine letters, gelijk is aan token in kleine letters.

  2. refuse(status): het volledige 400- of 426-antwoord hierboven, als bytes.

  3. check_upgrade(request): een 400 of een 426 volgens de regels hierboven, anders het 101-antwoord met accept_value van de sleutel, als bytes. Gebruik headers.get(name, "") voor een header die kan ontbreken.

Druk op Run. De sectie van les 4 drukt het 101-antwoord voor het verzoek van de RFC af als bytes-waarde, met \r\n en al:

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'

Die regel komt uit een lokale run van de afgewerkte les. Als die van jou overeenkomt, druk je op Submit. De serverkant van de handshake is af; de volgende les schrijft de client die erom vraagt.

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. has_token(value, token) geeft True terug als een item van de door komma’s gescheiden value, gestript en in kleine letters, gelijk is aan token in kleine letters, en anders False. Een token in een langer woord telt niet: has_token("upgrades", "upgrade") is False.

  2. refuse(status) geeft bytes terug: voor 400 HTTP/1.1 400 Bad Request; voor 426 HTTP/1.1 426 Upgrade Required gevolgd door Sec-WebSocket-Version: 13; daarna Content-Length: 0, Connection: close en de lege regel. Elke regel eindigt op \r\n.

  3. check_upgrade(request) neemt een aan in de vorm die parse_upgrade_request teruggeeft en geeft bytes terug. Het geeft refuse(400) terug als de niet GET is, de versie niet precies HTTP/1.1 is, host ontbreekt, upgrade geen websocket-token heeft, connection geen upgrade-token heeft, de sleutel of de versieheader ontbreekt, of de sleutel geen strikte (base64.b64decode(key, validate=True)) van precies 16 bytes is. Anders geeft het refuse(426) terug als de versie niet 13 is, en het 101-antwoord als ze dat wel is: HTTP/1.1 101 Switching Protocols, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Accept: en accept_value(key), dan de lege regel en niets erna.

Laat de functies voor les 5 en 6 zoals ze zijn. Druk op Run om het 101-antwoord voor het verzoek van de RFC te zien.

Druk daarna op Submit. De controle roept has_token aan op die je nog niet hebt gezien, en check_upgrade met verzoekdictionary’s die ze zelf bouwt, zodat ze niet van je parser afhangt: het verzoek van de RFC, een geldig verzoek met een sleutel die je nog niet hebt gezien en de tokens anders geschreven, tien verzoeken die een 400 moeten krijgen, elk met precies één fout, en één die een 426 moet krijgen. Ze leest elk antwoord zoals een client dat zou doen: de statusregel, de headers op naam in kleine letters, de lege regel aan het eind, en ofwel geen body ofwel een body zo lang als zijn Content-Length. De volgorde van de headers maakt niet uit, en een weigering mag extra headers of een korte body met de juiste Content-Length hebben; een 101 mag geen body en geen protocol- of extensieheader hebben. Het importeren van ws_handshake.py mag niets afdrukken.