Skip to main content

icann_rdap_common/
httpdata.rs

1//! Code for handling HTTP caching.
2
3use {
4    chrono::{DateTime, Duration, Utc},
5    serde::{Deserialize, Serialize},
6};
7
8/// Represents the data from HTTP responses.
9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
10pub struct HttpData {
11    pub content_length: Option<u64>,
12    pub content_type: Option<String>,
13    pub scheme: Option<String>,
14    pub host: String,
15    pub expires: Option<String>,
16    pub cache_control: Option<String>,
17    pub received: DateTime<Utc>,
18    pub status_code: u16,
19    pub location: Option<String>,
20    pub access_control_allow_origin: Option<String>,
21    pub access_control_allow_credentials: Option<String>,
22    pub strict_transport_security: Option<String>,
23    pub retry_after: Option<String>,
24    pub request_uri: Option<String>,
25}
26
27#[buildstructor::buildstructor]
28impl HttpData {
29    #[builder(visibility = "pub")]
30    fn new(
31        content_length: Option<u64>,
32        content_type: Option<String>,
33        scheme: Option<String>,
34        host: String,
35        expires: Option<String>,
36        cache_control: Option<String>,
37        status_code: u16,
38        location: Option<String>,
39        access_control_allow_origin: Option<String>,
40        access_control_allow_credentials: Option<String>,
41        strict_transport_security: Option<String>,
42        retry_after: Option<String>,
43        received: DateTime<Utc>,
44        request_uri: Option<String>,
45    ) -> Self {
46        Self {
47            content_length,
48            content_type,
49            scheme,
50            host,
51            expires,
52            cache_control,
53            received,
54            status_code,
55            location,
56            access_control_allow_origin,
57            access_control_allow_credentials,
58            strict_transport_security,
59            retry_after,
60            request_uri,
61        }
62    }
63
64    #[builder(entry = "now", visibility = "pub")]
65    fn new_now(
66        content_length: Option<u64>,
67        content_type: Option<String>,
68        scheme: String,
69        host: String,
70        expires: Option<String>,
71        cache_control: Option<String>,
72        status_code: Option<u16>,
73        location: Option<String>,
74        access_control_allow_origin: Option<String>,
75        access_control_allow_credentials: Option<String>,
76        strict_transport_security: Option<String>,
77        retry_after: Option<String>,
78        request_uri: Option<String>,
79    ) -> Self {
80        Self {
81            content_length,
82            content_type,
83            scheme: Some(scheme),
84            host,
85            expires,
86            cache_control,
87            received: Utc::now(),
88            status_code: status_code.unwrap_or(200),
89            location,
90            access_control_allow_origin,
91            access_control_allow_credentials,
92            strict_transport_security,
93            retry_after,
94            request_uri,
95        }
96    }
97
98    #[builder(entry = "example", visibility = "pub")]
99    fn new_example(
100        content_length: Option<u64>,
101        content_type: Option<String>,
102        expires: Option<String>,
103        cache_control: Option<String>,
104        status_code: Option<u16>,
105        location: Option<String>,
106        access_control_allow_origin: Option<String>,
107        access_control_allow_credentials: Option<String>,
108        strict_transport_security: Option<String>,
109        retry_after: Option<String>,
110        request_uri: Option<String>,
111    ) -> Self {
112        Self {
113            content_length,
114            content_type,
115            scheme: Some("http".to_string()),
116            host: "example.com".to_string(),
117            expires,
118            cache_control,
119            received: Utc::now(),
120            status_code: status_code.unwrap_or(200),
121            location,
122            access_control_allow_origin,
123            access_control_allow_credentials,
124            strict_transport_security,
125            retry_after,
126            request_uri,
127        }
128    }
129
130    pub fn is_expired(&self, max_age: i64) -> bool {
131        let now = Utc::now();
132        if now >= self.received + Duration::seconds(max_age) {
133            return true;
134        }
135        if let Some(cache_control) = &self.cache_control {
136            let cc_max_age = cache_control
137                .split(',')
138                .map(|s| s.trim())
139                .find(|s| s.starts_with("max-age="));
140            if let Some(cc_max_age) = cc_max_age {
141                let cc_max_age = cc_max_age.trim_start_matches("max-age=").parse::<i64>();
142                if let Ok(cc_max_age) = cc_max_age {
143                    return now >= self.received + Duration::seconds(cc_max_age);
144                }
145            }
146        }
147        if let Some(expires) = &self.expires {
148            let expire_time = DateTime::parse_from_rfc2822(expires);
149            return if let Ok(expire_time) = expire_time {
150                now >= expire_time
151            } else {
152                false
153            };
154        }
155        false
156    }
157
158    pub fn should_cache(&self) -> bool {
159        if let Some(cache_control) = &self.cache_control {
160            return !cache_control
161                .split(',')
162                .map(|s| s.trim())
163                .any(|s| s.eq("no-store") || s.eq("no-cache"));
164        }
165        true
166    }
167
168    pub fn from_lines(lines: &[String]) -> Result<(Self, &[String]), serde_json::Error> {
169        let count = lines.iter().take_while(|s| !s.starts_with("---")).count();
170        let cache_data = lines
171            .iter()
172            .take(count)
173            .cloned()
174            .collect::<Vec<String>>()
175            .join("");
176        let cache_data = serde_json::from_str(&cache_data)?;
177        Ok((cache_data, &lines[count + 1..]))
178    }
179
180    pub fn to_lines(&self, data: &str) -> Result<String, serde_json::Error> {
181        let mut lines = serde_json::to_string(self)?;
182        lines.push_str("\n---\n");
183        lines.push_str(data);
184        Ok(lines)
185    }
186
187    pub fn content_length(&self) -> Option<u64> {
188        self.content_length
189    }
190
191    pub fn content_type(&self) -> Option<&str> {
192        self.content_type.as_deref()
193    }
194
195    pub fn scheme(&self) -> Option<&str> {
196        self.scheme.as_deref()
197    }
198
199    pub fn host(&self) -> &str {
200        &self.host
201    }
202
203    pub fn expires(&self) -> Option<&str> {
204        self.expires.as_deref()
205    }
206
207    pub fn cache_control(&self) -> Option<&str> {
208        self.cache_control.as_deref()
209    }
210
211    pub fn received(&self) -> &DateTime<Utc> {
212        &self.received
213    }
214
215    pub fn status_code(&self) -> u16 {
216        self.status_code
217    }
218
219    pub fn location(&self) -> Option<&str> {
220        self.location.as_deref()
221    }
222
223    pub fn access_control_allow_origin(&self) -> Option<&str> {
224        self.access_control_allow_origin.as_deref()
225    }
226
227    pub fn access_control_allow_credentials(&self) -> Option<&str> {
228        self.access_control_allow_credentials.as_deref()
229    }
230
231    pub fn strict_transport_security(&self) -> Option<&str> {
232        self.strict_transport_security.as_deref()
233    }
234
235    pub fn retry_after(&self) -> Option<&str> {
236        self.retry_after.as_deref()
237    }
238
239    pub fn request_uri(&self) -> Option<&str> {
240        self.request_uri.as_deref()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use {
247        super::HttpData,
248        chrono::{Duration, Utc},
249        rstest::rstest,
250    };
251
252    #[rstest]
253    #[case(HttpData::example().cache_control("max-age=0").build(), 100, true)]
254    #[case(HttpData::example().cache_control("max-age=100").build(), 0, true)]
255    #[case(HttpData::example().cache_control("max-age=100").build(), 50, false)]
256    #[case(HttpData::example().build(), 0, true)]
257    #[case(HttpData::example().build(), 100, false)]
258    #[case(HttpData::example().expires(Utc::now().to_rfc2822()).build(), 100, true)]
259    #[case(HttpData::example().expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, false)]
260    #[case(HttpData::example().expires((Utc::now() + Duration::seconds(100)).to_rfc2822()).build(), 50, false)]
261    #[case(HttpData::example().cache_control("max-age=100").expires(Utc::now().to_rfc2822()).build(), 100, false)]
262    #[case(HttpData::example().cache_control("max-age=0").expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, true)]
263    fn test_cache_data_and_max_age_is_expired(
264        #[case] cache_data: HttpData,
265        #[case] max_age: i64,
266        #[case] expected: bool,
267    ) {
268        // GIVEN in parameters
269
270        // WHEN
271        let actual = cache_data.is_expired(max_age);
272
273        // THEN
274        assert_eq!(actual, expected);
275    }
276
277    #[rstest]
278    #[case(HttpData::example().cache_control("no-cache").build(), false)]
279    #[case(HttpData::example().cache_control("no-store").build(), false)]
280    #[case(HttpData::example().cache_control("max-age=40").build(), true)]
281    fn test_cache_control(#[case] cache_data: HttpData, #[case] expected: bool) {
282        // GIVEN in parameters
283
284        // WHEN
285        let actual = cache_data.should_cache();
286
287        // THEN
288        assert_eq!(actual, expected);
289    }
290
291    #[test]
292    fn test_data_and_data_cache_to_lines() {
293        // GIVEN
294        let data = "foo";
295        let cache_data = HttpData::example().content_length(14).build();
296
297        // WHEN
298        let actual = cache_data.to_lines(data).unwrap();
299
300        // THEN
301        let expected = format!("{}\n---\nfoo", serde_json::to_string(&cache_data).unwrap());
302        assert_eq!(actual, expected);
303    }
304
305    #[test]
306    fn test_from_lines() {
307        // GIVEN
308        let data = "foo";
309        let cache_data = HttpData::example().content_length(14).build();
310        let lines = cache_data
311            .to_lines(data)
312            .unwrap()
313            .split('\n')
314            .map(|s| s.to_string())
315            .collect::<Vec<String>>();
316
317        // WHEN
318        let actual = HttpData::from_lines(&lines).expect("parsing lines");
319
320        // THEN
321        assert_eq!(cache_data, actual.0);
322        assert_eq!(vec![data], actual.1);
323    }
324}