Papers
The WebSocket Protocol
Ian Fette, Alexey Melnikov · IETF RFC 6455, 2011
The problem: simulating bidirectional communication on top of HTTP
The RFC starts from a practical observation. Web applications that needed to exchange data in both directions between browser and server had been bending HTTP to do it. The usual approach was to poll the server repeatedly (long polling is a variant) and to send each client-to-server message as a separate HTTP call. The document lists the costs of this approach. The server has to use several TCP connections per client: one to send information to the client and a new one for each message arriving from the client. Every message from client to server carries the overhead of a full set of HTTP headers. And the client script has to do the work of matching each request sent with the response received that corresponds to it. The RFC presents WebSocket as a simpler alternative: a single TCP connection that carries traffic in both directions. In the document's own words, the protocol enables two-way communication between a client running untrusted code in a controlled environment and a remote host that has opted in to communications from that code. It reuses the origin-based security model that browsers already enforce, and consists of an opening handshake followed by a thin layer of message framing on top of TCP. The stated goal is to give browser applications two-way communication with servers without relying on opening multiple HTTP connections (as in long polling). The RFC presents it as a replacement for the bidirectional techniques that use HTTP as the transport.
The opening handshake: an HTTP request that upgrades the connection
A WebSocket connection starts out as an ordinary GET request, in HTTP/1.1 or higher, which lets it pass through infrastructure that already understands HTTP. The client sends headers saying it wants to switch protocols: Upgrade with the value websocket, Connection with the value Upgrade, Sec-WebSocket-Version 13, and a Sec-WebSocket-Key with a random base64 value (which, decoded, is 16 bytes). Browser clients also must send an Origin header; for other clients, it is optional. The client can also list subprotocols it knows how to speak. If the server accepts, it responds with status 101 Switching Protocols and its own Upgrade and Connection headers. It also adds Sec-WebSocket-Accept, which it computes by concatenating a fixed GUID, defined in the RFC, to the client's key, applying SHA-1 to the result, and base64-encoding the digest. The client checks this value, which shows that the server really understood the WebSocket handshake. After that, the same TCP connection starts carrying WebSocket frames instead of HTTP. The server can also refuse: it can respond 400 to a malformed request, 403 for an origin it does not allow, or 426 Upgrade Required along with a Sec-WebSocket-Version header listing the versions it supports. If the server picks one of the offered subprotocols, it indicates the choice in the Sec-WebSocket-Protocol response header.
Framing and masking: messages, fragments and masks from client to server
After the handshake, data travels in frames. Each frame has a FIN bit that marks the last fragment of a message, three reserved bits that must be zero unless an extension defines them, and a 4-bit opcode. The opcodes include 0x1 for text, 0x2 for binary, and 0x0 for the continuation of a fragmented message; 0x8, 0x9, and 0xA are the control frames covered in the next section. The payload length uses a variable-length encoding: values from 0 to 125 are the length itself, 126 means the next two bytes carry a 16-bit length, and 127 means the next eight bytes carry a 64-bit length. The shortest possible encoding must be used. A message can be split across several frames: the first has FIN cleared and a nonzero opcode, the middle ones use opcode 0 with FIN cleared, and the last one uses opcode 0 with FIN set. Without extensions, the message is equivalent to a single message whose payload is the concatenation, in order, of the fragments' payloads. Fragments must arrive in order and cannot be interleaved with fragments of another message, unless an extension capable of interpreting the interleaving has been negotiated; control frames may appear between them. Every frame sent from the client to the server must be masked: MASK bit set to 1 and a 4-byte key, with each payload byte XORed with the key byte at position i mod 4. Servers must not mask their frames. A server that receives an unmasked frame must close the connection, and a client that receives a masked frame must do the same.
Control frames: ping, pong and the closing handshake
The protocol defines three control frames. All have a payload limited to 125 bytes or less, and none can be fragmented; they can, however, be interleaved in the middle of a fragmented message. A Ping can carry application data. When a Ping arrives, the receiver must reply with a Pong that echoes the same data, unless it has already received a Close. A Pong can also be sent unsolicited, and no response is expected for it. A Ping can be sent at any time between connection establishment and closure, and the RFC notes that it can serve as a keepalive or as a way to check whether the remote endpoint is still responsive. Closing is also done by message. An endpoint that wants to end the connection sends a Close frame. The body is optional; if present, it must begin with a two-byte status code, optionally followed by UTF-8 text explaining the reason. After sending Close, the application must not send any more data frames, and the peer must reply with its own Close if it has not already sent one. Once both sides have sent and received Close, each considers the connection closed. The server should close the TCP connection immediately; the client should, as a rule, wait for the server to close first, but may close at any time after that, for example if the server is slow. Among the defined status codes are 1000 (normal closure), 1001 (endpoint going away, such as a server shutting down or a browser leaving the page), 1002 (protocol error), 1003 (unsupported data type) and 1009 (message too big).
Security model: origin checking, masking against proxy attacks, and TLS
The RFC grounds security in three separate ideas. First, the origin model. Browsers always send an Origin header in the handshake. A server that wants to serve only certain sites should, as a rule, compare that header with the expected origins and, if the origin is not acceptable, answer the handshake with 403 Forbidden. The RFC is explicit about the limit of this check: its goal is to stop malicious scripts running in a trusted browser. A non-browser client can put any string in Origin, so the server must not treat the header as proof of who is calling and must not trust any input to be valid. Second, masking. The RFC describes an attack in which a script uses an upgraded connection to emit bytes that look like an HTTP request, causing an intermediary proxy to cache a poisoned response for other users. Masking every client-to-server frame with a fresh, unpredictable key keeps the script from controlling the bytes that go over the network. If the next key were predictable, the attacker could pre-mask the message with it, and the client's masking would undo it, leaving something that looks like an HTTP request on the wire. Third, confidentiality and integrity come from running the protocol over TLS using wss URIs, and implementations should support TLS. The protocol does not define its own client authentication; the server may use whatever an ordinary HTTP server would use, such as cookies, HTTP authentication, or TLS authentication. Implementations with their own limits on frame size or reassembled message size should protect themselves against anyone who exceeds them and, as a rule, enforce a limit on those sizes.
See the ideas at work
Practice the designs that use this paper in the System Design school.