From c49020803824dc90a6a96ce7e6fafbd4994fa675 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Thu, 16 Nov 2023 11:53:23 +0100 Subject: [PATCH] Introduce hyper::ext::Http1RawMessage --- capi/include/hyper.h | 26 ------------ src/client/conn.rs | 24 ++++++----- src/ext.rs | 36 +++++++++++++++++ src/ffi/client.rs | 14 ------- src/ffi/http_types.rs | 25 +----------- src/proto/h1/conn.rs | 14 +++---- src/proto/h1/io.rs | 6 +-- src/proto/h1/mod.rs | 3 +- src/proto/h1/role.rs | 87 +++++++++++++++++++--------------------- src/server/conn.rs | 19 +++++++++ src/server/conn/http1.rs | 16 ++++++++ src/server/server.rs | 13 ++++++ tests/client.rs | 17 ++++++-- tests/server.rs | 40 +++++++++++++----- 14 files changed, 195 insertions(+), 145 deletions(-) diff --git a/capi/include/hyper.h b/capi/include/hyper.h index 1f938b8714..e14de53116 100644 --- a/capi/include/hyper.h +++ b/capi/include/hyper.h @@ -391,17 +391,6 @@ void hyper_clientconn_options_exec(struct hyper_clientconn_options *opts, */ enum hyper_code hyper_clientconn_options_http2(struct hyper_clientconn_options *opts, int enabled); -/* - Set the whether to include a copy of the raw headers in responses - received on this connection. - - Pass `0` to disable, `1` to enable. - - If enabled, see `hyper_response_headers_raw()` for usage. - */ -enum hyper_code hyper_clientconn_options_headers_raw(struct hyper_clientconn_options *opts, - int enabled); - /* Frees a `hyper_error`. */ @@ -557,21 +546,6 @@ const uint8_t *hyper_response_reason_phrase(const struct hyper_response *resp); */ size_t hyper_response_reason_phrase_len(const struct hyper_response *resp); -/* - Get a reference to the full raw headers of this response. - - You must have enabled `hyper_clientconn_options_headers_raw()`, or this - will return NULL. - - The returned `hyper_buf *` is just a reference, owned by the response. - You need to make a copy if you wish to use it after freeing the - response. - - The buffer is not null-terminated, see the `hyper_buf` functions for - getting the bytes and length. - */ -const struct hyper_buf *hyper_response_headers_raw(const struct hyper_response *resp); - /* Get the HTTP version used by this response. diff --git a/src/client/conn.rs b/src/client/conn.rs index 8da457da64..8096795542 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -192,8 +192,7 @@ pub struct Builder { h1_preserve_header_order: bool, h1_read_buf_exact_size: Option, h1_max_buf_size: Option, - #[cfg(feature = "ffi")] - h1_headers_raw: bool, + h1_raw_message: bool, #[cfg(feature = "http2")] h2_builder: proto::h2::client::Config, version: Proto, @@ -603,8 +602,7 @@ impl Builder { #[cfg(feature = "ffi")] h1_preserve_header_order: false, h1_max_buf_size: None, - #[cfg(feature = "ffi")] - h1_headers_raw: false, + h1_raw_message: false, #[cfg(feature = "http2")] h2_builder: Default::default(), #[cfg(feature = "http1")] @@ -811,9 +809,16 @@ impl Builder { self } - #[cfg(feature = "ffi")] - pub(crate) fn http1_headers_raw(&mut self, enabled: bool) -> &mut Self { - self.h1_headers_raw = enabled; + /// Set whether to include the raw bytes of HTTP/1 responses. + /// + /// This will store a [`Http1RawMessage`](crate::ext::Http1RawMessage) + /// in extensions of HTTP/1 responses. + /// + /// Note that this setting does not affect HTTP/2. + /// + /// Default is false. + pub fn http1_raw_message(&mut self, enabled: bool) -> &mut Self { + self.h1_raw_message = enabled; self } @@ -1033,8 +1038,9 @@ impl Builder { conn.set_h09_responses(); } - #[cfg(feature = "ffi")] - conn.set_raw_headers(opts.h1_headers_raw); + if opts.h1_raw_message { + conn.set_h1_raw_message(); + } if let Some(sz) = opts.h1_read_buf_exact_size { conn.set_read_buf_exact_size(sz); diff --git a/src/ext.rs b/src/ext.rs index 224206dd66..ae7cd47ea8 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -10,6 +10,7 @@ use http::HeaderMap; use std::collections::HashMap; #[cfg(feature = "http2")] use std::fmt; +use std::ops::Deref; #[cfg(any(feature = "http1", feature = "ffi"))] mod h1_reason_phrase; @@ -131,6 +132,41 @@ impl HeaderCaseMap { } } +/// Raw bytes of HTTP/1 requests and responses. +/// +/// Included in HTTP/1 requests and responses when `http1_raw_message` is set +/// to true. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Http1RawMessage { + pub(crate) buf: Bytes, +} + +impl Deref for Http1RawMessage { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.buf + } +} + +impl AsRef for Http1RawMessage { + fn as_ref(&self) -> &Bytes { + &self.buf + } +} + +impl AsRef<[u8]> for Http1RawMessage { + fn as_ref(&self) -> &[u8] { + &self + } +} + +impl From for Bytes { + fn from(message: Http1RawMessage) -> Self { + message.buf + } +} + #[cfg(feature = "ffi")] #[derive(Clone, Debug)] /// Hashmap diff --git a/src/ffi/client.rs b/src/ffi/client.rs index 670f77d141..485a3bd81b 100644 --- a/src/ffi/client.rs +++ b/src/ffi/client.rs @@ -166,17 +166,3 @@ ffi_fn! { } } } - -ffi_fn! { - /// Set the whether to include a copy of the raw headers in responses - /// received on this connection. - /// - /// Pass `0` to disable, `1` to enable. - /// - /// If enabled, see `hyper_response_headers_raw()` for usage. - fn hyper_clientconn_options_headers_raw(opts: *mut hyper_clientconn_options, enabled: c_int) -> hyper_code { - let opts = non_null! { &mut *opts ?= hyper_code::HYPERE_INVALID_ARG }; - opts.builder.http1_headers_raw(enabled != 0); - hyper_code::HYPERE_OK - } -} diff --git a/src/ffi/http_types.rs b/src/ffi/http_types.rs index ea10f139cb..098f618e1f 100644 --- a/src/ffi/http_types.rs +++ b/src/ffi/http_types.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use libc::{c_int, size_t}; use std::ffi::c_void; -use super::body::{hyper_body, hyper_buf}; +use super::body::hyper_body; use super::error::hyper_code; use super::task::{hyper_task_return_type, AsTaskType}; use super::{UserDataPointer, HYPER_ITER_CONTINUE}; @@ -25,8 +25,6 @@ pub struct hyper_headers { orig_order: OriginalHeaderOrder, } -pub(crate) struct RawHeaders(pub(crate) hyper_buf); - pub(crate) struct OnInformational { func: hyper_request_on_informational_callback, data: UserDataPointer, @@ -278,27 +276,6 @@ ffi_fn! { } } -ffi_fn! { - /// Get a reference to the full raw headers of this response. - /// - /// You must have enabled `hyper_clientconn_options_headers_raw()`, or this - /// will return NULL. - /// - /// The returned `hyper_buf *` is just a reference, owned by the response. - /// You need to make a copy if you wish to use it after freeing the - /// response. - /// - /// The buffer is not null-terminated, see the `hyper_buf` functions for - /// getting the bytes and length. - fn hyper_response_headers_raw(resp: *const hyper_response) -> *const hyper_buf { - let resp = non_null!(&*resp ?= std::ptr::null()); - match resp.0.extensions().get::() { - Some(raw) => &raw.0, - None => std::ptr::null(), - } - } ?= std::ptr::null() -} - ffi_fn! { /// Get the HTTP version used by this response. /// diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs index 5ab72f264e..4d044c67e7 100644 --- a/src/proto/h1/conn.rs +++ b/src/proto/h1/conn.rs @@ -66,8 +66,7 @@ where h09_responses: false, #[cfg(feature = "ffi")] on_informational: None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, notify_read: false, reading: Reading::Init, writing: Writing::Init, @@ -135,9 +134,8 @@ where self.state.allow_half_close = true; } - #[cfg(feature = "ffi")] - pub(crate) fn set_raw_headers(&mut self, enabled: bool) { - self.state.raw_headers = enabled; + pub(crate) fn set_h1_raw_message(&mut self) { + self.state.h1_raw_message = true; } pub(crate) fn into_inner(self) -> (I, Bytes) { @@ -210,8 +208,7 @@ where h09_responses: self.state.h09_responses, #[cfg(feature = "ffi")] on_informational: &mut self.state.on_informational, - #[cfg(feature = "ffi")] - raw_headers: self.state.raw_headers, + h1_raw_message: self.state.h1_raw_message, } )) { Ok(msg) => msg, @@ -838,8 +835,7 @@ struct State { /// received. #[cfg(feature = "ffi")] on_informational: Option, - #[cfg(feature = "ffi")] - raw_headers: bool, + h1_raw_message: bool, /// Set to true when the Dispatcher should poll read operations /// again. See the `maybe_notify` method for more. notify_read: bool, diff --git a/src/proto/h1/io.rs b/src/proto/h1/io.rs index 02d8a4a9ec..4ba8120833 100644 --- a/src/proto/h1/io.rs +++ b/src/proto/h1/io.rs @@ -200,8 +200,7 @@ where h09_responses: parse_ctx.h09_responses, #[cfg(feature = "ffi")] on_informational: parse_ctx.on_informational, - #[cfg(feature = "ffi")] - raw_headers: parse_ctx.raw_headers, + h1_raw_message: parse_ctx.h1_raw_message, }, )? { Some(msg) => { @@ -745,8 +744,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; assert!(buffered .parse::(cx, parse_ctx) diff --git a/src/proto/h1/mod.rs b/src/proto/h1/mod.rs index 5a2587a843..75f446e975 100644 --- a/src/proto/h1/mod.rs +++ b/src/proto/h1/mod.rs @@ -88,8 +88,7 @@ pub(crate) struct ParseContext<'a> { h09_responses: bool, #[cfg(feature = "ffi")] on_informational: &'a mut Option, - #[cfg(feature = "ffi")] - raw_headers: bool, + h1_raw_message: bool, } /// Passed to Http1Transaction::encode diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 6252207baf..c72c288731 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -15,9 +15,9 @@ use crate::body::DecodedLength; #[cfg(feature = "server")] use crate::common::date; use crate::error::Parse; -use crate::ext::HeaderCaseMap; #[cfg(feature = "ffi")] use crate::ext::OriginalHeaderOrder; +use crate::ext::{HeaderCaseMap, Http1RawMessage}; use crate::headers; use crate::proto::h1::{ Encode, Encoder, Http1Transaction, ParseContext, ParseResult, ParsedMessage, @@ -193,6 +193,12 @@ impl Http1Transaction for Server { let slice = buf.split_to(len).freeze(); + let mut extensions = http::Extensions::default(); + + if ctx.h1_raw_message { + extensions.insert(Http1RawMessage { buf: slice.clone() }); + } + // According to https://tools.ietf.org/html/rfc7230#section-3.3.3 // 1. (irrelevant to Request) // 2. (irrelevant to Request) @@ -311,8 +317,6 @@ impl Http1Transaction for Server { return Err(Parse::transfer_encoding_invalid()); } - let mut extensions = http::Extensions::default(); - if let Some(header_case_map) = header_case_map { extensions.insert(header_case_map); } @@ -986,10 +990,18 @@ impl Http1Transaction for Client { let mut slice = buf.split_to(len); - if ctx + let mut extensions = http::Extensions::default(); + + let slice = if ctx .h1_parser_config .obsolete_multiline_headers_in_responses_are_allowed() { + if ctx.h1_raw_message { + extensions.insert(Http1RawMessage { + buf: slice.clone().freeze(), + }); + } + for header in &headers_indices[..headers_len] { // SAFETY: array is valid up to `headers_len` let header = unsafe { &*header.as_ptr() }; @@ -999,9 +1011,17 @@ impl Http1Transaction for Client { } } } - } - let slice = slice.freeze(); + slice.freeze() + } else { + let slice = slice.freeze(); + + if ctx.h1_raw_message { + extensions.insert(Http1RawMessage { buf: slice.clone() }); + } + + slice + }; let mut headers = ctx.cached_headers.take().unwrap_or_else(HeaderMap::new); @@ -1050,8 +1070,6 @@ impl Http1Transaction for Client { headers.append(name, value); } - let mut extensions = http::Extensions::default(); - if let Some(header_case_map) = header_case_map { extensions.insert(header_case_map); } @@ -1068,11 +1086,6 @@ impl Http1Transaction for Client { extensions.insert(reason); } - #[cfg(feature = "ffi")] - if ctx.raw_headers { - extensions.insert(crate::ffi::RawHeaders(crate::ffi::hyper_buf(slice))); - } - let head = MessageHead { version, subject: status, @@ -1535,8 +1548,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .unwrap() @@ -1570,8 +1582,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); assert_eq!(raw.len(), 0); @@ -1600,8 +1611,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; Server::parse(&mut raw, ctx).unwrap_err(); } @@ -1628,8 +1638,7 @@ mod tests { h09_responses: true, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); assert_eq!(raw, H09_RESPONSE); @@ -1658,8 +1667,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; Client::parse(&mut raw, ctx).unwrap_err(); assert_eq!(raw, H09_RESPONSE); @@ -1692,8 +1700,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; let msg = Client::parse(&mut raw, ctx).unwrap().unwrap(); assert_eq!(raw.len(), 0); @@ -1723,8 +1730,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; Client::parse(&mut raw, ctx).unwrap_err(); } @@ -1749,8 +1755,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }; let parsed_message = Server::parse(&mut raw, ctx).unwrap().unwrap(); let orig_headers = parsed_message @@ -1796,8 +1801,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .expect("parse ok") @@ -1824,8 +1828,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .expect_err(comment) @@ -2061,8 +2064,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, } ) .expect("parse ok") @@ -2089,8 +2091,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .expect("parse ok") @@ -2117,8 +2118,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .expect_err("parse should err") @@ -2622,8 +2622,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .expect("parse ok") @@ -2714,8 +2713,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .unwrap() @@ -2762,8 +2760,7 @@ mod tests { h09_responses: false, #[cfg(feature = "ffi")] on_informational: &mut None, - #[cfg(feature = "ffi")] - raw_headers: false, + h1_raw_message: false, }, ) .unwrap() diff --git a/src/server/conn.rs b/src/server/conn.rs index 8ce4c95193..a2bcc9d54c 100644 --- a/src/server/conn.rs +++ b/src/server/conn.rs @@ -113,6 +113,7 @@ pub struct Http { h1_keep_alive: bool, h1_title_case_headers: bool, h1_preserve_header_case: bool, + h1_raw_message: bool, #[cfg(all(feature = "http1", feature = "runtime"))] h1_header_read_timeout: Option, h1_writev: Option, @@ -259,6 +260,7 @@ impl Http { h1_keep_alive: true, h1_title_case_headers: false, h1_preserve_header_case: false, + h1_raw_message: false, #[cfg(all(feature = "http1", feature = "runtime"))] h1_header_read_timeout: None, h1_writev: None, @@ -349,6 +351,19 @@ impl Http { self } + /// Set whether to include the raw bytes of HTTP/1 requests. + /// + /// This will store a [`Http1RawMessage`](crate::ext::Http1RawMessage) + /// in extensions of HTTP/1 requests. + /// + /// Default is false. + #[cfg(feature = "http1")] + #[cfg_attr(docsrs, doc(cfg(feature = "http1")))] + pub fn http1_raw_message(&mut self, enabled: bool) -> &mut Self { + self.h1_raw_message = enabled; + self + } + /// Set a timeout for reading client request headers. If a client does not /// transmit the entire header within this time, the connection is closed. /// @@ -606,6 +621,7 @@ impl Http { h1_keep_alive: self.h1_keep_alive, h1_title_case_headers: self.h1_title_case_headers, h1_preserve_header_case: self.h1_preserve_header_case, + h1_raw_message: self.h1_raw_message, #[cfg(all(feature = "http1", feature = "runtime"))] h1_header_read_timeout: self.h1_header_read_timeout, h1_writev: self.h1_writev, @@ -670,6 +686,9 @@ impl Http { if self.h1_preserve_header_case { conn.set_preserve_header_case(); } + if self.h1_raw_message { + conn.set_h1_raw_message(); + } #[cfg(all(feature = "http1", feature = "runtime"))] if let Some(header_read_timeout) = self.h1_header_read_timeout { conn.set_http1_header_read_timeout(header_read_timeout); diff --git a/src/server/conn/http1.rs b/src/server/conn/http1.rs index ab833b938b..a219b8d2b2 100644 --- a/src/server/conn/http1.rs +++ b/src/server/conn/http1.rs @@ -42,6 +42,7 @@ pub struct Builder { h1_keep_alive: bool, h1_title_case_headers: bool, h1_preserve_header_case: bool, + h1_raw_message: bool, h1_header_read_timeout: Option, h1_writev: Option, max_buf_size: Option, @@ -208,6 +209,7 @@ impl Builder { h1_keep_alive: true, h1_title_case_headers: false, h1_preserve_header_case: false, + h1_raw_message: false, h1_header_read_timeout: None, h1_writev: None, max_buf_size: None, @@ -260,6 +262,17 @@ impl Builder { self } + /// Set whether to include the raw bytes of HTTP/1 requests and responses. + /// + /// This will store a [`ext::Http1RawMessage`] in extensions of + /// HTTP/1 requests and responses. + /// + /// Default is false. + pub fn raw_message(&mut self, enabled: bool) -> &mut Self { + self.h1_raw_message = enabled; + self + } + /// Set a timeout for reading client request headers. If a client does not /// transmit the entire header within this time, the connection is closed. /// @@ -370,6 +383,9 @@ impl Builder { if self.h1_preserve_header_case { conn.set_preserve_header_case(); } + if self.h1_raw_message { + conn.set_h1_raw_message(); + } if let Some(header_read_timeout) = self.h1_header_read_timeout { conn.set_http1_header_read_timeout(header_read_timeout); } diff --git a/src/server/server.rs b/src/server/server.rs index 6d7d69e51b..72702f3ede 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -257,6 +257,19 @@ impl Builder { self } + /// Set whether to include the raw bytes of HTTP/1 requests. + /// + /// This will store a [`Http1RawMessage`](crate::ext::Http1RawMessage) + /// in extensions of HTTP/1 requests. + /// + /// Default is false. + #[cfg(feature = "http1")] + #[cfg_attr(docsrs, doc(cfg(feature = "http1")))] + pub fn http1_raw_message(mut self, enabled: bool) -> Self { + self.protocol.http1_raw_message(enabled); + self + } + /// Set whether HTTP/1 connections should support half-closures. /// /// Clients can chose to shutdown their write-side while waiting diff --git a/tests/client.rs b/tests/client.rs index 88fcd3a564..e934fe8ea3 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -2200,6 +2200,7 @@ mod conn { use tokio::net::{TcpListener as TkTcpListener, TcpStream}; use hyper::client::conn; + use hyper::ext::Http1RawMessage; use hyper::{self, Body, Method, Request, Response, StatusCode}; use super::{concat, s, support, tcp_connect, FutureHyperExt}; @@ -2381,6 +2382,8 @@ mod conn { .unwrap(); let addr = listener.local_addr().unwrap(); + const RESPONSE: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: \r\n 0\r\nLine-Folded-Header: hello\r\n world \r\n \r\n\r\n"; + let server = async move { let mut sock = listener.accept().await.unwrap().0; let mut buf = [0; 4096]; @@ -2392,15 +2395,14 @@ mod conn { 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 OK\r\nContent-Length: \r\n 0\r\nLine-Folded-Header: hello\r\n world \r\n \r\n\r\n") - .await - .unwrap(); + sock.write_all(RESPONSE).await.unwrap(); }; let client = async move { let tcp = tcp_connect(&addr).await.expect("connect"); let (mut client, conn) = conn::Builder::new() .http1_allow_obsolete_multiline_headers_in_responses(true) + .http1_raw_message(true) .handshake::<_, Body>(tcp) .await .expect("handshake"); @@ -2414,16 +2416,25 @@ mod conn { .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.headers().len(), 2); + assert_eq!( res.headers().get(http::header::CONTENT_LENGTH).unwrap(), "0" ); + assert_eq!( res.headers().get("line-folded-header").unwrap(), "hello world" ); + + let raw_response = res.extensions().get::().unwrap(); + + // The raw response includes the line folded headers prior to \r\n replacement. + assert_eq!(**raw_response, *RESPONSE); + assert!(res.body_mut().next().await.is_none()); }; diff --git a/tests/server.rs b/tests/server.rs index 786bb4b42f..a16ec8f42a 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -28,6 +28,7 @@ use tokio::net::{TcpListener, TcpStream as TkTcpStream}; use hyper::body::HttpBody as _; use hyper::client::Client; +use hyper::ext::Http1RawMessage; use hyper::server::conn::Http; use hyper::server::Server; use hyper::service::{make_service_fn, service_fn}; @@ -58,20 +59,21 @@ fn get_should_ignore_body() { #[test] fn get_with_body() { - let server = serve(); - let mut req = connect(server.addr()); - req.write_all( - b"\ + const REQUEST_HEAD: &[u8] = b"\ GET / HTTP/1.1\r\n\ Host: example.domain\r\n\ Content-Length: 19\r\n\ \r\n\ - I'm a good request.\r\n\ - ", - ) - .unwrap(); + "; + + let server = serve_opts().http1_raw_message().serve(); + let mut req = connect(server.addr()); + req.write_all(REQUEST_HEAD).unwrap(); + req.write_all(b"I'm a good request.\r\n").unwrap(); req.read(&mut [0; 256]).unwrap(); + assert_eq!(*server.raw_request(), *REQUEST_HEAD); + // note: doesn't include trailing \r\n, cause Content-Length wasn't 21 assert_eq!(server.body(), b"I'm a good request."); } @@ -2815,6 +2817,13 @@ impl Serve { &self.addr } + fn raw_request(&self) -> Http1RawMessage { + match self.msg_rx.recv() { + Ok(Msg::RawRequest(raw_request)) => raw_request, + res => panic!("expected raw request, found: {:?}", res), + } + } + fn body(&self) -> Vec { self.try_body().expect("body") } @@ -2832,7 +2841,7 @@ impl Serve { } Ok(Msg::Error(e)) => return Err(e), Ok(Msg::End) => break, - Err(e) => panic!("expected body, found: {:?}", e), + res => panic!("expected body, found: {:?}", res), } } Ok(buf) @@ -2950,6 +2959,7 @@ enum Reply { #[derive(Debug)] enum Msg { + RawRequest(Http1RawMessage), Chunk(Vec), Error(hyper::Error), End, @@ -2968,6 +2978,10 @@ impl tower_service::Service> for TestService { let tx = self.tx.clone(); let replies = self.reply.clone(); + if let Some(raw_request) = req.extensions_mut().remove::() { + tx.send(Msg::RawRequest(raw_request)).unwrap(); + } + Box::pin(async move { while let Some(chunk) = req.data().await { match chunk { @@ -3067,6 +3081,7 @@ fn serve_opts() -> ServeOptions { struct ServeOptions { keep_alive: bool, http1_only: bool, + http1_raw_message: bool, pipeline: bool, } @@ -3075,6 +3090,7 @@ impl Default for ServeOptions { ServeOptions { keep_alive: true, http1_only: false, + http1_raw_message: false, pipeline: false, } } @@ -3086,6 +3102,11 @@ impl ServeOptions { self } + fn http1_raw_message(mut self) -> Self { + self.http1_raw_message = true; + self + } + fn keep_alive(mut self, enabled: bool) -> Self { self.keep_alive = enabled; self @@ -3133,6 +3154,7 @@ impl ServeOptions { let builder = builder .http1_only(_options.http1_only) .http1_keepalive(_options.keep_alive) + .http1_raw_message(_options.http1_raw_message) .http1_pipeline_flush(_options.pipeline); let server = builder.serve(service);