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}