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 deadline = Instant::now()
23        .checked_add(timeout)
24        .ok_or(io::ErrorKind::InvalidInput)?;
25    capture_request(
26        stream,
27        Vec::new(),
28        deadline,
29        max_header_bytes,
30        max_body_bytes,
31    )
32}
33
34/// Read only a bounded CRLF request line, without waiting for headers or a body.
35///
36/// Leaves later bytes in the original socket, so a fixture can classify a
37/// concurrent request before deciding whether to complete it. Uses blocking
38/// reads and the caller's absolute deadline; no buffering consumes later bytes.
39/// Callers retain stream provenance, application classification and response policy.
40///
41/// # Errors
42/// Rejects invalid bounds, truncated/oversized/non-UTF-8 lines and deadline/I/O failures.
43pub fn read_http_request_line(
44    stream: &mut TcpStream,
45    deadline: Instant,
46    max_line_bytes: usize,
47) -> io::Result<Vec<u8>> {
48    if max_line_bytes < 3 {
49        return Err(io::ErrorKind::InvalidInput.into());
50    }
51    stream.set_nonblocking(false)?;
52    let mut line = Vec::new();
53    while !line.ends_with(b"\r\n") {
54        if line.len() == max_line_bytes {
55            return Err(io::ErrorKind::InvalidData.into());
56        }
57        set_remaining_timeout(stream, deadline)?;
58        let mut byte = [0];
59        match stream.read(&mut byte) {
60            Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()),
61            Ok(_) => line.push(byte[0]),
62            Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
63            Err(error) => return Err(error),
64        }
65        if byte[0] == b'\n' && !line.ends_with(b"\r\n") {
66            return Err(io::ErrorKind::InvalidData.into());
67        }
68    }
69    validate_line(&line)?;
70    Ok(line)
71}
72
73/// Complete a bounded request after [`read_http_request_line`] on the same socket.
74///
75/// Preserves the caller's original deadline across both phases, includes the line
76/// in the returned bytes/header budget, and uses [`read_http_request`]'s framing
77/// admission. The caller may instead retain an incomplete socket without calling
78/// this function, for example while another request determines a cancellation.
79/// This never reopens the socket or reads the request line a second time.
80///
81/// # Errors
82/// Returns framing, resource, deadline and I/O failures; malformed prefetched lines refuse.
83pub fn read_http_request_with_line(
84    stream: &mut TcpStream,
85    line: &[u8],
86    deadline: Instant,
87    max_header_bytes: usize,
88    max_body_bytes: usize,
89) -> io::Result<Vec<u8>> {
90    validate_line(line)?;
91    if line.len() > max_header_bytes {
92        return Err(io::ErrorKind::InvalidData.into());
93    }
94    capture_request(
95        stream,
96        line.to_vec(),
97        deadline,
98        max_header_bytes,
99        max_body_bytes,
100    )
101}
102
103fn validate_line(line: &[u8]) -> io::Result<()> {
104    let text = std::str::from_utf8(line).map_err(|_| io::ErrorKind::InvalidData)?;
105    let body = text
106        .strip_suffix("\r\n")
107        .ok_or(io::ErrorKind::InvalidData)?;
108    if body.is_empty() || body.contains(['\r', '\n']) {
109        return Err(io::ErrorKind::InvalidData.into());
110    }
111    Ok(())
112}
113
114fn set_remaining_timeout(stream: &TcpStream, deadline: Instant) -> io::Result<()> {
115    let remaining = deadline
116        .checked_duration_since(Instant::now())
117        .filter(|remaining| !remaining.is_zero())
118        .ok_or(io::ErrorKind::TimedOut)?;
119    stream.set_read_timeout(Some(remaining))
120}
121
122fn capture_request(
123    stream: &mut TcpStream,
124    mut bytes: Vec<u8>,
125    deadline: Instant,
126    max_header_bytes: usize,
127    max_body_bytes: usize,
128) -> io::Result<Vec<u8>> {
129    let invalid = || io::Error::from(io::ErrorKind::InvalidData);
130    let capacity = max_header_bytes
131        .checked_add(max_body_bytes)
132        .filter(|_| max_header_bytes >= 4)
133        .ok_or_else(invalid)?;
134    stream.set_nonblocking(false)?;
135    let mut chunk = [0; 4096];
136    loop {
137        set_remaining_timeout(stream, deadline)?;
138        let window = (capacity - bytes.len()).min(chunk.len());
139        if window == 0 {
140            return Err(invalid());
141        }
142        match stream.read(&mut chunk[..window]) {
143            Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()),
144            Ok(count) => bytes.extend_from_slice(&chunk[..count]),
145            Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
146            Err(error) => return Err(error),
147        }
148        if let Some(length) = request_length(&bytes, max_header_bytes, max_body_bytes)? {
149            if bytes.len() == length {
150                return Ok(bytes);
151            }
152            if bytes.len() > length {
153                return Err(invalid());
154            }
155        }
156    }
157}
158
159fn request_length(
160    bytes: &[u8],
161    max_header_bytes: usize,
162    max_body_bytes: usize,
163) -> io::Result<Option<usize>> {
164    let invalid = || io::Error::from(io::ErrorKind::InvalidData);
165    let Some(end) = bytes.windows(4).position(|part| part == b"\r\n\r\n") else {
166        return if bytes.len() > max_header_bytes {
167            Err(invalid())
168        } else {
169            Ok(None)
170        };
171    };
172    if end + 4 > max_header_bytes {
173        return Err(invalid());
174    }
175    let header = std::str::from_utf8(&bytes[..end]).map_err(|_| invalid())?;
176    let mut length = None;
177    for line in header.lines().skip(1) {
178        let (name, value) = line.split_once(':').ok_or_else(invalid)?;
179        if name.eq_ignore_ascii_case("transfer-encoding") {
180            return Err(invalid());
181        }
182        if name.eq_ignore_ascii_case("content-length") {
183            if length.is_some()
184                || value.trim().is_empty()
185                || !value.trim().bytes().all(|byte| byte.is_ascii_digit())
186            {
187                return Err(invalid());
188            }
189            length = Some(value.trim().parse::<usize>().map_err(|_| invalid())?);
190        }
191    }
192    let length = length.unwrap_or(0);
193    if length > max_body_bytes {
194        return Err(invalid());
195    }
196    end.checked_add(4)
197        .and_then(|end| end.checked_add(length))
198        .map(Some)
199        .ok_or_else(invalid)
200}
201
202#[cfg(test)]
203mod tests;