Skip to main content

nexora_proto/http2/
frame.rs

1//! Stateless HTTP/2 wire framing, with connection-state checks left to the caller.
2
3use crate::{Bytes, BytesMut, Http2ErrorCode, ProtoError, Result};
4
5pub const CLIENT_PREFACE: &[u8; 24] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
6pub const DEFAULT_INITIAL_WINDOW_SIZE: i64 = 65_535;
7pub const DEFAULT_MAX_FRAME_SIZE: u32 = 16_384;
8pub const MAX_FLOW_CONTROL_WINDOW: i64 = 0x7fff_ffff;
9const MAX_FRAME_SIZE: u32 = 0x00ff_ffff;
10const STREAM_MASK: u32 = 0x7fff_ffff;
11
12/// Frame types from RFC 9113, including extension type codes.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum Http2FrameType {
16    Data = 0x0,
17    Headers = 0x1,
18    Priority = 0x2,
19    RstStream = 0x3,
20    Settings = 0x4,
21    PushPromise = 0x5,
22    Ping = 0x6,
23    GoAway = 0x7,
24    WindowUpdate = 0x8,
25    Continuation = 0x9,
26    Unknown(u8),
27}
28
29impl Http2FrameType {
30    fn from_byte(value: u8) -> Self {
31        match value {
32            0 => Self::Data,
33            1 => Self::Headers,
34            2 => Self::Priority,
35            3 => Self::RstStream,
36            4 => Self::Settings,
37            5 => Self::PushPromise,
38            6 => Self::Ping,
39            7 => Self::GoAway,
40            8 => Self::WindowUpdate,
41            9 => Self::Continuation,
42            value => Self::Unknown(value),
43        }
44    }
45
46    fn as_byte(self) -> u8 {
47        match self {
48            Self::Data => 0,
49            Self::Headers => 1,
50            Self::Priority => 2,
51            Self::RstStream => 3,
52            Self::Settings => 4,
53            Self::PushPromise => 5,
54            Self::Ping => 6,
55            Self::GoAway => 7,
56            Self::WindowUpdate => 8,
57            Self::Continuation => 9,
58            Self::Unknown(value) => value,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Http2FrameHeader {
65    pub length: u32,
66    pub frame_type: Http2FrameType,
67    pub flags: u8,
68    pub stream_id: u32,
69}
70
71/// SETTINGS values. Omitted wire settings decode to their RFC initial values.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Http2Settings {
74    pub header_table_size: u32,
75    pub enable_push: bool,
76    pub max_concurrent_streams: u32,
77    pub initial_window_size: u32,
78    pub max_frame_size: u32,
79    pub max_header_list_size: u32,
80}
81
82impl Default for Http2Settings {
83    fn default() -> Self {
84        Self {
85            header_table_size: 4096,
86            enable_push: true,
87            max_concurrent_streams: u32::MAX,
88            initial_window_size: DEFAULT_INITIAL_WINDOW_SIZE as u32,
89            max_frame_size: DEFAULT_MAX_FRAME_SIZE,
90            max_header_list_size: u32::MAX,
91        }
92    }
93}
94
95/// Decoded frames. PUSH_PROMISE retains its complete wire payload in `Unknown`.
96/// Padding and deprecated HEADERS priority fields are consumed during decoding.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum Http2Frame {
99    Data {
100        stream_id: u32,
101        end_stream: bool,
102        data: Bytes,
103    },
104    Headers {
105        stream_id: u32,
106        end_stream: bool,
107        end_headers: bool,
108        header_block: Bytes,
109    },
110    Priority {
111        stream_id: u32,
112        exclusive: bool,
113        stream_dependency: u32,
114        weight: u8,
115    },
116    RstStream {
117        stream_id: u32,
118        error_code: Http2ErrorCode,
119    },
120    Settings {
121        ack: bool,
122        settings: Http2Settings,
123    },
124    Ping {
125        ack: bool,
126        opaque_data: [u8; 8],
127    },
128    GoAway {
129        last_stream_id: u32,
130        error_code: Http2ErrorCode,
131        debug_data: Bytes,
132    },
133    WindowUpdate {
134        stream_id: u32,
135        increment: u32,
136    },
137    Continuation {
138        stream_id: u32,
139        end_headers: bool,
140        header_block: Bytes,
141    },
142    Unknown {
143        header: Http2FrameHeader,
144        payload: Bytes,
145    },
146}
147
148/// Append one frame after validating every encoded field.
149///
150/// The caller enforces the peer's negotiated frame limit and stream state.
151/// An error leaves `out` unchanged. SETTINGS ACK ignores its `settings` field.
152///
153/// # Errors
154/// Returns an HTTP/2 error for invalid stream IDs, payload sizes, settings,
155/// priority dependencies, padding, reserved fields, or flow-control increments.
156///
157/// # Panics
158/// Does not panic for any frame value.
159pub fn encode_frame(frame: &Http2Frame, out: &mut BytesMut) -> Result<()> {
160    let header = encoded_header(frame)?;
161    validate_header(header, MAX_FRAME_SIZE)?;
162    match frame {
163        Http2Frame::Priority {
164            stream_id,
165            stream_dependency,
166            ..
167        } => validate_dependency(*stream_id, *stream_dependency)?,
168        Http2Frame::Settings {
169            ack: false,
170            settings,
171        } => validate_settings(settings)?,
172        Http2Frame::GoAway { last_stream_id, .. } if *last_stream_id > STREAM_MASK => {
173            return Err(protocol("last stream ID exceeds 31 bits"));
174        }
175        Http2Frame::WindowUpdate {
176            stream_id,
177            increment,
178        } => {
179            validate_increment(*stream_id, *increment)?;
180        }
181        Http2Frame::Unknown { payload, .. } => {
182            // Known type codes carried as raw frames obey the same wire checks.
183            validate_raw_send(header, payload.as_ref())?;
184            decode_payload(header, payload.as_ref())?;
185        }
186        _ => {}
187    }
188    out.put_u24(header.length);
189    out.put_u8(header.frame_type.as_byte());
190    out.put_u8(header.flags);
191    out.put_u32(header.stream_id);
192    match frame {
193        Http2Frame::Data { data, .. } => out.extend_from_slice(data.as_ref()),
194        Http2Frame::Headers { header_block, .. }
195        | Http2Frame::Continuation { header_block, .. } => {
196            out.extend_from_slice(header_block.as_ref());
197        }
198        Http2Frame::Priority {
199            exclusive,
200            stream_dependency,
201            weight,
202            ..
203        } => {
204            out.put_u32(*stream_dependency | (u32::from(*exclusive) << 31));
205            out.put_u8(*weight);
206        }
207        Http2Frame::RstStream { error_code, .. } => out.put_u32(error_code.as_u32()),
208        Http2Frame::Settings {
209            ack: false,
210            settings,
211        } => {
212            for (id, value) in [
213                (1, settings.header_table_size),
214                (2, u32::from(settings.enable_push)),
215                (3, settings.max_concurrent_streams),
216                (4, settings.initial_window_size),
217                (5, settings.max_frame_size),
218                (6, settings.max_header_list_size),
219            ] {
220                out.put_u16(id);
221                out.put_u32(value);
222            }
223        }
224        Http2Frame::Settings { ack: true, .. } => {}
225        Http2Frame::Ping { opaque_data, .. } => out.extend_from_slice(opaque_data),
226        Http2Frame::GoAway {
227            last_stream_id,
228            error_code,
229            debug_data,
230        } => {
231            out.put_u32(*last_stream_id);
232            out.put_u32(error_code.as_u32());
233            out.extend_from_slice(debug_data.as_ref());
234        }
235        Http2Frame::WindowUpdate { increment, .. } => out.put_u32(*increment),
236        Http2Frame::Unknown { payload, .. } => out.extend_from_slice(payload.as_ref()),
237    }
238    Ok(())
239}
240
241/// Decode and consume one complete frame, retaining all trailing bytes.
242///
243/// Incomplete frames return `Ok(None)` without consuming input. Invalid frames
244/// also leave input unchanged. Reserved bits and unrecognized flags are ignored.
245/// The caller checks stream state, continuation ordering, and endpoint roles.
246///
247/// # Errors
248/// Returns an HTTP/2 connection or stream error for malformed frames or payloads
249/// larger than `max_frame_size` (or the 24-bit wire limit).
250///
251/// # Panics
252/// Does not panic for any input buffer or frame limit.
253pub fn decode_frame(src: &mut BytesMut, max_frame_size: u32) -> Result<Option<Http2Frame>> {
254    let bytes = src.as_slice();
255    if bytes.len() < 9 {
256        return Ok(None);
257    }
258    let header = Http2FrameHeader {
259        length: (u32::from(bytes[0]) << 16) | (u32::from(bytes[1]) << 8) | u32::from(bytes[2]),
260        frame_type: Http2FrameType::from_byte(bytes[3]),
261        flags: bytes[4],
262        stream_id: read_u32(&bytes[5..9]) & STREAM_MASK,
263    };
264    validate_header(header, max_frame_size.min(MAX_FRAME_SIZE))?;
265    let end = 9 + header.length as usize;
266    if bytes.len() < end {
267        return Ok(None);
268    }
269    let frame = decode_payload(header, &bytes[9..end])?;
270    src.advance(end);
271    Ok(Some(frame))
272}
273
274fn validate_raw_send(header: Http2FrameHeader, payload: &[u8]) -> Result<()> {
275    let allowed_flags = match header.frame_type {
276        Http2FrameType::Data => 0x09,
277        Http2FrameType::Headers => 0x2d,
278        Http2FrameType::Settings | Http2FrameType::Ping => 0x01,
279        Http2FrameType::PushPromise => 0x0c,
280        Http2FrameType::Continuation => 0x04,
281        Http2FrameType::Unknown(_) => 0xff,
282        _ => 0,
283    };
284    if header.flags & !allowed_flags != 0 {
285        return Err(protocol("unused flags must be unset when sending"));
286    }
287    if matches!(
288        header.frame_type,
289        Http2FrameType::GoAway | Http2FrameType::WindowUpdate
290    ) && read_u32(payload) & !STREAM_MASK != 0
291    {
292        return Err(protocol("reserved payload bits must be unset when sending"));
293    }
294    if header.frame_type == Http2FrameType::PushPromise {
295        let block = unpadded(payload, header.flags & 8 != 0)?;
296        if block.len() >= 4 && read_u32(block) & !STREAM_MASK != 0 {
297            return Err(protocol(
298                "reserved promised stream bit must be unset when sending",
299            ));
300        }
301    }
302    Ok(())
303}
304
305fn encoded_header(frame: &Http2Frame) -> Result<Http2FrameHeader> {
306    let (length, frame_type, flags, stream_id) = match frame {
307        Http2Frame::Data {
308            stream_id,
309            end_stream,
310            data,
311        } => (
312            data.len(),
313            Http2FrameType::Data,
314            u8::from(*end_stream),
315            *stream_id,
316        ),
317        Http2Frame::Headers {
318            stream_id,
319            end_stream,
320            end_headers,
321            header_block,
322        } => (
323            header_block.len(),
324            Http2FrameType::Headers,
325            u8::from(*end_stream) | (u8::from(*end_headers) << 2),
326            *stream_id,
327        ),
328        Http2Frame::Priority { stream_id, .. } => (5, Http2FrameType::Priority, 0, *stream_id),
329        Http2Frame::RstStream { stream_id, .. } => (4, Http2FrameType::RstStream, 0, *stream_id),
330        Http2Frame::Settings { ack, .. } => (
331            if *ack { 0 } else { 36 },
332            Http2FrameType::Settings,
333            u8::from(*ack),
334            0,
335        ),
336        Http2Frame::Ping { ack, .. } => (8, Http2FrameType::Ping, u8::from(*ack), 0),
337        Http2Frame::GoAway { debug_data, .. } => (
338            debug_data
339                .len()
340                .checked_add(8)
341                .ok_or_else(|| frame_size("GOAWAY too large"))?,
342            Http2FrameType::GoAway,
343            0,
344            0,
345        ),
346        Http2Frame::WindowUpdate { stream_id, .. } => {
347            (4, Http2FrameType::WindowUpdate, 0, *stream_id)
348        }
349        Http2Frame::Continuation {
350            stream_id,
351            end_headers,
352            header_block,
353        } => (
354            header_block.len(),
355            Http2FrameType::Continuation,
356            u8::from(*end_headers) << 2,
357            *stream_id,
358        ),
359        Http2Frame::Unknown { header, payload } => {
360            if header.length as usize != payload.len() {
361                return Err(frame_size("raw frame length differs from payload"));
362            }
363            (
364                payload.len(),
365                Http2FrameType::from_byte(header.frame_type.as_byte()),
366                header.flags,
367                header.stream_id,
368            )
369        }
370    };
371    if length > MAX_FRAME_SIZE as usize {
372        return Err(frame_size("payload exceeds the 24-bit frame size"));
373    }
374    if stream_id > STREAM_MASK {
375        return Err(protocol("stream ID exceeds 31 bits"));
376    }
377    Ok(Http2FrameHeader {
378        length: length as u32,
379        frame_type,
380        flags,
381        stream_id,
382    })
383}
384
385fn validate_header(header: Http2FrameHeader, max_frame_size: u32) -> Result<()> {
386    if header.length > max_frame_size {
387        return Err(frame_size("payload exceeds maximum frame size"));
388    }
389    match header.frame_type {
390        Http2FrameType::Settings | Http2FrameType::Ping | Http2FrameType::GoAway => {
391            if header.stream_id != 0 {
392                return Err(protocol("connection frame uses a nonzero stream ID"));
393            }
394        }
395        Http2FrameType::Data
396        | Http2FrameType::Headers
397        | Http2FrameType::Priority
398        | Http2FrameType::RstStream
399        | Http2FrameType::PushPromise
400        | Http2FrameType::Continuation => {
401            if header.stream_id == 0 {
402                return Err(protocol("stream frame uses stream ID zero"));
403            }
404        }
405        Http2FrameType::WindowUpdate | Http2FrameType::Unknown(_) => {}
406    }
407    let invalid_length = match header.frame_type {
408        Http2FrameType::Priority => header.length != 5,
409        Http2FrameType::RstStream | Http2FrameType::WindowUpdate => header.length != 4,
410        Http2FrameType::Settings => {
411            !header.length.is_multiple_of(6) || (header.flags & 1 != 0 && header.length != 0)
412        }
413        Http2FrameType::Ping => header.length != 8,
414        Http2FrameType::GoAway => header.length < 8,
415        Http2FrameType::PushPromise => header.length < 4 + u32::from(header.flags & 8 != 0),
416        Http2FrameType::Headers => {
417            header.length
418                < u32::from(header.flags & 8 != 0) + if header.flags & 0x20 != 0 { 5 } else { 0 }
419        }
420        Http2FrameType::Data => header.flags & 8 != 0 && header.length == 0,
421        _ => false,
422    };
423    if invalid_length {
424        if header.frame_type == Http2FrameType::Priority {
425            return Err(stream_error(
426                header.stream_id,
427                Http2ErrorCode::FrameSizeError,
428                "PRIORITY payload must be five octets",
429            ));
430        }
431        return Err(frame_size("invalid payload length for frame type"));
432    }
433    Ok(())
434}
435
436fn decode_payload(header: Http2FrameHeader, payload: &[u8]) -> Result<Http2Frame> {
437    let stream_id = header.stream_id;
438    let flag = |mask| header.flags & mask != 0;
439    Ok(match header.frame_type {
440        Http2FrameType::Data => Http2Frame::Data {
441            stream_id,
442            end_stream: flag(1),
443            data: Bytes::copy_from_slice(unpadded(payload, flag(8))?),
444        },
445        Http2FrameType::Headers => {
446            let mut block = unpadded(payload, flag(8))?;
447            if flag(0x20) {
448                if block.len() < 5 {
449                    return Err(frame_size("truncated HEADERS priority fields"));
450                }
451                validate_dependency(stream_id, read_u32(&block[..4]) & STREAM_MASK)?;
452                block = &block[5..];
453            }
454            Http2Frame::Headers {
455                stream_id,
456                end_stream: flag(1),
457                end_headers: flag(4),
458                header_block: Bytes::copy_from_slice(block),
459            }
460        }
461        Http2FrameType::Priority => {
462            let dependency = read_u32(&payload[..4]);
463            validate_dependency(stream_id, dependency & STREAM_MASK)?;
464            Http2Frame::Priority {
465                stream_id,
466                exclusive: dependency & !STREAM_MASK != 0,
467                stream_dependency: dependency & STREAM_MASK,
468                weight: payload[4],
469            }
470        }
471        Http2FrameType::RstStream => Http2Frame::RstStream {
472            stream_id,
473            error_code: Http2ErrorCode::from_u32(read_u32(payload)),
474        },
475        Http2FrameType::Settings => {
476            let mut settings = Http2Settings::default();
477            for pair in payload.as_chunks::<6>().0 {
478                let id = u16::from_be_bytes([pair[0], pair[1]]);
479                let value = read_u32(&pair[2..]);
480                match id {
481                    1 => settings.header_table_size = value,
482                    2 => {
483                        if value > 1 {
484                            return Err(protocol("ENABLE_PUSH must be zero or one"));
485                        }
486                        settings.enable_push = value != 0;
487                    }
488                    3 => settings.max_concurrent_streams = value,
489                    4 => settings.initial_window_size = value,
490                    5 => settings.max_frame_size = value,
491                    6 => settings.max_header_list_size = value,
492                    _ => {}
493                }
494                // Each occurrence must be valid, including overwritten settings.
495                validate_settings(&settings)?;
496            }
497            Http2Frame::Settings {
498                ack: flag(1),
499                settings,
500            }
501        }
502        Http2FrameType::PushPromise => {
503            let block = unpadded(payload, flag(8))?;
504            if block.len() < 4 {
505                return Err(frame_size("truncated promised stream ID"));
506            }
507            let promised_id = read_u32(&block[..4]) & STREAM_MASK;
508            if promised_id == 0 || !promised_id.is_multiple_of(2) {
509                return Err(protocol("promised stream ID must be nonzero and even"));
510            }
511            Http2Frame::Unknown {
512                header,
513                payload: Bytes::copy_from_slice(payload),
514            }
515        }
516        Http2FrameType::Ping => {
517            let mut opaque_data = [0; 8];
518            opaque_data.copy_from_slice(payload);
519            Http2Frame::Ping {
520                ack: flag(1),
521                opaque_data,
522            }
523        }
524        Http2FrameType::GoAway => Http2Frame::GoAway {
525            last_stream_id: read_u32(&payload[..4]) & STREAM_MASK,
526            error_code: Http2ErrorCode::from_u32(read_u32(&payload[4..8])),
527            debug_data: Bytes::copy_from_slice(&payload[8..]),
528        },
529        Http2FrameType::WindowUpdate => {
530            let increment = read_u32(payload) & STREAM_MASK;
531            validate_increment(stream_id, increment)?;
532            Http2Frame::WindowUpdate {
533                stream_id,
534                increment,
535            }
536        }
537        Http2FrameType::Continuation => Http2Frame::Continuation {
538            stream_id,
539            end_headers: flag(4),
540            header_block: Bytes::copy_from_slice(payload),
541        },
542        Http2FrameType::Unknown(_) => Http2Frame::Unknown {
543            header,
544            payload: Bytes::copy_from_slice(payload),
545        },
546    })
547}
548
549fn unpadded(payload: &[u8], padded: bool) -> Result<&[u8]> {
550    if !padded {
551        return Ok(payload);
552    }
553    let Some(&padding) = payload.first() else {
554        return Err(frame_size("missing padding length"));
555    };
556    if usize::from(padding) >= payload.len() {
557        return Err(protocol("padding exceeds frame payload"));
558    }
559    let end = payload.len() - usize::from(padding);
560    if payload[end..].iter().any(|&byte| byte != 0) {
561        return Err(protocol("padding octets must be zero"));
562    }
563    Ok(&payload[1..end])
564}
565
566fn validate_settings(settings: &Http2Settings) -> Result<()> {
567    if settings.initial_window_size > STREAM_MASK {
568        return Err(connection_error(
569            Http2ErrorCode::FlowControlError,
570            "INITIAL_WINDOW_SIZE exceeds the flow-control window limit",
571        ));
572    }
573    if !(DEFAULT_MAX_FRAME_SIZE..=MAX_FRAME_SIZE).contains(&settings.max_frame_size) {
574        return Err(protocol("MAX_FRAME_SIZE is outside its permitted range"));
575    }
576    Ok(())
577}
578
579fn validate_dependency(stream_id: u32, dependency: u32) -> Result<()> {
580    if dependency > STREAM_MASK {
581        return Err(protocol("stream dependency exceeds 31 bits"));
582    }
583    if stream_id == dependency {
584        return Err(stream_error(
585            stream_id,
586            Http2ErrorCode::ProtocolError,
587            "stream cannot depend on itself",
588        ));
589    }
590    Ok(())
591}
592
593fn validate_increment(stream_id: u32, increment: u32) -> Result<()> {
594    if increment == 0 || increment > STREAM_MASK {
595        if stream_id != 0 {
596            return Err(stream_error(
597                stream_id,
598                Http2ErrorCode::ProtocolError,
599                "WINDOW_UPDATE increment must be in 1..=2^31-1",
600            ));
601        }
602        return Err(protocol("WINDOW_UPDATE increment must be in 1..=2^31-1"));
603    }
604    Ok(())
605}
606
607fn read_u32(bytes: &[u8]) -> u32 {
608    u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
609}
610
611fn connection_error(code: Http2ErrorCode, reason: &str) -> ProtoError {
612    ProtoError::Http2ConnectionError {
613        code,
614        reason: reason.to_owned(),
615    }
616}
617
618fn stream_error(stream_id: u32, code: Http2ErrorCode, reason: &str) -> ProtoError {
619    ProtoError::Http2StreamError {
620        stream_id,
621        code,
622        reason: reason.to_owned(),
623    }
624}
625
626fn protocol(reason: &str) -> ProtoError {
627    connection_error(Http2ErrorCode::ProtocolError, reason)
628}
629
630fn frame_size(reason: &str) -> ProtoError {
631    connection_error(Http2ErrorCode::FrameSizeError, reason)
632}
633
634#[cfg(test)]
635mod unit_tests {
636    use super::*;
637
638    #[test]
639    fn scenario_raw_send_flags_and_reserved_bits_preserve_output_on_error() -> Result<()> {
640        for (frame_type, flags, stream_id, payload) in [
641            (Http2FrameType::Data, 0x80, 1, &b"x"[..]),
642            (Http2FrameType::Ping, 0x80, 0, &[0; 8][..]),
643            (Http2FrameType::WindowUpdate, 0, 0, &[0x80, 0, 0, 1][..]),
644            (
645                Http2FrameType::GoAway,
646                0,
647                0,
648                &[0x80, 0, 0, 1, 0, 0, 0, 0][..],
649            ),
650            (Http2FrameType::PushPromise, 4, 1, &[0x80, 0, 0, 2][..]),
651            (
652                Http2FrameType::PushPromise,
653                0x0c,
654                1,
655                &[0, 0x80, 0, 0, 2][..],
656            ),
657        ] {
658            let header = Http2FrameHeader {
659                length: payload.len() as u32,
660                frame_type,
661                flags,
662                stream_id,
663            };
664            let frame = Http2Frame::Unknown {
665                header,
666                payload: Bytes::copy_from_slice(payload),
667            };
668            let mut output = BytesMut::new();
669            output.extend_from_slice(b"existing");
670            assert!(matches!(
671                encode_frame(&frame, &mut output),
672                Err(ProtoError::Http2ConnectionError {
673                    code: Http2ErrorCode::ProtocolError,
674                    ..
675                })
676            ));
677            assert_eq!(output.as_slice(), b"existing");
678
679            // Reserved bits and unused flags are ignored only on receipt.
680            let mut wire = BytesMut::new();
681            wire.put_u24(header.length);
682            wire.put_u8(header.frame_type.as_byte());
683            wire.put_u8(header.flags);
684            wire.put_u32(header.stream_id);
685            wire.extend_from_slice(payload);
686            assert!(decode_frame(&mut wire, DEFAULT_MAX_FRAME_SIZE)?.is_some());
687            assert!(wire.is_empty());
688        }
689        Ok(())
690    }
691}