Skip to main content

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