Skip to main content

rustlavel_ws/
frame.rs

1//! RFC 6455 framing: the bytes on the wire.
2//!
3//! A frame is a two-byte header, an optional extended length, an optional
4//! four-byte mask, and a payload:
5//!
6//! ```text
7//!  0               1               2               3
8//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
9//! +-+-+-+-+-------+-+-------------+-------------------------------+
10//! |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
11//! |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
12//! |N|V|V|V|       |S|             |   (if payload len==126/127)   |
13//! | |1|2|3|       |K|             |                               |
14//! +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
15//! |     Masking-key (if MASK set) |          Payload Data         |
16//! +-------------------------------+-------------------------------+
17//! ```
18//!
19//! Encoding and decoding are kept apart, the way the PostgreSQL driver keeps
20//! them apart, so both halves can be checked against hand-written bytes.
21//!
22//! Two rules from the RFC drive most of the checks here, and both are
23//! directional: a frame travelling **client → server must be masked**, and a
24//! frame travelling **server → client must not be**. That is why [`Frame::decode`]
25//! takes a [`Role`] rather than guessing.
26
27use crate::error::{WsError, WsResult};
28
29/// Which end of the connection is *reading*.
30///
31/// A server reads client frames (masked); a client reads server frames
32/// (unmasked). Tests use both directions, which is the point of naming it.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Role {
35    Server,
36    Client,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum OpCode {
41    Continuation,
42    Text,
43    Binary,
44    Close,
45    Ping,
46    Pong,
47}
48
49impl OpCode {
50    pub fn bits(self) -> u8 {
51        match self {
52            OpCode::Continuation => 0x0,
53            OpCode::Text => 0x1,
54            OpCode::Binary => 0x2,
55            OpCode::Close => 0x8,
56            OpCode::Ping => 0x9,
57            OpCode::Pong => 0xa,
58        }
59    }
60
61    pub fn from_bits(bits: u8) -> WsResult<OpCode> {
62        match bits {
63            0x0 => Ok(OpCode::Continuation),
64            0x1 => Ok(OpCode::Text),
65            0x2 => Ok(OpCode::Binary),
66            0x8 => Ok(OpCode::Close),
67            0x9 => Ok(OpCode::Ping),
68            0xa => Ok(OpCode::Pong),
69            other => Err(WsError::protocol(format!("opcode {other:#x} is reserved"))),
70        }
71    }
72
73    /// Control frames may be delivered between the fragments of a data message,
74    /// so they are never accumulated into one.
75    pub fn is_control(self) -> bool {
76        matches!(self, OpCode::Close | OpCode::Ping | OpCode::Pong)
77    }
78}
79
80/// A close status code.
81///
82/// A newtype rather than an enum because the range 3000–4999 belongs to the
83/// application and must survive a round trip unchanged.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CloseCode(pub u16);
86
87impl CloseCode {
88    pub const NORMAL: CloseCode = CloseCode(1000);
89    pub const GOING_AWAY: CloseCode = CloseCode(1001);
90    pub const PROTOCOL_ERROR: CloseCode = CloseCode(1002);
91    pub const UNSUPPORTED: CloseCode = CloseCode(1003);
92    /// Never sent on the wire: it means "the peer closed without a code".
93    pub const NO_STATUS: CloseCode = CloseCode(1005);
94    /// Never sent on the wire: it means "the connection died".
95    pub const ABNORMAL: CloseCode = CloseCode(1006);
96    pub const INVALID_PAYLOAD: CloseCode = CloseCode(1007);
97    pub const POLICY: CloseCode = CloseCode(1008);
98    pub const TOO_LARGE: CloseCode = CloseCode(1009);
99    pub const INTERNAL_ERROR: CloseCode = CloseCode(1011);
100
101    pub fn code(self) -> u16 {
102        self.0
103    }
104
105    /// Whether this code is one a peer is allowed to put in a close frame.
106    ///
107    /// 1005, 1006 and 1015 describe how a connection ended locally; sending one
108    /// is a protocol violation, and clients do occasionally try.
109    pub fn is_sendable(self) -> bool {
110        match self.0 {
111            1005 | 1006 | 1015 => false,
112            1000..=1014 => true,
113            3000..=4999 => true,
114            _ => false,
115        }
116    }
117}
118
119/// The body of a close frame: a status code, and optionally a reason.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct CloseFrame {
122    pub code: CloseCode,
123    pub reason: String,
124}
125
126impl CloseFrame {
127    pub fn new(code: CloseCode, reason: impl Into<String>) -> Self {
128        CloseFrame { code, reason: reason.into() }
129    }
130
131    /// Parse a close payload. An empty payload is legal and means "no status".
132    pub fn parse(payload: &[u8]) -> WsResult<Option<CloseFrame>> {
133        match payload.len() {
134            0 => Ok(None),
135            1 => Err(WsError::protocol("close payload must be empty or at least two bytes")),
136            _ => {
137                let code = CloseCode(u16::from_be_bytes([payload[0], payload[1]]));
138                if !code.is_sendable() {
139                    return Err(WsError::protocol(format!(
140                        "close code {} must not be sent on the wire",
141                        code.0
142                    )));
143                }
144                // The reason is required to be UTF-8, and a peer that gets this
145                // wrong is exactly the peer whose text frames cannot be trusted.
146                let reason =
147                    std::str::from_utf8(&payload[2..]).map_err(|_| WsError::InvalidUtf8)?;
148                Ok(Some(CloseFrame { code, reason: reason.to_string() }))
149            }
150        }
151    }
152
153    /// The wire payload: the code, then the reason.
154    ///
155    /// A control frame carries at most 125 bytes and two of them are the code,
156    /// so a long reason is truncated on a character boundary rather than being
157    /// allowed to make an unsendable frame. Reasons are diagnostics, and a
158    /// diagnostic that kills the connection it explains is worse than a short one.
159    pub fn to_payload(&self) -> Vec<u8> {
160        let mut end = self.reason.len().min(123);
161        while end > 0 && !self.reason.is_char_boundary(end) {
162            end -= 1;
163        }
164
165        let mut out = Vec::with_capacity(2 + end);
166        out.extend_from_slice(&self.code.0.to_be_bytes());
167        out.extend_from_slice(&self.reason.as_bytes()[..end]);
168        out
169    }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct Frame {
174    pub fin: bool,
175    pub opcode: OpCode,
176    /// Present only on frames a client sends. A server that masks its frames is
177    /// as wrong as a client that does not.
178    pub mask: Option<[u8; 4]>,
179    pub payload: Vec<u8>,
180}
181
182impl Frame {
183    /// A frame as a server sends it: final, and never masked.
184    pub fn server(opcode: OpCode, payload: Vec<u8>) -> Frame {
185        Frame { fin: true, opcode, mask: None, payload }
186    }
187
188    /// A frame as a client sends it. The mask is supplied rather than generated
189    /// so encoding stays a pure function that a test can pin to exact bytes.
190    pub fn client(opcode: OpCode, payload: Vec<u8>, mask: [u8; 4]) -> Frame {
191        Frame { fin: true, opcode, mask: Some(mask), payload }
192    }
193
194    pub fn encode(&self) -> Vec<u8> {
195        let mut out = Vec::with_capacity(self.payload.len() + 14);
196        out.push(if self.fin { 0x80 } else { 0 } | self.opcode.bits());
197
198        let masked = if self.mask.is_some() { 0x80 } else { 0 };
199        let length = self.payload.len();
200        // The three length forms. The RFC requires the *shortest* one that
201        // fits, so a 125-byte payload never uses the 16-bit form.
202        if length < 126 {
203            out.push(masked | length as u8);
204        } else if length <= u16::MAX as usize {
205            out.push(masked | 126);
206            out.extend_from_slice(&(length as u16).to_be_bytes());
207        } else {
208            out.push(masked | 127);
209            out.extend_from_slice(&(length as u64).to_be_bytes());
210        }
211
212        match self.mask {
213            Some(mask) => {
214                out.extend_from_slice(&mask);
215                let start = out.len();
216                out.extend_from_slice(&self.payload);
217                apply_mask(&mut out[start..], mask);
218            }
219            None => out.extend_from_slice(&self.payload),
220        }
221        out
222    }
223
224    /// Decode one frame from the front of `bytes`.
225    ///
226    /// Returns `Ok(None)` when the buffer does not hold a whole frame yet — the
227    /// caller reads more and asks again — and the number of bytes consumed
228    /// alongside the frame so the caller can drain exactly that much.
229    ///
230    /// `max_payload` is checked against the *declared* length, before anything
231    /// is allocated, so a peer announcing a 4 GiB frame costs nothing.
232    pub fn decode(
233        bytes: &[u8],
234        role: Role,
235        max_payload: usize,
236    ) -> WsResult<Option<(Frame, usize)>> {
237        if bytes.len() < 2 {
238            return Ok(None);
239        }
240
241        let (first, second) = (bytes[0], bytes[1]);
242        let fin = first & 0x80 != 0;
243        if first & 0x70 != 0 {
244            return Err(WsError::protocol("a reserved bit is set but no extension was negotiated"));
245        }
246        let opcode = OpCode::from_bits(first & 0x0f)?;
247        let masked = second & 0x80 != 0;
248
249        let mut cursor = 2;
250        let length = match second & 0x7f {
251            126 => {
252                if bytes.len() < cursor + 2 {
253                    return Ok(None);
254                }
255                let value = u16::from_be_bytes([bytes[cursor], bytes[cursor + 1]]) as usize;
256                cursor += 2;
257                if value < 126 {
258                    return Err(WsError::protocol(
259                        "16-bit length used for a payload that fits in 7 bits",
260                    ));
261                }
262                value
263            }
264            127 => {
265                if bytes.len() < cursor + 8 {
266                    return Ok(None);
267                }
268                let mut wide = [0u8; 8];
269                wide.copy_from_slice(&bytes[cursor..cursor + 8]);
270                let value = u64::from_be_bytes(wide);
271                cursor += 8;
272                if value & (1 << 63) != 0 {
273                    return Err(WsError::protocol("64-bit length has its high bit set"));
274                }
275                if value <= u16::MAX as u64 {
276                    return Err(WsError::protocol(
277                        "64-bit length used for a payload that fits in 16 bits",
278                    ));
279                }
280                // On a 32-bit target a legal 64-bit length can still be more
281                // than this process could ever hold; the size limit catches it.
282                match usize::try_from(value) {
283                    Ok(value) => value,
284                    Err(_) => {
285                        return Err(WsError::TooLarge { size: usize::MAX, limit: max_payload });
286                    }
287                }
288            }
289            short => short as usize,
290        };
291
292        if length > max_payload {
293            return Err(WsError::TooLarge { size: length, limit: max_payload });
294        }
295
296        if opcode.is_control() {
297            if length > 125 {
298                return Err(WsError::protocol("a control frame payload must be 125 bytes or fewer"));
299            }
300            if !fin {
301                return Err(WsError::protocol("a control frame must not be fragmented"));
302            }
303        }
304
305        match (role, masked) {
306            (Role::Server, false) => {
307                return Err(WsError::protocol("a frame from a client must be masked"));
308            }
309            (Role::Client, true) => {
310                return Err(WsError::protocol("a frame from a server must not be masked"));
311            }
312            _ => {}
313        }
314
315        let mask = if masked {
316            if bytes.len() < cursor + 4 {
317                return Ok(None);
318            }
319            let mask = [bytes[cursor], bytes[cursor + 1], bytes[cursor + 2], bytes[cursor + 3]];
320            cursor += 4;
321            Some(mask)
322        } else {
323            None
324        };
325
326        if bytes.len() < cursor + length {
327            return Ok(None);
328        }
329
330        let mut payload = bytes[cursor..cursor + length].to_vec();
331        if let Some(mask) = mask {
332            apply_mask(&mut payload, mask);
333        }
334
335        Ok(Some((Frame { fin, opcode, mask, payload }, cursor + length)))
336    }
337}
338
339/// Masking is its own inverse: the same four bytes cycle over the payload.
340pub fn apply_mask(payload: &mut [u8], mask: [u8; 4]) {
341    for (index, byte) in payload.iter_mut().enumerate() {
342        *byte ^= mask[index % 4];
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn encodes_a_short_unmasked_text_frame() {
352        let frame = Frame::server(OpCode::Text, b"Hello".to_vec());
353
354        // FIN + text, then an unmasked 7-bit length of 5.
355        assert_eq!(frame.encode(), b"\x81\x05Hello");
356    }
357
358    #[test]
359    fn decodes_the_masked_frame_from_the_rfc() {
360        // RFC 6455 section 5.7: a masked "Hello" sent by a client.
361        let wire = b"\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58";
362
363        let (frame, used) = Frame::decode(wire, Role::Server, 1024).unwrap().unwrap();
364
365        assert_eq!(used, wire.len());
366        assert!(frame.fin);
367        assert_eq!(frame.opcode, OpCode::Text);
368        assert_eq!(frame.mask, Some([0x37, 0xfa, 0x21, 0x3d]));
369        assert_eq!(frame.payload, b"Hello");
370        // Re-encoding with the same mask reproduces the original bytes exactly.
371        assert_eq!(frame.encode(), wire);
372    }
373
374    #[test]
375    fn each_payload_length_form_round_trips() {
376        for (length, header) in [
377            // 7-bit: the length lives in the second byte.
378            (5usize, vec![0x82, 5]),
379            (125, vec![0x82, 125]),
380            // 16-bit: 126 signals two more bytes.
381            (126, vec![0x82, 126, 0x00, 0x7e]),
382            (65_535, vec![0x82, 126, 0xff, 0xff]),
383            // 64-bit: 127 signals eight more bytes.
384            (65_536, vec![0x82, 127, 0, 0, 0, 0, 0, 1, 0, 0]),
385        ] {
386            let frame = Frame::server(OpCode::Binary, vec![0xab; length]);
387            let encoded = frame.encode();
388
389            assert_eq!(&encoded[..header.len()], &header[..], "header for {length} bytes");
390            assert_eq!(encoded.len(), header.len() + length);
391
392            let (decoded, used) = Frame::decode(&encoded, Role::Client, 1 << 20).unwrap().unwrap();
393            assert_eq!(used, encoded.len());
394            assert_eq!(decoded, frame, "round trip of {length} bytes");
395        }
396    }
397
398    #[test]
399    fn masking_is_its_own_inverse() {
400        let mask = [0x11, 0x22, 0x33, 0x44];
401        let mut bytes = b"the quick brown fox".to_vec();
402        let original = bytes.clone();
403
404        apply_mask(&mut bytes, mask);
405        assert_ne!(bytes, original);
406        apply_mask(&mut bytes, mask);
407        assert_eq!(bytes, original);
408    }
409
410    #[test]
411    fn a_partial_frame_asks_for_more_bytes_instead_of_failing() {
412        let encoded = Frame::server(OpCode::Text, vec![b'x'; 300]).encode();
413
414        for cut in [0, 1, 2, 3, 4, encoded.len() - 1] {
415            assert!(
416                Frame::decode(&encoded[..cut], Role::Client, 1024).unwrap().is_none(),
417                "{cut} bytes should not decode yet"
418            );
419        }
420        assert!(Frame::decode(&encoded, Role::Client, 1024).unwrap().is_some());
421    }
422
423    #[test]
424    fn a_server_rejects_an_unmasked_client_frame() {
425        let unmasked = Frame::server(OpCode::Text, b"hi".to_vec()).encode();
426
427        let error = Frame::decode(&unmasked, Role::Server, 1024).unwrap_err();
428        assert!(error.to_string().contains("must be masked"), "{error}");
429    }
430
431    #[test]
432    fn a_client_rejects_a_masked_server_frame() {
433        let masked = Frame::client(OpCode::Text, b"hi".to_vec(), [1, 2, 3, 4]).encode();
434
435        let error = Frame::decode(&masked, Role::Client, 1024).unwrap_err();
436        assert!(error.to_string().contains("must not be masked"), "{error}");
437    }
438
439    #[test]
440    fn reserved_bits_and_reserved_opcodes_are_refused() {
441        let reserved_bit = Frame::decode(&[0xc1, 0x00], Role::Client, 1024).unwrap_err();
442        assert!(reserved_bit.to_string().contains("reserved bit"), "{reserved_bit}");
443
444        let reserved_opcode = Frame::decode(&[0x83, 0x00], Role::Client, 1024).unwrap_err();
445        assert!(reserved_opcode.to_string().contains("reserved"), "{reserved_opcode}");
446    }
447
448    #[test]
449    fn control_frames_must_be_short_and_unfragmented() {
450        // Opcode 0x9 (ping) with a declared length of 126 needs the 16-bit form,
451        // which is already more than a control frame is allowed to carry.
452        let long_ping = [0x89, 126, 0x00, 0x7e];
453        let error = Frame::decode(&long_ping, Role::Client, 1 << 20).unwrap_err();
454        assert!(error.to_string().contains("125 bytes or fewer"), "{error}");
455
456        // FIN clear on a close frame.
457        let fragmented_close = [0x08, 0x00];
458        let error = Frame::decode(&fragmented_close, Role::Client, 1024).unwrap_err();
459        assert!(error.to_string().contains("must not be fragmented"), "{error}");
460    }
461
462    #[test]
463    fn a_declared_length_over_the_limit_fails_before_anything_is_allocated() {
464        // A two-byte header claiming a 4 GiB payload, and nothing else.
465        let header = [0x82, 127, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
466
467        let error = Frame::decode(&header, Role::Client, 1024).unwrap_err();
468        match error {
469            WsError::TooLarge { size, limit } => {
470                assert_eq!(size, 1 << 32);
471                assert_eq!(limit, 1024);
472            }
473            other => panic!("expected an oversize error, got {other}"),
474        }
475    }
476
477    #[test]
478    fn non_minimal_length_encodings_are_refused() {
479        // 16-bit form declaring 5 bytes, which the 7-bit form could have carried.
480        let padded_16 = [0x82, 126, 0x00, 0x05];
481        assert!(Frame::decode(&padded_16, Role::Client, 1024).is_err());
482
483        // 64-bit form declaring 5 bytes.
484        let padded_64 = [0x82, 127, 0, 0, 0, 0, 0, 0, 0, 5];
485        assert!(Frame::decode(&padded_64, Role::Client, 1024).is_err());
486    }
487
488    #[test]
489    fn a_fragmented_message_decodes_as_three_frames() {
490        // "Hel" + "lo " + "there", the way a client streams a long message.
491        let mut wire = Vec::new();
492        wire.extend(Frame { fin: false, opcode: OpCode::Text, mask: None, payload: b"Hel".to_vec() }.encode());
493        wire.extend(
494            Frame { fin: false, opcode: OpCode::Continuation, mask: None, payload: b"lo ".to_vec() }
495                .encode(),
496        );
497        wire.extend(
498            Frame { fin: true, opcode: OpCode::Continuation, mask: None, payload: b"there".to_vec() }
499                .encode(),
500        );
501
502        let mut rest = &wire[..];
503        let mut assembled = Vec::new();
504        let mut opcodes = Vec::new();
505
506        while let Some((frame, used)) = Frame::decode(rest, Role::Client, 1024).unwrap() {
507            opcodes.push((frame.opcode, frame.fin));
508            assembled.extend_from_slice(&frame.payload);
509            rest = &rest[used..];
510        }
511
512        assert_eq!(assembled, b"Hello there");
513        assert_eq!(
514            opcodes,
515            [
516                (OpCode::Text, false),
517                (OpCode::Continuation, false),
518                (OpCode::Continuation, true),
519            ]
520        );
521    }
522
523    #[test]
524    fn a_close_payload_carries_a_code_and_a_reason() {
525        let close = CloseFrame::new(CloseCode::NORMAL, "bye");
526        let payload = close.to_payload();
527
528        assert_eq!(payload, b"\x03\xe8bye");
529        assert_eq!(CloseFrame::parse(&payload).unwrap(), Some(close));
530        // An empty payload is legal: the peer closed without saying why.
531        assert_eq!(CloseFrame::parse(&[]).unwrap(), None);
532    }
533
534    #[test]
535    fn close_codes_that_only_describe_a_local_end_may_not_be_sent() {
536        for code in [1005u16, 1006, 1015] {
537            let payload = code.to_be_bytes();
538            assert!(CloseFrame::parse(&payload).is_err(), "{code} should be refused");
539        }
540        // The application range survives untouched.
541        let custom = CloseFrame::new(CloseCode(4001), "seat taken");
542        assert_eq!(CloseFrame::parse(&custom.to_payload()).unwrap(), Some(custom));
543    }
544
545    #[test]
546    fn a_one_byte_close_payload_is_a_protocol_error() {
547        assert!(CloseFrame::parse(&[0x03]).is_err());
548    }
549
550    #[test]
551    fn a_long_close_reason_is_truncated_on_a_character_boundary() {
552        // Two-byte characters, so a naive cut at 123 bytes would split one.
553        let close = CloseFrame::new(CloseCode::INTERNAL_ERROR, "é".repeat(100));
554        let payload = close.to_payload();
555
556        assert!(payload.len() <= 125, "a control frame may not exceed 125 bytes");
557        let parsed = CloseFrame::parse(&payload).unwrap().unwrap();
558        assert_eq!(parsed.code, CloseCode::INTERNAL_ERROR);
559        assert_eq!(parsed.reason, "é".repeat(61));
560    }
561}