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