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}