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