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