pub use cookie::Cookie;
use http::StatusCode;
use std::{
ops::{Deref, DerefMut},
str::FromStr,
};
use http_kit::{
header::{self, HeaderValue},
http_error, Request, Response,
};
use skyzen_core::{Extractor, Responder};
#[derive(Debug)]
pub struct CookieJar(cookie::CookieJar);
impl Deref for CookieJar {
type Target = cookie::CookieJar;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CookieJar {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromStr for CookieJar {
type Err = cookie::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cookies = cookie::Cookie::split_parse_encoded(s);
let mut jar = cookie::CookieJar::new();
for cookie in cookies {
let cookie = cookie?.into_owned();
jar.add_original(cookie);
}
Ok(Self(jar))
}
}
http_error!(
pub CookieParseError, StatusCode::BAD_REQUEST, "Failed to parse cookies"
);
impl Extractor for CookieJar {
type Error = CookieParseError;
async fn extract(request: &mut Request) -> Result<Self, Self::Error> {
let cookie = request
.headers()
.get(header::COOKIE)
.map_or(&[] as &[u8], |v| v.as_bytes());
let cookies = core::str::from_utf8(cookie)
.map_err(|_| CookieParseError::new())?
.parse::<Self>()
.map_err(|_| CookieParseError::new())?;
Ok(cookies)
}
}
http_error!(
pub CookieSetError, StatusCode::SERVICE_UNAVAILABLE, "Failed to set cookies"
);
impl Responder for CookieJar {
type Error = CookieSetError;
fn respond_to(self, _request: &Request, response: &mut Response) -> Result<(), Self::Error> {
for cookie in self.0.delta() {
response.headers_mut().append(
header::SET_COOKIE,
HeaderValue::try_from(cookie.encoded().to_string())
.map_err(|_| CookieSetError::new())?,
); }
Ok(())
}
}
#[cfg(test)]
mod tests {
use http_kit::{
header::{HeaderValue, COOKIE, SET_COOKIE},
HttpError,
};
use skyzen_core::{Extractor, Responder};
use super::{Cookie, CookieJar};
use crate::{Body, Request, Response, StatusCode};
#[tokio::test]
async fn extracts_multiple_percent_encoded_cookies() {
let mut request = Request::new(Body::empty());
request.headers_mut().insert(
COOKIE,
HeaderValue::from_static("session=abc%20123; theme=dark"),
);
let jar = CookieJar::extract(&mut request).await.unwrap();
assert_eq!(jar.get("session").unwrap().value(), "abc 123");
assert_eq!(jar.get("theme").unwrap().value(), "dark");
}
#[tokio::test]
async fn rejects_invalid_utf8_cookie_headers() {
let mut request = Request::new(Body::empty());
request
.headers_mut()
.insert(COOKIE, HeaderValue::from_bytes(b"\xFF").unwrap());
let error = CookieJar::extract(&mut request).await.unwrap_err();
assert_eq!(error.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn responder_emits_only_cookie_delta_headers() {
let request = Request::new(Body::empty());
let mut response = Response::new(Body::empty());
let mut jar: CookieJar = "session=old".parse().unwrap();
jar.add(Cookie::new("theme", "dark"));
jar.respond_to(&request, &mut response).unwrap();
let set_cookies: Vec<_> = response
.headers()
.get_all(SET_COOKIE)
.iter()
.map(|value| value.to_str().unwrap().to_owned())
.collect();
assert_eq!(set_cookies.len(), 1);
assert!(set_cookies[0].starts_with("theme=dark"));
assert!(set_cookies
.iter()
.all(|value| !value.starts_with("session=old")));
}
}