0%

Kapitel 9

Wer darf sich verbinden?

Wie ein Browser-Client aussieht

Anzeigetafel v2 hält jeden Zuschauer auf dem neuesten Stand und trennt Zuschauer, die nicht mehr antworten. Sie lässt aber auch jeden, der /score erreicht, die Punktestände ändern: scorekeeper_handler wendet eine Wertung von jedem an, der sich verbunden hat, und nichts fragt, wer das ist. Dieses Kapitel bringt den Server dazu zu fragen. Schau dir zuerst an, wer sich verbinden würde. Die echten Zuschauer der Anzeigetafel wären Webseiten, und eine Webseite spricht über den eigenen Client des Browsers.

Eine Zuschauerseite und eine Punktezählerseite

Hier ist der Teil einer Zuschauerseite, der mit der Anzeigetafel spricht, in , der Sprache, die Browser ausführen:

const socket = new WebSocket("ws://127.0.0.1:8765/watch");
socket.onmessage = (event) => {
  const snapshot = JSON.parse(event.data);
  showScores(snapshot.version, snapshot.scores);
};

Und der Teil einer Punktezählerseite, der eine Wertung sendet:

const keeper = new WebSocket("ws://127.0.0.1:8765/score");
keeper.onopen = () => {
  keeper.send(JSON.stringify({type: "score", team: "Owls", points: 3}));
};

Diese Snippets wurden für diese Lektion geschrieben. Sie werden nirgendwo auf dieser Seite bereitgestellt oder ausgeführt, und nichts hier kann einen Browser mit dem Server in deinem Arbeitsbereich verbinden; du musst sie nur lesen. Jede Zeile hat ein Gegenstück im Python-Client, den du in Kapitel 6 geschrieben hast:

  • new WebSocket(url) übernimmt die Rolle von connect(url): Es öffnet die , sendet die Upgrade-Anfrage und prüft die 101 und ihren Accept-. Es wartet aber auf nichts davon. Es kehrt sofort zurück, und der Browser ruft die onopen der Seite auf, wenn der fertig ist. Deshalb sendet die Punktezählerseite aus onopen heraus.

  • socket.onmessage = ... übergibt dem Browser eine Funktion. Wo dein Python-Client recv in einer aufgerufen hat, fragt die Seite nie nach einer Nachricht: Der Browser ruft diese Funktion einmal für jede ankommende Nachricht auf, mit dem Text der Nachricht in event.data.

  • JSON.parse und JSON.stringify sind json.loads und json.dumps. Die Umschläge sind dieselben, die Kapitel 7 definiert hat: Ein snapshot kommt herein, eine score geht hinaus.

  • send ist send.

showScores ist die eigene Zeichenfunktion der Seite, kein Teil der WebSocket-API.

Welchem Teil deines Python-Clients entspricht onmessage im Snippet?

Was der Browser für dich entscheidet

Dein Python-Client konnte der Upgrade-Anfrage jeden Header hinzufügen, den er wollte. Eine Webseite kann das nicht. Der Konstruktor WebSocket des Browsers nimmt eine und optional eine von Subprotokollen, sonst nichts; das ist die ganze Schnittstelle, die der WebSockets-Standard der WHATWG einer Seite gibt. Der Browser schreibt den Handshake selbst: Host, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, alle , die er für den Server hat, und einen Origin-Header, der die Seite nennt, die die Verbindung geöffnet hat, und um den es in Lektion 4 geht. Das Skript der Seite hat keine Möglichkeit, dieser Anfrage einen Header hinzuzufügen.

Authorization: Bearer <token>, der Header, den du in with Python bei jedem Schreibzugriff gelesen hast, ist für einen Browser-Client also unerreichbar. Programme, die keine Browser sind, haben keine solche Grenze: Der Python-Client der Bibliothek oder jedes Skript kann senden, welche Header es will. (Manche Dienste quetschen ein Token stattdessen in die Liste der Subprotokolle; dieser Kurs tut das nicht.)

Warum kann die Punktezählerseite eines Browsers Authorization: Bearer ... nicht mit ihrem WebSocket-Handshake senden?

Wohin das Token stattdessen kommt

Die URL wählt die Seite selbst, also kann ein Token im Query- reisen:

ws://127.0.0.1:8765/score?token=<the token>

Die Bibliothek übergibt dem Server das ganze Anfrageziel samt Query: connection.request.path ist für diesen Handshake /score?token=<the token>, und deshalb schneidet handle_connection ihn schon mit urlsplit, bevor es Pfade vergleicht. Die Prüfung der nächsten Lektion akzeptiert das Token von beiden Stellen: aus einem Bearer-Header, von einem Programm, das einen senden kann, oder als token-Wert in der Query, von einer Seite, die das nicht kann. Ein Token mit Zeichen, die in URLs eine besondere Bedeutung haben, muss dort prozentcodiert werden, wie jeder Query-Wert.

Die Query hat ihren Preis. URLs werden aufgeschrieben: im Zugriffslog des Servers (und im eigenen Debug-Log einer Bibliothek, wenn es eingeschaltet ist), in den Logs jedes Proxys unterwegs, im Verlauf des Browsers, in den Entwicklertools. Ein Token in einer URL kann später von jedem gelesen werden, der diese Stellen lesen kann. Ein Token, das in der Query reist, sollte also kurzlebig sein: für eine Sitzung ausgestellt und bald danach abgelaufen, damit eine Kopie, die in einem Log gefunden wird, schon nutzlos ist.

Der andere Weg ist ein Cookie. Der Browser sendet die Cookies, die er für die Website des Servers hat, mit dem Handshake, wie bei jeder Anfrage, also berührt das Skript der Seite das Token überhaupt nicht. Der Haken ist, dass der Browser das Cookie vielleicht von sich aus anhängt, selbst wenn die Seite, die die Verbindung geöffnet hat, jemand anderem gehört. Lektion 4 schaut sich dieses Problem an.

Was ist der wichtigste Preis dafür, das Token in den Query-String zu legen?

Optional: einem Browser-Client zuschauen

Dieser Schritt wird nicht geprüft. Wenn du einen echten Browser-Client bei der Arbeit sehen willst, öffne die Entwicklertools deines Browsers auf einer Seite dieser Website, die das zeigt, wähle den Bereich Network und filtere nach WebSocket-Verbindungen. Wenn die Seite eine WebSocket-Verbindung hält, wähle sie aus, um die Upgrade-Anfrage zu sehen, die der Browser geschrieben hat, die 101, die sie beantwortet hat, und die in beide Richtungen. Wenn du keine siehst, ist nichts falsch: Dieser Schritt zeigt nur einen echten Browser-Client bei der Arbeit.

Die nächste Lektion schreibt die Prüfung selbst: eine Funktion, die das Token in einem Bearer-Header oder in der Query findet, es mit dem erwarteten vergleicht und 401 antwortet, bevor irgendeine 101 gesendet wird.