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 8f98fe67e..60bf2262b 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 camel_case(&self) -> bool { + false + } + fn chunked(&self) -> bool; fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>; @@ -57,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() { @@ -74,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"), @@ -95,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") + } } _ => (), } @@ -133,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; @@ -158,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; @@ -221,6 +256,10 @@ impl MessageType for RequestHead { self.chunked() } + fn camel_case(&self) -> bool { + RequestHead::camel_case_headers(self) + } + fn headers(&self) -> &HeaderMap { &self.headers } @@ -418,11 +457,41 @@ impl<'a> io::Write for Writer<'a> { } } +fn write_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::*; use bytes::Bytes; + use super::*; + use crate::http::header::{HeaderValue, CONTENT_TYPE}; + #[test] fn test_chunked_te() { let mut bytes = BytesMut::new(); @@ -436,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 7f2dc603f..c279aaebf 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 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_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/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 2e6032649..5c09df816 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -235,6 +235,13 @@ impl ClientRequest { self } + /// Send headers in `Camel-Case` form. + #[inline] + pub fn camel_case(mut self) -> Self { + self.head.set_camel_case_headers(true); + self + } + /// Force close connection instead of returning it back to connections pool. /// This setting affect only http/1 connections. #[inline] 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]