[go: up one dir, main page]

ureq/
cookies.rs

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