diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs index c6b464d2d3..5f08982436 100644 --- a/src/proto/h1/conn.rs +++ b/src/proto/h1/conn.rs @@ -62,6 +62,8 @@ where #[cfg(feature = "server")] h1_header_read_timeout_running: false, #[cfg(feature = "server")] + date_header: true, + #[cfg(feature = "server")] timer: Time::Empty, preserve_header_case: false, #[cfg(feature = "ffi")] @@ -564,6 +566,8 @@ where keep_alive: self.state.wants_keep_alive(), req_method: &mut self.state.method, title_case_headers: self.state.title_case_headers, + #[cfg(feature = "server")] + date_header: self.state.date_header, }, buf, ) { @@ -859,6 +863,8 @@ struct State { #[cfg(feature = "server")] h1_header_read_timeout_running: bool, #[cfg(feature = "server")] + date_header: bool, + #[cfg(feature = "server")] timer: Time, preserve_header_case: bool, #[cfg(feature = "ffi")] diff --git a/src/proto/h1/mod.rs b/src/proto/h1/mod.rs index 5b9872cddb..074d0b88a8 100644 --- a/src/proto/h1/mod.rs +++ b/src/proto/h1/mod.rs @@ -103,6 +103,8 @@ pub(crate) struct Encode<'a, T> { keep_alive: bool, req_method: &'a mut Option, title_case_headers: bool, + #[cfg(feature = "server")] + date_header: bool, } /// Extra flags that a request "wants", like expect-continue or upgrades. diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 958340775a..f2c1b73084 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -934,7 +934,8 @@ impl Server { } // cached date is much faster than formatting every request - if !wrote_date { + // don't force the write if disabled + if !wrote_date && msg.date_header { dst.reserve(date::DATE_VALUE_LENGTH + 8); header_name_writer.write_header_name_with_colon(dst, "date: ", header::DATE); date::extend(dst); @@ -2503,6 +2504,7 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: true, + date_header: true, }, &mut vec, ) @@ -2534,6 +2536,7 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: false, + date_header: true, }, &mut vec, ) @@ -2568,6 +2571,7 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: true, + date_header: true, }, &mut vec, ) @@ -2592,6 +2596,7 @@ mod tests { keep_alive: true, req_method: &mut Some(Method::CONNECT), title_case_headers: false, + date_header: true, }, &mut vec, ) @@ -2621,6 +2626,7 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: true, + date_header: true, }, &mut vec, ) @@ -2655,6 +2661,7 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: false, + date_header: true, }, &mut vec, ) @@ -2689,17 +2696,54 @@ mod tests { keep_alive: true, req_method: &mut None, title_case_headers: true, + date_header: true, }, &mut vec, ) .unwrap(); + // this will also test that the date does exist let expected_response = b"HTTP/1.1 200 OK\r\nCONTENT-LENGTH: 10\r\nContent-Type: application/json\r\nDate: "; assert_eq!(&vec[..expected_response.len()], &expected_response[..]); } + #[test] + fn test_disabled_date_header() { + use crate::proto::BodyLength; + use http::header::{HeaderValue, CONTENT_LENGTH}; + + let mut head = MessageHead::default(); + head.headers + .insert("content-length", HeaderValue::from_static("10")); + head.headers + .insert("content-type", HeaderValue::from_static("application/json")); + + let mut orig_headers = HeaderCaseMap::default(); + orig_headers.insert(CONTENT_LENGTH, "CONTENT-LENGTH".into()); + head.extensions.insert(orig_headers); + + let mut vec = Vec::new(); + Server::encode( + Encode { + head: &mut head, + body: Some(BodyLength::Known(10)), + keep_alive: true, + req_method: &mut None, + title_case_headers: true, + date_header: false, + }, + &mut vec, + ) + .unwrap(); + + let expected_response = + b"HTTP/1.1 200 OK\r\nCONTENT-LENGTH: 10\r\nContent-Type: application/json\r\n\r\n"; + + assert_eq!(&vec, &expected_response); + } + #[test] fn parse_header_htabs() { let mut bytes = BytesMut::from("HTTP/1.1 200 OK\r\nserver: hello\tworld\r\n\r\n"); @@ -3037,6 +3081,7 @@ mod tests { keep_alive: true, req_method: &mut Some(Method::GET), title_case_headers: false, + date_header: true, }, &mut vec, ) @@ -3065,6 +3110,7 @@ mod tests { keep_alive: true, req_method: &mut Some(Method::GET), title_case_headers: false, + date_header: true, }, &mut vec, ) diff --git a/src/proto/h2/server.rs b/src/proto/h2/server.rs index 3ccb8a9c99..ee2d08eaaf 100644 --- a/src/proto/h2/server.rs +++ b/src/proto/h2/server.rs @@ -55,6 +55,7 @@ pub(crate) struct Config { pub(crate) keep_alive_timeout: Duration, pub(crate) max_send_buffer_size: usize, pub(crate) max_header_list_size: u32, + pub(crate) date_header: bool, } impl Default for Config { @@ -72,6 +73,7 @@ impl Default for Config { keep_alive_timeout: Duration::from_secs(20), max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE, max_header_list_size: DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE, + date_header: true, } } } @@ -86,6 +88,7 @@ pin_project! { timer: Time, service: S, state: State, + date_header: bool, } } @@ -108,6 +111,7 @@ where ping: Option<(ping::Recorder, ping::Ponger)>, conn: Connection, SendBuf>, closing: Option, + date_header: bool, } impl Server @@ -167,6 +171,7 @@ where hs: handshake, }, service, + date_header: config.date_header, } } @@ -219,6 +224,7 @@ where ping, conn, closing: None, + date_header: me.date_header, }) } State::Serving(ref mut srv) => { @@ -302,7 +308,13 @@ where req.extensions_mut().insert(Protocol::from_inner(protocol)); } - let fut = H2Stream::new(service.call(req), connect_parts, respond); + let fut = H2Stream::new( + service.call(req), + connect_parts, + respond, + self.date_header, + ); + exec.execute_h2stream(fut); } Some(Err(e)) => { @@ -357,6 +369,7 @@ pin_project! { reply: SendResponse>, #[pin] state: H2StreamState, + date_header: bool, } } @@ -392,10 +405,12 @@ where fut: F, connect_parts: Option, respond: SendResponse>, + date_header: bool, ) -> H2Stream { H2Stream { reply: respond, state: H2StreamState::Service { fut, connect_parts }, + date_header, } } } @@ -454,10 +469,12 @@ where let mut res = ::http::Response::from_parts(head, ()); super::strip_connection_headers(res.headers_mut(), false); - // set Date header if it isn't already set... - res.headers_mut() - .entry(::http::header::DATE) - .or_insert_with(date::update_and_header_value); + // set Date header if it isn't already set if instructed + if *me.date_header { + res.headers_mut() + .entry(::http::header::DATE) + .or_insert_with(date::update_and_header_value); + } if let Some(connect_parts) = connect_parts.take() { if res.status().is_success() { diff --git a/src/server/conn/http1.rs b/src/server/conn/http1.rs index 5f07df1dfd..cf22316296 100644 --- a/src/server/conn/http1.rs +++ b/src/server/conn/http1.rs @@ -79,6 +79,7 @@ pub struct Builder { h1_writev: Option, max_buf_size: Option, pipeline_flush: bool, + date_header: bool, } /// Deconstructed parts of a `Connection`. @@ -246,6 +247,7 @@ impl Builder { h1_writev: None, max_buf_size: None, pipeline_flush: false, + date_header: true, } } /// Set whether HTTP/1 connections should support half-closures. @@ -359,6 +361,16 @@ impl Builder { self } + /// Set whether the `date` header should be included in HTTP responses. + /// + /// Note that including the `date` header is recommended by RFC 7231. + /// + /// Default is true. + pub fn auto_date_header(&mut self, enabled: bool) -> &mut Self { + self.date_header = enabled; + self + } + /// Aggregates flushes to better support pipelined responses. /// /// Experimental, may have bugs. diff --git a/src/server/conn/http2.rs b/src/server/conn/http2.rs index bc754f2668..7094386755 100644 --- a/src/server/conn/http2.rs +++ b/src/server/conn/http2.rs @@ -277,6 +277,16 @@ impl Builder { self } + /// Set whether the `date` header should be included in HTTP responses. + /// + /// Note that including the `date` header is recommended by RFC 7231. + /// + /// Default is true. + pub fn auto_date_header(&mut self, enabled: bool) -> &mut Self { + self.h2_builder.date_header = enabled; + self + } + /// Bind a connection together with a [`Service`](crate::service::Service). /// /// This returns a Future that must be polled in order for HTTP to be diff --git a/tests/server.rs b/tests/server.rs index 661b98d180..133cde963f 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -2529,6 +2529,7 @@ async fn http2_keep_alive_detects_unresponsive_client() { .timer(TokioTimer) .keep_alive_interval(Duration::from_secs(1)) .keep_alive_timeout(Duration::from_secs(1)) + .auto_date_header(true) .serve_connection(socket, unreachable_service()) .await .expect_err("serve_connection should error"); @@ -2569,6 +2570,42 @@ async fn http2_keep_alive_with_responsive_client() { client.send_request(req).await.expect("client.send_request"); } +#[tokio::test] +async fn http2_check_date_header_disabled() { + let (listener, addr) = setup_tcp_listener(); + + tokio::spawn(async move { + let (socket, _) = listener.accept().await.expect("accept"); + let socket = TokioIo::new(socket); + + http2::Builder::new(TokioExecutor) + .timer(TokioTimer) + .keep_alive_interval(Duration::from_secs(1)) + .auto_date_header(false) + .keep_alive_timeout(Duration::from_secs(1)) + .serve_connection(socket, HelloWorld) + .await + .expect("serve_connection"); + }); + + let tcp = TokioIo::new(connect_async(addr).await); + let (mut client, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor) + .handshake(tcp) + .await + .expect("http handshake"); + + tokio::spawn(async move { + conn.await.expect("client conn"); + }); + + TokioTimer.sleep(Duration::from_secs(4)).await; + + let req = http::Request::new(Empty::::new()); + let resp = client.send_request(req).await.expect("client.send_request"); + + assert!(resp.headers().get("Date").is_none()); +} + fn is_ping_frame(buf: &[u8]) -> bool { buf[3] == 6 }