Skip to main content

netune_http1/
reader.rs

1//! Response head and body decoding.
2//!
3//! The reader owns its input and a small receive buffer. Everything it returns
4//! is a slice of that buffer, so no copy is made between the socket and the
5//! caller. Leftover bytes after the body ends are handed back by
6//! [`Http1Reader::into_inner`] — on a pooled connection they belong to the next
7//! response, and dropping them would corrupt the stream.
8
9use bytes::{Bytes, BytesMut};
10use http::{HeaderMap, Method, StatusCode, Version};
11use tokio::io::{AsyncRead, AsyncReadExt};
12
13use crate::error::HttpError;
14use crate::head::{BodyKind, ResponseHead};
15
16/// Hard bounds on what a peer may make us allocate or scan.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Limits {
19    /// Maximum bytes of status line plus headers.
20    pub max_head_bytes: usize,
21    /// Maximum number of header fields.
22    pub max_headers: usize,
23    /// Maximum size of a single chunk.
24    pub max_chunk_bytes: u64,
25    /// Maximum body accumulated by [`Http1Reader::read_body_to_end`].
26    pub max_body_bytes: u64,
27}
28
29impl Default for Limits {
30    fn default() -> Self {
31        Self {
32            max_head_bytes: 64 * 1024,
33            max_headers: 128,
34            max_chunk_bytes: 16 * 1024 * 1024,
35            max_body_bytes: 256 * 1024 * 1024,
36        }
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum BodyState {
42    /// The head has not been read yet.
43    Head,
44    Empty,
45    Length {
46        remaining: u64,
47    },
48    Chunked {
49        remaining: u64,
50        need_crlf: bool,
51    },
52    UntilClose,
53    Done,
54}
55
56/// Decodes one HTTP/1.x response from `io`.
57#[derive(Debug)]
58pub struct Http1Reader<R> {
59    io: R,
60    buf: BytesMut,
61    limits: Limits,
62    body: BodyState,
63}
64
65impl<R: AsyncRead + Unpin> Http1Reader<R> {
66    pub fn new(io: R) -> Self {
67        Self::with_limits(io, Limits::default())
68    }
69
70    pub fn with_limits(io: R, limits: Limits) -> Self {
71        Self::with_limits_and_buffer(io, limits, BytesMut::new())
72    }
73
74    /// Resume a reader on a pooled connection, seeding the buffer with bytes the
75    /// previous response left behind (pipelined or simply already delivered).
76    pub fn with_limits_and_buffer(io: R, limits: Limits, buffered: BytesMut) -> Self {
77        let mut buf = BytesMut::with_capacity(8 * 1024 + buffered.len());
78        buf.extend_from_slice(&buffered);
79        Self {
80            io,
81            buf,
82            limits,
83            body: BodyState::Head,
84        }
85    }
86
87    /// Recover the transport and any bytes already buffered beyond the body.
88    pub fn into_inner(self) -> (R, BytesMut) {
89        (self.io, self.buf)
90    }
91
92    /// Parse the response head. `request_method` is needed because a `HEAD`
93    /// response carries no body regardless of its framing headers.
94    pub async fn read_response_head(
95        &mut self,
96        request_method: &Method,
97    ) -> Result<ResponseHead, HttpError> {
98        if self.body != BodyState::Head {
99            return Err(HttpError::Protocol("head already read"));
100        }
101
102        let status_line = self.read_line(self.limits.max_head_bytes).await?;
103        let status_line = std::str::from_utf8(&status_line)
104            .map_err(|_| HttpError::Protocol("status line is not UTF-8"))?;
105        let mut parts = status_line.splitn(3, ' ');
106        let version = match parts.next() {
107            Some("HTTP/1.1") => Version::HTTP_11,
108            Some("HTTP/1.0") => Version::HTTP_10,
109            _ => return Err(HttpError::Protocol("unsupported HTTP version")),
110        };
111        let code = parts
112            .next()
113            .ok_or(HttpError::Protocol("status line has no status code"))?;
114        let status = code
115            .parse::<u16>()
116            .ok()
117            .filter(|code| (100..1000).contains(code))
118            .and_then(|code| StatusCode::from_u16(code).ok())
119            .ok_or(HttpError::Protocol("invalid status code"))?;
120
121        let mut headers = HeaderMap::new();
122        let mut count = 0usize;
123        let mut head_bytes = status_line.len();
124        loop {
125            let line = self
126                .read_line(self.limits.max_head_bytes.saturating_sub(head_bytes))
127                .await?;
128            head_bytes = head_bytes.saturating_add(line.len() + 2);
129            if head_bytes > self.limits.max_head_bytes {
130                return Err(HttpError::LimitExceeded("max_head_bytes"));
131            }
132            if line.is_empty() {
133                break;
134            }
135            count += 1;
136            if count > self.limits.max_headers {
137                return Err(HttpError::LimitExceeded("max_headers"));
138            }
139            if matches!(line.first(), Some(b' ') | Some(b'\t')) {
140                // RFC 9112 forbids obsolete line folding; accepting it is a
141                // request-smuggling vector.
142                return Err(HttpError::Protocol("obsolete header line folding"));
143            }
144            let colon = line
145                .iter()
146                .position(|&b| b == b':')
147                .ok_or(HttpError::Protocol("header line has no colon"))?;
148            let (name, rest) = line.split_at(colon);
149            let value = &rest[1..];
150            let name = http::header::HeaderName::from_bytes(name)
151                .map_err(|_| HttpError::Protocol("invalid header name"))?;
152            let value = http::header::HeaderValue::from_bytes(trim_ows(value))
153                .map_err(|_| HttpError::Protocol("invalid header value"))?;
154            headers.append(name, value);
155        }
156
157        let body = Self::body_kind(&headers, &status, request_method)?;
158        self.body = match body {
159            BodyKind::Empty => BodyState::Empty,
160            BodyKind::Length(len) => BodyState::Length { remaining: len },
161            BodyKind::Chunked => BodyState::Chunked {
162                remaining: 0,
163                need_crlf: false,
164            },
165            BodyKind::UntilClose => BodyState::UntilClose,
166        };
167        Ok(ResponseHead {
168            version,
169            status,
170            headers,
171            body,
172        })
173    }
174
175    fn body_kind(
176        headers: &HeaderMap,
177        status: &StatusCode,
178        request_method: &Method,
179    ) -> Result<BodyKind, HttpError> {
180        if request_method == Method::HEAD || ResponseHead::status_forbids_body(*status) {
181            return Ok(BodyKind::Empty);
182        }
183        let chunked = headers
184            .get_all(http::header::TRANSFER_ENCODING)
185            .iter()
186            .filter_map(|value| value.to_str().ok())
187            .flat_map(|value| value.split(','))
188            .any(|coding| coding.trim().eq_ignore_ascii_case("chunked"));
189        let lengths: Vec<u64> = headers
190            .get_all(http::header::CONTENT_LENGTH)
191            .iter()
192            .filter_map(|value| value.to_str().ok())
193            .flat_map(|value| value.split(','))
194            .filter_map(|value| value.trim().parse::<u64>().ok())
195            .collect();
196        if chunked {
197            if !lengths.is_empty() {
198                // RFC 9112 §6.1 lets transfer-encoding win, but a message with
199                // both is the classic smuggling shape: refuse it.
200                return Err(HttpError::Protocol(
201                    "both transfer-encoding and content-length",
202                ));
203            }
204            return Ok(BodyKind::Chunked);
205        }
206        if !lengths.is_empty() {
207            if lengths.iter().any(|len| *len != lengths[0]) {
208                return Err(HttpError::Protocol("conflicting content-length headers"));
209            }
210            return Ok(BodyKind::Length(lengths[0]));
211        }
212        Ok(BodyKind::UntilClose)
213    }
214
215    /// Read the next body bytes, or `None` at the end of the body.
216    ///
217    /// The returned slice may be smaller than the remaining body; call again.
218    pub async fn read_body_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
219        loop {
220            match self.body {
221                BodyState::Head => return Err(HttpError::Protocol("head not read")),
222                BodyState::Empty | BodyState::Done => return Ok(None),
223                BodyState::Length { remaining } => {
224                    if remaining == 0 {
225                        self.body = BodyState::Done;
226                        return Ok(None);
227                    }
228                    if self.buf.is_empty() {
229                        if self.fill().await? == 0 {
230                            return Err(HttpError::Incomplete("body truncated"));
231                        }
232                        continue;
233                    }
234                    let take = self
235                        .buf
236                        .len()
237                        .min(usize::try_from(remaining).unwrap_or(usize::MAX));
238                    let chunk = self.buf.split_to(take).freeze();
239                    self.body = BodyState::Length {
240                        remaining: remaining - take as u64,
241                    };
242                    return Ok(Some(chunk));
243                }
244                BodyState::Chunked {
245                    remaining,
246                    need_crlf,
247                } => {
248                    if need_crlf {
249                        let crlf = self.read_exact(2).await?;
250                        if &crlf[..] != b"\r\n" {
251                            return Err(HttpError::Protocol("chunk data not CRLF-terminated"));
252                        }
253                        self.body = BodyState::Chunked {
254                            remaining,
255                            need_crlf: false,
256                        };
257                        continue;
258                    }
259                    if remaining == 0 {
260                        let size = self.read_chunk_size().await?;
261                        if size == 0 {
262                            self.read_trailers().await?;
263                            self.body = BodyState::Done;
264                            return Ok(None);
265                        }
266                        if size > self.limits.max_chunk_bytes {
267                            return Err(HttpError::LimitExceeded("max_chunk_bytes"));
268                        }
269                        self.body = BodyState::Chunked {
270                            remaining: size,
271                            need_crlf: false,
272                        };
273                        continue;
274                    }
275                    if self.buf.is_empty() {
276                        if self.fill().await? == 0 {
277                            return Err(HttpError::Incomplete("chunk truncated"));
278                        }
279                        continue;
280                    }
281                    let take = self
282                        .buf
283                        .len()
284                        .min(usize::try_from(remaining).unwrap_or(usize::MAX));
285                    let chunk = self.buf.split_to(take).freeze();
286                    let remaining = remaining - take as u64;
287                    self.body = BodyState::Chunked {
288                        remaining,
289                        need_crlf: remaining == 0,
290                    };
291                    return Ok(Some(chunk));
292                }
293                BodyState::UntilClose => {
294                    if !self.buf.is_empty() {
295                        let chunk = self.buf.split().freeze();
296                        return Ok(Some(chunk));
297                    }
298                    if self.fill().await? == 0 {
299                        self.body = BodyState::Done;
300                        return Ok(None);
301                    }
302                }
303            }
304        }
305    }
306
307    /// Drain the body into one buffer, bounded by [`Limits::max_body_bytes`].
308    pub async fn read_body_to_end(&mut self) -> Result<Bytes, HttpError> {
309        let mut out = BytesMut::new();
310        while let Some(chunk) = self.read_body_chunk().await? {
311            if out.len() as u64 + chunk.len() as u64 > self.limits.max_body_bytes {
312                return Err(HttpError::LimitExceeded("max_body_bytes"));
313            }
314            out.extend_from_slice(&chunk);
315        }
316        Ok(out.freeze())
317    }
318
319    async fn read_chunk_size(&mut self) -> Result<u64, HttpError> {
320        // A chunk-size line is tiny; bound it independently of the head budget.
321        let line = self.read_line(8 * 1024).await?;
322        let digits = line
323            .iter()
324            .position(|&b| b == b';')
325            .map_or(&line[..], |semicolon| &line[..semicolon]);
326        let text = std::str::from_utf8(digits)
327            .map_err(|_| HttpError::Protocol("chunk size is not ASCII"))?
328            .trim();
329        if text.is_empty() || text.len() > 16 {
330            return Err(HttpError::Protocol("invalid chunk size"));
331        }
332        u64::from_str_radix(text, 16).map_err(|_| HttpError::Protocol("invalid chunk size"))
333    }
334
335    async fn read_trailers(&mut self) -> Result<(), HttpError> {
336        let mut count = 0usize;
337        let mut bytes = 0usize;
338        loop {
339            let line = self
340                .read_line(self.limits.max_head_bytes.saturating_sub(bytes))
341                .await?;
342            bytes = bytes.saturating_add(line.len() + 2);
343            if bytes > self.limits.max_head_bytes {
344                return Err(HttpError::LimitExceeded("max_head_bytes"));
345            }
346            if line.is_empty() {
347                return Ok(());
348            }
349            count += 1;
350            if count > self.limits.max_headers {
351                return Err(HttpError::LimitExceeded("max_headers"));
352            }
353        }
354    }
355
356    /// Read one CRLF-terminated line, without the terminator. `max` bounds how
357    /// many bytes may accumulate before a terminator arrives, so a peer that
358    /// never sends `\n` cannot grow the buffer without limit.
359    async fn read_line(&mut self, max: usize) -> Result<Bytes, HttpError> {
360        loop {
361            if let Some(position) = self.buf.iter().position(|&byte| byte == b'\n') {
362                let mut line = self.buf.split_to(position + 1);
363                line.truncate(position);
364                if line.last() == Some(&b'\r') {
365                    line.truncate(position - 1);
366                }
367                return Ok(line.freeze());
368            }
369            if self.buf.len() >= max {
370                return Err(HttpError::LimitExceeded("max_head_bytes"));
371            }
372            if self.fill().await? == 0 {
373                return Err(HttpError::Incomplete("connection closed inside head"));
374            }
375        }
376    }
377
378    /// Read exactly `n` bytes.
379    async fn read_exact(&mut self, n: usize) -> Result<Bytes, HttpError> {
380        while self.buf.len() < n {
381            if self.fill().await? == 0 {
382                return Err(HttpError::Protocol("body truncated"));
383            }
384        }
385        Ok(self.buf.split_to(n).freeze())
386    }
387
388    /// Pull more bytes from the transport. Returns 0 at end of stream.
389    async fn fill(&mut self) -> Result<usize, HttpError> {
390        let read = self.io.read_buf(&mut self.buf).await?;
391        Ok(read)
392    }
393}
394
395/// Trim optional whitespace (SP / HTAB) from both ends, as RFC 9112 requires for
396/// header values.
397fn trim_ows(mut value: &[u8]) -> &[u8] {
398    while matches!(value.first(), Some(b' ') | Some(b'\t')) {
399        value = &value[1..];
400    }
401    while matches!(value.last(), Some(b' ') | Some(b'\t')) {
402        value = &value[..value.len() - 1];
403    }
404    value
405}