Asking to Switch Protocols · practice
Accept or Refuse the Upgrade
Your server can read a request and compute the accept check_upgrade(request), which takes the
The yes
For the RFC’s example request, the server’s answer is this, every line ending in \r\n:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The status 101 Switching Protocols is in the 1xx class you met in Upgrade: websocket and Connection: Upgrade repeat what the client asked for, now as the server’s agreement. Sec-WebSocket-Accept carries the value from Lesson 3. Then comes the empty line, and nothing else: no Content-Length, no body.
After that empty line, HTTP is finished on this connection. The bytes that follow, in either direction, are
What may the server send right after the empty line that ends its 101 reply?
Your server never sends Sec-WebSocket-Protocol or Sec-WebSocket-Extensions in its 101, because this course negotiates neither.
Tokens, not exact values
Before it says yes, the server checks that the client really asked. The Upgrade header must contain the token websocket, and Connection must contain the token upgrade, both without regard to case. Lesson 1 showed why an exact comparison fails: Connection: keep-alive, Upgrade asks for the upgrade just as well as Connection: Upgrade does.
has_token(value, token) answers the question for any comma-separated header value. Cut the value at every comma, strip each piece, lowercase it, and compare it with the token, also lowercased:
What does this print?
value = "keep-alive, Upgrade"
for item in value.split(","):
print(item.strip().lower())
It is tempting to write "upgrade" in value.lower() instead. That finds upgrade inside upgrades too, and inside any longer word that contains it. Comparing whole tokens finds only the token, so has_token("upgrades", "upgrade") is False.
Checking the key
A key must be the base64.b64decode will tell you, with one ! in front of the RFC’s, !!!!dGhlIHNhbXBsZSBub25jZQ==, still decodes to the 16 bytes of the sample nonce. With validate=True it refuses that key and raises binascii.Error. Missing padding raises the same error either way.
binascii.Error is a kind of except ValueError: around the decode catches it. After a successful decode, check the length with len: dGhl is valid base64 but decodes to only 3 bytes.
Remember the doubled key from Lesson 2. Two Sec-WebSocket-Key lines arrive as one value with ", " in the middle, and a comma and a space are not base64 characters, so this check refuses that request too.
Two ways to say no
RFC 6455 lets a server refuse a
400 Bad Request answers every request that is malformed or incomplete. check_upgrade returns a 400 when:
the
is not GET;the version is not exactly
HTTP/1.1;there is no
hostheader;upgradehas nowebsockettoken, or is missing;connectionhas noupgradetoken, or is missing;sec-websocket-keyorsec-websocket-versionis missing;the key is not strict base64;
the key does not decode to 16 bytes.
The RFC allows any version from HTTP/1.1 up, but no later version of HTTP uses this handshake, so your server accepts HTTP/1.1 exactly.
426 Upgrade Required answers a request that is complete and well formed but asks for a WebSocket version other than 13. It carries a Sec-WebSocket-Version: 13 header, which tells the client the version this server speaks, so a client that knows several versions can try again with that one. Check the version last, after every 400 rule has passed. Do not confuse it with 505 HTTP Version Not Supported, which is about the HTTP version in the request line, not the WebSocket version. RFC 6455 suggests 426 here and requires only the Sec-WebSocket-Version header, so 426 is this course’s choice: it names the reason for the refusal.
Both refusals are 4xx statuses, the class that says the client’s request is at fault. refuse(status) builds either one: the status line, for 426 the version header, then Content-Length: 0 and Connection: close, then the empty line. Content-Length: 0 says no body follows, and Connection: close says the server closes the connection after this answer. The two responses are:
HTTP/1.1 400 Bad Request
Content-Length: 0
Connection: close
HTTP/1.1 426 Upgrade Required
Sec-WebSocket-Version: 13
Content-Length: 0
Connection: close
A real server may refuse with other statuses as well. The RFC’s own example of a version mismatch answers 400 Bad Request with a Sec-WebSocket-Version header, and a server that checks where a connection comes from might answer 403 Forbidden, as Chapter 9 does. These two cover everything your server checks now.
A request is complete and valid except for Sec-WebSocket-Version: 8. What does check_upgrade return?
RFC 6455 does not say in which order a server must check these rules, so a request with two faults could fairly get either status. Your server checks in the order of the
Your task
In ws_handshake.py, write these three
has_token(value, token):Trueif any comma-separated item ofvalue, stripped and lowercased, equalstokenlowercased.refuse(status): the complete 400 or 426 response above, as bytes.check_upgrade(request): a 400 or a 426 by the rules above, otherwise the 101 response withaccept_valueof the key, as bytes. Useheaders.get(name, "")for a header that may be missing.
Press Run. The Lesson 4 section prints the 101 response for the RFC’s request as a bytes value, \r\n and all:
Lesson 4: check_upgrade
b'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n'
That line is from a local run of the finished lesson. When yours matches, press Submit. The server side of the handshake is complete; the next lesson writes the client that asks.
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.
has_token(value, token)returnsTrueif any item of the comma-separatedvalue, stripped and lowercased, equalstokenlowercased, andFalseotherwise. A token inside a longer word does not count:has_token("upgrades", "upgrade")isFalse.refuse(status)returns bytes: for 400,HTTP/1.1 400 Bad Request; for 426,HTTP/1.1 426 Upgrade Requiredfollowed bySec-WebSocket-Version: 13; thenContent-Length: 0,Connection: closeand the empty line. Every line ends in\r\n.check_upgrade(request)takes ashaped like the one parse_upgrade_requestreturns and returns bytes. It returnsrefuse(400)if theis not GET, the version is not exactlyHTTP/1.1,hostis missing,upgradehas nowebsockettoken,connectionhas noupgradetoken, the key or the version header is missing, or the key is not strict( base64.b64decode(key, validate=True)) of exactly 16 bytes. Otherwise it returnsrefuse(426)if the version is not13, and the 101 response if it is:HTTP/1.1 101 Switching Protocols,Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-Accept:andaccept_value(key), then the empty line and nothing after it.
Leave the functions for Lessons 5 and 6 as they are. Press Run to see the 101 response for the RFC’s request.
Then press Submit. The check calls has_token on values you have not seen, and check_upgrade with request dictionaries it builds itself, so it does not depend on your parser: the RFC’s request, a valid request with a key you have not seen and the tokens written in other ways, ten requests that must get 400, each with exactly one fault, and one that must get 426. It reads each response as a client would: the status line, the headers by lowercase name, the empty line at the end, and either no body or a body as long as its Content-Length. Header order does not matter, and a refusal may carry extra headers or a short body with the right Content-Length; a 101 may carry neither a body nor a protocol or extensions header. Importing ws_handshake.py must print nothing.