From 632f7a41bf7a362747ee22ce50f70ec69d12e766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20M=C3=B6ller?= Date: Sun, 23 Jun 2024 14:25:07 +0200 Subject: [PATCH] feat: implements remaining http cache conditionalss (#407) * implements remaining http conditionals * computed etag is not optional --- src/server.rs | 37 +++++++++++++++-------- tests/cache.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 tests/cache.rs diff --git a/src/server.rs b/src/server.rs index 942da7d..a28c4f3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,8 +15,8 @@ use chrono::{LocalResult, TimeZone, Utc}; use futures_util::{pin_mut, TryStreamExt}; use headers::{ AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl, - ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfModifiedSince, IfNoneMatch, - IfRange, LastModified, Range, + ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfMatch, IfModifiedSince, + IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range, }; use http_body_util::{combinators::BoxBody, BodyExt, StreamBody}; use hyper::body::Frame; @@ -796,18 +796,29 @@ impl Server { let size = meta.len(); let mut use_range = true; if let Some((etag, last_modified)) = extract_cache_headers(&meta) { - let cached = { - if let Some(if_none_match) = headers.typed_get::() { - !if_none_match.precondition_passes(&etag) - } else if let Some(if_modified_since) = headers.typed_get::() { - !if_modified_since.is_modified(last_modified.into()) - } else { - false + if let Some(if_unmodified_since) = headers.typed_get::() { + if !if_unmodified_since.precondition_passes(last_modified.into()) { + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return Ok(()); + } + } + if let Some(if_match) = headers.typed_get::() { + if !if_match.precondition_passes(&etag) { + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return Ok(()); + } + } + if let Some(if_modified_since) = headers.typed_get::() { + if !if_modified_since.is_modified(last_modified.into()) { + *res.status_mut() = StatusCode::NOT_MODIFIED; + return Ok(()); + } + } + if let Some(if_none_match) = headers.typed_get::() { + if !if_none_match.precondition_passes(&etag) { + *res.status_mut() = StatusCode::NOT_MODIFIED; + return Ok(()); } - }; - if cached { - *res.status_mut() = StatusCode::NOT_MODIFIED; - return Ok(()); } res.headers_mut().typed_insert(last_modified); diff --git a/tests/cache.rs b/tests/cache.rs new file mode 100644 index 0000000..588f021 --- /dev/null +++ b/tests/cache.rs @@ -0,0 +1,80 @@ +mod fixtures; +mod utils; + +use chrono::{DateTime, Duration}; +use fixtures::{server, Error, TestServer}; +use reqwest::header::{ + HeaderName, ETAG, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE, + LAST_MODIFIED, +}; +use reqwest::StatusCode; +use rstest::rstest; + +#[rstest] +#[case(IF_UNMODIFIED_SINCE, Duration::days(1), StatusCode::OK)] +#[case(IF_UNMODIFIED_SINCE, Duration::days(0), StatusCode::OK)] +#[case(IF_UNMODIFIED_SINCE, Duration::days(-1), StatusCode::PRECONDITION_FAILED)] +#[case(IF_MODIFIED_SINCE, Duration::days(1), StatusCode::NOT_MODIFIED)] +#[case(IF_MODIFIED_SINCE, Duration::days(0), StatusCode::NOT_MODIFIED)] +#[case(IF_MODIFIED_SINCE, Duration::days(-1), StatusCode::OK)] +fn get_file_with_if_modified_since_condition( + #[case] header_condition: HeaderName, + #[case] duration_after_file_modified: Duration, + #[case] expected_code: StatusCode, + server: TestServer, +) -> Result<(), Error> { + let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?; + + let last_modified = resp + .headers() + .get(LAST_MODIFIED) + .and_then(|h| h.to_str().ok()) + .and_then(|s| DateTime::parse_from_rfc2822(s).ok()) + .expect("Recieved no valid last modified header"); + + let req_modified_time = (last_modified + duration_after_file_modified) + .format("%a, %e %b %Y %T GMT") + .to_string(); + + let resp = fetch!(b"GET", format!("{}index.html", server.url())) + .header(header_condition, req_modified_time) + .send()?; + + assert_eq!(resp.status(), expected_code); + Ok(()) +} + +fn same_etag(etag: &str) -> String { + etag.to_owned() +} + +fn different_etag(etag: &str) -> String { + format!("{}1234", etag) +} + +#[rstest] +#[case(IF_MATCH, same_etag, StatusCode::OK)] +#[case(IF_MATCH, different_etag, StatusCode::PRECONDITION_FAILED)] +#[case(IF_NONE_MATCH, same_etag, StatusCode::NOT_MODIFIED)] +#[case(IF_NONE_MATCH, different_etag, StatusCode::OK)] +fn get_file_with_etag_match( + #[case] header_condition: HeaderName, + #[case] etag_modifier: fn(&str) -> String, + #[case] expected_code: StatusCode, + server: TestServer, +) -> Result<(), Error> { + let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?; + + let etag = resp + .headers() + .get(ETAG) + .and_then(|h| h.to_str().ok()) + .expect("Recieved no valid etag header"); + + let resp = fetch!(b"GET", format!("{}index.html", server.url())) + .header(header_condition, etag_modifier(etag)) + .send()?; + + assert_eq!(resp.status(), expected_code); + Ok(()) +}