use crate::{
error::SaphirError,
file::etag::{EntityTag, SystemTimeExt},
request::Request,
};
use chrono::{DateTime, FixedOffset, Utc};
use hyper::Method;
use std::time::SystemTime;
fn check_if_match(etag: &EntityTag, if_match: &str) -> bool {
if_match.trim() == "*" || if_match.split(',').any(|string| etag.strong_eq(EntityTag::parse(string.trim())))
}
fn check_if_none_match(etag: &EntityTag, if_none_match: &str) -> bool {
if_none_match.trim() != "*" && if_none_match.split(',').all(|string| !etag.weak_eq(EntityTag::parse(string.trim())))
}
fn check_if_unmodified_since(last_modified: &SystemTime, if_unmodified_since: &SystemTime) -> bool {
last_modified.timestamp() <= if_unmodified_since.timestamp()
}
fn check_if_modified_since(last_modified: &SystemTime, if_modified_since: &SystemTime) -> bool {
!check_if_unmodified_since(last_modified, &if_modified_since)
}
fn is_method_get_head(method: &Method) -> bool {
match *method {
Method::GET | Method::HEAD => true,
_ => false,
}
}
pub fn is_precondition_failed(req: &Request, etag: &EntityTag, last_modified: &SystemTime) -> bool {
if let Some(if_match) = req.headers().get(http::header::IF_MATCH) {
if check_if_match(etag, if_match.to_str().unwrap_or_default()) {
if req.headers().get(http::header::IF_NONE_MATCH).is_some() && !is_method_get_head(req.method()) {
return true;
}
} else {
return true;
}
}
if let Some(if_unmodified_since) = req
.headers()
.get(http::header::IF_UNMODIFIED_SINCE)
.and_then(|header| header.to_str().ok())
.and_then(|s| date_from_http_str(s).ok())
.map(|time| time.into())
{
if check_if_unmodified_since(last_modified, &if_unmodified_since) {
if req.headers().get(http::header::IF_NONE_MATCH).is_some() && !is_method_get_head(req.method()) {
return true;
}
} else {
return true;
}
}
if req.headers().get(http::header::IF_NONE_MATCH).is_some() && !is_method_get_head(req.method()) {
return true;
}
false
}
pub fn is_fresh(req: &Request, etag: &EntityTag, last_modified: &SystemTime) -> bool {
if let Some(Ok(if_none_match)) = req.headers().get(http::header::IF_NONE_MATCH).map(|header| header.to_str()) {
!check_if_none_match(etag, if_none_match)
} else if let Some(since) = req
.headers()
.get(http::header::IF_UNMODIFIED_SINCE)
.and_then(|header| header.to_str().ok())
.and_then(|s| date_from_http_str(s).ok())
.map(|time| time.into())
{
!check_if_modified_since(last_modified, &since)
} else {
false
}
}
pub fn format_systemtime(time: SystemTime) -> String {
DateTime::<Utc>::from(time).format("%a, %d %b %Y %T GMT").to_string()
}
pub fn date_from_http_str(http: &str) -> Result<DateTime<FixedOffset>, SaphirError> {
match DateTime::parse_from_rfc2822(http)
.or_else(|_| DateTime::parse_from_str(http, "%A, %d-%b-%y %T %Z"))
.or_else(|_| DateTime::parse_from_str(http, "%c"))
{
Ok(t) => Ok(t),
Err(_) => Err(SaphirError::Other("Cannot parse date from header".to_owned())),
}
}
#[cfg(test)]
mod t {
use super::*;
use crate::{file::etag::EntityTag, prelude::Body};
use http::request::Builder;
use std::time::Duration;
mod match_none_match {
use super::*;
#[test]
fn any() {
let etag = EntityTag::Strong("".to_owned());
assert!(check_if_match(&etag, "*"));
assert!(!check_if_none_match(&etag, "*"));
}
#[test]
fn one() {
let etag = EntityTag::Strong("2".to_owned());
let tags = format!(
"{},{},{}",
EntityTag::Strong("0".to_owned()).get_tag(),
EntityTag::Strong("1".to_owned()).get_tag(),
EntityTag::Strong("2".to_owned()).get_tag(),
);
assert!(check_if_match(&etag, &tags));
assert!(!check_if_none_match(&etag, &tags));
}
#[test]
fn none() {
let etag = EntityTag::Strong("0".to_owned());
let tags = EntityTag::Strong("1".to_owned()).get_tag();
assert!(!check_if_match(&etag, &tags));
assert!(check_if_none_match(&etag, &tags));
}
}
mod modified_unmodified_since {
use super::*;
fn init_since() -> (SystemTime, SystemTime) {
let now = SystemTime::now();
(now, now)
}
#[test]
fn now() {
let (now, last_modified) = init_since();
assert!(!check_if_modified_since(&last_modified, &now));
assert!(check_if_unmodified_since(&last_modified, &now));
}
#[test]
fn after_one_sec() {
let (now, last_modified) = init_since();
let modified = now + Duration::from_secs(1);
assert!(!check_if_modified_since(&last_modified, &modified));
assert!(check_if_unmodified_since(&last_modified, &modified));
}
#[test]
fn one_sec_ago() {
let (now, last_modified) = init_since();
let modified = now - Duration::from_secs(1);
assert!(check_if_modified_since(&last_modified, &modified));
assert!(!check_if_unmodified_since(&last_modified, &modified));
}
}
fn init_request() -> (Builder, EntityTag, SystemTime) {
(
http::request::Request::builder().method("GET"),
EntityTag::Strong("hello".to_owned()),
SystemTime::now(),
)
}
mod fresh {
use super::*;
#[test]
fn no_precondition_header_fields() {
let (req, etag, date) = init_request();
let req = Request::new(req.body(Body::empty()).unwrap(), None);
assert!(!is_fresh(&req, &etag, &date));
}
#[test]
fn if_none_match_precedes_if_modified_since() {
let (req, etag, date) = init_request();
let if_none_match = etag.get_tag();
let if_modified_since = format_systemtime(date + Duration::from_secs(1));
let req = Request::new(
req.header(http::header::IF_NONE_MATCH, if_none_match)
.header(http::header::IF_MODIFIED_SINCE, if_modified_since)
.body(Body::empty())
.unwrap(),
None,
);
assert!(is_fresh(&req, &etag, &date));
}
}
mod precondition {
use super::*;
#[test]
fn ok_without_any_precondition() {
let (req, etag, date) = init_request();
let req = Request::new(req.body(Body::empty()).unwrap(), None);
assert!(!is_precondition_failed(&req, &etag, &date));
}
#[test]
fn failed_with_if_match_not_passes() {
let (req, etag, date) = init_request();
let if_match = EntityTag::Strong("".to_owned()).get_tag();
let req = Request::new(req.header(http::header::IF_MATCH, if_match).body(Body::empty()).unwrap(), None);
assert!(is_precondition_failed(&req, &etag, &date));
}
#[test]
fn with_if_match_passes_get() {
let (req, etag, date) = init_request();
let if_match = EntityTag::Strong("hello".to_owned()).get_tag();
let if_none_match = EntityTag::Strong("world".to_owned()).get_tag();
let req = Request::new(
req.header(http::header::IF_MATCH, if_match)
.header(http::header::IF_NONE_MATCH, if_none_match)
.body(Body::empty())
.unwrap(),
None,
);
assert!(!is_precondition_failed(&req, &etag, &date));
}
#[test]
fn with_if_match_fails_post() {
let (req, etag, date) = init_request();
let if_match = EntityTag::Strong("hello".to_owned()).get_tag();
let if_none_match = EntityTag::Strong("world".to_owned()).get_tag();
let req = Request::new(
req.method(Method::POST)
.header(http::header::IF_MATCH, if_match)
.header(http::header::IF_NONE_MATCH, if_none_match)
.body(Body::empty())
.unwrap(),
None,
);
assert!(is_precondition_failed(&req, &etag, &date));
}
#[test]
fn failed_with_if_unmodified_since_not_passes() {
let (req, etag, date) = init_request();
let if_unmodified_since = date - Duration::from_secs(1);
let req = Request::new(
req.header(http::header::IF_UNMODIFIED_SINCE, self::format_systemtime(if_unmodified_since))
.body(Body::empty())
.unwrap(),
None,
);
assert!(is_precondition_failed(&req, &etag, &date));
}
#[test]
fn with_if_unmodified_since_passes_get() {
let (req, etag, if_unmodified_since) = init_request();
let if_none_match = EntityTag::Strong("nonematch".to_owned()).get_tag();
let req = Request::new(
req.header(http::header::IF_UNMODIFIED_SINCE, self::format_systemtime(if_unmodified_since))
.header(http::header::IF_NONE_MATCH, if_none_match)
.body(Body::empty())
.unwrap(),
None,
);
assert!(!is_precondition_failed(&req, &etag, &if_unmodified_since));
}
#[test]
fn with_if_unmodified_since_fails_post() {
let (req, etag, if_unmodified_since) = init_request();
let if_none_match = EntityTag::Strong("nonematch".to_owned()).get_tag();
let req = Request::new(
req.method(Method::POST)
.header(http::header::IF_UNMODIFIED_SINCE, self::format_systemtime(if_unmodified_since))
.header(http::header::IF_NONE_MATCH, if_none_match)
.body(Body::empty())
.unwrap(),
None,
);
assert!(is_precondition_failed(&req, &etag, &if_unmodified_since));
}
}
}