feat(client): allow HTTP/0.9 responses behind a flag (#2473)

Fixes #2468
This commit is contained in:
Anthony Ramine
2021-03-26 19:25:00 +01:00
committed by GitHub
parent 51ed71b0a6
commit 68d4e4a3db
8 changed files with 158 additions and 4 deletions

View File

@@ -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

View File

@@ -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<usize>,
h1_max_buf_size: Option<usize>,
@@ -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);
}

View File

@@ -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))]

View File

@@ -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,

View File

@@ -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::<ClientTransaction>(cx, parse_ctx)

View File

@@ -72,6 +72,7 @@ pub(crate) struct ParseContext<'a> {
req_method: &'a mut Option<Method>,
#[cfg(feature = "ffi")]
preserve_header_case: bool,
h09_responses: bool,
}
/// Passed to Http1Transaction::encode

View File

@@ -683,8 +683,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())?;
@@ -710,7 +710,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()),
}
};
@@ -1222,6 +1233,7 @@ mod tests {
req_method: &mut method,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()
@@ -1244,6 +1256,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);
@@ -1261,10 +1274,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<RequestLine> {
@@ -1276,6 +1325,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect("parse ok")
@@ -1291,6 +1341,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect_err(comment)
@@ -1505,6 +1556,7 @@ mod tests {
req_method: &mut Some(Method::GET),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
}
)
.expect("parse ok")
@@ -1520,6 +1572,7 @@ mod tests {
req_method: &mut Some(m),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect("parse ok")
@@ -1535,6 +1588,7 @@ mod tests {
req_method: &mut Some(Method::GET),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect_err("parse should err")
@@ -1850,6 +1904,7 @@ mod tests {
req_method: &mut Some(Method::GET),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect("parse ok")
@@ -1931,6 +1986,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()
@@ -1966,6 +2022,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()

View File

@@ -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};