Skip to content

Commit

Permalink
feat(http1): add options to preserve header casing (#2480)
Browse files Browse the repository at this point in the history
Decouple preserving header case from FFI:

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.

Closes #2313
  • Loading branch information
nox authored Apr 21, 2021
1 parent 117cc49 commit dbea771
Show file tree
Hide file tree
Showing 12 changed files with 656 additions and 181 deletions.
11 changes: 11 additions & 0 deletions src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,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
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub struct Builder {
h09_responses: bool,
h1_parser_config: ParserConfig,
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 @@ -500,6 +501,7 @@ impl Builder {
h1_read_buf_exact_size: None,
h1_parser_config: Default::default(),
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 @@ -537,6 +539,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 @@ -719,6 +726,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
64 changes: 64 additions & 0 deletions src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! HTTP extensions

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

/// A map from header names to their original casing as received in an HTTP message.
///
/// 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)]
pub(crate) 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(crate) fn get_all<'a>(
&'a self,
name: &HeaderName,
) -> impl Iterator<Item = impl AsRef<[u8]> + 'a> + 'a {
self.get_all_internal(name).into_iter()
}

/// Returns a view of all spellings associated with that header name,
/// in the order they were found.
pub(crate) fn get_all_internal<'a>(&'a self, name: &HeaderName) -> ValueIter<'_, Bytes> {
self.0.get_all(name).into_iter()
}

pub(crate) fn default() -> Self {
Self(Default::default())
}

#[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
Original file line number Diff line number Diff line change
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
45 changes: 14 additions & 31 deletions src/ffi/http_types.rs
Original file line number Diff line number Diff line change
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 @@ -18,16 +19,11 @@ pub struct hyper_response(pub(super) Response<Body>);
/// An HTTP header map.
///
/// These can be part of a request or response.
#[derive(Default)]
pub struct hyper_headers {
pub(super) headers: HeaderMap,
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 @@ -229,7 +225,7 @@ impl hyper_response {
let orig_casing = resp
.extensions_mut()
.remove::<HeaderCaseMap>()
.unwrap_or_default();
.unwrap_or_else(HeaderCaseMap::default);
resp.extensions_mut().insert(hyper_headers {
headers,
orig_casing,
Expand Down Expand Up @@ -265,10 +261,7 @@ type hyper_headers_foreach_callback =
impl hyper_headers {
pub(super) fn get_or_default(ext: &mut http::Extensions) -> &mut hyper_headers {
if let None = ext.get_mut::<hyper_headers>() {
ext.insert(hyper_headers {
headers: Default::default(),
orig_casing: Default::default(),
});
ext.insert(hyper_headers::default());
}

ext.get_mut::<hyper_headers>().unwrap()
Expand All @@ -290,11 +283,11 @@ ffi_fn! {
//
// TODO: consider adding http::HeaderMap::entries() iterator
for name in headers.headers.keys() {
let mut names = headers.orig_casing.get_all(name).iter();
let mut names = headers.orig_casing.get_all(name);

for value in headers.headers.get_all(name) {
let (name_ptr, name_len) = if let Some(orig_name) = names.next() {
(orig_name.as_ptr(), orig_name.len())
(orig_name.as_ref().as_ptr(), orig_name.as_ref().len())
} else {
(
name.as_str().as_bytes().as_ptr(),
Expand Down Expand Up @@ -349,6 +342,15 @@ ffi_fn! {
}
}

impl Default for hyper_headers {
fn default() -> Self {
Self {
headers: Default::default(),
orig_casing: HeaderCaseMap::default(),
}
}
}

unsafe fn raw_name_value(
name: *const u8,
name_len: size_t,
Expand All @@ -370,25 +372,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
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ mod cfg;
mod common;
pub mod body;
mod error;
mod ext;
#[cfg(test)]
mod mock;
#[cfg(any(feature = "http1", feature = "http2",))]
Expand Down
22 changes: 6 additions & 16 deletions src/proto/h1/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ where
keep_alive: KA::Busy,
method: None,
h1_parser_config: ParserConfig::default(),
#[cfg(feature = "ffi")]
preserve_header_case: false,
title_case_headers: false,
h09_responses: false,
Expand Down Expand Up @@ -77,13 +76,16 @@ where
}

#[cfg(feature = "client")]
pub(crate) fn set_h1_parser_config(&mut self, parser_config: ParserConfig) {
self.state.h1_parser_config = parser_config;
}

pub(crate) fn set_title_case_headers(&mut self) {
self.state.title_case_headers = true;
}

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

#[cfg(feature = "client")]
Expand Down Expand Up @@ -158,7 +160,6 @@ where
cached_headers: &mut self.state.cached_headers,
req_method: &mut self.state.method,
h1_parser_config: self.state.h1_parser_config.clone(),
#[cfg(feature = "ffi")]
preserve_header_case: self.state.preserve_header_case,
h09_responses: self.state.h09_responses,
}
Expand Down Expand Up @@ -499,16 +500,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 @@ -772,7 +763,6 @@ struct State {
/// a body or not.
method: Option<Method>,
h1_parser_config: ParserConfig,
#[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
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ where
cached_headers: parse_ctx.cached_headers,
req_method: parse_ctx.req_method,
h1_parser_config: parse_ctx.h1_parser_config.clone(),
#[cfg(feature = "ffi")]
preserve_header_case: parse_ctx.preserve_header_case,
h09_responses: parse_ctx.h09_responses,
},
Expand Down Expand Up @@ -644,7 +643,6 @@ mod tests {
cached_headers: &mut None,
req_method: &mut None,
h1_parser_config: Default::default(),
#[cfg(feature = "ffi")]
preserve_header_case: false,
h09_responses: false,
};
Expand Down
1 change: 0 additions & 1 deletion src/proto/h1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ pub(crate) struct ParseContext<'a> {
cached_headers: &'a mut Option<HeaderMap>,
req_method: &'a mut Option<Method>,
h1_parser_config: ParserConfig,
#[cfg(feature = "ffi")]
preserve_header_case: bool,
h09_responses: bool,
}
Expand Down
Loading

0 comments on commit dbea771

Please sign in to comment.