0%

Hoofdstuk 9

Wie mag er verbinding maken?

Hoe een browserclient eruitziet

Scorebord v2 houdt elke kijker bij en laat kijkers vallen die niet meer antwoorden. Het laat ook iedereen die /score bereikt de scores veranderen: scorekeeper_handler verwerkt een score van wie er ook verbinding maakte, en niets vraagt wie dat is. Dit hoofdstuk laat de server het vragen. Kijk eerst naar wie er verbinding zou maken. De echte kijkers van het scorebord zouden webpagina’s zijn, en een webpagina spreekt via de eigen client van de browser.

Een kijkerspagina en een scorebijhouderspagina

Hier is het deel van een kijkerspagina dat met het scorebord praat, in , de taal die browsers uitvoeren:

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);
};

En het deel van een scorebijhouderspagina dat een score stuurt:

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

Deze fragmenten zijn voor deze les geschreven. Ze worden nergens op deze site geserveerd of uitgevoerd, en niets hier kan een browser verbinden met de server in je werkruimte; je hoeft ze alleen te lezen. Elke regel heeft een tegenhanger in de Python-client die je in hoofdstuk 6 schreef:

  • new WebSocket(url) speelt de rol van connect(url): het opent de , stuurt het upgradeverzoek en controleert de 101 en de acceptwaarde ervan. Het wacht daar alleen niet op. Het keert meteen terug, en de browser roept de onopen van de pagina aan als de klaar is. Daarom stuurt de scorebijhouderspagina vanuit onopen.

  • socket.onmessage = ... geeft de browser een functie. Waar je Python-client recv in een aanriep, vraagt de pagina nooit om een bericht: de browser roept deze functie één keer aan voor elk bericht dat binnenkomt, met de tekst van het bericht in event.data.

  • JSON.parse en JSON.stringify zijn json.loads en json.dumps. De enveloppen zijn dezelfde die hoofdstuk 7 definieerde: er komt een snapshot binnen, er gaat een score uit.

  • send is send.

showScores is de eigen tekenfunctie van de pagina, geen onderdeel van de WebSocket-API.

Waarmee komt onmessage in het fragment overeen in je Python-client?

Wat de browser voor je beslist

Je Python-client kon elke header die hij wilde aan het upgradeverzoek toevoegen. Een webpagina kan dat niet. De constructor WebSocket van de browser neemt een en, optioneel, een subprotocollen, en verder niets; dat is de hele interface die de WHATWG WebSockets Standard een pagina geeft. De browser schrijft de handshake zelf: Host, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, alle die hij voor de server heeft, en een header Origin die de pagina noemt die de verbinding opende, en daarover gaat les 4. Het script van de pagina kan op geen enkele manier een header aan dat verzoek toevoegen.

Dus Authorization: Bearer <token>, de header die je in with Python bij elke schrijfactie las, is buiten bereik voor een browserclient. Programma’s die geen browser zijn, hebben zo’n grens niet: de Python-client van de bibliotheek, of elk script, kan elke header sturen die hij wil. (Sommige services proppen in plaats daarvan een token in de lijst met subprotocollen; deze cursus niet.)

Waarom kan de scorebijhouderspagina in een browser geen Authorization: Bearer ... meesturen met haar WebSocket-handshake?

Waar het token dan heen gaat

De pagina kiest wel de URL, dus een token kan in de querystring meereizen:

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

De bibliotheek geeft de server het hele request-target, query inbegrepen: connection.request.path is voor die handshake /score?token=<the token>, en daarom knipt handle_connection het al met urlsplit voordat hij paden vergelijkt. De controle van de volgende les accepteert het token van beide plekken: een Bearer-header, van een programma dat er een kan sturen, of een token- in de query, van een pagina die dat niet kan. Een token met tekens die in URL’s een speciale betekenis hebben, moet daar met procentcodering worden geschreven, zoals elke querywaarde.

De query heeft een prijs. URL’s worden opgeschreven: in het toegangslog van de server (en in het eigen debuglog van een bibliotheek, als dat aanstaat), in de logs van elke proxy onderweg, in de geschiedenis van de browser, in de ontwikkelaarstools. Een token in een URL kan later worden gelezen door iedereen die die plekken kan lezen. Een token dat in de query meereist, moet daarom kortlevend zijn: uitgegeven voor één sessie en kort daarna verlopen, zodat een kopie die in een log wordt gevonden al nutteloos is.

De andere route is een cookie. De browser stuurt de cookies die hij voor de site van de server heeft met de handshake mee, zoals bij elk verzoek, dus het script van de pagina raakt het token helemaal niet aan. Het addertje is dat de browser het cookie uit zichzelf kan meesturen, ook als de pagina die de verbinding opende van iemand anders is. Les 4 kijkt naar dat probleem.

Wat is de belangrijkste prijs van het token in de querystring zetten?

Optioneel: bekijk een browserclient

Deze stap wordt niet gecontroleerd. Als je een echte browserclient aan het werk wilt zien, open dan de ontwikkelaarstools van je browser op een pagina van deze site die de toont, kies het paneel Network, en filter op WebSocket-verbindingen. Als de pagina een WebSocket-verbinding heeft, selecteer die dan om het upgradeverzoek te zien dat de browser schreef, de 101 die het beantwoordde, en de die beide kanten op gaan. Zie je er geen, dan is er niets mis: deze stap laat alleen een echte browserclient aan het werk zien.

De volgende les schrijft de controle zelf: een functie die het token vindt in een Bearer-header of in de query, het vergelijkt met het verwachte token, en 401 antwoordt voordat er een 101 wordt verstuurd.