feat(client): add option to allow misplaced spaces in HTTP/1 responses (#2506)

This commit is contained in:
Anthony Ramine
2021-04-20 23:17:48 +02:00
committed by GitHub
parent ed2fdb7b6a
commit 11345394d9
7 changed files with 117 additions and 5 deletions

View File

@@ -30,7 +30,7 @@ futures-util = { version = "0.3", default-features = false }
http = "0.2" http = "0.2"
http-body = "0.4" http-body = "0.4"
httpdate = "1.0" httpdate = "1.0"
httparse = "1.0" httparse = "1.4"
h2 = { version = "0.3", optional = true } h2 = { version = "0.3", optional = true }
itoa = "0.4.1" itoa = "0.4.1"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }

View File

@@ -961,6 +961,31 @@ impl Builder {
self 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 /// Set whether HTTP/1 connections will write header names as title case at
/// the socket level. /// the socket level.
/// ///

View File

@@ -56,6 +56,7 @@ use std::time::Duration;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{self, Either, FutureExt as _}; use futures_util::future::{self, Either, FutureExt as _};
use httparse::ParserConfig;
use pin_project::pin_project; use pin_project::pin_project;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service; use tower_service::Service;
@@ -123,6 +124,7 @@ where
pub struct Builder { pub struct Builder {
pub(super) exec: Exec, pub(super) exec: Exec,
h09_responses: bool, h09_responses: bool,
h1_parser_config: ParserConfig,
h1_title_case_headers: bool, h1_title_case_headers: bool,
h1_read_buf_exact_size: Option<usize>, h1_read_buf_exact_size: Option<usize>,
h1_max_buf_size: Option<usize>, h1_max_buf_size: Option<usize>,
@@ -496,6 +498,7 @@ impl Builder {
exec: Exec::Default, exec: Exec::Default,
h09_responses: false, h09_responses: false,
h1_read_buf_exact_size: None, h1_read_buf_exact_size: None,
h1_parser_config: Default::default(),
h1_title_case_headers: false, h1_title_case_headers: false,
h1_max_buf_size: None, h1_max_buf_size: None,
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
@@ -521,6 +524,14 @@ impl Builder {
self 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 { pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder {
self.h1_title_case_headers = enabled; self.h1_title_case_headers = enabled;
self self
@@ -704,6 +715,7 @@ impl Builder {
#[cfg(feature = "http1")] #[cfg(feature = "http1")]
Proto::Http1 => { Proto::Http1 => {
let mut conn = proto::Conn::new(io); let mut conn = proto::Conn::new(io);
conn.set_h1_parser_config(opts.h1_parser_config);
if opts.h1_title_case_headers { if opts.h1_title_case_headers {
conn.set_title_case_headers(); conn.set_title_case_headers();
} }

View File

@@ -5,6 +5,7 @@ use std::marker::PhantomData;
use bytes::{Buf, Bytes}; use bytes::{Buf, Bytes};
use http::header::{HeaderValue, CONNECTION}; use http::header::{HeaderValue, CONNECTION};
use http::{HeaderMap, Method, Version}; use http::{HeaderMap, Method, Version};
use httparse::ParserConfig;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use super::io::Buffered; use super::io::Buffered;
@@ -44,6 +45,7 @@ where
error: None, error: None,
keep_alive: KA::Busy, keep_alive: KA::Busy,
method: None, method: None,
h1_parser_config: ParserConfig::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
title_case_headers: false, title_case_headers: false,
@@ -79,6 +81,11 @@ where
self.state.title_case_headers = true; 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")] #[cfg(feature = "client")]
pub(crate) fn set_h09_responses(&mut self) { pub(crate) fn set_h09_responses(&mut self) {
self.state.h09_responses = true; self.state.h09_responses = true;
@@ -150,6 +157,7 @@ where
ParseContext { ParseContext {
cached_headers: &mut self.state.cached_headers, cached_headers: &mut self.state.cached_headers,
req_method: &mut self.state.method, req_method: &mut self.state.method,
h1_parser_config: self.state.h1_parser_config.clone(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: self.state.preserve_header_case, preserve_header_case: self.state.preserve_header_case,
h09_responses: self.state.h09_responses, h09_responses: self.state.h09_responses,
@@ -284,7 +292,10 @@ where
ret 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()); debug_assert!(!self.can_read_head() && !self.can_read_body());
if self.is_read_closed() { if self.is_read_closed() {
@@ -760,6 +771,7 @@ struct State {
/// This is used to know things such as if the message can include /// This is used to know things such as if the message can include
/// a body or not. /// a body or not.
method: Option<Method>, method: Option<Method>,
h1_parser_config: ParserConfig,
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: bool, preserve_header_case: bool,
title_case_headers: bool, title_case_headers: bool,

View File

@@ -159,6 +159,7 @@ where
ParseContext { ParseContext {
cached_headers: parse_ctx.cached_headers, cached_headers: parse_ctx.cached_headers,
req_method: parse_ctx.req_method, req_method: parse_ctx.req_method,
h1_parser_config: parse_ctx.h1_parser_config.clone(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: parse_ctx.preserve_header_case, preserve_header_case: parse_ctx.preserve_header_case,
h09_responses: parse_ctx.h09_responses, 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; self.read_blocked = false;
let next = self.read_buf_strategy.next(); let next = self.read_buf_strategy.next();
if self.read_buf_remaining_mut() < next { if self.read_buf_remaining_mut() < next {
@@ -378,7 +382,7 @@ impl ReadStrategy {
*decrease_now = false; *decrease_now = false;
} }
} }
}, }
#[cfg(feature = "client")] #[cfg(feature = "client")]
ReadStrategy::Exact(_) => (), ReadStrategy::Exact(_) => (),
} }
@@ -639,6 +643,7 @@ mod tests {
let parse_ctx = ParseContext { let parse_ctx = ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,

View File

@@ -1,5 +1,6 @@
use bytes::BytesMut; use bytes::BytesMut;
use http::{HeaderMap, Method}; use http::{HeaderMap, Method};
use httparse::ParserConfig;
use crate::body::DecodedLength; use crate::body::DecodedLength;
use crate::proto::{BodyLength, MessageHead}; use crate::proto::{BodyLength, MessageHead};
@@ -70,6 +71,7 @@ pub(crate) struct ParsedMessage<T> {
pub(crate) struct ParseContext<'a> { pub(crate) struct ParseContext<'a> {
cached_headers: &'a mut Option<HeaderMap>, cached_headers: &'a mut Option<HeaderMap>,
req_method: &'a mut Option<Method>, req_method: &'a mut Option<Method>,
h1_parser_config: ParserConfig,
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: bool, preserve_header_case: bool,
h09_responses: bool, h09_responses: bool,

View File

@@ -683,7 +683,8 @@ impl Http1Transaction for Client {
); );
let mut res = httparse::Response::new(&mut headers); let mut res = httparse::Response::new(&mut headers);
let bytes = buf.as_ref(); let bytes = buf.as_ref();
match res.parse(bytes) { match ctx.h1_parser_config.parse_response(&mut res, bytes)
{
Ok(httparse::Status::Complete(len)) => { Ok(httparse::Status::Complete(len)) => {
trace!("Response.parse Complete({})", len); trace!("Response.parse Complete({})", len);
let status = StatusCode::from_u16(res.code.unwrap())?; let status = StatusCode::from_u16(res.code.unwrap())?;
@@ -1231,6 +1232,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut method, req_method: &mut method,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1254,6 +1256,7 @@ mod tests {
let ctx = ParseContext { let ctx = ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(crate::Method::GET), req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1272,6 +1275,7 @@ mod tests {
let ctx = ParseContext { let ctx = ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1288,6 +1292,7 @@ mod tests {
let ctx = ParseContext { let ctx = ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(crate::Method::GET), req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: true, h09_responses: true,
@@ -1306,6 +1311,7 @@ mod tests {
let ctx = ParseContext { let ctx = ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(crate::Method::GET), req_method: &mut Some(crate::Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1314,6 +1320,48 @@ mod tests {
assert_eq!(raw, H09_RESPONSE); 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] #[test]
fn test_decoder_request() { fn test_decoder_request() {
fn parse(s: &str) -> ParsedMessage<RequestLine> { fn parse(s: &str) -> ParsedMessage<RequestLine> {
@@ -1323,6 +1371,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1339,6 +1388,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1554,6 +1604,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(Method::GET), req_method: &mut Some(Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1570,6 +1621,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(m), req_method: &mut Some(m),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1586,6 +1638,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(Method::GET), req_method: &mut Some(Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1902,6 +1955,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut None, cached_headers: &mut None,
req_method: &mut Some(Method::GET), req_method: &mut Some(Method::GET),
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -1984,6 +2038,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut headers, cached_headers: &mut headers,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,
@@ -2020,6 +2075,7 @@ mod tests {
ParseContext { ParseContext {
cached_headers: &mut headers, cached_headers: &mut headers,
req_method: &mut None, req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")] #[cfg(feature = "ffi")]
preserve_header_case: false, preserve_header_case: false,
h09_responses: false, h09_responses: false,