#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReadOnlyMethod {
Get,
Head,
}
impl ReadOnlyMethod {
pub fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Head => "HEAD",
}
}
}
impl std::fmt::Display for ReadOnlyMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestValidationError {
MethodNotAllowed,
InvalidContentLength,
BodyTooLarge,
UnsupportedTransferEncoding,
ConflictingBodyHeaders,
InvalidRequestTarget,
}
impl std::fmt::Display for RequestValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MethodNotAllowed => write!(f, "method not allowed for static serving"),
Self::InvalidContentLength => write!(f, "malformed Content-Length"),
Self::BodyTooLarge => write!(f, "request body too large"),
Self::UnsupportedTransferEncoding => {
write!(f, "unsupported Transfer-Encoding on read-only request")
}
Self::ConflictingBodyHeaders => {
write!(f, "both Content-Length and Transfer-Encoding present")
}
Self::InvalidRequestTarget => write!(f, "request target is not valid origin-form"),
}
}
}
impl std::error::Error for RequestValidationError {}
pub fn validate_method(method: &str) -> Result<ReadOnlyMethod, RequestValidationError> {
match method {
"GET" => Ok(ReadOnlyMethod::Get),
"HEAD" => Ok(ReadOnlyMethod::Head),
_ => Err(RequestValidationError::MethodNotAllowed),
}
}
pub fn validate_request_body(
content_length: Option<&str>,
transfer_encoding: Option<&str>,
max_body_bytes: u64,
) -> Result<(), RequestValidationError> {
if content_length.is_some() && transfer_encoding.is_some() {
return Err(RequestValidationError::ConflictingBodyHeaders);
}
if transfer_encoding.is_some() {
return Err(RequestValidationError::UnsupportedTransferEncoding);
}
if let Some(cl) = content_length {
let trimmed = cl.trim();
if trimmed.is_empty() {
return Err(RequestValidationError::InvalidContentLength);
}
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
return Err(RequestValidationError::InvalidContentLength);
}
let len: u64 = trimmed
.parse()
.map_err(|_| RequestValidationError::InvalidContentLength)?;
if len > max_body_bytes {
return Err(RequestValidationError::BodyTooLarge);
}
}
Ok(())
}
pub fn validate_request_target(target: &str) -> Result<(), RequestValidationError> {
if target.is_empty() || !target.starts_with('/') {
return Err(RequestValidationError::InvalidRequestTarget);
}
if target.contains(char::is_whitespace) {
return Err(RequestValidationError::InvalidRequestTarget);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_is_allowed() {
assert_eq!(validate_method("GET").unwrap(), ReadOnlyMethod::Get);
}
#[test]
fn head_is_allowed() {
assert_eq!(validate_method("HEAD").unwrap(), ReadOnlyMethod::Head);
}
#[test]
fn post_is_rejected() {
assert_eq!(
validate_method("POST").unwrap_err(),
RequestValidationError::MethodNotAllowed
);
}
#[test]
fn put_is_rejected() {
assert_eq!(
validate_method("PUT").unwrap_err(),
RequestValidationError::MethodNotAllowed
);
}
#[test]
fn delete_is_rejected() {
assert_eq!(
validate_method("DELETE").unwrap_err(),
RequestValidationError::MethodNotAllowed
);
}
#[test]
fn patch_is_rejected() {
assert_eq!(
validate_method("PATCH").unwrap_err(),
RequestValidationError::MethodNotAllowed
);
}
#[test]
fn options_is_rejected() {
assert_eq!(
validate_method("OPTIONS").unwrap_err(),
RequestValidationError::MethodNotAllowed
);
}
#[test]
fn zero_content_length_allowed() {
assert!(validate_request_body(Some("0"), None, 0).is_ok());
}
#[test]
fn positive_content_length_rejected() {
assert_eq!(
validate_request_body(Some("1024"), None, 0).unwrap_err(),
RequestValidationError::BodyTooLarge
);
}
#[test]
fn invalid_content_length_rejected() {
assert_eq!(
validate_request_body(Some("not-a-number"), None, 0).unwrap_err(),
RequestValidationError::InvalidContentLength
);
}
#[test]
fn negative_content_length_rejected() {
assert_eq!(
validate_request_body(Some("-1"), None, 0).unwrap_err(),
RequestValidationError::InvalidContentLength
);
}
#[test]
fn overflowing_content_length_rejected() {
assert_eq!(
validate_request_body(Some("99999999999999999999"), None, 0).unwrap_err(),
RequestValidationError::InvalidContentLength
);
}
#[test]
fn nonempty_transfer_encoding_rejected() {
assert_eq!(
validate_request_body(None, Some("chunked"), 0).unwrap_err(),
RequestValidationError::UnsupportedTransferEncoding
);
}
#[test]
fn content_length_and_transfer_encoding_rejected() {
assert_eq!(
validate_request_body(Some("0"), Some("chunked"), 0).unwrap_err(),
RequestValidationError::ConflictingBodyHeaders
);
}
#[test]
fn empty_transfer_encoding_rejected() {
assert_eq!(
validate_request_body(None, Some(""), 0).unwrap_err(),
RequestValidationError::UnsupportedTransferEncoding
);
}
#[test]
fn whitespace_transfer_encoding_rejected() {
assert_eq!(
validate_request_body(None, Some(" "), 0).unwrap_err(),
RequestValidationError::UnsupportedTransferEncoding
);
}
#[test]
fn no_headers_allowed() {
assert!(validate_request_body(None, None, 0).is_ok());
}
#[test]
fn empty_content_length_rejected() {
assert_eq!(
validate_request_body(Some(""), None, 0).unwrap_err(),
RequestValidationError::InvalidContentLength
);
}
#[test]
fn whitespace_only_content_length_rejected() {
assert_eq!(
validate_request_body(Some(" "), None, 0).unwrap_err(),
RequestValidationError::InvalidContentLength
);
}
#[test]
fn content_length_with_max_body_bytes() {
assert!(validate_request_body(Some("100"), None, 100).is_ok());
assert_eq!(
validate_request_body(Some("101"), None, 100).unwrap_err(),
RequestValidationError::BodyTooLarge
);
}
#[test]
fn valid_request_target() {
assert!(validate_request_target("/").is_ok());
assert!(validate_request_target("/foo").is_ok());
assert!(validate_request_target("/foo/bar").is_ok());
assert!(validate_request_target("/file.txt").is_ok());
}
#[test]
fn empty_request_target_rejected() {
assert_eq!(
validate_request_target("").unwrap_err(),
RequestValidationError::InvalidRequestTarget
);
}
#[test]
fn absolute_uri_request_target_rejected() {
assert_eq!(
validate_request_target("http://example.com/").unwrap_err(),
RequestValidationError::InvalidRequestTarget
);
}
#[test]
fn asterisk_request_target_rejected() {
assert_eq!(
validate_request_target("*").unwrap_err(),
RequestValidationError::InvalidRequestTarget
);
}
#[test]
fn whitespace_in_request_target_rejected() {
assert_eq!(
validate_request_target("/foo bar").unwrap_err(),
RequestValidationError::InvalidRequestTarget
);
}
#[test]
fn read_only_method_as_str() {
assert_eq!(ReadOnlyMethod::Get.as_str(), "GET");
assert_eq!(ReadOnlyMethod::Head.as_str(), "HEAD");
}
#[test]
fn read_only_method_display() {
assert_eq!(format!("{}", ReadOnlyMethod::Get), "GET");
assert_eq!(format!("{}", ReadOnlyMethod::Head), "HEAD");
}
#[test]
fn request_validation_error_is_display() {
let err = RequestValidationError::MethodNotAllowed;
assert!(!err.to_string().is_empty());
}
#[test]
fn request_validation_error_is_error() {
let err: &dyn std::error::Error = &RequestValidationError::BodyTooLarge;
assert!(!err.to_string().is_empty());
}
}