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:
@@ -2271,6 +2271,62 @@ mod conn {
|
||||
future::join(server, client).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_custom_reason_phrase() {
|
||||
let _ = ::pretty_env_logger::try_init();
|
||||
let listener = TkTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
|
||||
.await
|
||||
.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let server = async move {
|
||||
let mut sock = listener.accept().await.unwrap().0;
|
||||
let mut buf = [0; 4096];
|
||||
let n = sock.read(&mut buf).await.expect("read 1");
|
||||
|
||||
// Notably:
|
||||
// - Just a path, since just a path was set
|
||||
// - No host, since no host was set
|
||||
let expected = "GET /a HTTP/1.1\r\n\r\n";
|
||||
assert_eq!(s(&buf[..n]), expected);
|
||||
|
||||
sock.write_all(b"HTTP/1.1 200 Alright\r\nContent-Length: 0\r\n\r\n")
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let client = async move {
|
||||
let tcp = tcp_connect(&addr).await.expect("connect");
|
||||
let (mut client, conn) = conn::handshake(tcp).await.expect("handshake");
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
conn.await.expect("http conn");
|
||||
});
|
||||
|
||||
let req = Request::builder()
|
||||
.uri("/a")
|
||||
.body(Default::default())
|
||||
.unwrap();
|
||||
let mut res = client.send_request(req).await.expect("send_request");
|
||||
assert_eq!(res.status(), hyper::StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.extensions()
|
||||
.get::<hyper::ext::ReasonPhrase>()
|
||||
.expect("custom reason phrase is present")
|
||||
.as_bytes(),
|
||||
&b"Alright"[..]
|
||||
);
|
||||
assert_eq!(res.headers().len(), 1);
|
||||
assert_eq!(
|
||||
res.headers().get(http::header::CONTENT_LENGTH).unwrap(),
|
||||
"0"
|
||||
);
|
||||
assert!(res.body_mut().next().await.is_none());
|
||||
};
|
||||
|
||||
future::join(server, client).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incoming_content_length() {
|
||||
use hyper::body::HttpBody;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user