0%

Eén verbinding, veel verzoeken · oefening

Een chunked body decoderen

Les 2 las een chunked body met het oog. Deze les leert wire.py er een te lezen: read_chunked(reader, *, limit=MAX_BODY) neemt de readerdict die read_head net heeft gebruikt en geeft de body terug, waarbij de reader klaar blijft voor wat er daarna op de verbinding komt. Kijk eerst hoe het misgaat:

python pl_http.py chunked

Hier draaien je make_server en serve een kleine server die voor deze cursus is geschreven, in plaats van je leeslijst. pl_http stuurt GET /chunked, en die server antwoordt met de chunked body uit les 2. pl_http leest de kop van het antwoord zelf, zet de bytes die erna kwamen in de buffer van een reader en geeft de reader aan je read_chunked. Voordat je die schrijft, zegt het rapport:

--- your read_chunked() returned ---
raised NotImplementedError: not written yet: Lesson 3

Twee lezers die je al hebt

Twee in wire.py doen het lezen voor je.

read_exact(reader, n), uit hoofdstuk 1, geeft precies n bytes terug, eerst uit de buffer en daarna uit recv, en houdt eventuele extra bytes in de buffer. Hij werpt ConnectionError op wanneer de andere kant sluit voordat er n bytes zijn aangekomen.

read_crlf_line(reader, *, limit=MAX_LINE) is nieuw in dit hoofdstuk en al voor je geschreven. Hij doet voor één regel wat read_head voor een kop doet: hij geeft de bytes vóór de volgende CRLF terug, zonder de CRLF, en laat de rest in de buffer. Een regel die langer is dan MAX_LINE, 1024 bytes, werpt op, zodat een grootteregel niet eindeloos kan groeien, en een sluiting midden in een regel werpt ConnectionError op.

Met die twee bestaat elke chunk uit drie keer lezen:

the size line   read_crlf_line(reader)
the data        read_exact(reader, size)
the CRLF        read_exact(reader, 2), which must be b"\r\n"

Herhaal dat totdat de grootte 0 is. De twee bytes na de data zijn niet optioneel: een chunk waarvan de data niet door CRLF wordt gevolgd, is misvormd, en doorlezen alsof er niets aan de hand is, zou de volgende grootteregel op de verkeerde plek zetten. Werp ValueError op als ze iets anders zijn.

De grootteregel komt binnen als bytes. Decodeer hem met "iso-8859-1", zoals parse_head een kop decodeert, knip hem dan af bij de eerste ;, omdat alles daarna een extensie is die je negeert, en haal de spaties weg rond wat er overblijft.

Hex, strikt

De grootte is hexadecimaal, en int(text, 16) leest hexadecimaal. Hij leest ook meer dan dat:

Wat drukt dit af?

print(int("1a", 16), int("0x1a", 16))

Beide zijn 26, maar alleen 1a is een chunkgrootte. int met grondtal 16 accepteert ook 1_a en +1a, en maakt van -1a -26. Een chunkgrootte bestaat uit een of meer hexcijfers en verder niets, dus controleer elk teken tegen HEX_DIGITS, de "0123456789abcdefABCDEF" in wire.py, voordat je int aanroept, en werp ValueError op bij elk ander teken of bij een lege grootte.

Les 1 liet zien wat er gebeurt als twee programma’s op één verbinding het oneens zijn over waar een body eindigt: de bytes van het ene bericht worden gelezen als een ander bericht. Een grootte die de ene lezer accepteert en de andere weigert, is precies zo’n onenigheid. http.client zelf is hier soepel: hij knipt de grootteregel af bij ; en geeft de rest aan int(line, 16). Jouw lezer wordt strenger.

Het einde

Een grootte van 0 betekent dat de data op is, en na die regel volgen geen data en geen CRLF. Wat volgt, zijn trailerregels, die na de body worden gestuurd, en dan één lege regel. Lees regels met read_crlf_line totdat er een leeg is, gooi ze weg en geef de body terug. Tel de bytes van die regels, zoals read_head een kop begrenst, en werp ValueError op zodra ze MAX_HEAD overschrijden. Wat een trailerveld betekent, valt buiten deze cursus; je lezer hoeft er alleen voorbij te komen.

read_chunked heeft net de grootteregel 0 gelezen. Wat nu?

Alles wat na de lege regel komt, hoort bij het volgende antwoord op dezelfde verbinding. Laat het in reader["buffer"] staan. Daarom is de reader een die langer leeft dan één aanroep: de volgende read_head begint bij die bytes.

