0%

Doorverwijzingen · oefening

Los een relatieve location op

Een server die een boek verhuist, antwoordt met een header zoals location: /books/2. Dat is geen waarmee een client verbinding kan maken: er zit geen scheme, geen host en geen in. Het is een verwijzing relatief aan de URL waar de client om vroeg, en de client moet hem ten opzichte van die URL oplossen voordat hij iets kan versturen. staat dit bewust toe (RFC 9110, sectie 10.2.2). Een server zou een volledige URL kunnen bouwen uit de regel Host (hoofdstuk 2, les 4), maar dan moest hij die in elke location kopiëren, en een proxy ervoor kan hem hebben herschreven; een relatieve waarde laat de client, die de URL kent waar hij om vroeg, het oplossen doen. De regels voor het oplossen zijn dezelfde die elke browser voor links gebruikt, vastgelegd in RFC 3986, sectie 5. In deze les schrijf je de twee die de volger in les 4 nodig heeft: één die een location oplost, en één die het resultaat opsplitst in wat http.client vraagt.

De functie urljoin

Die regels schrijf je niet zelf. urljoin, uit dezelfde urllib.parse als urlsplit, neemt de URL die werd opgevraagd en de waarde van location, en geeft de absolute URL terug die ze samen aanduiden. De functie wordt al bovenaan wire.py geïmporteerd. Hier zijn de gangbare vormen van location, elk opgelost ten opzichte van één verzoek-URL:

from urllib.parse import urljoin

asked = "http://books.test/v0/books/2?x=1"
print(urljoin(asked, "/books/2"))
print(urljoin(asked, "../books/2"))
print(urljoin(asked, "?page=2"))
print(urljoin(asked, "//other.test/x"))
print(urljoin(asked, "http://books.test/books/2"))

De uitvoer, regel voor regel:

http://books.test/books/2
http://books.test/v0/books/2
http://books.test/v0/books/2?page=2
http://other.test/x
http://books.test/books/2

Een waarde die met / begint, vervangt het hele pad en de querystring, en behoudt scheme, host en poort. Een waarde die met // begint, vervangt ook de host. Een waarde die met ? begint, behoudt het pad en vervangt alleen de querystring. Een absolute URL wordt teruggegeven zoals hij is. Al het andere, zoals ../books/2 of 3, wordt opgelost als een bestandsnaam relatief aan de map waar de verzoek-URL in wijst: het pad tot en met de laatste /. Voor /v0/books/2 is die map /v0/books/, dus .. gaat omhoog naar /v0/ en books/2 komt weer uit op /v0/books/2.

De mapregel heeft één verrassing: of de verzoek-URL op een slash eindigt, verandert de map.

Wat drukt dit af?

from urllib.parse import urljoin

print(urljoin("http://books.test/v0/books/2", "/books/2"))
print(urljoin("http://books.test/books", "2"))

De tweede regel is http://books.test/2, niet http://books.test/books/2. /books heeft geen slash aan het eind, dus het laatste segment wordt als bestandsnaam behandeld, en 2 vervangt het. Een server die /books/2 bedoelt, moet dat zeggen met een waarde die met / begint, en dat is wat de leeslijst zal doen.

urljoin decodeert en codeert niets: %C3%A6 in een location blijft %C3%A6 in het resultaat, klaar om in een verzoekregel te gaan.

Elke location tegen zijn eigen URL

Een kan naar een volgende doorverwijzing leiden. Elke location wordt opgelost ten opzichte van de URL die hem teruggaf, en dat is na de eerste hop niet meer de URL waarmee je begon. Een volger houdt een huidige URL bij, lost elke nieuwe location daartegen op, en maakt het resultaat dan de huidige URL.

Een client vraagt om http://books.test/v0/books/2. Dat antwoord zegt location: /books/2. Het tweede antwoord zegt location: 3. Om welke URL vraagt de client als derde?

Hier alleen http

