0%

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 , mit der sich ein Client verbinden kann: Sie hat kein Schema, keinen Host und keinen . Es ist ein Verweis relativ zu der URL, nach der der Client gefragt hat, und der Client muss ihn gegen diese URL auflösen, bevor er etwas senden kann. erlaubt das mit Absicht (RFC 9110, Abschnitt 10.2.2). Ein Server könnte aus der Zeile Host eine vollständige URL bauen (Kapitel 2, Lektion 4), aber er müsste diesen in jedes 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 , die der Code aus Lektion 4, der folgt, braucht: eine, die ein 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 aus, wenn es etwas anderes als 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 und ein Anfrageziel. Also braucht der Code, der Weiterleitungen folgt, noch einen Schritt: eine absolute URL in diese drei Teile aufteilen. Genau das hast du in Kapitel 2 getan, verteilt auf build_request und fetch:

  • urlsplit(url).hostname ist der Host.

  • urlsplit(url).port ist der Port, oder None, wenn die URL keinen nennt; dann ist es 80, der Standard für http.

  • Das Ziel ist der Pfad, dann ? und die Query, wenn es eine gibt. Ein leerer Pfad, wie in http://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:

  1. resolve_location(request_url, location) gibt urljoin(request_url, location) zurück und löst ValueError aus, wenn das Schema des Ergebnisses nicht http ist.

  2. url_parts(url) gibt für eine http-URL ein mit "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 wird bewertet.

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 absolute zurück, die location benennt, wenn es von request_url zurückkam: urljoin(request_url, location). Wenn das Schema dieses Ergebnisses nicht http ist, etwa https oder ftp, löse stattdessen aus.

  • url_parts(url) gibt für eine http-URL ein mit drei Schlüsseln zurück: "host", den Hostnamen aus urlsplit; "port", ein int, 80, wenn die URL keinen nennt; 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 aus, um drei Ergebnisse zu sehen.

Drück dann Check my work. Es ruft beide mit URLs und location- auf, die du noch nicht gesehen hast: einem Wert, der mit / 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.