http2: test continuation progress on the 8.0.x state machine

Cover split END_STREAM headers in both directions, with and without an
already-closed peer, intermediate continuations, normal headers and DATA,
and duplicate end-of-stream handling.

Require a matching stream before inheriting the reassembly buffer's saved
END_STREAM flag. Otherwise an orphan continuation on stream B can inherit
stream A's flag and prematurely close B. Test that the orphan raises the
anomaly without consuming A's pending flag, then complete A normally.

Ticket: #9218
pull/16509/head
Jason Ish 4 days ago committed by Victor Julien
parent f7ceb8134b
commit 8461dbc9a5

@ -1137,7 +1137,7 @@ impl HTTP2State {
buf.stream_id = 0; buf.stream_id = 0;
buf.data.clear(); buf.data.clear();
} }
hs.end_stream = buf.end_stream; hs.end_stream = same && buf.end_stream;
if same { if same {
buf.end_stream = false; buf.end_stream = false;
} }
@ -1776,3 +1776,175 @@ pub unsafe extern "C" fn SCRegisterHttp2Parser() {
SCLogNotice!("Protocol detector and parser disabled for DOH2."); SCLogNotice!("Protocol detector and parser disabled for DOH2.");
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn frame_header(ftype: parser::HTTP2FrameType, flags: u8) -> parser::HTTP2FrameHeader {
parser::HTTP2FrameHeader {
length: 1,
ftype: ftype as u8,
flags,
reserved: 0,
stream_id: 1,
}
}
#[test]
fn test_http2_continuation_progress() {
use HTTP2TransactionState::*;
for (dir, half_closed, peer_closed) in [
(Direction::ToServer, HTTP2StateHalfClosedClient, HTTP2StateHalfClosedServer),
(Direction::ToClient, HTTP2StateHalfClosedServer, HTTP2StateHalfClosedClient),
] {
for end_stream in [false, true] {
for peer_first in [false, true] {
let mut state = HTTP2State::new();
let mut tx = HTTP2Transaction::new();
let initial = if peer_first { peer_closed } else { HTTP2StateOpen };
tx.state = initial;
let mut reass_limit_reached = false;
let flags = if end_stream { parser::HTTP2_FLAG_HEADER_EOS } else { 0 };
let header = frame_header(parser::HTTP2FrameType::Headers, flags);
let (data, events) = state.parse_frame_data(
&header, &[0x82], true, dir, &mut reass_limit_reached,
);
assert!(events.is_empty());
tx.handle_frame(&header, &data, dir);
assert_eq!(tx.state, initial);
let header = frame_header(parser::HTTP2FrameType::Continuation, 0);
let (data, events) = state.parse_frame_data(
&header, &[0x86], true, dir, &mut reass_limit_reached,
);
assert!(events.is_empty());
assert!(matches!(&data, HTTP2FrameTypeData::CONTINUATION(c) if !c.end_stream));
tx.handle_frame(&header, &data, dir);
assert_eq!(tx.state, initial);
let header = frame_header(
parser::HTTP2FrameType::Continuation, parser::HTTP2_FLAG_HEADER_END_HEADERS,
);
let (data, events) = state.parse_frame_data(
&header, &[0x84], true, dir, &mut reass_limit_reached,
);
assert!(events.is_empty());
assert!(matches!(&data, HTTP2FrameTypeData::CONTINUATION(c)
if c.end_stream == end_stream));
tx.handle_frame(&header, &data, dir);
let expected = if end_stream {
if peer_first { HTTP2StateClosed } else { half_closed }
} else {
initial
};
assert_eq!(tx.state, expected);
assert!(!reass_limit_reached);
let buf = if dir == Direction::ToClient {
&state.s2c_buf
} else {
&state.c2s_buf
};
assert_eq!(buf.stream_id, 0);
assert!(buf.data.is_empty());
assert!(!buf.end_stream);
if end_stream && !peer_first {
let peer_dir = if dir == Direction::ToServer {
Direction::ToClient
} else {
Direction::ToServer
};
let header = frame_header(
parser::HTTP2FrameType::Data, parser::HTTP2_FLAG_HEADER_EOS,
);
tx.handle_frame(&header, &HTTP2FrameTypeData::DATA, peer_dir);
assert_eq!(tx.state, HTTP2StateClosed);
}
}
}
}
}
#[test]
fn test_http2_orphan_continuation_end_stream() {
for dir in [Direction::ToServer, Direction::ToClient] {
let mut state = HTTP2State::new();
let mut reass_limit_reached = false;
let header = frame_header(parser::HTTP2FrameType::Headers, parser::HTTP2_FLAG_HEADER_EOS);
let (_, events) = state.parse_frame_data(
&header, &[0x82], true, dir, &mut reass_limit_reached,
);
assert!(events.is_empty());
let mut header = frame_header(
parser::HTTP2FrameType::Continuation, parser::HTTP2_FLAG_HEADER_END_HEADERS,
);
header.stream_id = 3;
let (data, events) = state.parse_frame_data(
&header, &[0x88], true, dir, &mut reass_limit_reached,
);
assert!(events.iter().any(|e| matches!(e, HTTP2Event::ContinuationWithoutHeaders)));
// An orphan continuation must not close another stream using stream 1's flag.
assert!(matches!(&data, HTTP2FrameTypeData::CONTINUATION(c) if !c.end_stream));
let buf = if dir == Direction::ToClient {
&state.s2c_buf
} else {
&state.c2s_buf
};
assert_eq!(buf.stream_id, 1);
assert!(buf.end_stream);
header.stream_id = 1;
let (data, events) = state.parse_frame_data(
&header, &[0x84], true, dir, &mut reass_limit_reached,
);
assert!(events.is_empty());
assert!(matches!(&data, HTTP2FrameTypeData::CONTINUATION(c) if c.end_stream));
}
}
#[test]
fn test_http2_complete_headers_and_data_progress() {
use HTTP2TransactionState::*;
for (dir, half_closed) in [
(Direction::ToServer, HTTP2StateHalfClosedClient),
(Direction::ToClient, HTTP2StateHalfClosedServer),
] {
let mut tx = HTTP2Transaction::new();
tx.state = HTTP2StateOpen;
let header = frame_header(
parser::HTTP2FrameType::Continuation, parser::HTTP2_FLAG_HEADER_EOS,
);
let data = HTTP2FrameTypeData::CONTINUATION(parser::HTTP2FrameContinuation {
blocks: Vec::new(),
end_stream: false,
});
// END_STREAM is inherited from HEADERS, not read from CONTINUATION flags.
tx.handle_frame(&header, &data, dir);
assert_eq!(tx.state, HTTP2StateOpen);
let data = HTTP2FrameTypeData::HEADERS(parser::HTTP2FrameHeaders {
padlength: None,
priority: None,
blocks: Vec::new(),
});
let header = frame_header(
parser::HTTP2FrameType::Headers,
parser::HTTP2_FLAG_HEADER_EOS | parser::HTTP2_FLAG_HEADER_END_HEADERS,
);
tx.handle_frame(&header, &data, dir);
assert_eq!(tx.state, half_closed);
// Duplicate END_STREAM must not close the other direction.
tx.handle_frame(&header, &data, dir);
assert_eq!(tx.state, half_closed);
tx.state = HTTP2StateOpen;
let header = frame_header(parser::HTTP2FrameType::Data, parser::HTTP2_FLAG_HEADER_EOS);
tx.handle_frame(&header, &HTTP2FrameTypeData::DATA, dir);
assert_eq!(tx.state, half_closed);
}
}
}

Loading…
Cancel
Save