Skip to content

Commit 4c7752d

Browse files
noxBenxiang Ge
authored and
Benxiang Ge
committed
feat(client): allow HTTP/0.9 responses behind a flag (hyperium#2473)
Fixes hyperium#2468
1 parent 2ce3908 commit 4c7752d

File tree

8 files changed

+158
-4
lines changed

8 files changed

+158
-4
lines changed

src/client/client.rs

+8
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,14 @@ impl Builder {
972972
self
973973
}
974974

975+
/// Set whether HTTP/0.9 responses should be tolerated.
976+
///
977+
/// Default is false.
978+
pub fn http09_responses(&mut self, val: bool) -> &mut Self {
979+
self.conn_builder.h09_responses(val);
980+
self
981+
}
982+
975983
/// Set whether the connection **must** use HTTP/2.
976984
///
977985
/// The destination must either allow HTTP2 Prior Knowledge, or the

src/client/conn.rs

+10
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ where
122122
#[derive(Clone, Debug)]
123123
pub struct Builder {
124124
pub(super) exec: Exec,
125+
h09_responses: bool,
125126
h1_title_case_headers: bool,
126127
h1_read_buf_exact_size: Option<usize>,
127128
h1_max_buf_size: Option<usize>,
@@ -493,6 +494,7 @@ impl Builder {
493494
pub fn new() -> Builder {
494495
Builder {
495496
exec: Exec::Default,
497+
h09_responses: false,
496498
h1_read_buf_exact_size: None,
497499
h1_title_case_headers: false,
498500
h1_max_buf_size: None,
@@ -514,6 +516,11 @@ impl Builder {
514516
self
515517
}
516518

519+
pub(super) fn h09_responses(&mut self, enabled: bool) -> &mut Builder {
520+
self.h09_responses = enabled;
521+
self
522+
}
523+
517524
pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder {
518525
self.h1_title_case_headers = enabled;
519526
self
@@ -700,6 +707,9 @@ impl Builder {
700707
if opts.h1_title_case_headers {
701708
conn.set_title_case_headers();
702709
}
710+
if opts.h09_responses {
711+
conn.set_h09_responses();
712+
}
703713
if let Some(sz) = opts.h1_read_buf_exact_size {
704714
conn.set_read_buf_exact_size(sz);
705715
}

src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![doc(html_root_url = "https://docs.rs/hyper/0.14.4")]
21
#![deny(missing_docs)]
32
#![deny(missing_debug_implementations)]
43
#![cfg_attr(test, deny(rust_2018_idioms))]

src/proto/h1/conn.rs

+11
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ where
4747
#[cfg(feature = "ffi")]
4848
preserve_header_case: false,
4949
title_case_headers: false,
50+
h09_responses: false,
5051
notify_read: false,
5152
reading: Reading::Init,
5253
writing: Writing::Init,
@@ -78,6 +79,11 @@ where
7879
self.state.title_case_headers = true;
7980
}
8081

82+
#[cfg(feature = "client")]
83+
pub(crate) fn set_h09_responses(&mut self) {
84+
self.state.h09_responses = true;
85+
}
86+
8187
#[cfg(feature = "server")]
8288
pub(crate) fn set_allow_half_close(&mut self) {
8389
self.state.allow_half_close = true;
@@ -146,6 +152,7 @@ where
146152
req_method: &mut self.state.method,
147153
#[cfg(feature = "ffi")]
148154
preserve_header_case: self.state.preserve_header_case,
155+
h09_responses: self.state.h09_responses,
149156
}
150157
)) {
151158
Ok(msg) => msg,
@@ -157,6 +164,9 @@ where
157164

158165
debug!("incoming body is {}", msg.decode);
159166

167+
// Prevent accepting HTTP/0.9 responses after the initial one, if any.
168+
self.state.h09_responses = false;
169+
160170
self.state.busy();
161171
self.state.keep_alive &= msg.keep_alive;
162172
self.state.version = msg.head.version;
@@ -753,6 +763,7 @@ struct State {
753763
#[cfg(feature = "ffi")]
754764
preserve_header_case: bool,
755765
title_case_headers: bool,
766+
h09_responses: bool,
756767
/// Set to true when the Dispatcher should poll read operations
757768
/// again. See the `maybe_notify` method for more.
758769
notify_read: bool,

src/proto/h1/io.rs

+2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ where
161161
req_method: parse_ctx.req_method,
162162
#[cfg(feature = "ffi")]
163163
preserve_header_case: parse_ctx.preserve_header_case,
164+
h09_responses: parse_ctx.h09_responses,
164165
},
165166
)? {
166167
Some(msg) => {
@@ -640,6 +641,7 @@ mod tests {
640641
req_method: &mut None,
641642
#[cfg(feature = "ffi")]
642643
preserve_header_case: false,
644+
h09_responses: false,
643645
};
644646
assert!(buffered
645647
.parse::<ClientTransaction>(cx, parse_ctx)

src/proto/h1/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub(crate) struct ParseContext<'a> {
7272
req_method: &'a mut Option<Method>,
7373
#[cfg(feature = "ffi")]
7474
preserve_header_case: bool,
75+
h09_responses: bool,
7576
}
7677

7778
/// Passed to Http1Transaction::encode

src/proto/h1/role.rs

+60-3
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,8 @@ impl Http1Transaction for Client {
690690
);
691691
let mut res = httparse::Response::new(&mut headers);
692692
let bytes = buf.as_ref();
693-
match res.parse(bytes)? {
694-
httparse::Status::Complete(len) => {
693+
match res.parse(bytes) {
694+
Ok(httparse::Status::Complete(len)) => {
695695
trace!("Response.parse Complete({})", len);
696696
let status = StatusCode::from_u16(res.code.unwrap())?;
697697

@@ -717,7 +717,18 @@ impl Http1Transaction for Client {
717717
let headers_len = res.headers.len();
718718
(len, status, reason, version, headers_len)
719719
}
720-
httparse::Status::Partial => return Ok(None),
720+
Ok(httparse::Status::Partial) => return Ok(None),
721+
Err(httparse::Error::Version) if ctx.h09_responses => {
722+
trace!("Response.parse accepted HTTP/0.9 response");
723+
724+
#[cfg(not(feature = "ffi"))]
725+
let reason = ();
726+
#[cfg(feature = "ffi")]
727+
let reason = None;
728+
729+
(0, StatusCode::OK, reason, Version::HTTP_09, 0)
730+
}
731+
Err(e) => return Err(e.into()),
721732
}
722733
};
723734

@@ -1229,6 +1240,7 @@ mod tests {
12291240
req_method: &mut method,
12301241
#[cfg(feature = "ffi")]
12311242
preserve_header_case: false,
1243+
h09_responses: false,
12321244
},
12331245
)
12341246
.unwrap()
@@ -1251,6 +1263,7 @@ mod tests {
12511263
req_method: &mut Some(crate::Method::GET),
12521264
#[cfg(feature = "ffi")]
12531265
preserve_header_case: false,
1266+
h09_responses: false,
12541267
};
12551268
let msg = Client::parse(&mut raw, ctx).unwrap().unwrap();
12561269
assert_eq!(raw.len(), 0);
@@ -1268,10 +1281,46 @@ mod tests {
12681281
req_method: &mut None,
12691282
#[cfg(feature = "ffi")]
12701283
preserve_header_case: false,
1284+
h09_responses: false,
12711285
};
12721286
Server::parse(&mut raw, ctx).unwrap_err();
12731287
}
12741288

1289+
const H09_RESPONSE: &'static str = "Baguettes are super delicious, don't you agree?";
1290+
1291+
#[test]
1292+
fn test_parse_response_h09_allowed() {
1293+
let _ = pretty_env_logger::try_init();
1294+
let mut raw = BytesMut::from(H09_RESPONSE);
1295+
let ctx = ParseContext {
1296+
cached_headers: &mut None,
1297+
req_method: &mut Some(crate::Method::GET),
1298+
#[cfg(feature = "ffi")]
1299+
preserve_header_case: false,
1300+
h09_responses: true,
1301+
};
1302+
let msg = Client::parse(&mut raw, ctx).unwrap().unwrap();
1303+
assert_eq!(raw, H09_RESPONSE);
1304+
assert_eq!(msg.head.subject, crate::StatusCode::OK);
1305+
assert_eq!(msg.head.version, crate::Version::HTTP_09);
1306+
assert_eq!(msg.head.headers.len(), 0);
1307+
}
1308+
1309+
#[test]
1310+
fn test_parse_response_h09_rejected() {
1311+
let _ = pretty_env_logger::try_init();
1312+
let mut raw = BytesMut::from(H09_RESPONSE);
1313+
let ctx = ParseContext {
1314+
cached_headers: &mut None,
1315+
req_method: &mut Some(crate::Method::GET),
1316+
#[cfg(feature = "ffi")]
1317+
preserve_header_case: false,
1318+
h09_responses: false,
1319+
};
1320+
Client::parse(&mut raw, ctx).unwrap_err();
1321+
assert_eq!(raw, H09_RESPONSE);
1322+
}
1323+
12751324
#[test]
12761325
fn test_decoder_request() {
12771326
fn parse(s: &str) -> ParsedMessage<RequestLine> {
@@ -1283,6 +1332,7 @@ mod tests {
12831332
req_method: &mut None,
12841333
#[cfg(feature = "ffi")]
12851334
preserve_header_case: false,
1335+
h09_responses: false,
12861336
},
12871337
)
12881338
.expect("parse ok")
@@ -1298,6 +1348,7 @@ mod tests {
12981348
req_method: &mut None,
12991349
#[cfg(feature = "ffi")]
13001350
preserve_header_case: false,
1351+
h09_responses: false,
13011352
},
13021353
)
13031354
.expect_err(comment)
@@ -1512,6 +1563,7 @@ mod tests {
15121563
req_method: &mut Some(Method::GET),
15131564
#[cfg(feature = "ffi")]
15141565
preserve_header_case: false,
1566+
h09_responses: false,
15151567
}
15161568
)
15171569
.expect("parse ok")
@@ -1527,6 +1579,7 @@ mod tests {
15271579
req_method: &mut Some(m),
15281580
#[cfg(feature = "ffi")]
15291581
preserve_header_case: false,
1582+
h09_responses: false,
15301583
},
15311584
)
15321585
.expect("parse ok")
@@ -1542,6 +1595,7 @@ mod tests {
15421595
req_method: &mut Some(Method::GET),
15431596
#[cfg(feature = "ffi")]
15441597
preserve_header_case: false,
1598+
h09_responses: false,
15451599
},
15461600
)
15471601
.expect_err("parse should err")
@@ -1857,6 +1911,7 @@ mod tests {
18571911
req_method: &mut Some(Method::GET),
18581912
#[cfg(feature = "ffi")]
18591913
preserve_header_case: false,
1914+
h09_responses: false,
18601915
},
18611916
)
18621917
.expect("parse ok")
@@ -1938,6 +1993,7 @@ mod tests {
19381993
req_method: &mut None,
19391994
#[cfg(feature = "ffi")]
19401995
preserve_header_case: false,
1996+
h09_responses: false,
19411997
},
19421998
)
19431999
.unwrap()
@@ -1973,6 +2029,7 @@ mod tests {
19732029
req_method: &mut None,
19742030
#[cfg(feature = "ffi")]
19752031
preserve_header_case: false,
2032+
h09_responses: false,
19762033
},
19772034
)
19782035
.unwrap()

