1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
8
9pub const MAX_HEADERS: usize = 64;
11
12const HOP_BY_HOP: &[&str] = &[
16 "connection",
17 "keep-alive",
18 "proxy-connection",
19 "proxy-authenticate",
20 "proxy-authorization",
21 "te",
22 "trailer",
23 "upgrade",
24];
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Target {
29 Connect {
31 host: String,
33 port: u16,
35 },
36 Http {
38 host: String,
40 port: u16,
42 head: Vec<u8>,
45 },
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Request {
51 pub method: String,
53 pub target: Target,
55 pub head_len: usize,
57}
58
59impl Request {
60 #[must_use]
62 pub fn host(&self) -> &str {
63 match &self.target {
64 Target::Connect { host, .. } | Target::Http { host, .. } => host,
65 }
66 }
67
68 #[must_use]
70 pub fn port(&self) -> u16 {
71 match &self.target {
72 Target::Connect { port, .. } | Target::Http { port, .. } => *port,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79#[non_exhaustive]
80pub enum RequestError {
81 #[error("malformed request: {0}")]
83 Malformed(String),
84 #[error("{0}")]
86 Unsupported(&'static str),
87}
88
89pub fn parse(buf: &[u8]) -> Result<Option<Request>, RequestError> {
92 let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
93 let mut req = httparse::Request::new(&mut headers);
94 let head_len = match req.parse(buf) {
95 Ok(httparse::Status::Complete(n)) => n,
96 Ok(httparse::Status::Partial) => return Ok(None),
97 Err(e) => return Err(RequestError::Malformed(e.to_string())),
98 };
99 let (Some(method), Some(raw_target), Some(version)) = (req.method, req.path, req.version)
100 else {
101 return Err(RequestError::Malformed(
102 "incomplete request line".to_owned(),
103 ));
104 };
105 let target = if method == "CONNECT" {
106 let (host, port) = authority(raw_target, None)?;
107 Target::Connect { host, port }
108 } else {
109 http_target(method, raw_target, version, req.headers)?
110 };
111 Ok(Some(Request {
112 method: method.to_owned(),
113 target,
114 head_len,
115 }))
116}
117
118fn http_target(
119 method: &str,
120 raw_target: &str,
121 version: u8,
122 headers: &[httparse::Header<'_>],
123) -> Result<Target, RequestError> {
124 let Some((scheme, rest)) = raw_target.split_once("://") else {
125 return Err(RequestError::Unsupported(
126 "only absolute-form requests (http://host/path) are proxied",
127 ));
128 };
129 if !scheme.eq_ignore_ascii_case("http") {
130 return Err(RequestError::Unsupported(
131 "only http:// is forwarded; use CONNECT for https",
132 ));
133 }
134 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
135 let (raw_authority, path) = rest.split_at(end);
136 let path = path.split('#').next().unwrap_or_default();
137 let (host, port) = authority(raw_authority, Some(80))?;
138 check_framing(version, headers)?;
139
140 let mut connection_tokens: Vec<String> = Vec::new();
141 for h in headers
142 .iter()
143 .filter(|h| h.name.eq_ignore_ascii_case("connection"))
144 {
145 let value = String::from_utf8_lossy(h.value);
146 connection_tokens.extend(value.split(',').map(|t| t.trim().to_ascii_lowercase()));
147 }
148 let slash = if path.starts_with('/') { "" } else { "/" };
149 let mut head = format!("{method} {slash}{path} HTTP/1.{version}\r\n").into_bytes();
150 let mut has_host = false;
151 for h in headers {
152 let name = h.name.to_ascii_lowercase();
153 if HOP_BY_HOP.contains(&name.as_str()) || connection_tokens.contains(&name) {
154 continue;
155 }
156 if name == "host" {
157 if has_host || host_header(h.value) != Some((host.clone(), port)) {
158 return Err(RequestError::Unsupported(
159 "Host header does not match the request target",
160 ));
161 }
162 has_host = true;
163 }
164 head.extend_from_slice(h.name.as_bytes());
165 head.extend_from_slice(b": ");
166 head.extend_from_slice(h.value);
167 head.extend_from_slice(b"\r\n");
168 }
169 if !has_host {
170 head.extend_from_slice(format!("Host: {raw_authority}\r\n").as_bytes());
171 }
172 head.extend_from_slice(b"Connection: close\r\n\r\n");
173 Ok(Target::Http { host, port, head })
174}
175
176fn check_framing(version: u8, headers: &[httparse::Header<'_>]) -> Result<(), RequestError> {
183 const AMBIGUOUS: RequestError = RequestError::Unsupported(
184 "ambiguous body framing (Content-Length with Transfer-Encoding, repeated, \
185 list-form length, or a transfer coding other than chunked)",
186 );
187 let mut lengths: Vec<&[u8]> = Vec::new();
188 let mut encodings: Vec<&[u8]> = Vec::new();
189 for h in headers {
190 if h.name.eq_ignore_ascii_case("content-length") {
191 lengths.push(h.value);
192 } else if h.name.eq_ignore_ascii_case("transfer-encoding") {
193 encodings.push(h.value);
194 }
195 }
196 if version == 0 && !encodings.is_empty() {
197 return Err(RequestError::Malformed(
198 "Transfer-Encoding in an HTTP/1.0 request".to_owned(),
199 ));
200 }
201 if lengths.len() > 1 || (!lengths.is_empty() && !encodings.is_empty()) {
202 return Err(AMBIGUOUS);
203 }
204 if encodings.len() > 1 {
205 return Err(AMBIGUOUS);
206 }
207 if let Some(bytes) = encodings.first() {
208 let value = std::str::from_utf8(bytes).map_err(|_| AMBIGUOUS)?;
209 let tokens: Vec<String> = value
210 .split(',')
211 .map(|t| t.trim().to_ascii_lowercase())
212 .collect();
213 if tokens.as_slice() != ["chunked"] {
214 return Err(AMBIGUOUS);
215 }
216 }
217 if let Some(bytes) = lengths.first() {
218 let value = std::str::from_utf8(bytes).map_err(|_| AMBIGUOUS)?;
219 let trimmed = value.trim();
220 let digits_only = !trimmed.is_empty() && trimmed.bytes().all(|b| b.is_ascii_digit());
225 if !digits_only || trimmed.parse::<u64>().is_err() {
226 return Err(AMBIGUOUS);
227 }
228 }
229 Ok(())
230}
231
232fn host_header(value: &[u8]) -> Option<(String, u16)> {
233 authority(std::str::from_utf8(value).ok()?.trim(), Some(80)).ok()
234}
235
236fn authority(raw: &str, default_port: Option<u16>) -> Result<(String, u16), RequestError> {
239 const BAD: RequestError = RequestError::Unsupported("invalid host or port in request target");
240 if raw.contains('@') {
241 return Err(RequestError::Unsupported(
242 "credentials in the request target are refused",
243 ));
244 }
245 let (host, port) = if let Some(rest) = raw.strip_prefix('[') {
246 let (v6, after) = rest.split_once(']').ok_or(BAD)?;
247 let ip: Ipv6Addr = v6.parse().map_err(|_| BAD)?;
248 let port = match after {
249 "" => None,
250 p => Some(p.strip_prefix(':').ok_or(BAD)?),
251 };
252 (ip.to_string(), port)
253 } else {
254 let (host, port) = match raw.split_once(':') {
255 Some((h, p)) => (h, Some(p)),
256 None => (raw, None),
257 };
258 (normalise_name(host).ok_or(BAD)?, port)
259 };
260 let port = match (port, default_port) {
261 (Some(p), _) => p.parse::<u16>().ok().filter(|p| *p != 0).ok_or(BAD)?,
262 (None, Some(d)) => d,
263 (None, None) => return Err(BAD),
264 };
265 Ok((host, port))
266}
267
268pub(crate) fn normalise_name(raw: &str) -> Option<String> {
270 let name = raw.strip_suffix('.').unwrap_or(raw).to_ascii_lowercase();
271 if let Ok(ip) = name.parse::<Ipv4Addr>() {
272 return Some(IpAddr::V4(ip).to_string());
273 }
274 let valid = !name.is_empty()
275 && name.len() <= 253
276 && name.split('.').all(|label| {
277 !label.is_empty()
278 && label.len() <= 63
279 && label
280 .bytes()
281 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
282 });
283 let last = name.rsplit('.').next().unwrap_or_default();
288 let numeric = last.bytes().all(|b| b.is_ascii_digit()) || last.starts_with("0x");
289 (valid && !numeric).then_some(name)
290}
291
292#[cfg(test)]
293mod tests;