summaryrefslogtreecommitdiff
path: root/cli/http_util.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/http_util.rs')
-rw-r--r--cli/http_util.rs152
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),