0%

HTTP is tekst op een socket · oefening

Een verzoek met de hand versturen

De vorige les splitste bytes die al binnen waren. Deze maakt ze: je schrijft het verzoek in wire.py, verstuurt het via een connect- naar practice-api en leest de kop van het antwoord van de terwijl die binnenkomt, stuk voor stuk, tot aan de lege regel.

Deze les draait op de servers van Python Land. Het tabblad Project bevat de bestanden van het hoofdstuk: wire.py, waarin je werkt, fakes.py, dat les 5 en 7 gebruiken, tests/test_wire.py, dat les 7 gebruikt, en README.md, met de routes en regels voor dit hoofdstuk. Het tabblad Shell is een in dezelfde map.

Open wire.py. De constanten zijn onder meer ADDRESS, de naam en van de server; HOST_LINE; HOOK_TARGET, het doel van het verzoek uit les 2, dat dit hoofdstuk het openingsverzoek noemt; en de limieten MAX_HEAD en MAX_BODY. make_reader en read_exact zijn de ontvangstbuffer uit Networks with Python, al geschreven. De functies daarna schrijf jij, een paar per les.

De verzoekregel

Een verzoekregel heeft drie delen, gescheiden door enkele spaties: de , die zegt wat er moet gebeuren (GET vraagt om een resource); het verzoekdoel (request target), dat de resource op de server noemt (een pad, en eventueel een query na ?); en de versie, HTTP/1.1. CRLF sluit de regel af. Een bouwt er een uit de delen:

Try it

Een server leest de verzoekregel door hem bij de spaties en bij de CRLF te knippen, dus het doel mag geen van die tekens bevatten. /v1/items/a b zou de server vier woorden geven waar hij er drie verwacht, en een doel met \r\n erin zou de regel te vroeg afsluiten en een beginnen die de aanroeper nooit wilde versturen. Daarom weigert request_line zo’n doel met , net zoals het een doel weigert dat niet met / begint. Hoofdstuk 2 laat zien hoe je elk teken veilig in een doel zet.

De Host-regel

De tweede regel is de Host-regel, die de server noemt waarvoor het verzoek bedoeld is. /1.1 eist hem in elk verzoek, en hoofdstuk 2 legt uit waarom. In dit hoofdstuk verandert hij nooit, dus wire.py levert hem als HOST_LINE:

HOST_LINE = "Host: practice-api:8080\r\n"

make_request(method, target) voegt de verzoekregel, HOST_LINE en nog één "\r\n" voor de lege regel samen, en codeert de tekst met "ascii", omdat een socket bytes verstuurt. Voor het openingsverzoek:

RegelWat het is
GET /v1/items?page=1&page_size=2 HTTP/1.1\r\nde verzoekregel
Host: practice-api:8080\r\nde Host-regel
\r\nde lege regel: hier eindigt de kop

Het verzoek eindigt op \r\n\r\n, wat de server vertelt dat de kop voorbij is; dit verzoek heeft geen body. Elke regel eindigt op CRLF, omdat HTTP/1.1 dat eist. Sommige servers accepteren ook een losse \n, maar een tolerante server is geen reden om minder te sturen dan het protocol vraagt.

De kop lezen terwijl hij binnenkomt

De kop van het antwoord eindigt bij de eerste \r\n\r\n. Eén recv geeft terug wat er tot dan toe is aangekomen, misschien het hele antwoord, misschien een deel van de eerste regel, dus de kop lezen is een rond de buffer van de reader:

  1. Zoek \r\n\r\n in reader["buffer"] met find, dat de positie teruggeeft, of -1 als het er niet in staat.

  2. Zolang het er niet in staat, roep je reader["sock"].recv(4096) aan, voeg je het stuk aan het eind van de buffer toe en zoek je opnieuw.

  3. Staat het er wel in, geef dan de bytes ervóór terug, en zet alles na de lege regel terug in reader["buffer"].

Controleer de buffer vóór de eerste recv: als de bytes er al zijn, hoef je nergens op te wachten. En bewaar wat na de kop komt. Dat is het begin van de body, die les 5 leest, en een antwoord komt vaak met kop en body in één stuk aan.

De eerste recv in read_head gaf b'HTTP/1.1 200 OK\r\nContent-Ty' terug. Wat moet read_head doen?

De lus kan op twee manieren eindigen zonder kop, en elk krijgt een eigen exception:

  • recv geeft b"" terug: de server sloot de verbinding voordat de kop eindigde. Werp ConnectionError op, zoals read_exact doet.

  • De buffer groeit voorbij limit bytes zonder \r\n\r\n: een kop die nooit eindigt, zou anders het geheugen van het programma vullen. Werp ValueError op, zoals je read_line in Networks with Python deed voor een regel die nooit eindigde. limit is standaard MAX_HEAD, 16384 bytes.

Versturen via connect

fetch zet alles bij elkaar. De eerste twee regels openen de verbinding en versturen het verzoek:

with connect(ADDRESS, timeout=timeout) as sock:
    sock.sendall(make_request(method, target))

