diff --git a/Cargo.toml b/Cargo.toml index 862c20f901..6e5d9a7284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/client/client.rs b/src/client/client.rs index 3c1a843090..112a5af806 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -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: /// @@ -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. /// diff --git a/src/client/conn.rs b/src/client/conn.rs index 2418c9fa83..85bc366be9 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -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. /// diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 4b6b447608..968b63cb8e 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -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); diff --git a/tests/client.rs b/tests/client.rs index 47e827f4bd..417e9bf2d9 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -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;