[go: up one dir, main page]

ureq/
cookies.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::sync::{Mutex, MutexGuard};
4
5use cookie_store::CookieStore;
6use http::Uri;
7
8use crate::http;
9use crate::util::UriExt;
10use crate::Error;
11
12#[cfg(feature = "json")]
13use std::io;
14
15#[derive(Debug)]
16pub(crate) struct SharedCookieJar {
17    inner: Mutex<CookieStore>,
18}
19
20/// Collection of cookies.
21///
22/// The jar is accessed using [`Agent::cookie_jar_lock`][crate::Agent::cookie_jar_lock].
23/// It can be saved and loaded.
24pub struct CookieJar<'a>(MutexGuard<'a, CookieStore>);
25
26/// Representation of an HTTP cookie.
27///
28/// Conforms to [IETF RFC6265](https://datatracker.ietf.org/doc/html/rfc6265)
29///
30/// ## Constructing a `Cookie`
31///
32/// To construct a cookie it must be parsed and bound to a uri:
33///
34/// ```
35/// use ureq::Cookie;
36/// use ureq::http::Uri;
37///
38/// let uri = Uri::from_static("https://my.server.com");
39/// let cookie = Cookie::parse("name=value", &uri)?;
40/// assert_eq!(cookie.to_string(), "name=value");
41/// # Ok::<_, ureq::Error>(())
42/// ```
43pub struct Cookie<'a>(CookieInner<'a>);
44
45#[allow(clippy::large_enum_variant)]
46enum CookieInner<'a> {
47    Borrowed(&'a cookie_store::Cookie<'a>),
48    Owned(cookie_store::Cookie<'a>),
49}
50
51impl<'a> CookieInner<'a> {
52    fn into_static(self) -> cookie_store::Cookie<'static> {
53        match self {
54            CookieInner::Borrowed(v) => v.clone().into_owned(),
55            CookieInner::Owned(v) => v.into_owned(),
56        }
57    }
58}
59
60impl<'a> Cookie<'a> {
61    /// Parses a new [`Cookie`] from a string
62    pub fn parse<S>(cookie_str: S, uri: &Uri) -> Result<Cookie<'a>, Error>
63    where
64        S: Into<Cow<'a, str>>,
65    {
66        let cookie = cookie_store::Cookie::parse(cookie_str, &uri.try_into_url()?)?;
67        Ok(Cookie(CookieInner::Owned(cookie)))
68    }
69
70    /// The cookie's name.
71    pub fn name(&self) -> &str {
72        match &self.0 {
73            CookieInner::Borrowed(v) => v.name(),
74            CookieInner::Owned(v) => v.name(),
75        }
76    }
77
78    /// The cookie's value.
79    pub fn value(&self) -> &str {
80        match &self.0 {
81            CookieInner::Borrowed(v) => v.value(),
82            CookieInner::Owned(v) => v.value(),
83        }
84    }
85
86    #[cfg(test)]
87    fn as_cookie_store(&self) -> &cookie_store::Cookie<'a> {
88        match &self.0 {
89            CookieInner::Borrowed(v) => v,
90            CookieInner::Owned(v) => v,
91        }
92    }
93}
94
95impl Cookie<'static> {
96    fn into_owned(self) -> cookie_store::Cookie<'static> {
97        match self.0 {
98            CookieInner::Owned(v) => v,
99            _ => unreachable!(),
100        }
101    }
102}
103
104impl<'a> CookieJar<'a> {
105    /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`,
106    /// `path`, and `name`.
107    pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<Cookie<'_>> {
108        self.0
109            .get(domain, path, name)
110            .map(|c| Cookie(CookieInner::Borrowed(c)))
111    }
112
113    /// Removes a `Cookie` from the jar, returning the `Cookie` if it was in the jar
114    pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option<Cookie<'static>> {
115        self.0
116            .remove(domain, path, name)
117            .map(|c| Cookie(CookieInner::Owned(c)))
118    }
119
120    /// Inserts `cookie`, received from `uri`, into the jar, following the rules of the
121    /// [IETF RFC6265 Storage Model](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3).
122    pub fn insert(&mut self, cookie: Cookie<'static>, uri: &Uri) -> Result<(), Error> {
123        let url = uri.try_into_url()?;
124        self.0.insert(cookie.into_owned(), &url)?;
125        Ok(())
126    }
127
128    /// Clear the contents of the jar
129    pub fn clear(&mut self) {
130        self.0.clear()
131    }
132
133    /// An iterator visiting all the __unexpired__ cookies in the jar
134    pub fn iter(&self) -> impl Iterator<Item = Cookie<'_>> {
135        self.0
136            .iter_unexpired()
137            .map(|c| Cookie(CookieInner::Borrowed(c)))
138    }
139
140    /// Serialize any __unexpired__ and __persistent__ cookies in the jar to JSON format and
141    /// write them to `writer`
142    #[cfg(feature = "json")]
143    pub fn save_json<W: io::Write>(&self, writer: &mut W) -> Result<(), Error> {
144        Ok(cookie_store::serde::json::save(&self.0, writer)?)
145    }
146
147    /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies
148    ///
149    /// Replaces all the contents of the current cookie jar.
150    #[cfg(feature = "json")]
151    pub fn load_json<R: io::BufRead>(&mut self, reader: R) -> Result<(), Error> {
152        let store = cookie_store::serde::json::load(reader)?;
153        *self.0 = store;
154        Ok(())
155    }
156
157    pub(crate) fn store_response_cookies<'b>(
158        &mut self,
159        iter: impl Iterator<Item = Cookie<'b>>,
160        uri: &Uri,
161    ) {
162        let url = uri.try_into_url().expect("uri to be a url");
163        let raw_cookies = iter.map(|c| c.0.into_static().into());
164        self.0.store_response_cookies(raw_cookies, &url);
165    }
166
167    /// Release the cookie jar.
168    pub fn release(self) {}
169}
170
171impl SharedCookieJar {
172    pub(crate) fn new() -> Self {
173        SharedCookieJar {
174            inner: Mutex::new(CookieStore::new()),
175        }
176    }
177
178    pub(crate) fn lock(&self) -> CookieJar<'_> {
179        let lock = self.inner.lock().unwrap();
180        CookieJar(lock)
181    }
182
183    pub(crate) fn get_request_cookies(&self, uri: &Uri) -> String {
184        let mut cookies = String::new();
185
186        let url = match uri.try_into_url() {
187            Ok(v) => v,
188            Err(e) => {
189                debug!("Bad url for cookie: {:?}", e);
190                return cookies;
191            }
192        };
193
194        let store = self.inner.lock().unwrap();
195
196        for c in store.matches(&url) {
197            if !is_cookie_rfc_compliant(c) {
198                debug!("Do not send non compliant cookie: {:?}", c.name());
199                continue;
200            }
201
202            if !cookies.is_empty() {
203                cookies.push(';');
204            }
205
206            cookies.push_str(&c.to_string());
207        }
208
209        cookies
210    }
211}
212
213fn is_cookie_rfc_compliant(cookie: &cookie_store::Cookie) -> bool {
214    // https://tools.ietf.org/html/rfc6265#page-9
215    // set-cookie-header = "Set-Cookie:" SP set-cookie-string
216    // set-cookie-string = cookie-pair *( ";" SP cookie-av )
217    // cookie-pair       = cookie-name "=" cookie-value
218    // cookie-name       = token
219    // cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
220    // cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
221    //                       ; US-ASCII characters excluding CTLs,
222    //                       ; whitespace DQUOTE, comma, semicolon,
223    //                       ; and backslash
224    // token             = <token, defined in [RFC2616], Section 2.2>
225
226    // https://tools.ietf.org/html/rfc2616#page-17
227    // CHAR           = <any US-ASCII character (octets 0 - 127)>
228    // ...
229    //        CTL            = <any US-ASCII control character
230    //                         (octets 0 - 31) and DEL (127)>
231    // ...
232    //        token          = 1*<any CHAR except CTLs or separators>
233    //        separators     = "(" | ")" | "<" | ">" | "@"
234    //                       | "," | ";" | ":" | "\" | <">
235    //                       | "/" | "[" | "]" | "?" | "="
236    //                       | "{" | "}" | SP | HT
237
238    fn is_valid_name(b: &u8) -> bool {
239        is_tchar(b)
240    }
241
242    fn is_valid_value(b: &u8) -> bool {
243        b.is_ascii()
244            && !b.is_ascii_control()
245            && !b.is_ascii_whitespace()
246            && *b != b'"'
247            && *b != b','
248            && *b != b';'
249            && *b != b'\\'
250    }
251
252    let name = cookie.name().as_bytes();
253
254    let valid_name = name.iter().all(is_valid_name);
255
256    if !valid_name {
257        log::trace!("cookie name is not valid: {:?}", cookie.name());
258        return false;
259    }
260
261    let value = cookie.value().as_bytes();
262
263    let valid_value = value.iter().all(is_valid_value);
264
265    if !valid_value {
266        // NB. Do not log cookie value since it might be secret
267        log::trace!("cookie value is not valid: {:?}", cookie.name());
268        return false;
269    }
270
271    true
272}
273
274#[inline]
275pub(crate) fn is_tchar(b: &u8) -> bool {
276    match b {
277        b'!' | b'#' | b'$' | b'%' | b'&' => true,
278        b'\'' | b'*' | b'+' | b'-' | b'.' => true,
279        b'^' | b'_' | b'`' | b'|' | b'~' => true,
280        b if b.is_ascii_alphanumeric() => true,
281        _ => false,
282    }
283}
284
285impl fmt::Display for Cookie<'_> {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        match &self.0 {
288            CookieInner::Borrowed(v) => v.fmt(f),
289            CookieInner::Owned(v) => v.fmt(f),
290        }
291    }
292}
293
294#[cfg(test)]
295mod test {
296
297    use std::convert::TryFrom;
298
299    use super::*;
300
301    fn uri() -> Uri {
302        Uri::try_from("https://example.test").unwrap()
303    }
304
305    #[test]
306    fn illegal_cookie_name() {
307        let cookie = Cookie::parse("borked/=value", &uri()).unwrap();
308        assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store()));
309    }
310
311    #[test]
312    fn illegal_cookie_value() {
313        let cookie = Cookie::parse("name=borked,", &uri()).unwrap();
314        assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store()));
315    }
316
317    #[test]
318    fn legal_cookie_name_value() {
319        let cookie = Cookie::parse("name=value", &uri()).unwrap();
320        assert!(is_cookie_rfc_compliant(cookie.as_cookie_store()));
321    }
322}