Skip to content

Commit 1134539

Browse files
authored
feat(client): add option to allow misplaced spaces in HTTP/1 responses (#2506)
1 parent ed2fdb7 commit 1134539

File tree

7 files changed

+117
-5
lines changed

7 files changed

+117
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ futures-util = { version = "0.3", default-features = false }
3030
http = "0.2"
3131
http-body = "0.4"
3232
httpdate = "1.0"
33-
httparse = "1.0"
33+
httparse = "1.4"
3434
h2 = { version = "0.3", optional = true }
3535
itoa = "0.4.1"
3636
tracing = { version = "0.1", default-features = false, features = ["std"] }

src/client/client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,31 @@ impl Builder {
961961
self
962962
}
963963

964+
/// Set whether HTTP/1 connections will accept spaces between header names
965+
/// and the colon that follow them in responses.
966+
///
967+
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
968+
/// to say about it:
969+
///
970+
/// > No whitespace is allowed between the header field-name and colon. In
971+
/// > the past, differences in the handling of such whitespace have led to
972+
/// > security vulnerabilities in request routing and response handling. A
973+
/// > server MUST reject any received request message that contains
974+
/// > whitespace between a header field-name and colon with a response code
975+
/// > of 400 (Bad Request). A proxy MUST remove any such whitespace from a
976+
/// > response message before forwarding the message downstream.
977+
///
978+
/// Note that this setting does not affect HTTP/2.
979+
///
980+
/// Default is false.
981+
///
982+
/// [RFC 7230 Section 3.2.4.]: https://tools.ietf.org/html/rfc7230#section-3.2.4
983+
pub fn http1_allow_spaces_after_header_name_in_responses(&mut self, val: bool) -> &mut Self {
984+
self.conn_builder
985+
.h1_allow_spaces_after_header_name_in_responses(val);
986+
self
987+
}
988+
964989
/// Set whether HTTP/1 connections will write header names as title case at
965990
/// the socket level.
966991
///

src/client/conn.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ use std::time::Duration;
5656

5757
use bytes::Bytes;
5858
use futures_util::future::{self, Either, FutureExt as _};
59+
use httparse::ParserConfig;
5960
use pin_project::pin_project;
6061
use tokio::io::{AsyncRead, AsyncWrite};
6162
use tower_service::Service;
@@ -123,6 +124,7 @@ where
123124
pub struct Builder {
124125
pub(super) exec: Exec,
125126
h09_responses: bool,
127+
h1_parser_config: ParserConfig,
126128
h1_title_case_headers: bool,
127129
h1_read_buf_exact_size: Option<usize>,
128130
h1_max_buf_size: Option<usize>,
@@ -496,6 +498,7 @@ impl Builder {
496498
exec: Exec::Default,
497499
h09_responses: false,
498500
h1_read_buf_exact_size: None,
501+
h1_parser_config: Default::default(),
499502
h1_title_case_headers: false,
500503
h1_max_buf_size: None,
501504
#[cfg(feature = "http2")]
@@ -521,6 +524,14 @@ impl Builder {
521524
self
522525
}
523526

527+
pub(crate) fn h1_allow_spaces_after_header_name_in_responses(
528+
&mut self,
529+
enabled: bool,
530+
) -> &mut Builder {
531+
self.h1_parser_config.allow_spaces_after_header_name_in_responses(enabled);
532+
self
533+
}
534+
524535
pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder {
525536
self.h1_title_case_headers = enabled;
526537
self
@@ -704,6 +715,7 @@ impl Builder {
704715
#[cfg(feature = "http1")]
705716
Proto::Http1 => {
706717
let mut conn = proto::Conn::new(io);
718+
conn.set_h1_parser_config(opts.h1_parser_config);
707719
if opts.h1_title_case_headers {
708720
conn.set_title_case_headers();
709721
}

src/proto/h1/conn.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::marker::PhantomData;
55
use bytes::{Buf, Bytes};
66
use http::header::{HeaderValue, CONNECTION};
77
use http::{HeaderMap, Method, Version};
8+
use httparse::ParserConfig;
89
use tokio::io::{AsyncRead, AsyncWrite};
910

1011
use super::io::Buffered;
@@ -44,6 +45,7 @@ where
4445
error: None,
4546
keep_alive: KA::Busy,
4647
method: None,
48+
h1_parser_config: ParserConfig::default(),
4749
#[cfg(feature = "ffi")]
4850
preserve_header_case: false,
4951
title_case_headers: false,
@@ -79,6 +81,11 @@ where
7981
self.state.title_case_headers = true;
8082
}
8183

84+
#[cfg(feature = "client")]
85+
pub(crate) fn set_h1_parser_config(&mut self, parser_config: ParserConfig) {
86+
self.state.h1_parser_config = parser_config;
87+
}
88+
8289
#[cfg(feature = "client")]
8390
pub(crate) fn set_h09_responses(&mut self) {
8491
self.state.h09_responses = true;
@@ -150,6 +157,7 @@ where
150157
ParseContext {
151158
cached_headers: &mut self.state.cached_headers,
152159
req_method: &mut self.state.method,
160+
h1_parser_config: self.state.h1_parser_config.clone(),
153161
#[cfg(feature = "ffi")]
154162
preserve_header_case: self.state.preserve_header_case,
155163
h09_responses: self.state.h09_responses,
@@ -284,7 +292,10 @@ where
284292
ret
285293
}
286294

287-
pub(crate) fn poll_read_keep_alive(&mut self, cx: &mut task::Context<'_>) -> Poll<crate::Result<()>> {
295+
pub(crate) fn poll_read_keep_alive(
296+
&mut self,
297+
cx: &mut task::Context<'_>,
298+
) -> Poll<crate::Result<()>> {
288299
debug_assert!(!self.can_read_head() && !self.can_read_body());
289300

290301
if self.is_read_closed() {
@@ -760,6 +771,7 @@ struct State {
760771
/// This is used to know things such as if the message can include
761772
/// a body or not.
762773
method: Option<Method>,
774+
h1_parser_config: ParserConfig,
763775
#[cfg(feature = "ffi")]
764776
preserve_header_case: bool,
765777
title_case_headers: bool,

src/proto/h1/io.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ where
159159
ParseContext {
160160
cached_headers: parse_ctx.cached_headers,
161161
req_method: parse_ctx.req_method,
162+
h1_parser_config: parse_ctx.h1_parser_config.clone(),
162163
#[cfg(feature = "ffi")]
163164
preserve_header_case: parse_ctx.preserve_header_case,
164165
h09_responses: parse_ctx.h09_responses,
@@ -183,7 +184,10 @@ where
183184
}
184185
}
185186

186-
pub(crate) fn poll_read_from_io(&mut self, cx: &mut task::Context<'_>) -> Poll<io::Result<usize>> {
187+
pub(crate) fn poll_read_from_io(
188+
&mut self,
189+
cx: &mut task::Context<'_>,
190+
) -> Poll<io::Result<usize>> {
187191
self.read_blocked = false;
188192
let next = self.read_buf_strategy.next();
189193
if self.read_buf_remaining_mut() < next {
@@ -378,7 +382,7 @@ impl ReadStrategy {
378382
*decrease_now = false;
379383
}
380384
}
381-
},
385+
}
382386
#[cfg(feature = "client")]
383387
ReadStrategy::Exact(_) => (),
384388
}
@@ -639,6 +643,7 @@ mod tests {
639643
let parse_ctx = ParseContext {
640644
cached_headers: &mut None,
641645
req_method: &mut None,
646+
h1_parser_config: Default::default(),
642647
#[cfg(feature = "ffi")]
643648
preserve_header_case: false,
644649
h09_responses: false,

src/proto/h1/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bytes::BytesMut;
22
use http::{HeaderMap, Method};
3+
use httparse::ParserConfig;
34

45
use crate::body::DecodedLength;
56
use crate::proto::{BodyLength, MessageHead};
@@ -70,6 +71,7 @@ pub(crate) struct ParsedMessage<T> {
7071
pub(crate) struct ParseContext<'a> {
7172
cached_headers: &'a mut Option<HeaderMap>,
7273
req_method: &'a mut Option<Method>,
74+
h1_parser_config: ParserConfig,
7375
#[cfg(feature = "ffi")]
7476
preserve_header_case: bool,
7577
h09_responses: bool,

src/proto/h1/role.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,8 @@ impl Http1Transaction for Client {
683683
);
684684
let mut res = httparse::Response::new(&mut headers);
685685
let bytes = buf.as_ref();
686-
match res.parse(bytes) {
686+
match ctx.h1_parser_config.parse_response(&mut res, bytes)
687+
{
687688
Ok(httparse::Status::Complete(len)) => {
688689
trace!("Response.parse Complete({})", len);
689690
let status = StatusCode::from_u16(res.code.unwrap())?;
@@ -1231,6 +1232,7 @@ mod tests {
12311232
ParseContext {
12321233
cached_headers: &mut None,
12331234
req_method: &mut method,
1235+
h1_parser_config: Default::default(),
12341236
#[cfg(feature = "ffi")]
12351237
preserve_header_case: false,
12361238
h09_responses: false,
@@ -1254,6 +1256,7 @@ mod tests {
12541256
let ctx = ParseContext {
12551257
cached_headers: &mut None,
12561258
req_method: &mut Some(crate::Method::GET),
1259+
h1_parser_config: Default::default(),
12571260
#[cfg(feature = "ffi")]
12581261
preserve_header_case: false,
12591262
h09_responses: false,
@@ -1272,6 +1275,7 @@ mod tests {
12721275
let ctx = ParseContext {
12731276
cached_headers: &mut None,
12741277
req_method: &mut None,
1278+
h1_parser_config: Default::default(),
12751279
#[cfg(feature = "ffi")]
12761280
preserve_header_case: false,
12771281
h09_responses: false,
@@ -1288,6 +1292,7 @@ mod tests {
12881292
let ctx = ParseContext {
12891293
cached_headers: &mut None,
12901294
req_method: &mut Some(crate::Method::GET),
1295+
h1_parser_config: Default::default(),
12911296
#[cfg(feature = "ffi")]
12921297
preserve_header_case: false,
12931298
h09_responses: true,
@@ -1306,6 +1311,7 @@ mod tests {
13061311
let ctx = ParseContext {
13071312
cached_headers: &mut None,
13081313
req_method: &mut Some(crate::Method::GET),
1314+
h1_parser_config: Default::default(),
13091315
#[cfg(feature = "ffi")]
13101316
preserve_header_case: false,
13111317
h09_responses: false,
@@ -1314,6 +1320,48 @@ mod tests {
13141320
assert_eq!(raw, H09_RESPONSE);
13151321
}
13161322

1323+
const RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON: &'static str =
1324+
"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Credentials : true\r\n\r\n";
1325+
1326+
#[test]
1327+
fn test_parse_allow_response_with_spaces_before_colons() {
1328+
use httparse::ParserConfig;
1329+
1330+
let _ = pretty_env_logger::try_init();
1331+
let mut raw = BytesMut::from(RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON);
1332+
let mut h1_parser_config = ParserConfig::default();
1333+
h1_parser_config.allow_spaces_after_header_name_in_responses(true);
1334+
let ctx = ParseContext {
1335+
cached_headers: &mut None,
1336+
req_method: &mut Some(crate::Method::GET),
1337+
h1_parser_config,
1338+
#[cfg(feature = "ffi")]
1339+
preserve_header_case: false,
1340+
h09_responses: false,
1341+
};
1342+
let msg = Client::parse(&mut raw, ctx).unwrap().unwrap();
1343+
assert_eq!(raw.len(), 0);
1344+
assert_eq!(msg.head.subject, crate::StatusCode::OK);
1345+
assert_eq!(msg.head.version, crate::Version::HTTP_11);
1346+
assert_eq!(msg.head.headers.len(), 1);
1347+
assert_eq!(msg.head.headers["Access-Control-Allow-Credentials"], "true");
1348+
}
1349+
1350+
#[test]
1351+
fn test_parse_reject_response_with_spaces_before_colons() {
1352+
let _ = pretty_env_logger::try_init();
1353+
let mut raw = BytesMut::from(RESPONSE_WITH_WHITESPACE_BETWEEN_HEADER_NAME_AND_COLON);
1354+
let ctx = ParseContext {
1355+
cached_headers: &mut None,
1356+
req_method: &mut Some(crate::Method::GET),
1357+
h1_parser_config: Default::default(),
1358+
#[cfg(feature = "ffi")]
1359+
preserve_header_case: false,
1360+
h09_responses: false,
1361+
};
1362+
Client::parse(&mut raw, ctx).unwrap_err();
1363+
}
1364+
13171365
#[test]
13181366
fn test_decoder_request() {
13191367
fn parse(s: &str) -> ParsedMessage<RequestLine> {
@@ -1323,6 +1371,7 @@ mod tests {
13231371
ParseContext {
13241372
cached_headers: &mut None,
13251373
req_method: &mut None,
1374+
h1_parser_config: Default::default(),
13261375
#[cfg(feature = "ffi")]
13271376
preserve_header_case: false,
13281377
h09_responses: false,
@@ -1339,6 +1388,7 @@ mod tests {
13391388
ParseContext {
13401389
cached_headers: &mut None,
13411390
req_method: &mut None,
1391+
h1_parser_config: Default::default(),
13421392
#[cfg(feature = "ffi")]
13431393
preserve_header_case: false,
13441394
h09_responses: false,
@@ -1554,6 +1604,7 @@ mod tests {
15541604
ParseContext {
15551605
cached_headers: &mut None,
15561606
req_method: &mut Some(Method::GET),
1607+
h1_parser_config: Default::default(),
15571608
#[cfg(feature = "ffi")]
15581609
preserve_header_case: false,
15591610
h09_responses: false,
@@ -1570,6 +1621,7 @@ mod tests {
15701621
ParseContext {
15711622
cached_headers: &mut None,
15721623
req_method: &mut Some(m),
1624+
h1_parser_config: Default::default(),
15731625
#[cfg(feature = "ffi")]
15741626
preserve_header_case: false,
15751627
h09_responses: false,
@@ -1586,6 +1638,7 @@ mod tests {
15861638
ParseContext {
15871639
cached_headers: &mut None,
15881640
req_method: &mut Some(Method::GET),
1641+
h1_parser_config: Default::default(),
15891642
#[cfg(feature = "ffi")]
15901643
preserve_header_case: false,
15911644
h09_responses: false,
@@ -1902,6 +1955,7 @@ mod tests {
19021955
ParseContext {
19031956
cached_headers: &mut None,
19041957
req_method: &mut Some(Method::GET),
1958+
h1_parser_config: Default::default(),
19051959
#[cfg(feature = "ffi")]
19061960
preserve_header_case: false,
19071961
h09_responses: false,
@@ -1984,6 +2038,7 @@ mod tests {
19842038
ParseContext {
19852039
cached_headers: &mut headers,
19862040
req_method: &mut None,
2041+
h1_parser_config: Default::default(),
19872042
#[cfg(feature = "ffi")]
19882043
preserve_header_case: false,
19892044
h09_responses: false,
@@ -2020,6 +2075,7 @@ mod tests {
20202075
ParseContext {
20212076
cached_headers: &mut headers,
20222077
req_method: &mut None,
2078+
h1_parser_config: Default::default(),
20232079
#[cfg(feature = "ffi")]
20242080
preserve_header_case: false,
20252081
h09_responses: false,

0 commit comments

Comments
 (0)