use std::fmt::{self, Formatter}; use std::io::{BufRead, Write}; use std::iter; use std::ops::Deref; use ::cookie::Cookie as RawCookie; use log::debug; use publicsuffix; use serde::de::{SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Url; use crate::cookie::Cookie; use crate::cookie_domain::{is_match as domain_match, CookieDomain}; use crate::cookie_path::is_match as path_match; use crate::utils::{is_http_scheme, is_secure}; use crate::CookieError; #[cfg(feature = "preserve_order")] use indexmap::IndexMap; #[cfg(not(feature = "preserve_order"))] use std::collections::HashMap; #[cfg(feature = "preserve_order")] type Map = IndexMap; #[cfg(not(feature = "preserve_order"))] type Map = HashMap; type NameMap = Map>; type PathMap = Map; type DomainMap = Map; #[derive(PartialEq, Clone, Debug, Eq)] pub enum StoreAction { /// The `Cookie` was successfully added to the store Inserted, /// The `Cookie` successfully expired a `Cookie` already in the store ExpiredExisting, /// The `Cookie` was added to the store, replacing an existing entry UpdatedExisting, } pub type StoreResult = Result; pub type InsertResult = Result; #[derive(Debug, Default)] pub struct CookieStore { /// Cookies stored by domain, path, then name cookies: DomainMap, /// If set, enables [public suffix](https://tools.ietf.org/html/rfc6265#section-5.3) rejection based on the provided `publicsuffix::List` public_suffix_list: Option, } impl CookieStore { /// Return an `Iterator` of the cookies for `url` in the store pub fn get_request_cookies(&self, url: &Url) -> impl Iterator> { self.matches(url).into_iter().map(|c| c.deref()) } /// Store the `cookies` received from `url` pub fn store_response_cookies>>( &mut self, cookies: I, url: &Url, ) { for cookie in cookies { debug!("inserting Set-Cookie '{:?}'", cookie); if let Err(e) = self.insert_raw(&cookie, url) { debug!("unable to store Set-Cookie: {:?}", e); } } } /// Specify a `publicsuffix::List` for the `CookieStore` to allow [public suffix /// matching](https://tools.ietf.org/html/rfc6265#section-5.3) pub fn with_suffix_list(self, psl: publicsuffix::List) -> CookieStore { CookieStore { cookies: self.cookies, public_suffix_list: Some(psl), } } /// Returns true if the `CookieStore` contains an __unexpired__ `Cookie` corresponding to the /// specified `domain`, `path`, and `name`. pub fn contains(&self, domain: &str, path: &str, name: &str) -> bool { self.get(domain, path, name).is_some() } /// Returns true if the `CookieStore` contains any (even an __expired__) `Cookie` corresponding /// to the specified `domain`, `path`, and `name`. pub fn contains_any(&self, domain: &str, path: &str, name: &str) -> bool { self.get_any(domain, path, name).is_some() } /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`, /// `path`, and `name`. pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'_>> { self.get_any(domain, path, name).and_then(|cookie| { if cookie.is_expired() { None } else { Some(cookie) } }) } /// Returns a mutable reference to the __unexpired__ `Cookie` corresponding to the specified /// `domain`, `path`, and `name`. fn get_mut(&mut self, domain: &str, path: &str, name: &str) -> Option<&mut Cookie<'static>> { self.get_mut_any(domain, path, name).and_then(|cookie| { if cookie.is_expired() { None } else { Some(cookie) } }) } /// Returns a reference to the (possibly __expired__) `Cookie` corresponding to the specified /// `domain`, `path`, and `name`. pub fn get_any(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'static>> { self.cookies.get(domain).and_then(|domain_cookies| { domain_cookies .get(path) .and_then(|path_cookies| path_cookies.get(name)) }) } /// Returns a mutable reference to the (possibly __expired__) `Cookie` corresponding to the /// specified `domain`, `path`, and `name`. fn get_mut_any( &mut self, domain: &str, path: &str, name: &str, ) -> Option<&mut Cookie<'static>> { self.cookies.get_mut(domain).and_then(|domain_cookies| { domain_cookies .get_mut(path) .and_then(|path_cookies| path_cookies.get_mut(name)) }) } /// Removes a `Cookie` from the store, returning the `Cookie` if it was in the store pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option> { #[cfg(not(feature = "preserve_order"))] fn map_remove(map: &mut Map, key: &Q) -> Option where K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, Q: std::cmp::Eq + std::hash::Hash + ?Sized, { map.remove(key) } #[cfg(feature = "preserve_order")] fn map_remove(map: &mut Map, key: &Q) -> Option where K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, Q: std::cmp::Eq + std::hash::Hash + ?Sized, { map.shift_remove(key) } let (removed, remove_domain) = match self.cookies.get_mut(domain) { None => (None, false), Some(domain_cookies) => { let (removed, remove_path) = match domain_cookies.get_mut(path) { None => (None, false), Some(path_cookies) => { let removed = map_remove(path_cookies, name); (removed, path_cookies.is_empty()) } }; if remove_path { map_remove(domain_cookies, path); (removed, domain_cookies.is_empty()) } else { (removed, false) } } }; if remove_domain { map_remove(&mut self.cookies, domain); } removed } /// Returns a collection of references to __unexpired__ cookies that path- and domain-match /// `request_url`, as well as having HttpOnly and Secure attributes compatible with the /// `request_url`. pub fn matches(&self, request_url: &Url) -> Vec<&Cookie<'static>> { // although we domain_match and path_match as we descend through the tree, we // still need to // do a full Cookie::matches() check in the last filter. Otherwise, we cannot // properly deal // with HostOnly Cookies. let cookies = self .cookies .iter() .filter(|&(d, _)| domain_match(d, request_url)) .flat_map(|(_, dcs)| { dcs.iter() .filter(|&(p, _)| path_match(p, request_url)) .flat_map(|(_, pcs)| { pcs.values() .filter(|c| !c.is_expired() && c.matches(request_url)) }) }); match (!is_http_scheme(request_url), !is_secure(request_url)) { (true, true) => cookies .filter(|c| !c.http_only().unwrap_or(false) && !c.secure().unwrap_or(false)) .collect(), (true, false) => cookies .filter(|c| !c.http_only().unwrap_or(false)) .collect(), (false, true) => cookies.filter(|c| !c.secure().unwrap_or(false)).collect(), (false, false) => cookies.collect(), } } /// Parses a new `Cookie` from `cookie_str` and inserts it into the store. pub fn parse(&mut self, cookie_str: &str, request_url: &Url) -> InsertResult { Cookie::parse(cookie_str, request_url) .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) } /// Converts a `cookie::Cookie` (from the `cookie` crate) into a `cookie_store::Cookie` and /// inserts it into the store. pub fn insert_raw(&mut self, cookie: &RawCookie<'_>, request_url: &Url) -> InsertResult { Cookie::try_from_raw_cookie(cookie, request_url) .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) } /// Inserts `cookie`, received from `request_url`, into the store, following the rules of the /// [IETF RFC6265 Storage Model](http://tools.ietf.org/html/rfc6265#section-5.3). If the /// `Cookie` is __unexpired__ and is successfully inserted, returns /// `Ok(StoreAction::Inserted)`. If the `Cookie` is __expired__ *and* matches an existing /// `Cookie` in the store, the existing `Cookie` wil be `expired()` and /// `Ok(StoreAction::ExpiredExisting)` will be returned. pub fn insert(&mut self, mut cookie: Cookie<'static>, request_url: &Url) -> InsertResult { if cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { // If the cookie was received from a "non-HTTP" API and the // cookie's http-only-flag is set, abort these steps and ignore the // cookie entirely. return Err(CookieError::NonHttpScheme); } else if let Some(ref psl) = self.public_suffix_list { // If the user agent is configured to reject "public suffixes" if cookie.domain.is_public_suffix(psl) { // and the domain-attribute is a public suffix: if cookie.domain.host_is_identical(request_url) { // If the domain-attribute is identical to the canonicalized // request-host: // Let the domain-attribute be the empty string. // (NB: at this point, an empty domain-attribute should be represented // as the HostOnly variant of CookieDomain) cookie.domain = CookieDomain::host_only(request_url)?; } else { // Otherwise: // Ignore the cookie entirely and abort these steps. return Err(CookieError::PublicSuffix); } } } else if !cookie.domain.matches(request_url) { // If the canonicalized request-host does not domain-match the // domain-attribute: // Ignore the cookie entirely and abort these steps. return Err(CookieError::DomainMismatch); } // NB: we do not bail out above on is_expired(), as servers can remove a cookie // by sending // an expired one, so we need to do the old_cookie check below before checking // is_expired() on an incoming cookie { // At this point in parsing, any non-present Domain attribute should have been // converted into a HostOnly variant let cookie_domain = cookie .domain .as_cow() .ok_or_else(|| CookieError::UnspecifiedDomain)?; if let Some(old_cookie) = self.get_mut(&cookie_domain, &cookie.path, cookie.name()) { if old_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { // 2. If the newly created cookie was received from a "non-HTTP" // API and the old-cookie's http-only-flag is set, abort these // steps and ignore the newly created cookie entirely. return Err(CookieError::NonHttpScheme); } else if cookie.is_expired() { old_cookie.expire(); return Ok(StoreAction::ExpiredExisting); } } } if !cookie.is_expired() { Ok( if self .cookies .entry(String::from(&cookie.domain)) .or_insert_with(Map::new) .entry(String::from(&cookie.path)) .or_insert_with(Map::new) .insert(cookie.name().to_owned(), cookie) .is_none() { StoreAction::Inserted } else { StoreAction::UpdatedExisting }, ) } else { Err(CookieError::Expired) } } /// Clear the contents of the store pub fn clear(&mut self) { self.cookies.clear() } /// An iterator visiting all the __unexpired__ cookies in the store pub fn iter_unexpired<'a>(&'a self) -> impl Iterator> + 'a { self.cookies .values() .flat_map(|dcs| dcs.values()) .flat_map(|pcs| pcs.values()) .filter(|c| !c.is_expired()) } /// An iterator visiting all (including __expired__) cookies in the store pub fn iter_any<'a>(&'a self) -> impl Iterator> + 'a { self.cookies .values() .flat_map(|dcs| dcs.values()) .flat_map(|pcs| pcs.values()) } /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string` /// and write them to `writer` pub fn save(&self, writer: &mut W, cookie_to_string: F) -> StoreResult<()> where W: Write, F: Fn(&Cookie<'static>) -> Result, crate::Error: From, { for cookie in self.iter_unexpired().filter_map(|c| { if c.is_persistent() { Some(cookie_to_string(c)) } else { None } }) { writeln!(writer, "{}", cookie?)?; } Ok(()) } /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and /// write them to `writer` pub fn save_json(&self, writer: &mut W) -> StoreResult<()> { self.save(writer, ::serde_json::to_string) } /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__ /// cookies pub fn load(reader: R, cookie_from_str: F) -> StoreResult where R: BufRead, F: Fn(&str) -> Result, E>, crate::Error: From, { let cookies = reader.lines().filter_map(|line_result| { let cookie = line_result .map_err(Into::into) .and_then(|line| cookie_from_str(&line).map_err(crate::Error::from)); match cookie { Ok(c) if c.is_expired() => None, _ => Some(cookie), } }); Self::from_cookies(cookies) } /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies pub fn load_json(reader: R) -> StoreResult { CookieStore::load(reader, |cookie| ::serde_json::from_str(cookie)) } fn from_cookies(iter: I) -> Result where I: IntoIterator, E>>, { let mut cookies = Map::new(); for cookie in iter { let cookie = cookie?; if !cookie.is_expired() { cookies .entry(String::from(&cookie.domain)) .or_insert_with(Map::new) .entry(String::from(&cookie.path)) .or_insert_with(Map::new) .insert(cookie.name().to_owned(), cookie); } } Ok(Self { cookies, public_suffix_list: None, }) } } impl Serialize for CookieStore { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.collect_seq(self.iter_unexpired().filter(|c| c.is_persistent())) } } struct CookieStoreVisitor; impl<'de> Visitor<'de> for CookieStoreVisitor { type Value = CookieStore; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!(formatter, "a sequence of cookies") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { CookieStore::from_cookies(iter::from_fn(|| seq.next_element().transpose())) } } impl<'de> Deserialize<'de> for CookieStore { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_seq(CookieStoreVisitor) } } #[cfg(test)] mod tests { use super::CookieStore; use super::{InsertResult, StoreAction}; use crate::cookie::Cookie; use crate::CookieError; use ::cookie::Cookie as RawCookie; use std::str::from_utf8; use time::OffsetDateTime; use crate::utils::test as test_utils; macro_rules! has_str { ($e: expr, $i: ident) => {{ let val = from_utf8(&$i[..]).unwrap(); assert!(val.contains($e), "exp: {}\nval: {}", $e, val); }}; } macro_rules! not_has_str { ($e: expr, $i: ident) => {{ let val = from_utf8(&$i[..]).unwrap(); assert!(!val.contains($e), "exp: {}\nval: {}", $e, val); }}; } macro_rules! inserted { ($e: expr) => { assert_eq!(Ok(StoreAction::Inserted), $e) }; } macro_rules! updated { ($e: expr) => { assert_eq!(Ok(StoreAction::UpdatedExisting), $e) }; } macro_rules! expired_existing { ($e: expr) => { assert_eq!(Ok(StoreAction::ExpiredExisting), $e) }; } macro_rules! domain_mismatch { ($e: expr) => { assert_eq!(Err(CookieError::DomainMismatch), $e) }; } macro_rules! non_http_scheme { ($e: expr) => { assert_eq!(Err(CookieError::NonHttpScheme), $e) }; } macro_rules! non_rel_scheme { ($e: expr) => { assert_eq!(Err(CookieError::NonRelativeScheme), $e) }; } macro_rules! expired_err { ($e: expr) => { assert_eq!(Err(CookieError::Expired), $e) }; } macro_rules! values_are { ($store: expr, $url: expr, $values: expr) => {{ let mut matched_values = $store .matches(&test_utils::url($url)) .iter() .map(|c| &c.value()[..]) .collect::>(); matched_values.sort(); let mut values: Vec<&str> = $values; values.sort(); assert!( matched_values == values, "\n{:?}\n!=\n{:?}\n", matched_values, values ); }}; } fn add_cookie( store: &mut CookieStore, cookie: &str, url: &str, expires: Option, max_age: Option, ) -> InsertResult { store.insert( test_utils::make_cookie(cookie, url, expires, max_age), &test_utils::url(url), ) } fn make_match_store() -> CookieStore { let mut store = CookieStore::default(); inserted!(add_cookie( &mut store, "cookie1=1", "http://example.com/foo/bar", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie2=2; Secure", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie3=3; HttpOnly", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie4=4; Secure; HttpOnly", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie5=5", "http://example.com/foo/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie6=6", "http://example.com/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie7=7", "http://bar.example.com/foo/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie8=8", "http://example.org/foo/bar", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie9=9", "http://bar.example.org/foo/bar", None, Some(60 * 5), )); store } macro_rules! check_matches { ($store: expr) => {{ values_are!($store, "http://unknowndomain.org/foo/bar", vec![]); values_are!($store, "http://example.org/foo/bar", vec!["8"]); values_are!($store, "http://example.org/bus/bar", vec![]); values_are!($store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!($store, "http://bar.example.org/bus/bar", vec![]); values_are!( $store, "https://example.com/sec/foo", vec!["6", "4", "3", "2"] ); values_are!($store, "http://example.com/sec/foo", vec!["6", "3"]); values_are!($store, "ftp://example.com/sec/foo", vec!["6"]); values_are!($store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!( $store, "http://example.com/foo/bar/bus", vec!["1", "5", "6"] ); }}; } #[test] fn insert_raw() { let mut store = CookieStore::default(); inserted!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); non_rel_scheme!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("data:nonrelativescheme"), )); non_http_scheme!(store.insert_raw( &RawCookie::parse("cookie1=value1; HttpOnly").unwrap(), &test_utils::url("ftp://example.com/"), )); expired_existing!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=0").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); expired_err!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); updated!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); expired_existing!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); domain_mismatch!(store.insert_raw( &RawCookie::parse("cookie1=value1; Domain=bar.example.com").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); } #[test] fn parse() { let mut store = CookieStore::default(); inserted!(store.parse( "cookie1=value1", &test_utils::url("http://example.com/foo/bar"), )); non_rel_scheme!(store.parse("cookie1=value1", &test_utils::url("data:nonrelativescheme"),)); non_http_scheme!(store.parse( "cookie1=value1; HttpOnly", &test_utils::url("ftp://example.com/"), )); expired_existing!(store.parse( "cookie1=value1; Max-Age=0", &test_utils::url("http://example.com/foo/bar"), )); expired_err!(store.parse( "cookie1=value1; Max-Age=-1", &test_utils::url("http://example.com/foo/bar"), )); updated!(store.parse( "cookie1=value1", &test_utils::url("http://example.com/foo/bar"), )); expired_existing!(store.parse( "cookie1=value1; Max-Age=-1", &test_utils::url("http://example.com/foo/bar"), )); domain_mismatch!(store.parse( "cookie1=value1; Domain=bar.example.com", &test_utils::url("http://example.com/foo/bar"), )); } #[test] fn save() { let mut output = vec![]; let mut store = CookieStore::default(); store.save_json(&mut output).unwrap(); assert_eq!("", from_utf8(&output[..]).unwrap()); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); store.save_json(&mut output).unwrap(); assert_eq!("", from_utf8(&output[..]).unwrap()); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); output.clear(); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); output.clear(); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "https://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); output.clear(); } #[test] fn serialize() { let mut output = vec![]; let mut store = CookieStore::default(); serde_json::to_writer(&mut output, &store).unwrap(); assert_eq!("[]", from_utf8(&output[..]).unwrap()); output.clear(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); serde_json::to_writer(&mut output, &store).unwrap(); assert_eq!("[]", from_utf8(&output[..]).unwrap()); output.clear(); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); output.clear(); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); output.clear(); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "https://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); output.clear(); } #[test] fn domains() { let mut store = CookieStore::default(); // The user agent will reject cookies unless the Domain attribute // specifies a scope for the cookie that would include the origin // server. For example, the user agent will accept a cookie with a // Domain attribute of "example.com" or of "foo.example.com" from // foo.example.com, but the user agent will not accept a cookie with a // Domain attribute of "bar.example.com" or of "baz.foo.example.com". fn domain_cookie_from(domain: &str, request_url: &str) -> Cookie<'static> { let cookie_str = format!("cookie1=value1; Domain={}", domain); Cookie::parse(cookie_str, &test_utils::url(request_url)).unwrap() } { let request_url = test_utils::url("http://foo.example.com"); // foo.example.com can submit cookies for example.com and foo.example.com inserted!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); inserted!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); // not for bar.example.com domain_mismatch!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); // not for bar.foo.example.com domain_mismatch!(store.insert( domain_cookie_from("bar.foo.example.com", "http://bar.foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.foo.example.com", "http://bar.foo.example.com",), &request_url, )); } { let request_url = test_utils::url("http://bar.example.com"); // bar.example.com can submit for example.com and bar.example.com updated!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); inserted!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); // bar.example.com cannot submit for foo.example.com domain_mismatch!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); } { let request_url = test_utils::url("http://example.com"); // example.com can submit for example.com updated!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); // example.com cannot submit for foo.example.com or bar.example.com domain_mismatch!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); } } #[test] fn http_only() { let mut store = CookieStore::default(); let c = Cookie::parse( "cookie1=value1; HttpOnly", &test_utils::url("http://example.com/foo/bar"), ) .unwrap(); // cannot add a HttpOnly cookies from a non-http source non_http_scheme!(store.insert(c, &test_utils::url("ftp://example.com/foo/bar"),)); } #[test] fn load() { let mut store = CookieStore::default(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); let mut output = vec![]; store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); let store = CookieStore::load_json(&output[..]).unwrap(); assert!(store.get("example.com", "/foo", "cookie0").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); assert!( store .get("foo.example.com", "/foo/", "cookie4") .unwrap() .value() == "value4" ); assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); output.clear(); let store = make_match_store(); store.save_json(&mut output).unwrap(); let store = CookieStore::load_json(&output[..]).unwrap(); check_matches!(&store); } #[test] fn deserialize() { let mut store = CookieStore::default(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); let mut output = vec![]; serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); assert!(store.get("example.com", "/foo", "cookie0").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); assert!( store .get("foo.example.com", "/foo/", "cookie4") .unwrap() .value() == "value4" ); assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); output.clear(); let store = make_match_store(); serde_json::to_writer(&mut output, &store).unwrap(); let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); check_matches!(&store); } #[test] fn clear() { let mut output = vec![]; let mut store = CookieStore::default(); inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); store.save_json(&mut output).unwrap(); has_str!("cookie1=value1", output); output.clear(); store.clear(); store.save_json(&mut output).unwrap(); assert_eq!("", from_utf8(&output[..]).unwrap()); } #[test] fn add_and_get() { let mut store = CookieStore::default(); assert!(store.get("example.com", "/foo", "cookie1").is_none()); inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo/bar", "cookie1").is_none()); assert!(store.get("example.com", "/foo", "cookie2").is_none()); assert!(store.get("example.org", "/foo", "cookie1").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); updated!(add_cookie( &mut store, "cookie1=value2", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); inserted!(add_cookie( &mut store, "cookie2=value3", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); inserted!(add_cookie( &mut store, "cookie3=value4; HttpOnly", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); non_http_scheme!(add_cookie( &mut store, "cookie3=value5", "ftp://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); } #[test] fn matches() { let store = make_match_store(); check_matches!(&store); } #[test] fn expiry() { let mut store = make_match_store(); let request_url = test_utils::url("http://foo.example.com"); let expired_cookie = Cookie::parse("cookie1=value1; Max-Age=-1", &request_url).unwrap(); expired_err!(store.insert(expired_cookie, &request_url)); check_matches!(&store); match store.get_mut("example.com", "/", "cookie6") { Some(cookie) => cookie.expire(), None => unreachable!(), } values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1", "5"]); match store.get_any("example.com", "/", "cookie6") { Some(cookie) => assert!(cookie.is_expired()), None => unreachable!(), } // inserting an expired cookie that matches an existing cookie should expire // the existing let request_url = test_utils::url("http://example.com/foo/"); let expired_cookie = Cookie::parse("cookie5=value5; Max-Age=-1", &request_url).unwrap(); expired_existing!(store.insert(expired_cookie, &request_url)); values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); match store.get_any("example.com", "/foo", "cookie5") { Some(cookie) => assert!(cookie.is_expired()), None => unreachable!(), } // save and loading the store should drop any expired cookies let mut output = vec![]; store.save_json(&mut output).unwrap(); store = CookieStore::load_json(&output[..]).unwrap(); values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); assert!(store.get_any("example.com", "/", "cookie6").is_none()); assert!(store.get_any("example.com", "/foo", "cookie5").is_none()); } #[test] fn non_persistent() { let mut store = make_match_store(); check_matches!(&store); let request_url = test_utils::url("http://example.com/tmp/"); let non_persistent = Cookie::parse("cookie10=value10", &request_url).unwrap(); inserted!(store.insert(non_persistent, &request_url)); match store.get("example.com", "/tmp", "cookie10") { None => unreachable!(), Some(cookie) => assert_eq!("value10", cookie.value()), } // save and loading the store should drop any non-persistent cookies let mut output = vec![]; store.save_json(&mut output).unwrap(); store = CookieStore::load_json(&output[..]).unwrap(); check_matches!(&store); assert!(store.get("example.com", "/tmp", "cookie10").is_none()); assert!(store.get_any("example.com", "/tmp", "cookie10").is_none()); } macro_rules! dump { ($e: expr, $i: ident) => {{ use serde_json; println!(""); println!( "==== {}: {} ====", $e, time::OffsetDateTime::now_utc().format(crate::rfc3339_fmt::RFC3339_FORMAT) ); for c in $i.iter_any() { println!( "{} {}", if c.is_expired() { "XXXXX" } else if c.is_persistent() { "PPPPP" } else { " " }, serde_json::to_string(c).unwrap() ); println!("----------------"); } println!("================"); }}; } fn matches_are(store: &CookieStore, url: &str, exp: Vec<&str>) { let matches = store .matches(&test_utils::url(url)) .iter() .map(|c| format!("{}={}", c.name(), c.value())) .collect::>(); for e in &exp { assert!( matches.iter().any(|m| &m[..] == *e), "{}: matches missing '{}'\nmatches: {:?}\n exp: {:?}", url, e, matches, exp ); } assert!( matches.len() == exp.len(), "{}: matches={:?} != exp={:?}", url, matches, exp ); } #[test] fn domain_collisions() { let mut store = CookieStore::default(); // - HostOnly, so no collisions here inserted!(add_cookie( &mut store, "cookie1=1a", "http://foo.bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie1=1b", "http://bus.example.com/", None, None, )); // - Suffix // both cookie2's domain-match bus.example.com inserted!(add_cookie( &mut store, "cookie2=2a; Domain=bus.example.com", "http://foo.bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie2=2b; Domain=example.com", "http://bus.example.com/", None, None, )); dump!("domain_collisions", store); matches_are( &store, "http://foo.bus.example.com/", vec!["cookie1=1a", "cookie2=2a", "cookie2=2b"], ); matches_are( &store, "http://bus.example.com/", vec!["cookie1=1b", "cookie2=2a", "cookie2=2b"], ); matches_are(&store, "http://example.com/", vec!["cookie2=2b"]); matches_are(&store, "http://foo.example.com/", vec!["cookie2=2b"]); } #[test] fn path_collisions() { let mut store = CookieStore::default(); // will be default-path: /foo/bar, and /foo, resp. // both should match /foo/bar/ inserted!(add_cookie( &mut store, "cookie3=3a", "http://bus.example.com/foo/bar/", None, None, )); inserted!(add_cookie( &mut store, "cookie3=3b", "http://bus.example.com/foo/", None, None, )); // - Path set explicitly inserted!(add_cookie( &mut store, "cookie4=4a; Path=/foo/bar/", "http://bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie4=4b; Path=/foo/", "http://bus.example.com/", None, None, )); dump!("path_collisions", store); matches_are( &store, "http://bus.example.com/foo/bar/", vec!["cookie3=3a", "cookie3=3b", "cookie4=4a", "cookie4=4b"], ); // Agrees w/ chrome, but not FF... FF also sends cookie4=4a, but this should be // a path-match // fail since request-uri /foo/bar is a *prefix* of the cookie path /foo/bar/ matches_are( &store, "http://bus.example.com/foo/bar", vec!["cookie3=3a", "cookie3=3b", "cookie4=4b"], ); matches_are( &store, "http://bus.example.com/foo/ba", vec!["cookie3=3b", "cookie4=4b"], ); matches_are( &store, "http://bus.example.com/foo/", vec!["cookie3=3b", "cookie4=4b"], ); // Agrees w/ chrome, but not FF... FF also sends cookie4=4b, but this should be // a path-match // fail since request-uri /foo is a *prefix* of the cookie path /foo/ matches_are(&store, "http://bus.example.com/foo", vec!["cookie3=3b"]); matches_are(&store, "http://bus.example.com/fo", vec![]); matches_are(&store, "http://bus.example.com/", vec![]); matches_are(&store, "http://bus.example.com", vec![]); } }