Skip to content

Commit

Permalink
feat(client): allow HTTP/0.9 responses behind a flag (#2473)
Browse files Browse the repository at this point in the history
Fixes #2468
  • Loading branch information
nox authored Mar 26, 2021
1 parent 51ed71b commit 68d4e4a
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 4 deletions.
8 changes: 8 additions & 0 deletions src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/client/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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))]
Expand Down
11 changes: 11 additions & 0 deletions src/proto/h1/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/proto/h1/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/proto/h1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 60 additions & 3 deletions src/proto/h1/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;

Expand All @@ -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()),
}
};

Expand Down Expand Up @@ -1222,6 +1233,7 @@ mod tests {
req_method: &mut method,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()
Expand All @@ -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);
Expand All @@ -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> {
Expand All @@ -1276,6 +1325,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect("parse ok")
Expand All @@ -1291,6 +1341,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect_err(comment)
Expand Down Expand Up @@ -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")
Expand All @@ -1520,6 +1572,7 @@ mod tests {
req_method: &mut Some(m),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.expect("parse ok")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -1931,6 +1986,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()
Expand Down Expand Up @@ -1966,6 +2022,7 @@ mod tests {
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
},
)
.unwrap()
Expand Down
66 changes: 66 additions & 0 deletions tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
)*},
Expand Down Expand Up @@ -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,
)*},
Expand All @@ -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,
)*},
Expand All @@ -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)]
Expand Down Expand Up @@ -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};
Expand Down

0 comments on commit 68d4e4a

Please sign in to comment.