Skip to main content

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 fmt::Debug for Cookie<'_> {
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(
108    headers: &hyper::HeaderMap,
109) -> impl Iterator<Item = &HeaderValue> + '_ {
110    headers.get_all(SET_COOKIE).iter()
111}
112
113pub(crate) fn extract_response_cookies(
114    headers: &hyper::HeaderMap,
115) -> impl Iterator<Item = Result<Cookie<'_>, CookieParseError>> + '_ {
116    headers.get_all(SET_COOKIE).iter().map(Cookie::parse)
117}
118
119/// Error representing a parse failure of a 'Set-Cookie' header.
120pub(crate) struct CookieParseError(cookie_crate::ParseError);
121
122impl fmt::Debug for CookieParseError {
123    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
124        self.0.fmt(f)
125    }
126}
127
128impl fmt::Display for CookieParseError {
129    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
130        self.0.fmt(f)
131    }
132}
133
134impl std::error::Error for CookieParseError {}
135
136// ===== impl Jar =====
137
138impl Jar {
139    /// Add a cookie to this jar.
140    ///
141    /// # Example
142    ///
143    /// ```
144    /// use reqwest::{cookie::Jar, Url};
145    ///
146    /// let cookie = "foo=bar; Domain=yolo.local";
147    /// let url = "https://yolo.local".parse::<Url>().unwrap();
148    ///
149    /// let jar = Jar::default();
150    /// jar.add_cookie_str(cookie, &url);
151    ///
152    /// // and now add to a `ClientBuilder`?
153    /// ```
154    pub fn add_cookie_str(&self, cookie: &str, url: &url::Url) {
155        let cookies = cookie_crate::Cookie::parse(cookie)
156            .ok()
157            .map(|c| c.into_owned())
158            .into_iter();
159        self.0.write().unwrap().store_response_cookies(cookies, url);
160    }
161}
162
163impl CookieStore for Jar {
164    fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) {
165        let iter =
166            cookie_headers.filter_map(|val| Cookie::parse(val).map(|c| c.0.into_owned()).ok());
167
168        self.0.write().unwrap().store_response_cookies(iter, url);
169    }
170
171    fn cookies(&self, url: &url::Url) -> Option<HeaderValue> {
172        let s = self
173            .0
174            .read()
175            .unwrap()
176            .get_request_values(url)
177            .map(|(name, value)| format!("{name}={value}"))
178            .collect::<Vec<_>>()
179            .join("; ");
180
181        if s.is_empty() {
182            return None;
183        }
184
185        HeaderValue::from_maybe_shared(Bytes::from(s)).ok()
186    }
187}
188
189pub(crate) mod service {
190    use crate::cookie;
191    use http::{Request, Response};
192    use http_body::Body;
193    use pin_project_lite::pin_project;
194    use std::future::Future;
195    use std::pin::Pin;
196    use std::sync::Arc;
197    use std::task::ready;
198    use std::task::Context;
199    use std::task::Poll;
200    use tower::Service;
201    use url::Url;
202
203    /// A [`Service`] that adds cookie support to a lower-level [`Service`].
204    #[derive(Clone)]
205    pub struct CookieService<S> {
206        inner: S,
207        cookie_store: Option<Arc<dyn cookie::CookieStore>>,
208    }
209
210    impl<S> CookieService<S> {
211        /// Create a new [`CookieService`].
212        pub fn new(inner: S, cookie_store: Option<Arc<dyn cookie::CookieStore>>) -> Self {
213            Self {
214                inner,
215                cookie_store,
216            }
217        }
218    }
219
220    impl<ReqBody, ResBody, S> Service<Request<ReqBody>> for CookieService<S>
221    where
222        S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone,
223        ReqBody: Body + Default,
224    {
225        type Response = Response<ResBody>;
226        type Error = S::Error;
227        type Future = ResponseFuture<S, ReqBody>;
228
229        #[inline]
230        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
231            self.inner.poll_ready(cx)
232        }
233
234        fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
235            let clone = self.inner.clone();
236            let mut inner = std::mem::replace(&mut self.inner, clone);
237            let url = Url::parse(req.uri().to_string().as_str()).expect("invalid URL");
238            if let Some(cookie_store) = self.cookie_store.as_ref() {
239                if req.headers().get(crate::header::COOKIE).is_none() {
240                    let headers = req.headers_mut();
241                    crate::util::add_cookie_header(headers, &**cookie_store, &url);
242                }
243            }
244
245            let cookie_store = self.cookie_store.clone();
246            ResponseFuture {
247                future: inner.call(req),
248                cookie_store,
249                url,
250            }
251        }
252    }
253
254    pin_project! {
255        #[allow(missing_debug_implementations)]
256        #[derive(Clone)]
257        /// A [`Future`] that adds cookie support to a lower-level [`Future`].
258        pub struct ResponseFuture<S, B>
259        where
260            S: Service<Request<B>>,
261        {
262            #[pin]
263            future: S::Future,
264            cookie_store: Option<Arc<dyn cookie::CookieStore>>,
265            url: Url,
266        }
267    }
268
269    impl<S, ReqBody, ResBody> Future for ResponseFuture<S, ReqBody>
270    where
271        S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone,
272        ReqBody: Body + Default,
273    {
274        type Output = Result<Response<ResBody>, S::Error>;
275
276        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
277            let cookie_store = self.cookie_store.clone();
278            let url = self.url.clone();
279            let res = ready!(self.project().future.as_mut().poll(cx)?);
280
281            if let Some(cookie_store) = cookie_store.as_ref() {
282                let mut cookies = cookie::extract_response_cookie_headers(res.headers()).peekable();
283                if cookies.peek().is_some() {
284                    cookie_store.set_cookies(&mut cookies, &url);
285                }
286            }
287            Poll::Ready(Ok(res))
288        }
289    }
290}