0%

Maskers en controleframes · eindproject

Eindproject: Project: een volledige framecodec

Elke regel van dit hoofdstuk woont nu in ws_frames.py, maar in aparte . Maskeren zit in encode_frame en decode_frame, de limieten voor controleframes in decode_frame, de close-payload in parse_close_payload, en fragmenten worden nog nergens geweigerd. Een programma dat van een verbinding leest, wil geen vijf functies aanroepen en zelf controleren. Het wil één aanroep die antwoordt op “wat stuurde de andere kant net?”. Dit project schrijft die aanroep, read_message, en geeft elke weigering in de codec de die een verbinding ervoor zou sturen.

Wat read_message teruggeeft

read_message(read_exact, *, role, max_size=MAX_SIZE) leest precies één via read_exact en geeft een van vijf events terug, elk een :

Het frameHet event
tekst{"type": "text", "data": ...}, de payload gedecodeerd uit naar een str
binair{"type": "binary", "data": ...}, de payload als bytes
{"type": "ping", "data": ...}, de payload als bytes
pong{"type": "pong", "data": ...}, de payload als bytes
close{"type": "close", "code": ..., "reason": ...}, zoals parse_close_payload ze leest

role zegt welke kant leest. Een "server" leest de frames van zijn client, dus roept hij decode_frame aan met require_mask=True; een "client" leest de frames van zijn server, met require_mask=False. max_size gaat ongewijzigd door naar decode_frame.

Niet alles wat misgaat, is een peer die een regel breekt. Als de verbinding midden in een frame eindigt, werpt read_exact ConnectionError op, en read_message laat die ongewijzigd door. Hoofdstuk 5 meldt dat einde als 1006, de code voor een verbinding die eindigde zonder close-frame, en dat is een ander verhaal dan een protocolfout.

Geef elke weigering haar code

Als een peer een regel breekt, sluit de lezer de verbinding met een code die zegt waarom. Tot nu toe was elke weigering in de codec een met één melding. Geef elke weigering ook haar code, als eerste vóór de melding:

try:
    raise ValueError(1002, "opcode 3 is reserved")
except ValueError as exc:
    print(exc.args == (1002, "opcode 3 is reserved"))
    print(exc.args[0], exc)

Een exception bewaart de waarden waarmee ze is gemaakt in haar attribuut args, een , dus een handler die de fout vangt as exc vindt de code in exc.args[0] en de reden in exc.args[1]. Dit blok drukt True af, en daarna 1002 (1002, 'opcode 3 is reserved'): een ValueError met twee waarden afdrukken toont de hele tuple. Hoofdstuk 5 leest exc.args[0] om te kiezen welk close-frame het stuurt.

Hier is elke weigering op het leespad, elf die al in de codec zitten en drie die read_message toevoegt:

#FunctieWeigeringCode
1, 2parse_header_byteseen gereserveerde (RSV) bit gezet; een gereserveerde opcode1002
3, 4, 5read_lengtheen lengte van 16 bits onder 126; een lengte van 64 bits met de hoogste bit gezet; een lengte van 64 bits onder 65.5361002
6decode_frameMASK niet wat de rol eist (de richtingsregel)1002
7, 8decode_frameeen controleframe van meer dan 125 bytes; een controleframe met FIN uit (één raise mag beide dekken)1002
9decode_frameeen payload groter dan max_size1009
10, 11parse_close_payloadeen payload van één byte; een code die niet mag reizen1002
nieuwread_messageFIN uit of opcode 0; tekst die geen UTF-8 is; een close-reden die geen UTF-8 is1003; 1007; 1007

De meeste weigeringen van de codec zijn 1002, protocolfout: de peer brak een framingregel. Een frame boven de groottelimiet breekt geen regel, dus krijgt het 1009, bericht te groot. Een fragment breekt ook geen regel: zoals les 5 uitlegde, voegt de leercodec gefragmenteerde berichten niet samen, en 1003, niet-ondersteunde data, is de minst verkeerde code voor die grens van de codec. De fouten aan de coderende kant, in encode_frame, reply_to_ping en close_payload, houden hun ene melding. Ze melden een fout in je eigen programma, niet iets wat een peer stuurde, dus er hoort geen sluitcode bij.

Wat drukt dit af?

exc = ValueError(1009, "too big")
print(exc.args[0], exc.args[1])

Specifiek vóór algemeen

Ongeldige UTF-8 duikt op twee plekken op: een tekstpayload, en de reden van een close. In beide werpt .decode("utf-8") UnicodeDecodeError op, en read_message maakt daar 1007 van, ongeldige payloaddata. Zet voor een tekstframe het decoderen in een try-blok met except UnicodeDecodeError: en werp ValueError(1007, ...) op vanuit de handler. Doe voor een close-frame hetzelfde rond de aanroep van parse_close_payload.

Noem in die handler UnicodeDecodeError, niet ValueError. UnicodeDecodeError is een soort ValueError, dus een handler voor ValueError zou ook de 1002-weigeringen vangen die parse_close_payload opwerpt voor een payload van één byte of een code die niet mag reizen, en die zouden als 1007 naar buiten komen. Door de specifieke exception te noemen, gaan die weigeringen onaangeroerd door met hun eigen code.

Een server leest een gemaskeerd tekstframe waarvan de payload c3 28 is. Welke code werpt read_message op?

