| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| #![deny(missing_docs)] |
| |
| //! A memory cache implementing the logic specified in <http://tools.ietf.org/html/rfc7234> |
| //! and <http://tools.ietf.org/html/rfc7232>. |
| |
| use std::collections::HashMap; |
| use std::ops::Bound; |
| use std::sync::Mutex; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| use std::time::{Duration, Instant, SystemTime}; |
| |
| use headers::{ |
| CacheControl, ContentRange, Expires, HeaderMapExt, LastModified, Pragma, Range, Vary, |
| }; |
| use http::header::HeaderValue; |
| use http::{HeaderMap, Method, StatusCode, header}; |
| use log::debug; |
| use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf}; |
| use malloc_size_of_derive::MallocSizeOf; |
| use net_traits::http_status::HttpStatus; |
| use net_traits::request::Request; |
| use net_traits::response::{HttpsState, Response, ResponseBody}; |
| use net_traits::{FetchMetadata, Metadata, ResourceFetchTiming}; |
| use servo_arc::Arc; |
| use servo_config::pref; |
| use servo_url::ServoUrl; |
| use tokio::sync::mpsc::{UnboundedSender as TokioSender, unbounded_channel as unbounded}; |
| |
| use crate::fetch::methods::{Data, DoneChannel}; |
| |
| /// The key used to differentiate requests in the cache. |
| #[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)] |
| pub struct CacheKey { |
| url: ServoUrl, |
| } |
| |
| impl CacheKey { |
| /// Create a cache-key from a request. |
| pub(crate) fn new(request: &Request) -> CacheKey { |
| CacheKey { |
| url: request.current_url(), |
| } |
| } |
| |
| fn from_servo_url(servo_url: &ServoUrl) -> CacheKey { |
| CacheKey { |
| url: servo_url.clone(), |
| } |
| } |
| } |
| |
| /// A complete cached resource. |
| #[derive(Clone)] |
| struct CachedResource { |
| request_headers: Arc<Mutex<HeaderMap>>, |
| body: Arc<Mutex<ResponseBody>>, |
| aborted: Arc<AtomicBool>, |
| awaiting_body: Arc<Mutex<Vec<TokioSender<Data>>>>, |
| metadata: CachedMetadata, |
| location_url: Option<Result<ServoUrl, String>>, |
| https_state: HttpsState, |
| status: HttpStatus, |
| url_list: Vec<ServoUrl>, |
| expires: Duration, |
| last_validated: Instant, |
| } |
| |
| impl MallocSizeOf for CachedResource { |
| fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { |
| // TODO: self.request_headers.unconditional_size_of(ops) + |
| self.body.unconditional_size_of(ops) + |
| self.aborted.unconditional_size_of(ops) + |
| self.awaiting_body.unconditional_size_of(ops) + |
| self.metadata.size_of(ops) + |
| self.location_url.size_of(ops) + |
| self.https_state.size_of(ops) + |
| self.status.size_of(ops) + |
| self.url_list.size_of(ops) + |
| self.expires.size_of(ops) + |
| self.last_validated.size_of(ops) |
| } |
| } |
| |
| /// Metadata about a loaded resource, such as is obtained from HTTP headers. |
| #[derive(Clone, MallocSizeOf)] |
| struct CachedMetadata { |
| /// Headers |
| #[ignore_malloc_size_of = "Defined in `http` and has private members"] |
| pub headers: Arc<Mutex<HeaderMap>>, |
| /// Final URL after redirects. |
| pub final_url: ServoUrl, |
| /// MIME type / subtype. |
| pub content_type: Option<String>, |
| /// Character set. |
| pub charset: Option<String>, |
| /// HTTP Status |
| pub status: HttpStatus, |
| } |
| /// Wrapper around a cached response, including information on re-validation needs |
| pub struct CachedResponse { |
| /// The response constructed from the cached resource |
| pub response: Response, |
| /// The revalidation flag for the stored response |
| pub needs_validation: bool, |
| } |
| |
| /// A memory cache. |
| #[derive(Default, MallocSizeOf)] |
| pub struct HttpCache { |
| /// cached responses. |
| entries: HashMap<CacheKey, Vec<CachedResource>>, |
| } |
| |
| /// Determine if a response is cacheable by default <https://tools.ietf.org/html/rfc7231#section-6.1> |
| fn is_cacheable_by_default(status_code: StatusCode) -> bool { |
| matches!( |
| status_code.as_u16(), |
| 200 | 203 | 204 | 206 | 300 | 301 | 404 | 405 | 410 | 414 | 501 |
| ) |
| } |
| |
| /// Determine if a given response is cacheable. |
| /// Based on <https://tools.ietf.org/html/rfc7234#section-3> |
| fn response_is_cacheable(metadata: &Metadata) -> bool { |
| // TODO: if we determine that this cache should be considered shared: |
| // 1. check for absence of private response directive <https://tools.ietf.org/html/rfc7234#section-5.2.2.6> |
| // 2. check for absence of the Authorization header field. |
| let mut is_cacheable = false; |
| let headers = metadata.headers.as_ref().unwrap(); |
| if headers.contains_key(header::EXPIRES) || |
| headers.contains_key(header::LAST_MODIFIED) || |
| headers.contains_key(header::ETAG) |
| { |
| is_cacheable = true; |
| } |
| if let Some(ref directive) = headers.typed_get::<CacheControl>() { |
| if directive.no_store() { |
| return false; |
| } |
| if directive.public() || |
| directive.s_max_age().is_some() || |
| directive.max_age().is_some() || |
| directive.no_cache() |
| { |
| // If cache-control is understood, we can use it and ignore pragma. |
| return true; |
| } |
| } |
| if let Some(pragma) = headers.typed_get::<Pragma>() { |
| if pragma.is_no_cache() { |
| return false; |
| } |
| } |
| is_cacheable |
| } |
| |
| /// Calculating Age |
| /// <https://tools.ietf.org/html/rfc7234#section-4.2.3> |
| fn calculate_response_age(response: &Response) -> Duration { |
| // TODO: follow the spec more closely (Date headers, request/response lag, ...) |
| response |
| .headers |
| .get(header::AGE) |
| .and_then(|age_header| age_header.to_str().ok()) |
| .and_then(|age_string| age_string.parse::<u64>().ok()) |
| .map(Duration::from_secs) |
| .unwrap_or_default() |
| } |
| |
| /// Determine the expiry date from relevant headers, |
| /// or uses a heuristic if none are present. |
| fn get_response_expiry(response: &Response) -> Duration { |
| // Calculating Freshness Lifetime <https://tools.ietf.org/html/rfc7234#section-4.2.1> |
| let age = calculate_response_age(response); |
| let now = SystemTime::now(); |
| if let Some(directives) = response.headers.typed_get::<CacheControl>() { |
| if directives.no_cache() { |
| // Requires validation on first use. |
| return Duration::ZERO; |
| } |
| if let Some(max_age) = directives.max_age().or(directives.s_max_age()) { |
| return max_age.saturating_sub(age); |
| } |
| } |
| match response.headers.typed_get::<Expires>() { |
| Some(expiry) => { |
| // `duration_since` fails if `now` is later than `expiry_time` in which case, |
| // this whole thing return `Duration::ZERO`. |
| let expiry_time: SystemTime = expiry.into(); |
| return expiry_time.duration_since(now).unwrap_or(Duration::ZERO); |
| }, |
| // Malformed Expires header, shouldn't be used to construct a valid response. |
| None if response.headers.contains_key(header::EXPIRES) => return Duration::ZERO, |
| _ => {}, |
| } |
| // Calculating Heuristic Freshness |
| // <https://tools.ietf.org/html/rfc7234#section-4.2.2> |
| if let Some(ref code) = response.status.try_code() { |
| // <https://tools.ietf.org/html/rfc7234#section-5.5.4> |
| // Since presently we do not generate a Warning header field with a 113 warn-code, |
| // 24 hours minus response age is the max for heuristic calculation. |
| let max_heuristic = Duration::from_secs(24 * 60 * 60).saturating_sub(age); |
| let heuristic_freshness = if let Some(last_modified) = |
| // If the response has a Last-Modified header field, |
| // caches are encouraged to use a heuristic expiration value |
| // that is no more than some fraction of the interval since that time. |
| response.headers.typed_get::<LastModified>() |
| { |
| // `time_since_last_modified` will be `Duration::ZERO` if `last_modified` is |
| // after `now`. |
| let last_modified: SystemTime = last_modified.into(); |
| let time_since_last_modified = now.duration_since(last_modified).unwrap_or_default(); |
| |
| // A typical setting of this fraction might be 10%. |
| let raw_heuristic_calc = time_since_last_modified / 10; |
| if raw_heuristic_calc < max_heuristic { |
| raw_heuristic_calc |
| } else { |
| max_heuristic |
| } |
| } else { |
| max_heuristic |
| }; |
| if is_cacheable_by_default(*code) { |
| // Status codes that are cacheable by default can use heuristics to determine freshness. |
| return heuristic_freshness; |
| } |
| // Other status codes can only use heuristic freshness if the public cache directive is present. |
| if let Some(ref directives) = response.headers.typed_get::<CacheControl>() { |
| if directives.public() { |
| return heuristic_freshness; |
| } |
| } |
| } |
| // Requires validation upon first use as default. |
| Duration::ZERO |
| } |
| |
| /// Request Cache-Control Directives |
| /// <https://tools.ietf.org/html/rfc7234#section-5.2.1> |
| fn get_expiry_adjustment_from_request_headers(request: &Request, expires: Duration) -> Duration { |
| let Some(directive) = request.headers.typed_get::<CacheControl>() else { |
| return expires; |
| }; |
| |
| if let Some(max_age) = directive.max_stale() { |
| return expires + max_age; |
| } |
| |
| match directive.max_age() { |
| Some(max_age) if expires > max_age => return Duration::ZERO, |
| Some(max_age) => return expires - max_age, |
| None => {}, |
| }; |
| |
| if let Some(min_fresh) = directive.min_fresh() { |
| if expires < min_fresh { |
| return Duration::ZERO; |
| } |
| return expires - min_fresh; |
| } |
| |
| if directive.no_cache() || directive.no_store() { |
| return Duration::ZERO; |
| } |
| |
| expires |
| } |
| |
| /// Create a CachedResponse from a request and a CachedResource. |
| fn create_cached_response( |
| request: &Request, |
| cached_resource: &CachedResource, |
| cached_headers: &HeaderMap, |
| done_chan: &mut DoneChannel, |
| ) -> Option<CachedResponse> { |
| debug!("creating a cached response for {:?}", request.url()); |
| if cached_resource.aborted.load(Ordering::Acquire) { |
| return None; |
| } |
| let resource_timing = ResourceFetchTiming::new(request.timing_type()); |
| let mut response = Response::new(cached_resource.metadata.final_url.clone(), resource_timing); |
| response.headers = cached_headers.clone(); |
| response.body = cached_resource.body.clone(); |
| if let ResponseBody::Receiving(_) = *cached_resource.body.lock().unwrap() { |
| debug!("existing body is in progress"); |
| let (done_sender, done_receiver) = unbounded(); |
| *done_chan = Some((done_sender.clone(), done_receiver)); |
| cached_resource |
| .awaiting_body |
| .lock() |
| .unwrap() |
| .push(done_sender); |
| } |
| response |
| .location_url |
| .clone_from(&cached_resource.location_url); |
| response.status.clone_from(&cached_resource.status); |
| response.url_list.clone_from(&cached_resource.url_list); |
| response.https_state = cached_resource.https_state; |
| response.referrer = request.referrer.to_url().cloned(); |
| response.referrer_policy = request.referrer_policy; |
| response.aborted = cached_resource.aborted.clone(); |
| |
| let expires = cached_resource.expires; |
| let adjusted_expires = get_expiry_adjustment_from_request_headers(request, expires); |
| let time_since_validated = Instant::now() - cached_resource.last_validated; |
| |
| // TODO: take must-revalidate into account <https://tools.ietf.org/html/rfc7234#section-5.2.2.1> |
| // TODO: if this cache is to be considered shared, take proxy-revalidate into account |
| // <https://tools.ietf.org/html/rfc7234#section-5.2.2.7> |
| let has_expired = adjusted_expires <= time_since_validated; |
| let cached_response = CachedResponse { |
| response, |
| needs_validation: has_expired, |
| }; |
| Some(cached_response) |
| } |
| |
| /// Create a new resource, based on the bytes requested, and an existing resource, |
| /// with a status-code of 206. |
| fn create_resource_with_bytes_from_resource( |
| bytes: &[u8], |
| resource: &CachedResource, |
| ) -> CachedResource { |
| CachedResource { |
| request_headers: resource.request_headers.clone(), |
| body: Arc::new(Mutex::new(ResponseBody::Done(bytes.to_owned()))), |
| aborted: Arc::new(AtomicBool::new(false)), |
| awaiting_body: Arc::new(Mutex::new(vec![])), |
| metadata: resource.metadata.clone(), |
| location_url: resource.location_url.clone(), |
| https_state: resource.https_state, |
| status: StatusCode::PARTIAL_CONTENT.into(), |
| url_list: resource.url_list.clone(), |
| expires: resource.expires, |
| last_validated: resource.last_validated, |
| } |
| } |
| |
| /// Support for range requests <https://tools.ietf.org/html/rfc7233>. |
| fn handle_range_request( |
| request: &Request, |
| candidates: &[&CachedResource], |
| range_spec: &Range, |
| done_chan: &mut DoneChannel, |
| ) -> Option<CachedResponse> { |
| let mut complete_cached_resources = candidates |
| .iter() |
| .filter(|resource| resource.status == StatusCode::OK); |
| let partial_cached_resources = candidates |
| .iter() |
| .filter(|resource| resource.status == StatusCode::PARTIAL_CONTENT); |
| if let Some(complete_resource) = complete_cached_resources.next() { |
| // TODO: take the full range spec into account. |
| // If we have a complete resource, take the request range from the body. |
| // When there isn't a complete resource available, we loop over cached partials, |
| // and see if any individual partial response can fulfill the current request for a bytes range. |
| // TODO: combine partials that in combination could satisfy the requested range? |
| // see <https://tools.ietf.org/html/rfc7233#section-4.3>. |
| // TODO: add support for complete and partial resources, |
| // whose body is in the ResponseBody::Receiving state. |
| let body_len = match *complete_resource.body.lock().unwrap() { |
| ResponseBody::Done(ref body) => body.len(), |
| _ => 0, |
| }; |
| let bound = range_spec |
| .satisfiable_ranges(body_len.try_into().unwrap()) |
| .next() |
| .unwrap(); |
| match bound { |
| (Bound::Included(beginning), Bound::Included(end)) => { |
| if let ResponseBody::Done(ref body) = *complete_resource.body.lock().unwrap() { |
| if end == u64::MAX { |
| // Prevent overflow on the addition below. |
| return None; |
| } |
| let b = beginning as usize; |
| let e = end as usize + 1; |
| let requested = body.get(b..e); |
| if let Some(bytes) = requested { |
| let new_resource = |
| create_resource_with_bytes_from_resource(bytes, complete_resource); |
| let cached_headers = new_resource.metadata.headers.lock().unwrap(); |
| let cached_response = create_cached_response( |
| request, |
| &new_resource, |
| &cached_headers, |
| done_chan, |
| ); |
| if let Some(cached_response) = cached_response { |
| return Some(cached_response); |
| } |
| } |
| } |
| }, |
| (Bound::Included(beginning), Bound::Unbounded) => { |
| if let ResponseBody::Done(ref body) = *complete_resource.body.lock().unwrap() { |
| let b = beginning as usize; |
| let requested = body.get(b..); |
| if let Some(bytes) = requested { |
| let new_resource = |
| create_resource_with_bytes_from_resource(bytes, complete_resource); |
| let cached_headers = new_resource.metadata.headers.lock().unwrap(); |
| let cached_response = create_cached_response( |
| request, |
| &new_resource, |
| &cached_headers, |
| done_chan, |
| ); |
| if let Some(cached_response) = cached_response { |
| return Some(cached_response); |
| } |
| } |
| } |
| }, |
| _ => return None, |
| } |
| } else { |
| for partial_resource in partial_cached_resources { |
| let headers = partial_resource.metadata.headers.lock().unwrap(); |
| let content_range = headers.typed_get::<ContentRange>(); |
| |
| let Some(body_len) = content_range.as_ref().and_then(|range| range.bytes_len()) else { |
| continue; |
| }; |
| match range_spec.satisfiable_ranges(body_len - 1).next().unwrap() { |
| (Bound::Included(beginning), Bound::Included(end)) => { |
| let (res_beginning, res_end) = match content_range { |
| Some(range) => { |
| if let Some(bytes_range) = range.bytes_range() { |
| bytes_range |
| } else { |
| continue; |
| } |
| }, |
| _ => continue, |
| }; |
| if res_beginning <= beginning && res_end >= end { |
| let resource_body = &*partial_resource.body.lock().unwrap(); |
| let requested = match resource_body { |
| ResponseBody::Done(body) => { |
| let b = beginning as usize - res_beginning as usize; |
| let e = end as usize - res_beginning as usize + 1; |
| body.get(b..e) |
| }, |
| _ => continue, |
| }; |
| if let Some(bytes) = requested { |
| let new_resource = |
| create_resource_with_bytes_from_resource(bytes, partial_resource); |
| let cached_response = |
| create_cached_response(request, &new_resource, &headers, done_chan); |
| if let Some(cached_response) = cached_response { |
| return Some(cached_response); |
| } |
| } |
| } |
| }, |
| |
| (Bound::Included(beginning), Bound::Unbounded) => { |
| let (res_beginning, res_end, total) = if let Some(range) = content_range { |
| match (range.bytes_range(), range.bytes_len()) { |
| (Some(bytes_range), Some(total)) => { |
| (bytes_range.0, bytes_range.1, total) |
| }, |
| _ => continue, |
| } |
| } else { |
| continue; |
| }; |
| if total == 0 { |
| // Prevent overflow in the below operations from occuring. |
| continue; |
| }; |
| if res_beginning <= beginning && res_end == total - 1 { |
| let resource_body = &*partial_resource.body.lock().unwrap(); |
| let requested = match resource_body { |
| ResponseBody::Done(body) => { |
| let from_byte = beginning as usize - res_beginning as usize; |
| body.get(from_byte..) |
| }, |
| _ => continue, |
| }; |
| if let Some(bytes) = requested { |
| let new_resource = |
| create_resource_with_bytes_from_resource(bytes, partial_resource); |
| let cached_response = |
| create_cached_response(request, &new_resource, &headers, done_chan); |
| if let Some(cached_response) = cached_response { |
| return Some(cached_response); |
| } |
| } |
| } |
| }, |
| |
| _ => continue, |
| } |
| } |
| } |
| |
| None |
| } |
| |
| impl HttpCache { |
| /// Constructing Responses from Caches. |
| /// <https://tools.ietf.org/html/rfc7234#section-4> |
| pub fn construct_response( |
| &self, |
| request: &Request, |
| done_chan: &mut DoneChannel, |
| ) -> Option<CachedResponse> { |
| if pref!(network_http_cache_disabled) { |
| return None; |
| } |
| |
| // TODO: generate warning headers as appropriate <https://tools.ietf.org/html/rfc7234#section-5.5> |
| debug!("trying to construct cache response for {:?}", request.url()); |
| if request.method != Method::GET { |
| // Only Get requests are cached, avoid a url based match for others. |
| debug!("non-GET method, not caching"); |
| return None; |
| } |
| let entry_key = CacheKey::new(request); |
| let resources = self |
| .entries |
| .get(&entry_key)? |
| .iter() |
| .filter(|r| !r.aborted.load(Ordering::Relaxed)); |
| let mut candidates = vec![]; |
| for cached_resource in resources { |
| let mut can_be_constructed = true; |
| let cached_headers = cached_resource.metadata.headers.lock().unwrap(); |
| let original_request_headers = cached_resource.request_headers.lock().unwrap(); |
| if let Some(vary_value) = cached_headers.typed_get::<Vary>() { |
| if vary_value.is_any() { |
| debug!("vary value is any, not caching"); |
| can_be_constructed = false |
| } else { |
| // For every header name found in the Vary header of the stored response. |
| // Calculating Secondary Keys with Vary <https://tools.ietf.org/html/rfc7234#section-4.1> |
| for vary_val in vary_value.iter_strs() { |
| match request.headers.get(vary_val) { |
| Some(header_data) => { |
| // If the header is present in the request. |
| if let Some(original_header_data) = |
| original_request_headers.get(vary_val) |
| { |
| // Check that the value of the nominated header field, |
| // in the original request, matches the value in the current request. |
| if original_header_data != header_data { |
| debug!("headers don't match, not caching"); |
| can_be_constructed = false; |
| break; |
| } |
| } |
| }, |
| None => { |
| // If a header field is absent from a request, |
| // it can only match a stored response if those headers, |
| // were also absent in the original request. |
| can_be_constructed = |
| original_request_headers.get(vary_val).is_none(); |
| if !can_be_constructed { |
| debug!("vary header present, not caching"); |
| } |
| }, |
| } |
| if !can_be_constructed { |
| break; |
| } |
| } |
| } |
| } |
| if can_be_constructed { |
| candidates.push(cached_resource); |
| } |
| } |
| // Support for range requests |
| if let Some(range_spec) = request.headers.typed_get::<Range>() { |
| return handle_range_request(request, candidates.as_slice(), &range_spec, done_chan); |
| } |
| while let Some(cached_resource) = candidates.pop() { |
| // Not a Range request. |
| // Do not allow 206 responses to be constructed. |
| // |
| // See https://tools.ietf.org/html/rfc7234#section-3.1 |
| // |
| // A cache MUST NOT use an incomplete response to answer requests unless the |
| // response has been made complete or the request is partial and |
| // specifies a range that is wholly within the incomplete response. |
| // |
| // TODO: Combining partial content to fulfill a non-Range request |
| // see https://tools.ietf.org/html/rfc7234#section-3.3 |
| match cached_resource.status.try_code() { |
| Some(ref code) => { |
| if *code == StatusCode::PARTIAL_CONTENT { |
| continue; |
| } |
| }, |
| None => continue, |
| } |
| // Returning a response that can be constructed |
| // TODO: select the most appropriate one, using a known mechanism from a selecting header field, |
| // or using the Date header to return the most recent one. |
| let cached_headers = cached_resource.metadata.headers.lock().unwrap(); |
| let cached_response = |
| create_cached_response(request, cached_resource, &cached_headers, done_chan); |
| if let Some(cached_response) = cached_response { |
| return Some(cached_response); |
| } |
| } |
| debug!("couldn't find an appropriate response, not caching"); |
| // The cache wasn't able to construct anything. |
| None |
| } |
| |
| /// Wake-up consumers of cached resources |
| /// whose response body was still receiving data when the resource was constructed, |
| /// and whose response has now either been completed or cancelled. |
| pub fn update_awaiting_consumers(&self, request: &Request, response: &Response) { |
| let entry_key = CacheKey::new(request); |
| |
| let cached_resources = match self.entries.get(&entry_key) { |
| None => return, |
| Some(resources) => resources, |
| }; |
| |
| let actual_response = response.actual_response(); |
| |
| // Ensure we only wake-up consumers of relevant resources, |
| // ie we don't want to wake-up 200 awaiting consumers with a 206. |
| let relevant_cached_resources = cached_resources.iter().filter(|resource| { |
| if actual_response.is_network_error() { |
| return *resource.body.lock().unwrap() == ResponseBody::Empty; |
| } |
| resource.status == actual_response.status |
| }); |
| |
| for cached_resource in relevant_cached_resources { |
| let mut awaiting_consumers = cached_resource.awaiting_body.lock().unwrap(); |
| if awaiting_consumers.is_empty() { |
| continue; |
| } |
| let to_send = if cached_resource.aborted.load(Ordering::Acquire) { |
| // In the case of an aborted fetch, |
| // wake-up all awaiting consumers. |
| // Each will then start a new network request. |
| // TODO: Wake-up only one consumer, and make it the producer on which others wait. |
| Data::Cancelled |
| } else { |
| match *cached_resource.body.lock().unwrap() { |
| ResponseBody::Done(_) | ResponseBody::Empty => Data::Done, |
| ResponseBody::Receiving(_) => { |
| continue; |
| }, |
| } |
| }; |
| for done_sender in awaiting_consumers.drain(..) { |
| let _ = done_sender.send(to_send.clone()); |
| } |
| } |
| } |
| |
| /// Freshening Stored Responses upon Validation. |
| /// <https://tools.ietf.org/html/rfc7234#section-4.3.4> |
| pub fn refresh( |
| &mut self, |
| request: &Request, |
| response: Response, |
| done_chan: &mut DoneChannel, |
| ) -> Option<Response> { |
| assert_eq!(response.status, StatusCode::NOT_MODIFIED); |
| let entry_key = CacheKey::new(request); |
| if let Some(cached_resources) = self.entries.get_mut(&entry_key) { |
| if let Some(cached_resource) = cached_resources.iter_mut().next() { |
| // done_chan will have been set to Some(..) by http_network_fetch. |
| // If the body is not receiving data, set the done_chan back to None. |
| // Otherwise, create a new dedicated channel to update the consumer. |
| // The response constructed here will replace the 304 one from the network. |
| let in_progress_channel = match *cached_resource.body.lock().unwrap() { |
| ResponseBody::Receiving(..) => Some(unbounded()), |
| ResponseBody::Empty | ResponseBody::Done(..) => None, |
| }; |
| match in_progress_channel { |
| Some((done_sender, done_receiver)) => { |
| *done_chan = Some((done_sender.clone(), done_receiver)); |
| cached_resource |
| .awaiting_body |
| .lock() |
| .unwrap() |
| .push(done_sender); |
| }, |
| None => *done_chan = None, |
| } |
| // Received a response with 304 status code, in response to a request that matches a cached resource. |
| // 1. update the headers of the cached resource. |
| // 2. return a response, constructed from the cached resource. |
| let resource_timing = ResourceFetchTiming::new(request.timing_type()); |
| let mut constructed_response = |
| Response::new(cached_resource.metadata.final_url.clone(), resource_timing); |
| constructed_response.body = cached_resource.body.clone(); |
| constructed_response |
| .status |
| .clone_from(&cached_resource.status); |
| constructed_response.https_state = cached_resource.https_state; |
| constructed_response.referrer = request.referrer.to_url().cloned(); |
| constructed_response.referrer_policy = request.referrer_policy; |
| constructed_response |
| .status |
| .clone_from(&cached_resource.status); |
| constructed_response |
| .url_list |
| .clone_from(&cached_resource.url_list); |
| cached_resource.expires = get_response_expiry(&constructed_response); |
| let mut stored_headers = cached_resource.metadata.headers.lock().unwrap(); |
| stored_headers.extend(response.headers); |
| constructed_response.headers = stored_headers.clone(); |
| return Some(constructed_response); |
| } |
| } |
| None |
| } |
| |
| fn invalidate_for_url(&mut self, url: &ServoUrl) { |
| let entry_key = CacheKey::from_servo_url(url); |
| if let Some(cached_resources) = self.entries.get_mut(&entry_key) { |
| for cached_resource in cached_resources.iter_mut() { |
| cached_resource.expires = Duration::ZERO; |
| } |
| } |
| } |
| |
| /// Invalidation. |
| /// <https://tools.ietf.org/html/rfc7234#section-4.4> |
| pub fn invalidate(&mut self, request: &Request, response: &Response) { |
| // TODO(eijebong): Once headers support typed_get, update this to use them |
| if let Some(Ok(location)) = response |
| .headers |
| .get(header::LOCATION) |
| .map(HeaderValue::to_str) |
| { |
| if let Ok(url) = request.current_url().join(location) { |
| self.invalidate_for_url(&url); |
| } |
| } |
| if let Some(Ok(content_location)) = response |
| .headers |
| .get(header::CONTENT_LOCATION) |
| .map(HeaderValue::to_str) |
| { |
| if let Ok(url) = request.current_url().join(content_location) { |
| self.invalidate_for_url(&url); |
| } |
| } |
| self.invalidate_for_url(&request.url()); |
| } |
| |
| /// Storing Responses in Caches. |
| /// <https://tools.ietf.org/html/rfc7234#section-3> |
| pub fn store(&mut self, request: &Request, response: &Response) { |
| if pref!(network_http_cache_disabled) { |
| return; |
| } |
| if request.method != Method::GET { |
| // Only Get requests are cached. |
| return; |
| } |
| if request.headers.contains_key(header::AUTHORIZATION) { |
| // https://tools.ietf.org/html/rfc7234#section-3.1 |
| // A shared cache MUST NOT use a cached response |
| // to a request with an Authorization header field |
| // |
| // TODO: unless a cache directive that allows such |
| // responses to be stored is present in the response. |
| return; |
| }; |
| let entry_key = CacheKey::new(request); |
| let metadata = match response.metadata() { |
| Ok(FetchMetadata::Filtered { |
| filtered: _, |
| unsafe_: metadata, |
| }) | |
| Ok(FetchMetadata::Unfiltered(metadata)) => metadata, |
| _ => return, |
| }; |
| if !response_is_cacheable(&metadata) { |
| return; |
| } |
| let expiry = get_response_expiry(response); |
| let cacheable_metadata = CachedMetadata { |
| headers: Arc::new(Mutex::new(response.headers.clone())), |
| final_url: metadata.final_url, |
| content_type: metadata.content_type.map(|v| v.0.to_string()), |
| charset: metadata.charset, |
| status: metadata.status, |
| }; |
| let entry_resource = CachedResource { |
| request_headers: Arc::new(Mutex::new(request.headers.clone())), |
| body: response.body.clone(), |
| aborted: response.aborted.clone(), |
| awaiting_body: Arc::new(Mutex::new(vec![])), |
| metadata: cacheable_metadata, |
| location_url: response.location_url.clone(), |
| https_state: response.https_state, |
| status: response.status.clone(), |
| url_list: response.url_list.clone(), |
| expires: expiry, |
| last_validated: Instant::now(), |
| }; |
| let entry = self.entries.entry(entry_key).or_default(); |
| entry.push(entry_resource); |
| // TODO: Complete incomplete responses, including 206 response, when stored here. |
| // See A cache MAY complete a stored incomplete response by making a subsequent range request |
| // https://tools.ietf.org/html/rfc7234#section-3.1 |
| } |
| |
| /// Clear the contents of this cache. |
| pub fn clear(&mut self) { |
| self.entries.clear(); |
| } |
| } |