connect is een , dus een test kan een functie meegeven die een fake socket teruggeeft, zoals je prikbordclient in Networks with Python deed. De time-out die aan socket.create_connection wordt meegegeven, blijft op de socket staan, dus elke latere recv wacht hoogstens timeout seconden, standaard 2, en werpt dan TimeoutError op. Voorlopig geeft fetch de kop terug als bytes; les 5 maakt er een volledig antwoord van.

Je opdracht

Schrijf in wire.py deze vier functies, in deze volgorde:

  1. request_line(method, target): geef f"{method} {target} HTTP/1.1\r\n" terug; werp ValueError op als het doel niet met / begint, of een spatie, \r of \n bevat.

  2. make_request(method, target): geef de verzoekregel, HOST_LINE en "\r\n" terug, gecodeerd met "ascii".

  3. read_head(reader, *, limit=MAX_HEAD): de lus hierboven.

  4. fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT): maak verbinding met ADDRESS met timeout=timeout, verstuur het verzoek, maak een reader en geef terug wat read_head teruggeeft.

Laat de functies die voor les 4 tot en met 6 gemarkeerd zijn zoals ze zijn.

Verstuur daarna het openingsverzoek vanuit het tabblad Shell. main drukt af wat fetch teruggeeft, dus je ziet de kop als bytes-:

python wire.py
python wire.py HEAD

De eerste verstuurt GET met HOOK_TARGET, de tweede HEAD met hetzelfde doel. Elk drukt de kop als bytes af: eerst de statusregel, elke regeleinde getoond als \r\n, en geen body. Beide komen meteen terug, omdat read_head bij de lege regel stopt. Niets uit je terminal wordt gecontroleerd.

Wil je zien waar de Host-regel voor dient, verstuur het verzoek dan zonder die regel. Dit commando schrijft de verzoekregel en de lege regel, en niets daartussen:

python -c "import socket, wire; sock = socket.create_connection(wire.ADDRESS, timeout=2.0); sock.sendall((wire.request_line('GET', wire.HOOK_TARGET) + '\r\n').encode('ascii')); print(wire.read_head(wire.make_reader(sock)))"

Laat je de Host-regel weg, dan zou je 400 Bad Request moeten krijgen met een korte HTML-pagina in plaats van : de webserver vóór practice-api weigert het verzoek voordat de dienst het ziet. Krijg je in plaats daarvan de gewone 200, dan bereikte je de dienst rechtstreeks, en die verdraagt het weglaten; HTTP/1.1 eist de regel toch. Het commando drukt alleen de kop af, dus kijk naar de statusregel.

Als de lus in read_head je in verwarring brengt, vraag het dan aan Monty:

Waarom moet mijn read_head in de buffer naar \r\n\r\n zoeken voordat het recv aanroept, en niet alleen daarna?

Als python wire.py de kop laat zien, druk je op Check my work. De volgende les maakt van die bytes een en een met headers.

Opdracht

Schrijf vier in wire.py, in deze volgorde. Check my work noemt de eerste die nog NotImplementedError opwerpt, zodat je ze een voor een tegenkomt.

  1. request_line(method, target) geeft f"{method} {target} HTTP/1.1\r\n" terug. Het werpt op als target niet met / begint, of als het een spatie, \r of \n bevat.

  2. make_request(method, target) geeft bytes terug: de verzoekregel, HOST_LINE en "\r\n", gecodeerd met "ascii".

  3. read_head(reader, *, limit=MAX_HEAD) zoekt \r\n\r\n in reader["buffer"], en zolang het er niet in staat, voegt het reader["sock"].recv(4096) aan het eind van de buffer toe en zoekt opnieuw. Het geeft de bytes vóór de lege regel terug en laat elke byte erna in reader["buffer"] staan. Het werpt ConnectionError op als recv eerst b"" teruggeeft, en ValueError als de buffer voorbij limit bytes groeit zonder \r\n\r\n.

  4. fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT) roept connect(ADDRESS, timeout=timeout) aan in een with-statement, verstuurt make_request(method, target) met sendall, verpakt de met make_reader en geeft terug wat read_head teruggeeft.

Laat parse_head, read_body en json_body zoals ze zijn.

Voer in de python wire.py en python wire.py HEAD uit.

Druk daarna op Check my work. De controle roept request_line en make_request aan met en doelen die je niet hebt gezien, en met doelen die geweigerd moeten worden. Ze roept read_head aan op vervangende sockets met antwoorden die je niet hebt gezien, geleverd in één stuk, één byte tegelijk, op elke positie in tweeën geknipt (ook binnen de lege regel en binnen de body) en in stukken van 7 bytes, elke keer zowel met een server die daarna sluit als met een die de verbinding openhoudt en niets meer stuurt. Ze controleert ook een buffer die al een hele kop bevat, een kop die nooit eindigt, en een server die halverwege de kop sluit. Ze roept fetch aan met een vervangende connect en controleert het adres, de time-out (de standaardwaarde en een die je meegeeft), de verstuurde bytes, de teruggegeven kop en of het with-blok is afgesloten. Ze leest je terminal niet en neemt geen contact op met practice-api.