1use std::io::{self, BufRead, Read};
6
7pub const MAX_BODY: usize = 256 * 1024 * 1024;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Request {
12 pub method: String,
13 pub target: String,
15 pub headers: Vec<(String, String)>,
16 pub body: Vec<u8>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Response {
21 pub status: u16,
22 pub headers: Vec<(String, String)>,
23 pub body: Vec<u8>,
24}
25
26impl Response {
27 pub fn header(&self, name: &str) -> Option<&str> {
28 self.headers
29 .iter()
30 .find(|(k, _)| k.eq_ignore_ascii_case(name))
31 .map(|(_, v)| v.as_str())
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Url {
38 pub https: bool,
39 pub host: String,
40 pub port: u16,
41 pub base_path: String,
43}
44
45impl Url {
46 pub fn parse(text: &str) -> Result<Url, String> {
47 let text = text.trim();
48 let (https, rest) = if let Some(rest) = text.strip_prefix("https://") {
49 (true, rest)
50 } else if let Some(rest) = text.strip_prefix("http://") {
51 (false, rest)
52 } else {
53 return Err(format!(
54 "the address `{text}` must start with http:// or https://"
55 ));
56 };
57 let (authority, path) = match rest.find('/') {
58 Some(i) => (&rest[..i], &rest[i..]),
59 None => (rest, ""),
60 };
61 if authority.is_empty() || authority.contains('@') {
62 return Err(format!("the address `{text}` has no valid host"));
63 }
64 let default_port = if https { 443 } else { 80 };
65 let (host, port) = if let Some(inner) = authority.strip_prefix('[') {
66 let end = inner
68 .find(']')
69 .ok_or_else(|| format!("the address `{text}` has an IPv6 without `]`"))?;
70 let port = match &inner[end + 1..] {
71 "" => default_port,
72 p => parse_port(p.strip_prefix(':').ok_or("invalid port")?, text)?,
73 };
74 (inner[..end].to_owned(), port)
75 } else {
76 match authority.rsplit_once(':') {
77 Some((host, port)) => (host.to_owned(), parse_port(port, text)?),
78 None => (authority.to_owned(), default_port),
79 }
80 };
81 if host.is_empty() {
82 return Err(format!("the address `{text}` has no valid host"));
83 }
84 Ok(Url {
85 https,
86 host,
87 port,
88 base_path: path.trim_end_matches('/').to_owned(),
89 })
90 }
91
92 pub fn host_header(&self) -> String {
94 let host = if self.host.contains(':') {
95 format!("[{}]", self.host)
96 } else {
97 self.host.clone()
98 };
99 let default_port = if self.https { 443 } else { 80 };
100 if self.port == default_port {
101 host
102 } else {
103 format!("{host}:{}", self.port)
104 }
105 }
106
107 pub fn with_port(&self, port: u16) -> Url {
109 Url {
110 port,
111 ..self.clone()
112 }
113 }
114
115 pub fn origin(&self) -> String {
117 format!(
118 "{}://{}",
119 if self.https { "https" } else { "http" },
120 self.host_header()
121 )
122 }
123}
124
125fn parse_port(port: &str, text: &str) -> Result<u16, String> {
126 port.parse::<u16>()
127 .ok()
128 .filter(|p| *p != 0)
129 .ok_or_else(|| format!("the address `{text}` has an invalid port"))
130}
131
132pub fn percent_encode(s: &str) -> String {
134 let mut out = String::with_capacity(s.len());
135 for b in s.bytes() {
136 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
137 out.push(b as char);
138 } else {
139 out.push_str(&format!("%{b:02X}"));
140 }
141 }
142 out
143}
144
145pub fn build_target(base_path: &str, path: &str, query: &[(String, String)]) -> String {
148 let mut target = format!("{base_path}{path}");
149 for (n, (key, value)) in query.iter().enumerate() {
150 target.push(if n == 0 { '?' } else { '&' });
151 target.push_str(&percent_encode(key));
152 target.push('=');
153 target.push_str(&percent_encode(value));
154 }
155 target
156}
157
158pub fn encode_request(host: &str, req: &Request) -> Vec<u8> {
160 let mut head = format!("{} {} HTTP/1.1\r\nHost: {host}\r\n", req.method, req.target);
161 for (k, v) in &req.headers {
162 head.push_str(&format!("{k}: {v}\r\n"));
163 }
164 if !req.body.is_empty() || matches!(req.method.as_str(), "POST" | "PUT" | "PATCH") {
165 head.push_str(&format!("Content-Length: {}\r\n", req.body.len()));
166 }
167 head.push_str("Connection: close\r\n\r\n");
168 let mut bytes = head.into_bytes();
169 bytes.extend_from_slice(&req.body);
170 bytes
171}
172
173#[derive(Debug)]
174pub enum ReadError {
175 Io {
177 error: io::Error,
178 received: bool,
179 },
180 Malformed(&'static str),
181}
182
183fn io_err(error: io::Error, received: bool) -> ReadError {
184 ReadError::Io { error, received }
185}
186
187fn read_line<R: BufRead>(r: &mut R, received: bool) -> Result<String, ReadError> {
188 let mut line = Vec::new();
189 let n = r
190 .take(64 * 1024)
191 .read_until(b'\n', &mut line)
192 .map_err(|e| io_err(e, received))?;
193 if n == 0 {
194 return Err(io_err(io::ErrorKind::UnexpectedEof.into(), received));
195 }
196 if line.last() != Some(&b'\n') {
197 return Err(ReadError::Malformed("header line without an end"));
198 }
199 while matches!(line.last(), Some(b'\n' | b'\r')) {
200 line.pop();
201 }
202 String::from_utf8(line).map_err(|_| ReadError::Malformed("header that is not UTF-8"))
203}
204
205pub fn read_response<R: BufRead>(r: &mut R) -> Result<Response, ReadError> {
207 let (status, headers) = loop {
208 let line = read_line(r, false)?;
209 let mut parts = line.splitn(3, ' ');
210 let version = parts.next().unwrap_or_default();
211 if !version.starts_with("HTTP/1.") {
212 return Err(ReadError::Malformed("invalid status line"));
213 }
214 let status: u16 = parts
215 .next()
216 .and_then(|s| s.parse().ok())
217 .ok_or(ReadError::Malformed("invalid status code"))?;
218 let mut headers = Vec::new();
219 loop {
220 let line = read_line(r, true)?;
221 if line.is_empty() {
222 break;
223 }
224 let (name, value) = line
225 .split_once(':')
226 .ok_or(ReadError::Malformed("header without ':'"))?;
227 headers.push((name.trim().to_owned(), value.trim().to_owned()));
228 }
229 if !(100..200).contains(&status) {
230 break (status, headers);
231 }
232 };
233
234 let header = |name: &str| {
235 headers
236 .iter()
237 .find(|(k, _)| k.eq_ignore_ascii_case(name))
238 .map(|(_, v)| v.as_str())
239 };
240 let chunked =
241 header("transfer-encoding").is_some_and(|v| v.to_ascii_lowercase().contains("chunked"));
242 let length = match header("content-length") {
243 Some(v) => Some(
244 v.parse::<usize>()
245 .map_err(|_| ReadError::Malformed("invalid Content-Length"))?,
246 ),
247 None => None,
248 };
249
250 let body = if matches!(status, 204 | 304) {
251 Vec::new()
252 } else if chunked {
253 read_chunked(r)?
254 } else if let Some(n) = length {
255 if n > MAX_BODY {
256 return Err(ReadError::Malformed("body too large"));
257 }
258 let mut body = vec![0; n];
259 r.read_exact(&mut body).map_err(|e| io_err(e, true))?;
260 body
261 } else {
262 let mut body = Vec::new();
265 match r.take(MAX_BODY as u64 + 1).read_to_end(&mut body) {
266 Ok(_) => {}
267 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {}
268 Err(e) => return Err(io_err(e, true)),
269 }
270 if body.len() > MAX_BODY {
271 return Err(ReadError::Malformed("body too large"));
272 }
273 body
274 };
275 Ok(Response {
276 status,
277 headers,
278 body,
279 })
280}
281
282fn read_chunked<R: BufRead>(r: &mut R) -> Result<Vec<u8>, ReadError> {
283 let mut body = Vec::new();
284 loop {
285 let line = read_line(r, true)?;
286 let size_text = line.split(';').next().unwrap_or_default().trim();
287 let size = usize::from_str_radix(size_text, 16)
288 .map_err(|_| ReadError::Malformed("invalid chunk size"))?;
289 if size == 0 {
290 while !read_line(r, true)?.is_empty() {}
292 return Ok(body);
293 }
294 if body.len() + size > MAX_BODY {
295 return Err(ReadError::Malformed("body too large"));
296 }
297 let start = body.len();
298 body.resize(start + size, 0);
299 r.read_exact(&mut body[start..])
300 .map_err(|e| io_err(e, true))?;
301 if !read_line(r, true)?.is_empty() {
302 return Err(ReadError::Malformed("chunk without a CRLF at the end"));
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests;