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    /// Two different reasons a code in 1000–1015 may not be sent:
108    ///
109    /// * **1005, 1006 and 1015 describe how a connection ended locally** — no
110    ///   status given, closed abnormally, TLS failed. They exist for an API to
111    ///   report, and a peer putting one on the wire is lying about what
112    ///   happened. Clients do occasionally try.
113    /// * **1004 is reserved** (RFC 6455 §7.4.1). It has no meaning, so a peer
114    ///   sending it is sending nothing we can act on. It used to fall inside
115    ///   `1000..=1014` and was accepted and echoed back; the Autobahn
116    ///   TestSuite's case 7.9.3 caught it.
117    ///
118    /// 1012, 1013 and 1014 stay sendable: IANA registered them after the RFC —
119    /// service restart, try again later, bad gateway.
120    pub fn is_sendable(self) -> bool {
121        match self.0 {
122            1004 | 1005 | 1006 | 1015 => false,
123            1000..=1014 => true,
124            3000..=4999 => true,
125            _ => false,
126        }
127    }
128}
129
130/// The body of a close frame: a status code, and optionally a reason.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct CloseFrame {
133    pub code: CloseCode,
134    pub reason: String,
135}
136
137impl CloseFrame {
138    pub fn new(code: CloseCode, reason: impl Into<String>) -> Self {
139        CloseFrame { code, reason: reason.into() }
140    }
141
142    /// Parse a close payload. An empty payload is legal and means "no status".
143    pub fn parse(payload: &[u8]) -> WsResult<Option<CloseFrame>> {
144        match payload.len() {
145            0 => Ok(None),
146            1 => Err(WsError::protocol("close payload must be empty or at least two bytes")),
147            _ => {
148                let code = CloseCode(u16::from_be_bytes([payload[0], payload[1]]));
149                if !code.is_sendable() {
150                    return Err(WsError::protocol(format!(
151                        "close code {} must not be sent on the wire",
152                        code.0
153                    )));
154                }
155                // The reason is required to be UTF-8, and a peer that gets this
156                // wrong is exactly the peer whose text frames cannot be trusted.
157                let reason =
158                    std::str::from_utf8(&payload[2..]).map_err(|_| WsError::InvalidUtf8)?;
159                Ok(Some(CloseFrame { code, reason: reason.to_string() }))
160            }
161        }
162    }
163
164    /// The wire payload: the code, then the reason.
165    ///
166    /// A control frame carries at most 125 bytes and two of them are the code,
167    /// so a long reason is truncated on a character boundary rather than being
168    /// allowed to make an unsendable frame. Reasons are diagnostics, and a
169    /// diagnostic that kills the connection it explains is worse than a short one.
170    pub fn to_payload(&self) -> Vec<u8> {
171        let mut end = self.reason.len().min(123);
172        while end > 0 && !self.reason.is_char_boundary(end) {
173            end -= 1;
174        }
175
176        let mut out = Vec::with_capacity(2 + end);
177        out.extend_from_slice(&self.code.0.to_be_bytes());
178        out.extend_from_slice(&self.reason.as_bytes()[..end]);
179        out
180    }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct Frame {
185    pub fin: bool,
186    pub opcode: OpCode,
187    /// Present only on frames a client sends. A server that masks its frames is
188    /// as wrong as a client that does not.
189    pub mask: Option<[u8; 4]>,
190    pub payload: Vec<u8>,
191}
192
193impl Frame {
194    /// A frame as a server sends it: final, and never masked.
195    pub fn server(opcode: OpCode, payload: Vec<u8>) -> Frame {
196        Frame { fin: true, opcode, mask: None, payload }
197    }
198
199    /// A frame as a client sends it. The mask is supplied rather than generated
200    /// so encoding stays a pure function that a test can pin to exact bytes.
201    pub fn client(opcode: OpCode, payload: Vec<u8>, mask: [u8; 4]) -> Frame {
202        Frame { fin: true, opcode, mask: Some(mask), payload }
203    }
204
205    pub fn encode(&self) -> Vec<u8> {
206        let mut out = Vec::with_capacity(self.payload.len() + 14);
207        out.push(if self.fin { 0x80 } else { 0 } | self.opcode.bits());
208
209        let masked = if self.mask.is_some() { 0x80 } else { 0 };
210        let length = self.payload.len();
211        // The three length forms. The RFC requires the *shortest* one that
212        // fits, so a 125-byte payload never uses the 16-bit form.
213        if length < 126 {
214            out.push(masked | length as u8);
215        } else if length <= u16::MAX as usize {
216            out.push(masked | 126);
217            out.extend_from_slice(&(length as u16).to_be_bytes());
218        } else {
219            out.push(masked | 127);
220            out.extend_from_slice(&(length as u64).to_be_bytes());
221        }
222
223        match self.mask {
224            Some(mask) => {
225                out.extend_from_slice(&mask);
226                let start = out.len();
227                out.extend_from_slice(&self.payload);
228                apply_mask(&mut out[start..], mask);
229            }
230            None => out.extend_from_slice(&self.payload),
231        }
232        out
233    }
234
235    /// Decode one frame from the front of `bytes`.
236    ///
237    /// Returns `Ok(None)` when the buffer does not hold a whole frame yet — the
238    /// caller reads more and asks again — and the number of bytes consumed
239    /// alongside the frame so the caller can drain exactly that much.
240    ///
241    /// `max_payload` is checked against the *declared* length, before anything
242    /// is allocated, so a peer announcing a 4 GiB frame costs nothing.
243    pub fn decode(
244        bytes: &[u8],
245        role: Role,
246        max_payload: usize,
247    ) -> WsResult<Option<(Frame, usize)>> {
248        if bytes.len() < 2 {
249            return Ok(None);
250        }
251
252        let (first, second) = (bytes[0], bytes[1]);
253        let fin = first & 0x80 != 0;
254        if first & 0x70 != 0 {
255            return Err(WsError::protocol("a reserved bit is set but no extension was negotiated"));
256        }
257        let opcode = OpCode::from_bits(first & 0x0f)?;
258        let masked = second & 0x80 != 0;
259
260        let mut cursor = 2;
261        let length = match second & 0x7f {
262            126 => {
263                if bytes.len() < cursor + 2 {
264                    return Ok(None);
265                }
266                let value = u16::from_be_bytes([bytes[cursor], bytes[cursor + 1]]) as usize;
267                cursor += 2;
268                if value < 126 {
269                    return Err(WsError::protocol(
270                        "16-bit length used for a payload that fits in 7 bits",
271                    ));
272                }
273                value
274            }
275            127 => {
276                if bytes.len() < cursor + 8 {
277                    return Ok(None);
278                }
279                let mut wide = [0u8; 8];
280                wide.copy_from_slice(&bytes[cursor..cursor + 8]);
281                let value = u64::from_be_bytes(wide);
282                cursor += 8;
283                if value & (1 << 63) != 0 {
284                    return Err(WsError::protocol("64-bit length has its high bit set"));
285                }
286                if value <= u16::MAX as u64 {
287                    return Err(WsError::protocol(
288                        "64-bit length used for a payload that fits in 16 bits",
289                    ));
290                }
291                // On a 32-bit target a legal 64-bit length can still be more
292                // than this process could ever hold; the size limit catches it.
293                match usize::try_from(value) {
294                    Ok(value) => value,
295                    Err(_) => {
296                        return Err(WsError::TooLarge { size: usize::MAX, limit: max_payload });
297                    }
298                }
299            }
300            short => short as usize,
301        };
302
303        if length > max_payload {
304            return Err(WsError::TooLarge { size: length, limit: max_payload });
305        }
306
307        if opcode.is_control() {
308            if length > 125 {
309                return Err(WsError::protocol("a control frame payload must be 125 bytes or fewer"));
310            }
311            if !fin {
312                return Err(WsError::protocol("a control frame must not be fragmented"));
313            }
314        }
315
316        match (role, masked) {
317            (Role::Server, false) => {
318                return Err(WsError::protocol("a frame from a client must be masked"));
319            }
320            (Role::Client, true) => {
321                return Err(WsError::protocol("a frame from a server must not be masked"));
322            }
323            _ => {}
324        }
325
326        let mask = if masked {
327            if bytes.len() < cursor + 4 {
328                return Ok(None);
329            }
330            let mask = [bytes[cursor], bytes[cursor + 1], bytes[cursor + 2], bytes[cursor + 3]];
331            cursor += 4;
332            Some(mask)
333        } else {
334            None
335        };
336
337        if bytes.len() < cursor + length {
338            return Ok(None);
339        }
340
341        let mut payload = bytes[cursor..cursor + length].to_vec();
342        if let Some(mask) = mask {
343            apply_mask(&mut payload, mask);
344        }
345
346        Ok(Some((Frame { fin, opcode, mask, payload }, cursor + length)))
347    }
348}
349
350/// Masking is its own inverse: the same four bytes cycle over the payload.
351pub fn apply_mask(payload: &mut [u8], mask: [u8; 4]) {
352    for (index, byte) in payload.iter_mut().enumerate() {
353        *byte ^= mask[index % 4];
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn encodes_a_short_unmasked_text_frame() {
363        let frame = Frame::server(OpCode::Text, b"Hello".to_vec());
364
365        // FIN + text, then an unmasked 7-bit length of 5.
366        assert_eq!(frame.encode(), b"\x81\x05Hello");
367    }
368
369    #[test]
370    fn decodes_the_masked_frame_from_the_rfc() {
371        // RFC 6455 section 5.7: a masked "Hello" sent by a client.
372        let wire = b"\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58";
373
374        let (frame, used) = Frame::decode(wire, Role::Server, 1024).unwrap().unwrap();
375
376        assert_eq!(used, wire.len());
377        assert!(frame.fin);
378        assert_eq!(frame.opcode, OpCode::Text);
379        assert_eq!(frame.mask, Some([0x37, 0xfa, 0x21, 0x3d]));
380        assert_eq!(frame.payload, b"Hello");
381        // Re-encoding with the same mask reproduces the original bytes exactly.
382        assert_eq!(frame.encode(), wire);
383    }
384
385    #[test]
386    fn each_payload_length_form_round_trips() {
387        for (length, header) in [
388            // 7-bit: the length lives in the second byte.
389            (5usize, vec![0x82, 5]),
390            (125, vec![0x82, 125]),
391            // 16-bit: 126 signals two more bytes.
392            (126, vec![0x82, 126, 0x00, 0x7e]),
393            (65_535, vec![0x82, 126, 0xff, 0xff]),
394            // 64-bit: 127 signals eight more bytes.
395            (65_536, vec![0x82, 127, 0, 0, 0, 0, 0, 1, 0, 0]),
396        ] {
397            let frame = Frame::server(OpCode::Binary, vec![0xab; length]);
398            let encoded = frame.encode();
399
400            assert_eq!(&encoded[..header.len()], &header[..], "header for {length} bytes");
401            assert_eq!(encoded.len(), header.len() + length);
402
403            let (decoded, used) = Frame::decode(&encoded, Role::Client, 1 << 20).unwrap().unwrap();
404            assert_eq!(used, encoded.len());
405            assert_eq!(decoded, frame, "round trip of {length} bytes");
406        }
407    }
408
409    #[test]
410    fn masking_is_its_own_inverse() {
411        let mask = [0x11, 0x22, 0x33, 0x44];
412        let mut bytes = b"the quick brown fox".to_vec();
413        let original = bytes.clone();
414
415        apply_mask(&mut bytes, mask);
416        assert_ne!(bytes, original);
417        apply_mask(&mut bytes, mask);
418        assert_eq!(bytes, original);
419    }
420
421    #[test]
422    fn a_partial_frame_asks_for_more_bytes_instead_of_failing() {
423        let encoded = Frame::server(OpCode::Text, vec![b'x'; 300]).encode();
424
425        for cut in [0, 1, 2, 3, 4, encoded.len() - 1] {
426            assert!(
427                Frame::decode(&encoded[..cut], Role::Client, 1024).unwrap().is_none(),
428                "{cut} bytes should not decode yet"
429            );
430        }
431        assert!(Frame::decode(&encoded, Role::Client, 1024).unwrap().is_some());
432    }
433
434    #[test]
435    fn a_server_rejects_an_unmasked_client_frame() {
436        let unmasked = Frame::server(OpCode::Text, b"hi".to_vec()).encode();
437
438        let error = Frame::decode(&unmasked, Role::Server, 1024).unwrap_err();
439        assert!(error.to_string().contains("must be masked"), "{error}");
440    }
441
442    #[test]
443    fn a_client_rejects_a_masked_server_frame() {
444        let masked = Frame::client(OpCode::Text, b"hi".to_vec(), [1, 2, 3, 4]).encode();
445
446        let error = Frame::decode(&masked, Role::Client, 1024).unwrap_err();
447        assert!(error.to_string().contains("must not be masked"), "{error}");
448    }
449
450    #[test]
451    fn reserved_bits_and_reserved_opcodes_are_refused() {
452        let reserved_bit = Frame::decode(&[0xc1, 0x00], Role::Client, 1024).unwrap_err();
453        assert!(reserved_bit.to_string().contains("reserved bit"), "{reserved_bit}");
454
455        let reserved_opcode = Frame::decode(&[0x83, 0x00], Role::Client, 1024).unwrap_err();
456        assert!(reserved_opcode.to_string().contains("reserved"), "{reserved_opcode}");
457    }
458
459    #[test]
460    fn control_frames_must_be_short_and_unfragmented() {
461        // Opcode 0x9 (ping) with a declared length of 126 needs the 16-bit form,
462        // which is already more than a control frame is allowed to carry.
463        let long_ping = [0x89, 126, 0x00, 0x7e];
464        let error = Frame::decode(&long_ping, Role::Client, 1 << 20).unwrap_err();
465        assert!(error.to_string().contains("125 bytes or fewer"), "{error}");
466
467        // FIN clear on a close frame.
468        let fragmented_close = [0x08, 0x00];
469        let error = Frame::decode(&fragmented_close, Role::Client, 1024).unwrap_err();
470        assert!(error.to_string().contains("must not be fragmented"), "{error}");
471    }
472
473    #[test]
474    fn a_declared_length_over_the_limit_fails_before_anything_is_allocated() {
475        // A two-byte header claiming a 4 GiB payload, and nothing else.
476        let header = [0x82, 127, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
477
478        let error = Frame::decode(&header, Role::Client, 1024).unwrap_err();
479        match error {
480            WsError::TooLarge { size, limit } => {
481                assert_eq!(size, 1 << 32);
482                assert_eq!(limit, 1024);
483            }
484            other => panic!("expected an oversize error, got {other}"),
485        }
486    }
487
488    #[test]
489    fn non_minimal_length_encodings_are_refused() {
490        // 16-bit form declaring 5 bytes, which the 7-bit form could have carried.
491        let padded_16 = [0x82, 126, 0x00, 0x05];
492        assert!(Frame::decode(&padded_16, Role::Client, 1024).is_err());
493
494        // 64-bit form declaring 5 bytes.
495        let padded_64 = [0x82, 127, 0, 0, 0, 0, 0, 0, 0, 5];
496        assert!(Frame::decode(&padded_64, Role::Client, 1024).is_err());
497    }
498
499    #[test]
500    fn a_fragmented_message_decodes_as_three_frames() {
501        // "Hel" + "lo " + "there", the way a client streams a long message.
502        let mut wire = Vec::new();
503        wire.extend(Frame { fin: false, opcode: OpCode::Text, mask: None, payload: b"Hel".to_vec() }.encode());
504        wire.extend(
505            Frame { fin: false, opcode: OpCode::Continuation, mask: None, payload: b"lo ".to_vec() }
506                .encode(),
507        );
508        wire.extend(
509            Frame { fin: true, opcode: OpCode::Continuation, mask: None, payload: b"there".to_vec() }
510                .encode(),
511        );
512
513        let mut rest = &wire[..];
514        let mut assembled = Vec::new();
515        let mut opcodes = Vec::new();
516
517        while let Some((frame, used)) = Frame::decode(rest, Role::Client, 1024).unwrap() {
518            opcodes.push((frame.opcode, frame.fin));
519            assembled.extend_from_slice(&frame.payload);
520            rest = &rest[used..];
521        }
522
523        assert_eq!(assembled, b"Hello there");
524        assert_eq!(
525            opcodes,
526            [
527                (OpCode::Text, false),
528                (OpCode::Continuation, false),
529                (OpCode::Continuation, true),
530            ]
531        );
532    }
533
534    #[test]
535    fn a_close_payload_carries_a_code_and_a_reason() {
536        let close = CloseFrame::new(CloseCode::NORMAL, "bye");
537        let payload = close.to_payload();
538
539        assert_eq!(payload, b"\x03\xe8bye");
540        assert_eq!(CloseFrame::parse(&payload).unwrap(), Some(close));
541        // An empty payload is legal: the peer closed without saying why.
542        assert_eq!(CloseFrame::parse(&[]).unwrap(), None);
543    }
544
545    /// Exactly the codes the Autobahn TestSuite sends in cases 7.9.1–7.9.9 and
546    /// expects refused with 1002. Taken from the suite rather than reasoned
547    /// out, because reasoning is what let 1004 through: it sat inside a range
548    /// that looked right.
549    #[test]
550    fn every_close_code_autobahn_calls_invalid_is_refused() {
551        for code in [0u16, 999, 1004, 1005, 1006, 1016, 1100, 2000, 2999] {
552            let payload = code.to_be_bytes();
553            assert!(CloseFrame::parse(&payload).is_err(), "{code} should be refused");
554        }
555    }
556
557    /// And the ones it calls valid (cases 7.7.x), plus the three IANA added
558    /// after the RFC — so a future tidy-up of the range cannot take them out.
559    #[test]
560    fn every_registered_close_code_is_accepted() {
561        for code in [1000u16, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 3000, 3999, 4000, 4999] {
562            let payload = code.to_be_bytes();
563            assert!(
564                CloseFrame::parse(&payload).is_ok(),
565                "{code} is a registered close code and was refused"
566            );
567        }
568    }
569
570    #[test]
571    fn close_codes_that_only_describe_a_local_end_may_not_be_sent() {
572        for code in [1005u16, 1006, 1015] {
573            let payload = code.to_be_bytes();
574            assert!(CloseFrame::parse(&payload).is_err(), "{code} should be refused");
575        }
576        // The application range survives untouched.
577        let custom = CloseFrame::new(CloseCode(4001), "seat taken");
578        assert_eq!(CloseFrame::parse(&custom.to_payload()).unwrap(), Some(custom));
579    }
580
581    #[test]
582    fn a_one_byte_close_payload_is_a_protocol_error() {
583        assert!(CloseFrame::parse(&[0x03]).is_err());
584    }
585
586    #[test]
587    fn a_long_close_reason_is_truncated_on_a_character_boundary() {
588        // Two-byte characters, so a naive cut at 123 bytes would split one.
589        let close = CloseFrame::new(CloseCode::INTERNAL_ERROR, "é".repeat(100));
590        let payload = close.to_payload();
591
592        assert!(payload.len() <= 125, "a control frame may not exceed 125 bytes");
593        let parsed = CloseFrame::parse(&payload).unwrap().unwrap();
594        assert_eq!(parsed.code, CloseCode::INTERNAL_ERROR);
595        assert_eq!(parsed.reason, "é".repeat(61));
596    }
597}