1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! HTTP/2 State Machine Violation Tests
//!
//! Tests client behavior when server violates the HTTP/2 state machine,
//! such as sending DATA frames on closed streams or using invalid stream IDs.
use specter::Client;
use std::time::Duration;
use tokio::time::timeout;
mod helpers;
use helpers::mock_h2_server::{MockH2Connection, MockH2Server};
/// Helper to perform basic H2 handshake.
/// Helper to perform H2 handshake and read the first HEADERS frame.
/// Returns the stream ID of the headers.
async fn perform_handshake_and_read_headers(conn: &MockH2Connection) -> std::io::Result<u32> {
// Read client preface
conn.read_preface().await?;
// Loop until we get HEADERS
let stream_id = loop {
let (len, frame_type, flags, sid, _) = conn.read_frame().await?;
tracing::debug!(
"Server RX: Type={} Flags={} Len={} Sid={}",
frame_type,
flags,
len,
sid
);
match frame_type {
0x01 => {
// HEADERS
break sid;
}
0x04 => {
// SETTINGS
if flags & 0x01 == 0 {
// Client Settings - Reply
conn.send_settings(&[(0x03, 100), (0x04, 65535)]).await?;
conn.send_settings_ack().await?;
} else {
tracing::debug!("Server RX: Settings ACK");
}
}
_ => {
// Ignore WINDOW_UPDATE (0x08) and others during handshake
}
}
};
Ok(stream_id)
}
#[tokio::test]
async fn test_data_on_closed_stream() {
let _ = tracing_subscriber::fmt()
.with_env_filter("trace")
.try_init();
let server = MockH2Server::new().await.unwrap();
let url = format!("http://127.0.0.1:{}/test", server.port());
let _handle = server.start(|conn| async move {
// Handshake and read request
let stream_id = perform_handshake_and_read_headers(&conn).await.unwrap();
assert_eq!(stream_id, 1, "Expected Client Stream ID 1");
// Send response HEADERS with END_STREAM (closing the stream)
let response_headers = encode_simple_response();
conn.send_headers(stream_id, &response_headers, true, true)
.await
.unwrap();
// Give client time to process
tokio::time::sleep(Duration::from_millis(50)).await;
// Violate state machine: send DATA on closed stream
conn.send_data(stream_id, b"This should not be accepted", false)
.await
.unwrap();
// Client should send RST_STREAM or GOAWAY
let result = timeout(Duration::from_secs(1), conn.read_frame()).await;
match result {
Ok(Ok((_, frame_type, _, _, _))) => {
tracing::info!("Received frame type {}", frame_type);
if frame_type == 0x03 { // RST_STREAM
// Success
} else if frame_type == 0x07 { // GOAWAY
// Success
} else {
tracing::warn!("Received unexpected frame type {}", frame_type);
// It is possible we receive a WindowUpdate or something else if timing is tight
}
}
Ok(Err(_)) => {
// Connection closed
}
Err(_) => {
// Timeout
}
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
// Client makes request
let client = Client::builder()
.prefer_http2(true)
.http2_prior_knowledge(true)
.build()
.unwrap();
let result = timeout(Duration::from_secs(2), client.get(url.as_str()).send()).await;
// Request should succeed (we got a valid response before the violation)
assert!(result.is_ok(), "Request timed out");
let response = result.unwrap();
assert!(response.is_ok(), "Request failed: {:?}", response.err());
}
#[tokio::test]
async fn test_server_initiated_stream_even_id() {
let _ = tracing_subscriber::fmt()
.with_env_filter("trace")
.try_init();
let server = MockH2Server::new().await.unwrap();
let url = format!("http://127.0.0.1:{}/test", server.port());
let _handle = server.start(|conn| async move {
// Handshake and read request
let stream_id = perform_handshake_and_read_headers(&conn).await.unwrap();
// Send valid response for client stream
let response_headers = encode_simple_response();
conn.send_headers(stream_id, &response_headers, true, true)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
// Violate state machine: server sends HEADERS on even stream ID (server-initiated)
let invalid_headers = encode_simple_response();
conn.send_headers(2, &invalid_headers, false, true)
.await
.unwrap();
// Client should send GOAWAY or RST_STREAM
let result = timeout(Duration::from_secs(1), conn.read_frame()).await;
if let Ok(Ok((_, frame_type, _, _, _))) = result {
tracing::info!("Received frame type {}", frame_type);
assert!(
frame_type == 0x03 || frame_type == 0x07,
"Expected 0x03 or 0x07, got {}",
frame_type
);
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
let client = Client::builder()
.prefer_http2(true)
.http2_prior_knowledge(true)
.build()
.unwrap();
let result = timeout(Duration::from_secs(2), client.get(url.as_str()).send()).await;
assert!(result.is_ok());
assert!(result.unwrap().is_ok());
}
/// Encode a minimal HTTP/2 response using literal headers (no dynamic table).
/// Returns: ":status: 200" + "content-length: 2"
fn encode_simple_response() -> Vec<u8> {
// :status: 200 (indexed, static table index 8)
// 0x88
// content-length: 0 (literal with no indexing)
// We can use index 28 (content-length) from static table
// 0x00 | 0x0f = 0x0f (Literal without indexing, Index 15... wait)
// Literal Header Field without Indexing - Indexed Name
// Format: 0000 NNNN
// Index 28 (11100).
// 28 > 15. So prefix 15, then varint.
// 0x0f, 13 (28-15=13 -> 0x0d).
// So 0x0f, 0x0d is CORRECT for "Name Index 28"!
// WAIT. My previous analysis was "Name Index 15".
// 0x0f is 0000 1111.
// If we want Index 28:
// 4-bit prefix max is 15.
// So we write 15 (0x0f).
// Remaining is 13.
// Next byte: 13 (0x0d).
// So `0x0f, 0x0d` means "Name matches Static Index 28 (content-length)".
//
// Then Value Length.
// Previous code:
// 0x88,
// 0x0f, 0x0d,
// b'c', b'o'... -> This was writing the NAME "content-length".
// BUT if we used Index 28, we DON'T write the name!
// We only write Valid Length + Value.
// So the previous code was mixing "Indexed Name" with "Literal Name".
// It wrote index 28, then wrote the name bytes as if it was a value? Or just garbage?
// It wrote "content-length" as value bytes?
// Correct encoding for content-length: 2
// 1. Indexed Name (Index 28).
// 0x0f, 0x0d.
// 2. Value Length (1).
// 0x01.
// 3. Value ("2").
// 0x32 ('2').
vec![
0x88, // :status: 200
0x0f, 0x0d, // Name Index 28 (content-length)
0x01, // Value length 1
b'0', // Value "0" (or 2?) Let's use 0 to match Empty Body handling in test
]
}