feat(ext): support non-canonical HTTP/1 reason phrases (#2792)

Add a new extension type `hyper::ext::ReasonPhrase` gated by either the `ffi` or `http1` Cargo
features. When enabled, store any non-canonical reason phrases in this extension when parsing
responses, and write this reason phrase instead of the canonical reason phrase when emitting
responses.

Reason phrases are a disused corner of the spec that implementations ought to treat as opaque blobs
of bytes. Unfortunately, real-world traffic sometimes does depend on being able to inspect and
manipulate them.

Non-canonical reason phrases are checked for validity at runtime to prevent invalid and dangerous
characters from being emitted when writing responses. An `unsafe` escape hatch is present for hyper
itself to create reason phrases that have been parsed (and therefore implicitly validated) by
httparse.
This commit is contained in:
Adam C. Foltzer
2022-06-08 15:57:33 -07:00
committed by GitHub
parent f12d4d4aa8
commit b2052a433f
6 changed files with 354 additions and 29 deletions

View File

@@ -1,6 +1,7 @@
#![deny(warnings)]
#![deny(rust_2018_idioms)]
use std::convert::TryInto;
use std::future::Future;
use std::io::{self, Read, Write};
use std::net::TcpListener as StdTcpListener;
@@ -383,6 +384,33 @@ mod response_body_lengths {
}
}
#[test]
fn get_response_custom_reason_phrase() {
let _ = pretty_env_logger::try_init();
let server = serve();
server.reply().reason_phrase("Cool");
let mut req = connect(server.addr());
req.write_all(
b"\
GET / HTTP/1.1\r\n\
Host: example.domain\r\n\
Connection: close\r\n\
\r\n\
",
)
.unwrap();
let mut response = String::new();
req.read_to_string(&mut response).unwrap();
let mut lines = response.lines();
assert_eq!(lines.next(), Some("HTTP/1.1 200 Cool"));
let mut lines = lines.skip_while(|line| !line.is_empty());
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
}
#[test]
fn get_chunked_response_with_ka() {
let foo_bar = b"foo bar baz";
@@ -2671,6 +2699,17 @@ impl<'a> ReplyBuilder<'a> {
self
}
fn reason_phrase(self, reason: &str) -> Self {
self.tx
.lock()
.unwrap()
.send(Reply::ReasonPhrase(
reason.as_bytes().try_into().expect("reason phrase"),
))
.unwrap();
self
}
fn version(self, version: hyper::Version) -> Self {
self.tx
.lock()
@@ -2744,6 +2783,7 @@ struct TestService {
#[derive(Debug)]
enum Reply {
Status(hyper::StatusCode),
ReasonPhrase(hyper::ext::ReasonPhrase),
Version(hyper::Version),
Header(HeaderName, HeaderValue),
Body(hyper::Body),
@@ -2799,6 +2839,9 @@ impl TestService {
Reply::Status(s) => {
*res.status_mut() = s;
}
Reply::ReasonPhrase(reason) => {
res.extensions_mut().insert(reason);
}
Reply::Version(v) => {
*res.version_mut() = v;
}