Skip to content

Commit

Permalink
feat(http1): decouple preserving header case from FFI (fixes hyperium…
Browse files Browse the repository at this point in the history
…#2313)

The feature is now supported in both the server and the client
and can be combined with the title case feature, for headers
which don't have entries in the header case map.
  • Loading branch information
nox committed Apr 8, 2021
1 parent 98e7e0b commit 4770598
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 164 deletions.
11 changes: 11 additions & 0 deletions src/client/client.rs
Expand Up @@ -972,6 +972,17 @@ impl Builder {
self
}

/// Set whether HTTP/1 connections will write header names as provided
/// at the socket level.
///
/// Note that this setting does not affect HTTP/2.
///
/// Default is false.
pub fn http1_preserve_header_case(&mut self, val: bool) -> &mut Self {
self.conn_builder.h1_preserve_header_case(val);
self
}

/// Set whether HTTP/0.9 responses should be tolerated.
///
/// Default is false.
Expand Down
10 changes: 10 additions & 0 deletions src/client/conn.rs
Expand Up @@ -124,6 +124,7 @@ pub struct Builder {
pub(super) exec: Exec,
h09_responses: bool,
h1_title_case_headers: bool,
h1_preserve_header_case: bool,
h1_read_buf_exact_size: Option<usize>,
h1_max_buf_size: Option<usize>,
#[cfg(feature = "http2")]
Expand Down Expand Up @@ -497,6 +498,7 @@ impl Builder {
h09_responses: false,
h1_read_buf_exact_size: None,
h1_title_case_headers: false,
h1_preserve_header_case: false,
h1_max_buf_size: None,
#[cfg(feature = "http2")]
h2_builder: Default::default(),
Expand Down Expand Up @@ -526,6 +528,11 @@ impl Builder {
self
}

pub(crate) fn h1_preserve_header_case(&mut self, enabled: bool) -> &mut Builder {
self.h1_preserve_header_case = enabled;
self
}

pub(super) fn h1_read_buf_exact_size(&mut self, sz: Option<usize>) -> &mut Builder {
self.h1_read_buf_exact_size = sz;
self.h1_max_buf_size = None;
Expand Down Expand Up @@ -707,6 +714,9 @@ impl Builder {
if opts.h1_title_case_headers {
conn.set_title_case_headers();
}
if opts.h1_preserve_header_case {
conn.set_preserve_header_case();
}
if opts.h09_responses {
conn.set_h09_responses();
}
Expand Down
51 changes: 51 additions & 0 deletions src/ext.rs
@@ -0,0 +1,51 @@
//! HTTP extensions

use bytes::Bytes;
#[cfg(feature = "http1")]
use http::header::{GetAll, HeaderName, IntoHeaderName};
use http::HeaderMap;

/// A map from header names to their original casing as received in an HTTP response.
///
/// If an HTTP/1 response `res` is parsed on a connection whose option
/// [`http1_preserve_header_case`] was set to true and the response included
/// the following headers:
///
/// ```ignore
/// x-Bread: Baguette
/// X-BREAD: Pain
/// x-bread: Ficelle
/// ```
///
/// Then `res.extensions().get::<HeaderCaseMap>()` will return a map with:
///
/// ```ignore
/// HeaderCaseMap({
/// "x-bread": ["x-Bread", "X-BREAD", "x-bread"],
/// })
/// ```
///
/// [`http1_preserve_header_case`]: /client/struct.Client.html#method.http1_preserve_header_case
#[derive(Clone, Debug, Default)]
pub struct HeaderCaseMap(HeaderMap<Bytes>);

#[cfg(feature = "http1")]
impl HeaderCaseMap {
/// Returns a view of all spellings associated with that header name,
/// in the order they were found.
pub fn get_all(&self, name: &HeaderName) -> GetAll<'_, Bytes> {
self.0.get_all(name)
}

#[cfg(any(test, feature = "ffi"))]
pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
self.0.insert(name, orig);
}

pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
where
N: IntoHeaderName,
{
self.0.append(name, orig);
}
}
5 changes: 4 additions & 1 deletion src/ffi/client.rs
Expand Up @@ -106,8 +106,11 @@ unsafe impl AsTaskType for hyper_clientconn {
ffi_fn! {
/// Creates a new set of HTTP clientconn options to be used in a handshake.
fn hyper_clientconn_options_new() -> *mut hyper_clientconn_options {
let mut builder = conn::Builder::new();
builder.h1_preserve_header_case(true);

Box::into_raw(Box::new(hyper_clientconn_options {
builder: conn::Builder::new(),
builder,
exec: WeakExec::new(),
}))
} ?= std::ptr::null_mut()
Expand Down
24 changes: 1 addition & 23 deletions src/ffi/http_types.rs
Expand Up @@ -6,6 +6,7 @@ use super::body::hyper_body;
use super::error::hyper_code;
use super::task::{hyper_task_return_type, AsTaskType};
use super::HYPER_ITER_CONTINUE;
use crate::ext::HeaderCaseMap;
use crate::header::{HeaderName, HeaderValue};
use crate::{Body, HeaderMap, Method, Request, Response, Uri};

Expand All @@ -24,10 +25,6 @@ pub struct hyper_headers {
orig_casing: HeaderCaseMap,
}

// Will probably be moved to `hyper::ext::http1`
#[derive(Debug, Default)]
pub(crate) struct HeaderCaseMap(HeaderMap<Bytes>);

#[derive(Debug)]
pub(crate) struct ReasonPhrase(pub(crate) Bytes);

Expand Down Expand Up @@ -370,25 +367,6 @@ unsafe fn raw_name_value(
Ok((name, value, orig_name))
}

// ===== impl HeaderCaseMap =====

impl HeaderCaseMap {
pub(crate) fn get_all(&self, name: &HeaderName) -> http::header::GetAll<'_, Bytes> {
self.0.get_all(name)
}

pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
self.0.insert(name, orig);
}

pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
where
N: http::header::IntoHeaderName,
{
self.0.append(name, orig);
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion src/ffi/mod.rs
Expand Up @@ -62,7 +62,7 @@ pub use self::io::*;
pub use self::task::*;

pub(crate) use self::body::UserBody;
pub(crate) use self::http_types::{HeaderCaseMap, ReasonPhrase};
pub(crate) use self::http_types::ReasonPhrase;

/// Return in iter functions to continue iterating.
pub const HYPER_ITER_CONTINUE: libc::c_int = 0;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -80,6 +80,7 @@ mod cfg;
mod common;
pub mod body;
mod error;
pub mod ext;
#[cfg(test)]
mod mock;
#[cfg(any(feature = "http1", feature = "http2",))]
Expand Down
18 changes: 5 additions & 13 deletions src/proto/h1/conn.rs
Expand Up @@ -44,7 +44,6 @@ where
error: None,
keep_alive: KA::Busy,
method: None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
title_case_headers: false,
h09_responses: false,
Expand Down Expand Up @@ -79,6 +78,11 @@ where
self.state.title_case_headers = true;
}

#[cfg(feature = "client")]
pub(crate) fn set_preserve_header_case(&mut self) {
self.state.preserve_header_case = true;
}

#[cfg(feature = "client")]
pub(crate) fn set_h09_responses(&mut self) {
self.state.h09_responses = true;
Expand Down Expand Up @@ -150,7 +154,6 @@ where
ParseContext {
cached_headers: &mut self.state.cached_headers,
req_method: &mut self.state.method,
#[cfg(feature = "ffi")]
preserve_header_case: self.state.preserve_header_case,
h09_responses: self.state.h09_responses,
}
Expand Down Expand Up @@ -488,16 +491,6 @@ where

self.enforce_version(&mut head);

// Maybe check if we should preserve header casing on received
// message headers...
#[cfg(feature = "ffi")]
{
if T::is_client() && !self.state.preserve_header_case {
self.state.preserve_header_case =
head.extensions.get::<crate::ffi::HeaderCaseMap>().is_some();
}
}

let buf = self.io.headers_buf();
match super::role::encode_headers::<T>(
Encode {
Expand Down Expand Up @@ -760,7 +753,6 @@ struct State {
/// This is used to know things such as if the message can include
/// a body or not.
method: Option<Method>,
#[cfg(feature = "ffi")]
preserve_header_case: bool,
title_case_headers: bool,
h09_responses: bool,
Expand Down
2 changes: 0 additions & 2 deletions src/proto/h1/io.rs
Expand Up @@ -159,7 +159,6 @@ where
ParseContext {
cached_headers: parse_ctx.cached_headers,
req_method: parse_ctx.req_method,
#[cfg(feature = "ffi")]
preserve_header_case: parse_ctx.preserve_header_case,
h09_responses: parse_ctx.h09_responses,
},
Expand Down Expand Up @@ -639,7 +638,6 @@ mod tests {
let parse_ctx = ParseContext {
cached_headers: &mut None,
req_method: &mut None,
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
};
Expand Down
1 change: 0 additions & 1 deletion src/proto/h1/mod.rs
Expand Up @@ -70,7 +70,6 @@ pub(crate) struct ParsedMessage<T> {
pub(crate) struct ParseContext<'a> {
cached_headers: &'a mut Option<HeaderMap>,
req_method: &'a mut Option<Method>,
#[cfg(feature = "ffi")]
preserve_header_case: bool,
h09_responses: bool,
}
Expand Down

0 comments on commit 4770598

Please sign in to comment.