Twee rollen, één functie

Hetzelfde frame kan voor de ene rol in orde zijn en voor de andere een protocolfout. Een gemaskeerd frame is precies wat een server verwacht en precies wat een client moet weigeren, en het role beslist dat. Alles na de richtingsregel is voor beide kanten hetzelfde: de limieten, de close-payload, de UTF-8-controle en de weigering van fragmenten.

read_message(read_exact, role="client") leest een gemaskeerd frame. Wat gebeurt er?

Fragmenten vóór inhoud

read_message voegt zijn controles toe nadat decode_frame een frame heeft teruggegeven, dus tegen die tijd is elke framingregel toegepast: de richtingsregel, de limieten voor controleframes en de groottelimiet. De fragmentcontrole komt daarna, voordat de opcode beslist welk event er wordt gebouwd. Een vervolgframe heeft opcode 0, en dat hoort bij geen van de vijf events. En een tekstframe met FIN uit bevat maar een deel van een bericht, dat midden in een teken kan ophouden dat in UTF-8 meerdere bytes kost; als je het eerst decodeerde, zou je het weigeren met 1007 en de tekst van de peer de schuld geven, terwijl de echte reden de eigen grens van de codec is, 1003.

Je opdracht

In ws_frames.py:

  1. Werk de tabel hierboven af: verander elke weigering in parse_header_bytes, read_length, decode_frame en parse_close_payload van ValueError(message) in ValueError(code, message) met de code die de tabel geeft. Rij 7 en 8 mogen één raise delen.

  2. Schrijf read_message(read_exact, *, role, max_size=MAX_SIZE): roep decode_frame aan met require_mask ingesteld voor de rol en max_size doorgegeven; werp ValueError(1003, ...) op voor een frame met FIN uit of opcode 0; geef het event voor elke opcode terug, en werp ValueError(1007, ...) op voor tekst of een close-reden die geen UTF-8 is. Laat ConnectionError door.

Druk op Run. De sectie van les 6 geeft een server het gemaskeerde “Hello” van de RFC, een lege gemaskeerde ping en een gemaskeerde close 1000, en drukt de drie events af die je read_message teruggeeft:

Lesson 6: what a server reads
[{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}]

Druk daarna op Submit. De controle draait eerst een paar onbekende gevallen uit les 1 tot en met 4. Daarna loopt ze de tabel rij voor rij af, stuurt read_message één frame dat elke regel breekt, en noemt de eerste functie waarvan de weigering de aanroeper bereikt zonder de juiste code. Daarna leest ze elk soort event voor beide rollen, in één keer afgeleverd, één byte en twee bytes per keer, twee frames direct achter elkaar, kapotte frames die geweigerd moeten worden met hun code en een str als reden, een te groot frame dat geweigerd moet worden voordat de payload gelezen wordt, en een verbinding die midden in een frame eindigt.

De leercodec is nu af voor berichten die in één frame passen, en verder niets: hij voegt geen fragmenten samen, en hij opent geen . Hoofdstuk 5 zet hem op een echte verbinding, tussen een server en een client in één programma.

Opdracht

Maak de leercodec in ws_frames.py af.

  1. Geef elke weigering op het leespad haar , als ValueError(code, message) met de code eerst:

    #WeigeringCode
    1, 2parse_header_byteseen gereserveerde (RSV) bit gezet; een gereserveerde 1002
    3, 4, 5read_lengtheen lengte van 16 bits onder 126; een lengte van 64 bits met de hoogste bit gezet; een lengte van 64 bits onder 65.5361002
    6decode_frameMASK niet wat de rol eist1002
    7, 8decode_frameeen controleframe van meer dan 125 bytes; een controleframe met FIN uit1002
    9decode_frameeen payload groter dan max_size1009
    10, 11parse_close_payloadeen payload van één byte; een code die niet mag reizen1002

    Laat de fouten van encode_frame, reply_to_ping en close_payload zoals ze zijn.

  2. Schrijf read_message(read_exact, *, role, max_size=MAX_SIZE). Decodeer één met require_mask=True als role "server" is en False als het "client" is, en geef max_size door. Werp ValueError(1003, ...) op voor een frame met FIN uit of opcode 0. Geef {"type": "text", "data": str}, {"type": "binary", "data": bytes}, {"type": "ping", "data": bytes}, {"type": "pong", "data": bytes} of {"type": "close", "code": int, "reason": str} terug. Werp ValueError(1007, ...) op als een tekstpayload of een close-reden geen is. Laat een ConnectionError uit read_exact ongewijzigd door.

Druk op Run: de sectie van les 6 moet [{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}] afdrukken. Druk daarna op Submit. De controle kijkt alleen naar wat read_message teruggeeft en opwerpt, met de code als exc.args[0] en een str als reden in exc.args[1]. Ze draait onbekende gevallen uit les 1 tot en met 4 opnieuw, loopt de tabel op volgorde af met één frame per rij en noemt de eerste functie waarvan de weigering zonder de juiste code aankomt, en leest daarna elk event voor beide rollen in één keer, één byte en twee bytes per keer, twee frames direct achter elkaar, de weigeringen met hun codes (een te groot frame vóór elke leesactie van zijn payload), en een frame dat halverwege is afgebroken, dat ConnectionError moet opwerpen. Events mogen extra sleutels bevatten.