From 4c7752d8a279b82599d0004ad3b95260ad5cfe49 Mon Sep 17 00:00:00 2001 From: Anthony Ramine <123095+nox@users.noreply.github.com> Date: Fri, 26 Mar 2021 19:25:00 +0100 Subject: [PATCH] feat(client): allow HTTP/0.9 responses behind a flag (#2473) Fixes #2468 --- src/client/client.rs | 8 ++++++ src/client/conn.rs | 10 +++++++ src/lib.rs | 1 - src/proto/h1/conn.rs | 11 ++++++++ src/proto/h1/io.rs | 2 ++ src/proto/h1/mod.rs | 1 + src/proto/h1/role.rs | 63 ++++++++++++++++++++++++++++++++++++++++-- tests/client.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 158 insertions(+), 4 deletions(-) diff --git a/src/client/client.rs b/src/client/client.rs index 37a3251a7a..418f3fb4e9 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -972,6 +972,14 @@ impl Builder { self } + /// Set whether HTTP/0.9 responses should be tolerated. + /// + /// Default is false. + pub fn http09_responses(&mut self, val: bool) -> &mut Self { + self.conn_builder.h09_responses(val); + self + } + /// Set whether the connection **must** use HTTP/2. /// /// The destination must either allow HTTP2 Prior Knowledge, or the diff --git a/src/client/conn.rs b/src/client/conn.rs index 2da083db16..b87600d85a 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -122,6 +122,7 @@ where #[derive(Clone, Debug)] pub struct Builder { pub(super) exec: Exec, + h09_responses: bool, h1_title_case_headers: bool, h1_read_buf_exact_size: Option, h1_max_buf_size: Option, @@ -493,6 +494,7 @@ impl Builder { pub fn new() -> Builder { Builder { exec: Exec::Default, + h09_responses: false, h1_read_buf_exact_size: None, h1_title_case_headers: false, h1_max_buf_size: None, @@ -514,6 +516,11 @@ impl Builder { self } + pub(super) fn h09_responses(&mut self, enabled: bool) -> &mut Builder { + self.h09_responses = enabled; + self + } + pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder { self.h1_title_case_headers = enabled; self @@ -700,6 +707,9 @@ impl Builder { if opts.h1_title_case_headers { conn.set_title_case_headers(); } + if opts.h09_responses { + conn.set_h09_responses(); + } if let Some(sz) = opts.h1_read_buf_exact_size { conn.set_read_buf_exact_size(sz); } diff --git a/src/lib.rs b/src/lib.rs index daddbdfb0a..059f8821c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -#![doc(html_root_url = "https://docs.rs/hyper/0.14.4")] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(rust_2018_idioms))] diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs index 48a81b853d..aed312835c 100644 --- a/src/proto/h1/conn.rs +++ b/src/proto/h1/conn.rs @@ -47,6 +47,7 @@ where #[cfg(feature = "ffi")] preserve_header_case: false, title_case_headers: false, + h09_responses: false, notify_read: false, reading: Reading::Init, writing: Writing::Init, @@ -78,6 +79,11 @@ where self.state.title_case_headers = true; } + #[cfg(feature = "client")] + pub(crate) fn set_h09_responses(&mut self) { + self.state.h09_responses = true; + } + #[cfg(feature = "server")] pub(crate) fn set_allow_half_close(&mut self) { self.state.allow_half_close = true; @@ -146,6 +152,7 @@ where req_method: &mut self.state.method, #[cfg(feature = "ffi")] preserve_header_case: self.state.preserve_header_case, + h09_responses: self.state.h09_responses, } )) { Ok(msg) => msg, @@ -157,6 +164,9 @@ where debug!("incoming body is {}", msg.decode); + // Prevent accepting HTTP/0.9 responses after the initial one, if any. + self.state.h09_responses = false; + self.state.busy(); self.state.keep_alive &= msg.keep_alive; self.state.version = msg.head.version; @@ -753,6 +763,7 @@ struct State { #[cfg(feature = "ffi")] preserve_header_case: bool, title_case_headers: bool, + h09_responses: bool, /// Set to true when the Dispatcher should poll read operations /// again. See the `maybe_notify` method for more. notify_read: bool, diff --git a/src/proto/h1/io.rs b/src/proto/h1/io.rs index 5536b5d164..c7ce48664b 100644 --- a/src/proto/h1/io.rs +++ b/src/proto/h1/io.rs @@ -161,6 +161,7 @@ where req_method: parse_ctx.req_method, #[cfg(feature = "ffi")] preserve_header_case: parse_ctx.preserve_header_case, + h09_responses: parse_ctx.h09_responses, }, )? { Some(msg) => { @@ -640,6 +641,7 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }; assert!(buffered .parse::(cx, parse_ctx) diff --git a/src/proto/h1/mod.rs b/src/proto/h1/mod.rs index 1498872ea8..01a9253fa3 100644 --- a/src/proto/h1/mod.rs +++ b/src/proto/h1/mod.rs @@ -72,6 +72,7 @@ pub(crate) struct ParseContext<'a> { req_method: &'a mut Option, #[cfg(feature = "ffi")] preserve_header_case: bool, + h09_responses: bool, } /// Passed to Http1Transaction::encode diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 5464c10f80..45a324b9ab 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -690,8 +690,8 @@ impl Http1Transaction for Client { ); let mut res = httparse::Response::new(&mut headers); let bytes = buf.as_ref(); - match res.parse(bytes)? { - httparse::Status::Complete(len) => { + match res.parse(bytes) { + Ok(httparse::Status::Complete(len)) => { trace!("Response.parse Complete({})", len); let status = StatusCode::from_u16(res.code.unwrap())?; @@ -717,7 +717,18 @@ impl Http1Transaction for Client { let headers_len = res.headers.len(); (len, status, reason, version, headers_len) } - httparse::Status::Partial => return Ok(None), + Ok(httparse::Status::Partial) => return Ok(None), + Err(httparse::Error::Version) if ctx.h09_responses => { + trace!("Response.parse accepted HTTP/0.9 response"); + + #[cfg(not(feature = "ffi"))] + let reason = (); + #[cfg(feature = "ffi")] + let reason = None; + + (0, StatusCode::OK, reason, Version::HTTP_09, 0) + } + Err(e) => return Err(e.into()), } }; @@ -1229,6 +1240,7 @@ mod tests { req_method: &mut method, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .unwrap() @@ -1251,6 +1263,7 @@ mod tests { req_method: &mut Some(crate::Method::GET), #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }; let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); assert_eq!(raw.len(), 0); @@ -1268,10 +1281,46 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }; Server::parse(&mut raw, ctx).unwrap_err(); } + const H09_RESPONSE: &'static str = "Baguettes are super delicious, don't you agree?"; + + #[test] + fn test_parse_response_h09_allowed() { + let _ = pretty_env_logger::try_init(); + let mut raw = BytesMut::from(H09_RESPONSE); + let ctx = ParseContext { + cached_headers: &mut None, + req_method: &mut Some(crate::Method::GET), + #[cfg(feature = "ffi")] + preserve_header_case: false, + h09_responses: true, + }; + let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); + assert_eq!(raw, H09_RESPONSE); + assert_eq!(msg.head.subject, crate::StatusCode::OK); + assert_eq!(msg.head.version, crate::Version::HTTP_09); + assert_eq!(msg.head.headers.len(), 0); + } + + #[test] + fn test_parse_response_h09_rejected() { + let _ = pretty_env_logger::try_init(); + let mut raw = BytesMut::from(H09_RESPONSE); + let ctx = ParseContext { + cached_headers: &mut None, + req_method: &mut Some(crate::Method::GET), + #[cfg(feature = "ffi")] + preserve_header_case: false, + h09_responses: false, + }; + Client::parse(&mut raw, ctx).unwrap_err(); + assert_eq!(raw, H09_RESPONSE); + } + #[test] fn test_decoder_request() { fn parse(s: &str) -> ParsedMessage { @@ -1283,6 +1332,7 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .expect("parse ok") @@ -1298,6 +1348,7 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .expect_err(comment) @@ -1512,6 +1563,7 @@ mod tests { req_method: &mut Some(Method::GET), #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, } ) .expect("parse ok") @@ -1527,6 +1579,7 @@ mod tests { req_method: &mut Some(m), #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .expect("parse ok") @@ -1542,6 +1595,7 @@ mod tests { req_method: &mut Some(Method::GET), #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .expect_err("parse should err") @@ -1857,6 +1911,7 @@ mod tests { req_method: &mut Some(Method::GET), #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .expect("parse ok") @@ -1938,6 +1993,7 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .unwrap() @@ -1973,6 +2029,7 @@ mod tests { req_method: &mut None, #[cfg(feature = "ffi")] preserve_header_case: false, + h09_responses: false, }, ) .unwrap() diff --git a/tests/client.rs b/tests/client.rs index 0585695a80..d22f8cf2ca 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -112,6 +112,43 @@ macro_rules! test { headers: { $($response_header_name:expr => $response_header_val:expr,)* }, body: $response_body:expr, ) => ( + test! { + name: $name, + server: + expected: $server_expected, + reply: $server_reply, + client: + set_host: $set_host, + title_case_headers: $title_case_headers, + allow_h09_responses: false, + request: {$( + $c_req_prop: $c_req_val, + )*}, + + response: + status: $client_status, + headers: { $($response_header_name => $response_header_val,)* }, + body: $response_body, + } + ); + ( + name: $name:ident, + server: + expected: $server_expected:expr, + reply: $server_reply:expr, + client: + set_host: $set_host:expr, + title_case_headers: $title_case_headers:expr, + allow_h09_responses: $allow_h09_responses:expr, + request: {$( + $c_req_prop:ident: $c_req_val:tt, + )*}, + + response: + status: $client_status:ident, + headers: { $($response_header_name:expr => $response_header_val:expr,)* }, + body: $response_body:expr, + ) => ( #[test] fn $name() { let _ = pretty_env_logger::try_init(); @@ -127,6 +164,7 @@ macro_rules! test { client: set_host: $set_host, title_case_headers: $title_case_headers, + allow_h09_responses: $allow_h09_responses, request: {$( $c_req_prop: $c_req_val, )*}, @@ -181,6 +219,7 @@ macro_rules! test { client: set_host: true, title_case_headers: false, + allow_h09_responses: false, request: {$( $c_req_prop: $c_req_val, )*}, @@ -205,6 +244,7 @@ macro_rules! test { client: set_host: $set_host:expr, title_case_headers: $title_case_headers:expr, + allow_h09_responses: $allow_h09_responses:expr, request: {$( $c_req_prop:ident: $c_req_val:tt, )*}, @@ -217,6 +257,7 @@ macro_rules! test { let client = Client::builder() .set_host($set_host) .http1_title_case_headers($title_case_headers) + .http09_responses($allow_h09_responses) .build(connector); #[allow(unused_assignments, unused_mut)] @@ -1067,6 +1108,31 @@ test! { body: &b"abc"[..], } +test! { + name: client_allows_http09_when_requested, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + Host: {addr}\r\n\ + \r\n\ + ", + reply: "Mmmmh, baguettes.", + + client: + set_host: true, + title_case_headers: true, + allow_h09_responses: true, + request: { + method: GET, + url: "http://{addr}/", + }, + response: + status: OK, + headers: {}, + body: &b"Mmmmh, baguettes."[..], +} + mod dispatch_impl { use super::*; use std::io::{self, Read, Write};