surf_cookie_middleware/
lib.rs

1#![forbid(unsafe_code, future_incompatible)]
2#![deny(
3    missing_docs,
4    missing_debug_implementations,
5    nonstandard_style,
6    missing_copy_implementations,
7    unused_qualifications
8)]
9
10//! # A middleware for sending received cookies in surf
11//!
12//! see [`CookieMiddleware`] for details
13//!
14use async_dup::{Arc, Mutex};
15use async_std::{
16    fs::{File, OpenOptions},
17    prelude::*,
18    sync::RwLock,
19};
20use std::{
21    convert::TryInto,
22    io::{self, Cursor, SeekFrom},
23    path::PathBuf,
24};
25use surf::{
26    http::headers::{COOKIE, SET_COOKIE},
27    middleware::{Middleware, Next},
28    utils::async_trait,
29    Client, Request, Response, Result, Url,
30};
31
32pub use cookie_store;
33pub use cookie_store::CookieStore;
34
35/// # A middleware for sending received cookies in surf
36///
37/// ## File system persistence
38///
39/// This middleware can optionally be constructed with a file or path
40/// to enable writing "persistent cookies" to disk after every
41/// received response.
42///
43/// ## Cloning semantics
44///
45/// All clones of this middleware will refer to the same data and fd
46/// (if persistence is enabled).
47///
48/// # Usage example
49///
50/// ```rust
51/// use surf::Client;
52/// use surf_cookie_middleware::CookieMiddleware;
53/// let client = Client::new().with(CookieMiddleware::new());
54/// // client.get(...).await?;
55/// // client.get(...).await?; <- this request will send any appropriate
56/// //                            cookies received from the first request,
57/// //                            based on request url
58/// ```
59
60#[derive(Default, Clone, Debug)]
61pub struct CookieMiddleware {
62    cookie_store: Arc<RwLock<CookieStore>>,
63    file: Option<Arc<Mutex<File>>>,
64}
65
66#[async_trait]
67impl Middleware for CookieMiddleware {
68    async fn handle(&self, mut req: Request, client: Client, next: Next<'_>) -> Result<Response> {
69        let url = req.url().clone();
70        self.set_cookies(&mut req).await;
71        let res = next.run(req, client).await?;
72        self.store_cookies(&url, &res).await?;
73        Ok(res)
74    }
75}
76
77impl CookieMiddleware {
78    /// Builds a new CookieMiddleware
79    ///
80    /// # Example
81    ///
82    /// ```rust
83    /// use surf_cookie_middleware::CookieMiddleware;
84    ///
85    /// let client = surf::Client::new().with(CookieMiddleware::new());
86    ///
87    /// // client.get(...).await?;
88    /// // client.get(...).await?; <- this request will send any appropriate
89    /// //                            cookies received from the first request,
90    /// //                            based on request url
91    /// ```
92    pub fn new() -> Self {
93        Self::with_cookie_store(Default::default())
94    }
95
96    /// Builds a CookieMiddleware with an existing [`cookie_store::CookieStore`]
97    ///
98    /// # Example
99    ///
100    /// ```rust
101    /// use surf_cookie_middleware::{CookieStore, CookieMiddleware};
102    ///
103    /// let cookie_store = CookieStore::default();
104    /// let client = surf::Client::new()
105    ///     .with(CookieMiddleware::with_cookie_store(cookie_store));
106    /// ```
107    pub fn with_cookie_store(cookie_store: CookieStore) -> Self {
108        Self {
109            cookie_store: Arc::new(RwLock::new(cookie_store)),
110            file: None,
111        }
112    }
113
114    /// Builds a CookieMiddleware from a path to a filesystem cookie
115    /// jar. These jars are stored in [ndjson](http://ndjson.org/)
116    /// format. If the file does not exist, it will be created. If the
117    /// file does exist, the cookie jar will be initialized with those
118    /// cookies.
119    ///
120    /// Currently this only persists "persistent cookies" -- cookies
121    /// with an expiry. "Session cookies" (without an expiry) are not
122    /// persisted to disk, nor are expired cookies.
123    ///
124    /// # Example
125    ///
126    /// ```rust
127    /// # fn main() -> std::io::Result<()> { async_std::task::block_on(async {
128    /// use surf_cookie_middleware::{CookieStore, CookieMiddleware};
129    ///
130    /// let cookie_store = CookieStore::default();
131    /// let client = surf::Client::new()
132    ///     .with(CookieMiddleware::from_path("./cookies.ndjson").await?);
133    /// # Ok(()) }) }
134    /// ```
135    pub async fn from_path(path: impl Into<PathBuf>) -> io::Result<Self> {
136        let path = path.into();
137        let file = OpenOptions::new()
138            .create(true)
139            .read(true)
140            .write(true)
141            .open(&path)
142            .await?;
143
144        Self::from_file(file).await
145    }
146
147    async fn load_from_file(file: &mut File) -> Option<CookieStore> {
148        let mut buf = Vec::new();
149        file.read_to_end(&mut buf).await.ok();
150        CookieStore::load_json(Cursor::new(&buf[..])).ok()
151    }
152
153    /// Builds a CookieMiddleware from a File (either
154    /// [`async_std::fs::File`] or [`std::fs::File`]) that represents
155    /// a filesystem cookie jar. These jars are stored in
156    /// [ndjson](http://ndjson.org/) format. The cookie jar will be
157    /// initialized with any cookies contained in this file, and
158    /// persisted to the file after every request.
159    ///
160    /// Currently this only persists "persistent cookies" -- cookies
161    /// with an expiry. "Session cookies" (without an expiry) are not
162    /// persisted to disk, nor are expired cookies.
163    ///
164    /// # Example
165    ///
166    /// ```rust
167    /// # fn main() -> std::io::Result<()> { async_std::task::block_on(async {
168    /// use surf::Client;
169    /// use surf_cookie_middleware::{CookieStore, CookieMiddleware};
170    /// let cookie_store = CookieStore::default();
171    /// let file = std::fs::File::create("./cookies.ndjson")?;
172    /// let client = Client::new()
173    ///     .with(CookieMiddleware::from_file(file).await?);
174    /// # Ok(()) }) }
175    /// ```
176    pub async fn from_file(file: impl Into<File>) -> io::Result<Self> {
177        let mut file = file.into();
178        let cookie_store = Self::load_from_file(&mut file).await;
179        Ok(Self {
180            file: Some(Arc::new(Mutex::new(file))),
181            cookie_store: Arc::new(RwLock::new(cookie_store.unwrap_or_default())),
182        })
183    }
184
185    async fn save(&self) -> Result<()> {
186        if let Some(ref file) = self.file {
187            let mut string: Vec<u8> = vec![0];
188            let mut cursor = Cursor::new(&mut string);
189
190            self.cookie_store
191                .read()
192                .await
193                .save_json(&mut cursor)
194                .unwrap();
195
196            let mut file = file.lock();
197            file.seek(SeekFrom::Start(0)).await?;
198            file.write_all(&string[..]).await?;
199            file.set_len(string.len().try_into()?).await?;
200            file.sync_all().await?;
201        }
202        Ok(())
203    }
204
205    async fn set_cookies(&self, req: &mut Request) {
206        let cookie_store = self.cookie_store.read().await;
207        let mut matches = cookie_store.matches(req.url());
208
209        // clients "SHOULD" sort by path length
210        matches.sort_by(|a, b| b.path.len().cmp(&a.path.len()));
211
212        let values = matches
213            .iter()
214            .map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
215            .collect::<Vec<_>>()
216            .join("; ");
217
218        req.insert_header(COOKIE, values);
219    }
220
221    async fn store_cookies(&self, request_url: &Url, res: &Response) -> Result<()> {
222        if let Some(set_cookies) = res.header(SET_COOKIE) {
223            let mut cookie_store = self.cookie_store.write().await;
224            for cookie in set_cookies {
225                match cookie_store.parse(cookie.as_str(), request_url) {
226                    Ok(action) => log::trace!("cookie action: {:?}", action),
227                    Err(e) => log::trace!("cookie parse error: {:?}", e),
228                }
229            }
230        }
231
232        self.save().await?;
233
234        Ok(())
235    }
236}