wclient/
cookie.rs

1// Copyright 2021 Juan A. Cáceres (cacexp@gmail.com)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use chrono::Utc;
16use chrono::NaiveDate;
17use std::time::UNIX_EPOCH;
18use chrono::DateTime;
19use std::str::FromStr;
20use std::io::{Error, ErrorKind};
21use std::time::{Duration, SystemTime};
22use std::ops::Add;
23use std::collections::{HashMap, HashSet};
24use std::cmp::{PartialEq, Eq};
25use std::hash::{Hash, Hasher};
26use lazy_static::lazy_static;
27use regex::Regex;
28
29pub(crate) const COOKIE: &str = "cookie";
30pub(crate) const COOKIE_EXPIRES: &str = "expires";
31pub(crate) const COOKIE_MAX_AGE: &str = "max-age";
32pub(crate) const COOKIE_DOMAIN: &str = "domain";
33pub(crate) const COOKIE_PATH: &str = "path";
34pub(crate) const COOKIE_SAME_SITE: &str = "samesite";
35pub(crate) const COOKIE_SAME_SITE_STRICT: &str = "strict";
36pub(crate) const COOKIE_SAME_SITE_LAX: &str = "lax";
37pub(crate) const COOKIE_SAME_SITE_NONE: &str = "none";
38pub(crate) const COOKIE_SECURE: &str = "secure";
39pub(crate) const COOKIE_HTTP_ONLY: &str = "httponly";
40
41/// Enum with `SameSite` possible values for `Set-Cookie` attribute
42#[derive(Debug,Copy,Clone,PartialEq)]
43pub enum SameSiteValue {Strict, Lax, None}
44
45impl FromStr for SameSiteValue {
46    type Err = Error;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        return match s {
50            COOKIE_SAME_SITE_STRICT => Ok(SameSiteValue::Strict),
51            COOKIE_SAME_SITE_LAX => Ok(SameSiteValue::Lax),
52            COOKIE_SAME_SITE_NONE => Ok(SameSiteValue::None),
53            _ => Err(
54                Error::new(ErrorKind::InvalidData,
55                           format!("Invalid SameSite cookie directive value: {}", s)))
56        }
57
58    }
59}
60
61/// Represents a cookie created from `Set-Cookie` response header. 
62/// 
63/// A `Cookie` can be parsed from the `Set-Cookie` value from an HTTP `Response` using the trait `FromStr`:
64///
65///  `let cookie = Cookie::from_str("id=a3fWa; Expires=Wed, 21 Oct 2022 07:28:00 GMT");`
66/// 
67/// See [RFC6265 Set-Cookie](https://datatracker.ietf.org/doc/html/rfc6265#section-4.2) for more information.
68
69#[derive(Debug,Clone)]
70pub struct Cookie {
71    /// Cookie name
72    pub(crate) name: String,
73    /// Cookie value
74    pub (crate) value: String,
75    /// Cookie domain, by default is the originating domain of the request
76    pub(crate) domain: String,
77    /// Cookie path, by default, it is the request's path
78    pub(crate) path: String,
79    /// When the Cookie expires, if None, it does not expire.
80    /// This value is obtained from Max-Age and Expires attributes (Max-Age has precedence)
81    pub(crate) expires: Option<SystemTime>,
82    /// Cookie same site value (option)
83    pub(crate) same_site: SameSiteValue,
84    /// Cookie requires HTTPS
85    pub(crate) secure: bool,
86    /// Browsers does not allow Javascript access to this cookie
87    pub(crate) http_only: bool,
88    /// Other Set-Cookie extensions
89    pub(crate) extensions: HashMap<String, String>    
90}
91
92
93impl Cookie {
94    /// Constructor. It takes ownership params:
95    ///
96    /// * `name`: Cookie name
97    /// * `value`: Cookie value, for binary data it is recommended [Base64](https://en.wikipedia.org/wiki/Base64) encoding.
98    /// * `domain`: Cookie domain, sets hosts (domain and subdomains) to which the cookie will be sent, in includes subdomains.
99    /// If not present in `Set-Cookie` header, it is taken from the HTTP request `Host` header
100    /// * `path`: Cookie path, paths (same path or children) to which the cookie will be sent
101    /// If not present in `Set-Cookie` header, it is taken from the HTTP request path
102
103    pub fn new (name: String, value: String, domain: String, path: String) -> Cookie {
104        Cookie {
105            name,
106            value,
107            domain,
108            path,
109            expires: None,
110            same_site: SameSiteValue::Lax,
111            secure: false,
112            http_only: false,
113            extensions: HashMap::new()            
114        }
115    }
116
117    /// Cookie name
118    pub fn name(& self) -> &str {
119        self.name.as_str()
120    }
121
122    /// Cookie value
123    pub fn value(& self) -> &str {
124        self.value.as_str()
125    }
126
127    /// Cookie domain: hosts to which the cookie will be sent
128    pub fn domain(& self) -> &str {
129        self.domain.as_str()
130    }
131
132    /// Cookie path
133    pub fn path(& self) -> &str {
134        self.path.as_str()
135    }
136    /// When the Cookie expires, if `None`, it does not expire.
137    /// This value is obtained from `Max-Age` and `Expires` attributes (Max-Age has precedence)
138    pub fn expires(& self) -> Option<SystemTime> {
139        self.expires.clone()
140    }
141
142    /// Cookie `Same-Site` value (optional)
143    pub fn same_site(& self) -> SameSiteValue {
144        self.same_site
145    }
146    /// Cookie requires HTTPS
147    pub fn secure(& self) -> bool {
148        self.secure
149    }
150    /// Cookie requires HTTP only
151    pub fn http_only(& self) -> bool {
152        self.http_only
153    }
154
155    /// Cookie extendions
156    pub fn extensions(&self) -> &HashMap<String, String> {
157        &self.extensions
158    }
159
160    /// Checks if the request path match the cookie path. 
161    /// 
162    /// Using [RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) Algorithm.    
163    pub fn path_match(&self, request_path: &str) -> bool {
164        
165        let cookie_path = self.path();
166
167        let cookie_path_len = cookie_path.len();
168        let request_path_len = request_path.len();
169 
170       
171        if !request_path.starts_with(cookie_path) {  // A. cookie path is a prefix of request path
172            return false;
173        }
174    
175        return request_path_len ==  cookie_path_len // 1. They are identical, or
176            // 2. A and cookie path ends with an slash
177            || cookie_path.chars().nth(cookie_path_len - 1).unwrap() == '/' 
178            // 3. A and the first char of request path that is not incled in request path is an slash
179            || request_path.chars().nth(cookie_path_len).unwrap() == '/'; 
180    }
181
182    /// Checks if the request domain match the cookie domain. 
183    /// 
184    /// Using [RFC6265 Section 4.1.1.3](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.3).
185    pub fn domain_match(&self, request_domain: &str) -> bool {
186        let cookie_domain = self.domain();
187        
188        if let Some(index) = request_domain.rfind(cookie_domain) {
189            if index == 0 { // same domain
190                return true;
191            }
192            // The cookie domain is a subdomain of request domain, acccept
193            return request_domain.chars().nth(index-1).unwrap() == '.';
194        }
195         
196        return false;
197    }
198
199    /// Checks if the cookie can be used on this request
200    pub fn request_match(&self, request_domain: &str, request_path: &str, secure: bool) -> bool {
201
202        // Match Secure restrictions 
203
204        if self.secure && !secure {
205            return false;
206        }        
207    
208        // Strict behaviour: it is only same-site if the domain is the same
209
210        if self.same_site == SameSiteValue::Strict && self.domain != request_domain {
211            return false;
212        }
213
214        // Lax behaviour: allow cross-site from subdomain to father domain
215        if self.same_site() == SameSiteValue::Lax && !self.domain_match(request_domain) {
216            return false;
217        }
218
219        // None: allow all cookies transfer but only it HTTPS is in use
220        if self.same_site == SameSiteValue::None && ! self.secure {
221            return false;
222        }
223
224        // PATH filtering
225
226        return self.path_match(request_path);      
227    }
228
229     /// Parses a cookie value and modifiers from a 'Set-Cookie'header
230    pub fn parse(s: &str, domain: &str, path: &str) ->  Result<Cookie, Error> {
231    let mut components = s.split(';');
232 
233        return if let Some(slice) = components.next() {
234            let (key, value) = parse_cookie_value(slice)?;
235            let mut cookie = Cookie::new(key, value, String::from(domain), String::from(path));
236            while let Some(param) = components.next() {
237                let directive = CookieDirective::from_str(param)?;
238                match directive {
239                    CookieDirective::Expires(date) => {
240                        if cookie.expires().is_none() {  // Max-Age already parsed, it has precedence
241                            cookie.expires = Some(date);
242                        }
243                    },
244                    CookieDirective::MaxAge(seconds) => {
245                         cookie.expires = Some(SystemTime::now().add(seconds));
246                    },
247                    CookieDirective::Domain(url) => {   // starting dot is ignored                      
248                       cookie.domain = if let Some(stripped) = url.as_str().strip_prefix(".") {
249                           String::from(stripped)
250                       } else {
251                           url
252                       }    
253                    },
254                    CookieDirective::Path(path) => cookie.path = path,
255                    CookieDirective::SameSite(val) => cookie.same_site = val,
256                    CookieDirective::Secure => cookie.secure = true,
257                    CookieDirective::HttpOnly => cookie.http_only = true,
258                    CookieDirective::Extension(name, value) => {
259                        let _res = cookie.extensions.insert(name, value);
260                    }
261                }
262            }         
263            Ok(cookie)
264        } else {
265            if CookieDirective::from_str(s).is_ok() {
266                return Err(Error::new(ErrorKind::InvalidData, "Cookie has not got name/value"));
267            };
268
269            let (key, value) = parse_cookie_value(s)?;            
270            Ok(Cookie::new(key, value, String::from(domain),  String::from(path)))
271        }
272    }
273}
274
275impl PartialEq for Cookie {
276    fn eq(&self, other: &Self) -> bool {
277        self.name == other.name 
278    }    
279}
280
281impl Eq for Cookie{}
282
283impl Hash for Cookie {
284    fn hash<H: Hasher>(&self, state: &mut H) {
285        self.name.hash(state);
286        self.domain.hash(state);
287    }
288}
289
290
291/// Helper function to parse the `Cookie` name and value
292pub(crate) fn parse_cookie_value(cookie: &str) -> Result<(String, String), Error>{
293    if let Some(index) = cookie.find('=') {
294        let key = String::from(cookie[0..index].trim());
295        let value = String::from(cookie[index + 1..].trim());
296        return Ok((key, value))
297    } else {
298        Err(Error::new(ErrorKind::InvalidData,
299                       format!("Malformed HTTP cookie: {}", cookie)))
300    }
301}
302
303/// Helper enum to parse directives and set up the `Cookie` values
304enum CookieDirective {
305    Expires(SystemTime),
306    MaxAge(Duration),
307    Domain(String),
308    Path(String),
309    SameSite(SameSiteValue),
310    Secure,
311    HttpOnly,
312    Extension(String, String)
313}
314
315// 
316const DATE_FORMAT_850: &str= "(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|Mon|Tue|Wed|Thu|Fri|Sat|Sun), \
317(0[1-9]|[123][0-9])-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-([0-9]{4}|[0-9]{2}) \
318([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]) GMT";
319
320// Regex for dates Sun, 06 Nov 1994 08:49:37 GMT
321const DATE_FORMAT_1123: &str= "(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \
322(0[1-9]|[123][0-9]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) \
323([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]) GMT";
324
325
326// Regex for dates Sun Nov 6 08:49:37 1994 
327const DATE_FORMAT_ASCT: &str= "(Mon|Tue|Wed|Thu|Fri|Sat|Sun) \
328(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ ]{1,2}([1-9]|0[1-9]|[123][0-9]) \
329([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]) ([0-9]{4})";
330
331/// Parses RFC 850 dates, with extension. 
332/// For example,  `Wed, 15-Nov-23 09:13:29 GMT` and  `Wed, 15-Nov-23 09:13:29 GMT` 
333/// or `Sunday, 06-Nov-94 08:49:37 GMT` dates.
334fn parse_rfc_850_date(date: &str) -> Result<SystemTime, Error> {
335    lazy_static! {
336        static ref RE: Regex = Regex::new(DATE_FORMAT_850).unwrap();
337    }
338
339    
340    if let Some(captures) = RE.captures(date) {
341        // Capture 0 is the full match and  1 is the day of the week name
342        let day : u32 = captures.get(2).unwrap().as_str().parse().unwrap();
343        let month = match captures.get(3).unwrap().as_str() {
344            "Jan" => 1,
345            "Feb" => 2,
346            "Mar" => 3,
347            "Apr" => 4,
348            "May" => 5,
349            "Jun" => 6,
350            "Jul" => 7,
351            "Aug" => 8,
352            "Sep" => 9,
353            "Oct" => 10,
354            "Nov" => 11,
355            "Dec" => 12,
356            _ => return Err(Error::new(ErrorKind::InvalidData, "Invalid date"))
357        };
358
359        let mut year: i32 = captures.get(4).unwrap().as_str().parse().unwrap();
360        // Fix millenium, for 2 digit year
361        year+= if year < 70 {2000} else if year < 100 {1900} else {0};
362
363        let hour : u32 = captures.get(5).unwrap().as_str().parse().unwrap();
364        let min : u32 = captures.get(6).unwrap().as_str().parse().unwrap();
365        let secs : u32 = captures.get(7).unwrap().as_str().parse().unwrap();
366
367        let naive =
368            NaiveDate::from_ymd(year, month, day)
369            .and_hms(hour,min,secs);
370        let time = DateTime::<Utc>::from_utc(naive, Utc);
371        let millis = Duration::from_millis(time.timestamp_millis() as u64);
372        let time = UNIX_EPOCH.clone().add(millis);
373
374        return Ok(time);
375
376
377    } else {
378        return Err(Error::new(ErrorKind::InvalidData, "Invalid date"));
379    }
380}
381
382/// Parses RFC 1123 dates. 
383/// For example,  `Sun, 06 Nov 1994 08:49:37 GMT` date.
384fn parse_rfc_1123_date(date: &str) -> Result<SystemTime, Error> {
385    lazy_static! {
386        static ref RE: Regex = Regex::new(DATE_FORMAT_1123).unwrap();
387    }
388
389    
390    if let Some(captures) = RE.captures(date) {
391        // Capture 0 is the full match and  1 is the day of the week name
392        let day : u32 = captures.get(2).unwrap().as_str().parse().unwrap();
393        let month = match captures.get(3).unwrap().as_str() {
394            "Jan" => 1,
395            "Feb" => 2,
396            "Mar" => 3,
397            "Apr" => 4,
398            "May" => 5,
399            "Jun" => 6,
400            "Jul" => 7,
401            "Aug" => 8,
402            "Sep" => 9,
403            "Oct" => 10,
404            "Nov" => 11,
405            "Dec" => 12,
406            _ => return Err(Error::new(ErrorKind::InvalidData, "Invalid date"))
407        };
408
409        let year: i32 = captures.get(4).unwrap().as_str().parse().unwrap();
410
411        let hour : u32 = captures.get(5).unwrap().as_str().parse().unwrap();
412        let min : u32 = captures.get(6).unwrap().as_str().parse().unwrap();
413        let secs : u32 = captures.get(7).unwrap().as_str().parse().unwrap();
414
415        let naive =
416            NaiveDate::from_ymd(year, month, day)
417            .and_hms(hour,min,secs);
418        let time = DateTime::<Utc>::from_utc(naive, Utc);
419        let millis = Duration::from_millis(time.timestamp_millis() as u64);
420        let time = UNIX_EPOCH.clone().add(millis);
421
422        return Ok(time);
423
424
425    } else {
426        return Err(Error::new(ErrorKind::InvalidData, "Invalid date"));
427    }
428}
429
430/// Parses Asct dates, with extension. 
431/// For example,  `Sun Nov 6 08:49:37 1994` dates.
432fn parse_asct_date(date: &str) -> Result<SystemTime, Error> {
433    lazy_static! {
434        static ref RE: Regex = Regex::new(DATE_FORMAT_ASCT).unwrap();
435    }
436
437    
438    if let Some(captures) = RE.captures(date) {
439        // Capture 0 is the full match and  1 is the day of the week name
440        let month = match captures.get(2).unwrap().as_str() {
441            "Jan" => 1,
442            "Feb" => 2,
443            "Mar" => 3,
444            "Apr" => 4,
445            "May" => 5,
446            "Jun" => 6,
447            "Jul" => 7,
448            "Aug" => 8,
449            "Sep" => 9,
450            "Oct" => 10,
451            "Nov" => 11,
452            "Dec" => 12,
453            _ => return Err(Error::new(ErrorKind::InvalidData, "Invalid date"))
454        };
455
456        let day : u32 = captures.get(3).unwrap().as_str().parse().unwrap();
457        
458        let hour : u32 = captures.get(4).unwrap().as_str().parse().unwrap();
459        let min :  u32 = captures.get(5).unwrap().as_str().parse().unwrap();
460        let secs : u32 = captures.get(6).unwrap().as_str().parse().unwrap();
461
462        let year: i32 = captures.get(7).unwrap().as_str().parse().unwrap();
463       
464        let naive =
465            NaiveDate::from_ymd(year, month, day)
466            .and_hms(hour,min,secs);
467        let time = DateTime::<Utc>::from_utc(naive, Utc);
468        let millis = Duration::from_millis(time.timestamp_millis() as u64);
469        let time = UNIX_EPOCH.clone().add(millis);
470
471        return Ok(time);
472
473
474    } else {
475        return Err(Error::new(ErrorKind::InvalidData, "Invalid date"));
476    }
477}
478
479/// Helper function to parse `CookieDirective`
480impl FromStr for CookieDirective {
481    
482    type Err = Error;
483
484    fn from_str(s: &str) -> Result<CookieDirective,Error> {
485        if let Some(index) = s.find('=') { // Cookie param with value
486            let key = s[0..index].trim().to_ascii_lowercase();
487            let value = s[index + 1..].trim();
488            return match key.as_str() {
489                COOKIE_EXPIRES => {
490                    let expires = parse_rfc_1123_date(value)
491                        .or_else(|_| parse_rfc_850_date(value))
492                        .or_else(|_| parse_asct_date(value))?; 
493
494                    Ok(CookieDirective::Expires(expires))
495                },
496                COOKIE_MAX_AGE => {  // Max-age value in seconds
497                    let digit = u64::from_str(value)
498                        .or_else(|e|  {
499                            Err(Error::new(ErrorKind::InvalidData, e))
500                        })?;
501                    Ok(CookieDirective::MaxAge(Duration::from_secs(digit)))
502                },
503                COOKIE_DOMAIN => {
504                    Ok(CookieDirective::Domain(String::from(value)))
505                },
506                COOKIE_PATH => {
507                    Ok(CookieDirective::Path(String::from(value)))
508                }
509                COOKIE_SAME_SITE => {
510                    let lower_case = value.to_ascii_lowercase();
511                    match SameSiteValue::from_str(lower_case.as_str()) {
512                        Ok(site_value) => Ok(CookieDirective::SameSite(site_value)),
513                        Err(e) => Err(e)
514                    }
515                },
516                _ => Ok(CookieDirective::Extension(key, value.to_string()))
517            }
518        } else {
519            match s.trim().to_ascii_lowercase().as_str() {
520                COOKIE_SECURE => Ok(CookieDirective::Secure),
521                COOKIE_HTTP_ONLY => Ok(CookieDirective::HttpOnly),
522                _ => return Err(
523                    Error::new(ErrorKind::InvalidData,
524                            format!("Invalid HTTP cookie directive: {}", s)))
525            }
526        }
527    }
528}
529
530/// Cookies repository trait. Keeps active cookies from a session.
531pub trait CookieJar {
532    /// Adds a cookie to the jar, if 'value' has no 'domain' member, 'request_domain' is used
533    fn cookie(&mut self, value: Cookie, request_domain: &str);
534
535    /// Gets the active cookie name/value list for the given domain (expired are deleted)
536    fn active_cookies(&mut self, request_domain: &str, request_path: &str, secure: bool) -> Vec<(String, String)>;
537 }
538
539
540 pub struct MemCookieJar {
541    // Hash set of cookies by target domain
542    cookies: HashSet<Cookie>
543 }
544
545 impl MemCookieJar {
546     pub fn new() -> MemCookieJar{
547        MemCookieJar {
548            cookies: HashSet::new()
549        }
550     }
551 }
552
553 
554 impl CookieJar for MemCookieJar {
555    fn cookie(&mut self, value: Cookie, request_domain: &str) {
556
557        if !value.domain_match(request_domain) {
558            return; // Discard different domain
559        } 
560        let now =  SystemTime::now();
561
562        if let Some(expires) = value.expires() {
563            if expires < now {
564                return; // Discard expired 
565            }
566        }
567       
568        self.cookies.insert(value);
569    }
570
571    fn active_cookies(&mut self, request_domain: &str, request_path: &str, secure: bool) -> Vec<(String, String)> {
572        
573        let mut result = Vec::new();
574
575        // First clean expired cookies
576        let now =  SystemTime::now();
577
578        self.cookies.retain( |c| {
579            if let Some(time) = c.expires {
580                return time < now;
581            }
582            return true;
583        });
584                
585        for cookie in self.cookies.iter() {
586            if cookie.request_match(request_domain, request_path, secure) {
587                result.push((cookie.name.clone(), cookie.value.clone()));
588            }
589        }
590
591        return result;
592    }
593 }
594
595#[cfg(test)]
596mod cookie_test;