Skip to content

Commit

Permalink
feat(http1): implement obsolete line folding (#2734)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nox committed Feb 9, 2022
1 parent 5ec094c commit 1f0c177
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -30,7 +30,7 @@ futures-util = { version = "0.3", default-features = false }
http = "0.2"
http-body = "0.4"
httpdate = "1.0"
httparse = "1.5.1"
httparse = "1.6"
h2 = { version = "0.3.9", optional = true }
itoa = "1"
tracing = { version = "0.1", default-features = false, features = ["std"] }
Expand Down
40 changes: 40 additions & 0 deletions src/client/client.rs
Expand Up @@ -1000,6 +1000,9 @@ impl Builder {
/// Set whether HTTP/1 connections will accept spaces between header names
/// and the colon that follow them in responses.
///
/// Newline codepoints (`\r` and `\n`) will be transformed to spaces when
/// parsing.
///
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
/// to say about it:
///
Expand All @@ -1022,6 +1025,43 @@ impl Builder {
self
}

/// Set whether HTTP/1 connections will accept obsolete line folding for
/// header values.
///
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
/// to say about it:
///
/// > A server that receives an obs-fold in a request message that is not
/// > within a message/http container MUST either reject the message by
/// > sending a 400 (Bad Request), preferably with a representation
/// > explaining that obsolete line folding is unacceptable, or replace
/// > each received obs-fold with one or more SP octets prior to
/// > interpreting the field value or forwarding the message downstream.
///
/// > A proxy or gateway that receives an obs-fold in a response message
/// > that is not within a message/http container MUST either discard the
/// > message and replace it with a 502 (Bad Gateway) response, preferably
/// > with a representation explaining that unacceptable line folding was
/// > received, or replace each received obs-fold with one or more SP
/// > octets prior to interpreting the field value or forwarding the
/// > message downstream.
///
/// > A user agent that receives an obs-fold in a response message that is
/// > not within a message/http container MUST replace each received
/// > obs-fold with one or more SP octets prior to interpreting the field
/// > value.
///
/// 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_obsolete_multiline_headers_in_responses(&mut self, val: bool) -> &mut Self {
self.conn_builder
.http1_allow_obsolete_multiline_headers_in_responses(val);
self
}

/// Set whether HTTP/1 connections should try to use vectored writes,
/// or always flatten into a single buffer.
///
Expand Down
43 changes: 43 additions & 0 deletions src/client/conn.rs
Expand Up @@ -615,6 +615,49 @@ impl Builder {
self
}

/// Set whether HTTP/1 connections will accept obsolete line folding for
/// header values.
///
/// Newline codepoints (`\r` and `\n`) will be transformed to spaces when
/// parsing.
///
/// You probably don't need this, here is what [RFC 7230 Section 3.2.4.] has
/// to say about it:
///
/// > A server that receives an obs-fold in a request message that is not
/// > within a message/http container MUST either reject the message by
/// > sending a 400 (Bad Request), preferably with a representation
/// > explaining that obsolete line folding is unacceptable, or replace
/// > each received obs-fold with one or more SP octets prior to
/// > interpreting the field value or forwarding the message downstream.
///
/// > A proxy or gateway that receives an obs-fold in a response message
/// > that is not within a message/http container MUST either discard the
/// > message and replace it with a 502 (Bad Gateway) response, preferably
/// > with a representation explaining that unacceptable line folding was
/// > received, or replace each received obs-fold with one or more SP
/// > octets prior to interpreting the field value or forwarding the
/// > message downstream.
///
/// > A user agent that receives an obs-fold in a response message that is
/// > not within a message/http container MUST replace each received
/// > obs-fold with one or more SP octets prior to interpreting the field
/// > value.
///
/// 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_obsolete_multiline_headers_in_responses(
&mut self,
enabled: bool,
) -> &mut Builder {
self.h1_parser_config
.allow_obsolete_multiline_headers_in_responses(enabled);
self
}

/// Set whether HTTP/1 connections should try to use vectored writes,
/// or always flatten into a single buffer.
///
Expand Down
16 changes: 15 additions & 1 deletion src/proto/h1/role.rs
Expand Up @@ -955,7 +955,21 @@ impl Http1Transaction for Client {
}
};

let slice = buf.split_to(len).freeze();
let mut slice = buf.split_to(len);

if ctx.h1_parser_config.obsolete_multiline_headers_in_responses_are_allowed() {
for header in &headers_indices[..headers_len] {
// SAFETY: array is valid up to `headers_len`
let header = unsafe { &*header.as_ptr() };
for b in &mut slice[header.value.0..header.value.1] {
if *b == b'\r' || *b == b'\n' {
*b = b' ';
}
}
}
}

let slice = slice.freeze();

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

Expand Down
57 changes: 57 additions & 0 deletions tests/client.rs
Expand Up @@ -2214,6 +2214,63 @@ mod conn {
future::join(server, client).await;
}

#[tokio::test]
async fn get_obsolete_line_folding() {
let _ = ::pretty_env_logger::try_init();
let listener = TkTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
.await
.unwrap();
let addr = listener.local_addr().unwrap();

let server = async move {
let mut sock = listener.accept().await.unwrap().0;
let mut buf = [0; 4096];
let n = sock.read(&mut buf).await.expect("read 1");

// Notably:
// - Just a path, since just a path was set
// - No host, since no host was set
let expected = "GET /a HTTP/1.1\r\n\r\n";
assert_eq!(s(&buf[..n]), expected);

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")
.await
.unwrap();
};

let client = async move {
let tcp = tcp_connect(&addr).await.expect("connect");
let (mut client, conn) = conn::Builder::new()
.http1_allow_obsolete_multiline_headers_in_responses(true)
.handshake::<_, Body>(tcp)
.await
.expect("handshake");

tokio::task::spawn(async move {
conn.await.expect("http conn");
});

let req = Request::builder()
.uri("/a")
.body(Default::default())
.unwrap();
let mut res = client.send_request(req).await.expect("send_request");
assert_eq!(res.status(), hyper::StatusCode::OK);
assert_eq!(res.headers().len(), 2);
assert_eq!(
res.headers().get(http::header::CONTENT_LENGTH).unwrap(),
"0"
);
assert_eq!(
res.headers().get("line-folded-header").unwrap(),
"hello world"
);
assert!(res.body_mut().next().await.is_none());
};

future::join(server, client).await;
}

#[test]
fn incoming_content_length() {
use hyper::body::HttpBody;
Expand Down

0 comments on commit 1f0c177

Please sign in to comment.