Verzoeken met de hand schrijven · oefening
Headers die de server ontving
Elk verzoek dat je hebt verstuurd, bevat één 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
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 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 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 Host kopieert de host en poort van de
| URL | Host-regel |
|---|---|
http://practice-api:8080/v1/headers | Host: practice-api:8080 |
http://books.test/shelves | Host: books.test |
http://books.test:80/shelves | Host: 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
Een aanroeper die één {"Accept": "application/json"}, bedoelt één regel. format_headers accepteert beide vormen: een waarde die een str is, wordt behandeld als een
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:
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
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.
host_header(host, port)geeft deHost-regel terug, eindigend op\r\n:Host: practice-api:8080\r\nvoor("practice-api", 8080), enHost: books.test\r\nvoor("books.test", None).format_headers(headers)geeft eenstrterug met één regelName: value\r\nper waarde. Loop over de namen en hun waarden zoals in het voorbeeld hierboven. Als een waarde eenstris, wat je kunt testen metisinstance(values, str), behandel hem dan als een lijst van één. WerpValueErrorop voor een lege naam; voor een naam die een spatie, een dubbele punt, een tab,\rof\nbevat; en voor een waarde die\rof\nbevat. Een lege dict geeft"".Verander
make_requestinmake_request(method, target, headers=None). Bouw de kop uit de verzoekregel, danhost_header(ADDRESS[0], ADDRESS[1])in plaats vanHOST_LINE, danformat_headers(headers)alsheadersnietNoneis, dan de lege regel.Hostkomt eerst, zoals HTTP aanraadt. VerwijderHOST_LINEbovenaan het bestand, zodat niets per ongeluk een tweedeHost-regel kan toevoegen.Geef
fetcheenheaders=Nonena de*, naastconnectentimeout, en geef hem door aanmake_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
Opdracht
Werk in wire.py in deze volgorde:
host_header(host, port)geeft"Host: <host>:<port>\r\n"terug, of"Host: <host>\r\n"alsportNoneis.format_headers(headers)geeft eenstrterug met één regel"Name: value\r\n"per, waarbij een waarde een stris of eenvan str. Het werptop voor een lege naam, een naam die een spatie, een dubbele punt, een tab, \rof\nbevat, en een waarde die\rof\nbevat. Een legegeeft "".make_request(method, target, headers=None)geeft de verzoekregel terug, danhost_header(ADDRESS[0], ADDRESS[1])in plaats vanHOST_LINE, danformat_headers(headers)alsheadersnietNoneis, dan de lege regel, gecodeerd als ASCII. VerwijderHOST_LINE, zodat het verzoek precies éénHost-regel heeft.fetchkrijgtheaders=Nonena de*en geeft het door aanmake_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 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-