use h2session::TimestampNs;
pub use h2session::{HttpRequest, HttpResponse};
use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
pub fn is_http1_request(data: &[u8]) -> bool {
data.starts_with(b"GET ")
|| data.starts_with(b"POST ")
|| data.starts_with(b"PUT ")
|| data.starts_with(b"DELETE ")
|| data.starts_with(b"HEAD ")
|| data.starts_with(b"OPTIONS ")
|| data.starts_with(b"PATCH ")
|| data.starts_with(b"CONNECT ")
}
pub fn is_http1_response(data: &[u8]) -> bool {
data.starts_with(b"HTTP/1.0") || data.starts_with(b"HTTP/1.1")
}
pub fn try_parse_http1_request(data: &[u8], timestamp_ns: TimestampNs) -> Option<HttpRequest> {
try_parse_http1_request_sized(data, timestamp_ns).map(|(req, _)| req)
}
pub fn try_parse_http1_request_sized(
data: &[u8],
timestamp_ns: TimestampNs,
) -> Option<(HttpRequest, usize)> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut req = httparse::Request::new(&mut headers);
let body_offset = match req.parse(data) {
Ok(httparse::Status::Complete(len)) => len,
_ => return None, };
let body_data = &data[body_offset..];
let (body, body_len) = match determine_body(req.headers, body_data, None) {
BodyResult::Complete { body, consumed } => (body, consumed),
BodyResult::Incomplete => return None,
};
let method = Method::from_bytes(req.method?.as_bytes()).ok()?;
let uri: Uri = req.path?.parse().ok()?;
let mut header_map = HeaderMap::new();
for h in req.headers.iter() {
let parsed = (
HeaderName::from_bytes(h.name.as_bytes()),
HeaderValue::from_bytes(h.value),
);
if let (Ok(name), Ok(value)) = parsed {
header_map.append(name, value);
}
}
let consumed = body_offset + body_len;
Some((
HttpRequest {
method,
uri,
headers: header_map,
body,
timestamp_ns,
version: Some(req.version?),
},
consumed,
))
}
pub fn try_parse_http1_response(data: &[u8], timestamp_ns: TimestampNs) -> Option<HttpResponse> {
try_parse_http1_response_sized(data, timestamp_ns).map(|(resp, _)| resp)
}
pub fn try_parse_http1_response_sized(
data: &[u8],
timestamp_ns: TimestampNs,
) -> Option<(HttpResponse, usize)> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut res = httparse::Response::new(&mut headers);
let body_offset = match res.parse(data) {
Ok(httparse::Status::Complete(len)) => len,
_ => return None, };
let body_data = &data[body_offset..];
let (body, body_len) = match determine_body(res.headers, body_data, res.code) {
BodyResult::Complete { body, consumed } => (body, consumed),
BodyResult::Incomplete => return None,
};
let status = StatusCode::from_u16(res.code?).ok()?;
let mut header_map = HeaderMap::new();
for h in res.headers.iter() {
let parsed = (
HeaderName::from_bytes(h.name.as_bytes()),
HeaderValue::from_bytes(h.value),
);
if let (Ok(name), Ok(value)) = parsed {
header_map.append(name, value);
}
}
let consumed = body_offset + body_len;
Some((
HttpResponse {
status,
headers: header_map,
body,
timestamp_ns,
version: Some(res.version?),
reason: res.reason.map(String::from),
},
consumed,
))
}
pub fn try_finalize_http1_response(data: &[u8], timestamp_ns: TimestampNs) -> Option<HttpResponse> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut res = httparse::Response::new(&mut headers);
let body_offset = match res.parse(data) {
Ok(httparse::Status::Complete(len)) => len,
_ => return None,
};
let body = data[body_offset..].to_vec();
let status = StatusCode::from_u16(res.code?).ok()?;
let mut header_map = HeaderMap::new();
for h in res.headers.iter() {
let parsed = (
HeaderName::from_bytes(h.name.as_bytes()),
HeaderValue::from_bytes(h.value),
);
if let (Ok(name), Ok(value)) = parsed {
header_map.append(name, value);
}
}
Some(HttpResponse {
status,
headers: header_map,
body,
timestamp_ns,
version: Some(res.version?),
reason: res.reason.map(String::from),
})
}
enum BodyResult {
Complete { body: Vec<u8>, consumed: usize },
Incomplete,
}
fn determine_body(
headers: &[httparse::Header<'_>],
body_data: &[u8],
response_status: Option<u16>,
) -> BodyResult {
for h in headers.iter() {
if h.name.eq_ignore_ascii_case("Content-Length") {
if let Ok(len_str) = std::str::from_utf8(h.value)
&& let Ok(content_length) = len_str.trim().parse::<usize>()
{
if body_data.len() >= content_length {
return BodyResult::Complete {
body: body_data[..content_length].to_vec(),
consumed: content_length,
};
}
return BodyResult::Incomplete;
}
return BodyResult::Incomplete; }
}
for h in headers.iter() {
if h.name.eq_ignore_ascii_case("Transfer-Encoding")
&& let Ok(value) = std::str::from_utf8(h.value)
&& value.to_ascii_lowercase().contains("chunked")
{
return decode_chunked_body(body_data);
}
}
match response_status {
None => BodyResult::Complete {
body: Vec::new(),
consumed: 0,
},
Some(code) if (100..200).contains(&code) || code == 204 || code == 304 => {
BodyResult::Complete {
body: Vec::new(),
consumed: 0,
}
},
Some(_) => BodyResult::Incomplete,
}
}
fn decode_chunked_body(data: &[u8]) -> BodyResult {
let mut decoded = Vec::new();
let mut pos = 0;
loop {
let line_end = match find_crlf(data, pos) {
Some(idx) => idx,
None => return BodyResult::Incomplete,
};
let size_bytes = &data[pos..line_end];
let size_part = match size_bytes.iter().position(|&b| b == b';') {
Some(semi_pos) => &size_bytes[..semi_pos],
None => size_bytes,
};
let Ok(size_str) = std::str::from_utf8(size_part) else {
return BodyResult::Incomplete; };
let Ok(chunk_size) = usize::from_str_radix(size_str.trim(), 16) else {
return BodyResult::Incomplete; };
pos = line_end + 2;
if chunk_size == 0 {
if pos + 2 > data.len() {
return BodyResult::Incomplete;
}
if data[pos..pos + 2] != *b"\r\n" {
match find_crlf_crlf(data, pos) {
Some(trailer_start) => {
return BodyResult::Complete {
body: decoded,
consumed: trailer_start + 4,
};
},
None => return BodyResult::Incomplete,
}
}
return BodyResult::Complete {
body: decoded,
consumed: pos + 2,
};
}
if pos + chunk_size > data.len() {
return BodyResult::Incomplete;
}
decoded.extend_from_slice(&data[pos..pos + chunk_size]);
pos += chunk_size;
if pos + 2 > data.len() {
return BodyResult::Incomplete;
}
if data[pos..pos + 2] != *b"\r\n" {
return BodyResult::Incomplete; }
pos += 2;
}
}
fn find_crlf(data: &[u8], from: usize) -> Option<usize> {
if from >= data.len() {
return None;
}
data[from..]
.windows(2)
.position(|w| w == b"\r\n")
.map(|p| from + p)
}
fn find_crlf_crlf(data: &[u8], from: usize) -> Option<usize> {
if from >= data.len() {
return None;
}
data[from..]
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|p| from + p)
}
#[cfg(test)]
mod tests;