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}