use crate::cookie_domain::CookieDomain;
use crate::cookie_expiration::CookieExpiration;
use crate::cookie_path::CookiePath;
use crate::utils::{is_http_scheme, is_secure};
use cookie::{Cookie as RawCookie, CookieBuilder as RawCookieBuilder, ParseError};
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt;
use std::ops::Deref;
use time;
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NonHttpScheme,
NonRelativeScheme,
DomainMismatch,
Expired,
Parse,
#[cfg(feature = "public_suffix")]
PublicSuffix,
UnspecifiedDomain,
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
Error::NonHttpScheme =>
"request-uri is not an http scheme but HttpOnly attribute set",
Error::NonRelativeScheme => {
"request-uri is not a relative scheme; cannot determine host"
}
Error::DomainMismatch => "request-uri does not domain-match the cookie",
Error::Expired => "attempted to utilize an Expired Cookie",
Error::Parse => "unable to parse string as cookie::Cookie",
#[cfg(feature = "public_suffix")]
Error::PublicSuffix => "domain-attribute value is a public suffix",
Error::UnspecifiedDomain => "domain-attribute is not specified",
}
)
}
}
impl From<ParseError> for Error {
fn from(_: ParseError) -> Error {
Error::Parse
}
}
pub type CookieResult<'a> = Result<Cookie<'a>, Error>;
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct Cookie<'a> {
#[serde(serialize_with = "serde_raw_cookie::serialize")]
#[serde(deserialize_with = "serde_raw_cookie::deserialize")]
raw_cookie: RawCookie<'a>,
pub path: CookiePath,
pub domain: CookieDomain,
pub expires: CookieExpiration,
}
mod serde_raw_cookie {
use cookie::Cookie as RawCookie;
use serde::de::Error;
use serde::de::Unexpected;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
pub fn serialize<S>(cookie: &RawCookie<'_>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
cookie.to_string().serialize(serializer)
}
pub fn deserialize<'a, D>(deserializer: D) -> Result<RawCookie<'static>, D::Error>
where
D: Deserializer<'a>,
{
let cookie = String::deserialize(deserializer)?;
match RawCookie::from_str(&cookie) {
Ok(cookie) => Ok(cookie),
Err(_) => Err(D::Error::invalid_value(
Unexpected::Str(&cookie),
&"a cookie string",
)),
}
}
}
impl<'a> Cookie<'a> {
pub fn matches(&self, request_url: &Url) -> bool {
self.path.matches(request_url)
&& self.domain.matches(request_url)
&& (!self.raw_cookie.secure().unwrap_or(false) || is_secure(request_url))
&& (!self.raw_cookie.http_only().unwrap_or(false) || is_http_scheme(request_url))
}
pub fn is_persistent(&self) -> bool {
match self.expires {
CookieExpiration::AtUtc(_) => true,
CookieExpiration::SessionEnd => false,
}
}
pub fn expire(&mut self) {
self.expires = CookieExpiration::from(0u64);
}
pub fn is_expired(&self) -> bool {
self.expires.is_expired()
}
pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool {
self.expires.expires_by(utc_tm)
}
pub fn parse<S>(cookie_str: S, request_url: &Url) -> CookieResult<'a>
where
S: Into<Cow<'a, str>>,
{
Cookie::try_from_raw_cookie(&RawCookie::parse(cookie_str)?, request_url)
}
pub fn try_from_raw_cookie(raw_cookie: &RawCookie<'a>, request_url: &Url) -> CookieResult<'a> {
if raw_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) {
return Err(Error::NonHttpScheme);
}
let domain = match CookieDomain::try_from(raw_cookie) {
Ok(d @ CookieDomain::Suffix(_)) => {
if !d.matches(request_url) {
Err(Error::DomainMismatch)
} else {
Ok(d)
}
}
Err(_) => Err(Error::Parse),
_ => CookieDomain::host_only(request_url),
}?;
let path = raw_cookie
.path()
.as_ref()
.and_then(|p| CookiePath::parse(p))
.unwrap_or_else(|| CookiePath::default_path(request_url));
let expires = if let Some(max_age) = raw_cookie.max_age() {
CookieExpiration::from(max_age)
} else if let Some(expiration) = raw_cookie.expires() {
CookieExpiration::from(expiration)
} else {
CookieExpiration::SessionEnd
};
Ok(Cookie {
raw_cookie: raw_cookie.clone(),
path,
expires,
domain,
})
}
pub fn into_owned(self) -> Cookie<'static> {
Cookie {
raw_cookie: self.raw_cookie.into_owned(),
path: self.path,
domain: self.domain,
expires: self.expires,
}
}
}
impl<'a> Deref for Cookie<'a> {
type Target = RawCookie<'a>;
fn deref(&self) -> &Self::Target {
&self.raw_cookie
}
}
impl<'a> From<Cookie<'a>> for RawCookie<'a> {
fn from(cookie: Cookie<'a>) -> RawCookie<'static> {
let mut builder =
RawCookieBuilder::new(cookie.name().to_owned(), cookie.value().to_owned());
match cookie.expires {
CookieExpiration::AtUtc(utc_tm) => {
builder = builder.expires(utc_tm);
}
CookieExpiration::SessionEnd => {}
}
if cookie.path.is_from_path_attr() {
builder = builder.path(String::from(cookie.path));
}
if let CookieDomain::Suffix(s) = cookie.domain {
builder = builder.domain(s);
}
builder.finish()
}
}
#[cfg(test)]
mod tests {
use super::Cookie;
use crate::cookie_domain::CookieDomain;
use crate::cookie_expiration::CookieExpiration;
use cookie::Cookie as RawCookie;
use time::{Duration, OffsetDateTime};
use url::Url;
use crate::utils::test as test_utils;
fn cmp_domain(cookie: &str, url: &str, exp: CookieDomain) {
let ua = test_utils::make_cookie(cookie, url, None, None);
assert!(ua.domain == exp, "\n{:?}", ua);
}
#[test]
fn no_domain() {
let url = test_utils::url("http://example.com/foo/bar");
cmp_domain(
"cookie1=value1",
"http://example.com/foo/bar",
CookieDomain::host_only(&url).expect("unable to parse domain"),
);
}
#[test]
fn empty_domain() {
let url = test_utils::url("http://example.com/foo/bar");
cmp_domain(
"cookie1=value1; Domain=",
"http://example.com/foo/bar",
CookieDomain::host_only(&url).expect("unable to parse domain"),
);
}
#[test]
fn mismatched_domain() {
let ua = Cookie::parse(
"cookie1=value1; Domain=notmydomain.com",
&test_utils::url("http://example.com/foo/bar"),
);
assert!(ua.is_err(), "{:?}", ua);
}
#[test]
fn domains() {
fn domain_from(domain: &str, request_url: &str, is_some: bool) {
let cookie_str = format!("cookie1=value1; Domain={}", domain);
let raw_cookie = RawCookie::parse(cookie_str).unwrap();
let cookie = Cookie::try_from_raw_cookie(&raw_cookie, &test_utils::url(request_url));
assert_eq!(is_some, cookie.is_ok())
}
domain_from("example.com", "http://foo.example.com", true);
domain_from(".example.com", "http://foo.example.com", true);
domain_from("foo.example.com", "http://foo.example.com", true);
domain_from(".foo.example.com", "http://foo.example.com", true);
domain_from("oo.example.com", "http://foo.example.com", false);
domain_from("myexample.com", "http://foo.example.com", false);
domain_from("bar.example.com", "http://foo.example.com", false);
domain_from("baz.foo.example.com", "http://foo.example.com", false);
}
#[test]
fn httponly() {
let c = RawCookie::parse("cookie1=value1; HttpOnly").unwrap();
let url = Url::parse("ftp://example.com/foo/bar").unwrap();
let ua = Cookie::try_from_raw_cookie(&c, &url);
assert!(ua.is_err(), "{:?}", ua);
}
#[test]
fn identical_domain() {
cmp_domain(
"cookie1=value1; Domain=example.com",
"http://example.com/foo/bar",
CookieDomain::Suffix(String::from("example.com")),
);
}
#[test]
fn identical_domain_leading_dot() {
cmp_domain(
"cookie1=value1; Domain=.example.com",
"http://example.com/foo/bar",
CookieDomain::Suffix(String::from("example.com")),
);
}
#[test]
fn identical_domain_two_leading_dots() {
cmp_domain(
"cookie1=value1; Domain=..example.com",
"http://..example.com/foo/bar",
CookieDomain::Suffix(String::from(".example.com")),
);
}
#[test]
fn upper_case_domain() {
cmp_domain(
"cookie1=value1; Domain=EXAMPLE.com",
"http://example.com/foo/bar",
CookieDomain::Suffix(String::from("example.com")),
);
}
fn cmp_path(cookie: &str, url: &str, exp: &str) {
let ua = test_utils::make_cookie(cookie, url, None, None);
assert!(String::from(ua.path.clone()) == exp, "\n{:?}", ua);
}
#[test]
fn no_path() {
cmp_path("cookie1=value1", "http://example.com/foo/bar/", "/foo/bar");
cmp_path("cookie1=value1", "http://example.com/foo/bar", "/foo");
cmp_path("cookie1=value1", "http://example.com/foo", "/");
cmp_path("cookie1=value1", "http://example.com/", "/");
cmp_path("cookie1=value1", "http://example.com", "/");
}
#[test]
fn empty_path() {
cmp_path(
"cookie1=value1; Path=",
"http://example.com/foo/bar/",
"/foo/bar",
);
cmp_path(
"cookie1=value1; Path=",
"http://example.com/foo/bar",
"/foo",
);
cmp_path("cookie1=value1; Path=", "http://example.com/foo", "/");
cmp_path("cookie1=value1; Path=", "http://example.com/", "/");
cmp_path("cookie1=value1; Path=", "http://example.com", "/");
}
#[test]
fn invalid_path() {
cmp_path(
"cookie1=value1; Path=baz",
"http://example.com/foo/bar/",
"/foo/bar",
);
cmp_path(
"cookie1=value1; Path=baz",
"http://example.com/foo/bar",
"/foo",
);
cmp_path("cookie1=value1; Path=baz", "http://example.com/foo", "/");
cmp_path("cookie1=value1; Path=baz", "http://example.com/", "/");
cmp_path("cookie1=value1; Path=baz", "http://example.com", "/");
}
#[test]
fn path() {
cmp_path(
"cookie1=value1; Path=/baz",
"http://example.com/foo/bar/",
"/baz",
);
cmp_path(
"cookie1=value1; Path=/baz/",
"http://example.com/foo/bar/",
"/baz/",
);
}
#[inline]
fn in_days(days: i64) -> OffsetDateTime {
OffsetDateTime::now_utc() + Duration::days(days)
}
#[inline]
fn in_minutes(mins: i64) -> OffsetDateTime {
OffsetDateTime::now_utc() + Duration::minutes(mins)
}
#[test]
fn max_age_bounds() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
None,
Some(9223372036854776),
);
assert!(match ua.expires {
CookieExpiration::AtUtc(_) => true,
_ => false,
});
}
#[test]
fn max_age() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
None,
Some(60),
);
assert!(!ua.is_expired());
assert!(ua.expires_by(&in_minutes(2)));
}
#[test]
fn expired() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
None,
Some(0u64),
);
assert!(ua.is_expired());
assert!(ua.expires_by(&in_days(-1)));
let ua = test_utils::make_cookie(
"cookie1=value1; Max-Age=0",
"http://example.com/foo/bar",
None,
None,
);
assert!(ua.is_expired());
assert!(ua.expires_by(&in_days(-1)));
let ua = test_utils::make_cookie(
"cookie1=value1; Max-Age=-1",
"http://example.com/foo/bar",
None,
None,
);
assert!(ua.is_expired());
assert!(ua.expires_by(&in_days(-1)));
}
#[test]
fn session_end() {
let ua =
test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None);
assert!(match ua.expires {
CookieExpiration::SessionEnd => true,
_ => false,
});
assert!(!ua.is_expired());
assert!(!ua.expires_by(&in_days(1)));
assert!(!ua.expires_by(&in_days(-1)));
}
#[test]
fn expires_tmrw_at_utc() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
Some(in_days(1)),
None,
);
assert!(!ua.is_expired());
assert!(ua.expires_by(&in_days(2)));
}
#[test]
fn expired_yest_at_utc() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
Some(in_days(-1)),
None,
);
assert!(ua.is_expired());
assert!(!ua.expires_by(&in_days(-2)));
}
#[test]
fn is_persistent() {
let ua =
test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None);
assert!(!ua.is_persistent()); let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
Some(in_days(1)),
None,
);
assert!(ua.is_persistent()); let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
Some(in_days(1)),
Some(60),
);
assert!(ua.is_persistent()); }
#[test]
fn max_age_overrides_expires() {
let ua = test_utils::make_cookie(
"cookie1=value1",
"http://example.com/foo/bar",
Some(in_days(-1)),
Some(60),
);
assert!(!ua.is_expired());
assert!(ua.expires_by(&in_minutes(2)));
}
#[test]
fn matches() {
fn do_match(exp: bool, cookie: &str, src_url: &str, request_url: Option<&str>) {
let ua = test_utils::make_cookie(cookie, src_url, None, None);
let request_url = request_url.unwrap_or(src_url);
assert!(
exp == ua.matches(&Url::parse(request_url).unwrap()),
"\n>> {:?}\nshould{}match\n>> {:?}\n",
ua,
if exp { " " } else { " NOT " },
request_url
);
}
fn is_match(cookie: &str, url: &str, request_url: Option<&str>) {
do_match(true, cookie, url, request_url);
}
fn is_mismatch(cookie: &str, url: &str, request_url: Option<&str>) {
do_match(false, cookie, url, request_url);
}
is_match("cookie1=value1", "http://example.com/foo/bar", None);
is_mismatch(
"cookie1=value1",
"http://example.com/bus/baz/",
Some("http://example.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; Path=/bus/baz",
"http://example.com/foo/bar",
None,
);
is_match(
"cookie1=value1",
"http://example.com/foo/bar",
Some("http://example.com/foo/bar"),
);
is_match(
"cookie1=value1; Path=/foo/",
"http://example.com/foo/bar",
None,
);
is_mismatch(
"cookie1=value1",
"http://example.com/fo/",
Some("http://example.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; Path=/fo",
"http://example.com/foo/bar",
None,
);
is_match(
"cookie1=value1",
"http://example.com/foo/",
Some("http://example.com/foo/bar"),
);
is_match(
"cookie1=value1; Path=/foo",
"http://example.com/foo/bar",
None,
);
is_match(
"cookie1=value1; Path=/",
"http://example.com/foo/bar",
Some("http://example.com/bus/baz"),
);
is_mismatch(
"cookie1=value1",
"http://example.com/foo/",
Some("http://notmydomain.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; Domain=example.com",
"http://foo.example.com/foo/",
Some("http://notmydomain.com/foo/bar"),
);
is_match(
"cookie1=value1; Secure",
"http://example.com/foo/bar",
Some("https://example.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; Secure",
"http://example.com/foo/bar",
Some("http://example.com/foo/bar"),
);
is_match(
"cookie1=value1",
"http://example.com/foo/bar",
Some("ftp://example.com/foo/bar"),
);
is_match(
"cookie1=value1; HttpOnly",
"http://example.com/foo/bar",
Some("http://example.com/foo/bar"),
);
is_match(
"cookie1=value1; HttpOnly",
"http://example.com/foo/bar",
Some("HTTP://example.com/foo/bar"),
);
is_match(
"cookie1=value1; HttpOnly",
"http://example.com/foo/bar",
Some("https://example.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; HttpOnly",
"http://example.com/foo/bar",
Some("ftp://example.com/foo/bar"),
);
is_mismatch(
"cookie1=value1; HttpOnly",
"http://example.com/foo/bar",
Some("data:nonrelativescheme"),
);
}
}
#[cfg(test)]
mod serde_tests {
use crate::cookie::Cookie;
use crate::cookie_expiration::CookieExpiration;
use crate::utils::test as test_utils;
use crate::utils::test::*;
use serde_json::json;
use time;
fn encode_decode(c: &Cookie<'_>, expected: serde_json::Value) {
let encoded = serde_json::to_value(c).unwrap();
assert_eq!(
expected,
encoded,
"\nexpected: '{}'\n encoded: '{}'",
expected.to_string(),
encoded.to_string()
);
let decoded: Cookie<'_> = serde_json::from_value(encoded).unwrap();
assert_eq!(
*c,
decoded,
"\nexpected: '{}'\n decoded: '{}'",
c.to_string(),
decoded.to_string()
);
}
#[test]
fn serde() {
encode_decode(
&test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None),
json!({
"raw_cookie": "cookie1=value1",
"path": ["/foo", false],
"domain": { "HostOnly": "example.com" },
"expires": "SessionEnd"
}),
);
encode_decode(
&test_utils::make_cookie(
"cookie2=value2; Domain=example.com",
"http://foo.example.com/foo/bar",
None,
None,
),
json!({
"raw_cookie": "cookie2=value2; Domain=example.com",
"path": ["/foo", false],
"domain": { "Suffix": "example.com" },
"expires": "SessionEnd"
}),
);
encode_decode(
&test_utils::make_cookie(
"cookie3=value3; Path=/foo/bar",
"http://foo.example.com/foo",
None,
None,
),
json!({
"raw_cookie": "cookie3=value3; Path=/foo/bar",
"path": ["/foo/bar", true],
"domain": { "HostOnly": "foo.example.com" },
"expires": "SessionEnd",
}),
);
let at_utc = time::macros::date!(2015 - 08 - 11)
.with_time(time::macros::time!(16:41:42))
.assume_utc();
encode_decode(
&test_utils::make_cookie(
"cookie4=value4",
"http://example.com/foo/bar",
Some(at_utc),
None,
),
json!({
"raw_cookie": "cookie4=value4; Expires=Tue, 11 Aug 2015 16:41:42 GMT",
"path": ["/foo", false],
"domain": { "HostOnly": "example.com" },
"expires": { "AtUtc": at_utc.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
}),
);
let expires = test_utils::make_cookie(
"cookie5=value5",
"http://example.com/foo/bar",
Some(in_minutes(10)),
None,
);
let utc_tm = match expires.expires {
CookieExpiration::AtUtc(ref utc_tm) => utc_tm,
CookieExpiration::SessionEnd => unreachable!(),
};
let utc_formatted = utc_tm
.format(&time::format_description::well_known::Rfc2822)
.unwrap()
.to_string()
.replace("+0000", "GMT");
let raw_cookie_value = format!("cookie5=value5; Expires={utc_formatted}");
encode_decode(
&expires,
json!({
"raw_cookie": raw_cookie_value,
"path":["/foo", false],
"domain": { "HostOnly": "example.com" },
"expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
}),
);
dbg!(&at_utc);
let max_age = test_utils::make_cookie(
"cookie6=value6",
"http://example.com/foo/bar",
Some(at_utc),
Some(10),
);
dbg!(&max_age);
let utc_tm = match max_age.expires {
CookieExpiration::AtUtc(ref utc_tm) => time::OffsetDateTime::parse(
&utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap(),
&time::format_description::well_known::Rfc3339,
)
.expect("could not re-parse time"),
CookieExpiration::SessionEnd => unreachable!(),
};
dbg!(&utc_tm);
encode_decode(
&max_age,
json!({
"raw_cookie": "cookie6=value6; Max-Age=10; Expires=Tue, 11 Aug 2015 16:41:42 GMT",
"path":["/foo", false],
"domain": { "HostOnly": "example.com" },
"expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
}),
);
let max_age = test_utils::make_cookie(
"cookie7=value7",
"http://example.com/foo/bar",
None,
Some(10),
);
let utc_tm = match max_age.expires {
CookieExpiration::AtUtc(ref utc_tm) => utc_tm,
CookieExpiration::SessionEnd => unreachable!(),
};
encode_decode(
&max_age,
json!({
"raw_cookie": "cookie7=value7; Max-Age=10",
"path":["/foo", false],
"domain": { "HostOnly": "example.com" },
"expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() },
}),
);
}
}