Files
micro-http/src/request.rs
Ilias Stamatis 876f3feccc Add support for the HTTP DELETE method
Add a Delete variant to the Method enum to support DELETE requests.
Reject DELETE requests with a body (same as GET requests).

Signed-off-by: Ilias Stamatis <ilstam@amazon.com>
2026-04-09 12:12:30 +01:00

570 lines
22 KiB
Rust

// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::str::from_utf8;
use crate::common::ascii::{CR, CRLF_LEN, LF, SP};
pub use crate::common::RequestError;
use crate::common::{Body, Method, Version};
use crate::headers::Headers;
// This type represents the RequestLine raw parts: method, uri and version.
type RequestLineParts<'a> = (&'a [u8], &'a [u8], &'a [u8]);
/// 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, Eq)]
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) {
// Slice access is safe because we checked above that `self.string` size <= `HTTP_SCHEME_PREFIX.len()`.
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'/') {
// Slice access is safe because `position` validates that `len` is a valid index.
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, Eq)]
pub struct RequestLine {
method: Method,
uri: Uri,
http_version: Version,
}
impl RequestLine {
fn parse_request_line(
request_line: &[u8],
) -> std::result::Result<RequestLineParts<'_>, RequestError> {
if let Some(method_end) = find(request_line, &[SP]) {
// The slice access is safe because `find` validates that `method_end` < `request_line` size.
let method = &request_line[..method_end];
// `uri_start` <= `request_line` size.
let uri_start = method_end.checked_add(1).ok_or(RequestError::Overflow)?;
// Slice access is safe because `uri_start` <= `request_line` size.
// If `uri_start` == `request_line` size, then `uri_and_version` will be an empty slice.
let uri_and_version = &request_line[uri_start..];
if let Some(uri_end) = find(uri_and_version, &[SP]) {
// Slice access is safe because `find` validates that `uri_end` < `uri_and_version` size.
let uri = &uri_and_version[..uri_end];
// `version_start` <= `uri_and_version` size.
let version_start = uri_end.checked_add(1).ok_or(RequestError::Overflow)?;
// Slice access is safe because `version_start` <= `uri_and_version` size.
let version = &uri_and_version[version_start..];
return Ok((method, uri, version));
}
}
// Request Line can be valid only if it contains the method, uri and version separated with SP.
Err(RequestError::InvalidRequest)
}
/// 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 (minimum 1 character), the HTTP version(HTTP/DIGIT.DIGIT) and
// 2 separators (SP).
fn min_len() -> usize {
// Addition is safe because these are small constants.
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>,
/// The optional files associated with the request.
pub files: Vec<File>,
}
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/>
/// * Empty Line "CRLF" <br/>
/// * Entity Body - Optional <br/>
/// The request headers and the entity body are not parsed and None is returned because
/// these are not used by the MMDS server.
/// 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
///
/// ```
/// use micro_http::Request;
///
/// let max_request_len = 2000;
/// let request_bytes = b"GET http://localhost/home HTTP/1.0\r\n\r\n";
/// let http_request = Request::try_from(request_bytes, Some(max_request_len)).unwrap();
/// ```
pub fn try_from(byte_stream: &[u8], max_len: Option<usize>) -> Result<Self, RequestError> {
// If a size limit is provided, verify the request length does not exceed it.
if let Some(limit) = max_len {
if byte_stream.len() >= limit {
return Err(RequestError::InvalidRequest);
}
}
// 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),
};
// Slice access is safe because `find` validates that `request_line_end` < `byte_stream` size.
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,
files: Vec::new(),
}),
Some(headers_end) => {
// Parse the request headers.
// Start by removing the leading CR LF from them.
// The addition is safe because `find()` guarantees that `request_line_end`
// precedes 2 `CRLF` sequences.
let headers_start = request_line_end + CRLF_LEN;
// Slice access is safe because starting from `request_line_end` there are at least two CRLF
// (enforced by `find` at the start of this method).
let headers_and_body = &byte_stream[headers_start..];
// Because we advanced the start with CRLF_LEN, we now have to subtract CRLF_LEN
// from the end in order to keep the same window.
// Underflow is not possible here because `byte_stream[request_line_end..]` starts with CR LF,
// so `headers_end` can be either zero (this case is treated separately in the first match arm)
// or >= 3 (current case).
let headers_end = headers_end - CRLF_LEN;
// Slice access is safe because `headers_end` is checked above
// (`find` gives a valid position, and subtracting 2 can't underflow).
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 => {
if request_line.method == Method::Get
|| request_line.method == Method::Delete
{
return Err(RequestError::InvalidRequest);
}
// Multiplication is safe because `CRLF_LEN` is a small constant.
// Addition is also safe because `headers_end` started out as the result
// of `find(<something>, CRLFCRLF)`, then `CRLF_LEN` was subtracted from it.
let crlf_end = headers_end + 2 * CRLF_LEN;
// This can't underflow because `headers_and_body.len()` >= `crlf_end`.
let body_len = headers_and_body.len() - crlf_end;
// Headers suggest we have a body, but the buffer is shorter than the specified
// content length.
if body_len < content_length as usize {
return Err(RequestError::InvalidRequest);
}
// Slice access is safe because `crlf_end` is the index after two CRLF
// (it is <= `headers_and_body` size).
let body_as_bytes = &headers_and_body[crlf_end..];
// 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,
files: Vec::new(),
})
}
// 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 request missing the separator.
let request_line = b"GET";
assert_eq!(
RequestLine::try_from(request_line).unwrap_err(),
RequestError::InvalidRequest
);
// 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::InvalidRequest
);
// Test for invalid format with no version.
let request_line = b"GET /";
assert_eq!(
RequestLine::try_from(request_line).unwrap_err(),
RequestError::InvalidRequest
);
}
#[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,
files: Vec::new(),
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, None).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 (missing CR LF).
let request_bytes = b"GET / HTTP/1.1";
assert_eq!(
Request::try_from(request_bytes, None).unwrap_err(),
RequestError::InvalidRequest
);
// Test for invalid Request (length is less than minimum).
let request_bytes = b"GET";
assert_eq!(
Request::try_from(request_bytes, None).unwrap_err(),
RequestError::InvalidRequest
);
// Test for invalid Request (`GET` requests should have no body).
let request_bytes = b"GET /machine-config HTTP/1.1\r\n\
Content-Length: 13\r\n\
Content-Type: application/json\r\n\r\nwhatever body";
assert_eq!(
Request::try_from(request_bytes, None).unwrap_err(),
RequestError::InvalidRequest
);
// Test for invalid Request (`DELETE` requests should have no body).
let request_bytes = b"DELETE /machine-config HTTP/1.1\r\n\
Content-Length: 13\r\n\
Content-Type: application/json\r\n\r\nwhatever body";
assert_eq!(
Request::try_from(request_bytes, None).unwrap_err(),
RequestError::InvalidRequest
);
// Test for request larger than maximum len provided.
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";
assert_eq!(
Request::try_from(request_bytes, Some(20)).unwrap_err(),
RequestError::InvalidRequest
);
// Test request smaller than maximum len provided is ok.
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";
assert!(Request::try_from(request_bytes, Some(500)).is_ok());
// Test for a request with the headers we are looking for.
let request_bytes = 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";
let request = Request::try_from(request_bytes, None).unwrap();
assert_eq!(request.uri(), &Uri::new("http://localhost/home"));
assert_eq!(request.http_version(), Version::Http11);
assert_eq!(request.method(), Method::Patch);
assert!(request.headers.chunked());
assert!(request.headers.expect());
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", None).unwrap_err();
// Test for an invalid encoding.
let request_bytes = 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";
assert!(Request::try_from(request_bytes, None).is_ok());
// Test for an invalid content length.
let request_bytes = b"PATCH http://localhost/home HTTP/1.1\r\n\
Content-Length: 5000\r\n\r\nthis is a short body";
let request = Request::try_from(request_bytes, None).unwrap_err();
assert_eq!(request, RequestError::InvalidRequest);
// Test for a request without a body and an optional header.
let request_bytes = b"GET http://localhost/ HTTP/1.0\r\n\
Accept-Encoding: gzip\r\n\r\n";
let request = Request::try_from(request_bytes, None).unwrap();
assert_eq!(request.uri(), &Uri::new("http://localhost/"));
assert_eq!(request.http_version(), Version::Http10);
assert_eq!(request.method(), Method::Get);
assert!(!request.headers.chunked());
assert!(!request.headers.expect());
assert_eq!(request.headers.content_length(), 0);
assert!(request.body.is_none());
let request_bytes = b"GET http://localhost/ HTTP/1.0\r\n\
Accept-Encoding: identity;q=0\r\n\r\n";
let request = Request::try_from(request_bytes, None);
assert_eq!(
request.unwrap_err(),
RequestError::HeaderError(crate::HttpHeaderError::InvalidValue(
"Accept-Encoding".to_string(),
"identity;q=0".to_string()
))
);
}
}