├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rusty-hook.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── counter-extractor.rs ├── counter.rs ├── hello_world.rs └── signed_private.rs └── src ├── extract.rs ├── lib.rs ├── private.rs ├── service ├── future.rs └── mod.rs └── signed.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | components: rustfmt, clippy 14 | 15 | - name: Check 16 | run: cargo check 17 | 18 | - name: Format 19 | run: cargo fmt -- --check 20 | 21 | - name: Clippy 22 | run: cargo clippy -- -D warnings 23 | 24 | - name: Clippy Examples 25 | run: cargo clippy --examples -- -D warnings 26 | 27 | test: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: stable 34 | 35 | - name: Test 36 | run: cargo test --all-features 37 | 38 | - name: Test Examples 39 | run: cargo test --examples 40 | 41 | readme: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - uses: actions-rs/toolchain@v1 46 | with: 47 | toolchain: stable 48 | 49 | - name: Install `cargo-sync-readme` 50 | uses: actions-rs/install@v0.1 51 | with: 52 | crate: cargo-sync-readme 53 | version: latest 54 | 55 | - name: Is readme in sync? 56 | run: cargo sync-readme -c 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-commit = "cargo sync-readme && git add README.md" 3 | pre-push = """\ 4 | cargo fmt -- --check \ 5 | && cargo test --all-features \ 6 | && cargo test --examples \ 7 | && cargo clippy -- -D warnings \ 8 | && cargo clippy --examples -- -D warnings \ 9 | && cargo sync-readme -c \ 10 | """ 11 | 12 | [logging] 13 | verbose = true 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["imbolc"] 3 | categories = ["web-programming"] 4 | description = "Cookie manager middleware for tower." 5 | edition = "2021" 6 | rust-version = "1.70" 7 | homepage = "https://github.com/imbolc/tower-cookies" 8 | keywords = ["axum", "cookie", "cookies", "tower"] 9 | license = "MIT" 10 | name = "tower-cookies" 11 | readme = "README.md" 12 | repository = "https://github.com/imbolc/tower-cookies" 13 | version = "0.11.0" 14 | 15 | [features] 16 | default = ["axum-core"] 17 | signed = ["cookie/signed"] 18 | private = ["cookie/secure"] 19 | 20 | [dependencies] 21 | axum-core = { version = "0.5", optional = true } 22 | cookie = { version = "0.18", features = ["percent-encode"] } 23 | futures-util = "0.3" 24 | http = "1.0" 25 | parking_lot = "0.12" 26 | pin-project-lite = "0.2" 27 | tower-layer = "0.3" 28 | tower-service = "0.3" 29 | 30 | [dev-dependencies] 31 | axum = "0.8" 32 | rusty-hook = "0.11" 33 | tokio = { version = "1", features = ["rt-multi-thread"] } 34 | tower = "0.5" 35 | tracing-subscriber = "0.3" 36 | http-body-util = "0.1" 37 | 38 | [package.metadata.docs.rs] 39 | all-features = true 40 | rustdoc-args = ["--cfg", "docsrs"] 41 | 42 | [[example]] 43 | name = "counter" 44 | required-features = ["axum-core"] 45 | 46 | [[example]] 47 | name = "hello_world" 48 | required-features = ["axum-core"] 49 | 50 | [[example]] 51 | name = "signed_private" 52 | required-features = ["axum-core", "signed", "private"] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Imbolc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/crates/l/tower-cookies.svg)](https://choosealicense.com/licenses/mit/) 2 | [![Crates.io](https://img.shields.io/crates/v/tower-cookies.svg)](https://crates.io/crates/tower-cookies) 3 | [![Docs.rs](https://docs.rs/tower-cookies/badge.svg)](https://docs.rs/tower-cookies) 4 | 5 | # tower-cookies 6 | 7 | 8 | 9 | A cookie manager middleware built on top of [tower]. 10 | 11 | ## Example 12 | 13 | With [axum]: 14 | 15 | ```rust,no_run 16 | use axum::{routing::get, Router}; 17 | use std::net::SocketAddr; 18 | use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | let app = Router::new() 23 | .route("/", get(handler)) 24 | .layer(CookieManagerLayer::new()); 25 | 26 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 27 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 28 | axum::serve(listener, app.into_make_service()) 29 | .await 30 | .unwrap(); 31 | } 32 | 33 | async fn handler(cookies: Cookies) -> &'static str { 34 | cookies.add(Cookie::new("hello_world", "hello_world")); 35 | 36 | "Check your cookies." 37 | } 38 | ``` 39 | 40 | A complete CRUD cookie example in [examples/counter.rs][example] 41 | 42 | [axum]: https://crates.io/crates/axum 43 | [tower]: https://crates.io/crates/tower 44 | [example]: https://github.com/imbolc/tower-cookies/blob/main/examples/counter.rs 45 | 46 | 47 | 48 | 49 | ## Safety 50 | 51 | This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust. 52 | 53 | 54 | ## Contributing 55 | 56 | We appreciate all kinds of contributions, thank you! 57 | 58 | 59 | ### Note on README 60 | 61 | Most of the readme is automatically copied from the crate documentation by [cargo-sync-readme][]. 62 | This way the readme is always in sync with the docs and examples are tested. 63 | 64 | So if you find a part of the readme you'd like to change between `` 65 | and `` markers, don't edit `README.md` directly, but rather change 66 | the documentation on top of `src/lib.rs` and then synchronize the readme with: 67 | ```bash 68 | cargo sync-readme 69 | ``` 70 | (make sure the cargo command is installed): 71 | ```bash 72 | cargo install cargo-sync-readme 73 | ``` 74 | 75 | If you have [rusty-hook] installed the changes will apply automatically on commit. 76 | 77 | 78 | ## License 79 | 80 | This project is licensed under the [MIT license](LICENSE). 81 | 82 | [cargo-sync-readme]: https://github.com/phaazon/cargo-sync-readme 83 | [rusty-hook]: https://github.com/swellaby/rusty-hook 84 | -------------------------------------------------------------------------------- /examples/counter-extractor.rs: -------------------------------------------------------------------------------- 1 | //! The example illustrates accessing cookies from an 2 | //! [`axum_core::extract::FromRequest::from_request`] implementation. 3 | //! The behavior is the same as `examples/counter.rs` but cookies leveraging is moved into an 4 | //! extractor. 5 | use axum::{routing::get, Router}; 6 | use axum_core::extract::FromRequestParts; 7 | use http::request::Parts; 8 | use std::net::SocketAddr; 9 | use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; 10 | 11 | const COOKIE_NAME: &str = "visited"; 12 | 13 | struct Counter(usize); 14 | 15 | impl FromRequestParts for Counter 16 | where 17 | S: Send + Sync, 18 | { 19 | type Rejection = (http::StatusCode, &'static str); 20 | 21 | async fn from_request_parts(req: &mut Parts, state: &S) -> Result { 22 | let cookies = Cookies::from_request_parts(req, state).await?; 23 | 24 | let visited = cookies 25 | .get(COOKIE_NAME) 26 | .and_then(|c| c.value().parse().ok()) 27 | .unwrap_or(0) 28 | + 1; 29 | cookies.add(Cookie::new(COOKIE_NAME, visited.to_string())); 30 | 31 | Ok(Counter(visited)) 32 | } 33 | } 34 | 35 | #[tokio::main] 36 | async fn main() { 37 | let app = Router::new() 38 | .route("/", get(handler)) 39 | .layer(CookieManagerLayer::new()); 40 | 41 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 42 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 43 | axum::serve(listener, app.into_make_service()) 44 | .await 45 | .unwrap(); 46 | } 47 | 48 | async fn handler(counter: Counter) -> String { 49 | format!("You have visited this page {} times", counter.0) 50 | } 51 | -------------------------------------------------------------------------------- /examples/counter.rs: -------------------------------------------------------------------------------- 1 | use axum::{routing::get, Router}; 2 | use std::net::SocketAddr; 3 | use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; 4 | 5 | const COOKIE_NAME: &str = "visited"; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let app = Router::new() 10 | .route("/", get(handler)) 11 | .layer(CookieManagerLayer::new()); 12 | 13 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 14 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 15 | axum::serve(listener, app.into_make_service()) 16 | .await 17 | .unwrap(); 18 | } 19 | 20 | async fn handler(cookies: Cookies) -> String { 21 | let visited = cookies 22 | .get(COOKIE_NAME) 23 | .and_then(|c| c.value().parse().ok()) 24 | .unwrap_or(0); 25 | if visited > 10 { 26 | cookies.remove(Cookie::new(COOKIE_NAME, "")); 27 | "Counter has been reset".into() 28 | } else { 29 | cookies.add(Cookie::new(COOKIE_NAME, (visited + 1).to_string())); 30 | format!("You've been here {} times before", visited) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use axum::{routing::get, Router}; 2 | use std::net::SocketAddr; 3 | use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let app = Router::new() 8 | .route("/", get(handler)) 9 | .layer(CookieManagerLayer::new()); 10 | 11 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 12 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 13 | axum::serve(listener, app.into_make_service()) 14 | .await 15 | .unwrap(); 16 | } 17 | 18 | async fn handler(cookies: Cookies) -> &'static str { 19 | cookies.add(Cookie::new("hello_world", "hello_world")); 20 | 21 | "Check your cookies." 22 | } 23 | -------------------------------------------------------------------------------- /examples/signed_private.rs: -------------------------------------------------------------------------------- 1 | //! The counter-example using private / signed cookies instead of raw ones 2 | //! Can be run by: `cargo run --all-features --example signed_private` 3 | use axum::{routing::get, Router}; 4 | use std::net::SocketAddr; 5 | use std::sync::OnceLock; 6 | use tower_cookies::{Cookie, CookieManagerLayer, Cookies, Key}; 7 | 8 | const COOKIE_NAME: &str = "visited_private"; 9 | static KEY: OnceLock = OnceLock::new(); 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let my_key: &[u8] = &[0; 64]; // Your real key must be cryptographically random 14 | KEY.set(Key::from(my_key)).ok(); 15 | 16 | let app = Router::new() 17 | .route("/", get(handler)) 18 | .layer(CookieManagerLayer::new()); 19 | 20 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 21 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 22 | axum::serve(listener, app.into_make_service()) 23 | .await 24 | .unwrap(); 25 | } 26 | 27 | async fn handler(cookies: Cookies) -> String { 28 | let key = KEY.get().unwrap(); 29 | let private_cookies = cookies.private(key); // You can use `cookies.signed` as well 30 | 31 | let visited = private_cookies 32 | .get(COOKIE_NAME) 33 | .and_then(|c| c.value().parse().ok()) 34 | .unwrap_or(0); 35 | if visited > 10 { 36 | cookies.remove(Cookie::new(COOKIE_NAME, "")); 37 | "Counter has been reset".into() 38 | } else { 39 | private_cookies.add(Cookie::new(COOKIE_NAME, (visited + 1).to_string())); 40 | format!("You've been here {} times before", visited) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/extract.rs: -------------------------------------------------------------------------------- 1 | use crate::Cookies; 2 | use axum_core::extract::FromRequestParts; 3 | use http::{request::Parts, StatusCode}; 4 | 5 | impl FromRequestParts for Cookies 6 | where 7 | S: Sync + Send, 8 | { 9 | type Rejection = (http::StatusCode, &'static str); 10 | 11 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { 12 | parts.extensions.get::().cloned().ok_or(( 13 | StatusCode::INTERNAL_SERVER_ERROR, 14 | "Can't extract cookies. Is `CookieManagerLayer` enabled?", 15 | )) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A cookie manager middleware built on top of [tower]. 2 | //! 3 | //! ## Example 4 | //! 5 | //! With [axum]: 6 | //! 7 | //! ```rust,no_run 8 | //! use axum::{routing::get, Router}; 9 | //! use std::net::SocketAddr; 10 | //! use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; 11 | //! 12 | //! # #[cfg(feature = "axum-core")] 13 | //! #[tokio::main] 14 | //! async fn main() { 15 | //! let app = Router::new() 16 | //! .route("/", get(handler)) 17 | //! .layer(CookieManagerLayer::new()); 18 | //! 19 | //! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 20 | //! let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 21 | //! axum::serve(listener, app.into_make_service()) 22 | //! .await 23 | //! .unwrap(); 24 | //! } 25 | //! # #[cfg(not(feature = "axum-core"))] 26 | //! # fn main() {} 27 | //! 28 | //! async fn handler(cookies: Cookies) -> &'static str { 29 | //! cookies.add(Cookie::new("hello_world", "hello_world")); 30 | //! 31 | //! "Check your cookies." 32 | //! } 33 | //! ``` 34 | //! 35 | //! A complete CRUD cookie example in [examples/counter.rs][example] 36 | //! 37 | //! [axum]: https://crates.io/crates/axum 38 | //! [tower]: https://crates.io/crates/tower 39 | //! [example]: https://github.com/imbolc/tower-cookies/blob/main/examples/counter.rs 40 | 41 | #![warn(clippy::all, missing_docs, nonstandard_style, future_incompatible)] 42 | #![forbid(unsafe_code)] 43 | #![cfg_attr(docsrs, feature(doc_cfg))] 44 | 45 | use cookie::CookieJar; 46 | use http::HeaderValue; 47 | use parking_lot::Mutex; 48 | use std::sync::Arc; 49 | 50 | #[doc(inline)] 51 | pub use self::service::{CookieManager, CookieManagerLayer}; 52 | 53 | #[cfg(feature = "signed")] 54 | pub use self::signed::SignedCookies; 55 | 56 | #[cfg(feature = "private")] 57 | pub use self::private::PrivateCookies; 58 | 59 | #[cfg(any(feature = "signed", feature = "private"))] 60 | pub use cookie::Key; 61 | 62 | pub use cookie::Cookie; 63 | 64 | #[doc(inline)] 65 | pub use cookie; 66 | 67 | #[cfg(feature = "axum-core")] 68 | #[cfg_attr(docsrs, doc(cfg(feature = "axum-core")))] 69 | mod extract; 70 | 71 | #[cfg(feature = "signed")] 72 | mod signed; 73 | 74 | #[cfg(feature = "private")] 75 | mod private; 76 | 77 | pub mod service; 78 | 79 | /// A parsed on-demand cookie jar. 80 | #[derive(Clone, Debug, Default)] 81 | pub struct Cookies { 82 | inner: Arc>, 83 | } 84 | 85 | impl Cookies { 86 | fn new(headers: Vec) -> Self { 87 | let inner = Inner { 88 | headers, 89 | ..Default::default() 90 | }; 91 | Self { 92 | inner: Arc::new(Mutex::new(inner)), 93 | } 94 | } 95 | 96 | /// Adds [`Cookie`] to this jar. If a [`Cookie`] with the same name already exists, it is 97 | /// replaced with provided cookie. 98 | pub fn add(&self, cookie: Cookie<'static>) { 99 | let mut inner = self.inner.lock(); 100 | inner.changed = true; 101 | inner.jar().add(cookie); 102 | } 103 | 104 | /// Returns the [`Cookie`] with the given name. Returns [`None`] if it doesn't exist. 105 | pub fn get(&self, name: &str) -> Option { 106 | let mut inner = self.inner.lock(); 107 | inner.jar().get(name).cloned() 108 | } 109 | 110 | /// Removes [`Cookie`] from this jar. 111 | /// 112 | /// **To properly generate the removal cookie, `cookie` must contain the same `path` and 113 | /// `domain` as the cookie that was initially set.** In particular, this means that passing a 114 | /// cookie from a browser to this method won't work because browsers don't set the cookie's 115 | /// `path` attribute. 116 | pub fn remove(&self, cookie: Cookie<'static>) { 117 | let mut inner = self.inner.lock(); 118 | inner.changed = true; 119 | inner.jar().remove(cookie); 120 | } 121 | 122 | /// Returns all the [`Cookie`]s present in this jar. 123 | /// 124 | /// This method collects [`Cookie`]s into a vector instead of iterating through them to 125 | /// minimize the mutex locking time. 126 | pub fn list(&self) -> Vec { 127 | let mut inner = self.inner.lock(); 128 | inner.jar().iter().cloned().collect() 129 | } 130 | 131 | /// Returns a child [`SignedCookies`] jar for interations with signed by the `key` cookies. 132 | /// 133 | /// # Example: 134 | /// ``` 135 | /// use cookie::{Cookie, Key}; 136 | /// use tower_cookies::Cookies; 137 | /// 138 | /// let cookies = Cookies::default(); 139 | /// let key = Key::generate(); 140 | /// let signed = cookies.signed(&key); 141 | /// 142 | /// let foo = Cookie::new("foo", "bar"); 143 | /// signed.add(foo.clone()); 144 | /// 145 | /// assert_eq!(signed.get("foo"), Some(foo.clone())); 146 | /// assert_ne!(cookies.get("foo"), Some(foo)); 147 | /// ``` 148 | #[cfg(feature = "signed")] 149 | pub fn signed<'a>(&self, key: &'a cookie::Key) -> SignedCookies<'a> { 150 | SignedCookies::new(self, key) 151 | } 152 | 153 | /// Returns a child [`PrivateCookies`] jar for encrypting and decrypting cookies. 154 | /// 155 | /// # Example: 156 | /// ``` 157 | /// use cookie::{Cookie, Key}; 158 | /// use tower_cookies::Cookies; 159 | /// 160 | /// let cookies = Cookies::default(); 161 | /// let key = Key::generate(); 162 | /// let private = cookies.private(&key); 163 | /// 164 | /// let foo = Cookie::new("foo", "bar"); 165 | /// private.add(foo.clone()); 166 | /// 167 | /// assert_eq!(private.get("foo"), Some(foo.clone())); 168 | /// assert_ne!(cookies.get("foo"), Some(foo)); 169 | /// ``` 170 | #[cfg(feature = "private")] 171 | pub fn private<'a>(&self, key: &'a cookie::Key) -> PrivateCookies<'a> { 172 | PrivateCookies::new(self, key) 173 | } 174 | } 175 | 176 | #[derive(Debug, Default)] 177 | struct Inner { 178 | headers: Vec, 179 | jar: Option, 180 | changed: bool, 181 | } 182 | 183 | impl Inner { 184 | fn jar(&mut self) -> &mut CookieJar { 185 | if self.jar.is_none() { 186 | let mut jar = CookieJar::new(); 187 | for header in &self.headers { 188 | if let Ok(header_str) = std::str::from_utf8(header.as_bytes()) { 189 | for cookie_str in header_str.split(';') { 190 | if let Ok(cookie) = cookie::Cookie::parse_encoded(cookie_str.to_owned()) { 191 | jar.add_original(cookie); 192 | } 193 | } 194 | } 195 | } 196 | self.jar = Some(jar); 197 | } 198 | self.jar.as_mut().unwrap() 199 | } 200 | } 201 | 202 | #[cfg(all(test, feature = "axum-core"))] 203 | mod tests { 204 | use crate::{CookieManagerLayer, Cookies}; 205 | use axum::{body::Body, routing::get, Router}; 206 | use cookie::Cookie; 207 | use http::{header, Request}; 208 | use http_body_util::BodyExt; 209 | use tower::ServiceExt; 210 | 211 | fn app() -> Router { 212 | Router::new() 213 | .route( 214 | "/list", 215 | get(|cookies: Cookies| async move { 216 | let mut items = cookies 217 | .list() 218 | .iter() 219 | .map(|c| format!("{}={}", c.name(), c.value())) 220 | .collect::>(); 221 | items.sort(); 222 | items.join(", ") 223 | }), 224 | ) 225 | .route( 226 | "/add", 227 | get(|cookies: Cookies| async move { 228 | cookies.add(Cookie::new("baz", "3")); 229 | cookies.add(Cookie::new("spam", "4")); 230 | }), 231 | ) 232 | .route( 233 | "/remove", 234 | get(|cookies: Cookies| async move { 235 | cookies.remove(Cookie::new("foo", "")); 236 | }), 237 | ) 238 | .layer(CookieManagerLayer::new()) 239 | } 240 | 241 | async fn body_string(body: Body) -> String { 242 | let bytes = body.collect().await.unwrap().to_bytes(); 243 | String::from_utf8_lossy(&bytes).into() 244 | } 245 | 246 | #[tokio::test] 247 | async fn read_cookies() { 248 | let req = Request::builder() 249 | .uri("/list") 250 | .header(header::COOKIE, "foo=1; bar=2") 251 | .body(Body::empty()) 252 | .unwrap(); 253 | let res = app().oneshot(req).await.unwrap(); 254 | assert_eq!(body_string(res.into_body()).await, "bar=2, foo=1"); 255 | } 256 | 257 | #[tokio::test] 258 | async fn read_multi_header_cookies() { 259 | let req = Request::builder() 260 | .uri("/list") 261 | .header(header::COOKIE, "foo=1") 262 | .header(header::COOKIE, "bar=2") 263 | .body(Body::empty()) 264 | .unwrap(); 265 | let res = app().oneshot(req).await.unwrap(); 266 | assert_eq!(body_string(res.into_body()).await, "bar=2, foo=1"); 267 | } 268 | 269 | #[tokio::test] 270 | async fn add_cookies() { 271 | let req = Request::builder() 272 | .uri("/add") 273 | .header(header::COOKIE, "foo=1; bar=2") 274 | .body(Body::empty()) 275 | .unwrap(); 276 | let res = app().oneshot(req).await.unwrap(); 277 | let mut hdrs: Vec<_> = res.headers().get_all(header::SET_COOKIE).iter().collect(); 278 | hdrs.sort(); 279 | assert_eq!(hdrs, ["baz=3", "spam=4"]); 280 | } 281 | 282 | #[tokio::test] 283 | async fn remove_cookies() { 284 | let req = Request::builder() 285 | .uri("/remove") 286 | .header(header::COOKIE, "foo=1; bar=2") 287 | .body(Body::empty()) 288 | .unwrap(); 289 | let res = app().oneshot(req).await.unwrap(); 290 | let mut hdrs = res.headers().get_all(header::SET_COOKIE).iter(); 291 | let hdr = hdrs.next().unwrap().to_str().unwrap(); 292 | assert!(hdr.starts_with("foo=; Max-Age=0")); 293 | assert_eq!(hdrs.next(), None); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/private.rs: -------------------------------------------------------------------------------- 1 | use crate::Cookies; 2 | use cookie::{Cookie, Key}; 3 | 4 | /// A cookie jar that provides authenticated encryption for its cookies. 5 | /// 6 | /// A _private_ child jar signs and encrypts all the cookies added to it and 7 | /// verifies and decrypts cookies retrieved from it. Any cookies stored in 8 | /// `PrivateCookies` are simultaneously assured confidentiality, integrity, and 9 | /// authenticity. In other words, clients cannot discover nor tamper with the 10 | /// contents of a cookie, nor can they fabricate cookie data. 11 | pub struct PrivateCookies<'a> { 12 | cookies: Cookies, 13 | key: &'a Key, 14 | } 15 | 16 | impl<'a> PrivateCookies<'a> { 17 | /// Creates an instance of `PrivateCookies` with parent `cookies` and key `key`. 18 | /// This method is typically called indirectly via the `private` 19 | /// method of [`Cookies`]. 20 | pub(crate) fn new(cookies: &Cookies, key: &'a Key) -> Self { 21 | Self { 22 | cookies: cookies.clone(), 23 | key, 24 | } 25 | } 26 | 27 | /// Adds `cookie` to the parent jar. The cookie's value is encrypted with 28 | /// authenticated encryption assuring confidentiality, integrity, and 29 | /// authenticity. 30 | pub fn add(&self, cookie: Cookie<'static>) { 31 | let mut inner = self.cookies.inner.lock(); 32 | inner.changed = true; 33 | inner.jar().private_mut(self.key).add(cookie); 34 | } 35 | 36 | /// Returns a reference to the `Cookie` inside this jar with the name `name` 37 | /// and authenticates and decrypts the cookie's value, returning a `Cookie` 38 | /// with the decrypted value. If the cookie cannot be found, or the cookie 39 | /// fails to authenticate or decrypt, `None` is returned. 40 | pub fn get(&self, name: &str) -> Option> { 41 | let mut inner = self.cookies.inner.lock(); 42 | inner.jar().private(self.key).get(name) 43 | } 44 | 45 | /// Removes the `cookie` from the parent jar. 46 | /// 47 | /// **To properly generate the removal cookie, `cookie` must contain the same `path` and 48 | /// `domain` as the cookie that was initially set.** In particular, this means that passing a 49 | /// cookie from a browser to this method won't work because browsers don't set the cookie's 50 | /// `path` attribute. 51 | pub fn remove(&self, cookie: Cookie<'static>) { 52 | self.cookies.remove(cookie); 53 | } 54 | } 55 | 56 | #[cfg(all(test, feature = "private"))] 57 | mod tests { 58 | use crate::Cookies; 59 | use cookie::{Cookie, Key}; 60 | 61 | #[test] 62 | fn get_absent() { 63 | let key = Key::generate(); 64 | let cookies = Cookies::new(vec![]); 65 | assert_eq!(cookies.private(&key).get("foo"), None); 66 | } 67 | 68 | #[test] 69 | fn add_get_private() { 70 | let key = Key::generate(); 71 | let cookies = Cookies::new(vec![]); 72 | let cookie = Cookie::new("foo", "bar"); 73 | let private = cookies.private(&key); 74 | private.add(cookie.clone()); 75 | assert_eq!(private.get("foo").unwrap(), cookie); 76 | } 77 | 78 | #[test] 79 | fn add_private_get_raw() { 80 | let key = Key::generate(); 81 | let cookies = Cookies::new(vec![]); 82 | let cookie = Cookie::new("foo", "bar"); 83 | cookies.private(&key).add(cookie.clone()); 84 | assert_ne!(cookies.get("foo").unwrap(), cookie); 85 | } 86 | 87 | #[test] 88 | fn add_raw_get_private() { 89 | let key = Key::generate(); 90 | let cookies = Cookies::new(vec![]); 91 | let cookie = Cookie::new("foo", "bar"); 92 | cookies.add(cookie); 93 | assert_eq!(cookies.private(&key).get("foo"), None); 94 | } 95 | 96 | #[test] 97 | fn messed_keys() { 98 | let key1 = Key::generate(); 99 | let key2 = Key::generate(); 100 | let cookies = Cookies::new(vec![]); 101 | let cookie = Cookie::new("foo", "bar"); 102 | cookies.private(&key1).add(cookie); 103 | assert_eq!(cookies.private(&key2).get("foo"), None); 104 | } 105 | 106 | #[test] 107 | fn remove() { 108 | let key = Key::generate(); 109 | let cookies = Cookies::new(vec![]); 110 | let private = cookies.private(&key); 111 | private.add(Cookie::new("foo", "bar")); 112 | let cookie = private.get("foo").unwrap(); 113 | private.remove(cookie); 114 | assert!(private.get("foo").is_none()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/service/future.rs: -------------------------------------------------------------------------------- 1 | //! [`Future`] types. 2 | 3 | use crate::Cookies; 4 | use futures_util::ready; 5 | use http::{header, HeaderValue, Response}; 6 | use pin_project_lite::pin_project; 7 | use std::{ 8 | future::Future, 9 | pin::Pin, 10 | task::{Context, Poll}, 11 | }; 12 | 13 | pin_project! { 14 | /// Response future for [`CookieManager`]. 15 | #[derive(Debug)] 16 | pub struct ResponseFuture { 17 | #[pin] 18 | pub(crate) future: F, 19 | pub(crate) cookies: Cookies, 20 | } 21 | } 22 | 23 | impl Future for ResponseFuture 24 | where 25 | F: Future, E>>, 26 | { 27 | type Output = F::Output; 28 | 29 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 30 | let this = self.project(); 31 | let mut res = ready!(this.future.poll(cx)?); 32 | 33 | let mut cookies = this.cookies.inner.lock(); 34 | if cookies.changed { 35 | let values: Vec<_> = cookies 36 | .jar() 37 | .delta() 38 | .filter_map(|c| HeaderValue::from_str(&c.to_string()).ok()) 39 | .collect(); 40 | let headers = res.headers_mut(); 41 | for value in values { 42 | headers.append(header::SET_COOKIE, value); 43 | } 44 | } 45 | 46 | Poll::Ready(Ok(res)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/service/mod.rs: -------------------------------------------------------------------------------- 1 | //! Middleware to use [`Cookies`]. 2 | 3 | use self::future::ResponseFuture; 4 | use crate::Cookies; 5 | use http::{header, Request, Response}; 6 | use std::task::{Context, Poll}; 7 | use tower_layer::Layer; 8 | use tower_service::Service; 9 | 10 | pub mod future; 11 | 12 | /// Middleware to use [`Cookies`]. 13 | #[derive(Clone, Debug)] 14 | pub struct CookieManager { 15 | inner: S, 16 | } 17 | 18 | impl CookieManager { 19 | /// Create a new cookie manager. 20 | pub fn new(inner: S) -> Self { 21 | Self { inner } 22 | } 23 | } 24 | 25 | impl Service> for CookieManager 26 | where 27 | S: Service, Response = Response>, 28 | { 29 | type Response = S::Response; 30 | type Error = S::Error; 31 | type Future = ResponseFuture; 32 | 33 | #[inline] 34 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 35 | self.inner.poll_ready(cx) 36 | } 37 | 38 | fn call(&mut self, mut req: Request) -> Self::Future { 39 | let value = req 40 | .headers() 41 | .get_all(header::COOKIE) 42 | .iter() 43 | .cloned() 44 | .collect(); 45 | let cookies = Cookies::new(value); 46 | req.extensions_mut().insert(cookies.clone()); 47 | 48 | ResponseFuture { 49 | future: self.inner.call(req), 50 | cookies, 51 | } 52 | } 53 | } 54 | 55 | /// Layer to apply [`CookieManager`] middleware. 56 | #[derive(Clone, Debug, Default)] 57 | pub struct CookieManagerLayer { 58 | _priv: (), 59 | } 60 | 61 | impl CookieManagerLayer { 62 | /// Create a new cookie manager layer. 63 | pub fn new() -> Self { 64 | Self { _priv: () } 65 | } 66 | } 67 | 68 | impl Layer for CookieManagerLayer { 69 | type Service = CookieManager; 70 | 71 | fn layer(&self, inner: S) -> Self::Service { 72 | CookieManager { inner } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/signed.rs: -------------------------------------------------------------------------------- 1 | use crate::Cookies; 2 | use cookie::{Cookie, Key}; 3 | 4 | /// A child cookie jar that authenticates its cookies. 5 | /// It signs all the cookies added to it and verifies cookies retrieved from it. 6 | /// Any cookies stored in `SignedCookies` are provided integrity and authenticity. In other 7 | /// words, clients cannot tamper with the contents of a cookie nor can they fabricate cookie 8 | /// values, but the data is visible in plaintext. 9 | pub struct SignedCookies<'a> { 10 | cookies: Cookies, 11 | key: &'a Key, 12 | } 13 | 14 | impl<'a> SignedCookies<'a> { 15 | /// Creates an instance of `SignedCookies` with parent `cookies` and key `key`. This method is 16 | /// typically called indirectly via the `signed` method of [`Cookies`]. 17 | pub(crate) fn new(cookies: &Cookies, key: &'a Key) -> Self { 18 | Self { 19 | cookies: cookies.clone(), 20 | key, 21 | } 22 | } 23 | 24 | /// Adds cookie to the parent jar. The cookie’s value is signed assuring integrity and 25 | /// authenticity. 26 | pub fn add(&self, cookie: Cookie<'static>) { 27 | let mut inner = self.cookies.inner.lock(); 28 | inner.changed = true; 29 | inner.jar().signed_mut(self.key).add(cookie); 30 | } 31 | 32 | /// Returns `Cookie` with the `name` and verifies the authenticity and integrity of the 33 | /// cookie’s value, returning a `Cookie` with the authenticated value. If the cookie cannot be 34 | /// found, or the cookie fails to verify, None is returned. 35 | pub fn get(&self, name: &str) -> Option> { 36 | let mut inner = self.cookies.inner.lock(); 37 | inner.jar().signed(self.key).get(name) 38 | } 39 | 40 | /// Removes the `cookie` from the parent jar. 41 | /// 42 | /// **To properly generate the removal cookie, `cookie` must contain the same `path` and 43 | /// `domain` as the cookie that was initially set.** In particular, this means that passing a 44 | /// cookie from a browser to this method won't work because browsers don't set the cookie's 45 | /// `path` attribute. 46 | pub fn remove(&self, cookie: Cookie<'static>) { 47 | self.cookies.remove(cookie); 48 | } 49 | } 50 | 51 | #[cfg(all(test, feature = "signed"))] 52 | mod tests { 53 | use crate::Cookies; 54 | use cookie::{Cookie, Key}; 55 | 56 | #[test] 57 | fn get_absent() { 58 | let key = Key::generate(); 59 | let cookies = Cookies::new(vec![]); 60 | assert_eq!(cookies.signed(&key).get("foo"), None); 61 | } 62 | 63 | #[test] 64 | fn add_get_signed() { 65 | let key = Key::generate(); 66 | let cookies = Cookies::new(vec![]); 67 | let cookie = Cookie::new("foo", "bar"); 68 | let signed = cookies.signed(&key); 69 | signed.add(cookie.clone()); 70 | assert_eq!(signed.get("foo").unwrap(), cookie); 71 | } 72 | 73 | #[test] 74 | fn add_signed_get_raw() { 75 | let key = Key::generate(); 76 | let cookies = Cookies::new(vec![]); 77 | let cookie = Cookie::new("foo", "bar"); 78 | cookies.signed(&key).add(cookie.clone()); 79 | assert_ne!(cookies.get("foo").unwrap(), cookie); 80 | } 81 | 82 | #[test] 83 | fn add_raw_get_signed() { 84 | let key = Key::generate(); 85 | let cookies = Cookies::new(vec![]); 86 | let cookie = Cookie::new("foo", "bar"); 87 | cookies.add(cookie); 88 | assert_eq!(cookies.signed(&key).get("foo"), None); 89 | } 90 | 91 | #[test] 92 | fn messed_keys() { 93 | let key1 = Key::generate(); 94 | let key2 = Key::generate(); 95 | let cookies = Cookies::new(vec![]); 96 | let cookie = Cookie::new("foo", "bar"); 97 | cookies.signed(&key1).add(cookie); 98 | assert_eq!(cookies.signed(&key2).get("foo"), None); 99 | } 100 | 101 | #[test] 102 | fn remove() { 103 | let key = Key::generate(); 104 | let cookies = Cookies::new(vec![]); 105 | let signed = cookies.signed(&key); 106 | signed.add(Cookie::new("foo", "bar")); 107 | let cookie = signed.get("foo").unwrap(); 108 | signed.remove(cookie); 109 | assert!(signed.get("foo").is_none()); 110 | } 111 | } 112 | --------------------------------------------------------------------------------