Skip to main content

net_wire/http3/
content.rs

1//! Allocation-free HTTP message-content accounting after HTTP/3 header validation.
2
3use core::fmt;
4
5use super::codepoints::Http3ErrorCode;
6use super::enums::message::Http3HeadersKind;
7use super::frame::Http3Data;
8use super::headers::Http3DecodedHeaderSection;
9
10/// Request context used to determine response content rules.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Http3ResponseContext {
13    /// A response to an ordinary request.
14    Ordinary,
15    /// A response to a HEAD request.
16    Head,
17    /// A response to a CONNECT request.
18    Connect,
19}
20
21/// The HTTP message role whose content is being accounted.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum Http3ContentKind {
24    /// An incoming request.
25    Request,
26    /// An incoming response with its request context.
27    Response(Http3ResponseContext),
28    /// An incoming pushed response.
29    Push,
30}
31
32/// The disposition selected after accepting a header section.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum Http3ContentDisposition {
35    /// An informational response was accepted; a final response is still required.
36    AwaitingFinalResponse,
37    /// Subsequent DATA frames are HTTP message content.
38    HttpMessage,
39    /// Subsequent stream bytes belong to a CONNECT tunnel.
40    ConnectTunnel,
41}
42
43/// Header-section operation expected by message-content accounting.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum Http3ContentOperation {
46    /// A DATA payload can be accepted.
47    Data,
48    /// Initial request headers are required.
49    InitialRequestHeaders,
50    /// An informational or final response header section is required.
51    ResponseHeaders,
52    /// A trailing header section is required.
53    Trailers,
54}
55
56/// Failure while accounting for HTTP message content and Content-Length.
57///
58/// Local operation sequencing and tunnel handoff failures have no HTTP/3 peer error code. The
59/// remaining variants describe peer message-content violations.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum Http3MessageContentError {
62    /// A Content-Length list member is empty or not an ASCII decimal number.
63    InvalidContentLength {
64        /// Decoded field index in wire order.
65        field_index: usize,
66        /// Comma-separated member index within the field value.
67        member_index: usize,
68    },
69    /// A Content-Length list member overflows `u64`.
70    ContentLengthOverflow {
71        /// Decoded field index in wire order.
72        field_index: usize,
73        /// Comma-separated member index within the field value.
74        member_index: usize,
75    },
76    /// A Content-Length list member differs from the earlier normalized value.
77    ConflictingContentLength {
78        /// Decoded field index in wire order.
79        field_index: usize,
80        /// Comma-separated member index within the field value.
81        member_index: usize,
82        /// Earlier normalized Content-Length.
83        expected: u64,
84        /// Conflicting normalized Content-Length.
85        actual: u64,
86    },
87    /// A trailer field section contains Content-Length.
88    ContentLengthInTrailers {
89        /// Decoded trailer field index in wire order.
90        field_index: usize,
91    },
92    /// Content-Length is forbidden for this message form.
93    ProhibitedContentLength {
94        /// Decoded field index in wire order.
95        field_index: usize,
96    },
97    /// A validated header section was supplied at the wrong content-accounting operation.
98    UnexpectedHeaderSection {
99        /// Operation expected by content accounting.
100        expected: Http3ContentOperation,
101        /// Validated role carried by the supplied section.
102        actual: Http3HeadersKind,
103    },
104    /// A local content-accounting operation cannot proceed in the current message phase.
105    OperationNotReady {
106        /// Operation that cannot proceed.
107        operation: Http3ContentOperation,
108    },
109    /// DATA is forbidden because this message form has no HTTP content.
110    ContentNotAllowed {
111        /// Forbidden DATA payload length.
112        data_length: usize,
113    },
114    /// A DATA payload length cannot be represented as `u64`.
115    DataLengthNotRepresentable {
116        /// DATA payload length.
117        length: usize,
118    },
119    /// Adding a DATA payload length overflows cumulative received length.
120    ReceivedLengthOverflow {
121        /// Cumulative length before the DATA frame.
122        received_length: u64,
123        /// DATA payload length after conversion to `u64`.
124        frame_length: u64,
125    },
126    /// Received HTTP content does not equal the declared Content-Length.
127    ContentLengthMismatch {
128        /// Declared normalized Content-Length.
129        declared: u64,
130        /// Received HTTP content length.
131        received: u64,
132    },
133    /// A caller supplied an HTTP-content operation after CONNECT tunnel handoff.
134    NotHttpMessageContent,
135    /// The message ended before required initial or final headers were accepted.
136    IncompleteMessage,
137}
138
139impl Http3MessageContentError {
140    /// Returns the HTTP/3 peer error code when this failure is peer-caused.
141    pub const fn error_code(self) -> Option<Http3ErrorCode> {
142        match self {
143            Self::UnexpectedHeaderSection { .. }
144            | Self::OperationNotReady { .. }
145            | Self::NotHttpMessageContent => None,
146            Self::InvalidContentLength { .. }
147            | Self::ContentLengthOverflow { .. }
148            | Self::ConflictingContentLength { .. }
149            | Self::ContentLengthInTrailers { .. }
150            | Self::ProhibitedContentLength { .. }
151            | Self::ContentNotAllowed { .. }
152            | Self::DataLengthNotRepresentable { .. }
153            | Self::ReceivedLengthOverflow { .. }
154            | Self::ContentLengthMismatch { .. }
155            | Self::IncompleteMessage => Some(Http3ErrorCode::MESSAGE_ERROR),
156        }
157    }
158}
159
160impl fmt::Display for Http3MessageContentError {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            Self::InvalidContentLength {
164                field_index,
165                member_index,
166            } => write!(
167                f,
168                "HTTP/3 Content-Length field {field_index} member {member_index} is invalid"
169            ),
170            Self::ContentLengthOverflow {
171                field_index,
172                member_index,
173            } => write!(
174                f,
175                "HTTP/3 Content-Length field {field_index} member {member_index} overflows"
176            ),
177            Self::ConflictingContentLength {
178                field_index,
179                member_index,
180                expected,
181                actual,
182            } => write!(
183                f,
184                "HTTP/3 Content-Length field {field_index} member {member_index} is {actual}, expected {expected}"
185            ),
186            Self::ContentLengthInTrailers { field_index } => {
187                write!(
188                    f,
189                    "HTTP/3 trailer field {field_index} contains Content-Length"
190                )
191            }
192            Self::ProhibitedContentLength { field_index } => {
193                write!(
194                    f,
195                    "HTTP/3 field {field_index} has prohibited Content-Length"
196                )
197            }
198            Self::UnexpectedHeaderSection { expected, actual } => write!(
199                f,
200                "HTTP/3 {actual:?} header section is unexpected while accepting {expected:?}"
201            ),
202            Self::OperationNotReady { operation } => {
203                write!(
204                    f,
205                    "HTTP/3 {operation:?} operation is not ready in the current message phase"
206                )
207            }
208            Self::ContentNotAllowed { data_length } => {
209                write!(
210                    f,
211                    "HTTP/3 DATA payload of {data_length} bytes is not allowed"
212                )
213            }
214            Self::DataLengthNotRepresentable { length } => {
215                write!(
216                    f,
217                    "HTTP/3 DATA payload length {length} cannot be represented as u64"
218                )
219            }
220            Self::ReceivedLengthOverflow {
221                received_length,
222                frame_length,
223            } => write!(
224                f,
225                "HTTP/3 received content length overflows: {received_length} plus {frame_length}"
226            ),
227            Self::ContentLengthMismatch { declared, received } => write!(
228                f,
229                "HTTP/3 Content-Length mismatch: declared {declared}, received {received}"
230            ),
231            Self::NotHttpMessageContent => {
232                f.write_str("HTTP/3 operation belongs to CONNECT tunnel handling, not HTTP content")
233            }
234            Self::IncompleteMessage => f.write_str("HTTP/3 message content headers are incomplete"),
235        }
236    }
237}
238
239impl core::error::Error for Http3MessageContentError {}
240
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242enum Phase {
243    BeforeInitial,
244    BeforeFinal,
245    Content,
246    NoContent,
247    NoContentTrailers,
248    Tunnel,
249    Trailers,
250}
251
252/// Bounded cumulative HTTP message-content and Content-Length state.
253///
254/// The caller supplies QPACK-decoded, semantically validated field sections before committing
255/// the corresponding message-stream transition. This state does not sequence frames or own tunnel
256/// bytes.
257#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub struct Http3MessageContentState {
259    kind: Http3ContentKind,
260    phase: Phase,
261    declared_length: Option<u64>,
262    received_length: u64,
263}
264
265impl Http3MessageContentState {
266    /// Creates accounting state for one incoming request, response, or push message.
267    pub const fn new(kind: Http3ContentKind) -> Self {
268        let phase = match kind {
269            Http3ContentKind::Request => Phase::BeforeInitial,
270            Http3ContentKind::Response(_) | Http3ContentKind::Push => Phase::BeforeFinal,
271        };
272        Self {
273            kind,
274            phase,
275            declared_length: None,
276            received_length: 0,
277        }
278    }
279
280    /// Returns this message's role.
281    pub const fn kind(self) -> Http3ContentKind {
282        self.kind
283    }
284
285    /// Returns the normalized initial Content-Length, if present.
286    pub const fn declared_length(self) -> Option<u64> {
287        self.declared_length
288    }
289
290    /// Returns HTTP message-content bytes accepted so far.
291    ///
292    /// Tunnel bytes are deliberately not counted.
293    pub const fn received_length(self) -> u64 {
294        self.received_length
295    }
296
297    /// Accepts an initial request header section or an informational/final response section.
298    pub fn accept_initial_headers(
299        &mut self,
300        section: Http3DecodedHeaderSection<'_>,
301    ) -> Result<Http3ContentDisposition, Http3MessageContentError> {
302        match self.phase {
303            Phase::BeforeInitial => self.accept_request(section),
304            Phase::BeforeFinal => self.accept_response(section),
305            _ => Err(Http3MessageContentError::OperationNotReady {
306                operation: self.initial_headers_operation(),
307            }),
308        }
309    }
310
311    /// Accounts for one HTTP DATA payload without buffering it.
312    pub fn receive_data(&mut self, data: Http3Data<'_>) -> Result<(), Http3MessageContentError> {
313        match self.phase {
314            Phase::Tunnel => Err(Http3MessageContentError::NotHttpMessageContent),
315            Phase::NoContent | Phase::NoContentTrailers if data.data().is_empty() => Ok(()),
316            Phase::NoContent | Phase::NoContentTrailers => {
317                Err(Http3MessageContentError::ContentNotAllowed {
318                    data_length: data.data().len(),
319                })
320            }
321            Phase::Content => {
322                let frame_length = u64::try_from(data.data().len()).map_err(|_| {
323                    Http3MessageContentError::DataLengthNotRepresentable {
324                        length: data.data().len(),
325                    }
326                })?;
327                let received_length = self.received_length.checked_add(frame_length).ok_or(
328                    Http3MessageContentError::ReceivedLengthOverflow {
329                        received_length: self.received_length,
330                        frame_length,
331                    },
332                )?;
333                if let Some(declared) = self.declared_length
334                    && received_length > declared
335                {
336                    return Err(Http3MessageContentError::ContentLengthMismatch {
337                        declared,
338                        received: received_length,
339                    });
340                }
341                self.received_length = received_length;
342                Ok(())
343            }
344            Phase::BeforeInitial | Phase::BeforeFinal | Phase::Trailers => {
345                Err(Http3MessageContentError::OperationNotReady {
346                    operation: Http3ContentOperation::Data,
347                })
348            }
349        }
350    }
351
352    /// Accepts an analyzer-valid trailer section after content.
353    pub fn accept_trailers(
354        &mut self,
355        section: Http3DecodedHeaderSection<'_>,
356    ) -> Result<(), Http3MessageContentError> {
357        if section.kind() != Http3HeadersKind::Trailers {
358            return Err(Http3MessageContentError::UnexpectedHeaderSection {
359                expected: Http3ContentOperation::Trailers,
360                actual: section.kind(),
361            });
362        }
363        match self.phase {
364            Phase::Tunnel => return Err(Http3MessageContentError::NotHttpMessageContent),
365            Phase::Content | Phase::NoContentTrailers => {}
366            _ => {
367                return Err(Http3MessageContentError::OperationNotReady {
368                    operation: Http3ContentOperation::Trailers,
369                });
370            }
371        }
372        if let Some(field_index) = first_content_length(section) {
373            return Err(Http3MessageContentError::ContentLengthInTrailers { field_index });
374        }
375        if self.phase == Phase::Content {
376            self.length_matches()?;
377        }
378        self.phase = Phase::Trailers;
379        Ok(())
380    }
381
382    /// Verifies that required headers arrived and Content-Length matches accepted HTTP content.
383    pub fn finish(&self) -> Result<(), Http3MessageContentError> {
384        match self.phase {
385            Phase::BeforeInitial | Phase::BeforeFinal => {
386                Err(Http3MessageContentError::IncompleteMessage)
387            }
388            Phase::Content => self.length_matches(),
389            Phase::NoContent | Phase::NoContentTrailers | Phase::Tunnel | Phase::Trailers => Ok(()),
390        }
391    }
392
393    fn accept_request(
394        &mut self,
395        section: Http3DecodedHeaderSection<'_>,
396    ) -> Result<Http3ContentDisposition, Http3MessageContentError> {
397        if section.kind() != Http3HeadersKind::Request {
398            return Err(Http3MessageContentError::UnexpectedHeaderSection {
399                expected: Http3ContentOperation::InitialRequestHeaders,
400                actual: section.kind(),
401            });
402        }
403        let declared_length = parse_content_length(section)?;
404        let connect = section
405            .section()
406            .iter()
407            .any(|field| field.name() == b":method" && field.value() == b"CONNECT");
408        self.declared_length = declared_length;
409        self.phase = if connect {
410            Phase::Tunnel
411        } else {
412            Phase::Content
413        };
414        Ok(if connect {
415            Http3ContentDisposition::ConnectTunnel
416        } else {
417            Http3ContentDisposition::HttpMessage
418        })
419    }
420
421    fn accept_response(
422        &mut self,
423        section: Http3DecodedHeaderSection<'_>,
424    ) -> Result<Http3ContentDisposition, Http3MessageContentError> {
425        match section.kind() {
426            Http3HeadersKind::InformationalResponse => {
427                if let Some(field_index) = first_content_length(section) {
428                    return Err(Http3MessageContentError::ProhibitedContentLength { field_index });
429                }
430                Ok(Http3ContentDisposition::AwaitingFinalResponse)
431            }
432            Http3HeadersKind::FinalResponse => self.accept_final_response(section),
433            _ => Err(Http3MessageContentError::UnexpectedHeaderSection {
434                expected: Http3ContentOperation::ResponseHeaders,
435                actual: section.kind(),
436            }),
437        }
438    }
439
440    fn accept_final_response(
441        &mut self,
442        section: Http3DecodedHeaderSection<'_>,
443    ) -> Result<Http3ContentDisposition, Http3MessageContentError> {
444        let status = response_status(section);
445        let prohibited = match self.kind {
446            Http3ContentKind::Response(Http3ResponseContext::Connect) => {
447                (200..=299).contains(&status)
448            }
449            Http3ContentKind::Response(Http3ResponseContext::Head)
450            | Http3ContentKind::Response(Http3ResponseContext::Ordinary)
451            | Http3ContentKind::Push => status == 204,
452            Http3ContentKind::Request => false,
453        };
454        if prohibited && let Some(field_index) = first_content_length(section) {
455            return Err(Http3MessageContentError::ProhibitedContentLength { field_index });
456        }
457        let declared_length = parse_content_length(section)?;
458        let (phase, disposition) = match self.kind {
459            Http3ContentKind::Response(Http3ResponseContext::Connect)
460                if (200..=299).contains(&status) =>
461            {
462                (Phase::Tunnel, Http3ContentDisposition::ConnectTunnel)
463            }
464            _ if status == 204 => (Phase::NoContent, Http3ContentDisposition::HttpMessage),
465            Http3ContentKind::Response(Http3ResponseContext::Ordinary)
466            | Http3ContentKind::Response(Http3ResponseContext::Head)
467            | Http3ContentKind::Push
468                if status == 205 =>
469            {
470                (
471                    Phase::NoContentTrailers,
472                    Http3ContentDisposition::HttpMessage,
473                )
474            }
475            Http3ContentKind::Response(Http3ResponseContext::Head) => (
476                Phase::NoContentTrailers,
477                Http3ContentDisposition::HttpMessage,
478            ),
479            Http3ContentKind::Response(Http3ResponseContext::Ordinary) | Http3ContentKind::Push
480                if status == 304 =>
481            {
482                (
483                    Phase::NoContentTrailers,
484                    Http3ContentDisposition::HttpMessage,
485                )
486            }
487            _ => (Phase::Content, Http3ContentDisposition::HttpMessage),
488        };
489        self.declared_length = declared_length;
490        self.phase = phase;
491        Ok(disposition)
492    }
493
494    fn length_matches(&self) -> Result<(), Http3MessageContentError> {
495        match self.declared_length {
496            Some(declared) if declared != self.received_length => {
497                Err(Http3MessageContentError::ContentLengthMismatch {
498                    declared,
499                    received: self.received_length,
500                })
501            }
502            _ => Ok(()),
503        }
504    }
505
506    const fn initial_headers_operation(&self) -> Http3ContentOperation {
507        match self.kind {
508            Http3ContentKind::Request => Http3ContentOperation::InitialRequestHeaders,
509            Http3ContentKind::Response(_) | Http3ContentKind::Push => {
510                Http3ContentOperation::ResponseHeaders
511            }
512        }
513    }
514}
515
516fn first_content_length(section: Http3DecodedHeaderSection<'_>) -> Option<usize> {
517    section
518        .section()
519        .iter()
520        .enumerate()
521        .find_map(|(index, field)| (field.name() == b"content-length").then_some(index))
522}
523
524fn parse_content_length(
525    section: Http3DecodedHeaderSection<'_>,
526) -> Result<Option<u64>, Http3MessageContentError> {
527    let mut declared = None;
528    for (field_index, field) in section.section().iter().enumerate() {
529        if field.name() != b"content-length" {
530            continue;
531        }
532        for (member_index, member) in field.value().split(|byte| *byte == b',').enumerate() {
533            let value = trim_ows(member);
534            let actual = parse_decimal(value, field_index, member_index)?;
535            if let Some(expected) = declared
536                && expected != actual
537            {
538                return Err(Http3MessageContentError::ConflictingContentLength {
539                    field_index,
540                    member_index,
541                    expected,
542                    actual,
543                });
544            }
545            declared = Some(actual);
546        }
547    }
548    Ok(declared)
549}
550
551fn parse_decimal(
552    value: &[u8],
553    field_index: usize,
554    member_index: usize,
555) -> Result<u64, Http3MessageContentError> {
556    if value.is_empty() || !value.iter().all(|byte| byte.is_ascii_digit()) {
557        return Err(Http3MessageContentError::InvalidContentLength {
558            field_index,
559            member_index,
560        });
561    }
562    let mut result = 0_u64;
563    for byte in value {
564        result = result
565            .checked_mul(10)
566            .and_then(|number| number.checked_add(u64::from(*byte - b'0')))
567            .ok_or(Http3MessageContentError::ContentLengthOverflow {
568                field_index,
569                member_index,
570            })?;
571    }
572    Ok(result)
573}
574
575fn trim_ows(mut value: &[u8]) -> &[u8] {
576    while matches!(value.first(), Some(b' ' | b'\t')) {
577        value = &value[1..];
578    }
579    while matches!(value.last(), Some(b' ' | b'\t')) {
580        value = &value[..value.len() - 1];
581    }
582    value
583}
584
585fn response_status(section: Http3DecodedHeaderSection<'_>) -> u16 {
586    for field in section.section().iter() {
587        if field.name() == b":status" {
588            let value = field.value();
589            if let (Some(hundreds), Some(tens), Some(ones)) =
590                (value.first(), value.get(1), value.get(2))
591            {
592                return u16::from(*hundreds - b'0') * 100
593                    + u16::from(*tens - b'0') * 10
594                    + u16::from(*ones - b'0');
595            }
596        }
597    }
598    0
599}