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