Skip to content

Commit

Permalink
feat(h1): implement obsolete line folding
Browse files Browse the repository at this point in the history
  • Loading branch information
nox authored and seanmonstar committed Feb 9, 2022
1 parent f1b89c1 commit f779500
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 @@ -598,6 +598,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 f779500

Please sign in to comment.