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 os.urandom(16) returns 16 bytes from the 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
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 connect= parameter you met in Networks with Python: a function passed as a
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 host is text such as server.example.com, or a name and a 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
Start with the status line, cut with split(" ", 2) as in 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 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:
upgradehas the tokenwebsocket, byhas_token, soWebSocketpasses andwebsocketsdoes not;connectionhas the tokenupgrade, byhas_token;sec-websocket-acceptequalsaccept_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:
make_client_key(random_bytes=os.urandom): callrandom_bytes(16)once and return the base64 of the result as astr.build_upgrade_request(host, path, key): the request above, as bytes.check_accept_response(data, key): raiseValueErrorunlessdatais 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 ws_handshake.py, in this order. Submit names the first one that still raises NotImplementedError, so you meet one at a time.
make_client_key(random_bytes=os.urandom)callsrandom_bytes(16)once and returns theof those bytes as a str. Call the, never os.urandomdirectly, so a caller can pass its own function.build_upgrade_request(host, path, key)returns bytes:GET,pathandHTTP/1.1on the request line, thenHost:withhostas given,Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-Key:withkey,Sec-WebSocket-Version: 13, and the empty line. Every line ends in\r\n. Send noOriginand noSec-WebSocket-Protocolline.check_accept_response(data, key)takes a reply head asread_headreturns it, without the final\r\n\r\n. Decode it with"iso-8859-1"and cut the status line withsplit(" ", 2). Raiseif the line has no code or the code is not exactly 101. Then read the headers withparse_headersand raiseValueErrorunlessupgradehas the tokenwebsocket,connectionhas the tokenupgrade, andsec-websocket-acceptequalsaccept_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.