diff --git a/h3i/src/actions/h3.rs b/h3i/src/actions/h3.rs index 5c4e5206b3..b61e9f4f01 100644 --- a/h3i/src/actions/h3.rs +++ b/h3i/src/actions/h3.rs @@ -49,7 +49,7 @@ use crate::encode_header_block; /// sequentially. Note that packets will be flushed when said iteration has /// completed, regardless of if an [`Action::FlushPackets`] was the terminal /// action. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Action { /// Send a [quiche::h3::frame::Frame] over a stream. SendFrame { diff --git a/h3i/src/main.rs b/h3i/src/main.rs index 309cd7ca75..7d1511d7ca 100644 --- a/h3i/src/main.rs +++ b/h3i/src/main.rs @@ -59,7 +59,7 @@ fn main() -> Result<(), ClientError> { }; let actions = match &config.qlog_input { - Some(v) => read_qlog(v), + Some(v) => read_qlog(v, config.host_override.as_deref()), None => prompt_frames(&config), }; @@ -84,6 +84,7 @@ struct Config { library_config: h3i::config::Config, pub qlog_input: Option, pub qlog_actions_output: bool, + pub host_override: Option, } fn config_from_clap() -> std::result::Result { @@ -188,6 +189,13 @@ fn config_from_clap() -> std::result::Result { .takes_value(true) .default_value("16777216"), ) + .arg( + Arg::with_name("replay-host-override") + .long("replay-host-override") + .help("Override the host or authority field in any replayed request headers.") + .requires("qlog-input") + .takes_value(true), + ) .get_matches(); let host_port = matches.value_of("host:port").unwrap().to_string(); @@ -258,6 +266,10 @@ fn config_from_clap() -> std::result::Result { .map(|s| s.to_string()) }); + let host_override = matches + .value_of("replay-host-override") + .map(|s| s.to_string()); + let library_config = h3i::config::Config { host_port, omit_sni, @@ -279,6 +291,7 @@ fn config_from_clap() -> std::result::Result { qlog_input, qlog_actions_output, library_config, + host_override, }) } @@ -288,7 +301,7 @@ fn sync_client( h3i::client::sync_client::connect(&config.library_config, actions) } -fn read_qlog(filename: &str) -> Vec { +fn read_qlog(filename: &str, host_override: Option<&str>) -> Vec { let file = std::fs::File::open(filename).expect("failed to open file"); let reader = BufReader::new(file); @@ -298,7 +311,7 @@ fn read_qlog(filename: &str) -> Vec { for event in qlog_reader { match event { qlog::reader::Event::Qlog(ev) => { - let ac: H3Actions = (ev).into(); + let ac: H3Actions = actions_from_qlog(ev, host_override); actions.extend(ac.0); }, diff --git a/h3i/src/recordreplay/qlog.rs b/h3i/src/recordreplay/qlog.rs index 680efa3595..e05ae01ba1 100644 --- a/h3i/src/recordreplay/qlog.rs +++ b/h3i/src/recordreplay/qlog.rs @@ -30,6 +30,7 @@ use qlog::events::h3::H3FrameCreated; use qlog::events::h3::H3Owner; use qlog::events::h3::H3StreamTypeSet; use qlog::events::h3::Http3Frame; +use qlog::events::h3::HttpHeader; use qlog::events::quic::ErrorSpace; use qlog::events::quic::PacketSent; use qlog::events::quic::QuicFrame; @@ -299,35 +300,40 @@ impl From<&Action> for QlogEvents { } } -impl From for H3Actions { - fn from(event: Event) -> Self { - let mut actions = vec![]; - match &event.data { - EventData::PacketSent(ps) => { - let packet_actions: H3Actions = ps.into(); - actions.extend(packet_actions.0); - }, - - EventData::H3FrameCreated(fc) => { - let frame_created = H3FrameCreatedEx { - frame_created: fc.clone(), - ex_data: event.ex_data.clone(), - }; - let h3_actions: H3Actions = frame_created.into(); - actions.extend(h3_actions.0); - }, +pub fn actions_from_qlog(event: Event, host_override: Option<&str>) -> H3Actions { + let mut actions = vec![]; + match &event.data { + EventData::PacketSent(ps) => { + let packet_actions: H3Actions = ps.into(); + actions.extend(packet_actions.0); + }, + + EventData::H3FrameCreated(fc) => { + let mut frame_created = H3FrameCreatedEx { + frame_created: fc.clone(), + ex_data: event.ex_data.clone(), + }; + + // Insert custom data so that conversion of frames to Actions can + // use it. + if let Some(host) = host_override { + frame_created + .ex_data + .insert("host_override".into(), host.into()); + } - EventData::H3StreamTypeSet(st) => { - let stream_actions = - from_qlog_stream_type_set(st, &event.ex_data); - actions.extend(stream_actions); - }, + actions.push(frame_created.into()); + }, - _ => (), - } + EventData::H3StreamTypeSet(st) => { + let stream_actions = from_qlog_stream_type_set(st, &event.ex_data); + actions.extend(stream_actions); + }, - Self(actions) + _ => (), } + + H3Actions(actions) } impl From for H3Actions { @@ -424,9 +430,22 @@ impl From<&PacketSent> for H3Actions { } } -impl From for H3Actions { +fn map_header( + hdr: &HttpHeader, host_override: Option<&str>, +) -> quiche::h3::Header { + if hdr.name.to_ascii_lowercase() == ":authority" || + hdr.name.to_ascii_lowercase() == "host" + { + if let Some(host) = host_override { + return quiche::h3::Header::new(hdr.name.as_bytes(), host.as_bytes()); + } + } + + quiche::h3::Header::new(hdr.name.as_bytes(), hdr.value.as_bytes()) +} + +impl From for Action { fn from(value: H3FrameCreatedEx) -> Self { - let mut actions = vec![]; let stream_id = value.frame_created.stream_id; let fin_stream = value .ex_data @@ -434,8 +453,13 @@ impl From for H3Actions { .unwrap_or(&serde_json::Value::Null) .as_bool() .unwrap_or_default(); + let host_override = value + .ex_data + .get("host_override") + .unwrap_or(&serde_json::Value::Null) + .as_str(); - match &value.frame_created.frame { + let ret = match &value.frame_created.frame { Http3Frame::Settings { settings } => { let mut raw_settings = vec![]; let mut additional_settings = vec![]; @@ -460,7 +484,8 @@ impl From for H3Actions { }, } } - actions.push(Action::SendFrame { + + Action::SendFrame { stream_id, fin_stream, frame: Frame::Settings { @@ -473,26 +498,22 @@ impl From for H3Actions { raw: Some(raw_settings), additional_settings: Some(additional_settings), }, - }) + } }, Http3Frame::Headers { headers } => { let hdrs: Vec = headers .iter() - .map(|h| { - quiche::h3::Header::new( - h.name.as_bytes(), - h.value.as_bytes(), - ) - }) + .map(|h| map_header(h, host_override)) .collect(); let header_block = encode_header_block(&hdrs).unwrap(); - actions.push(Action::SendHeadersFrame { + + Action::SendHeadersFrame { stream_id, fin_stream, headers: hdrs, frame: Frame::Headers { header_block }, - }); + } }, Http3Frame::Data { raw } => { @@ -506,25 +527,23 @@ impl From for H3Actions { .to_vec(); } - actions.push(Action::SendFrame { + Action::SendFrame { stream_id, fin_stream, frame: Frame::Data { payload }, - }) + } }, - Http3Frame::Goaway { id } => { - actions.push(Action::SendFrame { - stream_id, - fin_stream, - frame: Frame::GoAway { id: *id }, - }); + Http3Frame::Goaway { id } => Action::SendFrame { + stream_id, + fin_stream, + frame: Frame::GoAway { id: *id }, }, _ => unimplemented!(), - } + }; - H3Actions(actions) + ret } } @@ -569,6 +588,7 @@ mod tests { use std::time::Duration; use super::*; + use quiche::h3::Header; use serde_json; const NOW: f32 = 123.0; @@ -645,4 +665,54 @@ mod tests { let deser = serde_json::from_str::(expected).unwrap(); assert_eq!(deser.data, ev.data); } + + #[test] + fn deser_http_headers_to_action() { + let serialized = r#"{"time":0.074725,"name":"http:frame_created","data":{"stream_id":0,"frame":{"frame_type":"headers","headers":[{"name":":method","value":"GET"},{"name":":authority","value":"example.net"},{"name":":path","value":"/"},{"name":":scheme","value":"https"}]}},"fin_stream":true}"#; + let deserialized = serde_json::from_str::(serialized).unwrap(); + let actions = actions_from_qlog(deserialized, None); + assert!(actions.0.len() == 1); + + let headers = vec![ + Header::new(b":method", b"GET"), + Header::new(b":authority", b"example.net"), + Header::new(b":path", b"/"), + Header::new(b":scheme", b"https"), + ]; + let header_block = encode_header_block(&headers).unwrap(); + let frame = Frame::Headers { header_block }; + let expected = Action::SendHeadersFrame { + stream_id: 0, + fin_stream: true, + headers, + frame, + }; + + assert_eq!(actions.0[0], expected); + } + + #[test] + fn deser_http_headers_host_overrid_to_action() { + let serialized = r#"{"time":0.074725,"name":"http:frame_created","data":{"stream_id":0,"frame":{"frame_type":"headers","headers":[{"name":":method","value":"GET"},{"name":":authority","value":"bla.com"},{"name":":path","value":"/"},{"name":":scheme","value":"https"}]}},"fin_stream":true}"#; + let deserialized = serde_json::from_str::(serialized).unwrap(); + let actions = actions_from_qlog(deserialized, Some("example.org")); + assert!(actions.0.len() == 1); + + let headers = vec![ + Header::new(b":method", b"GET"), + Header::new(b":authority", b"example.org"), + Header::new(b":path", b"/"), + Header::new(b":scheme", b"https"), + ]; + let header_block = encode_header_block(&headers).unwrap(); + let frame = Frame::Headers { header_block }; + let expected = Action::SendHeadersFrame { + stream_id: 0, + fin_stream: true, + headers, + frame, + }; + + assert_eq!(actions.0[0], expected); + } }