Weiterleitungen · Übung
Löse eine relative location auf
Ein Server, der ein Buch verschiebt, antwortet mit einem Header wie location: /books/2. Das ist keine Host eine vollständige URL bauen (Kapitel 2, Lektion 4), aber er müsste diesen location kopieren, und ein Proxy vor ihm hat ihn vielleicht umgeschrieben; ein relativer Wert lässt den Client, der die angefragte URL kennt, das Auflösen übernehmen. Die Regeln für das Auflösen sind die, die jeder Browser für Links verwendet, aufgeschrieben in RFC 3986, Abschnitt 5. Diese Lektion schreibt die zwei location auflöst, und eine, die das Ergebnis in das aufteilt, was http.client verlangt.
Die Funktion urljoin
Diese Regeln schreibst du nicht selbst. urljoin, aus demselben urllib.parse wie urlsplit, nimmt die angefragte URL und den location-Wert und gibt die absolute URL zurück, die beide zusammen benennen. Es ist oben in wire.py bereits importiert. Hier sind die üblichen Formen von location, jede gegen eine Anfrage-URL aufgelöst:
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"))
Die Ausgabe, Zeile für Zeile:
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
Ein Wert, der mit / beginnt, ersetzt den ganzen Pfad und die Query und behält Schema, Host und Port. Ein Wert, der mit // beginnt, ersetzt auch den Host. Ein Wert, der mit ? beginnt, behält den Pfad und ersetzt nur die Query. Eine absolute URL wird unverändert zurückgegeben. Alles andere, etwa ../books/2 oder 3, wird wie ein Dateiname relativ zu dem Ordner aufgelöst, auf den die Anfrage-URL zeigt: der Pfad bis einschließlich seines letzten /. Für /v0/books/2 ist dieser Ordner /v0/books/, also geht .. eine Ebene hoch zu /v0/, und books/2 landet wieder bei /v0/books/2.
Die Ordnerregel hat eine Überraschung: Ob die Anfrage-URL mit einem Schrägstrich endet, ändert den Ordner.
Was gibt das aus?
from urllib.parse import urljoin
print(urljoin("http://books.test/v0/books/2", "/books/2"))
print(urljoin("http://books.test/books", "2"))
Die zweite Zeile ist http://books.test/2, nicht http://books.test/books/2. /books hat keinen Schrägstrich am Ende, also wird sein letztes Segment wie ein Dateiname behandelt, und 2 ersetzt es. Ein Server, der /books/2 meint, sollte das mit einem Wert sagen, der mit / beginnt, und genau das wird die Leseliste tun.
urljoin dekodiert und kodiert nichts: %C3%A6 in einem location bleibt %C3%A6 im Ergebnis, bereit für eine Anfragezeile.
Jede location gegen ihre eigene URL
Eine Weiterleitung kann zu einer weiteren Weiterleitung führen. Jedes location wird gegen die URL aufgelöst, die es zurückgegeben hat, und das ist nach dem ersten Hop nicht mehr die URL, mit der du angefangen hast. Ein Client, der Weiterleitungen folgt, führt eine aktuelle URL, löst jedes neue location gegen sie auf und macht das Ergebnis dann zur aktuellen URL.
Ein Client fragt nach http://books.test/v0/books/2. Diese Antwort sagt location: /books/2. Die zweite Antwort sagt location: 3. Nach welcher URL fragt der Client als Drittes?
Hier nur http
urljoin löst jede URL auf, auch eine mit dem Schema https oder ftp. Ein location kann durchaus dorthin zeigen: Ein Server kann location: https://books.test/books/2 senden. wire.py spricht nur einfaches HTTP, wie build_request seit Kapitel 2, und dieser Weiterleitung zu folgen, hieße, die Anfrage ohne die Verschlüsselung zu senden, die der Server verlangt hat. Deshalb prüft resolve_location das Schema der aufgelösten URL und löst http ist. Kapitel 9 erklärt, was https hinzufügt und warum die Website es nicht zeigen kann.
Host, Port, Ziel
http.client nimmt keine URL. HTTPConnection nimmt einen Host und einen Port, und conn.request nimmt eine build_request und fetch:
urlsplit(url).hostnameist der Host.urlsplit(url).portist der Port, oderNone, wenn die URL keinen nennt; dann ist es 80, der Standard fürhttp.Das Ziel ist der Pfad, dann
?und die Query, wenn es eine gibt. Ein leerer Pfad, wie inhttp://books.test, wird zu/.Das Fragment, der Teil nach
#, wird weggelassen. Es wird nie gesendet.
Ein Browser, der einer Weiterleitung folgt, überträgt das Fragment der alten URL auf die neue, damit die Seite immer noch an dieselbe Stelle scrollt. Dein Client sendet überhaupt kein Fragment, also hat er nichts zu übertragen.
Deine Aufgabe
Schreib in wire.py zwei Funktionen:
resolve_location(request_url, location)gibturljoin(request_url, location)zurück und löstValueErroraus, wenn das Schema des Ergebnisses nichthttpist.url_parts(url)gibt für einehttp-URL einmit "host","port"und"target"zurück, mit Port 80, wenn die URL keinen nennt,/für einen leeren Pfad, mit der Query und ohne Fragment.
Zusammen brauchen sie etwa elf Zeilen. Probier sie aus:
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'))
"
Die ersten beiden Zeilen sind http://books.test/books/2?x=1 und http://books.test/books/3, und das Dictionary hat den Host books.test, den Port 8080 und das Ziel /books/2?x=1. Nichts aus dem
Drück dann Check my work. Die Client-Seite kann jetzt jedes location in eine Anfrage verwandeln. Die Server-Seite hat noch nichts zu senden: Die nächste Lektion lässt die Leseliste ihre alten Pfade mit Weiterleitungen beantworten.
Aufgabe
Schreib resolve_location und url_parts in wire.py.
resolve_location(request_url, location)gibt die absolutezurück, die locationbenennt, wenn es vonrequest_urlzurückkam:urljoin(request_url, location). Wenn das Schema dieses Ergebnisses nichthttpist, etwahttpsoderftp, löse stattdessenaus. url_parts(url)gibt für einehttp-URL einmit drei Schlüsseln zurück: "host", den Hostnamen ausurlsplit;"port", ein int, 80, wenn die URL keinennennt; und "target", den Pfad, gefolgt von?und der Query, wenn es eine gibt, mit/für einen leeren Pfad und ohne das Fragment.
Führ die python -c-Zeilen aus der Lektion im
Drück dann Check my work. Es ruft beide location-/ beginnt, einem mit ../, einem bloßen Namen gegen eine URL mit und ohne Schrägstrich am Ende, einem Wert, der nur eine Query ist, einem, der mit // beginnt, einer absoluten http-URL und https- und ftp-Werten, die ValueError auslösen müssen; dann URLs mit und ohne Port, mit leerem Pfad, mit Query und mit Fragment. Jede erwartete Antwort wird exakt geprüft. Nichts aus deinem Terminal wird bewertet.