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