tests/client.rs

+66
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,43 @@ macro_rules! test {
112112
headers: { $($response_header_name:expr => $response_header_val:expr,)* },
113113
body: $response_body:expr,
114114
) => (
115+
test! {
116+
name: $name,
117+
server:
118+
expected: $server_expected,
119+
reply: $server_reply,
120+
client:
121+
set_host: $set_host,
122+
title_case_headers: $title_case_headers,
123+
allow_h09_responses: false,
124+
request: {$(
125+
$c_req_prop: $c_req_val,
126+
)*},
127+
128+
response:
129+
status: $client_status,
130+
headers: { $($response_header_name => $response_header_val,)* },
131+
body: $response_body,
132+
}
133+
);
134+
(
135+
name: $name:ident,
136+
server:
137+
expected: $server_expected:expr,
138+
reply: $server_reply:expr,
139+
client:
140+
set_host: $set_host:expr,
141+
title_case_headers: $title_case_headers:expr,
142+
allow_h09_responses: $allow_h09_responses:expr,
143+
request: {$(
144+
$c_req_prop:ident: $c_req_val:tt,
145+
)*},
146+
147+
response:
148+
status: $client_status:ident,
149+
headers: { $($response_header_name:expr => $response_header_val:expr,)* },
150+
body: $response_body:expr,
151+
) => (
115152
#[test]
116153
fn $name() {
117154
let _ = pretty_env_logger::try_init();
@@ -127,6 +164,7 @@ macro_rules! test {
127164
client:
128165
set_host: $set_host,
129166
title_case_headers: $title_case_headers,
167+
allow_h09_responses: $allow_h09_responses,
130168
request: {$(
131169
$c_req_prop: $c_req_val,
132170
)*},
@@ -181,6 +219,7 @@ macro_rules! test {
181219
client:
182220
set_host: true,
183221
title_case_headers: false,
222+
allow_h09_responses: false,
184223
request: {$(
185224
$c_req_prop: $c_req_val,
186225
)*},
@@ -205,6 +244,7 @@ macro_rules! test {
205244
client:
206245
set_host: $set_host:expr,
207246
title_case_headers: $title_case_headers:expr,
247+
allow_h09_responses: $allow_h09_responses:expr,
208248
request: {$(
209249
$c_req_prop:ident: $c_req_val:tt,
210250
)*},
@@ -217,6 +257,7 @@ macro_rules! test {
217257
let client = Client::builder()
218258
.set_host($set_host)
219259
.http1_title_case_headers($title_case_headers)
260+
.http09_responses($allow_h09_responses)
220261
.build(connector);
221262

222263
#[allow(unused_assignments, unused_mut)]
@@ -1067,6 +1108,31 @@ test! {
10671108
body: &b"abc"[..],
10681109
}
10691110

1111+
test! {
1112+
name: client_allows_http09_when_requested,
1113+
1114+
server:
1115+
expected: "\
1116+
GET / HTTP/1.1\r\n\
1117+
Host: {addr}\r\n\
1118+
\r\n\
1119+
",
1120+
reply: "Mmmmh, baguettes.",
1121+
1122+
client:
1123+
set_host: true,
1124+
title_case_headers: true,
1125+
allow_h09_responses: true,
1126+
request: {
1127+
method: GET,
1128+
url: "http://{addr}/",
1129+
},
1130+
response:
1131+
status: OK,
1132+
headers: {},
1133+
body: &b"Mmmmh, baguettes."[..],
1134+
}
1135+
10701136
mod dispatch_impl {
10711137
use super::*;
10721138
use std::io::{self, Read, Write};

0 commit comments

Comments
 (0)