Limieten

Het prikbord in Networks with Python weigerde een frame waarvan de aangekondigde lengte boven de limiet lag, nog voordat het er één byte van las. Dezelfde regel geldt hier, chunk voor chunk: controleer vóór het lezen van de data van een chunk of de body tot nu toe plus de grootte van deze chunk limit zou overschrijden, en werp ValueError op als dat zo is. Anders zou een grootteregel FFFFFFFF je client op 4 GB laten wachten. limit is standaard MAX_BODY, 1000000 bytes.

Je opdracht

Schrijf read_chunked(reader, *, limit=MAX_BODY) in wire.py: grootteregels, data, CRLF’s, de nulchunk, trailerregels en de lege regel, met de controles hierboven. Ongeveer twintig regels zijn genoeg, en een kleine hulpfunctie die een grootteregel in een getal omzet, is welkom.

Je kunt het uitproberen in het tabblad Shell met de gescripte uit fakes.py, waarvan recv de bytes uitdeelt die je hem geeft:

python - <<'EOF'
import wire
from fakes import scripted_socket

reader = wire.make_reader(scripted_socket([b"4\r\nWiki\r\n5;x=1\r\npedia\r\n0\r\n\r\nNEXT"]))
print(wire.read_chunked(reader))
print(reader["buffer"])
EOF

Het drukt b'Wikipedia' af en daarna b'NEXT', het begin van een volgend antwoord, dat nog in de buffer staat. Je kunt ook eigen tests toevoegen aan tests/test_wire.py en ze uitvoeren met python -m pytest -q; die zijn van jou, en Check my work voert ze niet uit. Voer daarna de helper opnieuw uit:

python pl_http.py chunked
--- your read_chunked() returned ---
body: b'{"books": ["Kindred", "Caf\xc3\xa9"]}'  (31 bytes)
left in the buffer: b''  (0 bytes)

Zeven bytes en vierentwintig, en niets over. Een decoder die 18 als achttien leest, neemt zes bytes te weinig en vindt dan geen CRLF waar hij er een verwacht.

Druk daarna op Check my work. Les 4 zet deze decoder in fetch, naast een lezer voor bodies die bij het sluiten eindigen.

Opdracht

Decodeer een chunked body in wire.py.

Schrijf read_chunked(reader, *, limit=MAX_BODY). Die leest uit de readerdict, met read_crlf_line en read_exact, en geeft de body terug als bytes:

  1. Lees een grootteregel. Decodeer hem als "iso-8859-1", laat alles vanaf de eerste ; weg en haal de spaties weg. Wat overblijft, moet uit een of meer tekens bestaan die elk in HEX_DIGITS staan; anders werp je op. Zet het daarna om met int(text, 16).

  2. Bij een grootte boven 0: als de body tot nu toe plus deze grootte groter is dan limit, werp dan ValueError op voordat je de data leest. Lees anders precies zoveel bytes, en daarna nog twee, die b"\r\n" moeten zijn (ValueError als dat niet zo is).

  3. Lees na de grootte 0 regels tot er een leeg is, en gooi ze weg. Werp ValueError op zodra die regels samen meer dan MAX_HEAD bytes zijn. Geef daarna de body terug.

Laat elke byte na de lege regel in reader["buffer"] staan. Laat een ConnectionError door een sluiting halverwege de verlaten.

In de geeft python pl_http.py chunked je functie een echt chunked antwoord en toont het wat ze teruggaf en wat er in de buffer achterbleef.

Druk daarna op Check my work. Die roept read_chunked aan met readers rond een vervangende waarvan recv bodies uitdeelt die je niet hebt gezien, gesplitst bij elke byte, één byte tegelijk en in onregelmatige stukken: groottes in hoofdletters en kleine letters, extensies, een chunk waarvan de data CRLF bevat, trailerregels, 1000 chunks van één byte en het begin van een volgend antwoord na het bericht. Hij controleert groottes zoals 0x7, -7, 7_, g en een lege, data die niet door CRLF wordt gevolgd, een sluiting midden in een chunk, te veel trailerbytes en een body boven limit, waarbij de data van de chunk die de limiet overschrijdt nooit mag worden opgevraagd. Als het bericht compleet is, blijft de vervangende socket open, zodat een decoder die na de lege regel doorleest, wordt betrapt. Je eigen tests worden niet uitgevoerd. Niets uit je terminal wordt beoordeeld.