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