Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow HTTP/0.9 responses behind a flag (fixes #2468) #2473

Merged
merged 2 commits into from Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/client/client.rs
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
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
@@ -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
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
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
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
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
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