1use std;
2
3use cookie::Cookie as RawCookie;
4use idna;
5#[cfg(feature = "public_suffix")]
6use publicsuffix::{List, Psl, Suffix};
7#[cfg(feature = "serde")]
8use serde_derive::{Deserialize, Serialize};
9use std::convert::TryFrom;
10use url::{Host, Url};
11
12use crate::utils::is_host_name;
13use crate::CookieError;
14
15pub fn is_match(domain: &str, request_url: &Url) -> bool {
16 CookieDomain::try_from(domain)
17 .map(|domain| domain.matches(request_url))
18 .unwrap_or(false)
19}
20
21#[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24pub enum CookieDomain {
25 HostOnly(String),
27 Suffix(String),
29 NotPresent,
31 Empty,
34}
35
36impl CookieDomain {
55 pub fn host_only(request_url: &Url) -> Result<CookieDomain, CookieError> {
58 request_url
59 .host()
60 .ok_or(CookieError::NonRelativeScheme)
61 .map(|h| match h {
62 Host::Domain(d) => CookieDomain::HostOnly(d.into()),
63 Host::Ipv4(addr) => CookieDomain::HostOnly(format!("{}", addr)),
64 Host::Ipv6(addr) => CookieDomain::HostOnly(format!("[{}]", addr)),
65 })
66 }
67
68 pub fn matches(&self, request_url: &Url) -> bool {
70 if let Some(url_host) = request_url.host_str() {
71 match *self {
72 CookieDomain::HostOnly(ref host) => host == url_host,
73 CookieDomain::Suffix(ref suffix) => {
74 suffix == url_host
75 || (is_host_name(url_host)
76 && url_host.ends_with(suffix)
77 && url_host[(url_host.len() - suffix.len() - 1)..].starts_with('.'))
78 }
79 CookieDomain::NotPresent | CookieDomain::Empty => false, }
81 } else {
82 false }
84 }
85
86 pub fn host_is_identical(&self, request_url: &Url) -> bool {
88 if let Some(url_host) = request_url.host_str() {
89 match *self {
90 CookieDomain::HostOnly(ref host) => host == url_host,
91 CookieDomain::Suffix(ref suffix) => suffix == url_host,
92 CookieDomain::NotPresent | CookieDomain::Empty => false, }
94 } else {
95 false }
97 }
98
99 #[cfg(feature = "public_suffix")]
102 pub fn is_public_suffix(&self, psl: &List) -> bool {
103 if let Some(domain) = self.as_cow().as_ref().map(|d| d.as_bytes()) {
104 psl.suffix(domain)
105 .filter(Suffix::is_known)
108 .filter(|suffix| suffix == &domain)
109 .is_some()
110 } else {
111 false
112 }
113 }
114
115 pub fn as_cow(&self) -> Option<std::borrow::Cow<'_, str>> {
118 match *self {
119 CookieDomain::HostOnly(ref s) | CookieDomain::Suffix(ref s) => {
120 Some(std::borrow::Cow::Borrowed(s))
121 }
122 CookieDomain::Empty | CookieDomain::NotPresent => None,
123 }
124 }
125}
126
127impl<'a> TryFrom<&'a str> for CookieDomain {
130 type Error = crate::Error;
131 fn try_from(value: &str) -> Result<CookieDomain, Self::Error> {
132 idna::domain_to_ascii(value.trim())
133 .map_err(super::IdnaErrors::from)
134 .map_err(Into::into)
135 .map(|domain| {
136 if domain.is_empty() || "." == domain {
137 CookieDomain::Empty
138 } else if domain.starts_with('.') {
139 CookieDomain::Suffix(String::from(&domain[1..]))
140 } else {
141 CookieDomain::Suffix(domain)
142 }
143 })
144 }
145}
146
147impl<'a, 'c> TryFrom<&'a RawCookie<'c>> for CookieDomain {
154 type Error = crate::Error;
155 fn try_from(cookie: &'a RawCookie<'c>) -> Result<CookieDomain, Self::Error> {
156 if let Some(domain) = cookie.domain() {
157 idna::domain_to_ascii(domain.trim())
158 .map_err(super::IdnaErrors::from)
159 .map_err(Into::into)
160 .map(|domain| {
161 if domain.is_empty() {
162 CookieDomain::Empty
163 } else {
164 CookieDomain::Suffix(domain)
165 }
166 })
167 } else {
168 Ok(CookieDomain::NotPresent)
169 }
170 }
171}
172
173impl<'a> From<&'a CookieDomain> for String {
174 fn from(c: &'a CookieDomain) -> String {
175 match *c {
176 CookieDomain::HostOnly(ref h) => h.to_owned(),
177 CookieDomain::Suffix(ref s) => s.to_owned(),
178 CookieDomain::Empty | CookieDomain::NotPresent => "".to_owned(),
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use cookie::Cookie as RawCookie;
186 use std::convert::TryFrom;
187 use url::Url;
188
189 use super::CookieDomain;
190 use crate::utils::test::*;
191
192 #[inline]
193 fn matches(expected: bool, cookie_domain: &CookieDomain, url: &str) {
194 let url = Url::parse(url).unwrap();
195 assert!(
196 expected == cookie_domain.matches(&url),
197 "cookie_domain: {:?} url: {:?}, url.host_str(): {:?}",
198 cookie_domain,
199 url,
200 url.host_str()
201 );
202 }
203
204 #[inline]
205 fn variants(expected: bool, cookie_domain: &CookieDomain, url: &str) {
206 matches(expected, cookie_domain, url);
207 matches(expected, cookie_domain, &format!("{}/", url));
208 matches(expected, cookie_domain, &format!("{}:8080", url));
209 matches(expected, cookie_domain, &format!("{}/foo/bar", url));
210 matches(expected, cookie_domain, &format!("{}:8080/foo/bar", url));
211 }
212
213 #[test]
214 fn matches_hostonly() {
215 {
216 let url = url("http://example.com");
217 let host_name = CookieDomain::host_only(&url).expect("unable to parse domain");
220 matches(false, &host_name, "data:nonrelative");
221 variants(true, &host_name, "http://example.com");
222 variants(false, &host_name, "http://example.org");
223 variants(false, &host_name, "http://foo.example.com");
230 variants(false, &host_name, "http://127.0.0.1");
231 variants(false, &host_name, "http://[::1]");
232 }
233
234 {
235 let url = url("http://127.0.0.1");
236 let ip4 = CookieDomain::host_only(&url).expect("unable to parse Ipv4");
237 matches(false, &ip4, "data:nonrelative");
238 variants(true, &ip4, "http://127.0.0.1");
239 variants(false, &ip4, "http://[::1]");
240 }
241
242 {
243 let url = url("http://[::1]");
244 let ip6 = CookieDomain::host_only(&url).expect("unable to parse Ipv6");
245 matches(false, &ip6, "data:nonrelative");
246 variants(false, &ip6, "http://127.0.0.1");
247 variants(true, &ip6, "http://[::1]");
248 }
249 }
250
251 #[test]
252 fn from_strs() {
253 assert_eq!(
254 CookieDomain::Empty,
255 CookieDomain::try_from("").expect("unable to parse domain")
256 );
257 assert_eq!(
258 CookieDomain::Empty,
259 CookieDomain::try_from(".").expect("unable to parse domain")
260 );
261 assert_eq!(
267 CookieDomain::Suffix(String::from(".")),
268 CookieDomain::try_from("..").expect("unable to parse domain")
269 );
270 assert_eq!(
271 CookieDomain::Suffix(String::from("example.com")),
272 CookieDomain::try_from("example.com").expect("unable to parse domain")
273 );
274 assert_eq!(
275 CookieDomain::Suffix(String::from("example.com")),
276 CookieDomain::try_from(".example.com").expect("unable to parse domain")
277 );
278 assert_eq!(
279 CookieDomain::Suffix(String::from(".example.com")),
280 CookieDomain::try_from("..example.com").expect("unable to parse domain")
281 );
282 }
283
284 #[test]
285 fn from_raw_cookie() {
286 fn raw_cookie(s: &str) -> RawCookie<'_> {
287 RawCookie::parse(s).unwrap()
288 }
289 assert_eq!(
290 CookieDomain::NotPresent,
291 CookieDomain::try_from(&raw_cookie("cookie=value")).expect("unable to parse domain")
292 );
293 assert_eq!(
295 CookieDomain::NotPresent,
296 CookieDomain::try_from(&raw_cookie("cookie=value; Domain="))
297 .expect("unable to parse domain")
298 );
299 assert_eq!(
301 CookieDomain::Empty,
302 CookieDomain::try_from(&raw_cookie("cookie=value; Domain=."))
303 .expect("unable to parse domain")
304 );
305 assert_eq!(
306 CookieDomain::Suffix(String::from("example.com")),
307 CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.example.com"))
308 .expect("unable to parse domain")
309 );
310 assert_eq!(
311 CookieDomain::Suffix(String::from("example.com")),
312 CookieDomain::try_from(&raw_cookie("cookie=value; Domain=example.com"))
313 .expect("unable to parse domain")
314 );
315 }
316
317 #[test]
318 fn matches_suffix() {
319 {
320 let suffix = CookieDomain::try_from("example.com").expect("unable to parse domain");
321 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"); }
327
328 {
329 let suffix = CookieDomain::try_from(".example.com").expect("unable to parse domain");
331 variants(true, &suffix, "http://example.com");
332 variants(true, &suffix, "http://foo.example.com");
333 variants(false, &suffix, "http://example.org");
334 variants(false, &suffix, "http://xample.com");
335 variants(false, &suffix, "http://fooexample.com");
336 }
337
338 {
339 let suffix = CookieDomain::try_from("..example.com").expect("unable to parse domain");
341 variants(true, &suffix, "http://.example.com");
342 variants(true, &suffix, "http://foo..example.com");
343 variants(false, &suffix, "http://example.com");
344 variants(false, &suffix, "http://foo.example.com");
345 variants(false, &suffix, "http://example.org");
346 variants(false, &suffix, "http://xample.com");
347 variants(false, &suffix, "http://fooexample.com");
348 }
349
350 {
351 let suffix = CookieDomain::try_from("127.0.0.1").expect("unable to parse Ipv4");
353 variants(true, &suffix, "http://127.0.0.1");
354 }
355
356 {
357 let suffix = CookieDomain::try_from("[::1]").expect("unable to parse Ipv6");
359 variants(true, &suffix, "http://[::1]");
360 }
361
362 {
363 let suffix = CookieDomain::try_from("0.0.1").expect("unable to parse Ipv4");
365 variants(false, &suffix, "http://127.0.0.1");
366 }
367 }
368}
369
370#[cfg(all(test, feature = "serde_json"))]
371mod serde_json_tests {
372 use serde_json;
373 use std::convert::TryFrom;
374
375 use crate::cookie_domain::CookieDomain;
376 use crate::utils::test::*;
377
378 fn encode_decode(cd: &CookieDomain, exp_json: &str) {
379 let encoded = serde_json::to_string(cd).unwrap();
380 assert!(
381 exp_json == encoded,
382 "expected: '{}'\n encoded: '{}'",
383 exp_json,
384 encoded
385 );
386 let decoded: CookieDomain = serde_json::from_str(&encoded).unwrap();
387 assert!(
388 *cd == decoded,
389 "expected: '{:?}'\n decoded: '{:?}'",
390 cd,
391 decoded
392 );
393 }
394
395 #[test]
396 fn serde() {
397 let url = url("http://example.com");
398 encode_decode(
399 &CookieDomain::host_only(&url).expect("cannot parse domain"),
400 "{\"HostOnly\":\"example.com\"}",
401 );
402 encode_decode(
403 &CookieDomain::try_from(".example.com").expect("cannot parse domain"),
404 "{\"Suffix\":\"example.com\"}",
405 );
406 encode_decode(&CookieDomain::NotPresent, "\"NotPresent\"");
407 encode_decode(&CookieDomain::Empty, "\"Empty\"");
408 }
409}