Skip to main content

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 {}