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 15, 2024
1 parent 57bdafd commit 312903d
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 126 deletions.
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
248 changes: 125 additions & 123 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,33 @@ 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);
},

EventData::H3StreamTypeSet(st) => {
let stream_actions =
from_qlog_stream_type_set(st, &event.ex_data);
actions.extend(stream_actions);
},

_ => (),
}

Self(actions)
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 frame_created = H3FrameCreatedEx {
frame_created: fc.clone(),
ex_data: event.ex_data.clone(),
};
let h3_actions: H3Actions =
actions_from_frame_created(frame_created, host_override);
actions.extend(h3_actions.0);
},

EventData::H3StreamTypeSet(st) => {
let stream_actions = from_qlog_stream_type_set(st, &event.ex_data);
actions.extend(stream_actions);
},

_ => (),
}

H3Actions(actions)
}

impl From<JsonEvent> for H3Actions {
Expand Down Expand Up @@ -424,108 +423,111 @@ impl From<&PacketSent> for H3Actions {
}
}

impl From<H3FrameCreatedEx> for H3Actions {
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();

match &value.frame_created.frame {
Http3Frame::Settings { settings } => {
let mut raw_settings = vec![];
let mut additional_settings = vec![];
// This is ugly but it reflects ambiguity in the qlog
// specs.
for s in settings {
match s.name.as_str() {
"MAX_FIELD_SECTION_SIZE" =>
raw_settings.push((0x6, s.value)),
"QPACK_MAX_TABLE_CAPACITY" =>
raw_settings.push((0x1, s.value)),
"QPACK_BLOCKED_STREAMS" =>
raw_settings.push((0x7, s.value)),
"SETTINGS_ENABLE_CONNECT_PROTOCOL" =>
raw_settings.push((0x8, s.value)),
"H3_DATAGRAM" => raw_settings.push((0x33, s.value)),

_ =>
if let Ok(ty) = s.name.parse::<u64>() {
raw_settings.push((ty, s.value));
additional_settings.push((ty, s.value));
},
}
}
actions.push(Action::SendFrame {
stream_id,
fin_stream,
frame: Frame::Settings {
max_field_section_size: None,
qpack_max_table_capacity: None,
qpack_blocked_streams: None,
connect_protocol_enabled: None,
h3_datagram: None,
grease: None,
raw: Some(raw_settings),
additional_settings: Some(additional_settings),
},
})
},
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());
}
}

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(),
)
})
.collect();
let header_block = encode_header_block(&hdrs).unwrap();
actions.push(Action::SendHeadersFrame {
stream_id,
fin_stream,
headers: hdrs,
frame: Frame::Headers { header_block },
});
},
quiche::h3::Header::new(hdr.name.as_bytes(), hdr.value.as_bytes())
}

Http3Frame::Data { raw } => {
let mut payload = vec![];
if let Some(r) = raw {
payload = r
.data
.clone()
.unwrap_or("".to_string())
.as_bytes()
.to_vec();
fn actions_from_frame_created(
value: H3FrameCreatedEx, host_override: Option<&str>,
) -> H3Actions {
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();

match &value.frame_created.frame {
Http3Frame::Settings { settings } => {
let mut raw_settings = vec![];
let mut additional_settings = vec![];
// This is ugly but it reflects ambiguity in the qlog
// specs.
for s in settings {
match s.name.as_str() {
"MAX_FIELD_SECTION_SIZE" => raw_settings.push((0x6, s.value)),
"QPACK_MAX_TABLE_CAPACITY" =>
raw_settings.push((0x1, s.value)),
"QPACK_BLOCKED_STREAMS" => raw_settings.push((0x7, s.value)),
"SETTINGS_ENABLE_CONNECT_PROTOCOL" =>
raw_settings.push((0x8, s.value)),
"H3_DATAGRAM" => raw_settings.push((0x33, s.value)),

_ =>
if let Ok(ty) = s.name.parse::<u64>() {
raw_settings.push((ty, s.value));
additional_settings.push((ty, s.value));
},
}
}
actions.push(Action::SendFrame {
stream_id,
fin_stream,
frame: Frame::Settings {
max_field_section_size: None,
qpack_max_table_capacity: None,
qpack_blocked_streams: None,
connect_protocol_enabled: None,
h3_datagram: None,
grease: None,
raw: Some(raw_settings),
additional_settings: Some(additional_settings),
},
})
},

Http3Frame::Headers { headers } => {
let hdrs: Vec<quiche::h3::Header> = headers
.iter()
.map(|h| map_header(h, host_override))
.collect();
let header_block = encode_header_block(&hdrs).unwrap();
actions.push(Action::SendHeadersFrame {
stream_id,
fin_stream,
headers: hdrs,
frame: Frame::Headers { header_block },
});
},

Http3Frame::Data { raw } => {
let mut payload = vec![];
if let Some(r) = raw {
payload =
r.data.clone().unwrap_or("".to_string()).as_bytes().to_vec();
}

actions.push(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 },
});
},
actions.push(Action::SendFrame {
stream_id,
fin_stream,
frame: Frame::Data { payload },
})
},

_ => unimplemented!(),
}
Http3Frame::Goaway { id } => {
actions.push(Action::SendFrame {
stream_id,
fin_stream,
frame: Frame::GoAway { id: *id },
});
},

H3Actions(actions)
_ => unimplemented!(),
}

H3Actions(actions)
}

fn from_qlog_stream_type_set(
Expand Down

0 comments on commit 312903d

Please sign in to comment.