diff options
Diffstat (limited to 'cli/http_util.rs')
-rw-r--r-- | cli/http_util.rs | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/cli/http_util.rs b/cli/http_util.rs index 87ed7d598..562cd06f2 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,6 +1,9 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::auth_tokens::AuthToken; +use cache_control::Cachability; +use cache_control::CacheControl; +use chrono::DateTime; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::AnyError; @@ -13,6 +16,8 @@ use deno_runtime::deno_fetch::reqwest::Client; use deno_runtime::deno_fetch::reqwest::StatusCode; use log::debug; use std::collections::HashMap; +use std::time::Duration; +use std::time::SystemTime; /// Construct the next uri based on base uri and location header fragment /// See <https://tools.ietf.org/html/rfc3986#section-4.2> @@ -46,6 +51,153 @@ fn resolve_url_from_location(base_url: &Url, location: &str) -> Url { // Vec<(String, String)> pub type HeadersMap = HashMap<String, String>; +/// A structure used to determine if a entity in the http cache can be used. +/// +/// This is heavily influenced by +/// https://github.com/kornelski/rusty-http-cache-semantics which is BSD +/// 2-Clause Licensed and copyright Kornel LesiĆski +pub(crate) struct CacheSemantics { + cache_control: CacheControl, + cached: SystemTime, + headers: HashMap<String, String>, + now: SystemTime, +} + +impl CacheSemantics { + pub fn new( + headers: HashMap<String, String>, + cached: SystemTime, + now: SystemTime, + ) -> Self { + let cache_control = headers + .get("cache-control") + .map(|v| CacheControl::from_value(v).unwrap_or_default()) + .unwrap_or_default(); + Self { + cache_control, + cached, + headers, + now, + } + } + + fn age(&self) -> Duration { + let mut age = self.age_header_value(); + + if let Ok(resident_time) = self.now.duration_since(self.cached) { + age += resident_time; + } + + age + } + + fn age_header_value(&self) -> Duration { + Duration::from_secs( + self + .headers + .get("age") + .and_then(|v| v.parse().ok()) + .unwrap_or(0), + ) + } + + fn is_stale(&self) -> bool { + self.max_age() <= self.age() + } + + fn max_age(&self) -> Duration { + if self.cache_control.cachability == Some(Cachability::NoCache) { + return Duration::from_secs(0); + } + + if self.headers.get("vary").map(|s| s.trim()) == Some("*") { + return Duration::from_secs(0); + } + + if let Some(max_age) = self.cache_control.max_age { + return max_age; + } + + let default_min_ttl = Duration::from_secs(0); + + let server_date = self.raw_server_date(); + if let Some(expires) = self.headers.get("expires") { + return match DateTime::parse_from_rfc2822(expires) { + Err(_) => Duration::from_secs(0), + Ok(expires) => { + let expires = SystemTime::UNIX_EPOCH + + Duration::from_secs(expires.timestamp().max(0) as _); + return default_min_ttl + .max(expires.duration_since(server_date).unwrap_or_default()); + } + }; + } + + if let Some(last_modified) = self.headers.get("last-modified") { + if let Ok(last_modified) = DateTime::parse_from_rfc2822(last_modified) { + let last_modified = SystemTime::UNIX_EPOCH + + Duration::from_secs(last_modified.timestamp().max(0) as _); + if let Ok(diff) = server_date.duration_since(last_modified) { + let secs_left = diff.as_secs() as f64 * 0.1; + return default_min_ttl.max(Duration::from_secs(secs_left as _)); + } + } + } + + default_min_ttl + } + + fn raw_server_date(&self) -> SystemTime { + self + .headers + .get("date") + .and_then(|d| DateTime::parse_from_rfc2822(d).ok()) + .and_then(|d| { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(d.timestamp() as _)) + }) + .unwrap_or(self.cached) + } + + /// Returns true if the cached value is "fresh" respecting cached headers, + /// otherwise returns false. + pub fn should_use(&self) -> bool { + if self.cache_control.cachability == Some(Cachability::NoCache) { + return false; + } + + if let Some(max_age) = self.cache_control.max_age { + if self.age() > max_age { + return false; + } + } + + if let Some(min_fresh) = self.cache_control.min_fresh { + if self.time_to_live() < min_fresh { + return false; + } + } + + if self.is_stale() { + let has_max_stale = self.cache_control.max_stale.is_some(); + let allows_stale = has_max_stale + && self + .cache_control + .max_stale + .map_or(true, |val| val > self.age() - self.max_age()); + if !allows_stale { + return false; + } + } + + true + } + + fn time_to_live(&self) -> Duration { + self.max_age().checked_sub(self.age()).unwrap_or_default() + } +} + #[derive(Debug, PartialEq)] pub enum FetchOnceResult { Code(Vec<u8>, HeadersMap), |