actori_http/cookie/
parse.rs

1use std::borrow::Cow;
2use std::cmp;
3use std::convert::From;
4use std::error::Error;
5use std::fmt;
6use std::str::Utf8Error;
7
8use chrono::Duration;
9use percent_encoding::percent_decode;
10
11use super::{Cookie, CookieStr, SameSite};
12
13/// Enum corresponding to a parsing error.
14#[derive(Debug, PartialEq, Eq, Clone, Copy)]
15pub enum ParseError {
16    /// The cookie did not contain a name/value pair.
17    MissingPair,
18    /// The cookie's name was empty.
19    EmptyName,
20    /// Decoding the cookie's name or value resulted in invalid UTF-8.
21    Utf8Error(Utf8Error),
22    /// It is discouraged to exhaustively match on this enum as its variants may
23    /// grow without a breaking-change bump in version numbers.
24    #[doc(hidden)]
25    __Nonexhasutive,
26}
27
28impl ParseError {
29    /// Returns a description of this error as a string
30    pub fn as_str(&self) -> &'static str {
31        match *self {
32            ParseError::MissingPair => "the cookie is missing a name/value pair",
33            ParseError::EmptyName => "the cookie's name is empty",
34            ParseError::Utf8Error(_) => {
35                "decoding the cookie's name or value resulted in invalid UTF-8"
36            }
37            ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"),
38        }
39    }
40}
41
42impl fmt::Display for ParseError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{}", self.as_str())
45    }
46}
47
48impl From<Utf8Error> for ParseError {
49    fn from(error: Utf8Error) -> ParseError {
50        ParseError::Utf8Error(error)
51    }
52}
53
54impl Error for ParseError {}
55
56fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> {
57    let haystack_start = haystack.as_ptr() as usize;
58    let needle_start = needle.as_ptr() as usize;
59
60    if needle_start < haystack_start {
61        return None;
62    }
63
64    if (needle_start + needle.len()) > (haystack_start + haystack.len()) {
65        return None;
66    }
67
68    let start = needle_start - haystack_start;
69    let end = start + needle.len();
70    Some((start, end))
71}
72
73fn name_val_decoded(
74    name: &str,
75    val: &str,
76) -> Result<(CookieStr, CookieStr), ParseError> {
77    let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?;
78    let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?;
79    let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned()));
80    let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned()));
81
82    Ok((name, val))
83}
84
85// This function does the real parsing but _does not_ set the `cookie_string` in
86// the returned cookie object. This only exists so that the borrow to `s` is
87// returned at the end of the call, allowing the `cookie_string` field to be
88// set in the outer `parse` function.
89fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
90    let mut attributes = s.split(';');
91    let key_value = match attributes.next() {
92        Some(s) => s,
93        _ => panic!(),
94    };
95
96    // Determine the name = val.
97    let (name, value) = match key_value.find('=') {
98        Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
99        None => return Err(ParseError::MissingPair),
100    };
101
102    if name.is_empty() {
103        return Err(ParseError::EmptyName);
104    }
105
106    // Create a cookie with all of the defaults. We'll fill things in while we
107    // iterate through the parameters below.
108    let (name, value) = if decode {
109        name_val_decoded(name, value)?
110    } else {
111        let name_indexes = indexes_of(name, s).expect("name sub");
112        let value_indexes = indexes_of(value, s).expect("value sub");
113        let name = CookieStr::Indexed(name_indexes.0, name_indexes.1);
114        let value = CookieStr::Indexed(value_indexes.0, value_indexes.1);
115
116        (name, value)
117    };
118
119    let mut cookie = Cookie {
120        name,
121        value,
122        cookie_string: None,
123        expires: None,
124        max_age: None,
125        domain: None,
126        path: None,
127        secure: None,
128        http_only: None,
129        same_site: None,
130    };
131
132    for attr in attributes {
133        let (key, value) = match attr.find('=') {
134            Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())),
135            None => (attr.trim(), None),
136        };
137
138        match (&*key.to_ascii_lowercase(), value) {
139            ("secure", _) => cookie.secure = Some(true),
140            ("httponly", _) => cookie.http_only = Some(true),
141            ("max-age", Some(v)) => {
142                // See RFC 6265 Section 5.2.2, negative values indicate that the
143                // earliest possible expiration time should be used, so set the
144                // max age as 0 seconds.
145                cookie.max_age = match v.parse() {
146                    Ok(val) if val <= 0 => Some(Duration::zero()),
147                    Ok(val) => {
148                        // Don't panic if the max age seconds is greater than what's supported by
149                        // `Duration`.
150                        let val = cmp::min(val, Duration::max_value().num_seconds());
151                        Some(Duration::seconds(val))
152                    }
153                    Err(_) => continue,
154                };
155            }
156            ("domain", Some(mut domain)) if !domain.is_empty() => {
157                if domain.starts_with('.') {
158                    domain = &domain[1..];
159                }
160
161                let (i, j) = indexes_of(domain, s).expect("domain sub");
162                cookie.domain = Some(CookieStr::Indexed(i, j));
163            }
164            ("path", Some(v)) => {
165                let (i, j) = indexes_of(v, s).expect("path sub");
166                cookie.path = Some(CookieStr::Indexed(i, j));
167            }
168            ("samesite", Some(v)) => {
169                if v.eq_ignore_ascii_case("strict") {
170                    cookie.same_site = Some(SameSite::Strict);
171                } else if v.eq_ignore_ascii_case("lax") {
172                    cookie.same_site = Some(SameSite::Lax);
173                } else {
174                    // We do nothing here, for now. When/if the `SameSite`
175                    // attribute becomes standard, the spec says that we should
176                    // ignore this cookie, i.e, fail to parse it, when an
177                    // invalid value is passed in. The draft is at
178                    // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html.
179                }
180            }
181            ("expires", Some(v)) => {
182                // Try strptime with three date formats according to
183                // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try
184                // additional ones as encountered in the real world.
185                let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z")
186                    .or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z"))
187                    .or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z"))
188                    .or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y"));
189
190                if let Ok(time) = tm {
191                    cookie.expires = Some(time)
192                }
193            }
194            _ => {
195                // We're going to be permissive here. If we have no idea what
196                // this is, then it's something nonstandard. We're not going to
197                // store it (because it's not compliant), but we're also not
198                // going to emit an error.
199            }
200        }
201    }
202
203    Ok(cookie)
204}
205
206pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
207where
208    S: Into<Cow<'c, str>>,
209{
210    let s = cow.into();
211    let mut cookie = parse_inner(&s, decode)?;
212    cookie.cookie_string = Some(s);
213    Ok(cookie)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::{Cookie, SameSite};
219    use chrono::Duration;
220    use time::strptime;
221
222    macro_rules! assert_eq_parse {
223        ($string:expr, $expected:expr) => {
224            let cookie = match Cookie::parse($string) {
225                Ok(cookie) => cookie,
226                Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
227            };
228
229            assert_eq!(cookie, $expected);
230        };
231    }
232
233    macro_rules! assert_ne_parse {
234        ($string:expr, $expected:expr) => {
235            let cookie = match Cookie::parse($string) {
236                Ok(cookie) => cookie,
237                Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
238            };
239
240            assert_ne!(cookie, $expected);
241        };
242    }
243
244    #[test]
245    fn parse_same_site() {
246        let expected = Cookie::build("foo", "bar")
247            .same_site(SameSite::Lax)
248            .finish();
249
250        assert_eq_parse!("foo=bar; SameSite=Lax", expected);
251        assert_eq_parse!("foo=bar; SameSite=lax", expected);
252        assert_eq_parse!("foo=bar; SameSite=LAX", expected);
253        assert_eq_parse!("foo=bar; samesite=Lax", expected);
254        assert_eq_parse!("foo=bar; SAMESITE=Lax", expected);
255
256        let expected = Cookie::build("foo", "bar")
257            .same_site(SameSite::Strict)
258            .finish();
259
260        assert_eq_parse!("foo=bar; SameSite=Strict", expected);
261        assert_eq_parse!("foo=bar; SameSITE=Strict", expected);
262        assert_eq_parse!("foo=bar; SameSite=strict", expected);
263        assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
264        assert_eq_parse!("foo=bar; SameSite=STRICT", expected);
265    }
266
267    #[test]
268    fn parse() {
269        assert!(Cookie::parse("bar").is_err());
270        assert!(Cookie::parse("=bar").is_err());
271        assert!(Cookie::parse(" =bar").is_err());
272        assert!(Cookie::parse("foo=").is_ok());
273
274        let expected = Cookie::build("foo", "bar=baz").finish();
275        assert_eq_parse!("foo=bar=baz", expected);
276
277        let mut expected = Cookie::build("foo", "bar").finish();
278        assert_eq_parse!("foo=bar", expected);
279        assert_eq_parse!("foo = bar", expected);
280        assert_eq_parse!(" foo=bar ", expected);
281        assert_eq_parse!(" foo=bar ;Domain=", expected);
282        assert_eq_parse!(" foo=bar ;Domain= ", expected);
283        assert_eq_parse!(" foo=bar ;Ignored", expected);
284
285        let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish();
286        assert_ne_parse!(" foo=bar ;HttpOnly", unexpected);
287        assert_ne_parse!(" foo=bar; httponly", unexpected);
288
289        expected.set_http_only(true);
290        assert_eq_parse!(" foo=bar ;HttpOnly", expected);
291        assert_eq_parse!(" foo=bar ;httponly", expected);
292        assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected);
293        assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected);
294
295        expected.set_secure(true);
296        assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected);
297        assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected);
298
299        unexpected.set_http_only(true);
300        unexpected.set_secure(true);
301        assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected);
302        assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected);
303        assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected);
304
305        unexpected.set_secure(false);
306        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
307        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
308        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
309
310        expected.set_max_age(Duration::zero());
311        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
312        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
313        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
314        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected);
315
316        expected.set_max_age(Duration::minutes(1));
317        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected);
318        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age =   60 ", expected);
319
320        expected.set_max_age(Duration::seconds(4));
321        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected);
322        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected);
323
324        unexpected.set_secure(true);
325        unexpected.set_max_age(Duration::minutes(1));
326        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected);
327        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected);
328        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected);
329        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected);
330        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected);
331
332        expected.set_path("/");
333        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected);
334        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected);
335
336        expected.set_path("/foo");
337        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected);
338        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected);
339        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected);
340        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected);
341
342        unexpected.set_max_age(Duration::seconds(4));
343        unexpected.set_path("/bar");
344        assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected);
345        assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected);
346
347        expected.set_domain("www.foo.com");
348        assert_eq_parse!(
349            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
350             Domain=www.foo.com",
351            expected
352        );
353
354        expected.set_domain("foo.com");
355        assert_eq_parse!(
356            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
357             Domain=foo.com",
358            expected
359        );
360        assert_eq_parse!(
361            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
362             Domain=FOO.COM",
363            expected
364        );
365
366        unexpected.set_path("/foo");
367        unexpected.set_domain("bar.com");
368        assert_ne_parse!(
369            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
370             Domain=foo.com",
371            unexpected
372        );
373        assert_ne_parse!(
374            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
375             Domain=FOO.COM",
376            unexpected
377        );
378
379        let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
380        let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap();
381        expected.set_expires(expires);
382        assert_eq_parse!(
383            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
384             Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
385            expected
386        );
387
388        unexpected.set_domain("foo.com");
389        let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap();
390        expected.set_expires(bad_expires);
391        assert_ne_parse!(
392            " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
393             Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
394            unexpected
395        );
396    }
397
398    #[test]
399    fn odd_characters() {
400        let expected = Cookie::new("foo", "b%2Fr");
401        assert_eq_parse!("foo=b%2Fr", expected);
402    }
403
404    #[test]
405    fn odd_characters_encoded() {
406        let expected = Cookie::new("foo", "b/r");
407        let cookie = match Cookie::parse_encoded("foo=b%2Fr") {
408            Ok(cookie) => cookie,
409            Err(e) => panic!("Failed to parse: {:?}", e),
410        };
411
412        assert_eq!(cookie, expected);
413    }
414
415    #[test]
416    fn do_not_panic_on_large_max_ages() {
417        let max_seconds = Duration::max_value().num_seconds();
418        let expected = Cookie::build("foo", "bar").max_age(max_seconds).finish();
419        assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected);
420    }
421}