#![warn(clippy::all, missing_docs, nonstandard_style, future_incompatible)]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use cookie::CookieJar;
use http::HeaderValue;
use parking_lot::Mutex;
use std::sync::Arc;
#[doc(inline)]
pub use self::service::{CookieManager, CookieManagerLayer};
#[cfg(feature = "signed")]
pub use self::signed::SignedCookies;
#[cfg(feature = "private")]
pub use self::private::PrivateCookies;
#[cfg(any(feature = "signed", feature = "private"))]
pub use cookie::Key;
pub use cookie::Cookie;
#[doc(inline)]
pub use cookie;
#[cfg(feature = "axum-core")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum-core")))]
mod extract;
#[cfg(feature = "signed")]
mod signed;
#[cfg(feature = "private")]
mod private;
pub mod service;
#[derive(Clone, Debug, Default)]
pub struct Cookies {
inner: Arc<Mutex<Inner>>,
}
impl Cookies {
fn new(headers: Vec<HeaderValue>) -> Self {
let inner = Inner {
headers,
..Default::default()
};
Self {
inner: Arc::new(Mutex::new(inner)),
}
}
pub fn add(&self, cookie: Cookie<'static>) {
let mut inner = self.inner.lock();
inner.changed = true;
inner.jar().add(cookie);
}
pub fn get(&self, name: &str) -> Option<Cookie> {
let mut inner = self.inner.lock();
inner.jar().get(name).cloned()
}
pub fn remove(&self, cookie: Cookie<'static>) {
let mut inner = self.inner.lock();
inner.changed = true;
inner.jar().remove(cookie);
}
pub fn list(&self) -> Vec<Cookie> {
let mut inner = self.inner.lock();
inner.jar().iter().cloned().collect()
}
#[cfg(feature = "signed")]
pub fn signed<'a>(&self, key: &'a cookie::Key) -> SignedCookies<'a> {
SignedCookies::new(self, key)
}
#[cfg(feature = "private")]
pub fn private<'a>(&self, key: &'a cookie::Key) -> PrivateCookies<'a> {
PrivateCookies::new(self, key)
}
}
#[derive(Debug, Default)]
struct Inner {
headers: Vec<HeaderValue>,
jar: Option<CookieJar>,
changed: bool,
}
impl Inner {
fn jar(&mut self) -> &mut CookieJar {
if self.jar.is_none() {
let mut jar = CookieJar::new();
for header in &self.headers {
if let Ok(header_str) = std::str::from_utf8(header.as_bytes()) {
for cookie_str in header_str.split(';') {
if let Ok(cookie) = cookie::Cookie::parse_encoded(cookie_str.to_owned()) {
jar.add_original(cookie);
}
}
}
}
self.jar = Some(jar);
}
self.jar.as_mut().unwrap()
}
}
#[cfg(all(test, feature = "axum-core"))]
mod tests {
use crate::{CookieManagerLayer, Cookies};
use axum::{body::Body, routing::get, Router};
use cookie::Cookie;
use http::{header, Request};
use http_body_util::BodyExt;
use tower::ServiceExt;
fn app() -> Router {
Router::new()
.route(
"/list",
get(|cookies: Cookies| async move {
let mut items = cookies
.list()
.iter()
.map(|c| format!("{}={}", c.name(), c.value()))
.collect::<Vec<_>>();
items.sort();
items.join(", ")
}),
)
.route(
"/add",
get(|cookies: Cookies| async move {
cookies.add(Cookie::new("baz", "3"));
cookies.add(Cookie::new("spam", "4"));
}),
)
.route(
"/remove",
get(|cookies: Cookies| async move {
cookies.remove(Cookie::new("foo", ""));
}),
)
.layer(CookieManagerLayer::new())
}
async fn body_string(body: Body) -> String {
let bytes = body.collect().await.unwrap().to_bytes();
String::from_utf8_lossy(&bytes).into()
}
#[tokio::test]
async fn read_cookies() {
let req = Request::builder()
.uri("/list")
.header(header::COOKIE, "foo=1; bar=2")
.body(Body::empty())
.unwrap();
let res = app().oneshot(req).await.unwrap();
assert_eq!(body_string(res.into_body()).await, "bar=2, foo=1");
}
#[tokio::test]
async fn read_multi_header_cookies() {
let req = Request::builder()
.uri("/list")
.header(header::COOKIE, "foo=1")
.header(header::COOKIE, "bar=2")
.body(Body::empty())
.unwrap();
let res = app().oneshot(req).await.unwrap();
assert_eq!(body_string(res.into_body()).await, "bar=2, foo=1");
}
#[tokio::test]
async fn add_cookies() {
let req = Request::builder()
.uri("/add")
.header(header::COOKIE, "foo=1; bar=2")
.body(Body::empty())
.unwrap();
let res = app().oneshot(req).await.unwrap();
let mut hdrs: Vec<_> = res.headers().get_all(header::SET_COOKIE).iter().collect();
hdrs.sort();
assert_eq!(hdrs, ["baz=3", "spam=4"]);
}
#[tokio::test]
async fn remove_cookies() {
let req = Request::builder()
.uri("/remove")
.header(header::COOKIE, "foo=1; bar=2")
.body(Body::empty())
.unwrap();
let res = app().oneshot(req).await.unwrap();
let mut hdrs = res.headers().get_all(header::SET_COOKIE).iter();
let hdr = hdrs.next().unwrap().to_str().unwrap();
assert!(hdr.starts_with("foo=; Max-Age=0"));
assert_eq!(hdrs.next(), None);
}
}