Doorverwijzingen · oefening
Los een relatieve location op
Een server die een boek verhuist, antwoordt met een header zoals location: /books/2. Dat is geen Host (hoofdstuk 2, les 4), maar dan moest hij die 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 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 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 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 build_request en fetch:
urlsplit(url).hostnameis de host.urlsplit(url).portis de poort, ofNoneals de URL er geen noemt; dan is het 80, de standaard voorhttp.Het target is het pad, gevolgd door
?en de querystring als die er is. Een leeg pad, zoals inhttp://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:
resolve_location(request_url, location)geefturljoin(request_url, location)terug, en werptValueErrorop als het scheme van het resultaat niethttpis.url_parts(url)geeft voor eenhttp-URL eenterug 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
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 absoluteterug die locationaanduidt als die terugkwam vanrequest_url:urljoin(request_url, location). Als het scheme van dat resultaat niethttpis, zoalshttpsofftp, werp je in plaats daarvanop. url_parts(url)geeft voor eenhttp-URL eenterug met drie sleutels: "host", de hostnaam uiturlsplit;"port", een int, 80 als de URL geennoemt; 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
Klik daarna op Check my work. Die roept beide location-/ 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.