0%

Verzoeken met de hand schrijven · oefening

Headers die de server ontving

Elk verzoek dat je hebt verstuurd, bevat één , en die schreef je niet zelf: HOST_LINE, Host: practice-api:8080, geleverd in hoofdstuk 1 met de belofte dat dit hoofdstuk hem zou uitleggen. In deze les bouw je die regel uit het adres, schrijf je alle andere headerregels waar een aanroeper om vraagt, weiger je de regels die het verzoek zouden breken, en stuur je één header naar de route /v1/headers van practice-api, die meldt wat hij ontving.

Waarom Host bestaat

Als je client verbinding maakt met practice-api op 8080, wordt de naam eerst omgezet in een , zoals in Networks with Python. De verbinding zelf bevat alleen dat adres en de poort. De naam waarmee je begon, is verdwenen tegen de tijd dat de eerste byte aankomt.

Dat doet ertoe, omdat één adres vaak voor veel namen antwoordt. Eén server kan books.test, shop.test en news.test tegelijk hosten, die allemaal naar hetzelfde IP-adres wijzen en allemaal op poort 80 zitten. Als er een verzoek aankomt, moet de server weten welke daarvan de client bedoelde, en niets in de verbinding zegt dat. De Host-regel zegt het: het verzoek noemt de server waarvoor het bedoeld is, en de server kiest de site die antwoordt.

Daarom eist /1.1 dat elk verzoek precies één Host-regel bevat, en moet een server een verzoek zonder die regel weigeren met 400 Bad Request. De webserver vóór practice-api zou precies dat moeten doen, met een korte HTML-pagina in plaats van . Krijgt een verzoek zonder Host ooit in plaats daarvan de gewone 200, dan bereikte het de dienst rechtstreeks, en die verdraagt het weglaten; HTTP/1.1 eist de regel toch, en je client stuurt hem altijd.

De van Host kopieert de host en poort van de , precies zoals de URL ze schreef:

URLHost-regel
http://practice-api:8080/v1/headersHost: practice-api:8080
http://books.test/shelvesHost: books.test
http://books.test:80/shelvesHost: books.test:80

Een URL die geen poort noemt, krijgt een Host-regel zonder poort, ook al maakt de client verbinding met poort 80. Daarom bewaarde les 2 de None van urlsplit: je host_header(host, port) vat port=None op als “de URL noemde geen poort” en schrijft alleen de host. Omdat 80 de standaardpoort voor http is, noemen Host: books.test en Host: books.test:80 dezelfde server, en servers accepteren beide; je host_header schrijft de poort telkens als hij er een krijgt.

De verbinding bereikte practice-api op poort 8080 al. Waarom verstuurt het verzoek toch Host: practice-api:8080?

Headersyntaxis

Elke headerregel heeft dezelfde vorm: een naam, een dubbele punt, dan de waarde, eindigend op \r\n. Een spatie na de dubbele punt is gebruikelijk, en je client schrijft er altijd een:

X-Practice-Name: http-with-python

