0%

Asking to Switch Protocols · practice

Compute the Accept Value

Your parser now hands the server a , and in it headers["sec-websocket-key"] holds the client’s key as a stripped . Before the server can say yes, it needs the one its yes must carry: 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 and understood it as one. A server that does not speak , or a cache between the two that answers from a stored reply, has no reason to compute anything from a header called 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:

  1. the key text, with GUID appended to it;

  2. encoded to bytes with "ascii", since every character in both is ASCII;

  3. hashed with SHA-1, giving a 20-byte digest;

  4. written in , giving 28 characters;

  5. decoded back to a str, because it goes into a header as text.

You know both tools from with Python. hashlib turned a body into a fingerprint in Chapter 8, there with . SHA-1 works the same way through hashlib.sha1: it takes bytes and returns a hash whose .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:

Try it

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 gives anything else, compare its steps with the above. A value that has the wrong length usually means the hex text instead of the digest, or a hash other than SHA-1. A value of the right length that still differs means the input was wrong: the GUID missing or mistyped, or the key decoded before hashing.

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 for Lessons 4 to 6 as they are. Press Run: the Lesson 3 section should print s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, the RFC 6455 gives for its example key.

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.