Skip to main content

sui_http/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::time::Duration;
5
6// Matches hyper's default.
7const DEFAULT_HTTP1_HEADER_READ_TIMEOUT_SECS: u64 = 30;
8const DEFAULT_HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 20;
9// Matches hyper's post-Rapid-Reset (CVE-2023-44487) hardened default.
10const DEFAULT_MAX_CONCURRENT_STREAMS: u32 = 200;
11const DEFAULT_TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
12const DEFAULT_MAX_PENDING_CONNECTIONS: usize = 4096;
13
14#[derive(Debug, Clone)]
15pub struct Config {
16    init_stream_window_size: Option<u32>,
17    init_connection_window_size: Option<u32>,
18    max_concurrent_streams: Option<u32>,
19    pub(crate) tcp_keepalive: Option<Duration>,
20    pub(crate) tcp_nodelay: bool,
21    http2_keepalive_interval: Option<Duration>,
22    http2_keepalive_timeout: Option<Duration>,
23    http2_adaptive_window: Option<bool>,
24    http2_max_pending_accept_reset_streams: Option<usize>,
25    http2_max_header_list_size: Option<u32>,
26    max_frame_size: Option<u32>,
27    http1_header_read_timeout: Option<Duration>,
28    pub(crate) accept_http1: bool,
29    enable_connect_protocol: bool,
30    pub(crate) max_connection_age: Option<Duration>,
31    pub(crate) max_connection_age_grace: Option<Duration>,
32    pub(crate) tls_handshake_timeout: Duration,
33    pub(crate) max_pending_connections: usize,
34}
35
36impl Default for Config {
37    fn default() -> Self {
38        Self {
39            init_stream_window_size: None,
40            init_connection_window_size: None,
41            max_concurrent_streams: Some(DEFAULT_MAX_CONCURRENT_STREAMS),
42            tcp_keepalive: None,
43            tcp_nodelay: true,
44            http2_keepalive_interval: None,
45            http2_keepalive_timeout: None,
46            http2_adaptive_window: None,
47            http2_max_pending_accept_reset_streams: None,
48            http2_max_header_list_size: None,
49            max_frame_size: None,
50            http1_header_read_timeout: Some(Duration::from_secs(
51                DEFAULT_HTTP1_HEADER_READ_TIMEOUT_SECS,
52            )),
53            accept_http1: true,
54            enable_connect_protocol: true,
55            max_connection_age: None,
56            max_connection_age_grace: None,
57            tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
58            max_pending_connections: DEFAULT_MAX_PENDING_CONNECTIONS,
59        }
60    }
61}
62
63impl Config {
64    /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2
65    /// stream-level flow control.
66    ///
67    /// If `None` is specified, hyper's default is used (currently 1 MiB;
68    /// the HTTP/2 spec default of 65,535 bytes only applies to
69    /// implementations that never adjust it).
70    ///
71    /// [spec]: https://httpwg.org/specs/rfc9113.html#InitialWindowSize
72    pub fn initial_stream_window_size(self, sz: impl Into<Option<u32>>) -> Self {
73        Self {
74            init_stream_window_size: sz.into(),
75            ..self
76        }
77    }
78
79    /// Sets the max connection-level flow control for HTTP2
80    ///
81    /// If `None` is specified, hyper's default is used (currently 1 MiB).
82    ///
83    /// Note that hyper's default equals the per-stream window, so a single
84    /// stream stalled mid-upload can pin the entire connection receive
85    /// window and starve every other stream on the connection. Workloads
86    /// with large or streaming request bodies should consider raising this
87    /// to a multiple of the stream window.
88    pub fn initial_connection_window_size(self, sz: impl Into<Option<u32>>) -> Self {
89        Self {
90            init_connection_window_size: sz.into(),
91            ..self
92        }
93    }
94
95    /// Sets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option for HTTP2
96    /// connections.
97    ///
98    /// Default is 200, matching hyper's hardened default. Passing `None`
99    /// removes the limit entirely and advertises unlimited concurrent
100    /// streams to the peer; this makes the server vulnerable to
101    /// Rapid-Reset-style resource exhaustion and should be an explicit,
102    /// deliberate choice.
103    ///
104    /// [spec]: https://httpwg.org/specs/rfc9113.html#n-stream-concurrency
105    pub fn max_concurrent_streams(self, max: impl Into<Option<u32>>) -> Self {
106        Self {
107            max_concurrent_streams: max.into(),
108            ..self
109        }
110    }
111
112    /// Sets the maximum time option in milliseconds that a connection may exist
113    ///
114    /// When a connection reaches its maximum age it is shut down
115    /// gracefully: for HTTP/2 a GOAWAY is sent, and in-flight requests are
116    /// allowed to complete. See [`Config::max_connection_age_grace`] for
117    /// bounding how long that completion may take.
118    ///
119    /// Default is no limit (`None`).
120    pub fn max_connection_age(self, max_connection_age: Duration) -> Self {
121        Self {
122            max_connection_age: Some(max_connection_age),
123            ..self
124        }
125    }
126
127    /// Sets the grace period allowed after a graceful shutdown of a
128    /// connection is initiated before the connection is forcefully closed.
129    ///
130    /// The grace period applies however the graceful shutdown was
131    /// triggered: [`Config::max_connection_age`] expiring,
132    /// `ConnectionInfo::close`, or `ServerHandle::trigger_shutdown`.
133    ///
134    /// A graceful shutdown waits for in-flight requests to complete, but a
135    /// stream that can make no progress -- for example, a response wedged
136    /// behind HTTP/2 flow-control windows that a stalled or vanished peer
137    /// never reopens -- would keep the connection alive forever. Once the
138    /// grace period expires the connection is dropped along with any
139    /// streams still in flight, following the semantics of grpc-go's
140    /// `MAX_CONNECTION_AGE_GRACE`. This is the only server-side mechanism
141    /// that reclaims send-stalled streams: middleware cannot do it because
142    /// the response body is no longer polled once the stream stalls.
143    ///
144    /// Default is an unlimited grace period (`None`): the connection stays
145    /// open until every in-flight request completes.
146    pub fn max_connection_age_grace(self, max_connection_age_grace: Duration) -> Self {
147        Self {
148            max_connection_age_grace: Some(max_connection_age_grace),
149            ..self
150        }
151    }
152
153    /// Set whether HTTP2 Ping frames are enabled on accepted connections.
154    ///
155    /// If `None` is specified, HTTP2 keepalive is disabled, otherwise the duration
156    /// specified will be the time interval between HTTP2 Ping frames.
157    /// The timeout for receiving an acknowledgement of the keepalive ping
158    /// can be set with [`Config::http2_keepalive_timeout`].
159    ///
160    /// Default is no HTTP2 keepalive (`None`)
161    pub fn http2_keepalive_interval(self, http2_keepalive_interval: Option<Duration>) -> Self {
162        Self {
163            http2_keepalive_interval,
164            ..self
165        }
166    }
167
168    /// Sets a timeout for receiving an acknowledgement of the keepalive ping.
169    ///
170    /// If the ping is not acknowledged within the timeout, the connection will be closed.
171    /// Does nothing if http2_keep_alive_interval is disabled.
172    ///
173    /// Default is 20 seconds.
174    pub fn http2_keepalive_timeout(self, http2_keepalive_timeout: Option<Duration>) -> Self {
175        Self {
176            http2_keepalive_timeout,
177            ..self
178        }
179    }
180
181    /// Sets whether to use an adaptive flow control. Defaults to false.
182    /// Enabling this will override the limits set in http2_initial_stream_window_size and
183    /// http2_initial_connection_window_size.
184    ///
185    /// Warning: enabling this resets both receive windows to the HTTP/2
186    /// spec default of 65,535 bytes until BDP probing ramps them back up.
187    /// Until then the whole connection has a single stalled stream's worth
188    /// of window, so one slow reader can starve every other stream on the
189    /// connection. For multiplexed streaming workloads this measurably
190    /// underperforms the static defaults; prefer setting explicit window
191    /// sizes instead.
192    pub fn http2_adaptive_window(self, enabled: Option<bool>) -> Self {
193        Self {
194            http2_adaptive_window: enabled,
195            ..self
196        }
197    }
198
199    /// Configures the maximum number of pending reset streams allowed before a GOAWAY will be sent.
200    ///
201    /// This will default to whatever the default in h2 is. As of v0.3.17, it is 20.
202    ///
203    /// See <https://github.com/hyperium/hyper/issues/2877> for more information.
204    pub fn http2_max_pending_accept_reset_streams(self, max: Option<usize>) -> Self {
205        Self {
206            http2_max_pending_accept_reset_streams: max,
207            ..self
208        }
209    }
210
211    /// Set whether TCP keepalive messages are enabled on accepted connections.
212    ///
213    /// If `None` is specified, keepalive is disabled, otherwise the duration
214    /// specified will be the time to remain idle before sending TCP keepalive
215    /// probes.
216    ///
217    /// Default is no keepalive (`None`)
218    pub fn tcp_keepalive(self, tcp_keepalive: Option<Duration>) -> Self {
219        Self {
220            tcp_keepalive,
221            ..self
222        }
223    }
224
225    /// Set the value of `TCP_NODELAY` option for accepted connections. Enabled by default.
226    pub fn tcp_nodelay(self, enabled: bool) -> Self {
227        Self {
228            tcp_nodelay: enabled,
229            ..self
230        }
231    }
232
233    /// Sets the max size of received header frames.
234    ///
235    /// This will default to whatever the default in hyper is. As of v1.4.1, it is 16 KiB.
236    pub fn http2_max_header_list_size(self, max: impl Into<Option<u32>>) -> Self {
237        Self {
238            http2_max_header_list_size: max.into(),
239            ..self
240        }
241    }
242
243    /// Sets the maximum frame size to use for HTTP2.
244    ///
245    /// Passing `None` will do nothing.
246    ///
247    /// If not set, will default from underlying transport.
248    pub fn max_frame_size(self, frame_size: impl Into<Option<u32>>) -> Self {
249        Self {
250            max_frame_size: frame_size.into(),
251            ..self
252        }
253    }
254
255    /// Sets a timeout for receiving the complete header block of an HTTP/1
256    /// request.
257    ///
258    /// If a client does not transmit its entire header block within this
259    /// duration the connection is closed. This is the defense against
260    /// slowloris-style attacks, where clients hold sockets open
261    /// indefinitely by sending partial requests. Pass `None` to disable
262    /// the timeout.
263    ///
264    /// Has no effect on HTTP/2 connections, whose liveness is covered by
265    /// [`Config::http2_keepalive_interval`].
266    ///
267    /// Default is 30 seconds, matching hyper.
268    pub fn http1_header_read_timeout(self, timeout: Option<Duration>) -> Self {
269        Self {
270            http1_header_read_timeout: timeout,
271            ..self
272        }
273    }
274
275    /// Allow this accepting http1 requests.
276    ///
277    /// When `false`, plain-text connections are served in HTTP/2-only
278    /// (prior knowledge) mode: the protocol sniff is skipped and anything
279    /// that is not an HTTP/2 preface is rejected at the transport level.
280    /// TLS connections additionally stop advertising `http/1.1` via ALPN.
281    /// hyper's HTTP/1 upgrade mechanism is unavailable in this mode;
282    /// HTTP/2 extended CONNECT is unaffected.
283    ///
284    /// Default is `true`.
285    pub fn accept_http1(self, accept_http1: bool) -> Self {
286        Config {
287            accept_http1,
288            ..self
289        }
290    }
291
292    /// Sets the timeout for TLS handshakes on incoming connections.
293    ///
294    /// Connections that do not complete the TLS handshake within this duration are dropped.
295    ///
296    /// Default is 5 seconds.
297    pub fn tls_handshake_timeout(self, timeout: Duration) -> Self {
298        Config {
299            tls_handshake_timeout: timeout,
300            ..self
301        }
302    }
303
304    /// Sets the maximum number of pending TLS handshakes.
305    ///
306    /// When this limit is reached, new incoming connections are dropped until existing
307    /// handshakes complete or time out.
308    ///
309    /// Default is 4096.
310    pub fn max_pending_connections(self, max: usize) -> Self {
311        Config {
312            max_pending_connections: max,
313            ..self
314        }
315    }
316
317    pub(crate) fn connection_builder(
318        &self,
319    ) -> hyper_util::server::conn::auto::Builder<hyper_util::rt::TokioExecutor> {
320        let mut builder =
321            hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
322
323        if !self.accept_http1 {
324            builder = builder.http2_only();
325        }
326
327        if self.enable_connect_protocol {
328            builder.http2().enable_connect_protocol();
329        }
330
331        let http2_keepalive_timeout = self
332            .http2_keepalive_timeout
333            .unwrap_or_else(|| Duration::new(DEFAULT_HTTP2_KEEPALIVE_TIMEOUT_SECS, 0));
334
335        // The timer is required for the header read timeout to take
336        // effect: hyper silently disables its defaulted timeout when no
337        // timer is set.
338        builder
339            .http1()
340            .timer(hyper_util::rt::TokioTimer::new())
341            .header_read_timeout(self.http1_header_read_timeout);
342
343        builder
344            .http2()
345            .timer(hyper_util::rt::TokioTimer::new())
346            .initial_connection_window_size(self.init_connection_window_size)
347            .initial_stream_window_size(self.init_stream_window_size)
348            .max_concurrent_streams(self.max_concurrent_streams)
349            .keep_alive_interval(self.http2_keepalive_interval)
350            .keep_alive_timeout(http2_keepalive_timeout)
351            .adaptive_window(self.http2_adaptive_window.unwrap_or_default())
352            .max_pending_accept_reset_streams(self.http2_max_pending_accept_reset_streams)
353            .max_frame_size(self.max_frame_size);
354
355        if let Some(max_header_list_size) = self.http2_max_header_list_size {
356            builder.http2().max_header_list_size(max_header_list_size);
357        }
358
359        builder
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    /// These defaults are security- and availability-relevant: hyper treats
368    /// an explicit `None` for `max_concurrent_streams` as "remove the
369    /// limit", so defaulting the field to `None` silently erased hyper's
370    /// hardened 200-stream default. Pin them so they cannot regress.
371    #[test]
372    fn default_advertises_a_concurrent_stream_limit() {
373        let config = Config::default();
374        assert_eq!(config.max_concurrent_streams, Some(200));
375    }
376
377    /// The header read timeout is the slowloris defense for HTTP/1
378    /// connections; pin the default so it cannot silently regress to
379    /// disabled.
380    #[test]
381    fn default_enables_http1_header_read_timeout() {
382        let config = Config::default();
383        assert_eq!(
384            config.http1_header_read_timeout,
385            Some(Duration::from_secs(30))
386        );
387    }
388}