Skip to main content

ic_testkit/testing/
mod.rs

1//! Small native helpers for synthetic transport tests.
2
3use std::{
4    io::{self, Read as _},
5    net::TcpStream,
6    time::{Duration, Instant},
7};
8
9/// Read one bounded raw HTTP request from an accepted test socket.
10///
11/// Selects blocking reads explicitly, including sockets inheriting nonblocking
12/// mode on macOS. Every read uses the remaining single deadline. Missing
13/// Content-Length means no body; repeated/invalid lengths and transfer encoding
14/// are rejected. This helper does not authenticate or interpret application data.
15/// The caller owns responses, write timeouts and failure injection.
16pub fn read_http_request(
17    stream: &mut TcpStream,
18    timeout: Duration,
19    max_header_bytes: usize,
20    max_body_bytes: usize,
21) -> io::Result<Vec<u8>> {
22    let invalid = || io::Error::from(io::ErrorKind::InvalidData);
23    let capacity = max_header_bytes
24        .checked_add(max_body_bytes)
25        .filter(|_| max_header_bytes >= 4)
26        .ok_or_else(invalid)?;
27    let deadline = Instant::now().checked_add(timeout).ok_or_else(invalid)?;
28    stream.set_nonblocking(false)?;
29    let mut bytes = Vec::new();
30    let mut chunk = [0; 4096];
31    loop {
32        let remaining = deadline
33            .checked_duration_since(Instant::now())
34            .filter(|remaining| !remaining.is_zero())
35            .ok_or(io::ErrorKind::TimedOut)?;
36        stream.set_read_timeout(Some(remaining))?;
37        let window = (capacity - bytes.len()).min(chunk.len());
38        if window == 0 {
39            return Err(invalid());
40        }
41        match stream.read(&mut chunk[..window]) {
42            Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()),
43            Ok(count) => bytes.extend_from_slice(&chunk[..count]),
44            Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
45            Err(error) => return Err(error),
46        }
47        if let Some(length) = request_length(&bytes, max_header_bytes, max_body_bytes)? {
48            if bytes.len() == length {
49                return Ok(bytes);
50            }
51            if bytes.len() > length {
52                return Err(invalid());
53            }
54        }
55    }
56}
57
58fn request_length(
59    bytes: &[u8],
60    max_header_bytes: usize,
61    max_body_bytes: usize,
62) -> io::Result<Option<usize>> {
63    let invalid = || io::Error::from(io::ErrorKind::InvalidData);
64    let Some(end) = bytes.windows(4).position(|part| part == b"\r\n\r\n") else {
65        return if bytes.len() > max_header_bytes {
66            Err(invalid())
67        } else {
68            Ok(None)
69        };
70    };
71    if end + 4 > max_header_bytes {
72        return Err(invalid());
73    }
74    let header = std::str::from_utf8(&bytes[..end]).map_err(|_| invalid())?;
75    let mut length = None;
76    for line in header.lines().skip(1) {
77        let (name, value) = line.split_once(':').ok_or_else(invalid)?;
78        if name.eq_ignore_ascii_case("transfer-encoding") {
79            return Err(invalid());
80        }
81        if name.eq_ignore_ascii_case("content-length") {
82            if length.is_some()
83                || value.trim().is_empty()
84                || !value.trim().bytes().all(|byte| byte.is_ascii_digit())
85            {
86                return Err(invalid());
87            }
88            length = Some(value.trim().parse::<usize>().map_err(|_| invalid())?);
89        }
90    }
91    let length = length.unwrap_or(0);
92    if length > max_body_bytes {
93        return Err(invalid());
94    }
95    end.checked_add(4)
96        .and_then(|end| end.checked_add(length))
97        .map(Some)
98        .ok_or_else(invalid)
99}
100
101#[cfg(test)]
102mod tests;