urljoin lost elke URL op, ook een met scheme https of ftp. Een location kan daar best naar wijzen: een server kan location: https://books.test/books/2 sturen. wire.py spreekt alleen gewone HTTP, zoals build_request sinds hoofdstuk 2 doet, en die doorverwijzing volgen zou betekenen dat je het verzoek verstuurt zonder de versleuteling waar de server om vroeg. Daarom controleert resolve_location het scheme van de opgeloste URL, en werpt het op als dat iets anders is dan http. Hoofdstuk 9 legt uit wat https toevoegt en waarom de site het niet kan laten zien.

Host, poort, target

http.client neemt geen URL aan. HTTPConnection neemt een host en een poort, en conn.request neemt een en een request-target. De volger heeft dus nog één stap nodig: een absolute URL opsplitsen in die drie delen. Precies dit deed je in hoofdstuk 2, verspreid over build_request en fetch:

  • urlsplit(url).hostname is de host.

  • urlsplit(url).port is de poort, of None als de URL er geen noemt; dan is het 80, de standaard voor http.

  • Het target is het pad, gevolgd door ? en de querystring als die er is. Een leeg pad, zoals in http://books.test, wordt /.

  • Het fragment, het deel na #, wordt weggelaten. Het wordt nooit verstuurd.

Een browser die een doorverwijzing volgt, neemt het fragment van de oude URL mee naar de nieuwe, zodat de pagina nog steeds naar dezelfde plek scrolt. Je client stuurt helemaal geen fragment, dus hij heeft niets mee te nemen.

Jouw opdracht

Schrijf in wire.py twee functies:

  1. resolve_location(request_url, location) geeft urljoin(request_url, location) terug, en werpt ValueError op als het scheme van het resultaat niet http is.

  2. url_parts(url) geeft voor een http-URL een terug met "host", "port" en "target", met poort 80 als de URL er geen noemt, / voor een leeg pad, de querystring behouden en geen fragment.

Samen zijn ze ongeveer elf regels. Probeer ze uit:

python -c "
import wire
print(wire.resolve_location('http://books.test/v0/books/2?x=1', '/books/2?x=1'))
print(wire.resolve_location('http://books.test/books/2', '3'))
print(wire.url_parts('http://books.test:8080/books/2?x=1#top'))
"

De eerste twee regels zijn http://books.test/books/2?x=1 en http://books.test/books/3, en de dict heeft host books.test, poort 8080 en target /books/2?x=1. Niets uit de wordt beoordeeld.

Klik daarna op Check my work. De clientkant kan nu van elke location een verzoek maken. De serverkant heeft nog niets te versturen: de volgende les laat de leeslijst zijn oude paden met doorverwijzingen beantwoorden.

Opdracht

Schrijf resolve_location en url_parts in wire.py.

  • resolve_location(request_url, location) geeft de absolute terug die location aanduidt als die terugkwam van request_url: urljoin(request_url, location). Als het scheme van dat resultaat niet http is, zoals https of ftp, werp je in plaats daarvan op.

  • url_parts(url) geeft voor een http-URL een terug met drie sleutels: "host", de hostnaam uit urlsplit; "port", een int, 80 als de URL geen noemt; en "target", het pad gevolgd door ? en de querystring als die er is, met / voor een leeg pad en zonder het fragment.

Voer de python -c-regels uit de les uit in de om drie resultaten te zien.

Klik daarna op Check my work. Die roept beide aan met URL’s en location- die je nog niet hebt gezien: een waarde die met / begint, een met ../, een kale naam tegen een URL met en zonder slash aan het eind, een waarde die alleen een querystring is, een die met // begint, een absolute http-URL, en https- en ftp-waarden die ValueError moeten opwerpen; daarna URL’s met en zonder poort, met een leeg pad, met een querystring en met een fragment. Elk verwacht antwoord wordt exact gecontroleerd. Niets uit je terminal wordt beoordeeld.