Skip to content

Commit 1f0c177

Browse files
authoredFeb 9, 2022
feat(http1): implement obsolete line folding (#2734)
The client now has an option to allow parsing responses with obsolete line folding in headers. The option is off by default, since the spec recommends to reject such things if you can.
1 parent 5ec094c commit 1f0c177

File tree

5 files changed

+156
-2
lines changed

5 files changed

+156
-2
lines changed
 

‎Cargo.toml

+1-1
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.5.1"
33+
httparse = "1.6"
3434
h2 = { version = "0.3.9", optional = true }
3535
itoa = "1"
3636
tracing = { version = "0.1", default-features = false, features = ["std"] }

‎src/client/client.rs

+40
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,9 @@ impl Builder {
10001000
/// Set whether HTTP/1 connections will accept spaces between header names
10011001
/// and the colon that follow them in responses.
10021002
///
1003+
/// Newline codepoints (`\r` and `\n`) will be transformed to spaces when
1004+
/// parsing.
1005+
///
10031006
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
10041007
/// to say about it:
10051008
///
@@ -1022,6 +1025,43 @@ impl Builder {
10221025
self
10231026
}
10241027

1028+
/// Set whether HTTP/1 connections will accept obsolete line folding for
1029+
/// header values.
1030+
///
1031+
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
1032+
/// to say about it:
1033+
///
1034+
/// > A server that receives an obs-fold in a request message that is not
1035+
/// > within a message/http container MUST either reject the message by
1036+
/// > sending a 400 (Bad Request), preferably with a representation
1037+
/// > explaining that obsolete line folding is unacceptable, or replace
1038+
/// > each received obs-fold with one or more SP octets prior to
1039+
/// > interpreting the field value or forwarding the message downstream.
1040+
///
1041+
/// > A proxy or gateway that receives an obs-fold in a response message
1042+
/// > that is not within a message/http container MUST either discard the
1043+
/// > message and replace it with a 502 (Bad Gateway) response, preferably
1044+
/// > with a representation explaining that unacceptable line folding was
1045+
/// > received, or replace each received obs-fold with one or more SP
1046+
/// > octets prior to interpreting the field value or forwarding the
1047+
/// > message downstream.
1048+
///
1049+
/// > A user agent that receives an obs-fold in a response message that is
1050+
/// > not within a message/http container MUST replace each received
1051+
/// > obs-fold with one or more SP octets prior to interpreting the field
1052+
/// > value.
1053+
///
1054+
/// Note that this setting does not affect HTTP/2.
1055+
///
1056+
/// Default is false.
1057+
///
1058+
/// [RFC 7230 Section 3.2.4.]: https://tools.ietf.org/html/rfc7230#section-3.2.4
1059+
pub fn http1_allow_obsolete_multiline_headers_in_responses(&mut self, val: bool) -> &mut Self {
1060+
self.conn_builder
1061+
.http1_allow_obsolete_multiline_headers_in_responses(val);
1062+
self
1063+
}
1064+
10251065
/// Set whether HTTP/1 connections should try to use vectored writes,
10261066
/// or always flatten into a single buffer.
10271067
///

‎src/client/conn.rs

+43
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,49 @@ impl Builder {
615615
self
616616
}
617617

618+
/// Set whether HTTP/1 connections will accept obsolete line folding for
619+
/// header values.
620+
///
621+
/// Newline codepoints (`\r` and `\n`) will be transformed to spaces when
622+
/// parsing.
623+
///
624+
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
625+
/// to say about it:
626+
///
627+
/// > A server that receives an obs-fold in a request message that is not
628+
/// > within a message/http container MUST either reject the message by
629+
/// > sending a 400 (Bad Request), preferably with a representation
630+
/// > explaining that obsolete line folding is unacceptable, or replace
631+
/// > each received obs-fold with one or more SP octets prior to
632+
/// > interpreting the field value or forwarding the message downstream.
633+
///
634+
/// > A proxy or gateway that receives an obs-fold in a response message
635+
/// > that is not within a message/http container MUST either discard the
636+
/// > message and replace it with a 502 (Bad Gateway) response, preferably
637+
/// > with a representation explaining that unacceptable line folding was
638+
/// > received, or replace each received obs-fold with one or more SP
639+
/// > octets prior to interpreting the field value or forwarding the
640+
/// > message downstream.
641+
///
642+
/// > A user agent that receives an obs-fold in a response message that is
643+
/// > not within a message/http container MUST replace each received
644+
/// > obs-fold with one or more SP octets prior to interpreting the field
645+
/// > value.
646+
///
647+
/// Note that this setting does not affect HTTP/2.
648+
///
649+
/// Default is false.
650+
///
651+
/// [RFC 7230 Section 3.2.4.]: https://tools.ietf.org/html/rfc7230#section-3.2.4
652+
pub fn http1_allow_obsolete_multiline_headers_in_responses(
653+
&mut self,
654+
enabled: bool,
655+
) -> &mut Builder {
656+
self.h1_parser_config
657+
.allow_obsolete_multiline_headers_in_responses(enabled);
658+
self
659+
}
660+
618661
/// Set whether HTTP/1 connections should try to use vectored writes,
619662
/// or always flatten into a single buffer.
620663
///

‎src/proto/h1/role.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,21 @@ impl Http1Transaction for Client {
955955
}
956956
};
957957

958-
let slice = buf.split_to(len).freeze();
958+
let mut slice = buf.split_to(len);
959+
960+
if ctx.h1_parser_config.obsolete_multiline_headers_in_responses_are_allowed() {
961+
for header in &headers_indices[..headers_len] {
962+
// SAFETY: array is valid up to `headers_len`
963+
let header = unsafe { &*header.as_ptr() };
964+
for b in &mut slice[header.value.0..header.value.1] {
965+
if *b == b'\r' || *b == b'\n' {
966+
*b = b' ';
967+
}
968+
}
969+
}
970+
}
971+
972+
let slice = slice.freeze();
959973

960974
let mut headers = ctx.cached_headers.take().unwrap_or_else(HeaderMap::new);
961975

‎tests/client.rs

+57
Original file line numberDiff line numberDiff line change
@@ -2214,6 +2214,63 @@ mod conn {
22142214
future::join(server, client).await;
22152215
}
22162216

2217+
#[tokio::test]
2218+
async fn get_obsolete_line_folding() {
2219+
let _ = ::pretty_env_logger::try_init();
2220+
let listener = TkTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
2221+
.await
2222+
.unwrap();
2223+
let addr = listener.local_addr().unwrap();
2224+
2225+
let server = async move {
2226+
let mut sock = listener.accept().await.unwrap().0;
2227+
let mut buf = [0; 4096];
2228+
let n = sock.read(&mut buf).await.expect("read 1");
2229+
2230+
// Notably:
2231+
// - Just a path, since just a path was set
2232+
// - No host, since no host was set
2233+
let expected = "GET /a HTTP/1.1\r\n\r\n";
2234+
assert_eq!(s(&buf[..n]), expected);
2235+
2236+
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: \r\n 0\r\nLine-Folded-Header: hello\r\n world \r\n \r\n\r\n")
2237+
.await
2238+
.unwrap();
2239+
};
2240+
2241+
let client = async move {
2242+
let tcp = tcp_connect(&addr).await.expect("connect");
2243+
let (mut client, conn) = conn::Builder::new()
2244+
.http1_allow_obsolete_multiline_headers_in_responses(true)
2245+
.handshake::<_, Body>(tcp)
2246+
.await
2247+
.expect("handshake");
2248+
2249+
tokio::task::spawn(async move {
2250+
conn.await.expect("http conn");
2251+
});
2252+
2253+
let req = Request::builder()
2254+
.uri("/a")
2255+
.body(Default::default())
2256+
.unwrap();
2257+
let mut res = client.send_request(req).await.expect("send_request");
2258+
assert_eq!(res.status(), hyper::StatusCode::OK);
2259+
assert_eq!(res.headers().len(), 2);
2260+
assert_eq!(
2261+
res.headers().get(http::header::CONTENT_LENGTH).unwrap(),
2262+
"0"
2263+
);
2264+
assert_eq!(
2265+
res.headers().get("line-folded-header").unwrap(),
2266+
"hello world"
2267+
);
2268+
assert!(res.body_mut().next().await.is_none());
2269+
};
2270+
2271+
future::join(server, client).await;
2272+
}
2273+
22172274
#[test]
22182275
fn incoming_content_length() {
22192276
use hyper::body::HttpBody;

0 commit comments

Comments
 (0)
Please sign in to comment.