464 lines
16 KiB
Rust
464 lines
16 KiB
Rust
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use std::str::from_utf8;
|
|
|
|
use crate::common::ascii::{CR, CRLF_LEN, LF, SP};
|
|
use crate::common::headers::Headers;
|
|
use crate::common::{Body, Method, Version};
|
|
|
|
pub use crate::common::RequestError;
|
|
|
|
/// Finds the first occurrence of `sequence` in the `bytes` slice.
|
|
///
|
|
/// Returns the starting position of the `sequence` in `bytes` or `None` if the
|
|
/// `sequence` is not found.
|
|
pub(crate) fn find(bytes: &[u8], sequence: &[u8]) -> Option<usize> {
|
|
bytes
|
|
.windows(sequence.len())
|
|
.position(|window| window == sequence)
|
|
}
|
|
|
|
/// Wrapper over HTTP URIs.
|
|
///
|
|
/// The `Uri` can not be used directly and it is only accessible from an HTTP Request.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Uri {
|
|
string: String,
|
|
}
|
|
|
|
impl Uri {
|
|
fn new(slice: &str) -> Self {
|
|
Self {
|
|
string: String::from(slice),
|
|
}
|
|
}
|
|
|
|
fn try_from(bytes: &[u8]) -> Result<Self, RequestError> {
|
|
if bytes.is_empty() {
|
|
return Err(RequestError::InvalidUri("Empty URI not allowed."));
|
|
}
|
|
let utf8_slice =
|
|
from_utf8(bytes).map_err(|_| RequestError::InvalidUri("Cannot parse URI as UTF-8."))?;
|
|
Ok(Self::new(utf8_slice))
|
|
}
|
|
|
|
/// Returns the absolute path of the `Uri`.
|
|
///
|
|
/// URIs can be represented in absolute form or relative form. The absolute form includes
|
|
/// the HTTP scheme, followed by the absolute path as follows:
|
|
/// "http:" "//" host [ ":" port ] [ abs_path ]
|
|
/// The relative URIs can be one of net_path | abs_path | rel_path.
|
|
/// This method only handles absolute URIs and relative URIs specified by abs_path.
|
|
/// The abs_path is expected to start with '/'.
|
|
///
|
|
/// # Errors
|
|
/// Returns an empty byte array when the host or the path are empty/invalid.
|
|
pub fn get_abs_path(&self) -> &str {
|
|
const HTTP_SCHEME_PREFIX: &str = "http://";
|
|
|
|
if self.string.starts_with(HTTP_SCHEME_PREFIX) {
|
|
let without_scheme = &self.string[HTTP_SCHEME_PREFIX.len()..];
|
|
if without_scheme.is_empty() {
|
|
return "";
|
|
}
|
|
// The host in this case includes the port and contains the bytes after http:// up to
|
|
// the next '/'.
|
|
match without_scheme.bytes().position(|byte| byte == b'/') {
|
|
Some(len) => &without_scheme[len..],
|
|
None => "",
|
|
}
|
|
} else {
|
|
if self.string.starts_with('/') {
|
|
return self.string.as_str();
|
|
}
|
|
|
|
""
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wrapper over an HTTP Request Line.
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct RequestLine {
|
|
method: Method,
|
|
uri: Uri,
|
|
http_version: Version,
|
|
}
|
|
|
|
impl RequestLine {
|
|
fn parse_request_line(request_line: &[u8]) -> (&[u8], &[u8], &[u8]) {
|
|
if let Some(method_end) = find(request_line, &[SP]) {
|
|
let method = &request_line[..method_end];
|
|
|
|
let uri_and_version = &request_line[(method_end + 1)..];
|
|
|
|
if let Some(uri_end) = find(uri_and_version, &[SP]) {
|
|
let uri = &uri_and_version[..uri_end];
|
|
|
|
let version = &uri_and_version[(uri_end + 1)..];
|
|
|
|
return (method, uri, version);
|
|
}
|
|
|
|
return (method, uri_and_version, b"");
|
|
}
|
|
|
|
(b"", b"", b"")
|
|
}
|
|
|
|
/// 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<Self, RequestError> {
|
|
let (method, uri, version) = Self::parse_request_line(request_line);
|
|
|
|
Ok(Self {
|
|
method: Method::try_from(method)?,
|
|
uri: Uri::try_from(uri)?,
|
|
http_version: Version::try_from(version)?,
|
|
})
|
|
}
|
|
|
|
// Returns the minimum length of a valid request. The request must contain
|
|
// the method (GET), the URI (minmum 1 character), the HTTP version(HTTP/DIGIT.DIGIT) and
|
|
// 2 separators (SP).
|
|
fn min_len() -> usize {
|
|
Method::Get.raw().len() + 1 + Version::Http10.raw().len() + 2
|
|
}
|
|
}
|
|
|
|
/// Wrapper over an HTTP Request.
|
|
#[derive(Debug)]
|
|
pub struct Request {
|
|
/// The request line of the request.
|
|
pub request_line: RequestLine,
|
|
/// The headers of the request.
|
|
pub headers: Headers,
|
|
/// The body of the request.
|
|
pub body: Option<Body>,
|
|
}
|
|
|
|
impl Request {
|
|
/// Parses a byte slice into a HTTP Request.
|
|
///
|
|
/// The byte slice is expected to have the following format: </br>
|
|
/// * Request Line: "GET SP Request-uri SP HTTP/1.0 CRLF" - Mandatory </br>
|
|
/// * Request Headers "<headers> CRLF"- Optional </br>
|
|
/// * Entity Body - Optional </br>
|
|
/// The only supported method is GET and the HTTP protocol is expected to be HTTP/1.0
|
|
/// or HTTP/1.1.
|
|
///
|
|
/// # Errors
|
|
/// The function returns InvalidRequest when parsing the byte stream fails.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// extern crate micro_http;
|
|
/// use micro_http::Request;
|
|
///
|
|
/// let http_request = Request::try_from(b"GET http://localhost/home HTTP/1.0\r\n\r\n").unwrap();
|
|
/// ```
|
|
pub fn try_from(byte_stream: &[u8]) -> Result<Self, RequestError> {
|
|
// The first line of the request is the Request Line. The line ending is CR LF.
|
|
let request_line_end = match find(byte_stream, &[CR, LF]) {
|
|
Some(len) => len,
|
|
// If no CR LF is found in the stream, the request format is invalid.
|
|
None => return Err(RequestError::InvalidRequest),
|
|
};
|
|
|
|
let request_line_bytes = &byte_stream[..request_line_end];
|
|
if request_line_bytes.len() < RequestLine::min_len() {
|
|
return Err(RequestError::InvalidRequest);
|
|
}
|
|
|
|
let request_line = RequestLine::try_from(request_line_bytes)?;
|
|
|
|
// Find the next CR LF CR LF sequence in our buffer starting at the end on the Request
|
|
// Line, including the trailing CR LF previously found.
|
|
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(Self {
|
|
request_line,
|
|
headers: Headers::default(),
|
|
body: None,
|
|
}),
|
|
Some(headers_end) => {
|
|
// Parse the request headers.
|
|
// Start by removing the leading CR LF from them.
|
|
let headers_and_body = &byte_stream[(request_line_end + CRLF_LEN)..];
|
|
let headers_end = headers_end - CRLF_LEN;
|
|
let headers = Headers::try_from(&headers_and_body[..headers_end])?;
|
|
|
|
// Parse the body of the request.
|
|
// Firstly check if we have a body.
|
|
let body = match headers.content_length() {
|
|
0 => {
|
|
// No request body.
|
|
None
|
|
}
|
|
content_length => {
|
|
// Headers suggest we have a body, but the buffer is shorter than the specified
|
|
// content length.
|
|
if headers_and_body.len() - (headers_end + 2 * CRLF_LEN)
|
|
< content_length as usize
|
|
{
|
|
return Err(RequestError::InvalidRequest);
|
|
}
|
|
let body_as_bytes = &headers_and_body[(headers_end + 2 * CRLF_LEN)..];
|
|
// If the actual length of the body is different than the `Content-Length` value
|
|
// in the headers then this request is invalid.
|
|
if body_as_bytes.len() == content_length as usize {
|
|
Some(Body::new(body_as_bytes))
|
|
} else {
|
|
return Err(RequestError::InvalidRequest);
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Self {
|
|
request_line,
|
|
headers,
|
|
body,
|
|
})
|
|
}
|
|
// If we can't find a CR LF CR LF even though the request should have headers
|
|
// the request format is invalid.
|
|
None => Err(RequestError::InvalidRequest),
|
|
}
|
|
}
|
|
|
|
/// Returns the `Uri` from the parsed `Request`.
|
|
///
|
|
/// The return value can be used to get the absolute path of the URI.
|
|
pub fn uri(&self) -> &Uri {
|
|
&self.request_line.uri
|
|
}
|
|
|
|
/// Returns the HTTP `Version` of the `Request`.
|
|
pub fn http_version(&self) -> Version {
|
|
self.request_line.http_version
|
|
}
|
|
|
|
/// Returns the HTTP `Method` of the `Request`.
|
|
pub fn method(&self) -> Method {
|
|
self.request_line.method
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
impl PartialEq for Request {
|
|
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()
|
|
&& self.headers.expect() == other.headers.expect()
|
|
&& self.headers.chunked() == other.headers.chunked()
|
|
}
|
|
}
|
|
|
|
impl RequestLine {
|
|
pub fn new(method: Method, uri: &str, http_version: Version) -> Self {
|
|
Self {
|
|
method,
|
|
uri: Uri::new(uri),
|
|
http_version,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_uri() {
|
|
for tc in &vec![
|
|
("http://localhost/home", "/home"),
|
|
("http://localhost:8080/home", "/home"),
|
|
("http://localhost/home/sub", "/home/sub"),
|
|
("/home", "/home"),
|
|
("home", ""),
|
|
("http://", ""),
|
|
("http://192.168.0.0", ""),
|
|
] {
|
|
assert_eq!(Uri::new(tc.0).get_abs_path(), tc.1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_find() {
|
|
let bytes: &[u8; 13] = b"abcacrgbabsjl";
|
|
|
|
for tc in &vec![
|
|
("ac", Some(3)),
|
|
("rgb", Some(5)),
|
|
("ab", Some(0)),
|
|
("l", Some(12)),
|
|
("abcacrgbabsjl", Some(0)),
|
|
("jle", None),
|
|
("asdkjhasjhdjhgsadg", None),
|
|
] {
|
|
assert_eq!(find(&bytes[..], tc.0.as_bytes()), tc.1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_into_request_line() {
|
|
let expected_request_line = RequestLine {
|
|
http_version: Version::Http10,
|
|
method: Method::Get,
|
|
uri: Uri::new("http://localhost/home"),
|
|
};
|
|
|
|
let request_line = b"GET http://localhost/home HTTP/1.0";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap(),
|
|
expected_request_line
|
|
);
|
|
|
|
let expected_request_line = RequestLine {
|
|
http_version: Version::Http11,
|
|
method: Method::Get,
|
|
uri: Uri::new("http://localhost/home"),
|
|
};
|
|
|
|
// Happy case with request line ending in CRLF.
|
|
let request_line = b"GET http://localhost/home HTTP/1.1";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap(),
|
|
expected_request_line
|
|
);
|
|
|
|
// Happy case with request line ending in LF instead of CRLF.
|
|
let request_line = b"GET http://localhost/home HTTP/1.1";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap(),
|
|
expected_request_line
|
|
);
|
|
|
|
// Test for invalid method.
|
|
let request_line = b"POST http://localhost/home HTTP/1.0";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap_err(),
|
|
RequestError::InvalidHttpMethod("Unsupported HTTP method.")
|
|
);
|
|
|
|
// Test for invalid uri.
|
|
let request_line = b"GET HTTP/1.0";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap_err(),
|
|
RequestError::InvalidUri("Empty URI not allowed.")
|
|
);
|
|
|
|
// Test for invalid HTTP version.
|
|
let request_line = b"GET http://localhost/home HTTP/2.0";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap_err(),
|
|
RequestError::InvalidHttpVersion("Unsupported HTTP version.")
|
|
);
|
|
|
|
// Test for invalid format with no method, uri or version.
|
|
let request_line = b"nothing";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap_err(),
|
|
RequestError::InvalidHttpMethod("Unsupported HTTP method.")
|
|
);
|
|
|
|
// Test for invalid format with no version.
|
|
let request_line = b"GET /";
|
|
assert_eq!(
|
|
RequestLine::try_from(request_line).unwrap_err(),
|
|
RequestError::InvalidHttpVersion("Unsupported HTTP version.")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_into_request() {
|
|
let expected_request = Request {
|
|
request_line: RequestLine {
|
|
http_version: Version::Http10,
|
|
method: Method::Get,
|
|
uri: Uri::new("http://localhost/home"),
|
|
},
|
|
body: None,
|
|
headers: Headers::default(),
|
|
};
|
|
let request_bytes = b"GET http://localhost/home HTTP/1.0\r\n\
|
|
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT\r\n\r\n";
|
|
let request = Request::try_from(request_bytes).unwrap();
|
|
assert_eq!(request, expected_request);
|
|
assert_eq!(request.uri(), &Uri::new("http://localhost/home"));
|
|
assert_eq!(request.http_version(), Version::Http10);
|
|
assert!(request.body.is_none());
|
|
|
|
// Test for invalid Request (length is less than minimum).
|
|
let request_bytes = b"GET";
|
|
assert_eq!(
|
|
Request::try_from(request_bytes).unwrap_err(),
|
|
RequestError::InvalidRequest
|
|
);
|
|
|
|
// Test for a request with the headers we are looking for.
|
|
let request = Request::try_from(
|
|
b"PATCH http://localhost/home HTTP/1.1\r\n\
|
|
Expect: 100-continue\r\n\
|
|
Transfer-Encoding: chunked\r\n\
|
|
Content-Length: 26\r\n\r\nthis is not\n\r\na json \nbody",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(request.uri(), &Uri::new("http://localhost/home"));
|
|
assert_eq!(request.http_version(), Version::Http11);
|
|
assert_eq!(request.method(), Method::Patch);
|
|
assert_eq!(request.headers.chunked(), true);
|
|
assert_eq!(request.headers.expect(), true);
|
|
assert_eq!(request.headers.content_length(), 26);
|
|
assert_eq!(
|
|
request.body.unwrap().body,
|
|
String::from("this is not\n\r\na json \nbody")
|
|
.as_bytes()
|
|
.to_vec()
|
|
);
|
|
|
|
// Test for an invalid request format.
|
|
Request::try_from(b"PATCH http://localhost/home HTTP/1.1\r\n").unwrap_err();
|
|
|
|
// Test for an invalid encoding.
|
|
let request = Request::try_from(
|
|
b"PATCH http://localhost/home HTTP/1.1\r\n\
|
|
Expect: 100-continue\r\n\
|
|
Transfer-Encoding: identity; q=0\r\n\
|
|
Content-Length: 26\r\n\r\nthis is not\n\r\na json \nbody",
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(request, RequestError::InvalidHeader);
|
|
|
|
// Test for an invalid content length.
|
|
let request = Request::try_from(
|
|
b"PATCH http://localhost/home HTTP/1.1\r\n\
|
|
Expect: 100-continue\r\n\
|
|
Content-Length: 5000\r\n\r\nthis is a short body",
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(request, RequestError::InvalidRequest);
|
|
|
|
// Test for a request without a body and an optional header.
|
|
let request = Request::try_from(
|
|
b"GET http://localhost/ HTTP/1.0\r\n\
|
|
Accept-Encoding: gzip\r\n\r\n",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(request.uri(), &Uri::new("http://localhost/"));
|
|
assert_eq!(request.http_version(), Version::Http10);
|
|
assert_eq!(request.method(), Method::Get);
|
|
assert_eq!(request.headers.chunked(), false);
|
|
assert_eq!(request.headers.expect(), false);
|
|
assert_eq!(request.headers.content_length(), 0);
|
|
assert!(request.body.is_none());
|
|
}
|
|
}
|