use zeroize::Zeroizing;
use super::{HOP_BY_HOP, MAX_HEADERS, RequestError, authority, check_framing};
pub(crate) struct Inner {
pub(crate) method: String,
pub(crate) head: Zeroizing<Vec<u8>>,
pub(crate) head_len: usize,
pub(crate) expects_continue: bool,
pub(crate) framing: crate::body::Framing,
}
pub(crate) fn parse(buf: &[u8], host: &str, port: u16) -> Result<Option<Inner>, RequestError> {
let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
let mut req = httparse::Request::new(&mut headers);
let head_len = match req.parse(buf) {
Ok(httparse::Status::Complete(n)) => n,
Ok(httparse::Status::Partial) => return Ok(None),
Err(e) => return Err(RequestError::Malformed(e.to_string())),
};
let (Some(method), Some(target), Some(version)) = (req.method, req.path, req.version) else {
return Err(RequestError::Malformed(
"incomplete request line".to_owned(),
));
};
if method == "CONNECT" || !target.starts_with('/') {
return Err(RequestError::Unsupported(
"inside a tunnel only a path is requested (`GET /path`), not another host",
));
}
check_framing(version, req.headers)?;
let mut connection_tokens: Vec<String> = Vec::new();
for h in req
.headers
.iter()
.filter(|h| h.name.eq_ignore_ascii_case("connection"))
{
let value = String::from_utf8_lossy(h.value);
connection_tokens.extend(value.split(',').map(|t| t.trim().to_ascii_lowercase()));
}
let mut head = Zeroizing::new(format!("{method} {target} HTTP/1.{version}\r\n").into_bytes());
let mut hosts = 0;
let mut expects_continue = false;
for h in req.headers.iter() {
let name = h.name.to_ascii_lowercase();
if HOP_BY_HOP.contains(&name.as_str()) || connection_tokens.contains(&name) {
continue;
}
match name.as_str() {
"host" => {
let named = std::str::from_utf8(h.value)
.ok()
.and_then(|v| authority(v.trim(), Some(443)).ok());
if named != Some((host.to_owned(), port)) {
return Err(RequestError::Unsupported(
"the Host header does not name the host the tunnel was opened to",
));
}
hosts += 1;
}
"expect" => {
if !h.value.eq_ignore_ascii_case(b"100-continue") {
return Err(RequestError::Unsupported(
"an `Expect` other than `100-continue`",
));
}
expects_continue = true;
continue;
}
"accept-encoding" => continue,
_ => {}
}
head.extend_from_slice(h.name.as_bytes());
head.extend_from_slice(b": ");
head.extend_from_slice(h.value);
head.extend_from_slice(b"\r\n");
}
if hosts != 1 {
return Err(RequestError::Unsupported(
"a request inside a tunnel needs exactly one Host header",
));
}
head.extend_from_slice(b"Accept-Encoding: identity\r\nConnection: close\r\n\r\n");
let framing = crate::body::framing(&head);
Ok(Some(Inner {
method: method.to_owned(),
head,
head_len,
expects_continue,
framing,
}))
}