use bytes::Bytes;
use super::{
engine::{Dir, Engine, EngineEvent, EngineLimits},
poison::HttpPoison,
types::{RequestHead, ResponseHead, SmugglingPolicy, SwitchKind},
};
use crate::FlowSide;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum HttpEvent {
RequestHead(RequestHead),
ResponseHead(ResponseHead),
Body {
dir: FlowSide,
data: Bytes,
raw: Bytes,
},
Trailers {
dir: FlowSide,
trailers: Vec<(Bytes, Bytes)>,
raw: Bytes,
},
End { dir: FlowSide },
SwitchProtocols { kind: SwitchKind },
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct HttpProxyConfig {
pub max_head_bytes: usize,
pub max_headers: usize,
pub max_chunk_line_bytes: usize,
pub max_trailer_bytes: usize,
pub max_pipelined: usize,
pub smuggling: SmugglingPolicy,
pub max_buffered_bytes: usize,
}
impl HttpProxyConfig {
#[must_use]
pub fn with_max_head_bytes(mut self, n: usize) -> Self {
self.max_head_bytes = n;
self
}
#[must_use]
pub fn with_max_headers(mut self, n: usize) -> Self {
self.max_headers = n;
self
}
#[must_use]
pub fn with_max_chunk_line_bytes(mut self, n: usize) -> Self {
self.max_chunk_line_bytes = n;
self
}
#[must_use]
pub fn with_max_trailer_bytes(mut self, n: usize) -> Self {
self.max_trailer_bytes = n;
self
}
#[must_use]
pub fn with_max_pipelined(mut self, n: usize) -> Self {
self.max_pipelined = n;
self
}
#[must_use]
pub fn with_smuggling_policy(mut self, policy: SmugglingPolicy) -> Self {
self.smuggling = policy;
self
}
#[must_use]
pub fn with_max_buffered_bytes(mut self, n: usize) -> Self {
self.max_buffered_bytes = n;
self
}
}
impl Default for HttpProxyConfig {
fn default() -> Self {
Self {
max_head_bytes: 64 * 1024,
max_headers: 128,
max_chunk_line_bytes: 256,
max_trailer_bytes: 8 * 1024,
max_pipelined: 64,
smuggling: SmugglingPolicy::Strict,
max_buffered_bytes: 256 * 1024,
}
}
}
#[derive(Debug, Clone)]
pub struct HttpProxyParser {
engine: Engine,
config: HttpProxyConfig,
}
impl Default for HttpProxyParser {
fn default() -> Self {
Self::new()
}
}
impl HttpProxyParser {
pub fn new() -> Self {
Self::with_config(HttpProxyConfig::default())
}
pub fn with_config(config: HttpProxyConfig) -> Self {
let limits = EngineLimits {
max_head_bytes: config.max_head_bytes,
max_headers: config.max_headers,
max_chunk_line_bytes: config.max_chunk_line_bytes,
max_trailer_bytes: config.max_trailer_bytes,
max_pipelined: config.max_pipelined,
policy: config.smuggling,
};
Self {
engine: Engine::new(limits),
config,
}
}
pub fn push(&mut self, dir: FlowSide, data: &Bytes) -> usize {
if self.is_poisoned() || self.engine.is_tunnelled() {
return 0;
}
let dir = to_dir(dir);
let room = self
.config
.max_buffered_bytes
.saturating_sub(self.engine.buffered(dir));
let take = room.min(data.len());
if take > 0 {
self.engine.push(dir, &data[..take]);
}
take
}
pub fn fin(&mut self, dir: FlowSide) {
self.engine.fin(to_dir(dir));
}
pub fn next_event(&mut self) -> Option<HttpEvent> {
for dir in [Dir::Request, Dir::Response] {
match self.engine.poll(dir) {
Ok(Some(ev)) => {
if matches!(ev, EngineEvent::Head(_)) {
crate::obs::record_http_message(dir == Dir::Request);
}
return Some(convert(dir, ev));
}
Ok(None) => continue,
Err(_) => {
if let Some(reason) = self.engine.poison(dir) {
crate::obs::record_http_poisoned(reason);
}
continue;
}
}
}
None
}
pub fn is_poisoned(&self) -> bool {
self.engine.is_desynced(Dir::Request) || self.engine.is_desynced(Dir::Response)
}
pub fn poison(&self) -> Option<HttpPoison> {
self.engine
.poison(Dir::Request)
.or_else(|| self.engine.poison(Dir::Response))
}
pub fn poison_reason(&self) -> Option<&'static str> {
self.poison().map(HttpPoison::as_str)
}
pub fn is_tunnelled(&self) -> bool {
self.engine.is_tunnelled()
}
pub fn take_tunnel_residue(&mut self, dir: FlowSide) -> Bytes {
self.engine.take_residue(to_dir(dir))
}
pub fn is_done(&self) -> bool {
if self.engine.is_tunnelled() {
return true;
}
self.engine.is_closed(Dir::Request) && self.engine.is_closed(Dir::Response)
}
pub fn buffered(&self, dir: FlowSide) -> usize {
self.engine.buffered(to_dir(dir))
}
}
#[derive(Debug, Clone)]
pub struct HttpProxySession {
inner: HttpProxyParser,
}
impl Default for HttpProxySession {
fn default() -> Self {
Self::new()
}
}
impl HttpProxySession {
pub fn new() -> Self {
Self {
inner: HttpProxyParser::new(),
}
}
pub fn with_config(config: HttpProxyConfig) -> Self {
Self {
inner: HttpProxyParser::with_config(config),
}
}
pub fn parser(&self) -> &HttpProxyParser {
&self.inner
}
fn feed(&mut self, dir: FlowSide, bytes: &[u8], out: &mut Vec<HttpEvent>) {
if bytes.is_empty() {
return;
}
let mut data = Bytes::copy_from_slice(bytes);
loop {
let accepted = self.inner.push(dir, &data);
while let Some(ev) = self.inner.next_event() {
out.push(ev);
}
if accepted == 0 || accepted == data.len() {
break;
}
data = data.slice(accepted..);
}
}
}
impl crate::SessionParser for HttpProxySession {
type Message = HttpEvent;
fn feed_initiator(&mut self, bytes: &[u8], _ts: crate::Timestamp, out: &mut Vec<HttpEvent>) {
self.feed(FlowSide::Initiator, bytes, out);
}
fn feed_responder(&mut self, bytes: &[u8], _ts: crate::Timestamp, out: &mut Vec<HttpEvent>) {
self.feed(FlowSide::Responder, bytes, out);
}
fn fin_initiator(&mut self, out: &mut Vec<HttpEvent>) {
self.inner.fin(FlowSide::Initiator);
while let Some(ev) = self.inner.next_event() {
out.push(ev);
}
}
fn fin_responder(&mut self, out: &mut Vec<HttpEvent>) {
self.inner.fin(FlowSide::Responder);
while let Some(ev) = self.inner.next_event() {
out.push(ev);
}
}
fn rst_initiator(&mut self) {
self.inner = HttpProxyParser::with_config(self.inner.config.clone());
}
fn rst_responder(&mut self) {
self.inner = HttpProxyParser::with_config(self.inner.config.clone());
}
fn parser_kind(&self) -> crate::ParserKind {
crate::ParserKind::Http1
}
fn is_poisoned(&self) -> bool {
self.inner.is_poisoned()
}
fn poison_reason(&self) -> Option<&str> {
self.inner.poison_reason()
}
fn is_done(&self) -> bool {
self.inner.is_done()
}
}
fn to_dir(side: FlowSide) -> Dir {
match side {
FlowSide::Initiator => Dir::Request,
FlowSide::Responder => Dir::Response,
}
}
fn to_side(dir: Dir) -> FlowSide {
match dir {
Dir::Request => FlowSide::Initiator,
Dir::Response => FlowSide::Responder,
}
}
fn convert(dir: Dir, ev: EngineEvent) -> HttpEvent {
let side = to_side(dir);
match ev {
EngineEvent::Head(h) => match dir {
Dir::Request => HttpEvent::RequestHead(RequestHead {
method: h.method,
path: h.path,
version: h.version,
headers: h.headers,
framing: h.framing,
applied: h.applied,
raw: h.raw,
}),
Dir::Response => HttpEvent::ResponseHead(ResponseHead {
status: h.status,
reason: h.reason,
version: h.version,
headers: h.headers,
framing: h.framing,
interim: h.interim,
applied: h.applied,
raw: h.raw,
}),
},
EngineEvent::Body { decoded, raw } => HttpEvent::Body {
dir: side,
data: decoded,
raw,
},
EngineEvent::Trailers { fields, raw } => HttpEvent::Trailers {
dir: side,
trailers: fields,
raw,
},
EngineEvent::End => HttpEvent::End { dir: side },
EngineEvent::Switch(kind) => HttpEvent::SwitchProtocols { kind },
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::BodyFraming;
fn push_all(p: &mut HttpProxyParser, dir: FlowSide, bytes: &[u8]) {
let data = Bytes::copy_from_slice(bytes);
let n = p.push(dir, &data);
assert_eq!(n, data.len(), "test inputs fit the default caps");
}
fn drain(p: &mut HttpProxyParser) -> Vec<HttpEvent> {
let mut out = Vec::new();
while let Some(ev) = p.next_event() {
out.push(ev);
}
out
}
fn raw_of(evs: &[HttpEvent]) -> Vec<u8> {
let mut v = Vec::new();
for ev in evs {
match ev {
HttpEvent::RequestHead(h) => v.extend_from_slice(&h.raw),
HttpEvent::ResponseHead(h) => v.extend_from_slice(&h.raw),
HttpEvent::Body { raw, .. } => v.extend_from_slice(raw),
HttpEvent::Trailers { raw, .. } => v.extend_from_slice(raw),
_ => {}
}
}
v
}
fn decoded_of(evs: &[HttpEvent]) -> Vec<u8> {
let mut v = Vec::new();
for ev in evs {
if let HttpEvent::Body { data, .. } = ev {
v.extend_from_slice(data);
}
}
v
}
#[test]
fn head_arrives_before_any_body_byte() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /submit HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 1000\r\n\r\n",
);
let ev = p.next_event().expect("head must be ready");
match ev {
HttpEvent::RequestHead(h) => {
assert_eq!(h.method.as_ref(), b"POST");
assert_eq!(h.host(), Some("api.example.com"));
assert_eq!(h.framing, BodyFraming::ContentLength(1000));
}
other => panic!("expected RequestHead, got {other:?}"),
}
assert!(p.next_event().is_none(), "no body has been fed yet");
}
#[test]
fn body_is_never_accumulated() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /u HTTP/1.1\r\nContent-Length: 100000\r\n\r\n",
);
let _ = drain(&mut p);
let chunk = Bytes::from_static(&[b'x'; 4096]);
let mut sent = 0usize;
while sent < 100_000 {
let slice = chunk.slice(..chunk.len().min(100_000 - sent));
let n = p.push(FlowSide::Initiator, &slice);
sent += n;
let _ = drain(&mut p);
assert!(
p.buffered(FlowSide::Initiator) <= HttpProxyConfig::default().max_buffered_bytes,
"the parser must never accumulate a body"
);
}
assert_eq!(p.buffered(FlowSide::Initiator), 0);
}
#[test]
fn raw_spans_reproduce_the_wire() {
let wire: &[u8] = b"POST /u HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n\
5\r\nhello\r\n6\r\n world\r\n0\r\nX-Sum: 9\r\n\r\n";
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, wire);
let evs = drain(&mut p);
assert_eq!(raw_of(&evs), wire);
assert_eq!(decoded_of(&evs), b"hello world");
let trailers: Vec<_> = evs
.iter()
.filter_map(|e| match e {
HttpEvent::Trailers { trailers, .. } => Some(trailers.clone()),
_ => None,
})
.collect();
assert_eq!(trailers.len(), 1);
assert_eq!(trailers[0][0].0.as_ref(), b"X-Sum");
assert_eq!(trailers[0][0].1.as_ref(), b"9");
assert!(matches!(evs.last(), Some(HttpEvent::End { .. })));
}
#[test]
fn split_feeds_produce_the_same_events() {
let wire: &[u8] = b"POST /u HTTP/1.1\r\nContent-Length: 11\r\n\r\nhello world\
GET /next HTTP/1.1\r\nHost: h\r\n\r\n";
let mut whole = HttpProxyParser::new();
push_all(&mut whole, FlowSide::Initiator, wire);
let a = drain(&mut whole);
let mut drip = HttpProxyParser::new();
let mut b = Vec::new();
for byte in wire {
push_all(&mut drip, FlowSide::Initiator, std::slice::from_ref(byte));
b.extend(drain(&mut drip));
}
assert_eq!(raw_of(&a), raw_of(&b));
assert_eq!(decoded_of(&a), decoded_of(&b));
assert_eq!(raw_of(&b), wire);
}
#[test]
fn pipelined_requests_each_end_cleanly() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /a HTTP/1.1\r\nContent-Length: 3\r\n\r\nAAA\
POST /b HTTP/1.1\r\nContent-Length: 2\r\n\r\nBB\
GET /c HTTP/1.1\r\n\r\n",
);
let evs = drain(&mut p);
let paths: Vec<String> = evs
.iter()
.filter_map(|e| match e {
HttpEvent::RequestHead(h) => Some(h.path_str().unwrap().to_string()),
_ => None,
})
.collect();
assert_eq!(paths, vec!["/a", "/b", "/c"]);
let ends = evs
.iter()
.filter(|e| matches!(e, HttpEvent::End { .. }))
.count();
assert_eq!(ends, 3, "one End per message");
}
#[test]
fn response_framing_uses_the_request_method() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"HEAD /x HTTP/1.1\r\n\r\n");
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n",
);
let evs = drain(&mut p);
match &evs[0] {
HttpEvent::ResponseHead(h) => assert_eq!(h.framing, BodyFraming::None),
other => panic!("expected ResponseHead, got {other:?}"),
}
assert!(matches!(evs[1], HttpEvent::End { .. }));
}
#[test]
fn backpressure_short_count_when_caller_does_not_drain() {
let cfg = HttpProxyConfig {
max_buffered_bytes: 4096,
..HttpProxyConfig::default()
};
let mut p = HttpProxyParser::with_config(cfg);
let big = Bytes::from(vec![b'x'; 8192]);
let n = p.push(FlowSide::Initiator, &big);
assert_eq!(n, 4096);
assert_eq!(p.push(FlowSide::Initiator, &big.slice(n..)), 0);
}
#[test]
fn framing_desync_poisons_with_a_typed_reason() {
let cfg = HttpProxyConfig {
max_head_bytes: 128,
..HttpProxyConfig::default()
};
let mut p = HttpProxyParser::with_config(cfg);
push_all(&mut p, FlowSide::Initiator, &[b'A'; 200]);
let _ = drain(&mut p);
assert!(p.is_poisoned());
assert_eq!(p.poison(), Some(HttpPoison::HeadOverflow));
assert_eq!(p.poison_reason(), Some("head-overflow"));
assert_eq!(
p.push(
FlowSide::Initiator,
&Bytes::from_static(b"GET / HTTP/1.1\r\n\r\n")
),
0
);
}
#[test]
fn bad_chunk_size_is_reported_as_such() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /u HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n",
);
let _ = drain(&mut p);
assert_eq!(p.poison(), Some(HttpPoison::InvalidChunkSize));
}
fn statuses(evs: &[HttpEvent]) -> Vec<(u16, bool)> {
evs.iter()
.filter_map(|e| match e {
HttpEvent::ResponseHead(h) => Some((h.status, h.interim)),
_ => None,
})
.collect()
}
#[test]
fn interim_responses_precede_the_final_one() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /u HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 2\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok",
);
let evs = drain(&mut p);
assert_eq!(statuses(&evs), vec![(100, true), (200, false)]);
assert_eq!(decoded_of(&evs), b"ok");
assert_eq!(
evs.iter()
.filter(|e| matches!(e, HttpEvent::End { .. }))
.count(),
1
);
}
#[test]
fn multiple_interims_then_final() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"GET /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 103 Early Hints\r\nLink: </s.css>\r\n\r\n\
HTTP/1.1 103 Early Hints\r\nLink: </t.css>\r\n\r\n\
HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nx",
);
let evs = drain(&mut p);
assert_eq!(statuses(&evs), vec![(103, true), (103, true), (200, false)]);
}
#[test]
fn response_reader_runs_before_the_request_body_is_sent() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"POST /u HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 1000000\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 100 Continue\r\n\r\n",
);
let evs = drain(&mut p);
assert_eq!(statuses(&evs), vec![(100, true)]);
}
#[test]
fn fin_preserves_the_tunnel() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 200 Connection Established\r\n\r\n",
);
let _ = drain(&mut p);
assert!(p.is_tunnelled());
p.fin(FlowSide::Initiator);
assert!(p.is_tunnelled(), "a tunnel outlives a half-close");
assert!(p.is_done());
let late = Bytes::from_static(b"tunnel bytes");
assert_eq!(
p.push(FlowSide::Responder, &late),
0,
"post-fin tunnel must still refuse bytes, not drop them"
);
}
#[test]
fn tunnel_residue_after_upgrade_is_retrievable() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"GET /chat HTTP/1.1\r\nHost: h\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n\x82\x05FRAME",
);
let evs = drain(&mut p);
assert!(matches!(
evs.last(),
Some(HttpEvent::SwitchProtocols { .. })
));
assert_eq!(
p.take_tunnel_residue(FlowSide::Responder).as_ref(),
b"\x82\x05FRAME",
"the coalesced first frame must survive the switch"
);
assert!(
p.take_tunnel_residue(FlowSide::Responder).is_empty(),
"residue is taken once"
);
assert!(p.take_tunnel_residue(FlowSide::Initiator).is_empty());
}
#[test]
fn tunnel_residue_h2_preface_is_byte_exact() {
let mut p = HttpProxyParser::new();
let wire: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00\x00\x04\x00\x00\x00\x00\x00";
push_all(&mut p, FlowSide::Initiator, wire);
let evs = drain(&mut p);
assert!(matches!(
evs.first(),
Some(HttpEvent::SwitchProtocols {
kind: SwitchKind::Http2PriorKnowledge
})
));
assert_eq!(p.take_tunnel_residue(FlowSide::Initiator).as_ref(), wire);
}
#[test]
fn no_residue_without_a_switch() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"GET / HTTP/1.1\r\nHost: h\r\n\r\n",
);
let _ = drain(&mut p);
assert!(p.take_tunnel_residue(FlowSide::Initiator).is_empty());
assert!(p.take_tunnel_residue(FlowSide::Responder).is_empty());
}
#[test]
fn connect_tunnel_switches_and_stops_parsing() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 200 Connection Established\r\n\r\n",
);
let evs = drain(&mut p);
assert!(matches!(
evs.last(),
Some(HttpEvent::SwitchProtocols {
kind: SwitchKind::ConnectTunnel
})
));
assert!(p.is_tunnelled());
let refused = Bytes::from_static(b"\x16\x03\x01\x02\x00\x01");
assert_eq!(p.push(FlowSide::Initiator, &refused), 0);
assert!(p.next_event().is_none());
assert!(!p.is_poisoned());
}
#[test]
fn failed_connect_is_a_normal_response() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"CONNECT example.com:443 HTTP/1.1\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 403 Forbidden\r\nContent-Length: 2\r\n\r\nno",
);
let evs = drain(&mut p);
assert!(
!evs.iter()
.any(|e| matches!(e, HttpEvent::SwitchProtocols { .. })),
"a rejected CONNECT does not open a tunnel"
);
assert_eq!(decoded_of(&evs), b"no");
}
#[test]
fn websocket_upgrade_switches_with_the_protocol_token() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"GET /chat HTTP/1.1\r\nHost: h\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n",
);
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n",
);
let evs = drain(&mut p);
match evs.last() {
Some(HttpEvent::SwitchProtocols {
kind: SwitchKind::Upgrade { protocol },
}) => assert_eq!(protocol.as_ref(), b"websocket"),
other => panic!("expected an Upgrade switch, got {other:?}"),
}
assert_eq!(statuses(&evs), vec![(101, false)]);
}
#[test]
fn http2_preface_is_recognised_not_mistaken_for_a_request() {
let mut p = HttpProxyParser::new();
push_all(
&mut p,
FlowSide::Initiator,
b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00\x00\x04\x00\x00\x00\x00\x00",
);
let evs = drain(&mut p);
assert!(matches!(
evs.first(),
Some(HttpEvent::SwitchProtocols {
kind: SwitchKind::Http2PriorKnowledge
})
));
assert!(!p.is_poisoned(), "h2 traffic is not a framing failure");
}
#[test]
fn partial_preface_waits_instead_of_guessing() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"PRI * HTTP/2.0\r\n");
assert!(
p.next_event().is_none(),
"a prefix of the preface must not decide either way"
);
push_all(&mut p, FlowSide::Initiator, b"\r\nSM\r\n\r\n");
assert!(matches!(
p.next_event(),
Some(HttpEvent::SwitchProtocols {
kind: SwitchKind::Http2PriorKnowledge
})
));
}
#[test]
fn connection_close_completes_the_direction() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"GET /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nhi",
);
let evs = drain(&mut p);
assert_eq!(decoded_of(&evs), b"hi");
p.fin(FlowSide::Initiator);
assert!(
p.is_done(),
"after a Connection: close response the connection is finished"
);
}
#[test]
fn http_1_0_response_without_keep_alive_completes() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"GET /a HTTP/1.0\r\n\r\n");
let _ = drain(&mut p);
push_all(
&mut p,
FlowSide::Responder,
b"HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nhi",
);
let _ = drain(&mut p);
p.fin(FlowSide::Initiator);
assert!(p.is_done(), "HTTP/1.0 defaults to closing");
}
#[test]
fn adapter_emits_the_same_events_through_the_trait() {
use crate::SessionParser;
let mut s = HttpProxySession::new();
let mut out = Vec::new();
s.feed_initiator(
b"POST /a HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello",
crate::Timestamp::default(),
&mut out,
);
assert!(matches!(out[0], HttpEvent::RequestHead(_)));
assert_eq!(decoded_of(&out), b"hello");
assert!(matches!(out.last(), Some(HttpEvent::End { .. })));
}
#[test]
fn adapter_reports_poison_so_the_driver_ends_the_flow() {
use crate::SessionParser;
let mut s = HttpProxySession::new();
let mut out = Vec::new();
s.feed_initiator(
b"POST /a HTTP/1.1\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
crate::Timestamp::default(),
&mut out,
);
assert!(s.is_poisoned());
assert_eq!(
s.poison_reason(),
Some("content-length-with-transfer-encoding")
);
}
#[test]
fn adapter_survives_a_feed_larger_than_the_buffer_cap() {
use crate::SessionParser;
let cfg = HttpProxyConfig {
max_buffered_bytes: 512,
..HttpProxyConfig::default()
};
let mut s = HttpProxySession::with_config(cfg);
let mut wire = b"POST /a HTTP/1.1\r\nContent-Length: 4096\r\n\r\n".to_vec();
wire.extend(std::iter::repeat_n(b'x', 4096));
let mut out = Vec::new();
s.feed_initiator(&wire, crate::Timestamp::default(), &mut out);
assert_eq!(decoded_of(&out).len(), 4096, "no body bytes may be lost");
assert!(matches!(out.last(), Some(HttpEvent::End { .. })));
}
#[test]
fn adapter_reset_clears_state() {
use crate::SessionParser;
let mut s = HttpProxySession::new();
let mut out = Vec::new();
s.feed_initiator(
b"POST /a HTTP/1.1\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n\r\n",
crate::Timestamp::default(),
&mut out,
);
assert!(s.is_poisoned());
s.rst_initiator();
assert!(!s.is_poisoned(), "a reset connection starts clean");
}
#[test]
fn clean_fin_is_not_a_poison() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"GET /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut p);
p.fin(FlowSide::Initiator);
p.fin(FlowSide::Responder);
assert!(!p.is_poisoned());
assert!(p.is_done());
}
#[test]
fn close_delimited_response_flushes_at_fin() {
let mut p = HttpProxyParser::new();
push_all(&mut p, FlowSide::Initiator, b"GET /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut p);
push_all(&mut p, FlowSide::Responder, b"HTTP/1.1 200 OK\r\n\r\nhello");
let evs = drain(&mut p);
assert_eq!(decoded_of(&evs), b"hello");
p.fin(FlowSide::Responder);
assert!(!p.is_poisoned());
}
}