From 64f603b0766e52e21b1bf9bacdf8e2e250c54f31 Mon Sep 17 00:00:00 2001 From: Peter Ding Date: Thu, 25 Apr 2019 01:48:49 +0800 Subject: [PATCH 1/2] Support to set header names of `ClientRequest` as Camel-Case (#713) * Support to set header names of `ClientRequest` as Camel-Case This is the case for supporting to request for servers which don't perfectly implement the `RFC 7230`. It is important for an app which uses `ClientRequest` as core part. * Add field `upper_camel_case_headers` to `ClientRequest`. * Add function `set_upper_camel_case_headers` to `ClientRequest` and `ClientRequestBuilder` to set field `upper_camel_case_headers`. * Add trait `client::writer::UpperCamelCaseHeader` for `http::header::HeaderName`, let it can be converted to Camel-Case then writed to buffer. * Add test `test_client::test_upper_camel_case_headers`. * Support upper Camel-Case headers * [actix-http] Add field `upper_camel_case_headers` for `RequestHead` * [actix-http] Add code for `MessageType` to support upper camel case * [awc] Add functions for `ClientRequest` to set upper camel case * Use `Flags::CAMEL_CASE` for upper camel case of headers --- actix-http/src/h1/encoder.rs | 36 ++++++++++++++++++++++++++++++++++++ actix-http/src/message.rs | 18 ++++++++++++++++++ awc/src/request.rs | 14 ++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 8f98fe67e..177661b5b 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -43,6 +43,10 @@ pub(crate) trait MessageType: Sized { fn headers(&self) -> &HeaderMap; + fn upper_camel_case(&self) -> bool { + false + } + fn chunked(&self) -> bool; fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>; @@ -221,6 +225,10 @@ impl MessageType for RequestHead { self.chunked() } + fn upper_camel_case(&self) -> bool { + self.upper_camel_case_headers() + } + fn headers(&self) -> &HeaderMap { &self.headers } @@ -418,6 +426,34 @@ impl<'a> io::Write for Writer<'a> { } } +fn write_upper_camel_case(value: &[u8], buffer: &mut [u8]) { + let mut index = 0; + let key = value; + let mut key_iter = key.iter(); + + if let Some(c) = key_iter.next() { + if *c >= b'a' && *c <= b'z' { + buffer[index] = *c ^ b' '; + index += 1; + } + } else { + return; + } + + while let Some(c) = key_iter.next() { + buffer[index] = *c; + index += 1; + if *c == b'-' { + if let Some(c) = key_iter.next() { + if *c >= b'a' && *c <= b'z' { + buffer[index] = *c ^ b' '; + index += 1; + } + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 7f2dc603f..f3129c758 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -27,6 +27,7 @@ bitflags! { const UPGRADE = 0b0000_0100; const EXPECT = 0b0000_1000; const NO_CHUNKING = 0b0001_0000; + const CAMEL_CASE = 0b0010_0000; } } @@ -97,6 +98,23 @@ impl RequestHead { &mut self.headers } + /// Is to uppercase headers with Camel-Case. + /// Befault is `false` + #[inline] + pub fn upper_camel_case_headers(&self) -> bool { + self.flags.contains(Flags::CAMEL_CASE) + } + + /// Set `true` to send headers which are uppercased with Camel-Case. + #[inline] + pub fn set_upper_camel_case_headers(&mut self, val: bool) { + if val { + self.flags.insert(Flags::CAMEL_CASE); + } else { + self.flags.remove(Flags::CAMEL_CASE); + } + } + #[inline] /// Set connection type of the message pub fn set_connection_type(&mut self, ctype: ConnectionType) { diff --git a/awc/src/request.rs b/awc/src/request.rs index 2e6032649..d99a9418e 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -235,6 +235,20 @@ impl ClientRequest { self } + /// Is to uppercase headers with Camel-Case. + /// Befault is `false` + #[inline] + pub fn upper_camel_case_headers(&self) -> bool { + self.head.upper_camel_case_headers() + } + + /// Set `true` to send headers which are uppercased with Camel-Case. + #[inline] + pub fn set_upper_camel_case_headers(&mut self, value: bool) -> &mut Self { + self.head.set_upper_camel_case_headers(value); + self + } + /// Force close connection instead of returning it back to connections pool. /// This setting affect only http/1 connections. #[inline] From 2e19f572ee9b4270508408fd9521dffb22eb773e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 24 Apr 2019 11:27:57 -0700 Subject: [PATCH 2/2] add tests for camel case headers rendering --- actix-http/CHANGES.md | 5 ++ actix-http/src/h1/encoder.rs | 117 ++++++++++++++++++++++++++++---- actix-http/src/message.rs | 4 +- actix-http/tests/test_client.rs | 2 - awc/CHANGES.md | 4 ++ awc/src/request.rs | 13 +--- awc/tests/test_client.rs | 4 ++ 7 files changed, 123 insertions(+), 26 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 37d0eec65..c51b421c3 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,10 @@ # Changes +### Added + +* Allow to render h1 request headers in `Camel-Case` + + ## [0.1.3] - 2019-04-23 ### Fixed diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 177661b5b..60bf2262b 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -43,7 +43,7 @@ pub(crate) trait MessageType: Sized { fn headers(&self) -> &HeaderMap; - fn upper_camel_case(&self) -> bool { + fn camel_case(&self) -> bool { false } @@ -61,6 +61,7 @@ pub(crate) trait MessageType: Sized { ) -> io::Result<()> { let chunked = self.chunked(); let mut skip_len = length != BodySize::Stream; + let camel_case = self.camel_case(); // Content length if let Some(status) = self.status() { @@ -78,18 +79,30 @@ pub(crate) trait MessageType: Sized { match length { BodySize::Stream => { if chunked { - dst.put_slice(b"\r\ntransfer-encoding: chunked\r\n") + if camel_case { + dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") + } else { + dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") + } } else { skip_len = false; dst.put_slice(b"\r\n"); } } BodySize::Empty => { - dst.put_slice(b"\r\ncontent-length: 0\r\n"); + if camel_case { + dst.put_slice(b"\r\nContent-Length: 0\r\n"); + } else { + dst.put_slice(b"\r\ncontent-length: 0\r\n"); + } } BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized64(len) => { - dst.put_slice(b"\r\ncontent-length: "); + if camel_case { + dst.put_slice(b"\r\nContent-Length: "); + } else { + dst.put_slice(b"\r\ncontent-length: "); + } write!(dst.writer(), "{}\r\n", len)?; } BodySize::None => dst.put_slice(b"\r\n"), @@ -99,10 +112,18 @@ pub(crate) trait MessageType: Sized { match ctype { ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::KeepAlive if version < Version::HTTP_11 => { - dst.put_slice(b"connection: keep-alive\r\n") + if camel_case { + dst.put_slice(b"Connection: keep-alive\r\n") + } else { + dst.put_slice(b"connection: keep-alive\r\n") + } } ConnectionType::Close if version >= Version::HTTP_11 => { - dst.put_slice(b"connection: close\r\n") + if camel_case { + dst.put_slice(b"Connection: close\r\n") + } else { + dst.put_slice(b"connection: close\r\n") + } } _ => (), } @@ -137,7 +158,12 @@ pub(crate) trait MessageType: Sized { buf = &mut *(dst.bytes_mut() as *mut _); } } - buf[pos..pos + k.len()].copy_from_slice(k); + // use upper Camel-Case + if camel_case { + write_camel_case(k, &mut buf[pos..pos + k.len()]); + } else { + buf[pos..pos + k.len()].copy_from_slice(k); + } pos += k.len(); buf[pos..pos + 2].copy_from_slice(b": "); pos += 2; @@ -162,7 +188,12 @@ pub(crate) trait MessageType: Sized { buf = &mut *(dst.bytes_mut() as *mut _); } } - buf[pos..pos + k.len()].copy_from_slice(k); + // use upper Camel-Case + if camel_case { + write_camel_case(k, &mut buf[pos..pos + k.len()]); + } else { + buf[pos..pos + k.len()].copy_from_slice(k); + } pos += k.len(); buf[pos..pos + 2].copy_from_slice(b": "); pos += 2; @@ -225,8 +256,8 @@ impl MessageType for RequestHead { self.chunked() } - fn upper_camel_case(&self) -> bool { - self.upper_camel_case_headers() + fn camel_case(&self) -> bool { + RequestHead::camel_case_headers(self) } fn headers(&self) -> &HeaderMap { @@ -426,7 +457,7 @@ impl<'a> io::Write for Writer<'a> { } } -fn write_upper_camel_case(value: &[u8], buffer: &mut [u8]) { +fn write_camel_case(value: &[u8], buffer: &mut [u8]) { let mut index = 0; let key = value; let mut key_iter = key.iter(); @@ -456,9 +487,11 @@ fn write_upper_camel_case(value: &[u8], buffer: &mut [u8]) { #[cfg(test)] mod tests { - use super::*; use bytes::Bytes; + use super::*; + use crate::http::header::{HeaderValue, CONTENT_TYPE}; + #[test] fn test_chunked_te() { let mut bytes = BytesMut::new(); @@ -472,4 +505,64 @@ mod tests { Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n") ); } + + #[test] + fn test_camel_case() { + let mut bytes = BytesMut::with_capacity(2048); + let mut head = RequestHead::default(); + head.set_camel_case_headers(true); + head.headers.insert(DATE, HeaderValue::from_static("date")); + head.headers + .insert(CONTENT_TYPE, HeaderValue::from_static("plain/text")); + + let _ = head.encode_headers( + &mut bytes, + Version::HTTP_11, + BodySize::Empty, + ConnectionType::Close, + &ServiceConfig::default(), + ); + assert_eq!( + bytes.take().freeze(), + Bytes::from_static(b"\r\nContent-Length: 0\r\nConnection: close\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") + ); + + let _ = head.encode_headers( + &mut bytes, + Version::HTTP_11, + BodySize::Stream, + ConnectionType::KeepAlive, + &ServiceConfig::default(), + ); + assert_eq!( + bytes.take().freeze(), + Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") + ); + + let _ = head.encode_headers( + &mut bytes, + Version::HTTP_11, + BodySize::Sized64(100), + ConnectionType::KeepAlive, + &ServiceConfig::default(), + ); + assert_eq!( + bytes.take().freeze(), + Bytes::from_static(b"\r\nContent-Length: 100\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") + ); + + head.headers + .append(CONTENT_TYPE, HeaderValue::from_static("xml")); + let _ = head.encode_headers( + &mut bytes, + Version::HTTP_11, + BodySize::Stream, + ConnectionType::KeepAlive, + &ServiceConfig::default(), + ); + assert_eq!( + bytes.take().freeze(), + Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: xml\r\nContent-Type: plain/text\r\n\r\n") + ); + } } diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index f3129c758..c279aaebf 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -101,13 +101,13 @@ impl RequestHead { /// Is to uppercase headers with Camel-Case. /// Befault is `false` #[inline] - pub fn upper_camel_case_headers(&self) -> bool { + pub fn camel_case_headers(&self) -> bool { self.flags.contains(Flags::CAMEL_CASE) } /// Set `true` to send headers which are uppercased with Camel-Case. #[inline] - pub fn set_upper_camel_case_headers(&mut self, val: bool) { + pub fn set_camel_case_headers(&mut self, val: bool) { if val { self.flags.insert(Flags::CAMEL_CASE); } else { diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 6d382478f..a4f1569cc 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -59,9 +59,7 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - println!("REQ: {:?}", srv.get("/").force_close()); let response = srv.block_on(srv.get("/").force_close().send()).unwrap(); - println!("RES: {:?}", response); assert!(response.status().is_success()); } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 30fd4a6d2..10ab87bda 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,9 @@ # Changes +### Added + +* Allow to send headers in `Camel-Case` form. + ## [0.1.1] - 2019-04-19 ### Added diff --git a/awc/src/request.rs b/awc/src/request.rs index d99a9418e..5c09df816 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -235,17 +235,10 @@ impl ClientRequest { self } - /// Is to uppercase headers with Camel-Case. - /// Befault is `false` + /// Send headers in `Camel-Case` form. #[inline] - pub fn upper_camel_case_headers(&self) -> bool { - self.head.upper_camel_case_headers() - } - - /// Set `true` to send headers which are uppercased with Camel-Case. - #[inline] - pub fn set_upper_camel_case_headers(&mut self, value: bool) -> &mut Self { - self.head.set_upper_camel_case_headers(value); + pub fn camel_case(mut self) -> Self { + self.head.set_camel_case_headers(true); self } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 7e2dc6ba4..94684dd97 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -90,6 +90,10 @@ fn test_simple() { // read response let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + // camel case + let response = srv.block_on(srv.post("/").camel_case().send()).unwrap(); + assert!(response.status().is_success()); } #[test]