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::pub_domains::is_pub_domain;
16use net_traits::{CookieSource, ends_with_ignore_ascii_case};
17use nom::branch::alt;
18use nom::bytes::complete::{tag, tag_no_case, take, take_while_m_n};
19use nom::combinator::{opt, recognize, value};
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 if !has_path_specified || !cookie.path().is_some_and(|path| path == "/") {
254 return None;
255 }
256 }
257
258 // Step 22. If the cookie-name is empty and either of the following conditions are true,
259 // abort these steps and ignore the cookie entirely:
260 if cookie.name().is_empty() {
261 // 1. the cookie-value begins with a case-insensitive match for the string "__Secure-"
262 if has_case_insensitive_prefix(cookie.value(), "__Secure-") {
263 return None;
264 }
265
266 // 2. the cookie-value begins with a case-insensitive match for the string "__Host-"
267 if has_case_insensitive_prefix(cookie.value(), "__Host-") {
268 return None;
269 }
270 }
271
272 Some(ServoCookie {
273 cookie,
274 host_only,
275 persistent,
276 creation_time: SystemTime::now(),
277 last_access: SystemTime::now(),
278 expiry_time,
279 })
280 }
281
282 pub fn touch(&mut self) {
283 self.last_access = SystemTime::now();
284 }
285
286 pub fn set_expiry_time_in_past(&mut self) {
287 self.expiry_time = Some(SystemTime::UNIX_EPOCH)
288 }
289
290 /// <http://tools.ietf.org/html/rfc6265#section-5.1.4>
291 pub fn default_path(request_path: &str) -> &str {
292 // Step 2
293 if !request_path.starts_with('/') {
294 return "/";
295 }
296
297 // Step 3
298 let rightmost_slash_idx = request_path.rfind('/').unwrap();
299 if rightmost_slash_idx == 0 {
300 // There's only one slash; it's the first character
301 return "/";
302 }
303
304 // Step 4
305 &request_path[..rightmost_slash_idx]
306 }
307
308 /// <http://tools.ietf.org/html/rfc6265#section-5.1.4>
309 pub fn path_match(request_path: &str, cookie_path: &str) -> bool {
310 // A request-path path-matches a given cookie-path if at least one of
311 // the following conditions holds:
312
313 // The cookie-path and the request-path are identical.
314 request_path == cookie_path ||
315 (request_path.starts_with(cookie_path) &&
316 (
317 // The cookie-path is a prefix of the request-path, and the last
318 // character of the cookie-path is %x2F ("/").
319 cookie_path.ends_with('/') ||
320 // The cookie-path is a prefix of the request-path, and the first
321 // character of the request-path that is not included in the cookie-
322 // path is a %x2F ("/") character.
323 request_path[cookie_path.len()..].starts_with('/')
324 ))
325 }
326
327 /// <http://tools.ietf.org/html/rfc6265#section-5.1.3>
328 pub fn domain_match(string: &str, domain_string: &str) -> bool {
329 string.eq_ignore_ascii_case(domain_string) ||
330 (ends_with_ignore_ascii_case(string, domain_string) &&
331 string.as_bytes()[string.len() - domain_string.len() - 1] == b'.' &&
332 string.parse::<Ipv4Addr>().is_err() &&
333 string.parse::<Ipv6Addr>().is_err())
334 }
335
336 /// <http://tools.ietf.org/html/rfc6265#section-5.4> step 1
337 pub fn appropriate_for_url(&self, url: &ServoUrl, source: CookieSource) -> bool {
338 if log_enabled!(Level::Debug) {
339 debug!(
340 " === SENT COOKIE : {} {} {:?} {:?}",
341 self.cookie.name(),
342 self.cookie.value(),
343 self.cookie.domain(),
344 self.cookie.path()
345 );
346 }
347
348 let domain = url.host_str();
349 // Either: The cookie's host-only-flag is true and the canonicalized host of the
350 // retrieval's URI is identical to the cookie's domain
351 // Or: The cookie's host-only-flag is false and the canonicalized host of the
352 // retrieval's URI domain-matches the cookie's domain
353 if self.host_only {
354 if self.cookie.domain() != domain {
355 return false;
356 }
357 } else if let (Some(domain), Some(cookie_domain)) = (domain, &self.cookie.domain()) &&
358 !ServoCookie::domain_match(domain, cookie_domain)
359 {
360 return false;
361 }
362
363 // The retrieval's URI's path path-matches the cookie's path.
364 if let Some(cookie_path) = self.cookie.path() &&
365 !ServoCookie::path_match(url.path(), cookie_path)
366 {
367 return false;
368 }
369
370 // If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection
371 if self.cookie.secure().unwrap_or(false) && !url.is_secure_scheme() {
372 return false;
373 }
374
375 // If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP"
376 if self.cookie.http_only().unwrap_or(false) && source == CookieSource::NonHTTP {
377 return false;
378 }
379 // TODO: Apply same site checks
380 // TOOD: Apply Partitioning checks
381
382 true
383 }
384
385 /// <https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-20.html#name-dates>
386 pub fn parse_date(string: &str) -> Option<OffsetDateTime> {
387 let string_in_bytes = string.as_bytes();
388
389 // Helper closures
390 let parse_ascii_u8 =
391 |bytes: &[u8]| -> Option<u8> { std::str::from_utf8(bytes).ok()?.parse::<u8>().ok() };
392 let parse_ascii_i32 =
393 |bytes: &[u8]| -> Option<i32> { std::str::from_utf8(bytes).ok()?.parse::<i32>().ok() };
394
395 // Step 1. Using the grammar below, divide the cookie-date into date-tokens.
396 // *OCTET
397 let any_octets = |input| Ok(("".as_bytes(), input));
398 // delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E
399 let delimiter: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
400 let (input, bytes) = take(1usize)(input)?;
401 if matches!(bytes[0], 0x09 | 0x20..=0x2F | 0x3B..=0x40 | 0x5B..=0x60 | 0x7B..=0x7E) {
402 Ok((input, bytes[0]))
403 } else {
404 Err(nom::Err::Error(nom::error::Error::new(
405 input,
406 nom::error::ErrorKind::Verify,
407 )))
408 }
409 };
410 // non-delimiter = %x00-08 / %x0A-1F / DIGIT / ":" / ALPHA / %x7F-FF
411 let non_delimiter: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
412 let (input, bytes) = take(1usize)(input)?;
413 if matches!(bytes[0],
414 0x00..=0x08 | 0x0A..=0x1F | b'0'..=b'9' | b':' | b'A'..=b'Z' | b'a'..=b'z' | 0x7F..=0xFF)
415 {
416 Ok((input, bytes[0]))
417 } else {
418 Err(nom::Err::Error(nom::error::Error::new(
419 input,
420 nom::error::ErrorKind::Verify,
421 )))
422 }
423 };
424 // non-digit = %x00-2F / %x3A-FF
425 let non_digit: fn(&[u8]) -> IResult<&[u8], u8> = |input| {
426 let (input, bytes) = take(1usize)(input)?;
427 if matches!(bytes[0], 0x00..=0x2F | 0x3A..=0xFF) {
428 Ok((input, bytes[0]))
429 } else {
430 Err(nom::Err::Error(nom::error::Error::new(
431 input,
432 nom::error::ErrorKind::Verify,
433 )))
434 }
435 };
436 // time-field = 1*2DIGIT
437 let time_field =
438 |input| take_while_m_n(1, 2, |byte: u8| byte.is_ascii_digit()).parse(input);
439 // hms-time = time-field ":" time-field ":" time-field
440 let hms_time = |input| {
441 (
442 time_field,
443 preceded(tag(":"), time_field),
444 preceded(tag(":"), time_field),
445 )
446 .parse(input)
447 };
448 // time = hms-time [ non-digit *OCTET ]
449 let time = |input| terminated(hms_time, opt((non_digit, any_octets))).parse(input);
450 // year = 2*4DIGIT [ non-digit *OCTET ]
451 let year = |input| {
452 terminated(
453 take_while_m_n(2, 4, |byte: u8| byte.is_ascii_digit()),
454 opt((non_digit, any_octets)),
455 )
456 .parse(input)
457 };
458 // month = ( "jan" / "feb" / "mar" / "apr" /
459 // "may" / "jun" / "jul" / "aug" /
460 // "sep" / "oct" / "nov" / "dec" ) *OCTET
461 let month = |input| {
462 terminated(
463 alt((
464 value(Month::January, tag_no_case("jan")),
465 value(Month::February, tag_no_case("feb")),
466 value(Month::March, tag_no_case("mar")),
467 value(Month::April, tag_no_case("apr")),
468 value(Month::May, tag_no_case("may")),
469 value(Month::June, tag_no_case("jun")),
470 value(Month::July, tag_no_case("jul")),
471 value(Month::August, tag_no_case("aug")),
472 value(Month::September, tag_no_case("sep")),
473 value(Month::October, tag_no_case("oct")),
474 value(Month::November, tag_no_case("nov")),
475 value(Month::December, tag_no_case("dec")),
476 )),
477 any_octets,
478 )
479 .parse(input)
480 };
481 // day-of-month = 1*2DIGIT [ non-digit *OCTET ]
482 let day_of_month = |input| {
483 terminated(
484 take_while_m_n(1, 2, |byte: u8| byte.is_ascii_digit()),
485 opt((non_digit, any_octets)),
486 )
487 .parse(input)
488 };
489 // date-token = 1*non-delimiter
490 let date_token = |input| recognize(many1(non_delimiter)).parse(input);
491 // date-token-list = date-token *( 1*delimiter date-token )
492 let date_token_list = |input| separated_list1(delimiter, date_token).parse(input);
493 // cookie-date = *delimiter date-token-list *delimiter
494 let cookie_date =
495 |input| delimited(many0(delimiter), date_token_list, many0(delimiter)).parse(input);
496
497 // Step 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date:
498 let mut time_value: Option<(u8, u8, u8)> = None; // Also represents found-time flag.
499 let mut day_of_month_value: Option<u8> = None; // Also represents found-day-of-month flag.
500 let mut month_value: Option<Month> = None; // Also represents found-month flag.
501 let mut year_value: Option<i32> = None; // Also represents found-year flag.
502
503 let (_, date_tokens) = cookie_date(string_in_bytes).ok()?;
504 for date_token in date_tokens {
505 // Step 2.1. If the found-time flag is not set and the token matches the time production,
506 if time_value.is_none() &&
507 let Ok((_, result)) = time(date_token)
508 {
509 // set the found-time flag and set the hour-value, minute-value, and
510 // second-value to the numbers denoted by the digits in the date-token,
511 // respectively.
512 if let (Some(hour), Some(minute), Some(second)) = (
513 parse_ascii_u8(result.0),
514 parse_ascii_u8(result.1),
515 parse_ascii_u8(result.2),
516 ) {
517 time_value = Some((hour, minute, second));
518 }
519 // Skip the remaining sub-steps and continue to the next date-token.
520 continue;
521 }
522
523 // Step 2.2. If the found-day-of-month flag is not set and the date-token matches the
524 // day-of-month production,
525 if day_of_month_value.is_none() &&
526 let Ok((_, result)) = day_of_month(date_token)
527 {
528 // set the found-day-of-month flag and set the day-of-month-value to the number
529 // denoted by the date-token.
530 day_of_month_value = parse_ascii_u8(result);
531 // Skip the remaining sub-steps and continue to the next date-token.
532 continue;
533 }
534
535 // Step 2.3. If the found-month flag is not set and the date-token matches the month production,
536 if month_value.is_none() &&
537 let Ok((_, result)) = month(date_token)
538 {
539 // set the found-month flag and set the month-value to the month denoted by the date-token.
540 month_value = Some(result);
541 // Skip the remaining sub-steps and continue to the next date-token.
542 continue;
543 }
544
545 // Step 2.4. If the found-year flag is not set and the date-token matches the year production,
546 if year_value.is_none() &&
547 let Ok((_, result)) = year(date_token)
548 {
549 // set the found-year flag and set the year-value to the number denoted by the date-token.
550 year_value = parse_ascii_i32(result);
551 // Skip the remaining sub-steps and continue to the next date-token.
552 continue;
553 }
554 }
555
556 // Step 3. If the year-value is greater than or equal to 70 and less than or equal to 99,
557 // increment the year-value by 1900.
558 if let Some(value) = year_value &&
559 (70..=99).contains(&value)
560 {
561 year_value = Some(value + 1900);
562 }
563
564 // Step 4. If the year-value is greater than or equal to 0 and less than or equal to 69,
565 // increment the year-value by 2000.
566 if let Some(value) = year_value &&
567 (0..=69).contains(&value)
568 {
569 year_value = Some(value + 2000);
570 }
571
572 // Step 5. Abort these steps and fail to parse the cookie-date if:
573 // * at least one of the found-day-of-month, found-month, found-year, or found-time flags is not set,
574 if day_of_month_value.is_none() ||
575 month_value.is_none() ||
576 year_value.is_none() ||
577 time_value.is_none()
578 {
579 return None;
580 }
581 // * the day-of-month-value is less than 1 or greater than 31,
582 if let Some(value) = day_of_month_value &&
583 !(1..=31).contains(&value)
584 {
585 return None;
586 }
587 // * the year-value is less than 1601,
588 if let Some(value) = year_value &&
589 value < 1601
590 {
591 return None;
592 }
593 // * the hour-value is greater than 23,
594 // * the minute-value is greater than 59, or
595 // * the second-value is greater than 59.
596 if let Some((hour_value, minute_value, second_value)) = time_value &&
597 (hour_value > 23 || minute_value > 59 || second_value > 59)
598 {
599 return None;
600 }
601
602 // Step 6. Let the parsed-cookie-date be the date whose day-of-month, month, year, hour,
603 // minute, and second (in UTC) are the day-of-month-value, the month-value, the year-value,
604 // the hour-value, the minute-value, and the second-value, respectively. If no such date
605 // exists, abort these steps and fail to parse the cookie-date.
606 let parsed_cookie_date = OffsetDateTime::new_utc(
607 Date::from_calendar_date(
608 year_value.unwrap(),
609 month_value.unwrap(),
610 day_of_month_value.unwrap(),
611 )
612 .ok()?,
613 Time::from_hms(
614 time_value.unwrap().0,
615 time_value.unwrap().1,
616 time_value.unwrap().2,
617 )
618 .ok()?,
619 );
620
621 // Step 7. Return the parsed-cookie-date as the result of this algorithm.
622 Some(parsed_cookie_date)
623 }
624
625 /// Returns true if the slice only contains bytes that are safe to use in cookie strings.
626 /// Rejects 0x7f, and values < 0x1f except 0x09
627 /// <https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-6>
628 pub fn is_valid_name_or_value(bytes: &[u8]) -> bool {
629 !bytes
630 .iter()
631 .any(|c| *c == 0x7f || (*c <= 0x1f && *c != 0x09))
632 }
633}