simoom_h2/lib.rs
1//! Sans-io HTTP/2 (RFC 9113) connection engine for simoom, with DoS accounting.
2//!
3//! A [`Connection`] takes received bytes plus the current time and produces [`Event`]s and
4//! outbound bytes. It never touches sockets, threads or clocks; the event loop feeds it:
5//!
6//! ```
7//! use simoom_h2::{Config, Connection, Event};
8//! let mut server = Connection::server(Config::default());
9//! let mut client = Connection::client(Config::default());
10//! let id = client.send_request(b"GET", b"https", b"example.com", b"/", &[], true).unwrap();
11//!
12//! let c2s = client.output().to_vec();
13//! client.advance_output(c2s.len());
14//! let mut request = None;
15//! let used = server.recv(&c2s, 0, &mut |ev| {
16//! if let Event::Headers { stream, headers, .. } = ev {
17//! request = Some((stream, headers.path().unwrap().to_vec()));
18//! }
19//! });
20//! assert_eq!(used, c2s.len()); // a caller keeps unconsumed bytes for the next call
21//! let (stream, path) = request.unwrap();
22//! assert_eq!((stream, &path[..]), (id, &b"/"[..]));
23//!
24//! server.send_response(stream, 200, &[(b"content-type", b"text/plain")], false).unwrap();
25//! assert_eq!(server.send_data(stream, b"hello", true), Ok(5));
26//! server.stream_finished(stream);
27//!
28//! let s2c = server.output().to_vec();
29//! server.advance_output(s2c.len());
30//! let mut body = Vec::new();
31//! client.recv(&s2c, 0, &mut |ev| {
32//! if let Event::Data { data, .. } = ev {
33//! body.extend_from_slice(data);
34//! }
35//! });
36//! assert_eq!(body, b"hello");
37//! ```
38//!
39//! Received data stays charged to the flow-control windows until the application calls
40//! [`Connection::release`]; that bounds what a stream and the connection can buffer. Outbound
41//! data goes through [`Connection::send_data`], which accepts only what the windows and the
42//! output buffer cap allow and reports the rest as backpressure; [`Connection::poll_writable`]
43//! names streams that can send again.
44//!
45//! DoS accounting (plan §2.7): the concurrency limit counts streams whose handler is still
46//! running (the application calls [`Connection::stream_finished`]), not just open streams, so
47//! resets cannot free capacity while work continues (MadeYouReset, CVE-2025-8671; Rapid Reset,
48//! CVE-2023-44487). Sliding-window [`Budgets`] cap RST_STREAM (received and caused), PING,
49//! SETTINGS, empty frames, PRIORITY and small WINDOW_UPDATE frames; header blocks are capped in
50//! bytes and in CONTINUATION frames. Exceeding any of them sends `GOAWAY(ENHANCE_YOUR_CALM)`.
51#![forbid(unsafe_code)]
52
53mod budget;
54mod conn;
55pub mod frame;
56mod headers;
57mod upgrade;
58
59pub use budget::Budgets;
60pub use conn::{http2_settings_header, Connection, Role};
61pub use frame::{ErrorCode, PREFACE};
62pub use headers::Headers;
63pub use upgrade::{base64url_decode, base64url_encode};
64
65use std::fmt;
66
67/// Settings and limits for one side of a connection.
68#[derive(Clone, Debug)]
69pub struct Config {
70 /// SETTINGS_HEADER_TABLE_SIZE: the HPACK table we let the peer use for our decoder.
71 pub header_table_size: u32,
72 /// Largest HPACK table our encoder keeps, whatever the peer allows.
73 pub encoder_table_size: u32,
74 /// SETTINGS_MAX_CONCURRENT_STREAMS, and the cap on streams whose handler is running.
75 pub max_concurrent_streams: u32,
76 /// SETTINGS_INITIAL_WINDOW_SIZE: the starting per-stream receive window.
77 pub initial_window_size: u32,
78 /// The starting connection receive window.
79 pub connection_window_size: u32,
80 /// Grow the receive windows from bandwidth-delay estimates (PING round trips).
81 pub window_autotune: bool,
82 /// Per-stream window cap for auto-tuning: the most a stream can have unreleased.
83 pub max_stream_window: u32,
84 /// Connection window cap for auto-tuning: the most the connection can have unreleased.
85 pub max_connection_window: u32,
86 /// SETTINGS_MAX_FRAME_SIZE we accept.
87 pub max_frame_size: u32,
88 /// SETTINGS_MAX_HEADER_LIST_SIZE, enforced on the decoded list and on the compressed header
89 /// block across CONTINUATION frames.
90 pub max_header_list_size: u32,
91 /// Most CONTINUATION frames in one header block.
92 pub max_continuation_frames: u32,
93 /// Output buffer cap: DATA is accepted only while less than this is waiting to be written,
94 /// and input processing pauses while twice this is waiting.
95 pub max_send_buffer: usize,
96 /// How long the peer may take to acknowledge our SETTINGS.
97 pub settings_timeout_ms: u64,
98 /// Graceful shutdown: how long to wait for the PING that follows the first GOAWAY.
99 pub graceful_timeout_ms: u64,
100 /// Frame budgets per sliding window.
101 pub budgets: Budgets,
102 /// Server: announce SETTINGS_ENABLE_CONNECT_PROTOCOL and accept extended CONNECT requests
103 /// with a `:protocol` pseudo-header (RFC 8441, WebSocket over HTTP/2). Default off; needs
104 /// the `connect-protocol` cargo feature (on by default).
105 pub enable_connect_protocol: bool,
106 /// PING keepalive: after this many milliseconds without any frame from the peer, send a
107 /// PING. `None` (the default) sends none. Needs the `keepalive` cargo feature (on by
108 /// default).
109 pub keepalive_interval_ms: Option<u64>,
110 /// PING keepalive: close the connection (GOAWAY `NO_ERROR`, then [`Event::Closed`]) when
111 /// nothing arrives from the peer within this many milliseconds of a keepalive PING.
112 /// Default 20 s.
113 pub keepalive_timeout_ms: u64,
114}
115
116impl Default for Config {
117 fn default() -> Self {
118 Config {
119 header_table_size: 4096,
120 encoder_table_size: 4096,
121 max_concurrent_streams: 100,
122 initial_window_size: 256 * 1024,
123 connection_window_size: 1024 * 1024,
124 window_autotune: true,
125 max_stream_window: 4 * 1024 * 1024,
126 max_connection_window: 16 * 1024 * 1024,
127 max_frame_size: 16_384,
128 max_header_list_size: 16 * 1024,
129 max_continuation_frames: 32,
130 max_send_buffer: 128 * 1024,
131 settings_timeout_ms: 10_000,
132 graceful_timeout_ms: 1_000,
133 budgets: Budgets::default(),
134 enable_connect_protocol: false,
135 keepalive_interval_ms: None,
136 keepalive_timeout_ms: 20_000,
137 }
138 }
139}
140
141/// Something the peer did, reported by [`Connection::recv`] and [`Connection::handle_timeout`].
142#[derive(Debug)]
143pub enum Event<'a> {
144 /// Request headers (server), or response headers including 1xx (client).
145 Headers { stream: u32, headers: &'a Headers, end_stream: bool },
146 /// Request or response body bytes. Call [`Connection::release`] once they are consumed.
147 Data { stream: u32, data: &'a [u8], end_stream: bool },
148 /// Trailers; they always end the stream.
149 Trailers { stream: u32, trailers: &'a Headers },
150 /// The stream ended abnormally: reset by the peer (`remote`), refused by its GOAWAY, or
151 /// reset by us after a stream error. Stop its work, then call
152 /// [`Connection::stream_finished`].
153 Reset { stream: u32, code: ErrorCode, remote: bool },
154 /// The peer's SETTINGS were applied (and acknowledged).
155 Settings,
156 /// The answer to [`Connection::ping`].
157 Pong { data: [u8; 8] },
158 /// The peer is shutting down: no stream above `last_stream` will be processed.
159 GoAway { last_stream: u32, code: ErrorCode, debug: &'a [u8] },
160 /// We closed the connection with a GOAWAY carrying `code` (a connection error or an
161 /// exceeded budget). Flush the output, then close the transport.
162 Closed { code: ErrorCode },
163}
164
165/// Errors from the sending API.
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub enum Error {
168 /// No such stream (never opened, or closed and forgotten).
169 UnknownStream,
170 /// The stream was reset.
171 StreamReset,
172 /// We already ended the stream.
173 StreamClosed,
174 /// Final headers were already sent.
175 HeadersAlreadySent,
176 /// Data or trailers before the headers.
177 HeadersNotSent,
178 /// A field name or value that HTTP/2 does not allow, or an invalid status.
179 InvalidHeader,
180 /// The peer's SETTINGS_MAX_CONCURRENT_STREAMS is reached.
181 StreamLimit,
182 /// No stream identifiers left: open a new connection.
183 StreamIdsExhausted,
184 /// The peer sent GOAWAY: no new streams.
185 GoingAway,
186 /// The connection is closed.
187 ConnectionClosed,
188 /// The call is for the other role (for example a client sending a response).
189 WrongRole,
190 /// An `HTTP2-Settings` value that is not valid base64url SETTINGS.
191 InvalidUpgradeSettings,
192 /// The header block is larger than the peer's SETTINGS_MAX_HEADER_LIST_SIZE; nothing was
193 /// sent.
194 HeaderListTooLarge,
195 /// An extended CONNECT before the server enabled it with SETTINGS_ENABLE_CONNECT_PROTOCOL
196 /// (RFC 8441 §3); nothing was sent.
197 ConnectProtocolDisabled,
198}
199
200impl fmt::Display for Error {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 fmt::Debug::fmt(self, f)
203 }
204}
205
206impl std::error::Error for Error {}