Skip to content

Commit 930dc08

Browse files
committed
feat(server): add h1 idle_timeout
1 parent 67a4a49 commit 930dc08

File tree

5 files changed

+224
-37
lines changed

5 files changed

+224
-37
lines changed

src/error.rs

+10
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ pub(super) enum Kind {
6464
/// User took too long to send headers
6565
#[cfg(all(feature = "http1", feature = "server"))]
6666
HeaderTimeout,
67+
/// User took too long to send another request
68+
#[cfg(all(feature = "http1", feature = "server"))]
69+
IdleTimeout,
6770
/// Error while reading a body from connection.
6871
#[cfg(all(
6972
any(feature = "client", feature = "server"),
@@ -360,6 +363,11 @@ impl Error {
360363
Error::new(Kind::HeaderTimeout)
361364
}
362365

366+
#[cfg(all(feature = "http1", feature = "server"))]
367+
pub(super) fn new_idle_timeout() -> Error {
368+
Error::new(Kind::IdleTimeout)
369+
}
370+
363371
#[cfg(feature = "http1")]
364372
#[cfg(feature = "server")]
365373
pub(super) fn new_user_unsupported_status_code() -> Error {
@@ -458,6 +466,8 @@ impl Error {
458466
Kind::Canceled => "operation was canceled",
459467
#[cfg(all(feature = "http1", feature = "server"))]
460468
Kind::HeaderTimeout => "read header from client timeout",
469+
#[cfg(all(feature = "http1", feature = "server"))]
470+
Kind::IdleTimeout => "idle client timeout",
461471
#[cfg(all(
462472
any(feature = "client", feature = "server"),
463473
any(feature = "http1", feature = "http2")

src/proto/h1/conn.rs

+86-31
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ where
6161
#[cfg(feature = "server")]
6262
h1_header_read_timeout: None,
6363
#[cfg(feature = "server")]
64-
h1_header_read_timeout_fut: None,
65-
#[cfg(feature = "server")]
6664
h1_header_read_timeout_running: false,
6765
#[cfg(feature = "server")]
66+
h1_idle_timeout: None,
67+
#[cfg(feature = "server")]
68+
h1_idle_timeout_running: false,
69+
#[cfg(feature = "server")]
70+
h1_timeout_fut: None,
71+
#[cfg(feature = "server")]
6872
date_header: true,
6973
#[cfg(feature = "server")]
7074
timer: Time::Empty,
@@ -147,6 +151,11 @@ where
147151
self.state.h1_header_read_timeout = Some(val);
148152
}
149153

154+
#[cfg(feature = "server")]
155+
pub(crate) fn set_http1_idle_timeout(&mut self, val: Duration) {
156+
self.state.h1_idle_timeout = Some(val);
157+
}
158+
150159
#[cfg(feature = "server")]
151160
pub(crate) fn set_allow_half_close(&mut self) {
152161
self.state.allow_half_close = true;
@@ -217,24 +226,10 @@ where
217226
trace!("Conn::read_head");
218227

219228
#[cfg(feature = "server")]
220-
if !self.state.h1_header_read_timeout_running {
221-
if let Some(h1_header_read_timeout) = self.state.h1_header_read_timeout {
222-
let deadline = Instant::now() + h1_header_read_timeout;
223-
self.state.h1_header_read_timeout_running = true;
224-
match self.state.h1_header_read_timeout_fut {
225-
Some(ref mut h1_header_read_timeout_fut) => {
226-
trace!("resetting h1 header read timeout timer");
227-
self.state.timer.reset(h1_header_read_timeout_fut, deadline);
228-
}
229-
None => {
230-
trace!("setting h1 header read timeout timer");
231-
self.state.h1_header_read_timeout_fut =
232-
Some(self.state.timer.sleep_until(deadline));
233-
}
234-
}
235-
}
236-
}
229+
let first_head = !self.io.is_read_blocked();
237230

231+
#[cfg_attr(not(feature = "server"), allow(unused))]
232+
let mut progress = false;
238233
let msg = match self.io.parse::<T>(
239234
cx,
240235
ParseContext {
@@ -249,20 +244,69 @@ where
249244
#[cfg(feature = "ffi")]
250245
on_informational: &mut self.state.on_informational,
251246
},
247+
&mut progress,
252248
) {
253249
Poll::Ready(Ok(msg)) => msg,
254250
Poll::Ready(Err(e)) => return self.on_read_head_error(e),
255251
Poll::Pending => {
252+
// - Use the read timeout on the first head to avoid common DoS.
253+
// - If made progress in reading header, must no longer be idle.
256254
#[cfg(feature = "server")]
257-
if self.state.h1_header_read_timeout_running {
258-
if let Some(ref mut h1_header_read_timeout_fut) =
259-
self.state.h1_header_read_timeout_fut
260-
{
261-
if Pin::new(h1_header_read_timeout_fut).poll(cx).is_ready() {
262-
self.state.h1_header_read_timeout_running = false;
263-
264-
warn!("read header from client timeout");
265-
return Poll::Ready(Some(Err(crate::Error::new_header_timeout())));
255+
if first_head || progress {
256+
if !self.state.h1_header_read_timeout_running {
257+
if let Some(h1_header_read_timeout) = self.state.h1_header_read_timeout {
258+
let deadline = Instant::now() + h1_header_read_timeout;
259+
self.state.h1_idle_timeout_running = false;
260+
self.state.h1_header_read_timeout_running = true;
261+
match self.state.h1_timeout_fut {
262+
Some(ref mut ht_timeout_fut) => {
263+
trace!("resetting h1 timeout timer for header read");
264+
self.state.timer.reset(ht_timeout_fut, deadline);
265+
}
266+
None => {
267+
trace!("setting h1 timeout timer for header read");
268+
self.state.h1_timeout_fut =
269+
Some(self.state.timer.sleep_until(deadline));
270+
}
271+
}
272+
} else if std::mem::take(&mut self.state.h1_idle_timeout_running) {
273+
trace!("unsetting h1 timeout timer for idle");
274+
self.state.h1_timeout_fut = None;
275+
}
276+
}
277+
} else if !self.state.h1_header_read_timeout_running
278+
&& !self.state.h1_idle_timeout_running
279+
{
280+
if let Some(h1_idle_timeout) = self.state.h1_idle_timeout {
281+
let deadline = Instant::now() + h1_idle_timeout;
282+
self.state.h1_idle_timeout_running = true;
283+
match self.state.h1_timeout_fut {
284+
Some(ref mut h1_timeout_fut) => {
285+
trace!("resetting h1 timeout timer for idle");
286+
self.state.timer.reset(h1_timeout_fut, deadline);
287+
}
288+
None => {
289+
trace!("setting h1 timeout timer for idle");
290+
self.state.h1_timeout_fut =
291+
Some(self.state.timer.sleep_until(deadline));
292+
}
293+
}
294+
}
295+
}
296+
297+
#[cfg(feature = "server")]
298+
if self.state.h1_header_read_timeout_running || self.state.h1_idle_timeout_running {
299+
if let Some(ref mut h1_timeout_fut) = self.state.h1_timeout_fut {
300+
if Pin::new(h1_timeout_fut).poll(cx).is_ready() {
301+
return Poll::Ready(Some(Err(
302+
if self.state.h1_header_read_timeout_running {
303+
warn!("read header from client timeout");
304+
crate::Error::new_header_timeout()
305+
} else {
306+
warn!("idle client timeout");
307+
crate::Error::new_idle_timeout()
308+
},
309+
)));
266310
}
267311
}
268312
}
@@ -274,7 +318,8 @@ where
274318
#[cfg(feature = "server")]
275319
{
276320
self.state.h1_header_read_timeout_running = false;
277-
self.state.h1_header_read_timeout_fut = None;
321+
self.state.h1_idle_timeout_running = false;
322+
self.state.h1_timeout_fut = None;
278323
}
279324

280325
// Note: don't deconstruct `msg` into local variables, it appears
@@ -919,10 +964,14 @@ struct State {
919964
#[cfg(feature = "server")]
920965
h1_header_read_timeout: Option<Duration>,
921966
#[cfg(feature = "server")]
922-
h1_header_read_timeout_fut: Option<Pin<Box<dyn Sleep>>>,
923-
#[cfg(feature = "server")]
924967
h1_header_read_timeout_running: bool,
925968
#[cfg(feature = "server")]
969+
h1_idle_timeout: Option<Duration>,
970+
#[cfg(feature = "server")]
971+
h1_idle_timeout_running: bool,
972+
#[cfg(feature = "server")]
973+
h1_timeout_fut: Option<Pin<Box<dyn Sleep>>>,
974+
#[cfg(feature = "server")]
926975
date_header: bool,
927976
#[cfg(feature = "server")]
928977
timer: Time,
@@ -1106,6 +1155,12 @@ impl State {
11061155
self.reading = Reading::Init;
11071156
self.writing = Writing::Init;
11081157

1158+
#[cfg(feature = "server")]
1159+
if self.h1_idle_timeout.is_some() {
1160+
// Next read will start and poll the idle timeout.
1161+
self.notify_read = true;
1162+
}
1163+
11091164
// !T::should_read_first() means Client.
11101165
//
11111166
// If Client connection has just gone idle, the Dispatcher

src/proto/h1/io.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ where
169169
&mut self,
170170
cx: &mut Context<'_>,
171171
parse_ctx: ParseContext<'_>,
172+
progress: &mut bool,
172173
) -> Poll<crate::Result<ParsedMessage<S::Incoming>>>
173174
where
174175
S: Http1Transaction,
@@ -205,6 +206,7 @@ where
205206
trace!("parse eof");
206207
return Poll::Ready(Err(crate::Error::new_incomplete()));
207208
}
209+
*progress = true;
208210
}
209211
}
210212

@@ -702,7 +704,7 @@ mod tests {
702704
on_informational: &mut None,
703705
};
704706
assert!(buffered
705-
.parse::<ClientTransaction>(cx, parse_ctx)
707+
.parse::<ClientTransaction>(cx, parse_ctx, &mut false)
706708
.is_pending());
707709
Poll::Ready(())
708710
})

src/server/conn/http1.rs

+20
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub struct Builder {
7676
h1_preserve_header_case: bool,
7777
h1_max_headers: Option<usize>,
7878
h1_header_read_timeout: Dur,
79+
h1_idle_timeout: Dur,
7980
h1_writev: Option<bool>,
8081
max_buf_size: Option<usize>,
8182
pipeline_flush: bool,
@@ -238,6 +239,7 @@ impl Builder {
238239
h1_preserve_header_case: false,
239240
h1_max_headers: None,
240241
h1_header_read_timeout: Dur::Default(Some(Duration::from_secs(30))),
242+
h1_idle_timeout: Dur::Default(Some(Duration::from_secs(120))),
241243
h1_writev: None,
242244
max_buf_size: None,
243245
pipeline_flush: false,
@@ -322,6 +324,21 @@ impl Builder {
322324
self
323325
}
324326

327+
/// Set a timeout for idle time between requests. If a client does not
328+
/// transmit another request within this time after receiving the last
329+
/// response, the connection is closed.
330+
///
331+
/// Requires a [`Timer`] set by [`Builder::timer`] to take effect. Panics if `idle_timeout` is configured
332+
/// without a [`Timer`].
333+
///
334+
/// Pass `None` to disable.
335+
///
336+
/// Default is 120 seconds.
337+
pub fn idle_timeout(&mut self, idle_timeout: impl Into<Option<Duration>>) -> &mut Self {
338+
self.h1_idle_timeout = Dur::Configured(idle_timeout.into());
339+
self
340+
}
341+
325342
/// Set whether HTTP/1 connections should try to use vectored writes,
326343
/// or always flatten into a single buffer.
327344
///
@@ -448,6 +465,9 @@ impl Builder {
448465
{
449466
conn.set_http1_header_read_timeout(dur);
450467
};
468+
if let Some(dur) = self.timer.check(self.h1_idle_timeout, "idle_timeout") {
469+
conn.set_http1_idle_timeout(dur);
470+
};
451471
if let Some(writev) = self.h1_writev {
452472
if writev {
453473
conn.set_write_strategy_queue();

0 commit comments

Comments
 (0)