use super::{client_cancellation::open_get, fixture::*};
use crate::{
Conn, HttpConfig, Method,
h2::{
H2ErrorCode, H2Transport,
frame::{Frame, PriorityInfo, headers as headers_frame},
},
};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
const RESTORE_TABLE_SIZE: &[u8] = &[0x3f, 0xe1, 0x1f];
const INSERT_SYNC: &[u8] = b"\x40\x06x-sync\x05probe";
const USE_SYNC: &[u8] = &[0x80 | 62];
const GET_REQUEST: &[u8] = b"\x82\x86\x84\x01\x04test";
const STATUS_OK: &[u8] = &[0x88];
const METHOD_GET: &[u8] = &[0x82];
const UNKNOWN_INDEX: &[u8] = &[0x80 | 63];
fn peer_headers_fragment(
fx: &mut DriverFixture,
stream_id: u32,
fragment: &[u8],
priority: Option<PriorityInfo>,
end_headers: bool,
) {
let prefix_len = headers_frame::encoded_prefix_len(0, priority.is_some());
let mut frame = vec![0; prefix_len + fragment.len()];
let len = u32::try_from(fragment.len()).expect("fragment fits u32");
headers_frame::encode_prefix(stream_id, true, end_headers, priority, len, 0, &mut frame)
.expect("encode HEADERS prefix");
frame[prefix_len..].copy_from_slice(fragment);
fx.peer.write_all(&frame);
}
#[derive(Clone, Copy, Debug)]
enum Delivery {
Whole,
Split,
Interleaved,
}
fn deliver_block(
fx: &mut DriverFixture,
stream_id: u32,
block: &[u8],
priority: Option<PriorityInfo>,
delivery: Delivery,
between: impl FnOnce(&mut DriverFixture),
) -> Vec<Frame> {
if matches!(delivery, Delivery::Whole) {
peer_headers_fragment(fx, stream_id, block, priority, true);
let _ = fx.tick();
return fx.next_outbound_frames();
}
let (head, tail) = block.split_at(block.len() / 2);
peer_headers_fragment(fx, stream_id, head, priority, false);
if matches!(delivery, Delivery::Interleaved) {
assert!(fx.tick().is_pending());
between(fx);
assert!(fx.tick().is_pending());
}
fx.peer_continuation(stream_id, tail, true);
let _ = fx.tick();
fx.next_outbound_frames()
}
fn assert_no_goaway(frames: &[Frame]) {
assert_eq!(count_goaways(frames), 0, "connection closed: {frames:?}");
}
fn rst_on(frames: &[Frame], id: u32) -> Option<H2ErrorCode> {
frames.iter().find_map(|frame| match frame {
Frame::RstStream {
stream_id,
error_code,
} if *stream_id == id => Some(*error_code),
_ => None,
})
}
fn assert_rst(frames: &[Frame], id: u32, code: H2ErrorCode) {
assert_eq!(rst_on(frames, id), Some(code), "stream {id}: {frames:?}");
}
#[derive(Clone, Copy, Debug)]
enum ServerRejection {
Refused,
Reset,
SelfDependent,
Malformed,
}
struct Rejection {
id: u32,
code: H2ErrorCode,
priority: Option<PriorityInfo>,
prefix: &'static [u8],
occupant: Option<Conn<H2Transport>>,
}
fn expect_conn(fx: &mut DriverFixture) -> Conn<H2Transport> {
match fx.tick() {
Poll::Ready(Some(Ok(conn))) => conn,
other => panic!(
"expected a Conn, got {other:?}: {:?}",
fx.next_outbound_frames()
),
}
}
fn server_with_rejection(rejection: ServerRejection) -> (DriverFixture, Rejection) {
let mut fx = DriverFixture::new_server_with_config(
HttpConfig::default().with_h2_max_concurrent_streams(1),
);
fx.complete_handshake();
let mut setup = Rejection {
id: 1,
code: H2ErrorCode::ProtocolError,
priority: None,
prefix: &[],
occupant: None,
};
match rejection {
ServerRejection::Refused => {
fx.peer_open_stream(1, Method::Get, "/", true);
setup.occupant = Some(expect_conn(&mut fx));
setup.id = 3;
setup.code = H2ErrorCode::RefusedStream;
}
ServerRejection::Reset => {
fx.peer_open_stream(1, Method::Post, "/", false);
let _conn = expect_conn(&mut fx);
fx.peer_rst_stream(1, H2ErrorCode::Cancel);
assert!(fx.tick().is_pending());
setup.code = H2ErrorCode::StreamClosed;
}
ServerRejection::SelfDependent => {
setup.priority = Some(PriorityInfo {
exclusive: false,
stream_dependency: 1,
weight: 16,
});
}
ServerRejection::Malformed => setup.prefix = METHOD_GET,
}
fx.next_outbound_frames();
(fx, setup)
}
fn server_rejected_block_keeps_hpack_in_sync(rejection: ServerRejection, delivery: Delivery) {
let (mut fx, mut setup) = server_with_rejection(rejection);
let block = [RESTORE_TABLE_SIZE, setup.prefix, GET_REQUEST, INSERT_SYNC].concat();
let mut occupant = setup.occupant.take();
let frames = deliver_block(&mut fx, setup.id, &block, setup.priority, delivery, |_| {
drop(occupant.take());
});
let context = format!("{rejection:?} {delivery:?}");
assert_eq!(
rst_on(&frames, setup.id),
Some(setup.code),
"{context}: {frames:?}"
);
assert_no_goaway(&frames);
drop(occupant);
assert!(fx.tick().is_pending());
fx.next_outbound_frames();
let next_id = setup.id + 2;
peer_headers_fragment(
&mut fx,
next_id,
&[GET_REQUEST, USE_SYNC].concat(),
None,
true,
);
let conn = expect_conn(&mut fx);
assert_eq!(
conn.request_headers().get_str("x-sync"),
Some("probe"),
"{context}"
);
assert_no_goaway(&fx.next_outbound_frames());
}
const ALL_DELIVERIES: [Delivery; 3] = [Delivery::Whole, Delivery::Split, Delivery::Interleaved];
#[test]
fn refused_stream_block_keeps_hpack_in_sync() {
for delivery in ALL_DELIVERIES {
server_rejected_block_keeps_hpack_in_sync(ServerRejection::Refused, delivery);
}
}
#[test]
fn reset_stream_block_keeps_hpack_in_sync() {
for delivery in ALL_DELIVERIES {
server_rejected_block_keeps_hpack_in_sync(ServerRejection::Reset, delivery);
}
}
#[test]
fn self_dependent_stream_block_keeps_hpack_in_sync() {
for delivery in ALL_DELIVERIES {
server_rejected_block_keeps_hpack_in_sync(ServerRejection::SelfDependent, delivery);
}
}
#[test]
fn malformed_request_block_keeps_hpack_in_sync() {
for delivery in ALL_DELIVERIES {
server_rejected_block_keeps_hpack_in_sync(ServerRejection::Malformed, delivery);
}
}
#[test]
fn compression_error_in_rejected_block_closes_connection() {
for rejection in [
ServerRejection::Refused,
ServerRejection::Reset,
ServerRejection::SelfDependent,
] {
let (mut fx, setup) = server_with_rejection(rejection);
let block = [GET_REQUEST, UNKNOWN_INDEX].concat();
let frames = deliver_block(
&mut fx,
setup.id,
&block,
setup.priority,
Delivery::Whole,
|_| {},
);
assert!(
frames.iter().any(|frame| matches!(
frame,
Frame::Goaway {
error_code: H2ErrorCode::CompressionError,
..
}
)),
"{rejection:?}: {frames:?}"
);
}
}
#[test]
fn block_for_stream_canceled_mid_block_is_dropped_in_sync() {
let mut fx = DriverFixture::new_client();
fx.complete_handshake_client();
let (canceled, transport) = open_get(&mut fx);
let mut transport = Some(transport);
let frames = deliver_block(
&mut fx,
canceled,
&[STATUS_OK, INSERT_SYNC].concat(),
None,
Delivery::Interleaved,
|_| drop(transport.take()),
);
let resets = frames
.iter()
.filter(
|frame| matches!(frame, Frame::RstStream { stream_id, .. } if *stream_id == canceled),
)
.count();
assert_eq!(resets, 1, "only the cancellation itself: {frames:?}");
assert_no_goaway(&frames);
let (next, _transport) = open_get(&mut fx);
peer_headers_fragment(&mut fx, next, &[STATUS_OK, USE_SYNC].concat(), None, true);
assert!(fx.tick().is_pending());
assert_no_goaway(&fx.next_outbound_frames());
assert_response_header(&fx, next, "x-sync", "probe");
}
fn assert_response_header(fx: &DriverFixture, id: u32, name: &'static str, value: &str) {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let Poll::Ready(Ok(field_section)) =
Pin::new(&mut fx.connection.response_headers(id)).poll(&mut cx)
else {
panic!("response headers on stream {id} not ready");
};
assert_eq!(field_section.headers().get_str(name), Some(value));
}
#[test]
fn late_block_on_locally_reset_server_stream_keeps_hpack_in_sync() {
for delivery in [Delivery::Whole, Delivery::Split] {
let mut fx = DriverFixture::new_server();
fx.complete_handshake();
fx.peer_open_stream(1, Method::Post, "/", false);
drop(expect_conn(&mut fx));
let _ = fx.tick();
assert_rst(&fx.next_outbound_frames(), 1, H2ErrorCode::Cancel);
let block = [RESTORE_TABLE_SIZE, INSERT_SYNC].concat();
let frames = deliver_block(&mut fx, 1, &block, None, delivery, |_| {});
assert_eq!(rst_on(&frames, 1), None, "{delivery:?}: {frames:?}");
assert_no_goaway(&frames);
peer_headers_fragment(&mut fx, 3, &[GET_REQUEST, USE_SYNC].concat(), None, true);
let conn = expect_conn(&mut fx);
assert_eq!(conn.request_headers().get_str("x-sync"), Some("probe"));
}
}
#[test]
fn late_block_on_canceled_client_stream_keeps_hpack_in_sync() {
for delivery in [Delivery::Whole, Delivery::Split] {
let mut fx = DriverFixture::new_client();
fx.complete_handshake_client();
let (canceled, transport) = open_get(&mut fx);
drop(transport);
assert!(fx.tick().is_pending());
fx.next_outbound_frames();
let (next, _transport) = open_get(&mut fx);
let block = [RESTORE_TABLE_SIZE, STATUS_OK, INSERT_SYNC].concat();
let frames = deliver_block(&mut fx, canceled, &block, None, delivery, |_| {});
assert_eq!(rst_on(&frames, canceled), None, "{delivery:?}: {frames:?}");
assert_no_goaway(&frames);
peer_headers_fragment(&mut fx, next, &[STATUS_OK, USE_SYNC].concat(), None, true);
assert!(fx.tick().is_pending());
assert_no_goaway(&fx.next_outbound_frames());
assert_response_header(&fx, next, "x-sync", "probe");
}
}