From 569230220fd70090118faa7a29c7768a193b991f Mon Sep 17 00:00:00 2001 From: karthik nedunchezhiyan Date: Sun, 26 Jan 2020 19:16:05 +0530 Subject: [PATCH] micro_http: some doc and code corrections Signed-off-by: karthik nedunchezhiyan Signed-off-by: YUAN LYU --- src/common/headers.rs | 111 +++++++++++++++++++++++++++++------------- src/common/mod.rs | 64 ++++++++++++------------ src/connection.rs | 44 +++++++++++------ src/request.rs | 21 +++++--- src/response.rs | 32 ++++++------ src/server.rs | 24 ++++++--- 6 files changed, 186 insertions(+), 110 deletions(-) diff --git a/src/common/headers.rs b/src/common/headers.rs index 060b568..959bfc0 100644 --- a/src/common/headers.rs +++ b/src/common/headers.rs @@ -21,25 +21,32 @@ pub enum Header { } impl Header { + /// Returns a byte slice representation of the object. pub fn raw(&self) -> &'static [u8] { match self { - Header::ContentLength => b"Content-Length", - Header::ContentType => b"Content-Type", - Header::Expect => b"Expect", - Header::TransferEncoding => b"Transfer-Encoding", - Header::Server => b"Server", + Self::ContentLength => b"Content-Length", + Self::ContentType => b"Content-Type", + Self::Expect => b"Expect", + Self::TransferEncoding => b"Transfer-Encoding", + Self::Server => b"Server", } } + /// Parses a byte slice into a Header structure. Header must be ASCII, so also + /// UTF-8 valid. + /// + /// # Errors + /// `InvalidRequest` is returned if slice contains invalid utf8 characters. + /// `InvalidHeader` is returned if unsupported header found. fn try_from(string: &[u8]) -> Result { if let Ok(mut utf8_string) = String::from_utf8(string.to_vec()) { utf8_string.make_ascii_lowercase(); match utf8_string.trim() { - "content-length" => Ok(Header::ContentLength), - "content-type" => Ok(Header::ContentType), - "expect" => Ok(Header::Expect), - "transfer-encoding" => Ok(Header::TransferEncoding), - "server" => Ok(Header::Server), + "content-length" => Ok(Self::ContentLength), + "content-type" => Ok(Self::ContentType), + "expect" => Ok(Self::Expect), + "transfer-encoding" => Ok(Self::TransferEncoding), + "server" => Ok(Self::Server), _ => Err(RequestError::InvalidHeader), } } else { @@ -75,16 +82,18 @@ pub struct Headers { chunked: bool, } -impl Headers { +impl Default for Headers { /// By default Requests are created with no headers. - pub fn default() -> Headers { - Headers { - content_length: 0, - expect: false, - chunked: false, + fn default() -> Self { + Self { + content_length: Default::default(), + expect: Default::default(), + chunked: Default::default(), } } +} +impl Headers { /// Expects one header line and parses it, updating the header structure or returning an /// error if the header is invalid. /// @@ -94,6 +103,17 @@ impl Headers { /// `InvalidHeader` is returned when the parsed header is formatted incorrectly or suggests /// that the client is using HTTP features that we do not support in this implementation, /// which invalidates the request. + /// + /// # Examples + /// + /// ``` + /// extern crate micro_http; + /// use micro_http::Headers; + /// + /// let mut request_header = Headers::default(); + /// assert!(request_header.parse_header_line(b"Content-Length: 24").is_ok()); + /// assert!(request_header.parse_header_line(b"Content-Length: 24: 2").is_err()); + /// ``` pub fn parse_header_line(&mut self, header_line: &[u8]) -> Result<(), RequestError> { // Headers must be ASCII, so also UTF-8 valid. match std::str::from_utf8(header_line) { @@ -104,17 +124,13 @@ impl Headers { } if let Ok(head) = Header::try_from(entry[0].as_bytes()) { match head { - Header::ContentLength => { - let try_numeric: Result = - std::str::FromStr::from_str(entry[1].trim()); - match try_numeric { - Ok(content_length) if content_length >= 0 => { - self.content_length = content_length; - Ok(()) - } - _ => Err(RequestError::InvalidHeader), + Header::ContentLength => match entry[1].trim().parse::() { + Ok(content_length) => { + self.content_length = content_length; + Ok(()) } - } + Err(_) => Err(RequestError::InvalidHeader), + }, Header::ContentType => { match MediaType::try_from(entry[1].trim().as_bytes()) { Ok(_) => Ok(()), @@ -165,7 +181,7 @@ impl Headers { #[cfg(test)] pub fn new(content_length: i32, expect: bool, chunked: bool) -> Self { - Headers { + Self { content_length, expect, chunked, @@ -196,7 +212,7 @@ impl Headers { pub fn try_from(bytes: &[u8]) -> Result { // Headers must be ASCII, so also UTF-8 valid. if let Ok(text) = std::str::from_utf8(bytes) { - let mut headers = Headers::default(); + let mut headers = Self::default(); let header_lines = text.split("\r\n"); for header_line in header_lines { @@ -224,30 +240,57 @@ pub enum MediaType { } impl Default for MediaType { + /// Default value for MediaType is application/json fn default() -> Self { - MediaType::ApplicationJson + Self::ApplicationJson } } impl MediaType { - fn try_from(bytes: &[u8]) -> Result { + /// Parses a byte slice into a MediaType structure for a HTTP request. MediaType + /// must be ASCII, so also UTF-8 valid. + /// + /// # Errors + /// The function returns `InvalidRequest` when parsing the byte stream fails or + /// unsupported MediaType found. + /// + /// # Examples + /// + /// ``` + /// extern crate micro_http; + /// use micro_http::MediaType; + /// + /// assert!(MediaType::try_from(b"application/json").is_ok()); + /// assert!(MediaType::try_from(b"application/json2").is_err()); + /// ``` + pub fn try_from(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(RequestError::InvalidRequest); } let utf8_slice = String::from_utf8(bytes.to_vec()).map_err(|_| RequestError::InvalidRequest)?; match utf8_slice.as_str().trim() { - "text/plain" => Ok(MediaType::PlainText), - "application/json" => Ok(MediaType::ApplicationJson), + "text/plain" => Ok(Self::PlainText), + "application/json" => Ok(Self::ApplicationJson), _ => Err(RequestError::InvalidRequest), } } /// Returns a static string representation of the object. + /// + /// # Examples + /// + /// ``` + /// extern crate micro_http; + /// use micro_http::MediaType; + /// + /// let media_type = MediaType::ApplicationJson; + /// assert_eq!(media_type.as_str(), "application/json"); + /// ``` pub fn as_str(self) -> &'static str { match self { - MediaType::PlainText => "text/plain", - MediaType::ApplicationJson => "application/json", + Self::PlainText => "text/plain", + Self::ApplicationJson => "application/json", } } } diff --git a/src/common/mod.rs b/src/common/mod.rs index bb134e1..00236f6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -33,12 +33,12 @@ pub enum RequestError { impl Display for RequestError { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - RequestError::InvalidHttpMethod(inner) => write!(f, "Invalid HTTP Method: {}", inner), - RequestError::InvalidUri(inner) => write!(f, "Invalid URI: {}", inner), - RequestError::InvalidHttpVersion(inner) => write!(f, "Invalid HTTP Version: {}", inner), - RequestError::UnsupportedHeader => write!(f, "Unsupported header."), - RequestError::InvalidHeader => write!(f, "Invalid header."), - RequestError::InvalidRequest => write!(f, "Invalid request."), + Self::InvalidHttpMethod(inner) => write!(f, "Invalid HTTP Method: {}", inner), + Self::InvalidUri(inner) => write!(f, "Invalid URI: {}", inner), + Self::InvalidHttpVersion(inner) => write!(f, "Invalid HTTP Version: {}", inner), + Self::UnsupportedHeader => write!(f, "Unsupported header."), + Self::InvalidHeader => write!(f, "Invalid header."), + Self::InvalidRequest => write!(f, "Invalid request."), } } } @@ -59,10 +59,10 @@ pub enum ConnectionError { impl Display for ConnectionError { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - ConnectionError::ParseError(inner) => write!(f, "Parsing error: {}", inner), - ConnectionError::StreamError(inner) => write!(f, "Stream error: {}", inner), - ConnectionError::ConnectionClosed => write!(f, "Connection closed."), - ConnectionError::InvalidWrite => write!(f, "Invalid write attempt."), + Self::ParseError(inner) => write!(f, "Parsing error: {}", inner), + Self::StreamError(inner) => write!(f, "Stream error: {}", inner), + Self::ConnectionClosed => write!(f, "Connection closed."), + Self::InvalidWrite => write!(f, "Invalid write attempt."), } } } @@ -96,9 +96,9 @@ pub enum ServerError { impl Display for ServerError { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - ServerError::IOError(inner) => write!(f, "IO error: {}", inner), - ServerError::ConnectionError(inner) => write!(f, "Connection error: {}", inner), - ServerError::ServerFull => write!(f, "Server is full."), + Self::IOError(inner) => write!(f, "IO error: {}", inner), + Self::ConnectionError(inner) => write!(f, "Connection error: {}", inner), + Self::ServerFull => write!(f, "Server is full."), } } } @@ -122,7 +122,7 @@ pub struct Body { impl Body { /// Creates a new `Body` from a `String` input. pub fn new>>(body: T) -> Self { - Body { body: body.into() } + Self { body: body.into() } } /// Returns the body as an `u8 slice`. @@ -159,12 +159,12 @@ impl Method { /// an error, but when using the input b"GET", it returns Method::Get. /// /// # Errors - /// Returns `RequestError` if the method specified by `bytes` is unsupported. + /// `InvalidHttpMethod` is returned if the specified HTTP method is unsupported. pub fn try_from(bytes: &[u8]) -> Result { match bytes { - b"GET" => Ok(Method::Get), - b"PUT" => Ok(Method::Put), - b"PATCH" => Ok(Method::Patch), + b"GET" => Ok(Self::Get), + b"PUT" => Ok(Self::Put), + b"PATCH" => Ok(Self::Patch), _ => Err(RequestError::InvalidHttpMethod("Unsupported HTTP method.")), } } @@ -172,9 +172,9 @@ impl Method { /// Returns an `u8 slice` corresponding to the Method. pub fn raw(self) -> &'static [u8] { match self { - Method::Get => b"GET", - Method::Put => b"PUT", - Method::Patch => b"PATCH", + Self::Get => b"GET", + Self::Put => b"PUT", + Self::Patch => b"PATCH", } } @@ -208,12 +208,19 @@ pub enum Version { Http11, } +impl Default for Version { + /// Returns the default HTTP version = HTTP/1.1. + fn default() -> Self { + Self::Http11 + } +} + impl Version { /// HTTP Version as an `u8 slice`. pub fn raw(self) -> &'static [u8] { match self { - Version::Http10 => b"HTTP/1.0", - Version::Http11 => b"HTTP/1.1", + Self::Http10 => b"HTTP/1.0", + Self::Http11 => b"HTTP/1.1", } } @@ -223,21 +230,16 @@ impl Version { /// The version is case sensitive and the accepted input is upper case. /// /// # Errors - /// Returns a `RequestError` when the version is not supported. + /// Returns a `InvalidHttpVersion` when the HTTP version is not supported. pub fn try_from(bytes: &[u8]) -> Result { match bytes { - b"HTTP/1.0" => Ok(Version::Http10), - b"HTTP/1.1" => Ok(Version::Http11), + b"HTTP/1.0" => Ok(Self::Http10), + b"HTTP/1.1" => Ok(Self::Http11), _ => Err(RequestError::InvalidHttpVersion( "Unsupported HTTP version.", )), } } - - /// Returns the default HTTP version = HTTP/1.1. - pub fn default() -> Self { - Version::Http11 - } } #[cfg(test)] diff --git a/src/connection.rs b/src/connection.rs index 898bcc3..ce7a594 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -55,7 +55,7 @@ pub struct HttpConnection { impl HttpConnection { /// Creates an empty connection. pub fn new(stream: T) -> Self { - HttpConnection { + Self { pending_request: None, stream, state: ConnectionState::WaitingForRequestLine, @@ -114,8 +114,12 @@ impl HttpConnection { } } - // Reads a maximum of `BUFFER_SIZE` bytes from the stream into `buffer`. - // The return value represents the end index of what we have just appended. + /// Reads a maximum of 1024 bytes from the stream into `buffer`. + /// The return value represents the end index of what we have just appended. + /// + /// # Errors + /// `StreamError` is returned if any error occurred while reading the stream. + /// `ConnectionClosed` is returned if the client closed the connection. fn read_bytes(&mut self) -> Result { loop { // Append new bytes to what we already have in the buffer. @@ -129,8 +133,11 @@ impl HttpConnection { } } - // Parses bytes in `buffer` for a valid request line. - // Returns `false` if there are no more bytes to be parsed in the buffer. + /// Parses bytes in `buffer` for a valid request line. + /// Returns `false` if there are no more bytes to be parsed in the buffer. + /// + /// # Errors + /// `ParseError` is returned if unable to parse request line or line longer than BUFFER_SIZE. fn parse_request_line( &mut self, start: &mut usize, @@ -141,13 +148,12 @@ impl HttpConnection { let line = &self.buffer[*start..(*start + line_end_index)]; *start = *start + line_end_index + CRLF_LEN; - let request_line = - RequestLine::try_from(line).map_err(ConnectionError::ParseError)?; // Form the request with a valid request line, which is the bare minimum // for a valid request. self.pending_request = Some(Request { - request_line, + request_line: RequestLine::try_from(line) + .map_err(ConnectionError::ParseError)?, headers: Headers::default(), body: None, }); @@ -170,19 +176,23 @@ impl HttpConnection { } } - // Parses bytes in `buffer` for header fields. - // Returns `false` if there are no more bytes to be parsed in the buffer. + /// Parses bytes in `buffer` for header fields. + /// Returns `false` if there are no more bytes to be parsed in the buffer. + /// + /// # Errors + /// `ParseError` is returned if unable to parse header or line longer than BUFFER_SIZE. fn parse_headers( &mut self, line_start_index: &mut usize, end_cursor: usize, ) -> Result { match find(&self.buffer[*line_start_index..end_cursor], &[CR, LF]) { - // We have found the end of the headers. // `line_start_index` points to the end of the most recently found CR LF // sequence. That means that if we found the next CR LF sequence at this index, // they are, in fact, a CR LF CR LF sequence, which marks the end of the header // fields, per HTTP specification. + + // We have found the end of the header. Some(0) => { // If our current state is `WaitingForHeaders`, it means that we already have // a valid request formed from a request line, so it's safe to unwrap. @@ -243,8 +253,11 @@ impl HttpConnection { } } - // Parses bytes in `buffer` to be put into the request body, if there should be one. - // Returns `false` if there are no more bytes to be parsed in the buffer. + /// Parses bytes in `buffer` to be put into the request body, if there should be one. + /// Returns `false` if there are no more bytes to be parsed in the buffer. + /// + /// # Errors + /// `ParseError` is returned when the body is larger than the specified content-length. fn parse_body( &mut self, line_start_index: &mut usize, @@ -296,7 +309,10 @@ impl HttpConnection { /// Tries to write the first available response to the provided stream. /// Meant to be used only with non-blocking streams and an `EPOLL` structure. - /// Should be called whenever an `EPOLLOUT` event is signaled. + /// Should be called whenever an `EPOLLOUT` event is signaled. If no bytes + /// were written to the stream or error occurred while trying to write to stream, + /// we will discard all responses from response_queue because there is no way + /// to deliver it to client. /// /// # Errors /// `StreamError` is returned when an IO operation fails. diff --git a/src/request.rs b/src/request.rs index 2036b4b..58518d8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -29,7 +29,7 @@ pub struct Uri { impl Uri { fn new(slice: &str) -> Self { - Uri { + Self { string: String::from(slice), } } @@ -40,7 +40,7 @@ impl Uri { } let utf8_slice = from_utf8(bytes).map_err(|_| RequestError::InvalidUri("Cannot parse URI as UTF-8."))?; - Ok(Uri::new(utf8_slice)) + Ok(Self::new(utf8_slice)) } /// Returns the absolute path of the `Uri`. @@ -108,10 +108,15 @@ impl RequestLine { } /// Tries to parse a byte stream in a request line. Fails if the request line is malformed. + /// + /// # Errors + /// `InvalidHttpMethod` is returned if the specified HTTP method is unsupported. + /// `InvalidHttpVersion` is returned if the specified HTTP version is unsupported. + /// `InvalidUri` is returned if the specified Uri is not valid. pub fn try_from(request_line: &[u8]) -> Result { - let (method, uri, version) = RequestLine::parse_request_line(request_line); + let (method, uri, version) = Self::parse_request_line(request_line); - Ok(RequestLine { + Ok(Self { method: Method::try_from(method)?, uri: Uri::try_from(uri)?, http_version: Version::try_from(version)?, @@ -127,7 +132,7 @@ impl RequestLine { #[cfg(test)] pub fn new(method: Method, uri: &str, http_version: Version) -> Self { - RequestLine { + Self { method, uri: Uri::new(uri), http_version, @@ -187,7 +192,7 @@ impl Request { match find(&byte_stream[request_line_end..], &[CR, LF, CR, LF]) { // If we have found a CR LF CR LF at the end of the Request Line, the request // is complete. - Some(0) => Ok(Request { + Some(0) => Ok(Self { request_line, headers: Headers::default(), body: None, @@ -225,7 +230,7 @@ impl Request { } }; - Ok(Request { + Ok(Self { request_line, headers, body, @@ -260,7 +265,7 @@ mod tests { use super::*; impl PartialEq for Request { - fn eq(&self, other: &Request) -> bool { + fn eq(&self, other: &Self) -> bool { // Ignore the other fields of Request for now because they are not used. self.request_line == other.request_line && self.headers.content_length() == other.headers.content_length() diff --git a/src/response.rs b/src/response.rs index 5e0cb79..2916c99 100644 --- a/src/response.rs +++ b/src/response.rs @@ -35,14 +35,14 @@ impl StatusCode { /// Returns the status code as bytes. pub fn raw(self) -> &'static [u8; 3] { match self { - StatusCode::Continue => b"100", - StatusCode::OK => b"200", - StatusCode::NoContent => b"204", - StatusCode::BadRequest => b"400", - StatusCode::NotFound => b"404", - StatusCode::InternalServerError => b"500", - StatusCode::NotImplemented => b"501", - StatusCode::ServiceUnavailable => b"503", + Self::Continue => b"100", + Self::OK => b"200", + Self::NoContent => b"204", + Self::BadRequest => b"400", + Self::NotFound => b"404", + Self::InternalServerError => b"500", + Self::NotImplemented => b"501", + Self::ServiceUnavailable => b"503", } } } @@ -54,7 +54,7 @@ struct StatusLine { impl StatusLine { fn new(http_version: Version, status_code: StatusCode) -> Self { - StatusLine { + Self { http_version, status_code, } @@ -81,10 +81,10 @@ pub struct ResponseHeaders { impl Default for ResponseHeaders { fn default() -> Self { - ResponseHeaders { + Self { content_length: Default::default(), content_type: Default::default(), - server: "Firecracker API".to_string(), + server: String::from("Firecracker API"), } } } @@ -134,7 +134,9 @@ impl ResponseHeaders { /// Wrapper over an HTTP Response. /// /// The Response is created using a `Version` and a `StatusCode`. When creating a Response object, -/// the body is initialized to `None`. The body can be updated with a call to `set_body`. +/// the body is initialized to `None` and the header is initialized with the `default` value. The body +/// can be updated with a call to `set_body`. The header can be updated with `set_content_type` and +/// `set_server`. pub struct Response { status_line: StatusLine, headers: ResponseHeaders, @@ -143,11 +145,11 @@ pub struct Response { impl Response { /// Creates a new HTTP `Response` with an empty body. - pub fn new(http_version: Version, status_code: StatusCode) -> Response { - Response { + pub fn new(http_version: Version, status_code: StatusCode) -> Self { + Self { status_line: StatusLine::new(http_version, status_code), headers: ResponseHeaders::default(), - body: None, + body: Default::default(), } } diff --git a/src/server.rs b/src/server.rs index cfaa832..624473a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -38,7 +38,7 @@ impl ServerRequest { /// Creates a new `ServerRequest` object from an existing `Request`, /// adding an identification token. pub fn new(request: Request, id: u64) -> Self { - ServerRequest { request, id } + Self { request, id } } /// Returns a reference to the inner request. @@ -68,8 +68,8 @@ pub struct ServerResponse { } impl ServerResponse { - fn new(response: Response, id: u64) -> ServerResponse { - ServerResponse { response, id } + fn new(response: Response, id: u64) -> Self { + Self { response, id } } } @@ -97,7 +97,7 @@ struct ClientConnection { impl ClientConnection { fn new(connection: HttpConnection) -> Self { - ClientConnection { + Self { connection, state: ClientConnectionState::AwaitingIncoming, in_flight_response_count: 0, @@ -201,13 +201,14 @@ impl ClientConnection { /// HTTP Server implementation using Unix Domain Sockets and `EPOLL` to /// handle multiple connections on the same thread. /// -/// The function that does the data exchange is `handle_notifications`. +/// The function that handles incoming connections, parses incoming +/// requests and sends responses for awaiting requests is `requests`. /// It can be called in a loop, which will render the thread that the /// server runs on incapable of performing other operations, or it can /// be used in another `EPOLL` structure, as it provides its `epoll_fd`, /// the file descriptor of the epoll structure used within the server, /// and it can be added to another one using the `EPOLLIN` flag. Whenever -/// there is a notification on that fd, `handle_notifications` should be +/// there is a notification on that fd, `requests` should be /// called once. /// /// # Example @@ -263,7 +264,7 @@ impl HttpServer { pub fn new>(path_to_socket: P) -> Result { let socket = UnixListener::bind(path_to_socket).map_err(ServerError::IOError)?; let epoll_fd = epoll::create(true).map_err(ServerError::IOError)?; - Ok(HttpServer { + Ok(Self { socket, epoll_fd, connections: HashMap::new(), @@ -477,7 +478,8 @@ impl HttpServer { /// Accepts a new incoming connection and adds it to the `epoll` notification structure. /// /// # Errors - /// `IOError` is returned when an `epoll::ctl` operation fails. + /// `IOError` is returned when socket or epoll operations fail. + /// `ServerFull` is returned if server full capacity has been reached. fn handle_new_connection(&mut self) -> Result<()> { if self.connections.len() == MAX_CONNECTIONS { // If we want a replacement policy for connections @@ -509,6 +511,9 @@ impl HttpServer { /// Changes the event type for a connection to either listen for incoming bytes /// or for when the stream is ready for writing. + /// + /// # Errors + /// `IOError` is returned when an `EPOLL_CTL_MOD` control operation fails. fn epoll_mod(epoll_fd: RawFd, stream_fd: RawFd, evset: epoll::Events) -> Result<()> { let event = epoll::Event::new(evset, stream_fd as u64); epoll::ctl( @@ -521,6 +526,9 @@ impl HttpServer { } /// Adds a stream to the `epoll` notification structure with the `EPOLLIN` event set. + /// + /// # Errors + /// `IOError` is returned when an `EPOLL_CTL_ADD` control operation fails. fn epoll_add(epoll_fd: RawFd, stream_fd: RawFd) -> Result<()> { epoll::ctl( epoll_fd,