reqwest/
cookie.rs

1//! HTTP Cookies
2
3use crate::header::{HeaderValue, SET_COOKIE};
4use bytes::Bytes;
5use std::convert::TryInto;
6use std::fmt;
7use std::sync::RwLock;
8use std::time::SystemTime;
9
10/// Actions for a persistent cookie store providing session support.
11pub trait CookieStore: Send + Sync {
12    /// Store a set of Set-Cookie header values received from `url`
13    fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url);
14    /// Get any Cookie values in the store for `url`
15    fn cookies(&self, url: &url::Url) -> Option<HeaderValue>;
16}
17
18/// A single HTTP cookie.
19pub struct Cookie<'a>(cookie_crate::Cookie<'a>);
20
21/// A good default `CookieStore` implementation.
22///
23/// This is the implementation used when simply calling `cookie_store(true)`.
24/// This type is exposed to allow creating one and filling it with some
25/// existing cookies more easily, before creating a `Client`.
26///
27/// For more advanced scenarios, such as needing to serialize the store or
28/// manipulate it between requests, you may refer to the
29/// [reqwest_cookie_store crate](https://crates.io/crates/reqwest_cookie_store).
30#[derive(Debug, Default)]
31pub struct Jar(RwLock<cookie_store::CookieStore>);
32
33// ===== impl Cookie =====
34
35impl<'a> Cookie<'a> {
36    fn parse(value: &'a HeaderValue) -> Result<Cookie<'a>, CookieParseError> {
37        std::str::from_utf8(value.as_bytes())
38            .map_err(cookie_crate::ParseError::from)
39            .and_then(cookie_crate::Cookie::parse)
40            .map_err(CookieParseError)
41            .map(Cookie)
42    }
43
44    /// The name of the cookie.
45    pub fn name(&self) -> &str {
46        self.0.name()
47    }
48
49    /// The value of the cookie.
50    pub fn value(&self) -> &str {
51        self.0.value()
52    }
53
54    /// Returns true if the 'HttpOnly' directive is enabled.
55    pub fn http_only(&self) -> bool {
56        self.0.http_only().unwrap_or(false)
57    }
58
59    /// Returns true if the 'Secure' directive is enabled.
60    pub fn secure(&self) -> bool {
61        self.0.secure().unwrap_or(false)
62    }
63
64    /// Returns true if  'SameSite' directive is 'Lax'.
65    pub fn same_site_lax(&self) -> bool {
66        self.0.same_site() == Some(cookie_crate::SameSite::Lax)
67    }
68
69    /// Returns true if  'SameSite' directive is 'Strict'.
70    pub fn same_site_strict(&self) -> bool {
71        self.0.same_site() == Some(cookie_crate::SameSite::Strict)
72    }
73
74    /// Returns the path directive of the cookie, if set.
75    pub fn path(&self) -> Option<&str> {
76        self.0.path()
77    }
78
79    /// Returns the domain directive of the cookie, if set.
80    pub fn domain(&self) -> Option<&str> {
81        self.0.domain()
82    }
83
84    /// Get the Max-Age information.
85    pub fn max_age(&self) -> Option<std::time::Duration> {
86        self.0.max_age().map(|d| {
87            d.try_into()
88                .expect("time::Duration into std::time::Duration")
89        })
90    }
91
92    /// The cookie expiration time.
93    pub fn expires(&self) -> Option<SystemTime> {
94        match self.0.expires() {
95            Some(cookie_crate::Expiration::DateTime(offset)) => Some(SystemTime::from(offset)),
96            None | Some(cookie_crate::Expiration::Session) => None,
97        }
98    }
99}
100
101impl<'a> fmt::Debug for Cookie<'a> {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        self.0.fmt(f)
104    }
105}
106
107pub(crate) fn extract_response_cookie_headers<'a>(
108    headers: &'a hyper::HeaderMap,
109) -> impl Iterator<Item = &'a HeaderValue> + 'a {
110    headers.get_all(SET_COOKIE).iter()
111}
112
113pub(crate) fn extract_response_cookies<'a>(
114    headers: &'a hyper::HeaderMap,
115) -> impl Iterator<Item = Result<Cookie<'a>, CookieParseError>> + 'a {
116    headers
117        .get_all(SET_COOKIE)
118        .iter()
119        .map(|value| Cookie::parse(value))
120}
121
122/// Error representing a parse failure of a 'Set-Cookie' header.
123pub(crate) struct CookieParseError(cookie_crate::ParseError);
124
125impl<'a> fmt::Debug for CookieParseError {
126    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127        self.0.fmt(f)
128    }
129}
130
131impl<'a> fmt::Display for CookieParseError {
132    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133        self.0.fmt(f)
134    }
135}
136
137impl std::error::Error for CookieParseError {}
138
139// ===== impl Jar =====
140
141impl Jar {
142    /// Add a cookie to this jar.
143    ///
144    /// # Example
145    ///
146    /// ```
147    /// use reqwest::{cookie::Jar, Url};
148    ///
149    /// let cookie = "foo=bar; Domain=yolo.local";
150    /// let url = "https://yolo.local".parse::<Url>().unwrap();
151    ///
152    /// let jar = Jar::default();
153    /// jar.add_cookie_str(cookie, &url);
154    ///
155    /// // and now add to a `ClientBuilder`?
156    /// ```
157    pub fn add_cookie_str(&self, cookie: &str, url: &url::Url) {
158        let cookies = cookie_crate::Cookie::parse(cookie)
159            .ok()
160            .map(|c| c.into_owned())
161            .into_iter();
162        self.0.write().unwrap().store_response_cookies(cookies, url);
163    }
164}
165
166impl CookieStore for Jar {
167    fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) {
168        let iter =
169            cookie_headers.filter_map(|val| Cookie::parse(val).map(|c| c.0.into_owned()).ok());
170
171        self.0.write().unwrap().store_response_cookies(iter, url);
172    }
173
174    fn cookies(&self, url: &url::Url) -> Option<HeaderValue> {
175        let s = self
176            .0
177            .read()
178            .unwrap()
179            .get_request_values(url)
180            .map(|(name, value)| format!("{name}={value}"))
181            .collect::<Vec<_>>()
182            .join("; ");
183
184        if s.is_empty() {
185            return None;
186        }
187
188        HeaderValue::from_maybe_shared(Bytes::from(s)).ok()
189    }
190}
191
192pub(crate) mod service {
193    use crate::cookie;
194    use http::{Request, Response};
195    use http_body::Body;
196    use pin_project_lite::pin_project;
197    use std::future::Future;
198    use std::pin::Pin;
199    use std::sync::Arc;
200    use std::task::ready;
201    use std::task::Context;
202    use std::task::Poll;
203    use tower::Service;
204    use url::Url;
205
206    /// A [`Service`] that adds cookie support to a lower-level [`Service`].
207    #[derive(Clone)]
208    pub struct CookieService<S> {
209        inner: S,
210        cookie_store: Option<Arc<dyn cookie::CookieStore>>,
211    }
212
213    impl<S> CookieService<S> {
214        /// Create a new [`CookieService`].
215        pub fn new(inner: S, cookie_store: Option<Arc<dyn cookie::CookieStore>>) -> Self {
216            Self {
217                inner,
218                cookie_store,
219            }
220        }
221    }
222
223    impl<ReqBody, ResBody, S> Service<Request<ReqBody>> for CookieService<S>
224    where
225        S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone,
226        ReqBody: Body + Default,
227    {
228        type Response = Response<ResBody>;
229        type Error = S::Error;
230        type Future = ResponseFuture<S, ReqBody>;
231
232        #[inline]
233        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
234            self.inner.poll_ready(cx)
235        }
236
237        fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
238            let clone = self.inner.clone();
239            let mut inner = std::mem::replace(&mut self.inner, clone);
240            let url = Url::parse(req.uri().to_string().as_str()).expect("invalid URL");
241            if let Some(cookie_store) = self.cookie_store.as_ref() {
242                if req.headers().get(crate::header::COOKIE).is_none() {
243                    let headers = req.headers_mut();
244                    crate::util::add_cookie_header(headers, &**cookie_store, &url);
245                }
246            }
247
248            let cookie_store = self.cookie_store.clone();
249            ResponseFuture {
250                future: inner.call(req),
251                cookie_store,
252                url,
253            }
254        }
255    }
256
257    pin_project! {
258        #[allow(missing_debug_implementations)]
259        #[derive(Clone)]
260        /// A [`Future`] that adds cookie support to a lower-level [`Future`].
261        pub struct ResponseFuture<S, B>
262        where
263            S: Service<Request<B>>,
264        {
265            #[pin]
266            future: S::Future,
267            cookie_store: Option<Arc<dyn cookie::CookieStore>>,
268            url: Url,
269        }
270    }
271
272    impl<S, ReqBody, ResBody> Future for ResponseFuture<S, ReqBody>
273    where
274        S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone,
275        ReqBody: Body + Default,
276    {
277        type Output = Result<Response<ResBody>, S::Error>;
278
279        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
280            let cookie_store = self.cookie_store.clone();
281            let url = self.url.clone();
282            let res = ready!(self.project().future.as_mut().poll(cx)?);
283
284            if let Some(cookie_store) = cookie_store.as_ref() {
285                let mut cookies = cookie::extract_response_cookie_headers(res.headers()).peekable();
286                if cookies.peek().is_some() {
287                    cookie_store.set_cookies(&mut cookies, &url);
288                }
289            }
290            Poll::Ready(Ok(res))
291        }
292    }
293}