De naam is één woord: bij de eerste dubbele punt eindigt hij, dus hij kan geen dubbele punt bevatten, en ook geen spatie, tab of regeleinde. De volledige regel van HTTP voor namen is nog strenger, en verbiedt ook tekens zoals /, (, , en @; je client weigert de tekens die het verzoek zouden breken. De waarde is de rest van de regel, en die mag spaties en dubbele punten bevatten; hij kan nooit \r of \n bevatten, omdat die de regel beëindigen.

Als een veld meerdere waarden heeft, schrijft de client één regel per waarde, met elke keer dezelfde naam. Dat is de vorm van parse_head uit hoofdstuk 1, andersom gelezen. Daar werden de regels X-Trace: a en x-trace: b tot {"x-trace": ["a", "b"]}; hier krijgt een aanroeper die {"X-Tag": ["a", "b"]} meegeeft twee regels:

X-Tag: a
X-Tag: b

Van de naar de regels gaan, zijn twee , één over de namen en één over de waarden van elke naam:

Try it

Een aanroeper die één meegeeft, {"Accept": "application/json"}, bedoelt één regel. format_headers accepteert beide vormen: een waarde die een str is, wordt behandeld als een van één, zodat de binnenste lus nooit letter voor letter door een string loopt.

Een regeleinde in een waarde

Headerwaarden komen vaak ergens anders vandaan: de naam van een gebruiker, een titel, een waarde uit een bestand. Stel dat een ervan een regeleinde bevat:

Try it

De aanroeper bedoelde één header, X-Note, met een vreemde waarde. De server leest er twee: X-Note: hello, en X-Admin: yes, een regel die niemand met opzet schreef. Dit is header-injectie (header injection). Als iemand anders een deel van een waarde kan kiezen, kan die elke header toevoegen die hij wil, of de kop vroegtijdig beëindigen met een lege regel en een body beginnen. De oplossing is eenvoudig en streng: weiger elke naam of waarde die \r of \n bevat, en werp op voordat er iets wordt verstuurd.

Een aanroeper geeft {"X-Note": "hello\r\nX-Admin: yes"} mee, en een client schrijft het zonder controle. Wat leest de server?

Je opdracht

Vier delen van wire.py horen bij deze les, in deze volgorde.

  1. host_header(host, port) geeft de Host-regel terug, eindigend op \r\n: Host: practice-api:8080\r\n voor ("practice-api", 8080), en Host: books.test\r\n voor ("books.test", None).

  2. format_headers(headers) geeft een str terug met één regel Name: value\r\n per waarde. Loop over de namen en hun waarden zoals in het voorbeeld hierboven. Als een waarde een str is, wat je kunt testen met isinstance(values, str), behandel hem dan als een lijst van één. Werp ValueError op voor een lege naam; voor een naam die een spatie, een dubbele punt, een tab, \r of \n bevat; en voor een waarde die \r of \n bevat. Een lege dict geeft "".

  3. Verander make_request in make_request(method, target, headers=None). Bouw de kop uit de verzoekregel, dan host_header(ADDRESS[0], ADDRESS[1]) in plaats van HOST_LINE, dan format_headers(headers) als headers niet None is, dan de lege regel. Host komt eerst, zoals HTTP aanraadt. Verwijder HOST_LINE bovenaan het bestand, zodat niets per ongeluk een tweede Host-regel kan toevoegen.

  4. Geef fetch een headers=None na de *, naast connect en timeout, en geef hem door aan make_request.

Lees eerst je eigen bytes. In de :

python -c "import wire; print(wire.make_request('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS))"
b'GET /v1/headers HTTP/1.1\r\nHost: practice-api:8080\r\nX-Practice-Name: http-with-python\r\n\r\n'

Dit zijn de exacte bytes die je client verstuurt: de verzoekregel, je Host-regel gebouwd uit ADDRESS, de oefenheader en de lege regel. Controleer ze een voor een. De live uitvoeringen in dit hoofdstuk geven headers=wire.PRACTICE_HEADERS mee, zoals README.md zegt; python wire.py op zichzelf verstuurt alleen Host.

Wat de server ontving

Verstuur nu het verzoek. /v1/headers antwoordt met de header X-Practice-Name die hij ontving, en met niets anders: hij meldt Host en andere headers niet. practice-api staat ook achter een kleine webserver die elk verzoek aan hem doorgeeft, en die voorste server is zo ingesteld dat hij zijn eigen Host-regel voor de dienst schrijft, dus zelfs een route die Host meldde, zou de jouwe niet laten zien. Voor Host en elke andere regel zijn je eigen verzoekbytes hierboven het verslag van wat je verstuurde. Voor X-Practice-Name is het antwoord van de server een tweede verslag:

python -c "import wire; print(wire.fetch('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS)['body'])"

Je zou {"header":"X-Practice-Name","value":"http-with-python"} moeten zien: de route weerspiegelt die ene header, in welke schrijfwijze je de naam ook verstuurde, en geen andere. Ziet de body er anders uit, vergelijk hem dan met je verzoekbytes hierboven; dat is wat je verstuurde.

De body wordt als bytes afgedrukt, dus hij eindigt op \n, de newline die practice-api na elke JSON-body zet.

Verstuur tot slot twee waarden voor de oefenheader. format_headers schrijft ze als twee regels, en practice-api weigert een herhaalde oefenheader:

python -c "import wire; r = wire.fetch('GET', '/v1/headers', headers={'X-Practice-Name': ['a', 'b']}); print(r['status'], r['body'])"

Je zou 400 moeten zien met de melding "X-Practice-Name must not be repeated": de server zag twee regels met dezelfde naam, en dat is wat een herhaald veld op de lijn is. Zie je iets anders, dan bevatten je verzoekbytes toch de twee regels; vergelijk ze met het antwoord. Niets in deze uitvoering wordt gecontroleerd; het is er zodat je het kunt zien.

Druk daarna op Check my work.

Als een controle mislukt, vraag Monty dan de foutmelding te vergelijken met je lus in format_headers en te zeggen welke regel die breekt, zonder de code te schrijven.

Je client schrijft nu elke regel van een verzoek zelf, maar fetch verstuurt nog steeds alleen naar practice-api, en alleen een doel dat je apart hebt gebouwd. De volgende les brengt de onderdelen samen in één die elke , elke URL, elke headers en een body aanneemt.

Opdracht

Werk in wire.py in deze volgorde:

  1. host_header(host, port) geeft "Host: <host>:<port>\r\n" terug, of "Host: <host>\r\n" als port None is.

  2. format_headers(headers) geeft een str terug met één regel "Name: value\r\n" per , waarbij een waarde een str is of een van str. Het werpt op voor een lege naam, een naam die een spatie, een dubbele punt, een tab, \r of \n bevat, en een waarde die \r of \n bevat. Een lege geeft "".

  3. make_request(method, target, headers=None) geeft de verzoekregel terug, dan host_header(ADDRESS[0], ADDRESS[1]) in plaats van HOST_LINE, dan format_headers(headers) als headers niet None is, dan de lege regel, gecodeerd als ASCII. Verwijder HOST_LINE, zodat het verzoek precies één Host-regel heeft.

  4. fetch krijgt headers=None na de * en geeft het door aan make_request.

Druk in de wire.make_request('GET', '/v1/headers', headers=wire.PRACTICE_HEADERS) af en lees elke regel, haal daarna /v1/headers op met de oefenheader, en nog een keer met twee waarden ervoor.

Druk daarna op Check my work. De controle roept host_header aan met hosts en die je niet hebt gezien, met en zonder poort; format_headers met namen en waarden die je niet hebt gezien, waaronder een herhaald veld, een waarde met dubbele punten, en negen onveilige namen en waarden die elk ValueError moeten opwerpen; en make_request met en zonder headers, een keer met ADDRESS veranderd in een andere host en poort, zodat de Host-regel uit ADDRESS moet komen. Ze roept fetch met headers aan via een vervangende connect- en leest het verzoek dat het verstuurde, terwijl het antwoord aankomt in stukken die op elke positie zijn geknipt, op een verbinding die sluit of openblijft. De volgorde van headers met verschillende namen wordt niet gecontroleerd. Ze neemt nooit contact op met practice-api en leest je terminal niet.