feat(client): add option to allow misplaced spaces in HTTP/1 responses (#2506)
This commit is contained in:
		| @@ -961,6 +961,31 @@ impl Builder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set whether HTTP/1 connections will accept spaces between header names | ||||
|     /// and the colon that follow them in responses. | ||||
|     /// | ||||
|     /// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has | ||||
|     /// to say about it: | ||||
|     /// | ||||
|     /// > No whitespace is allowed between the header field-name and colon. In | ||||
|     /// > the past, differences in the handling of such whitespace have led to | ||||
|     /// > security vulnerabilities in request routing and response handling. A | ||||
|     /// > server MUST reject any received request message that contains | ||||
|     /// > whitespace between a header field-name and colon with a response code | ||||
|     /// > of 400 (Bad Request). A proxy MUST remove any such whitespace from a | ||||
|     /// > response message before forwarding the message downstream. | ||||
|     /// | ||||
|     /// Note that this setting does not affect HTTP/2. | ||||
|     /// | ||||
|     /// Default is false. | ||||
|     /// | ||||
|     /// [RFC 7230 Section 3.2.4.]: https://tools.ietf.org/html/rfc7230#section-3.2.4 | ||||
|     pub fn http1_allow_spaces_after_header_name_in_responses(&mut self, val: bool) -> &mut Self { | ||||
|         self.conn_builder | ||||
|             .h1_allow_spaces_after_header_name_in_responses(val); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set whether HTTP/1 connections will write header names as title case at | ||||
|     /// the socket level. | ||||
|     /// | ||||
|   | ||||
| @@ -56,6 +56,7 @@ use std::time::Duration; | ||||
|  | ||||
| use bytes::Bytes; | ||||
| use futures_util::future::{self, Either, FutureExt as _}; | ||||
| use httparse::ParserConfig; | ||||
| use pin_project::pin_project; | ||||
| use tokio::io::{AsyncRead, AsyncWrite}; | ||||
| use tower_service::Service; | ||||
| @@ -123,6 +124,7 @@ where | ||||
| pub struct Builder { | ||||
|     pub(super) exec: Exec, | ||||
|     h09_responses: bool, | ||||
|     h1_parser_config: ParserConfig, | ||||
|     h1_title_case_headers: bool, | ||||
|     h1_read_buf_exact_size: Option<usize>, | ||||
|     h1_max_buf_size: Option<usize>, | ||||
| @@ -496,6 +498,7 @@ impl Builder { | ||||
|             exec: Exec::Default, | ||||
|             h09_responses: false, | ||||
|             h1_read_buf_exact_size: None, | ||||
|             h1_parser_config: Default::default(), | ||||
|             h1_title_case_headers: false, | ||||
|             h1_max_buf_size: None, | ||||
|             #[cfg(feature = "http2")] | ||||
| @@ -521,6 +524,14 @@ impl Builder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn h1_allow_spaces_after_header_name_in_responses( | ||||
|         &mut self, | ||||
|         enabled: bool, | ||||
|     ) -> &mut Builder { | ||||
|         self.h1_parser_config.allow_spaces_after_header_name_in_responses(enabled); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder { | ||||
|         self.h1_title_case_headers = enabled; | ||||
|         self | ||||
| @@ -704,6 +715,7 @@ impl Builder { | ||||
|                 #[cfg(feature = "http1")] | ||||
|                 Proto::Http1 => { | ||||
|                     let mut conn = proto::Conn::new(io); | ||||
|                     conn.set_h1_parser_config(opts.h1_parser_config); | ||||
|                     if opts.h1_title_case_headers { | ||||
|                         conn.set_title_case_headers(); | ||||
|                     } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ use std::marker::PhantomData; | ||||
| use bytes::{Buf, Bytes}; | ||||
| use http::header::{HeaderValue, CONNECTION}; | ||||
| use http::{HeaderMap, Method, Version}; | ||||
| use httparse::ParserConfig; | ||||
| use tokio::io::{AsyncRead, AsyncWrite}; | ||||
|  | ||||
| use super::io::Buffered; | ||||
| @@ -44,6 +45,7 @@ where | ||||
|                 error: None, | ||||
|                 keep_alive: KA::Busy, | ||||
|                 method: None, | ||||
|                 h1_parser_config: ParserConfig::default(), | ||||
|                 #[cfg(feature = "ffi")] | ||||
|                 preserve_header_case: false, | ||||
|                 title_case_headers: false, | ||||
| @@ -79,6 +81,11 @@ where | ||||
|         self.state.title_case_headers = true; | ||||
|     } | ||||
|  | ||||
|     #[cfg(feature = "client")] | ||||
|     pub(crate) fn set_h1_parser_config(&mut self, parser_config: ParserConfig) { | ||||
|         self.state.h1_parser_config = parser_config; | ||||
|     } | ||||
|  | ||||
|     #[cfg(feature = "client")] | ||||
|     pub(crate) fn set_h09_responses(&mut self) { | ||||
|         self.state.h09_responses = true; | ||||
| @@ -150,6 +157,7 @@ where | ||||
|             ParseContext { | ||||
|                 cached_headers: &mut self.state.cached_headers, | ||||
|                 req_method: &mut self.state.method, | ||||
|                 h1_parser_config: self.state.h1_parser_config.clone(), | ||||
|                 #[cfg(feature = "ffi")] | ||||
|                 preserve_header_case: self.state.preserve_header_case, | ||||
|                 h09_responses: self.state.h09_responses, | ||||
| @@ -284,7 +292,10 @@ where | ||||
|         ret | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn poll_read_keep_alive(&mut self, cx: &mut task::Context<'_>) -> Poll<crate::Result<()>> { | ||||
|     pub(crate) fn poll_read_keep_alive( | ||||
|         &mut self, | ||||
|         cx: &mut task::Context<'_>, | ||||
|     ) -> Poll<crate::Result<()>> { | ||||
|         debug_assert!(!self.can_read_head() && !self.can_read_body()); | ||||
|  | ||||
|         if self.is_read_closed() { | ||||
| @@ -760,6 +771,7 @@ struct State { | ||||
|     /// This is used to know things such as if the message can include | ||||
|     /// a body or not. | ||||
|     method: Option<Method>, | ||||
|     h1_parser_config: ParserConfig, | ||||
|     #[cfg(feature = "ffi")] | ||||
|     preserve_header_case: bool, | ||||
|     title_case_headers: bool, | ||||
|   | ||||
| @@ -159,6 +159,7 @@ where | ||||
|                 ParseContext { | ||||
|                     cached_headers: parse_ctx.cached_headers, | ||||
|                     req_method: parse_ctx.req_method, | ||||
|                     h1_parser_config: parse_ctx.h1_parser_config.clone(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: parse_ctx.preserve_header_case, | ||||
|                     h09_responses: parse_ctx.h09_responses, | ||||
| @@ -183,7 +184,10 @@ where | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn poll_read_from_io(&mut self, cx: &mut task::Context<'_>) -> Poll<io::Result<usize>> { | ||||
|     pub(crate) fn poll_read_from_io( | ||||
|         &mut self, | ||||
|         cx: &mut task::Context<'_>, | ||||
|     ) -> Poll<io::Result<usize>> { | ||||
|         self.read_blocked = false; | ||||
|         let next = self.read_buf_strategy.next(); | ||||
|         if self.read_buf_remaining_mut() < next { | ||||
| @@ -378,7 +382,7 @@ impl ReadStrategy { | ||||
|                         *decrease_now = false; | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             } | ||||
|             #[cfg(feature = "client")] | ||||
|             ReadStrategy::Exact(_) => (), | ||||
|         } | ||||
| @@ -639,6 +643,7 @@ mod tests { | ||||
|             let parse_ctx = ParseContext { | ||||
|                 cached_headers: &mut None, | ||||
|                 req_method: &mut None, | ||||
|                 h1_parser_config: Default::default(), | ||||
|                 #[cfg(feature = "ffi")] | ||||
|                 preserve_header_case: false, | ||||
|                 h09_responses: false, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use bytes::BytesMut; | ||||
| use http::{HeaderMap, Method}; | ||||
| use httparse::ParserConfig; | ||||
|  | ||||
| use crate::body::DecodedLength; | ||||
| use crate::proto::{BodyLength, MessageHead}; | ||||
| @@ -70,6 +71,7 @@ pub(crate) struct ParsedMessage<T> { | ||||
| pub(crate) struct ParseContext<'a> { | ||||
|     cached_headers: &'a mut Option<HeaderMap>, | ||||
|     req_method: &'a mut Option<Method>, | ||||
|     h1_parser_config: ParserConfig, | ||||
|     #[cfg(feature = "ffi")] | ||||
|     preserve_header_case: bool, | ||||
|     h09_responses: bool, | ||||
|   | ||||
| @@ -683,7 +683,8 @@ impl Http1Transaction for Client { | ||||
|                 ); | ||||
|                 let mut res = httparse::Response::new(&mut headers); | ||||
|                 let bytes = buf.as_ref(); | ||||
|                 match res.parse(bytes) { | ||||
|                 match ctx.h1_parser_config.parse_response(&mut res, bytes) | ||||
|                 { | ||||
|                     Ok(httparse::Status::Complete(len)) => { | ||||
|                         trace!("Response.parse Complete({})", len); | ||||
|                         let status = StatusCode::from_u16(res.code.unwrap())?; | ||||
| @@ -1231,6 +1232,7 @@ mod tests { | ||||
|             ParseContext { | ||||
|                 cached_headers: &mut None, | ||||
|                 req_method: &mut method, | ||||
|                 h1_parser_config: Default::default(), | ||||
|                 #[cfg(feature = "ffi")] | ||||
|                 preserve_header_case: false, | ||||
|                 h09_responses: false, | ||||
| @@ -1254,6 +1256,7 @@ mod tests { | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut Some(crate::Method::GET), | ||||
|             h1_parser_config: Default::default(), | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: false, | ||||
| @@ -1272,6 +1275,7 @@ mod tests { | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut None, | ||||
|             h1_parser_config: Default::default(), | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: false, | ||||
| @@ -1288,6 +1292,7 @@ mod tests { | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut Some(crate::Method::GET), | ||||
|             h1_parser_config: Default::default(), | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: true, | ||||
| @@ -1306,6 +1311,7 @@ mod tests { | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut Some(crate::Method::GET), | ||||
|             h1_parser_config: Default::default(), | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: false, | ||||
| @@ -1314,6 +1320,48 @@ mod tests { | ||||
|         assert_eq!(raw, H09_RESPONSE); | ||||
|     } | ||||
|  | ||||
|     const RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON: &'static str = | ||||
|         "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials : true\r\n\r\n"; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_parse_allow_response_with_spaces_before_colons() { | ||||
|         use httparse::ParserConfig; | ||||
|  | ||||
|         let _ = pretty_env_logger::try_init(); | ||||
|         let mut raw = BytesMut::from(RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON); | ||||
|         let mut h1_parser_config = ParserConfig::default(); | ||||
|         h1_parser_config.allow_spaces_after_header_name_in_responses(true); | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut Some(crate::Method::GET), | ||||
|             h1_parser_config, | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: false, | ||||
|         }; | ||||
|         let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); | ||||
|         assert_eq!(raw.len(), 0); | ||||
|         assert_eq!(msg.head.subject, crate::StatusCode::OK); | ||||
|         assert_eq!(msg.head.version, crate::Version::HTTP_11); | ||||
|         assert_eq!(msg.head.headers.len(), 1); | ||||
|         assert_eq!(msg.head.headers["Access-Control-Allow-Credentials"], "true"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_parse_reject_response_with_spaces_before_colons() { | ||||
|         let _ = pretty_env_logger::try_init(); | ||||
|         let mut raw = BytesMut::from(RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON); | ||||
|         let ctx = ParseContext { | ||||
|             cached_headers: &mut None, | ||||
|             req_method: &mut Some(crate::Method::GET), | ||||
|             h1_parser_config: Default::default(), | ||||
|             #[cfg(feature = "ffi")] | ||||
|             preserve_header_case: false, | ||||
|             h09_responses: false, | ||||
|         }; | ||||
|         Client::parse(&mut raw, ctx).unwrap_err(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_decoder_request() { | ||||
|         fn parse(s: &str) -> ParsedMessage<RequestLine> { | ||||
| @@ -1323,6 +1371,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut None, | ||||
|                     req_method: &mut None, | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -1339,6 +1388,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut None, | ||||
|                     req_method: &mut None, | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -1554,6 +1604,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut None, | ||||
|                     req_method: &mut Some(Method::GET), | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -1570,6 +1621,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut None, | ||||
|                     req_method: &mut Some(m), | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -1586,6 +1638,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut None, | ||||
|                     req_method: &mut Some(Method::GET), | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -1902,6 +1955,7 @@ mod tests { | ||||
|             ParseContext { | ||||
|                 cached_headers: &mut None, | ||||
|                 req_method: &mut Some(Method::GET), | ||||
|                 h1_parser_config: Default::default(), | ||||
|                 #[cfg(feature = "ffi")] | ||||
|                 preserve_header_case: false, | ||||
|                 h09_responses: false, | ||||
| @@ -1984,6 +2038,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut headers, | ||||
|                     req_method: &mut None, | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
| @@ -2020,6 +2075,7 @@ mod tests { | ||||
|                 ParseContext { | ||||
|                     cached_headers: &mut headers, | ||||
|                     req_method: &mut None, | ||||
|                     h1_parser_config: Default::default(), | ||||
|                     #[cfg(feature = "ffi")] | ||||
|                     preserve_header_case: false, | ||||
|                     h09_responses: false, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user