use std;
use cookie::Cookie as RawCookie;
use idna;
#[cfg(feature = "public_suffix")]
use publicsuffix::{List, Psl, Suffix};
use serde_derive::{Deserialize, Serialize};
use std::convert::TryFrom;
use url::{Host, Url};
use crate::utils::is_host_name;
use crate::CookieError;
pub fn is_match(domain: &str, request_url: &Url) -> bool {
CookieDomain::try_from(domain)
.map(|domain| domain.matches(request_url))
.unwrap_or(false)
}
#[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum CookieDomain {
HostOnly(String),
Suffix(String),
NotPresent,
Empty,
}
impl CookieDomain {
pub fn host_only(request_url: &Url) -> Result<CookieDomain, CookieError> {
request_url
.host()
.ok_or(CookieError::NonRelativeScheme)
.map(|h| match h {
Host::Domain(d) => CookieDomain::HostOnly(d.into()),
Host::Ipv4(addr) => CookieDomain::HostOnly(format!("{}", addr)),
Host::Ipv6(addr) => CookieDomain::HostOnly(format!("[{}]", addr)),
})
}
pub fn matches(&self, request_url: &Url) -> bool {
if let Some(url_host) = request_url.host_str() {
match *self {
CookieDomain::HostOnly(ref host) => host == url_host,
CookieDomain::Suffix(ref suffix) => {
suffix == url_host
|| (is_host_name(url_host)
&& url_host.ends_with(suffix)
&& url_host[(url_host.len() - suffix.len() - 1)..].starts_with('.'))
}
CookieDomain::NotPresent | CookieDomain::Empty => false, }
} else {
false }
}
pub fn host_is_identical(&self, request_url: &Url) -> bool {
if let Some(url_host) = request_url.host_str() {
match *self {
CookieDomain::HostOnly(ref host) => host == url_host,
CookieDomain::Suffix(ref suffix) => suffix == url_host,
CookieDomain::NotPresent | CookieDomain::Empty => false, }
} else {
false }
}
#[cfg(feature = "public_suffix")]
pub fn is_public_suffix(&self, psl: &List) -> bool {
if let Some(domain) = self.as_cow().as_ref().map(|d| d.as_bytes()) {
psl.suffix(domain)
.filter(Suffix::is_known)
.filter(|suffix| suffix == &domain)
.is_some()
} else {
false
}
}
pub fn as_cow(&self) -> Option<std::borrow::Cow<'_, str>> {
match *self {
CookieDomain::HostOnly(ref s) | CookieDomain::Suffix(ref s) => {
Some(std::borrow::Cow::Borrowed(s))
}
CookieDomain::Empty | CookieDomain::NotPresent => None,
}
}
}
impl<'a> TryFrom<&'a str> for CookieDomain {
type Error = crate::Error;
fn try_from(value: &str) -> Result<CookieDomain, Self::Error> {
idna::domain_to_ascii(value.trim())
.map_err(super::IdnaErrors::from)
.map_err(Into::into)
.map(|domain| {
if domain.is_empty() || "." == domain {
CookieDomain::Empty
} else if domain.starts_with('.') {
CookieDomain::Suffix(String::from(&domain[1..]))
} else {
CookieDomain::Suffix(domain)
}
})
}
}
impl<'a, 'c> TryFrom<&'a RawCookie<'c>> for CookieDomain {
type Error = crate::Error;
fn try_from(cookie: &'a RawCookie<'c>) -> Result<CookieDomain, Self::Error> {
if let Some(domain) = cookie.domain() {
idna::domain_to_ascii(domain.trim())
.map_err(super::IdnaErrors::from)
.map_err(Into::into)
.map(|domain| {
if domain.is_empty() {
CookieDomain::Empty
} else {
CookieDomain::Suffix(domain)
}
})
} else {
Ok(CookieDomain::NotPresent)
}
}
}
impl<'a> From<&'a CookieDomain> for String {
fn from(c: &'a CookieDomain) -> String {
match *c {
CookieDomain::HostOnly(ref h) => h.to_owned(),
CookieDomain::Suffix(ref s) => s.to_owned(),
CookieDomain::Empty | CookieDomain::NotPresent => "".to_owned(),
}
}
}
#[cfg(test)]
mod tests {
use cookie::Cookie as RawCookie;
use std::convert::TryFrom;
use url::Url;
use super::CookieDomain;
use crate::utils::test::*;
#[inline]
fn matches(expected: bool, cookie_domain: &CookieDomain, url: &str) {
let url = Url::parse(url).unwrap();
assert!(
expected == cookie_domain.matches(&url),
"cookie_domain: {:?} url: {:?}, url.host_str(): {:?}",
cookie_domain,
url,
url.host_str()
);
}
#[inline]
fn variants(expected: bool, cookie_domain: &CookieDomain, url: &str) {
matches(expected, cookie_domain, url);
matches(expected, cookie_domain, &format!("{}/", url));
matches(expected, cookie_domain, &format!("{}:8080", url));
matches(expected, cookie_domain, &format!("{}/foo/bar", url));
matches(expected, cookie_domain, &format!("{}:8080/foo/bar", url));
}
#[test]
fn matches_hostonly() {
{
let url = url("http://example.com");
let host_name = CookieDomain::host_only(&url).expect("unable to parse domain");
matches(false, &host_name, "data:nonrelative");
variants(true, &host_name, "http://example.com");
variants(false, &host_name, "http://example.org");
variants(false, &host_name, "http://foo.example.com");
variants(false, &host_name, "http://127.0.0.1");
variants(false, &host_name, "http://[::1]");
}
{
let url = url("http://127.0.0.1");
let ip4 = CookieDomain::host_only(&url).expect("unable to parse Ipv4");
matches(false, &ip4, "data:nonrelative");
variants(true, &ip4, "http://127.0.0.1");
variants(false, &ip4, "http://[::1]");
}
{
let url = url("http://[::1]");
let ip6 = CookieDomain::host_only(&url).expect("unable to parse Ipv6");
matches(false, &ip6, "data:nonrelative");
variants(false, &ip6, "http://127.0.0.1");
variants(true, &ip6, "http://[::1]");
}
}
#[test]
fn from_strs() {
assert_eq!(
CookieDomain::Empty,
CookieDomain::try_from("").expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Empty,
CookieDomain::try_from(".").expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from(".")),
CookieDomain::try_from("..").expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from("example.com")),
CookieDomain::try_from("example.com").expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from("example.com")),
CookieDomain::try_from(".example.com").expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from(".example.com")),
CookieDomain::try_from("..example.com").expect("unable to parse domain")
);
}
#[test]
fn from_raw_cookie() {
fn raw_cookie(s: &str) -> RawCookie<'_> {
RawCookie::parse(s).unwrap()
}
assert_eq!(
CookieDomain::NotPresent,
CookieDomain::try_from(&raw_cookie("cookie=value")).expect("unable to parse domain")
);
assert_eq!(
CookieDomain::NotPresent,
CookieDomain::try_from(&raw_cookie("cookie=value; Domain="))
.expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Empty,
CookieDomain::try_from(&raw_cookie("cookie=value; Domain=."))
.expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from("example.com")),
CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.example.com"))
.expect("unable to parse domain")
);
assert_eq!(
CookieDomain::Suffix(String::from("example.com")),
CookieDomain::try_from(&raw_cookie("cookie=value; Domain=example.com"))
.expect("unable to parse domain")
);
}
#[test]
fn matches_suffix() {
{
let suffix = CookieDomain::try_from("example.com").expect("unable to parse domain");
variants(true, &suffix, "http://example.com"); variants(true, &suffix, "http://foo.example.com"); variants(false, &suffix, "http://example.org"); variants(false, &suffix, "http://xample.com"); variants(false, &suffix, "http://fooexample.com"); }
{
let suffix = CookieDomain::try_from(".example.com").expect("unable to parse domain");
variants(true, &suffix, "http://example.com");
variants(true, &suffix, "http://foo.example.com");
variants(false, &suffix, "http://example.org");
variants(false, &suffix, "http://xample.com");
variants(false, &suffix, "http://fooexample.com");
}
{
let suffix = CookieDomain::try_from("..example.com").expect("unable to parse domain");
variants(true, &suffix, "http://.example.com");
variants(true, &suffix, "http://foo..example.com");
variants(false, &suffix, "http://example.com");
variants(false, &suffix, "http://foo.example.com");
variants(false, &suffix, "http://example.org");
variants(false, &suffix, "http://xample.com");
variants(false, &suffix, "http://fooexample.com");
}
{
let suffix = CookieDomain::try_from("127.0.0.1").expect("unable to parse Ipv4");
variants(true, &suffix, "http://127.0.0.1");
}
{
let suffix = CookieDomain::try_from("[::1]").expect("unable to parse Ipv6");
variants(true, &suffix, "http://[::1]");
}
{
let suffix = CookieDomain::try_from("0.0.1").expect("unable to parse Ipv4");
variants(false, &suffix, "http://127.0.0.1");
}
}
}
#[cfg(test)]
mod serde_tests {
use serde_json;
use std::convert::TryFrom;
use crate::cookie_domain::CookieDomain;
use crate::utils::test::*;
fn encode_decode(cd: &CookieDomain, exp_json: &str) {
let encoded = serde_json::to_string(cd).unwrap();
assert!(
exp_json == encoded,
"expected: '{}'\n encoded: '{}'",
exp_json,
encoded
);
let decoded: CookieDomain = serde_json::from_str(&encoded).unwrap();
assert!(
*cd == decoded,
"expected: '{:?}'\n decoded: '{:?}'",
cd,
decoded
);
}
#[test]
fn serde() {
let url = url("http://example.com");
encode_decode(
&CookieDomain::host_only(&url).expect("cannot parse domain"),
"{\"HostOnly\":\"example.com\"}",
);
encode_decode(
&CookieDomain::try_from(".example.com").expect("cannot parse domain"),
"{\"Suffix\":\"example.com\"}",
);
encode_decode(&CookieDomain::NotPresent, "\"NotPresent\"");
encode_decode(&CookieDomain::Empty, "\"Empty\"");
}
}