Skip to content

Commit

Permalink
h3i: add --replay-host-override option
Browse files Browse the repository at this point in the history
One use case for replaying a recorded session with the h3i CLI is to execture
the same sequence of actions to different target servers. Many servers will
validate that there is a provided "host" or ":authority" header field, and that
it matches the certificate SAN.

This change adds the "--replay-host-override" option that can be used in
combination with the --qlog-input option. It replaces any "host" or ":authority"
header in a HEADERS frame, with the provided value.
  • Loading branch information
LPardue committed Nov 18, 2024
1 parent 57bdafd commit 2a265cf
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 52 deletions.
2 changes: 1 addition & 1 deletion h3i/src/actions/h3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 16 additions & 3 deletions h3i/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

Expand All @@ -84,6 +84,7 @@ struct Config {
library_config: h3i::config::Config,
pub qlog_input: Option<String>,
pub qlog_actions_output: bool,
pub host_override: Option<String>,
}

fn config_from_clap() -> std::result::Result<Config, String> {
Expand Down Expand Up @@ -188,6 +189,13 @@ fn config_from_clap() -> std::result::Result<Config, String> {
.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();
Expand Down Expand Up @@ -258,6 +266,10 @@ fn config_from_clap() -> std::result::Result<Config, String> {
.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,
Expand All @@ -279,6 +291,7 @@ fn config_from_clap() -> std::result::Result<Config, String> {
qlog_input,
qlog_actions_output,
library_config,
host_override,
})
}

Expand All @@ -288,7 +301,7 @@ fn sync_client(
h3i::client::sync_client::connect(&config.library_config, actions)
}

fn read_qlog(filename: &str) -> Vec<Action> {
fn read_qlog(filename: &str, host_override: Option<&str>) -> Vec<Action> {
let file = std::fs::File::open(filename).expect("failed to open file");
let reader = BufReader::new(file);

Expand All @@ -298,7 +311,7 @@ fn read_qlog(filename: &str) -> Vec<Action> {
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);
},

Expand Down
166 changes: 118 additions & 48 deletions h3i/src/recordreplay/qlog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -299,35 +300,40 @@ impl From<&Action> for QlogEvents {
}
}

impl From<Event> 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<JsonEvent> for H3Actions {
Expand Down Expand Up @@ -424,18 +430,36 @@ impl From<&PacketSent> for H3Actions {
}
}

impl From<H3FrameCreatedEx> 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<H3FrameCreatedEx> 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
.get("fin_stream")
.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![];
Expand All @@ -460,7 +484,8 @@ impl From<H3FrameCreatedEx> for H3Actions {
},
}
}
actions.push(Action::SendFrame {

Action::SendFrame {
stream_id,
fin_stream,
frame: Frame::Settings {
Expand All @@ -473,26 +498,22 @@ impl From<H3FrameCreatedEx> for H3Actions {
raw: Some(raw_settings),
additional_settings: Some(additional_settings),
},
})
}
},

Http3Frame::Headers { headers } => {
let hdrs: Vec<quiche::h3::Header> = 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 } => {
Expand All @@ -506,25 +527,23 @@ impl From<H3FrameCreatedEx> 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
}
}

Expand Down Expand Up @@ -569,6 +588,7 @@ mod tests {
use std::time::Duration;

use super::*;
use quiche::h3::Header;
use serde_json;

const NOW: f32 = 123.0;
Expand Down Expand Up @@ -645,4 +665,54 @@ mod tests {
let deser = serde_json::from_str::<JsonEvent>(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::<Event>(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::<Event>(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);
}
}

0 comments on commit 2a265cf

Please sign in to comment.