Skip to main content

net/
cookie.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//! Implementation of cookie creation and matching as specified by
6//! <http://tools.ietf.org/html/rfc6265>
7
8use std::borrow::ToOwned;
9use std::net::{Ipv4Addr, Ipv6Addr};
10use std::time::SystemTime;
11
12use cookie::Cookie;
13use log::{Level, debug, log_enabled};
14use malloc_size_of_derive::MallocSizeOf;
15use net_traits::CookieSource;
16use net_traits::pub_domains::is_pub_domain;
17use nom::branch::alt;
18use nom::bytes::complete::{tag, tag_no_case, take, take_while_m_n};
19use nom::combinator::{opt, recognize};
20use nom::multi::{many0, many1, separated_list1};
21use nom::sequence::{delimited, preceded, terminated};
22use nom::{IResult, Parser};
23use serde::{Deserialize, Serialize};
24use servo_url::ServoUrl;
25use time::{Date, Duration, Month, OffsetDateTime, Time};
26
27/// A stored cookie that wraps the definition in cookie-rs. This is used to implement
28/// various behaviours defined in the spec that rely on an associated request URL,
29/// which cookie-rs and hyper's header parsing do not support.
30#[derive(Clone, Debug, Deserialize, Serialize, MallocSizeOf)]
31pub struct ServoCookie {
32    #[serde(
33        deserialize_with = "hyper_serde::deserialize",
34        serialize_with = "hyper_serde::serialize"
35    )]
36    pub cookie: Cookie<'static>,
37    pub host_only: bool,
38    pub persistent: bool,
39    pub creation_time: SystemTime,
40    pub last_access: SystemTime,
41    pub expiry_time: Option<SystemTime>,
42}
43
44impl ServoCookie {
45    pub fn from_cookie_string(
46        cookie_str: &str,
47        request: &ServoUrl,
48        source: CookieSource,
49    ) -> Option<ServoCookie> {
50        let mut cookie = Cookie::parse(cookie_str.to_owned()).ok()?;
51
52        // Cookie::parse uses RFC 2616 <http://tools.ietf.org/html/rfc2616#section-3.3.1> to parse
53        // cookie expiry date. If it fails to parse the expiry date, try to parse again with
54        // less strict algorithm from RFC6265.
55        // TODO: We can remove this code and the ServoCookie::parse_date function if cookie-rs
56        // library fixes this upstream.
57        if cookie.expires_datetime().is_none() {
58            let expiry_date_str = cookie_str
59                .split(';')
60                .filter_map(|key_value| {
61                    key_value
62                        .find('=')
63                        .map(|i| (key_value[..i].trim(), key_value[(i + 1)..].trim()))
64                })
65                .find_map(|(key, value)| key.eq_ignore_ascii_case("expires").then_some(value));
66            if let Some(date_str) = expiry_date_str {
67                cookie.set_expires(Self::parse_date(date_str));
68            }
69        }
70
71        ServoCookie::new_wrapped(cookie, request, source)
72    }
73
74    /// Steps 6-22 from <https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#name-storage-model>
75    pub fn new_wrapped(
76        mut cookie: Cookie<'static>,
77        request: &ServoUrl,
78        source: CookieSource,
79    ) -> Option<ServoCookie> {
80        let persistent;
81        let expiry_time;
82
83        // Step 6. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age":
84        if let Some(max_age) = cookie.max_age() {
85            // 1. Set the cookie's persistent-flag to true.
86            persistent = true;
87
88            // The user agent MUST limit the maximum value of the Max-Age attribute.
89            // The limit SHOULD NOT be greater than 400 days (34560000 seconds) in the future.
90            let clamped_max_age = max_age.min(Duration::seconds(34_560_000));
91
92            // 2. Set the cookie's expiry-time to attribute-value of the last
93            // attribute in the cookie-attribute-list with an attribute-name of "Max-Age".
94            expiry_time = Some(SystemTime::now() + clamped_max_age);
95            cookie.set_max_age(clamped_max_age);
96            // cookie-rs doesn't seem to mirror the max-age value to expiry and vice versa so we do explicitly
97            cookie.set_expires(Some(OffsetDateTime::now_utc() + clamped_max_age));
98        }
99        // Otherwise, if the cookie-attribute-list contains an attribute with an attribute-name of "Expires":
100        else if let Some(date_time) = cookie.expires_datetime() {
101            // 1. Set the cookie's persistent-flag to true.
102            persistent = true;
103
104            // The user agent MUST limit the maximum value of the Expires attribute.
105            // The limit SHOULD NOT be greater than 400 days (34560000 seconds) in the future.
106            let clamped_date_time =
107                date_time.min(OffsetDateTime::now_utc() + Duration::seconds(34_560_000));
108
109            // 2. Set the cookie's expiry-time to attribute-value of the last attribute in the
110            // cookie-attribute-list with an attribute-name of "Expires".
111            expiry_time = Some(clamped_date_time.into());
112            cookie.set_expires(Some(clamped_date_time));
113            // cookie-rs doesn't seem to mirror the max-age value to expiry and vice versa so we do explicitly
114            cookie.set_max_age(Some(clamped_date_time - OffsetDateTime::now_utc()));
115        }
116        //  Otherwise:
117        else {
118            // 1. Set the cookie's persistent-flag to false.
119            persistent = false;
120
121            // 2. Set the cookie's expiry-time to the latest representable date.
122            expiry_time = None;
123        }
124
125        let url_host = request.host_str().unwrap_or("").to_owned();
126
127        // Step 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
128        let mut domain = if let Some(domain) = cookie.domain() {
129            // 1. Let the domain-attribute be the attribute-value of the last attribute in the
130            // cookie-attribute-list [..]
131            // NOTE: This is done by the cookie crate
132            domain.to_owned()
133        }
134        // Otherwise:
135        else {
136            // 1. Let the domain-attribute be the empty string.
137            String::new()
138        };
139
140        // TODO Step 8. If the domain-attribute contains a character that is not in the range of [USASCII] characters,
141        // abort these steps and ignore the cookie entirely.
142        // NOTE: (is this done by the cookies crate?)
143
144        // Step 9. If the user agent is configured to reject "public suffixes" and the domain-attribute
145        // is a public suffix:
146        if is_pub_domain(&domain) {
147            // 1. If the domain-attribute is identical to the canonicalized request-host:
148            if domain == url_host {
149                // 1. Let the domain-attribute be the empty string.
150                domain = String::new();
151            }
152            //  Otherwise:
153            else {
154                // 1.Abort these steps and ignore the cookie entirely.
155                return None;
156            }
157        }
158
159        // Step 10. If the domain-attribute is non-empty:
160        let host_only;
161        if !domain.is_empty() {
162            // 1. If the canonicalized request-host does not domain-match the domain-attribute:
163            if !ServoCookie::domain_match(&url_host, &domain) {
164                // 1. Abort these steps and ignore the cookie entirely.
165                return None;
166            } else {
167                // 1. Set the cookie's host-only-flag to false.
168                host_only = false;
169
170                // 2. Set the cookie's domain to the domain-attribute.
171                cookie.set_domain(domain);
172            }
173        }
174        // Otherwise:
175        else {
176            // 1. Set the cookie's host-only-flag to true.
177            host_only = true;
178
179            // 2. Set the cookie's domain to the canonicalized request-host.
180            cookie.set_domain(url_host);
181        };
182
183        // Step 11. If the cookie-attribute-list contains an attribute with an attribute-name of "Path",
184        // set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list
185        // with both an attribute-name of "Path" and an attribute-value whose length is no more than 1024 octets.
186        // Otherwise, set the cookie's path to the default-path of the request-uri.
187        let mut has_path_specified = true;
188        let mut path = cookie
189            .path()
190            .unwrap_or_else(|| {
191                has_path_specified = false;
192                ""
193            })
194            .to_owned();
195        // TODO: Why do we do this?
196        if !path.starts_with('/') {
197            path = ServoCookie::default_path(request.path()).to_string();
198        }
199        cookie.set_path(path);
200
201        // Step 12. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure",
202        // set the cookie's secure-only-flag to true. Otherwise, set the cookie's secure-only-flag to false.
203        let secure_only = cookie.secure().unwrap_or(false);
204
205        // Step 13. If the request-uri does not denote a "secure" connection (as defined by the user agent),
206        // and the cookie's secure-only-flag is true, then abort these steps and ignore the cookie entirely.
207        if secure_only && !request.is_secure_scheme() {
208            return None;
209        }
210
211        // Step 14. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly",
212        // set the cookie's http-only-flag to true. Otherwise, set the cookie's http-only-flag to false.
213        let http_only = cookie.http_only().unwrap_or(false);
214
215        // Step 15. If the cookie was received from a "non-HTTP" API and the cookie's
216        // http-only-flag is true, abort these steps and ignore the cookie entirely.
217        if http_only && source == CookieSource::NonHTTP {
218            return None;
219        }
220
221        // TODO: Step 16, Ignore cookies from insecure request uris based on existing cookies
222
223        // TODO: Steps 17-19, same-site-flag
224
225        // Step 20. If the cookie-name begins with a case-insensitive match for the string "__Secure-",
226        // abort these steps and ignore the cookie entirely unless the cookie's secure-only-flag is true.
227        let has_case_insensitive_prefix = |value: &str, prefix: &str| {
228            value
229                .get(..prefix.len())
230                .is_some_and(|p| p.eq_ignore_ascii_case(prefix))
231        };
232        if has_case_insensitive_prefix(cookie.name(), "__Secure-") &&
233            !cookie.secure().unwrap_or(false)
234        {
235            return None;
236        }
237
238        // Step 21. If the cookie-name begins with a case-insensitive match for the string "__Host-",
239        // abort these steps and ignore the cookie entirely unless the cookie meets all the following criteria:
240        if has_case_insensitive_prefix(cookie.name(), "__Host-") {
241            // 1. The cookie's secure-only-flag is true.
242            if !secure_only {
243                return None;
244            }
245
246            // 2. The cookie's host-only-flag is true.
247            if !host_only {
248                return None;
249            }
250
251            // 3. The cookie-attribute-list contains an attribute with an attribute-name of "Path",
252            // and the cookie's path is /.
253            #[allow(clippy::nonminimal_bool)]
254            if !has_path_specified || !cookie.path().is_some_and(|path| path == "/") {
255                return None;
256            }
257        }
258
259        // Step 22. If the cookie-name is empty and either of the following conditions are true,
260        // abort these steps and ignore the cookie entirely:
261        if cookie.name().is_empty() {
262            // 1. the cookie-value begins with a case-insensitive match for the string "__Secure-"
263            if has_case_insensitive_prefix(cookie.value(), "__Secure-") {
264                return None;
265            }
266
267            // 2. the cookie-value begins with a case-insensitive match for the string "__Host-"
268            if has_case_insensitive_prefix(cookie.value(), "__Host-") {
269                return None;
270            }
271        }
272
273        Some(ServoCookie {
274            cookie,
275            host_only,
276            persistent,
277            creation_time: SystemTime::now(),
278            last_access: SystemTime::now(),
279            expiry_time,
280        })
281    }
282
283    pub fn touch(&mut self) {
284        self.last_access = SystemTime::now();
285    }
286
287    pub fn set_expiry_time_in_past(&mut self) {
288        self.expiry_time = Some(SystemTime::UNIX_EPOCH)
289    }
290
291    /// <http://tools.ietf.org/html/rfc6265#section-5.1.4>
292    pub fn default_path(request_path: &str) -> &str {
293        // Step 2
294        if !request_path.starts_with('/') {
295            return "/";
296        }
297
298        // Step 3
299        let rightmost_slash_idx = request_path.rfind('/').unwrap();
300        if rightmost_slash_idx == 0 {
301            // There's only one slash; it's the first character
302            return "/";
303        }
304
305        // Step 4
306        &request_path[..rightmost_slash_idx]
307    }
308
309    /// <http://tools.ietf.org/html/rfc6265#section-5.1.4>
310    pub fn path_match(request_path: &str, cookie_path: &str) -> bool {
311        // A request-path path-matches a given cookie-path if at least one of
312        // the following conditions holds:
313
314        // The cookie-path and the request-path are identical.
315        request_path == cookie_path ||
316            (request_path.starts_with(cookie_path) &&
317                (
318                    // The cookie-path is a prefix of the request-path, and the last
319                    // character of the cookie-path is %x2F ("/").
320                    cookie_path.ends_with('/') ||
321            // The cookie-path is a prefix of the request-path, and the first
322            // character of the request-path that is not included in the cookie-
323            // path is a %x2F ("/") character.
324            request_path[cookie_path.len()..].starts_with('/')
325                ))
326    }
327
328    /// <http://tools.ietf.org/html/rfc6265#section-5.1.3>
329    pub fn domain_match(string: &str, domain_string: &str) -> bool {
330        let string = &string.to_lowercase();
331        let domain_string = &domain_string.to_lowercase();
332
333        string == domain_string ||
334            (string.ends_with(domain_string) &&
335                string.as_bytes()[string.len() - domain_string.len() - 1] == b'.' &&
336                string.parse::<Ipv4Addr>().is_err() &&
337                string.parse::<Ipv6Addr>().is_err())
338    }
339
340    /// <http://tools.ietf.org/html/rfc6265#section-5.4> step 1
341    pub fn appropriate_for_url(&self, url: &ServoUrl, source: CookieSource) -> bool {
342        if log_enabled!(Level::Debug) {
343            debug!(
344                " === SENT COOKIE : {} {} {:?} {:?}",
345                self.cookie.name(),
346                self.cookie.value(),
347                self.cookie.domain(),
348                self.cookie.path()
349            );
350        }
351
352        let domain = url.host_str();
353        // Either: The cookie's host-only-flag is true and the canonicalized host of the
354        // retrieval's URI is identical to the cookie's domain
355        // Or: The cookie's host-only-flag is false and the canonicalized host of the
356        // retrieval's URI domain-matches the cookie's domain
357        if self.host_only {
358            if self.cookie.domain() != domain {
359                return false;
360            }
361        } else if let (Some(domain), Some(cookie_domain)) = (domain, &self.cookie.domain()) &&
362            !ServoCookie::domain_match(domain, cookie_domain)
363        {
364            return false;
365        }
366
367        // The retrieval's URI's path path-matches the cookie's path.
368        if let Some(cookie_path) = self.cookie.path() &&
369            !ServoCookie::path_match(url.path(), cookie_path)
370        {
371            return false;
372        }
373
374        // If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection
375        if self.cookie.secure().unwrap_or(false) && !url.is_secure_scheme() {
376            return false;
377        }
378
379        // If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP"
380        if self.cookie.http_only().unwrap_or(false) && source == CookieSource::NonHTTP {
381            return false;
382        }
383        // TODO: Apply same site checks
384        // TOOD: Apply Partitioning checks
385
386        true
387    }
388
389    /// <https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-20.html#name-dates>
390    pub fn parse_date(string: &str) -> Option<OffsetDateTime> {
391        let string_in_bytes = string.as_bytes();
392
393        // Helper closures
394        let parse_ascii_u8 =
395            |bytes: &[u8]| -> Option<u8> { std::str::from_utf8(bytes).ok()?.parse::<u8>().ok() };
396        let parse_ascii_i32 =
397            |bytes: &[u8]| -> Option<i32> { std::str::from_utf8(bytes).ok()?.parse::<i32>().ok() };
398
399        // Step 1. Using the grammar below, divide the cookie-date into date-tokens.
400        // *OCTET
401        let any_octets = |input| Ok(("".as_bytes(), input));
402        // delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E
403        let delimiter: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
404            let (input, bytes) = take(1usize)(input)?;
405            if matches!(bytes[0], 0x09 | 0x20..=0x2F | 0x3B..=0x40 | 0x5B..=0x60 | 0x7B..=0x7E) {
406                Ok((input, bytes[0]))
407            } else {
408                Err(nom::Err::Error(nom::error::Error::new(
409                    input,
410                    nom::error::ErrorKind::Verify,
411                )))
412            }
413        };
414        // non-delimiter = %x00-08 / %x0A-1F / DIGIT / ":" / ALPHA / %x7F-FF
415        let non_delimiter: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
416            let (input, bytes) = take(1usize)(input)?;
417            if matches!(bytes[0],
418                0x00..=0x08 | 0x0A..=0x1F | b'0'..=b'9' | b':' | b'A'..=b'Z' | b'a'..=b'z' | 0x7F..=0xFF)
419            {
420                Ok((input, bytes[0]))
421            } else {
422                Err(nom::Err::Error(nom::error::Error::new(
423                    input,
424                    nom::error::ErrorKind::Verify,
425                )))
426            }
427        };
428        // non-digit = %x00-2F / %x3A-FF
429        let non_digit: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
430            let (input, bytes) = take(1usize)(input)?;
431            if matches!(bytes[0], 0x00..=0x2F | 0x3A..=0xFF) {
432                Ok((input, bytes[0]))
433            } else {
434                Err(nom::Err::Error(nom::error::Error::new(
435                    input,
436                    nom::error::ErrorKind::Verify,
437                )))
438            }
439        };
440        // time-field = 1*2DIGIT
441        let time_field =
442            |input| take_while_m_n(1, 2, |byte: u8| byte.is_ascii_digit()).parse(input);
443        // hms-time = time-field ":" time-field ":" time-field
444        let hms_time = |input| {
445            (
446                time_field,
447                preceded(tag(":"), time_field),
448                preceded(tag(":"), time_field),
449            )
450                .parse(input)
451        };
452        // time = hms-time [ non-digit *OCTET ]
453        let time = |input| terminated(hms_time, opt((non_digit, any_octets))).parse(input);
454        // year = 2*4DIGIT [ non-digit *OCTET ]
455        let year = |input| {
456            terminated(
457                take_while_m_n(2, 4, |byte: u8| byte.is_ascii_digit()),
458                opt((non_digit, any_octets)),
459            )
460            .parse(input)
461        };
462        // month = ( "jan" / "feb" / "mar" / "apr" /
463        //           "may" / "jun" / "jul" / "aug" /
464        //           "sep" / "oct" / "nov" / "dec" ) *OCTET
465        let month = |input| {
466            terminated(
467                alt((
468                    tag_no_case("jan"),
469                    tag_no_case("feb"),
470                    tag_no_case("mar"),
471                    tag_no_case("apr"),
472                    tag_no_case("may"),
473                    tag_no_case("jun"),
474                    tag_no_case("jul"),
475                    tag_no_case("aug"),
476                    tag_no_case("sep"),
477                    tag_no_case("oct"),
478                    tag_no_case("nov"),
479                    tag_no_case("dec"),
480                )),
481                any_octets,
482            )
483            .parse(input)
484        };
485        // day-of-month = 1*2DIGIT [ non-digit *OCTET ]
486        let day_of_month = |input| {
487            terminated(
488                take_while_m_n(1, 2, |byte: u8| byte.is_ascii_digit()),
489                opt((non_digit, any_octets)),
490            )
491            .parse(input)
492        };
493        // date-token = 1*non-delimiter
494        let date_token = |input| recognize(many1(non_delimiter)).parse(input);
495        // date-token-list = date-token *( 1*delimiter date-token )
496        let date_token_list = |input| separated_list1(delimiter, date_token).parse(input);
497        // cookie-date = *delimiter date-token-list *delimiter
498        let cookie_date =
499            |input| delimited(many0(delimiter), date_token_list, many0(delimiter)).parse(input);
500
501        // Step 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date:
502        let mut time_value: Option<(u8, u8, u8)> = None; // Also represents found-time flag.
503        let mut day_of_month_value: Option<u8> = None; // Also represents found-day-of-month flag.
504        let mut month_value: Option<Month> = None; // Also represents found-month flag.
505        let mut year_value: Option<i32> = None; // Also represents found-year flag.
506
507        let (_, date_tokens) = cookie_date(string_in_bytes).ok()?;
508        for date_token in date_tokens {
509            // Step 2.1. If the found-time flag is not set and the token matches the time production,
510            if time_value.is_none() &&
511                let Ok((_, result)) = time(date_token)
512            {
513                // set the found-time flag and set the hour-value, minute-value, and
514                // second-value to the numbers denoted by the digits in the date-token,
515                // respectively.
516                if let (Some(hour), Some(minute), Some(second)) = (
517                    parse_ascii_u8(result.0),
518                    parse_ascii_u8(result.1),
519                    parse_ascii_u8(result.2),
520                ) {
521                    time_value = Some((hour, minute, second));
522                }
523                // Skip the remaining sub-steps and continue to the next date-token.
524                continue;
525            }
526
527            // Step 2.2. If the found-day-of-month flag is not set and the date-token matches the
528            // day-of-month production,
529            if day_of_month_value.is_none() &&
530                let Ok((_, result)) = day_of_month(date_token)
531            {
532                // set the found-day-of-month flag and set the day-of-month-value to the number
533                // denoted by the date-token.
534                day_of_month_value = parse_ascii_u8(result);
535                // Skip the remaining sub-steps and continue to the next date-token.
536                continue;
537            }
538
539            // Step 2.3. If the found-month flag is not set and the date-token matches the month production,
540            if month_value.is_none() &&
541                let Ok((_, result)) = month(date_token)
542            {
543                // set the found-month flag and set the month-value to the month denoted by the date-token.
544                month_value = match std::str::from_utf8(result)
545                    .unwrap()
546                    .to_ascii_lowercase()
547                    .as_str()
548                {
549                    "jan" => Some(Month::January),
550                    "feb" => Some(Month::February),
551                    "mar" => Some(Month::March),
552                    "apr" => Some(Month::April),
553                    "may" => Some(Month::May),
554                    "jun" => Some(Month::June),
555                    "jul" => Some(Month::July),
556                    "aug" => Some(Month::August),
557                    "sep" => Some(Month::September),
558                    "oct" => Some(Month::October),
559                    "nov" => Some(Month::November),
560                    "dec" => Some(Month::December),
561                    _ => None,
562                };
563                // Skip the remaining sub-steps and continue to the next date-token.
564                continue;
565            }
566
567            // Step 2.4. If the found-year flag is not set and the date-token matches the year production,
568            if year_value.is_none() &&
569                let Ok((_, result)) = year(date_token)
570            {
571                // set the found-year flag and set the year-value to the number denoted by the date-token.
572                year_value = parse_ascii_i32(result);
573                // Skip the remaining sub-steps and continue to the next date-token.
574                continue;
575            }
576        }
577
578        // Step 3. If the year-value is greater than or equal to 70 and less than or equal to 99,
579        // increment the year-value by 1900.
580        if let Some(value) = year_value &&
581            (70..=99).contains(&value)
582        {
583            year_value = Some(value + 1900);
584        }
585
586        // Step 4. If the year-value is greater than or equal to 0 and less than or equal to 69,
587        // increment the year-value by 2000.
588        if let Some(value) = year_value &&
589            (0..=69).contains(&value)
590        {
591            year_value = Some(value + 2000);
592        }
593
594        // Step 5. Abort these steps and fail to parse the cookie-date if:
595        // * at least one of the found-day-of-month, found-month, found-year, or found-time flags is not set,
596        if day_of_month_value.is_none() ||
597            month_value.is_none() ||
598            year_value.is_none() ||
599            time_value.is_none()
600        {
601            return None;
602        }
603        // * the day-of-month-value is less than 1 or greater than 31,
604        if let Some(value) = day_of_month_value &&
605            !(1..=31).contains(&value)
606        {
607            return None;
608        }
609        // * the year-value is less than 1601,
610        if let Some(value) = year_value &&
611            value < 1601
612        {
613            return None;
614        }
615        // * the hour-value is greater than 23,
616        // * the minute-value is greater than 59, or
617        // * the second-value is greater than 59.
618        if let Some((hour_value, minute_value, second_value)) = time_value &&
619            (hour_value > 23 || minute_value > 59 || second_value > 59)
620        {
621            return None;
622        }
623
624        // Step 6. Let the parsed-cookie-date be the date whose day-of-month, month, year, hour,
625        // minute, and second (in UTC) are the day-of-month-value, the month-value, the year-value,
626        // the hour-value, the minute-value, and the second-value, respectively. If no such date
627        // exists, abort these steps and fail to parse the cookie-date.
628        let parsed_cookie_date = OffsetDateTime::new_utc(
629            Date::from_calendar_date(
630                year_value.unwrap(),
631                month_value.unwrap(),
632                day_of_month_value.unwrap(),
633            )
634            .ok()?,
635            Time::from_hms(
636                time_value.unwrap().0,
637                time_value.unwrap().1,
638                time_value.unwrap().2,
639            )
640            .ok()?,
641        );
642
643        // Step 7. Return the parsed-cookie-date as the result of this algorithm.
644        Some(parsed_cookie_date)
645    }
646
647    /// Returns true if the slice only contains bytes that are safe to use in cookie strings.
648    /// Rejects 0x7f, and values < 0x1f except 0x09
649    /// <https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-6>
650    pub fn is_valid_name_or_value(bytes: &[u8]) -> bool {
651        !bytes
652            .iter()
653            .any(|c| *c == 0x7f || (*c <= 0x1f && *c != 0x09))
654    }
655}