ic_testkit/testing/
mod.rs1use std::{
4 io::{self, Read as _},
5 net::TcpStream,
6 time::{Duration, Instant},
7};
8
9pub 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
34pub 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
73pub 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;