use bytes::Bytes;
use super::{poison::HttpPoison, proxy::HttpEvent, types::HttpVersion};
use crate::FlowSide;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
#[non_exhaustive]
pub enum HttpAccessOutcome {
Completed,
NoResponse,
Refused { reason: HttpPoison },
Switched,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct HttpAccessRecord {
pub method: Bytes,
pub path: Bytes,
pub authority: Option<String>,
pub version: HttpVersion,
pub status: Option<u16>,
pub request_body_bytes: u64,
pub response_body_bytes: u64,
pub outcome: HttpAccessOutcome,
}
impl HttpAccessRecord {
pub fn method_str(&self) -> Option<&str> {
std::str::from_utf8(&self.method).ok()
}
pub fn path_str(&self) -> Option<&str> {
std::str::from_utf8(&self.path).ok()
}
pub fn status_class(&self) -> Option<u8> {
let cls = self.status? / 100;
(1..=5).contains(&cls).then_some(cls as u8)
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpAccessLog {
pending: std::collections::VecDeque<Pending>,
response_bytes: u64,
status: Option<u16>,
switched: bool,
}
#[derive(Debug, Clone)]
struct Pending {
method: Bytes,
path: Bytes,
authority: Option<String>,
version: HttpVersion,
body_bytes: u64,
complete: bool,
}
impl HttpAccessLog {
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, event: &HttpEvent, out: &mut Vec<HttpAccessRecord>) {
match event {
HttpEvent::RequestHead(head) => {
self.pending.push_back(Pending {
method: head.method.clone(),
path: head.path.clone(),
authority: head.authority().ok().map(|a| a.host),
version: head.version,
body_bytes: 0,
complete: false,
});
}
HttpEvent::ResponseHead(head) => {
if !head.interim {
self.response_bytes = 0;
if let Some(p) = self.pending.front_mut() {
p.complete = true;
}
self.status = Some(head.status);
}
}
HttpEvent::Body { dir, raw, .. } => match dir {
FlowSide::Initiator => {
if let Some(p) = self.pending.iter_mut().find(|p| !p.complete) {
p.body_bytes += raw.len() as u64;
}
}
FlowSide::Responder => self.response_bytes += raw.len() as u64,
},
HttpEvent::Trailers { dir, raw, .. } => match dir {
FlowSide::Initiator => {
if let Some(p) = self.pending.iter_mut().find(|p| !p.complete) {
p.body_bytes += raw.len() as u64;
}
}
FlowSide::Responder => self.response_bytes += raw.len() as u64,
},
HttpEvent::End { dir } => {
match dir {
FlowSide::Initiator => {
if let Some(p) = self.pending.iter_mut().find(|p| !p.complete) {
p.complete = true;
}
}
FlowSide::Responder => {
if let Some(p) = self.pending.pop_front() {
out.push(self.record(p, HttpAccessOutcome::Completed));
}
self.response_bytes = 0;
self.status = None;
}
}
}
HttpEvent::SwitchProtocols { .. } => {
self.switched = true;
}
}
}
pub fn finish(&mut self, poison: Option<HttpPoison>, out: &mut Vec<HttpAccessRecord>) {
let outcome = match (poison, self.switched) {
(Some(reason), _) => HttpAccessOutcome::Refused { reason },
(None, true) => HttpAccessOutcome::Switched,
(None, false) => HttpAccessOutcome::NoResponse,
};
let had_pending = !self.pending.is_empty();
while let Some(p) = self.pending.pop_front() {
out.push(self.record(p, outcome.clone()));
}
if !had_pending && poison.is_some() {
out.push(self.record(
Pending {
method: Bytes::new(),
path: Bytes::new(),
authority: None,
version: HttpVersion::Http1_1,
body_bytes: 0,
complete: true,
},
outcome,
));
}
self.response_bytes = 0;
self.status = None;
self.switched = false;
}
fn record(&self, p: Pending, outcome: HttpAccessOutcome) -> HttpAccessRecord {
let completed = matches!(outcome, HttpAccessOutcome::Completed);
HttpAccessRecord {
method: p.method,
path: p.path,
authority: p.authority,
version: p.version,
status: if completed { self.status } else { None },
request_body_bytes: p.body_bytes,
response_body_bytes: if completed { self.response_bytes } else { 0 },
outcome,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::HttpProxyParser;
fn run(client: &[u8], server: &[u8]) -> (Vec<HttpAccessRecord>, HttpProxyParser) {
let mut proxy = HttpProxyParser::new();
let mut log = HttpAccessLog::new();
let mut out = Vec::new();
proxy.push(FlowSide::Initiator, &Bytes::copy_from_slice(client));
proxy.push(FlowSide::Responder, &Bytes::copy_from_slice(server));
while let Some(ev) = proxy.next_event() {
log.observe(&ev, &mut out);
}
log.finish(proxy.poison(), &mut out);
(out, proxy)
}
#[test]
fn records_a_completed_exchange() {
let (recs, _) = run(
b"POST /orders HTTP/1.1\r\nHost: api.example\r\nContent-Length: 5\r\n\r\nhello",
b"HTTP/1.1 201 Created\r\nContent-Length: 2\r\n\r\nok",
);
assert_eq!(recs.len(), 1);
let r = &recs[0];
assert_eq!(r.method_str(), Some("POST"));
assert_eq!(r.path_str(), Some("/orders"));
assert_eq!(r.authority.as_deref(), Some("api.example"));
assert_eq!(r.status, Some(201));
assert_eq!(r.request_body_bytes, 5);
assert_eq!(r.response_body_bytes, 2);
assert_eq!(r.outcome, HttpAccessOutcome::Completed);
assert_eq!(r.status_class(), Some(2));
}
#[test]
fn counts_chunked_wire_bytes_without_holding_them() {
let (recs, proxy) = run(
b"POST /u HTTP/1.1\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n",
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].request_body_bytes, 15);
assert_eq!(proxy.buffered(FlowSide::Initiator), 0);
}
#[test]
fn pipelined_exchanges_pair_in_order() {
let (recs, _) = run(
b"GET /a HTTP/1.1\r\nHost: h\r\n\r\nGET /b HTTP/1.1\r\nHost: h\r\n\r\n",
b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nA\
HTTP/1.1 404 Not Found\r\nContent-Length: 1\r\n\r\nB",
);
let seen: Vec<(&str, Option<u16>)> = recs
.iter()
.map(|r| (r.path_str().unwrap(), r.status))
.collect();
assert_eq!(seen, vec![("/a", Some(200)), ("/b", Some(404))]);
}
#[test]
fn interim_responses_do_not_end_the_exchange() {
let (recs, _) = run(
b"POST /u HTTP/1.1\r\nHost: h\r\nExpect: 100-continue\r\nContent-Length: 2\r\n\r\nhi",
b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 204 No Content\r\n\r\n",
);
assert_eq!(recs.len(), 1, "the interim is not its own record");
assert_eq!(recs[0].status, Some(204));
}
#[test]
fn an_unanswered_request_is_reported() {
let (recs, _) = run(b"GET /a HTTP/1.1\r\nHost: h\r\n\r\n", b"");
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].outcome, HttpAccessOutcome::NoResponse);
assert_eq!(recs[0].status, None);
}
#[test]
fn a_refused_connection_says_why() {
let (recs, _) = run(
b"POST /a HTTP/1.1\r\nHost: h\r\nContent-Length: 6\r\n\
Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
b"",
);
assert_eq!(recs.len(), 1, "a refusal must produce a record");
assert_eq!(
recs[0].outcome,
HttpAccessOutcome::Refused {
reason: HttpPoison::ContentLengthWithTransferEncoding
}
);
assert!(recs[0].method.is_empty());
assert!(recs[0].path.is_empty());
}
#[test]
fn a_refusal_after_a_head_keeps_the_request_details() {
let (recs, _) = run(
b"GET /wanted HTTP/1.1\r\nHost: h\r\n\r\n\
POST /a HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\n",
b"",
);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].path_str(), Some("/wanted"));
assert!(matches!(recs[0].outcome, HttpAccessOutcome::Refused { .. }));
}
#[test]
fn a_tunnelled_exchange_is_marked_switched() {
let (recs, _) = run(
b"CONNECT h:443 HTTP/1.1\r\nHost: h:443\r\n\r\n",
b"HTTP/1.1 200 Connection Established\r\n\r\n",
);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].outcome, HttpAccessOutcome::Switched);
assert_eq!(recs[0].method_str(), Some("CONNECT"));
}
}