0%

Vragen om van protocol te wisselen · oefening

Het upgradeverzoek parsen

De vorige les las het upgradeverzoek van de RFC met het oog. Je server moet het met code lezen: de bytes van een verzoekkop omzetten in een met de , het pad, de versie en de headers. In with Python parsete je zo de kop van een antwoord. Een verzoek verschilt op twee punten: het begint met een verzoekregel in plaats van een statusregel, en deze cursus bewaart elke header als één in plaats van een .

Het project

Vanaf deze les staat de code van het hoofdstuk in een project op de servers van Python Land, en de bestanden blijven van les tot les bewaard. De projecteditor toont twee bestanden:

  • ws_handshake.py is waar je werkt. Het bevat de constanten GUID en MAX_HEAD, twee afgewerkte , en een stub voor elke functie die dit hoofdstuk vraagt, waarvan de docstring de les noemt die hem schrijft.

  • handshake_demo.py is het programma dat de knop Run start. Het roept de functie van elke les aan op het voorbeeld van de RFC en drukt het resultaat af, of not written yet voor een stub. Het opent geen verbinding tot les 6.

De afgewerkte functies ken je. make_reader(sock) omhult een met een ontvangstbuffer, zoals in Networks with Python, en read_head(reader) is je functie uit HTTP with Python les 1.3: hij geeft de bytes vóór de lege regel terug, zonder de \r\n\r\n, en houdt wat erna komt in de buffer. Dat is de kop die je parser ontvangt.

Van bytes naar regels

De eerste stappen zijn die uit HTTP with Python les 1.4. Decodeer de bytes met "iso-8859-1", dat elke byte in één teken omzet en nooit faalt, en splits de tekst bij "\r\n". De eerste regel is de verzoekregel; de rest zijn .

Knip de verzoekregel bij enkele spaties in drie delen met split(" ", 2): methode, pad en versie. Het getal 2 betekent hooguit twee knippen, dus een versie met iets erachter, zoals in GET /chat HTTP/1.1 extra, blijft in het derde deel, waar je het kunt zien en weigeren.

Headers in een dict

Een headerregel is een naam, een dubbele punt en een . Knip hem alleen bij de eerste dubbele punt, omdat een waarde zelf dubbele punten kan bevatten: een Host-header met een , zoals Host: example.com:8080, heeft er een in zijn waarde.

Wat drukt dit af?

print("Host: example.com:8080".split(":", 1))

De waarde houdt de spatie die na de dubbele punt kwam, en ze kan ook spaties aan het eind hebben, dus pas .strip() erop toe. Headernamen worden vergeleken zonder op hoofdletters te letten, dus sla elke naam op met .lower(). Laat de hoofdletters van de waarden met : Upgrade: WebSocket houdt zijn hoofdletters in de dictionary, en les 4 vergelijkt tokens zonder op hoofdletters te letten als ze ze controleert.

parse_headers(lines) doet dit voor een lijst headerregels. parse_upgrade_request(data) decodeert en splitst de kop, knipt de verzoekregel, geeft de andere regels aan parse_headers, en geeft één dictionary terug:

{"method": ..., "path": ..., "version": ..., "headers": {...}}

Een verzoek zonder enige headerregel geeft "headers": {}, dus parse_headers([]) geeft een lege dictionary terug.

Een naam die twee keer voorkomt

HTTP laat een afzender één header over meerdere regels met dezelfde naam verdelen. Connection: keep-alive op de ene regel en Connection: Upgrade op een andere betekenen hetzelfde als Connection: keep-alive, Upgrade. RFC 9110 zegt dat een ontvanger ze zo mag samenvoegen: de waarden in de volgorde waarin ze aankwamen, verbonden door een komma en een spatie.

In HTTP with Python hield je antwoordparser voor elke naam een lijst waarden bij. Deze cursus bewaart in plaats daarvan één string en voegt de waarden van een herhaalde naam samen met ", ", omdat elke header die de leest al een door komma’s gescheiden lijst tokens is. Een latere regel mag een eerdere nooit overschrijven: het Upgrade-token in de tweede Connection-regel is het token waar het om gaat.

Een verzoek heeft Connection: keep-alive en, twee regels later, Connection: Upgrade. Wat moet headers["connection"] bevatten?

