use std::convert::Infallible;
use std::fmt;
use axum_core::extract::FromRequestParts;
use axum_core::response::{IntoResponse, Response};
use http::StatusCode;
use http::header::COOKIE;
use http::request::Parts;
use crate::jar::CookieJar;
use crate::report::{PairIssue, Reported};
#[derive(Clone, Debug, Default)]
pub struct CookieJarBuf {
raw: Vec<u8>,
}
impl CookieJarBuf {
pub fn from_header(raw: impl Into<Vec<u8>>) -> Self {
Self { raw: raw.into() }
}
pub fn jar(&self) -> CookieJar<'_> {
CookieJar::parse_bytes(&self.raw)
}
pub fn jar_strict(&self) -> CookieJar<'_> {
CookieJar::parse_bytes_strict(&self.raw)
}
pub fn jar_reported(&self) -> Reported<CookieJar<'_>, PairIssue<'_>> {
CookieJar::parse_bytes_reported(&self.raw)
}
pub fn jar_strict_reported(&self) -> Reported<CookieJar<'_>, PairIssue<'_>> {
CookieJar::parse_bytes_strict_reported(&self.raw)
}
pub fn try_jar(&self) -> Result<CookieJar<'_>, BadCookieHeader> {
Self::clean_or_reject(self.jar_reported())
}
pub fn try_jar_strict(&self) -> Result<CookieJar<'_>, BadCookieHeader> {
Self::clean_or_reject(self.jar_strict_reported())
}
fn clean_or_reject<'a>(
reported: Reported<CookieJar<'a>, PairIssue<'a>>,
) -> Result<CookieJar<'a>, BadCookieHeader> {
match reported.into_result() {
Ok(jar) => Ok(jar),
Err((_, issues)) => Err(BadCookieHeader {
issues: issues.len(),
}),
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.raw
}
}
#[derive(Clone, Debug)]
pub struct BadCookieHeader {
issues: usize,
}
impl BadCookieHeader {
pub fn issues(&self) -> usize {
self.issues
}
}
impl fmt::Display for BadCookieHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} malformed pair(s) in the Cookie header", self.issues)
}
}
impl std::error::Error for BadCookieHeader {}
impl IntoResponse for BadCookieHeader {
fn into_response(self) -> Response {
(StatusCode::BAD_REQUEST, self.to_string()).into_response()
}
}
impl<S> FromRequestParts<S> for CookieJarBuf
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Infallible> {
let mut raw = Vec::new();
for value in parts.headers.get_all(COOKIE) {
if !raw.is_empty() {
raw.extend_from_slice(b"; ");
}
raw.extend_from_slice(value.as_bytes());
}
Ok(Self { raw })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_header_lends_both_views() {
let buf = CookieJarBuf::from_header(r#"a=1; SID=x; pref="dark mode""#);
assert_eq!(buf.as_bytes(), br#"a=1; SID=x; pref="dark mode""#);
assert_eq!(buf.jar().get("pref").map(|c| c.value()), Some("dark mode"));
assert_eq!(buf.jar().get("SID").map(|c| c.value()), Some("x"));
assert_eq!(buf.jar_strict().get("SID").map(|c| c.value()), Some("x"));
assert!(buf.jar_strict().get("pref").is_none());
}
#[test]
fn empty_header_yields_empty_jars() {
let buf = CookieJarBuf::default();
assert!(buf.jar().is_empty());
assert!(buf.jar_strict().is_empty());
assert_eq!(buf.as_bytes(), b"");
}
#[test]
fn obs_text_costs_only_its_own_pair() {
let buf = CookieJarBuf::from_header(&b"good=1; bad=caf\xE9; SID=deadbeef"[..]);
assert_eq!(buf.jar().get("good").map(|c| c.value()), Some("1"));
assert_eq!(
buf.jar_strict().get("SID").map(|c| c.value()),
Some("deadbeef")
);
assert!(buf.jar().get("bad").is_none());
}
}