Asking to Switch Protocols · practice
Compute the Accept Value
Your parser now hands the server a headers["sec-websocket-key"] holds the client’s key as a stripped Sec-WebSocket-Accept, computed from that key. This lesson computes it, and RFC 6455 gives you an exact answer to check against.
What the value proves
RFC 6455 section 1.3 explains why the server must compute something at all. The server has to show the client that it received this Sec-WebSocket-Key. Because the client picks a new key for every request, no stored or made-up answer will match. So when the value in the reply is the one the client expects, the client knows its request reached something that understood it.
That is all it proves. The recipe below is public and uses nothing secret, so anyone can compute the value for any key. It does not show who runs the server, it encrypts nothing, and it is not a password. It is a check that both sides are speaking the same protocol.
The recipe
The RFC fixes one more ingredient, a 36-character string that every WebSocket server in the world uses, written in the RFC’s section 1.3:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
It is in ws_handshake.py as the constant GUID. The RFC chose it because programs that do not understand WebSockets are unlikely to use it, which is what makes the result meaningful. The accept value is:
the key text, with
GUIDappended to it;encoded to bytes with
"ascii", since every character in both is ASCII;hashed with SHA-1, giving a 20-byte digest;
written in
, giving 28 characters; decoded back to a
str, because it goes into a header as text.
You know both tools from hashlib turned a body into a fingerprint in Chapter 8, there with hashlib.sha1: it takes bytes and returns a hash .digest() is the raw bytes and whose .hexdigest() is the same value as hexadecimal text, two characters per byte. SHA-1 is no longer trusted for security work, but the RFC does not use it for security; it uses it as a fixed recipe both sides can follow. base64 carried bytes as header text in Chapter 7.
The digest and the hex text are easy to confuse, so compare them on a value that has nothing to do with WebSockets:
It prints 20 40 v5l2ih1SSK6sFm0W6rvnBjTvc4I=. The 20 bytes of the digest become 40 hexadecimal characters, or 28 base64 characters with one = of padding. The accept value is the base64 of the digest, the third of those. The 40-character hex text is a different value, and a client comparing it with the 28 characters it expects refuses the handshake.
What does this print?
import hashlib
sha = hashlib.sha1(b"x")
print(len(sha.digest()), len(sha.hexdigest()))
Text, not decoded bytes
The key is base64, and you decoded it in Lesson 1 to see its 16 bytes. That makes it tempting to hash the decoded bytes. The RFC does not: it hashes the key exactly as the client sent it, the 24 characters of the header value with surrounding spaces removed, followed by the GUID. Your parser already stripped the value, so key + GUID is the right text as it stands.
What goes into SHA-1?
For the RFC’s example key, dGhlIHNhbXBsZSBub25jZQ==, the result is the value RFC 6455 prints:
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
If your
Your task
In ws_handshake.py, write accept_value(key): return the base64 text of the SHA-1 digest of key + GUID, encoded as ASCII, as a str. Two lines are enough. Leave the functions for Lessons 4 to 6 as they are.
Press Run. The Lesson 3 section of handshake_demo.py now prints the accept value for the RFC’s key, under the dictionary from Lesson 2:
Lesson 3: accept_value
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
That is a line from a local run of the finished lesson. Compare it, character by character, with the RFC’s value above. When they match, press Submit. The check calls accept_value with the RFC’s key and with keys you have not seen, and it checks that the result is text, not bytes.
The server can now compute its proof. The next lesson decides when to send it, and what to send instead when the request is not one it should accept.
Task
Write accept_value(key) in ws_handshake.py.
Append the constant GUID to the key text, encode the result with "ascii", take the SHA-1 digest with hashlib.sha1(...).digest(), encode the digest with base64.b64encode, and return it as a str. Use the key exactly as you receive it, as text; do not decode it first.
Leave the s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, the
Then press Submit. The check calls accept_value with the RFC’s key and with three keys you have not seen, compares each result with the value the RFC’s recipe gives, and checks that the result is a str. When a result is wrong, the message names the step it points to: the GUID, the digest form, the hash, or the decoded key. Importing ws_handshake.py must print nothing.