Samenvoegen heeft nog een effect waar RFC 6455 op rekent. Een verzoek mag Sec-WebSocket-Key maar één keer bevatten. Als een client hem twee keer stuurt, bevat je dictionary twee sleutels verbonden door ", ", wat geen geldige sleutel is, en de controle van les 4 weigert hem zonder een eigen regel daarvoor.

Wat telt als misvormd

Je parser weigert wat hij niet kan lezen in plaats van te gokken. Werp op voor:

  • een verzoekregel die bij enkele spaties niet in precies drie delen uiteenvalt, zoals GET /chat, of GET /chat HTTP/1.1 extra, waar het derde deel een spatie bevat;

  • een versie die niet met HTTP/ begint, zoals FTP/1.0;

  • een headerregel zonder dubbele punt;

  • een headerregel waarvan de naam leeg is of een spatie bevat, zoals : x of Bad Name: x.

Geef elke ValueError een melding die de geweigerde regel citeert. Echte servers verschillen in wat ze toelaten; sommige accepteren twee spaties achter elkaar in de verzoekregel. Een strikte parser weigert met een reden, en niets wat hij accepteert, is gegokt.

Je opdracht

Schrijf in ws_handshake.py parse_headers(lines) en daarna parse_upgrade_request(data), zoals hierboven beschreven. Laat de functies voor les 3 tot en met 6 zoals ze zijn. Druk daarna op Run. Met je parser op zijn plaats drukt handshake_demo.py de dictionary voor het verzoek van de RFC af, en de secties van de latere lessen zeggen nog not written yet:

Lesson 2: parse_upgrade_request
{'method': 'GET', 'path': '/chat', 'version': 'HTTP/1.1', 'headers': {'host': 'server.example.com', 'upgrade': 'websocket', 'connection': 'Upgrade', 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==', 'origin': 'http://example.com', 'sec-websocket-protocol': 'chat, superchat', 'sec-websocket-version': '13'}}
Lesson 3: accept_value
  not written yet
Lesson 4: check_upgrade
  not written yet
Lesson 5: build_upgrade_request
  not written yet
Lesson 6: run_loopback is not written yet

Die uitvoer komt uit een lokale run van de afgewerkte les. Een naam met hoofdletters of een waarde met spaties eromheen wijst op .lower() of .strip().

Als de dictionary er goed uitziet, druk je op Submit. De controle roept je twee functies aan op verzoekkoppen die je nog niet hebt gezien, met ongebruikelijke hoofdletters, opgevulde waarden, een herhaalde naam en een dubbele punt in een waarde, en op koppen die geweigerd moeten worden.

Niets in de dictionary zegt al of de server het verzoek moet accepteren. De volgende les berekent de ene waarde waar het antwoord van de server niet zonder kan.

Opdracht

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

  1. parse_headers(lines) neemt een () aan en geeft een terug. Knip elke regel alleen bij zijn eerste dubbele punt; de naam gaat erin in kleine letters, de met de spaties eromheen weggestript. Als een naam opnieuw voorkomt, voeg je de nieuwe waarde met ", " aan de opgeslagen waarde toe, op volgorde. Werp op voor een regel zonder dubbele punt, en voor een naam die leeg is of een spatie bevat. parse_headers([]) geeft {} terug.

  2. parse_upgrade_request(data) neemt de kop aan als bytes, zoals read_head hem teruggeeft: alles vóór de lege regel, zonder de laatste \r\n\r\n. Decodeer hem met "iso-8859-1", splits hem bij "\r\n", en knip de eerste regel met split(" ", 2). Werp ValueError op tenzij dat precies drie delen geeft, het derde geen spatie bevat en met HTTP/ begint. Geef {"method": ..., "path": ..., "version": ..., "headers": parse_headers(the other lines)} terug.

Laat de functies voor les 3 tot en met 6 zoals ze zijn. Druk op Run om je dictionary voor het verzoek van de RFC te zien.

Druk daarna op Submit. De controle roept beide functies rechtstreeks aan, zonder : op het verzoek van de RFC, op een verzoekkop die je nog niet hebt gezien met namen in vreemde hoofdletters, waarden met extra spaties, een herhaalde Connection-header en een dubbele punt in een waarde, en op verzoekregels en headerregels die ValueError moeten opwerpen. Ze controleert de waarden in je dictionary, niet de volgorde van de sleutels. Het importeren van ws_handshake.py mag niets afdrukken.