Add CookieStore trait and expose default Jar (#1203)
				
					
				
			This adds a new trait, `CookieStore`, which allows for custom implementations of storage for a client's cookies. After implementing, you can call `ClientBuilder::cookie_provider` to use it over the default storage. The default store is exposed as `Jar`, to ease the most common use case which is to add a few cookies to the store when creating a client. Co-authored-by: Patrick Fernie <patrick.fernie@gmail.com>
This commit is contained in:
		| @@ -3,8 +3,6 @@ use std::any::Any; | |||||||
| use std::convert::TryInto; | use std::convert::TryInto; | ||||||
| use std::net::IpAddr; | use std::net::IpAddr; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| #[cfg(feature = "cookies")] |  | ||||||
| use std::sync::RwLock; |  | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| use std::{fmt, str}; | use std::{fmt, str}; | ||||||
|  |  | ||||||
| @@ -105,7 +103,7 @@ struct Config { | |||||||
|     local_address: Option<IpAddr>, |     local_address: Option<IpAddr>, | ||||||
|     nodelay: bool, |     nodelay: bool, | ||||||
|     #[cfg(feature = "cookies")] |     #[cfg(feature = "cookies")] | ||||||
|     cookie_store: Option<cookie::CookieStore>, |     cookie_store: Option<Arc<dyn cookie::CookieStore>>, | ||||||
|     trust_dns: bool, |     trust_dns: bool, | ||||||
|     error: Option<crate::Error>, |     error: Option<crate::Error>, | ||||||
|     https_only: bool, |     https_only: bool, | ||||||
| @@ -350,7 +348,7 @@ impl ClientBuilder { | |||||||
|             inner: Arc::new(ClientRef { |             inner: Arc::new(ClientRef { | ||||||
|                 accepts: config.accepts, |                 accepts: config.accepts, | ||||||
|                 #[cfg(feature = "cookies")] |                 #[cfg(feature = "cookies")] | ||||||
|                 cookie_store: config.cookie_store.map(RwLock::new), |                 cookie_store: config.cookie_store, | ||||||
|                 hyper: hyper_client, |                 hyper: hyper_client, | ||||||
|                 headers: config.headers, |                 headers: config.headers, | ||||||
|                 redirect_policy: config.redirect_policy, |                 redirect_policy: config.redirect_policy, | ||||||
| @@ -464,11 +462,31 @@ impl ClientBuilder { | |||||||
|     #[cfg(feature = "cookies")] |     #[cfg(feature = "cookies")] | ||||||
|     #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] |     #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] | ||||||
|     pub fn cookie_store(mut self, enable: bool) -> ClientBuilder { |     pub fn cookie_store(mut self, enable: bool) -> ClientBuilder { | ||||||
|         self.config.cookie_store = if enable { |         if enable { | ||||||
|             Some(cookie::CookieStore::default()) |             self.cookie_provider(Arc::new(cookie::Jar::default())) | ||||||
|         } else { |         } else { | ||||||
|             None |             self.config.cookie_store = None; | ||||||
|         }; |             self | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// Set the persistent cookie store for the client. | ||||||
|  |     /// | ||||||
|  |     /// Cookies received in responses will be passed to this store, and | ||||||
|  |     /// additional requests will query this store for cookies. | ||||||
|  |     /// | ||||||
|  |     /// By default, no cookie store is used. | ||||||
|  |     /// | ||||||
|  |     /// # Optional | ||||||
|  |     /// | ||||||
|  |     /// This requires the optional `cookies` feature to be enabled. | ||||||
|  |     #[cfg(feature = "cookies")] | ||||||
|  |     #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] | ||||||
|  |     pub fn cookie_provider<C: cookie::CookieStore + 'static>( | ||||||
|  |         mut self, | ||||||
|  |         cookie_store: Arc<C>, | ||||||
|  |     ) -> ClientBuilder { | ||||||
|  |         self.config.cookie_store = Some(cookie_store as _); | ||||||
|         self |         self | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1109,10 +1127,9 @@ impl Client { | |||||||
|         // Add cookies from the cookie store. |         // Add cookies from the cookie store. | ||||||
|         #[cfg(feature = "cookies")] |         #[cfg(feature = "cookies")] | ||||||
|         { |         { | ||||||
|             if let Some(cookie_store_wrapper) = self.inner.cookie_store.as_ref() { |             if let Some(cookie_store) = self.inner.cookie_store.as_ref() { | ||||||
|                 if headers.get(crate::header::COOKIE).is_none() { |                 if headers.get(crate::header::COOKIE).is_none() { | ||||||
|                     let cookie_store = cookie_store_wrapper.read().unwrap(); |                     add_cookie_header(&mut headers, &**cookie_store, &url); | ||||||
|                     add_cookie_header(&mut headers, &cookie_store, &url); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -1289,7 +1306,7 @@ impl Config { | |||||||
| struct ClientRef { | struct ClientRef { | ||||||
|     accepts: Accepts, |     accepts: Accepts, | ||||||
|     #[cfg(feature = "cookies")] |     #[cfg(feature = "cookies")] | ||||||
|     cookie_store: Option<RwLock<cookie::CookieStore>>, |     cookie_store: Option<Arc<dyn cookie::CookieStore>>, | ||||||
|     headers: HeaderMap, |     headers: HeaderMap, | ||||||
|     hyper: HyperClient, |     hyper: HyperClient, | ||||||
|     redirect_policy: redirect::Policy, |     redirect_policy: redirect::Policy, | ||||||
| @@ -1431,14 +1448,11 @@ impl Future for PendingRequest { | |||||||
|  |  | ||||||
|             #[cfg(feature = "cookies")] |             #[cfg(feature = "cookies")] | ||||||
|             { |             { | ||||||
|                 if let Some(store_wrapper) = self.client.cookie_store.as_ref() { |                 if let Some(ref cookie_store) = self.client.cookie_store { | ||||||
|                     let mut cookies = cookie::extract_response_cookies(&res.headers()) |                     let mut cookies = | ||||||
|                         .filter_map(|res| res.ok()) |                         cookie::extract_response_cookie_headers(&res.headers()).peekable(); | ||||||
|                         .map(|cookie| cookie.into_inner().into_owned()) |  | ||||||
|                         .peekable(); |  | ||||||
|                     if cookies.peek().is_some() { |                     if cookies.peek().is_some() { | ||||||
|                         let mut store = store_wrapper.write().unwrap(); |                         cookie_store.set_cookies(&mut cookies, &self.url); | ||||||
|                         store.0.store_response_cookies(cookies, &self.url); |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -1531,11 +1545,8 @@ impl Future for PendingRequest { | |||||||
|                             // Add cookies from the cookie store. |                             // Add cookies from the cookie store. | ||||||
|                             #[cfg(feature = "cookies")] |                             #[cfg(feature = "cookies")] | ||||||
|                             { |                             { | ||||||
|                                 if let Some(cookie_store_wrapper) = |                                 if let Some(ref cookie_store) = self.client.cookie_store { | ||||||
|                                     self.client.cookie_store.as_ref() |                                     add_cookie_header(&mut headers, &**cookie_store, &self.url); | ||||||
|                                 { |  | ||||||
|                                     let cookie_store = cookie_store_wrapper.read().unwrap(); |  | ||||||
|                                     add_cookie_header(&mut headers, &cookie_store, &self.url); |  | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
| @@ -1592,18 +1603,9 @@ fn make_referer(next: &Url, previous: &Url) -> Option<HeaderValue> { | |||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(feature = "cookies")] | #[cfg(feature = "cookies")] | ||||||
| fn add_cookie_header(headers: &mut HeaderMap, cookie_store: &cookie::CookieStore, url: &Url) { | fn add_cookie_header(headers: &mut HeaderMap, cookie_store: &dyn cookie::CookieStore, url: &Url) { | ||||||
|     let header = cookie_store |     if let Some(header) = cookie_store.cookies(url) { | ||||||
|         .0 |         headers.insert(crate::header::COOKIE, header); | ||||||
|         .get_request_cookies(url) |  | ||||||
|         .map(|c| format!("{}={}", c.name(), c.value())) |  | ||||||
|         .collect::<Vec<_>>() |  | ||||||
|         .join("; "); |  | ||||||
|     if !header.is_empty() { |  | ||||||
|         headers.insert( |  | ||||||
|             crate::header::COOKIE, |  | ||||||
|             HeaderValue::from_bytes(header.as_bytes()).unwrap(), |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -194,6 +194,25 @@ impl ClientBuilder { | |||||||
|         self.with_inner(|inner| inner.cookie_store(enable)) |         self.with_inner(|inner| inner.cookie_store(enable)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Set the persistent cookie store for the client. | ||||||
|  |     /// | ||||||
|  |     /// Cookies received in responses will be passed to this store, and | ||||||
|  |     /// additional requests will query this store for cookies. | ||||||
|  |     /// | ||||||
|  |     /// By default, no cookie store is used. | ||||||
|  |     /// | ||||||
|  |     /// # Optional | ||||||
|  |     /// | ||||||
|  |     /// This requires the optional `cookies` feature to be enabled. | ||||||
|  |     #[cfg(feature = "cookies")] | ||||||
|  |     #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] | ||||||
|  |     pub fn cookie_provider<C: crate::cookie::CookieStore + 'static>( | ||||||
|  |         self, | ||||||
|  |         cookie_store: Arc<C>, | ||||||
|  |     ) -> ClientBuilder { | ||||||
|  |         self.with_inner(|inner| inner.cookie_provider(cookie_store)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// Enable auto gzip decompression by checking the `Content-Encoding` response header. |     /// Enable auto gzip decompression by checking the `Content-Encoding` response header. | ||||||
|     /// |     /// | ||||||
|     /// If auto gzip decompresson is turned on: |     /// If auto gzip decompresson is turned on: | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								src/cookie.rs
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								src/cookie.rs
									
									
									
									
									
								
							| @@ -1,16 +1,36 @@ | |||||||
| //! HTTP Cookies | //! HTTP Cookies | ||||||
|  |  | ||||||
| use std::convert::TryInto; | use std::convert::TryInto; | ||||||
|  |  | ||||||
| use crate::header; |  | ||||||
| use std::fmt; | use std::fmt; | ||||||
|  | use std::sync::RwLock; | ||||||
| use std::time::SystemTime; | use std::time::SystemTime; | ||||||
|  |  | ||||||
|  | use crate::header::{HeaderValue, SET_COOKIE}; | ||||||
|  | use bytes::Bytes; | ||||||
|  |  | ||||||
|  | /// Actions for a persistent cookie store providing session supprt. | ||||||
|  | pub trait CookieStore: Send + Sync { | ||||||
|  |     /// Store a set of Set-Cookie header values recevied from `url` | ||||||
|  |     fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url); | ||||||
|  |     /// Get any Cookie values in the store for `url` | ||||||
|  |     fn cookies(&self, url: &url::Url) -> Option<HeaderValue>; | ||||||
|  | } | ||||||
|  |  | ||||||
| /// A single HTTP cookie. | /// A single HTTP cookie. | ||||||
| pub struct Cookie<'a>(cookie_crate::Cookie<'a>); | pub struct Cookie<'a>(cookie_crate::Cookie<'a>); | ||||||
|  |  | ||||||
|  | /// A good default `CookieStore` implementation. | ||||||
|  | /// | ||||||
|  | /// This is the implementation used when simply calling `cookie_store(true)`. | ||||||
|  | /// This type is exposed to allow creating one and filling it with some | ||||||
|  | /// existing cookies more easily, before creating a `Client`. | ||||||
|  | #[derive(Debug, Default)] | ||||||
|  | pub struct Jar(RwLock<cookie_store::CookieStore>); | ||||||
|  |  | ||||||
|  | // ===== impl Cookie ===== | ||||||
|  |  | ||||||
| impl<'a> Cookie<'a> { | impl<'a> Cookie<'a> { | ||||||
|     fn parse(value: &'a crate::header::HeaderValue) -> Result<Cookie<'a>, CookieParseError> { |     fn parse(value: &'a HeaderValue) -> Result<Cookie<'a>, CookieParseError> { | ||||||
|         std::str::from_utf8(value.as_bytes()) |         std::str::from_utf8(value.as_bytes()) | ||||||
|             .map_err(cookie_crate::ParseError::from) |             .map_err(cookie_crate::ParseError::from) | ||||||
|             .and_then(cookie_crate::Cookie::parse) |             .and_then(cookie_crate::Cookie::parse) | ||||||
| @@ -18,10 +38,6 @@ impl<'a> Cookie<'a> { | |||||||
|             .map(Cookie) |             .map(Cookie) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub(crate) fn into_inner(self) -> cookie_crate::Cookie<'a> { |  | ||||||
|         self.0 |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// The name of the cookie. |     /// The name of the cookie. | ||||||
|     pub fn name(&self) -> &str { |     pub fn name(&self) -> &str { | ||||||
|         self.0.name() |         self.0.name() | ||||||
| @@ -82,25 +98,21 @@ impl<'a> fmt::Debug for Cookie<'a> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub(crate) fn extract_response_cookie_headers<'a>( | ||||||
|  |     headers: &'a hyper::HeaderMap, | ||||||
|  | ) -> impl Iterator<Item = &'a HeaderValue> + 'a { | ||||||
|  |     headers.get_all(SET_COOKIE).iter() | ||||||
|  | } | ||||||
|  |  | ||||||
| pub(crate) fn extract_response_cookies<'a>( | pub(crate) fn extract_response_cookies<'a>( | ||||||
|     headers: &'a hyper::HeaderMap, |     headers: &'a hyper::HeaderMap, | ||||||
| ) -> impl Iterator<Item = Result<Cookie<'a>, CookieParseError>> + 'a { | ) -> impl Iterator<Item = Result<Cookie<'a>, CookieParseError>> + 'a { | ||||||
|     headers |     headers | ||||||
|         .get_all(header::SET_COOKIE) |         .get_all(SET_COOKIE) | ||||||
|         .iter() |         .iter() | ||||||
|         .map(|value| Cookie::parse(value)) |         .map(|value| Cookie::parse(value)) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// A persistent cookie store that provides session support. |  | ||||||
| #[derive(Default)] |  | ||||||
| pub(crate) struct CookieStore(pub(crate) cookie_store::CookieStore); |  | ||||||
|  |  | ||||||
| impl<'a> fmt::Debug for CookieStore { |  | ||||||
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |  | ||||||
|         self.0.fmt(f) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Error representing a parse failure of a 'Set-Cookie' header. | /// Error representing a parse failure of a 'Set-Cookie' header. | ||||||
| pub(crate) struct CookieParseError(cookie_crate::ParseError); | pub(crate) struct CookieParseError(cookie_crate::ParseError); | ||||||
|  |  | ||||||
| @@ -117,3 +129,56 @@ impl<'a> fmt::Display for CookieParseError { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl std::error::Error for CookieParseError {} | impl std::error::Error for CookieParseError {} | ||||||
|  |  | ||||||
|  | // ===== impl Jar ===== | ||||||
|  |  | ||||||
|  | impl Jar { | ||||||
|  |     /// Add a cookie to this jar. | ||||||
|  |     /// | ||||||
|  |     /// # Example | ||||||
|  |     /// | ||||||
|  |     /// ``` | ||||||
|  |     /// use reqwest::{cookie::Jar, Url}; | ||||||
|  |     /// | ||||||
|  |     /// let cookie = "foo=bar; Domain=yolo.local"; | ||||||
|  |     /// let url = "https://yolo.local".parse::<Url>().unwrap(); | ||||||
|  |     /// | ||||||
|  |     /// let jar = Jar::default(); | ||||||
|  |     /// jar.add_cookie_str(cookie, &url); | ||||||
|  |     /// | ||||||
|  |     /// // and now add to a `ClientBuilder`? | ||||||
|  |     /// ``` | ||||||
|  |     pub fn add_cookie_str(&self, cookie: &str, url: &url::Url) { | ||||||
|  |         let cookies = cookie_crate::Cookie::parse(cookie) | ||||||
|  |             .ok() | ||||||
|  |             .map(|c| c.into_owned()) | ||||||
|  |             .into_iter(); | ||||||
|  |         self.0.write().unwrap().store_response_cookies(cookies, url); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl CookieStore for Jar { | ||||||
|  |     fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) { | ||||||
|  |         let iter = | ||||||
|  |             cookie_headers.filter_map(|val| Cookie::parse(val).map(|c| c.0.into_owned()).ok()); | ||||||
|  |  | ||||||
|  |         self.0.write().unwrap().store_response_cookies(iter, url); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn cookies(&self, url: &url::Url) -> Option<HeaderValue> { | ||||||
|  |         let s = self | ||||||
|  |             .0 | ||||||
|  |             .read() | ||||||
|  |             .unwrap() | ||||||
|  |             .get_request_cookies(url) | ||||||
|  |             .map(|c| format!("{}={}", c.name(), c.value())) | ||||||
|  |             .collect::<Vec<_>>() | ||||||
|  |             .join("; "); | ||||||
|  |  | ||||||
|  |         if s.is_empty() { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         HeaderValue::from_maybe_shared(Bytes::from(s)).ok() | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user