0%

Chapter 9

Who May Connect?

What a Browser Client Looks Like

Scoreboard v2 keeps every viewer current and drops viewers that stop answering. It also lets anyone who reaches /score change the scores: scorekeeper_handler applies a score from whoever connected, and nothing asks who that is. This chapter makes the server ask. First, look at who would connect. The scoreboard’s real viewers would be web pages, and a web page speaks through the browser’s own client.

A viewer page and a scorekeeper page

Here is the part of a viewer page that talks to the scoreboard, in , the language browsers run:

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

And the part of a scorekeeper page that sends a score:

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

These snippets were written for this lesson. They are not served or run anywhere on this site, and nothing here can connect a browser to the server in your workspace; you only need to read them. Each line has a partner in the Python client you wrote in Chapter 6:

  • new WebSocket(url) plays the part of connect(url): it opens the , sends the upgrade request and checks the 101 and its accept . It does not wait for any of that, though. It returns at once, and the browser calls the page’s onopen when the is done. That is why the scorekeeper page sends from inside onopen.

  • socket.onmessage = ... hands the browser a function. Where your Python client called recv in a , the page never asks for a message: the browser calls this function once for each message that arrives, with the message’s text in event.data.

  • JSON.parse and JSON.stringify are json.loads and json.dumps. The envelopes are the same ones Chapter 7 defined: a snapshot comes in, a score goes out.

  • send is send.

showScores is the page’s own drawing function, not part of the WebSocket API.

A browser's viewer page does this:

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

What does onmessage correspond to in your Python client?

What the browser decides for you

Your Python client could add any header it liked to the upgrade request. A web page cannot. The browser’s WebSocket constructor takes a and, optionally, a of subprotocols, and nothing else; that is the whole interface the WHATWG WebSockets Standard gives a page. The browser writes the handshake itself: Host, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, any it holds for the server, and an Origin header naming the page that opened the connection, which Lesson 4 is about. The page’s script has no way to add a header to that request.

So Authorization: Bearer <token>, the header you read on every write in with Python, is out of reach for a browser client. Programs that are not browsers have no such limit: the library’s Python client, or any script, can send whatever headers it wants. (Some services squeeze a token into the subprotocol list instead; this course does not.)

Why can a browser's scorekeeper page not send Authorization: Bearer ... with its WebSocket handshake?

Where the token goes instead

The page does choose the URL, so a token can travel in the query :

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

The library hands the server the whole request target, query included: connection.request.path for that handshake is /score?token=<the token>, which is why handle_connection already cuts it with urlsplit before comparing paths. Next lesson’s check accepts the token from either place: a Bearer header, from a program that can send one, or a token value in the query, from a page that cannot. A token with characters that are special in URLs must be percent-encoded there, as any query value is.

The query has a cost. URLs get written down: in the server’s access log (and in a library’s own debug log, when that is turned on), in the logs of any proxy on the way, in the browser’s history, in the developer tools. A token in a URL can be read later by anyone who can read those places. So a token that travels in the query should be short-lived: issued for one session and expired soon after, so that a copy found in a log is already useless.

The other route is a cookie. The browser sends the cookies it holds for the server’s site with the handshake, as it does with any request, so the page’s script never touches the token at all. The catch is that the browser may attach the cookie on its own even when the page that opened the connection belongs to someone else. Lesson 4 looks at that problem.

What is the main cost of putting the token in the query string?

Optional: watch a browser client

This step is not checked. If you want to see a real browser client at work, open your browser’s developer tools on a page of this site that shows the , choose the Network panel, and filter it to WebSocket connections. If the page holds a WebSocket connection, select it to see the upgrade request the browser wrote, the 101 that answered it, and the going each way. If you see none, nothing is wrong: this step only shows a real browser client at work.

Next lesson writes the check itself: a function that finds the token in a Bearer header or in the query, compares it with the expected one, and answers 401 before any 101 is sent.