0%

Asking to Switch Protocols · practice

Write the Client's Half

Your server can parse a request, decide, and answer. Nothing sends it a request yet. This lesson writes the other side: the client that makes a key, writes the upgrade request, and checks the server’s reply before it believes the protocol has switched.

A fresh key

The client’s key is 16 random bytes in , new for every request. Python’s os.urandom(16) returns 16 bytes from the ’s source of unpredictable randomness, which is the right source for values another program must not be able to guess. base64.b64encode turns them into the 24 characters of a key, and .decode("ascii") makes that text.

A test cannot check a random key, because it cannot know it in advance. So make_client_key takes the source of randomness as a , with the real one as its default:

def make_client_key(random_bytes=os.urandom):
    ...

Inside, call random_bytes(16). In normal use that calls os.urandom(16). A test passes a different instead, one that returns known bytes, and then knows exactly which key comes out. This is the same idea as the connect= parameter you met in Networks with Python: a function passed as a , so a test can swap in its own.

import base64


def fixed(n):
    return bytes(range(n))


key = base64.b64encode(fixed(16)).decode("ascii")

With that function, the key is the same on every run: the base64 of the bytes 0 to 15. A test calls make_client_key(random_bytes=fixed). It passes fixed, the function itself, without parentheses, and make_client_key decides when to call it and with what.

Why does make_client_key take random_bytes as a parameter?

The request

build_upgrade_request(host, path, key) writes the request you read in Lesson 1, minus the two optional lines:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

That is the request for the RFC’s host, path and key; your function puts its own three in their places. host is text such as server.example.com, or a name and a such as 127.0.0.1:50123 when the server is not on the default port, and it goes into the Host line as given. path goes into the request line as given, query and all. Every line ends in \r\n, an empty line ends the head, and the request has no body. Your client sends no Origin line, because it is not a browser, and no Sec-WebSocket-Protocol line, because it asks for no subprotocol. Return the request encoded with "ascii".

Checking the reply

The server answers with a head, which read_head returns without its final \r\n\r\n. check_accept_response(data, key) decides whether that head means yes. It returns nothing when it does, and raises with a message saying why when it does not.

Start with the status line, cut with split(" ", 2) as in with Python lesson 1.4. A line such as HTTP/1.1 alone has no code at all; check how many parts you got before you read the second one, and raise ValueError if the code is missing.

Any status other than 101 means the failed, for this client. RFC 6455 says a client handles any other status “per HTTP procedures”: a 401 could lead to a login, a 3xx to a redirect. Your client follows no redirects and sends no credentials, so for it every other status, a 426 or a 200 or a 100 Continue, is a failed handshake. Compare the code with "101" exactly; another 1xx status is not a switch.

The words after the code, the reason phrase, are for people. A server may write 101 Web Socket Protocol Handshake instead of 101 Switching Protocols, and the handshake is just as good. Decide by the code and the headers, never by the reason.

The reply is HTTP/1.1 426 Upgrade Required. What does check_accept_response do?

A server answers HTTP/1.1 101 Web Socket Protocol Handshake with correct headers. Is that a success?

For a 101, read the headers with your parse_headers and check three of them, the server’s side of what you checked in Lesson 4:

  1. upgrade has the token websocket, by has_token, so WebSocket passes and websockets does not;

  2. connection has the token upgrade, by has_token;

  3. sec-websocket-accept equals accept_value(key) for the key this client sent, exactly.

The last comparison is case-sensitive on purpose. In base64, a and A are different characters standing for different bits, so an accept value that differs only in case is a different value, and it means the server did not compute the answer to this key.

Your task

In ws_handshake.py, write these three functions, in this order:

  1. make_client_key(random_bytes=os.urandom): call random_bytes(16) once and return the base64 of the result as a str.

  2. build_upgrade_request(host, path, key): the request above, as bytes.

  3. check_accept_response(data, key): raise ValueError unless data is a 101 head with the two tokens and the right accept value.

Leave serve_handshake and client_handshake for Lesson 6. Press Run. The Lesson 5 section prints the request for the RFC’s host, path and key, as bytes:

Lesson 5: build_upgrade_request
b'GET /chat HTTP/1.1\r\nHost: server.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n'

That line is from a local run of the finished lesson. When yours matches, press Submit.

You now have both halves of the handshake, each checked on its own. In the project that ends this chapter, they meet over a real connection.

Task

Write three in ws_handshake.py, in this order. Submit names the first one that still raises NotImplementedError, so you meet one at a time.

  1. make_client_key(random_bytes=os.urandom) calls random_bytes(16) once and returns the of those bytes as a str. Call the , never os.urandom directly, so a caller can pass its own function.

  2. build_upgrade_request(host, path, key) returns bytes: GET, path and HTTP/1.1 on the request line, then Host: with host as given, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key: with key, Sec-WebSocket-Version: 13, and the empty line. Every line ends in \r\n. Send no Origin and no Sec-WebSocket-Protocol line.

  3. check_accept_response(data, key) takes a reply head as read_head returns it, without the final \r\n\r\n. Decode it with "iso-8859-1" and cut the status line with split(" ", 2). Raise if the line has no code or the code is not exactly 101. Then read the headers with parse_headers and raise ValueError unless upgrade has the token websocket, connection has the token upgrade, and sec-websocket-accept equals accept_value(key) exactly. Return nothing.

Leave serve_handshake and client_handshake for Lesson 6. Press Run to see the request for the RFC’s example.

Then press Submit. The check passes make_client_key functions of its own that return known bytes and records what they were asked for. It parses the requests build_upgrade_request writes for hosts and paths you have not seen, so header order does not matter. It calls check_accept_response on 101 heads that must pass, one of them with an unusual reason phrase and header casing, and on heads that must raise ValueError: other statuses, missing or wrong tokens, and accept values that do not belong to the key. Importing ws_handshake.py must print nothing.