From 683b85d07e90b4d38359d8eb2f65d4b50950b2d8 Mon Sep 17 00:00:00 2001 From: Alexandru Cihodaru Date: Thu, 3 Sep 2020 18:23:24 +0300 Subject: [PATCH] Added support for Accept-Encoding Signed-off-by: Alexandru Cihodaru Signed-off-by: YUAN LYU --- src/common/headers.rs | 113 +++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 2 +- src/request.rs | 10 ++++ src/response.rs | 32 ++++++++++-- src/server.rs | 8 +++ 5 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/common/headers.rs b/src/common/headers.rs index c3546f0..dba0dac 100644 --- a/src/common/headers.rs +++ b/src/common/headers.rs @@ -21,6 +21,8 @@ pub enum Header { Server, /// Header `Accept` Accept, + /// Header `Accept-Encoding` + AcceptEncoding, } impl Header { @@ -33,6 +35,7 @@ impl Header { Self::TransferEncoding => b"Transfer-Encoding", Self::Server => b"Server", Self::Accept => b"Accept", + Self::AcceptEncoding => b"Accept-Encoding", } } @@ -52,6 +55,7 @@ impl Header { "transfer-encoding" => Ok(Self::TransferEncoding), "server" => Ok(Self::Server), "accept" => Ok(Self::Accept), + "accept-encoding" => Ok(Self::AcceptEncoding), invalid_key => Err(RequestError::HeaderError(HttpHeaderError::UnsupportedName( invalid_key.to_string(), ))), @@ -199,6 +203,7 @@ impl Headers { )), }, Header::Server => Ok(()), + Header::AcceptEncoding => Encoding::try_from(entry[1].trim().as_bytes()), } } else { Err(RequestError::HeaderError( @@ -286,6 +291,63 @@ impl Headers { } } +/// Wrapper over supported AcceptEncoding. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Encoding {} + +impl Encoding { + /// Parses a byte slice and checks if identity encoding is invalidated. Encoding + /// must be ASCII, so also UTF-8 valid. + /// + /// # Errors + /// `InvalidRequest` is returned when the byte stream is empty. + /// + /// `InvalidValue` is returned when the identity encoding is invalidated. + /// + /// `InvalidUtf8String` is returned when the byte stream contains invalid characters. + /// + /// # Examples + /// + /// ``` + /// use micro_http::Encoding; + /// + /// assert!(Encoding::try_from(b"deflate").is_ok()); + /// assert!(Encoding::try_from(b"identity;q=0").is_err()); + /// ``` + pub fn try_from(bytes: &[u8]) -> Result<(), RequestError> { + if bytes.is_empty() { + return Err(RequestError::InvalidRequest); + } + match std::str::from_utf8(bytes) { + Ok(headers_str) => { + let entry = headers_str.split(',').collect::>(); + + for encoding in entry { + match encoding.trim() { + "identity;q=0" => { + Err(RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + encoding.to_string(), + ))) + } + "*;q=0" if !headers_str.contains("identity") => { + Err(RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + encoding.to_string(), + ))) + } + _ => Ok(()), + }?; + } + Ok(()) + } + Err(utf8_err) => Err(RequestError::HeaderError( + HttpHeaderError::InvalidUtf8String(utf8_err), + )), + } + } +} + /// Wrapper over supported Media Types. #[derive(Clone, Copy, Debug, PartialEq)] pub enum MediaType { @@ -404,6 +466,42 @@ mod tests { assert_eq!(media_type.as_str(), "text/plain"); } + #[test] + fn test_try_from_encoding() { + assert_eq!( + Encoding::try_from(b"").unwrap_err(), + RequestError::InvalidRequest + ); + + assert_eq!( + Encoding::try_from(b"identity;q=0").unwrap_err(), + RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + "identity;q=0".to_string() + )) + ); + + assert!(Encoding::try_from(b"identity;q").is_ok()); + + assert_eq!( + Encoding::try_from(b"*;q=0").unwrap_err(), + RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + "*;q=0".to_string() + )) + ); + + let bytes: [u8; 10] = [130, 140, 150, 130, 140, 150, 130, 140, 150, 160]; + assert!(Encoding::try_from(&bytes[..]).is_err()); + + assert!(Encoding::try_from(b"identity;q=1").is_ok()); + assert!(Encoding::try_from(b"identity;q=0.1").is_ok()); + assert!(Encoding::try_from(b"deflate, identity, *;q=0").is_ok()); + assert!(Encoding::try_from(b"br").is_ok()); + assert!(Encoding::try_from(b"compress").is_ok()); + assert!(Encoding::try_from(b"gzip").is_ok()); + } + #[test] fn test_try_from_headers() { // Valid headers. @@ -526,9 +624,9 @@ mod tests { assert!(header .parse_header_line(b"Accept: application/json") .is_ok()); - assert!(header.accept == MediaType::ApplicationJson); + assert_eq!(header.accept, MediaType::ApplicationJson); assert!(header.parse_header_line(b"Accept: text/plain").is_ok()); - assert!(header.accept == MediaType::PlainText); + assert_eq!(header.accept, MediaType::PlainText); // Test invalid accept media type. assert!(header @@ -543,6 +641,17 @@ mod tests { " -1".to_string() ))) ); + + assert!(header + .parse_header_line(b"Accept-Encoding: deflate") + .is_ok()); + assert_eq!( + header.parse_header_line(b"Accept-Encoding: compress, identity;q=0"), + Err(RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + " identity;q=0".to_string() + ))) + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 16ca0ae..88e4a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,5 +120,5 @@ pub use crate::request::{Request, RequestError}; pub use crate::response::{Response, StatusCode}; pub use crate::server::{HttpServer, ServerError, ServerRequest, ServerResponse}; -pub use crate::common::headers::{Headers, MediaType}; +pub use crate::common::headers::{Encoding, Headers, MediaType}; pub use crate::common::{Body, HttpHeaderError, Method, Version}; diff --git a/src/request.rs b/src/request.rs index 2971f5a..af5d3a8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -510,5 +510,15 @@ mod tests { assert_eq!(request.headers.expect(), false); assert_eq!(request.headers.content_length(), 0); assert!(request.body.is_none()); + + let request = Request::try_from(b"GET http://localhost/ HTTP/1.0\r\n\ + Accept-Encoding: identity;q=0\r\n\r\n"); + assert_eq!( + request.unwrap_err(), + RequestError::HeaderError(HttpHeaderError::InvalidValue( + "Accept-Encoding".to_string(), + "identity;q=0".to_string() + )) + ); } } diff --git a/src/response.rs b/src/response.rs index cd22973..93e4d4f 100644 --- a/src/response.rs +++ b/src/response.rs @@ -84,6 +84,7 @@ pub struct ResponseHeaders { content_type: MediaType, server: String, allow: Vec, + accept_encoding: bool, } impl Default for ResponseHeaders { @@ -93,6 +94,7 @@ impl Default for ResponseHeaders { content_type: Default::default(), server: String::from("Firecracker API"), allow: Vec::new(), + accept_encoding: false, } } } @@ -140,6 +142,13 @@ impl ResponseHeaders { buf.write_all(&[COLON, SP])?; buf.write_all(self.content_length.to_string().as_bytes())?; buf.write_all(&[CR, LF])?; + + if self.accept_encoding { + buf.write_all(Header::AcceptEncoding.raw())?; + buf.write_all(&[COLON, SP])?; + buf.write_all(b"identity")?; + buf.write_all(&[CR, LF])?; + } } buf.write_all(&[CR, LF]) @@ -159,6 +168,12 @@ impl ResponseHeaders { pub fn set_content_type(&mut self, content_type: MediaType) { self.content_type = content_type; } + + /// Sets the encoding type to be written in the HTTP response. + #[allow(unused)] + pub fn set_encoding(&mut self) { + self.accept_encoding = true; + } } /// Wrapper over an HTTP Response. @@ -198,6 +213,11 @@ impl Response { self.headers.set_content_type(content_type); } + /// Updates the encoding type of `Response`. + pub fn set_encoding(&mut self) { + self.headers.set_encoding(); + } + /// Sets the HTTP response server. pub fn set_server(&mut self, server: &str) { self.headers.set_server(server); @@ -274,8 +294,9 @@ mod tests { let body = "This is a test"; response.set_body(Body::new(body)); response.set_content_type(MediaType::PlainText); + response.set_encoding(); - assert!(response.status() == StatusCode::OK); + assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().unwrap(), Body::new(body)); assert_eq!(response.http_version(), Version::Http10); assert_eq!(response.content_length(), 14); @@ -285,12 +306,13 @@ mod tests { Server: Firecracker API\r\n\ Connection: keep-alive\r\n\ Content-Type: text/plain\r\n\ - Content-Length: 14\r\n\r\n\ + Content-Length: 14\r\n\ + Accept-Encoding: identity\r\n\r\n\ This is a test"; - let mut response_buf: [u8; 126] = [0; 126]; + let mut response_buf: [u8; 153] = [0; 153]; assert!(response.write_all(&mut response_buf.as_mut()).is_ok()); - assert!(response_buf.as_ref() == expected_response); + assert_eq!(response_buf.as_ref(), expected_response); // Test response `Allow` header. let mut response = Response::new(Version::Http10, StatusCode::OK); @@ -320,7 +342,7 @@ mod tests { response.set_content_type(MediaType::PlainText); response.set_server(server); - assert!(response.status() == StatusCode::OK); + assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().unwrap(), Body::new(body)); assert_eq!(response.http_version(), Version::Http10); assert_eq!(response.content_length(), 14); diff --git a/src/server.rs b/src/server.rs index 64949dd..669b1c6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -773,6 +773,14 @@ mod tests { Content-Length: 136\r\n\r\n{ \"error\": \"Invalid header. \ Reason: Invalid value. Key:Content-Length; Value: alpha\nAll previous unanswered requests will be dropped.\" }"; assert_eq!(&buf[..], &error_message[..]); + + socket + .write_all( + b"PATCH /machine-config HTTP/1.1\r\n\ + Content-Length: alpha\r\n\ + Content-Type: application/json\r\n\r\nwhatever body", + ) + .unwrap(); } #[test]