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