cookie_store/
cookie.rs

1use crate::cookie_domain::CookieDomain;
2use crate::cookie_expiration::CookieExpiration;
3use crate::cookie_path::CookiePath;
4
5use crate::utils::{is_http_scheme, is_secure};
6use cookie::{Cookie as RawCookie, CookieBuilder as RawCookieBuilder, ParseError};
7#[cfg(feature = "serde")]
8use serde_derive::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::convert::TryFrom;
11use std::fmt;
12use std::ops::Deref;
13use time;
14use url::Url;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Error {
18    /// Cookie had attribute HttpOnly but was received from a request-uri which was not an http
19    /// scheme
20    NonHttpScheme,
21    /// Cookie did not specify domain but was received from non-relative-scheme request-uri from
22    /// which host could not be determined
23    NonRelativeScheme,
24    /// Cookie received from a request-uri that does not domain-match
25    DomainMismatch,
26    /// Cookie is Expired
27    Expired,
28    /// `cookie::Cookie` Parse error
29    Parse,
30    #[cfg(feature = "public_suffix")]
31    /// Cookie specified a public suffix domain-attribute that does not match the canonicalized
32    /// request-uri host
33    PublicSuffix,
34    /// Tried to use a CookieDomain variant of `Empty` or `NotPresent` in a context requiring a Domain value
35    UnspecifiedDomain,
36}
37
38impl std::error::Error for Error {}
39
40impl fmt::Display for Error {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(
43            f,
44            "{}",
45            match *self {
46                Error::NonHttpScheme =>
47                    "request-uri is not an http scheme but HttpOnly attribute set",
48                Error::NonRelativeScheme => {
49                    "request-uri is not a relative scheme; cannot determine host"
50                }
51                Error::DomainMismatch => "request-uri does not domain-match the cookie",
52                Error::Expired => "attempted to utilize an Expired Cookie",
53                Error::Parse => "unable to parse string as cookie::Cookie",
54                #[cfg(feature = "public_suffix")]
55                Error::PublicSuffix => "domain-attribute value is a public suffix",
56                Error::UnspecifiedDomain => "domain-attribute is not specified",
57            }
58        )
59    }
60}
61
62// cookie::Cookie::parse returns Result<Cookie, ()>
63impl From<ParseError> for Error {
64    fn from(_: ParseError) -> Error {
65        Error::Parse
66    }
67}
68
69pub type CookieResult<'a> = Result<Cookie<'a>, Error>;
70
71/// A cookie conforming more closely to [IETF RFC6265](https://datatracker.ietf.org/doc/html/rfc6265)
72#[derive(PartialEq, Clone, Debug)]
73#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
74pub struct Cookie<'a> {
75    /// The parsed Set-Cookie data
76    #[cfg_attr(feature = "serde", serde(serialize_with = "serde_raw_cookie::serialize"))]
77    #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_raw_cookie::deserialize"))]
78    raw_cookie: RawCookie<'a>,
79    /// The Path attribute from a Set-Cookie header or the default-path as
80    /// determined from
81    /// the request-uri
82    pub path: CookiePath,
83    /// The Domain attribute from a Set-Cookie header, or a HostOnly variant if no
84    /// non-empty Domain attribute
85    /// found
86    pub domain: CookieDomain,
87    /// For a persistent Cookie (see [IETF RFC6265 Section
88    /// 5.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3)),
89    /// the expiration time as defined by the Max-Age or Expires attribute,
90    /// otherwise SessionEnd,
91    /// indicating a non-persistent `Cookie` that should expire at the end of the
92    /// session
93    pub expires: CookieExpiration,
94}
95
96#[cfg(feature = "serde")]
97mod serde_raw_cookie {
98    use cookie::Cookie as RawCookie;
99    use serde::de::Error;
100    use serde::de::Unexpected;
101    use serde::{Deserialize, Deserializer, Serialize, Serializer};
102    use std::str::FromStr;
103
104    pub fn serialize<S>(cookie: &RawCookie<'_>, serializer: S) -> Result<S::Ok, S::Error>
105    where
106        S: Serializer,
107    {
108        cookie.to_string().serialize(serializer)
109    }
110
111    pub fn deserialize<'a, D>(deserializer: D) -> Result<RawCookie<'static>, D::Error>
112    where
113        D: Deserializer<'a>,
114    {
115        let cookie = String::deserialize(deserializer)?;
116        match RawCookie::from_str(&cookie) {
117            Ok(cookie) => Ok(cookie),
118            Err(_) => Err(D::Error::invalid_value(
119                Unexpected::Str(&cookie),
120                &"a cookie string",
121            )),
122        }
123    }
124}
125
126impl<'a> Cookie<'a> {
127    /// Whether this `Cookie` should be included for `request_url`
128    pub fn matches(&self, request_url: &Url) -> bool {
129        self.path.matches(request_url)
130            && self.domain.matches(request_url)
131            && (!self.raw_cookie.secure().unwrap_or(false) || is_secure(request_url))
132            && (!self.raw_cookie.http_only().unwrap_or(false) || is_http_scheme(request_url))
133    }
134
135    /// Should this `Cookie` be persisted across sessions?
136    pub fn is_persistent(&self) -> bool {
137        match self.expires {
138            CookieExpiration::AtUtc(_) => true,
139            CookieExpiration::SessionEnd => false,
140        }
141    }
142
143    /// Expire this cookie
144    pub fn expire(&mut self) {
145        self.expires = CookieExpiration::from(0u64);
146    }
147
148    /// Return whether the `Cookie` is expired *now*
149    pub fn is_expired(&self) -> bool {
150        self.expires.is_expired()
151    }
152
153    /// Indicates if the `Cookie` expires as of `utc_tm`.
154    pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool {
155        self.expires.expires_by(utc_tm)
156    }
157
158    /// Parses a new `cookie_store::Cookie` from `cookie_str`.
159    pub fn parse<S>(cookie_str: S, request_url: &Url) -> CookieResult<'a>
160    where
161        S: Into<Cow<'a, str>>,
162    {
163        Cookie::try_from_raw_cookie(&RawCookie::parse(cookie_str)?, request_url)
164    }
165
166    /// Create a new `cookie_store::Cookie` from a `cookie::Cookie` (from the `cookie` crate)
167    /// received from `request_url`.
168    pub fn try_from_raw_cookie(raw_cookie: &RawCookie<'a>, request_url: &Url) -> CookieResult<'a> {
169        if raw_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) {
170            // If the cookie was received from a "non-HTTP" API and the
171            // cookie's http-only-flag is set, abort these steps and ignore the
172            // cookie entirely.
173            return Err(Error::NonHttpScheme);
174        }
175
176        let domain = match CookieDomain::try_from(raw_cookie) {
177            // 6.   If the domain-attribute is non-empty:
178            Ok(d @ CookieDomain::Suffix(_)) => {
179                if !d.matches(request_url) {
180                    //    If the canonicalized request-host does not domain-match the
181                    //    domain-attribute:
182                    //       Ignore the cookie entirely and abort these steps.
183                    Err(Error::DomainMismatch)
184                } else {
185                    //    Otherwise:
186                    //       Set the cookie's host-only-flag to false.
187                    //       Set the cookie's domain to the domain-attribute.
188                    Ok(d)
189                }
190            }
191            Err(_) => Err(Error::Parse),
192            // Otherwise:
193            //    Set the cookie's host-only-flag to true.
194            //    Set the cookie's domain to the canonicalized request-host.
195            _ => CookieDomain::host_only(request_url),
196        }?;
197
198        let path = raw_cookie
199            .path()
200            .as_ref()
201            .and_then(|p| CookiePath::parse(p))
202            .unwrap_or_else(|| CookiePath::default_path(request_url));
203
204        // per RFC6265, Max-Age takes precedence, then Expires, otherwise is Session
205        // only
206        let expires = if let Some(max_age) = raw_cookie.max_age() {
207            CookieExpiration::from(max_age)
208        } else if let Some(expiration) = raw_cookie.expires() {
209            CookieExpiration::from(expiration)
210        } else {
211            CookieExpiration::SessionEnd
212        };
213
214        Ok(Cookie {
215            raw_cookie: raw_cookie.clone(),
216            path,
217            expires,
218            domain,
219        })
220    }
221
222    pub fn into_owned(self) -> Cookie<'static> {
223        Cookie {
224            raw_cookie: self.raw_cookie.into_owned(),
225            path: self.path,
226            domain: self.domain,
227            expires: self.expires,
228        }
229    }
230}
231
232impl<'a> Deref for Cookie<'a> {
233    type Target = RawCookie<'a>;
234    fn deref(&self) -> &Self::Target {
235        &self.raw_cookie
236    }
237}
238
239impl<'a> From<Cookie<'a>> for RawCookie<'a> {
240    fn from(cookie: Cookie<'a>) -> RawCookie<'static> {
241        let mut builder =
242            RawCookieBuilder::new(cookie.name().to_owned(), cookie.value().to_owned());
243
244        // Max-Age is relative, will not have same meaning now, so only set `Expires`.
245        match cookie.expires {
246            CookieExpiration::AtUtc(utc_tm) => {
247                builder = builder.expires(utc_tm);
248            }
249            CookieExpiration::SessionEnd => {}
250        }
251
252        if cookie.path.is_from_path_attr() {
253            builder = builder.path(String::from(cookie.path));
254        }
255
256        if let CookieDomain::Suffix(s) = cookie.domain {
257            builder = builder.domain(s);
258        }
259
260        builder.build()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::Cookie;
267    use crate::cookie_domain::CookieDomain;
268    use crate::cookie_expiration::CookieExpiration;
269    use cookie::Cookie as RawCookie;
270    use time::{Duration, OffsetDateTime};
271    use url::Url;
272
273    use crate::utils::test as test_utils;
274
275    fn cmp_domain(cookie: &str, url: &str, exp: CookieDomain) {
276        let ua = test_utils::make_cookie(cookie, url, None, None);
277        assert!(ua.domain == exp, "\n{:?}", ua);
278    }
279
280    #[test]
281    fn no_domain() {
282        let url = test_utils::url("http://example.com/foo/bar");
283        cmp_domain(
284            "cookie1=value1",
285            "http://example.com/foo/bar",
286            CookieDomain::host_only(&url).expect("unable to parse domain"),
287        );
288    }
289
290    // per RFC6265:
291    // If the attribute-value is empty, the behavior is undefined.  However,
292    //   the user agent SHOULD ignore the cookie-av entirely.
293    #[test]
294    fn empty_domain() {
295        let url = test_utils::url("http://example.com/foo/bar");
296        cmp_domain(
297            "cookie1=value1; Domain=",
298            "http://example.com/foo/bar",
299            CookieDomain::host_only(&url).expect("unable to parse domain"),
300        );
301    }
302
303    #[test]
304    fn mismatched_domain() {
305        let ua = Cookie::parse(
306            "cookie1=value1; Domain=notmydomain.com",
307            &test_utils::url("http://example.com/foo/bar"),
308        );
309        assert!(ua.is_err(), "{:?}", ua);
310    }
311
312    #[test]
313    fn domains() {
314        fn domain_from(domain: &str, request_url: &str, is_some: bool) {
315            let cookie_str = format!("cookie1=value1; Domain={}", domain);
316            let raw_cookie = RawCookie::parse(cookie_str).unwrap();
317            let cookie = Cookie::try_from_raw_cookie(&raw_cookie, &test_utils::url(request_url));
318            assert_eq!(is_some, cookie.is_ok())
319        }
320        //        The user agent will reject cookies unless the Domain attribute
321        // specifies a scope for the cookie that would include the origin
322        // server.  For example, the user agent will accept a cookie with a
323        // Domain attribute of "example.com" or of "foo.example.com" from
324        // foo.example.com, but the user agent will not accept a cookie with a
325        // Domain attribute of "bar.example.com" or of "baz.foo.example.com".
326        domain_from("example.com", "http://foo.example.com", true);
327        domain_from(".example.com", "http://foo.example.com", true);
328        domain_from("foo.example.com", "http://foo.example.com", true);
329        domain_from(".foo.example.com", "http://foo.example.com", true);
330
331        domain_from("oo.example.com", "http://foo.example.com", false);
332        domain_from("myexample.com", "http://foo.example.com", false);
333        domain_from("bar.example.com", "http://foo.example.com", false);
334        domain_from("baz.foo.example.com", "http://foo.example.com", false);
335    }
336
337    #[test]
338    fn httponly() {
339        let c = RawCookie::parse("cookie1=value1; HttpOnly").unwrap();
340        let url = Url::parse("ftp://example.com/foo/bar").unwrap();
341        let ua = Cookie::try_from_raw_cookie(&c, &url);
342        assert!(ua.is_err(), "{:?}", ua);
343    }
344
345    #[test]
346    fn identical_domain() {
347        cmp_domain(
348            "cookie1=value1; Domain=example.com",
349            "http://example.com/foo/bar",
350            CookieDomain::Suffix(String::from("example.com")),
351        );
352    }
353
354    #[test]
355    fn identical_domain_leading_dot() {
356        cmp_domain(
357            "cookie1=value1; Domain=.example.com",
358            "http://example.com/foo/bar",
359            CookieDomain::Suffix(String::from("example.com")),
360        );
361    }
362
363    #[test]
364    fn identical_domain_two_leading_dots() {
365        cmp_domain(
366            "cookie1=value1; Domain=..example.com",
367            "http://..example.com/foo/bar",
368            CookieDomain::Suffix(String::from(".example.com")),
369        );
370    }
371
372    #[test]
373    fn upper_case_domain() {
374        cmp_domain(
375            "cookie1=value1; Domain=EXAMPLE.com",
376            "http://example.com/foo/bar",
377            CookieDomain::Suffix(String::from("example.com")),
378        );
379    }
380
381    fn cmp_path(cookie: &str, url: &str, exp: &str) {
382        let ua = test_utils::make_cookie(cookie, url, None, None);
383        assert!(String::from(ua.path.clone()) == exp, "\n{:?}", ua);
384    }
385
386    #[test]
387    fn no_path() {
388        // no Path specified
389        cmp_path("cookie1=value1", "http://example.com/foo/bar/", "/foo/bar");
390        cmp_path("cookie1=value1", "http://example.com/foo/bar", "/foo");
391        cmp_path("cookie1=value1", "http://example.com/foo", "/");
392        cmp_path("cookie1=value1", "http://example.com/", "/");
393        cmp_path("cookie1=value1", "http://example.com", "/");
394    }
395
396    #[test]
397    fn empty_path() {
398        // Path specified with empty value
399        cmp_path(
400            "cookie1=value1; Path=",
401            "http://example.com/foo/bar/",
402            "/foo/bar",
403        );
404        cmp_path(
405            "cookie1=value1; Path=",
406            "http://example.com/foo/bar",
407            "/foo",
408        );
409        cmp_path("cookie1=value1; Path=", "http://example.com/foo", "/");
410        cmp_path("cookie1=value1; Path=", "http://example.com/", "/");
411        cmp_path("cookie1=value1; Path=", "http://example.com", "/");
412    }
413
414    #[test]
415    fn invalid_path() {
416        // Invalid Path specified (first character not /)
417        cmp_path(
418            "cookie1=value1; Path=baz",
419            "http://example.com/foo/bar/",
420            "/foo/bar",
421        );
422        cmp_path(
423            "cookie1=value1; Path=baz",
424            "http://example.com/foo/bar",
425            "/foo",
426        );
427        cmp_path("cookie1=value1; Path=baz", "http://example.com/foo", "/");
428        cmp_path("cookie1=value1; Path=baz", "http://example.com/", "/");
429        cmp_path("cookie1=value1; Path=baz", "http://example.com", "/");
430    }
431
432    #[test]
433    fn path() {
434        // Path specified, single /
435        cmp_path(
436            "cookie1=value1; Path=/baz",
437            "http://example.com/foo/bar/",
438            "/baz",
439        );
440        // Path specified, multiple / (for valid attribute-value on path, take full
441        // string)
442        cmp_path(
443            "cookie1=value1; Path=/baz/",
444            "http://example.com/foo/bar/",
445            "/baz/",
446        );
447    }
448
449    // expiry-related tests
450    #[inline]
451    fn in_days(days: i64) -> OffsetDateTime {
452        OffsetDateTime::now_utc() + Duration::days(days)
453    }
454    #[inline]
455    fn in_minutes(mins: i64) -> OffsetDateTime {
456        OffsetDateTime::now_utc() + Duration::minutes(mins)
457    }
458
459    #[test]
460    fn max_age_bounds() {
461        let ua = test_utils::make_cookie(
462            "cookie1=value1",
463            "http://example.com/foo/bar",
464            None,
465            Some(9223372036854776),
466        );
467        assert!(match ua.expires {
468            CookieExpiration::AtUtc(_) => true,
469            _ => false,
470        });
471    }
472
473    #[test]
474    fn max_age() {
475        let ua = test_utils::make_cookie(
476            "cookie1=value1",
477            "http://example.com/foo/bar",
478            None,
479            Some(60),
480        );
481        assert!(!ua.is_expired());
482        assert!(ua.expires_by(&in_minutes(2)));
483    }
484
485    #[test]
486    fn expired() {
487        let ua = test_utils::make_cookie(
488            "cookie1=value1",
489            "http://example.com/foo/bar",
490            None,
491            Some(0u64),
492        );
493        assert!(ua.is_expired());
494        assert!(ua.expires_by(&in_days(-1)));
495        let ua = test_utils::make_cookie(
496            "cookie1=value1; Max-Age=0",
497            "http://example.com/foo/bar",
498            None,
499            None,
500        );
501        assert!(ua.is_expired());
502        assert!(ua.expires_by(&in_days(-1)));
503        let ua = test_utils::make_cookie(
504            "cookie1=value1; Max-Age=-1",
505            "http://example.com/foo/bar",
506            None,
507            None,
508        );
509        assert!(ua.is_expired());
510        assert!(ua.expires_by(&in_days(-1)));
511    }
512
513    #[test]
514    fn session_end() {
515        let ua =
516            test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None);
517        assert!(match ua.expires {
518            CookieExpiration::SessionEnd => true,
519            _ => false,
520        });
521        assert!(!ua.is_expired());
522        assert!(!ua.expires_by(&in_days(1)));
523        assert!(!ua.expires_by(&in_days(-1)));
524    }
525
526    #[test]
527    fn expires_tmrw_at_utc() {
528        let ua = test_utils::make_cookie(
529            "cookie1=value1",
530            "http://example.com/foo/bar",
531            Some(in_days(1)),
532            None,
533        );
534        assert!(!ua.is_expired());
535        assert!(ua.expires_by(&in_days(2)));
536    }
537
538    #[test]
539    fn expired_yest_at_utc() {
540        let ua = test_utils::make_cookie(
541            "cookie1=value1",
542            "http://example.com/foo/bar",
543            Some(in_days(-1)),
544            None,
545        );
546        assert!(ua.is_expired());
547        assert!(!ua.expires_by(&in_days(-2)));
548    }
549
550    #[test]
551    fn is_persistent() {
552        let ua =
553            test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None);
554        assert!(!ua.is_persistent()); // SessionEnd
555        let ua = test_utils::make_cookie(
556            "cookie1=value1",
557            "http://example.com/foo/bar",
558            Some(in_days(1)),
559            None,
560        );
561        assert!(ua.is_persistent()); // AtUtc from Expires
562        let ua = test_utils::make_cookie(
563            "cookie1=value1",
564            "http://example.com/foo/bar",
565            Some(in_days(1)),
566            Some(60),
567        );
568        assert!(ua.is_persistent()); // AtUtc from Max-Age
569    }
570
571    #[test]
572    fn max_age_overrides_expires() {
573        // Expires indicates expiration yesterday, but Max-Age indicates expiry in 1
574        // minute
575        let ua = test_utils::make_cookie(
576            "cookie1=value1",
577            "http://example.com/foo/bar",
578            Some(in_days(-1)),
579            Some(60),
580        );
581        assert!(!ua.is_expired());
582        assert!(ua.expires_by(&in_minutes(2)));
583    }
584
585    // A request-path path-matches a given cookie-path if at least one of
586    // the following conditions holds:
587    // o  The cookie-path and the request-path are identical.
588    // o  The cookie-path is a prefix of the request-path, and the last
589    //    character of the cookie-path is %x2F ("/").
590    // o  The cookie-path is a prefix of the request-path, and the first
591    //    character of the request-path that is not included in the cookie-
592    //    path is a %x2F ("/") character.
593    #[test]
594    fn matches() {
595        fn do_match(exp: bool, cookie: &str, src_url: &str, request_url: Option<&str>) {
596            let ua = test_utils::make_cookie(cookie, src_url, None, None);
597            let request_url = request_url.unwrap_or(src_url);
598            assert!(
599                exp == ua.matches(&Url::parse(request_url).unwrap()),
600                "\n>> {:?}\nshould{}match\n>> {:?}\n",
601                ua,
602                if exp { " " } else { " NOT " },
603                request_url
604            );
605        }
606        fn is_match(cookie: &str, url: &str, request_url: Option<&str>) {
607            do_match(true, cookie, url, request_url);
608        }
609        fn is_mismatch(cookie: &str, url: &str, request_url: Option<&str>) {
610            do_match(false, cookie, url, request_url);
611        }
612
613        // match: request-path & cookie-path (defaulted from request-uri) identical
614        is_match("cookie1=value1", "http://example.com/foo/bar", None);
615        // mismatch: request-path & cookie-path do not match
616        is_mismatch(
617            "cookie1=value1",
618            "http://example.com/bus/baz/",
619            Some("http://example.com/foo/bar"),
620        );
621        is_mismatch(
622            "cookie1=value1; Path=/bus/baz",
623            "http://example.com/foo/bar",
624            None,
625        );
626        // match: cookie-path a prefix of request-path and last character of
627        // cookie-path is /
628        is_match(
629            "cookie1=value1",
630            "http://example.com/foo/bar",
631            Some("http://example.com/foo/bar"),
632        );
633        is_match(
634            "cookie1=value1; Path=/foo/",
635            "http://example.com/foo/bar",
636            None,
637        );
638        // mismatch: cookie-path a prefix of request-path but last character of
639        // cookie-path is not /
640        // and first character of request-path not included in cookie-path is not /
641        is_mismatch(
642            "cookie1=value1",
643            "http://example.com/fo/",
644            Some("http://example.com/foo/bar"),
645        );
646        is_mismatch(
647            "cookie1=value1; Path=/fo",
648            "http://example.com/foo/bar",
649            None,
650        );
651        // match: cookie-path a prefix of request-path and first character of
652        // request-path
653        // not included in the cookie-path is /
654        is_match(
655            "cookie1=value1",
656            "http://example.com/foo/",
657            Some("http://example.com/foo/bar"),
658        );
659        is_match(
660            "cookie1=value1; Path=/foo",
661            "http://example.com/foo/bar",
662            None,
663        );
664        // match: Path overridden to /, which matches all paths from the domain
665        is_match(
666            "cookie1=value1; Path=/",
667            "http://example.com/foo/bar",
668            Some("http://example.com/bus/baz"),
669        );
670        // mismatch: different domain
671        is_mismatch(
672            "cookie1=value1",
673            "http://example.com/foo/",
674            Some("http://notmydomain.com/foo/bar"),
675        );
676        is_mismatch(
677            "cookie1=value1; Domain=example.com",
678            "http://foo.example.com/foo/",
679            Some("http://notmydomain.com/foo/bar"),
680        );
681        // match: secure protocol
682        is_match(
683            "cookie1=value1; Secure",
684            "http://example.com/foo/bar",
685            Some("https://example.com/foo/bar"),
686        );
687        // mismatch: non-secure protocol
688        is_mismatch(
689            "cookie1=value1; Secure",
690            "http://example.com/foo/bar",
691            Some("http://example.com/foo/bar"),
692        );
693        // match: no http restriction
694        is_match(
695            "cookie1=value1",
696            "http://example.com/foo/bar",
697            Some("ftp://example.com/foo/bar"),
698        );
699        // match: http protocol
700        is_match(
701            "cookie1=value1; HttpOnly",
702            "http://example.com/foo/bar",
703            Some("http://example.com/foo/bar"),
704        );
705        is_match(
706            "cookie1=value1; HttpOnly",
707            "http://example.com/foo/bar",
708            Some("HTTP://example.com/foo/bar"),
709        );
710        is_match(
711            "cookie1=value1; HttpOnly",
712            "http://example.com/foo/bar",
713            Some("https://example.com/foo/bar"),
714        );
715        // mismatch: http requried
716        is_mismatch(
717            "cookie1=value1; HttpOnly",
718            "http://example.com/foo/bar",
719            Some("ftp://example.com/foo/bar"),
720        );
721        is_mismatch(
722            "cookie1=value1; HttpOnly",
723            "http://example.com/foo/bar",
724            Some("data:nonrelativescheme"),
725        );
726    }
727}
728
729#[cfg(all(test, feature = "serde_json"))]
730mod serde_json_tests {
731    use crate::cookie::Cookie;
732    use crate::cookie_expiration::CookieExpiration;
733    use crate::utils::test as test_utils;
734    use crate::utils::test::*;
735    use serde_json::json;
736    use time;
737
738    fn encode_decode(c: &Cookie<'_>, expected: serde_json::Value) {
739        let encoded = serde_json::to_value(c).unwrap();
740        assert_eq!(
741            expected,
742            encoded,
743            "\nexpected: '{}'\n encoded: '{}'",
744            expected.to_string(),
745            encoded.to_string()
746        );
747        let decoded: Cookie<'_> = serde_json::from_value(encoded).unwrap();
748        assert_eq!(
749            *c,
750            decoded,
751            "\nexpected: '{}'\n decoded: '{}'",
752            c.to_string(),
753            decoded.to_string()
754        );
755    }
756
757    #[test]
758    fn serde() {
759        encode_decode(
760            &test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None),
761            json!({
762                "raw_cookie": "cookie1=value1",
763                "path": ["/foo", false],
764                "domain": { "HostOnly": "example.com" },
765                "expires": "SessionEnd"
766            }),
767        );
768
769        encode_decode(
770            &test_utils::make_cookie(
771                "cookie2=value2; Domain=example.com",
772                "http://foo.example.com/foo/bar",
773                None,
774                None,
775            ),
776            json!({
777                "raw_cookie": "cookie2=value2; Domain=example.com",
778                "path": ["/foo", false],
779                "domain": { "Suffix": "example.com" },
780                "expires": "SessionEnd"
781            }),
782        );
783
784        encode_decode(
785            &test_utils::make_cookie(
786                "cookie3=value3; Path=/foo/bar",
787                "http://foo.example.com/foo",
788                None,
789                None,
790            ),
791            json!({
792                "raw_cookie": "cookie3=value3; Path=/foo/bar",
793                "path": ["/foo/bar", true],
794                "domain": { "HostOnly": "foo.example.com" },
795                "expires": "SessionEnd",
796            }),
797        );
798
799        let at_utc = time::macros::date!(2015 - 08 - 11)
800            .with_time(time::macros::time!(16:41:42))
801            .assume_utc();
802        encode_decode(
803            &test_utils::make_cookie(
804                "cookie4=value4",
805                "http://example.com/foo/bar",
806                Some(at_utc),
807                None,
808            ),
809            json!({
810                "raw_cookie": "cookie4=value4; Expires=Tue, 11 Aug 2015 16:41:42 GMT",
811                "path": ["/foo", false],
812                "domain": { "HostOnly": "example.com" },
813                "expires": { "AtUtc": at_utc.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
814            }),
815        );
816
817        let expires = test_utils::make_cookie(
818            "cookie5=value5",
819            "http://example.com/foo/bar",
820            Some(in_minutes(10)),
821            None,
822        );
823        let utc_tm = match expires.expires {
824            CookieExpiration::AtUtc(ref utc_tm) => utc_tm,
825            CookieExpiration::SessionEnd => unreachable!(),
826        };
827
828        let utc_formatted = utc_tm
829            .format(&time::format_description::well_known::Rfc2822)
830            .unwrap()
831            .to_string()
832            .replace("+0000", "GMT");
833        let raw_cookie_value = format!("cookie5=value5; Expires={utc_formatted}");
834
835        encode_decode(
836            &expires,
837            json!({
838                "raw_cookie": raw_cookie_value,
839                "path":["/foo", false],
840                "domain": { "HostOnly": "example.com" },
841                "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
842            }),
843        );
844        dbg!(&at_utc);
845        let max_age = test_utils::make_cookie(
846            "cookie6=value6",
847            "http://example.com/foo/bar",
848            Some(at_utc),
849            Some(10),
850        );
851        dbg!(&max_age);
852        let utc_tm = match max_age.expires {
853            CookieExpiration::AtUtc(ref utc_tm) => time::OffsetDateTime::parse(
854                &utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap(),
855                &time::format_description::well_known::Rfc3339,
856            )
857            .expect("could not re-parse time"),
858            CookieExpiration::SessionEnd => unreachable!(),
859        };
860        dbg!(&utc_tm);
861        encode_decode(
862            &max_age,
863            json!({
864                "raw_cookie": "cookie6=value6; Max-Age=10; Expires=Tue, 11 Aug 2015 16:41:42 GMT",
865                "path":["/foo", false],
866                "domain": { "HostOnly": "example.com" },
867                "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
868            }),
869        );
870
871        let max_age = test_utils::make_cookie(
872            "cookie7=value7",
873            "http://example.com/foo/bar",
874            None,
875            Some(10),
876        );
877        let utc_tm = match max_age.expires {
878            CookieExpiration::AtUtc(ref utc_tm) => utc_tm,
879            CookieExpiration::SessionEnd => unreachable!(),
880        };
881        encode_decode(
882            &max_age,
883            json!({
884                "raw_cookie": "cookie7=value7; Max-Age=10",
885                "path":["/foo", false],
886                "domain": { "HostOnly": "example.com" },
887                "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
888            }),
889        );
890    }
891}