feat(client): allow HTTP/0.9 responses behind a flag (#2473)
Fixes #2468
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user