Skip to main content

net/fetch/
cors_cache.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! An implementation of the [CORS preflight cache](https://fetch.spec.whatwg.org/#cors-preflight-cache)
6//! For now this library is XHR-specific.
7//! For stuff involving `<img>`, `<iframe>`, `<form>`, etc please check what
8//! the request mode should be and compare with the fetch spec
9//! This library will eventually become the core of the Fetch crate
10//! with CORSRequest being expanded into FetchRequest (etc)
11
12use std::time::{Duration, Instant};
13
14use http::Method;
15use http::header::HeaderName;
16use net_traits::request::{CredentialsMode, NetworkPartitionKey, Origin, Request};
17use servo_url::ServoUrl;
18
19/// Union type for CORS cache entries
20///
21/// Each entry might pertain to a header or method
22#[derive(Clone, Debug)]
23pub enum HeaderOrMethod {
24    HeaderData(HeaderName),
25    MethodData(Method),
26}
27
28impl HeaderOrMethod {
29    fn match_header(&self, header_name: &HeaderName) -> bool {
30        match *self {
31            HeaderOrMethod::HeaderData(ref n) => n == header_name,
32            _ => false,
33        }
34    }
35
36    fn match_method(&self, method: &Method) -> bool {
37        match *self {
38            HeaderOrMethod::MethodData(ref m) => m == method,
39            _ => false,
40        }
41    }
42}
43
44/// <https://fetch.spec.whatwg.org/#cache-entry>
45#[derive(Clone, Debug)]
46pub struct CorsCacheEntry {
47    /// <https://fetch.spec.whatwg.org/#concept-cache-origin>
48    pub origin: Origin,
49    /// <https://fetch.spec.whatwg.org/#concept-cache-url>
50    pub url: ServoUrl,
51    /// <https://fetch.spec.whatwg.org/#concept-cache-max-age>
52    pub max_age: Duration,
53    /// <https://fetch.spec.whatwg.org/#concept-cache-credentials>
54    pub credentials: bool,
55    /// <https://fetch.spec.whatwg.org/#concept-cache-method>
56    /// <https://fetch.spec.whatwg.org/#concept-cache-header-name>
57    pub header_or_method: HeaderOrMethod,
58    /// <https://fetch.spec.whatwg.org/#cors-preflight-cache>
59    /// <https://fetch.spec.whatwg.org/#concept-cache-key>
60    pub network_partition_key: NetworkPartitionKey,
61    created: Instant,
62}
63
64impl CorsCacheEntry {
65    fn new(
66        origin: Origin,
67        url: ServoUrl,
68        max_age: Duration,
69        credentials: bool,
70        header_or_method: HeaderOrMethod,
71        network_partition_key: NetworkPartitionKey,
72    ) -> CorsCacheEntry {
73        CorsCacheEntry {
74            origin,
75            url,
76            max_age,
77            credentials,
78            header_or_method,
79            network_partition_key,
80            created: Instant::now(),
81        }
82    }
83}
84
85/// <https://fetch.spec.whatwg.org/#request-determine-the-network-partition-key>
86fn network_partition_key_for_request(request: &Request) -> NetworkPartitionKey {
87    // Step 1. If request’s reserved client is non-null,
88    // then return the result of determining the network partition key
89    // given request’s reserved client.
90    // TODO
91
92    // Step 2. If request’s client is non-null,
93    // then return the result of determining the network partition key
94    // given request’s client.
95    // Step 3. Return null.
96    request
97        .client
98        .as_ref()
99        .and_then(|c| c.network_partition_key.clone())
100}
101
102fn match_headers(cors_cache: &CorsCacheEntry, cors_req: &Request) -> bool {
103    cors_cache.origin == cors_req.origin &&
104        cors_cache.url == cors_req.current_url() &&
105        (cors_cache.credentials || cors_req.credentials_mode != CredentialsMode::Include) &&
106        cors_cache.network_partition_key == network_partition_key_for_request(cors_req)
107}
108
109/// A simple, vector-based CORS Cache
110#[derive(Clone, Default)]
111pub struct CorsCache(Vec<CorsCacheEntry>);
112
113impl CorsCache {
114    fn find_entry_by_header<'a>(
115        &'a mut self,
116        request: &Request,
117        header_name: &HeaderName,
118    ) -> Option<&'a mut CorsCacheEntry> {
119        self.cleanup();
120        self.0
121            .iter_mut()
122            .find(|e| match_headers(e, request) && e.header_or_method.match_header(header_name))
123    }
124
125    fn find_entry_by_method<'a>(
126        &'a mut self,
127        request: &Request,
128        method: &Method,
129    ) -> Option<&'a mut CorsCacheEntry> {
130        // we can take the method from CorSRequest itself
131        self.cleanup();
132        self.0
133            .iter_mut()
134            .find(|e| match_headers(e, request) && e.header_or_method.match_method(method))
135    }
136
137    /// Remove old entries
138    pub fn cleanup(&mut self) {
139        let now = Instant::now();
140        self.0.retain(|entry| now < entry.created + entry.max_age);
141    }
142
143    /// Returns true if an entry with a
144    /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found
145    pub fn match_header(&mut self, request: &Request, header_name: &HeaderName) -> bool {
146        self.find_entry_by_header(request, header_name).is_some()
147    }
148
149    /// Updates max age if an entry for a
150    /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found.
151    ///
152    /// If not, it will insert an equivalent entry
153    pub fn match_header_and_update(
154        &mut self,
155        request: &Request,
156        header_name: &HeaderName,
157        new_max_age: Duration,
158    ) -> bool {
159        match self
160            .find_entry_by_header(request, header_name)
161            .map(|e| e.max_age = new_max_age)
162        {
163            Some(_) => true,
164            None => {
165                self.insert(CorsCacheEntry::new(
166                    request.origin.clone(),
167                    request.current_url(),
168                    new_max_age,
169                    request.credentials_mode == CredentialsMode::Include,
170                    HeaderOrMethod::HeaderData(header_name.clone()),
171                    network_partition_key_for_request(request),
172                ));
173                false
174            },
175        }
176    }
177
178    /// Returns true if an entry with a
179    /// [matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found
180    pub fn match_method(&mut self, request: &Request, method: &Method) -> bool {
181        self.find_entry_by_method(request, method).is_some()
182    }
183
184    /// Updates max age if an entry for
185    /// [a matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found.
186    ///
187    /// If not, it will insert an equivalent entry
188    pub fn match_method_and_update(
189        &mut self,
190        request: &Request,
191        method: &Method,
192        new_max_age: Duration,
193    ) -> bool {
194        match self
195            .find_entry_by_method(request, method)
196            .map(|e| e.max_age = new_max_age)
197        {
198            Some(_) => true,
199            None => {
200                self.insert(CorsCacheEntry::new(
201                    request.origin.clone(),
202                    request.current_url(),
203                    new_max_age,
204                    request.credentials_mode == CredentialsMode::Include,
205                    HeaderOrMethod::MethodData(method.clone()),
206                    network_partition_key_for_request(request),
207                ));
208                false
209            },
210        }
211    }
212
213    /// Insert an entry
214    pub fn insert(&mut self, entry: CorsCacheEntry) {
215        self.cleanup();
216        self.0.push(entry);
217    }
218
219    /// <https://fetch.spec.whatwg.org/#concept-cache-clear>
220    pub(crate) fn clear_entries_for_origin_and_url(&mut self, origin: &Origin, url: &ServoUrl) {
221        // > To clear cache entries, given a request,
222        // > remove any cache entries in the user agent’s CORS-preflight cache
223        // > whose key equals the result of determining the network partition key
224        // > given request, byte-serialized origin is the result of byte-serializing
225        // > a request origin with request, and URL equals request’s current URL.
226        self.cleanup();
227        self.0
228            .retain(|cache_entry| !(cache_entry.origin == *origin && cache_entry.url == *url));
229    }
230}