use std::convert::Infallible;
use std::fmt;
use axum_core::extract::FromRequestParts;
use axum_core::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
use http::StatusCode;
use http::header::{COOKIE, SET_COOKIE};
use http::request::Parts;
use crate::jar::CookieJar;
use crate::report::{PairIssue, Reported};
use crate::set_cookie::SetCookie;
#[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) -> Reported<CookieJar<'_>, PairIssue<'_>> {
CookieJar::parse_bytes(&self.raw)
}
pub fn jar_strict(&self) -> Reported<CookieJar<'_>, PairIssue<'_>> {
CookieJar::parse_bytes_strict(&self.raw)
}
pub fn try_jar(&self) -> Result<CookieJar<'_>, BadCookieHeader> {
Self::clean_or_reject(self.jar())
}
pub fn try_jar_strict(&self) -> Result<CookieJar<'_>, BadCookieHeader> {
Self::clean_or_reject(self.jar_strict())
}
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()
}
}
#[derive(Debug)]
pub struct BadSetCookie {
source: http::header::InvalidHeaderValue,
}
impl fmt::Display for BadSetCookie {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a Set-Cookie value could not form a header value \
(a Raw-encoded value carrying a header-illegal byte)",
)
}
}
impl std::error::Error for BadSetCookie {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
impl IntoResponse for BadSetCookie {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}
impl IntoResponseParts for SetCookie<'_> {
type Error = BadSetCookie;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
let value = http::HeaderValue::try_from(&self).map_err(|source| BadSetCookie { source })?;
res.headers_mut().append(SET_COOKIE, value);
Ok(res)
}
}
impl IntoResponse for SetCookie<'_> {
fn into_response(self) -> Response {
(self, ()).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 values = parts.headers.get_all(COOKIE);
let (count, bytes) = values
.iter()
.fold((0usize, 0usize), |(count, bytes), value| {
(count + 1, bytes + value.as_bytes().len())
});
let mut raw = Vec::with_capacity(bytes + 2 * count.saturating_sub(1));
for value in values {
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().value.get("pref").map(|c| c.value()),
Some("dark mode")
);
assert_eq!(buf.jar().value.get("SID").map(|c| c.value()), Some("x"));
assert_eq!(
buf.jar_strict().value.get("SID").map(|c| c.value()),
Some("x")
);
assert!(buf.jar_strict().value.get("pref").is_none());
}
#[test]
fn empty_header_yields_empty_jars() {
let buf = CookieJarBuf::default();
assert!(buf.jar().value.is_empty());
assert!(buf.jar_strict().value.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().value.get("good").map(|c| c.value()), Some("1"));
assert_eq!(
buf.jar_strict().value.get("SID").map(|c| c.value()),
Some("deadbeef")
);
assert!(buf.jar().value.get("bad").is_none());
}
#[test]
fn set_cookie_into_response_appends_the_header() {
let response = SetCookie::new("SID", "x").secure().into_response();
assert_eq!(response.status(), StatusCode::OK);
let values: Vec<_> = response.headers().get_all(SET_COOKIE).iter().collect();
assert_eq!(values.len(), 1);
assert_eq!(values[0], "SID=x; Secure");
}
#[test]
fn cookies_accumulate_through_append_never_insert() {
let response = (SetCookie::new("a", "1"), SetCookie::new("b", "2"), "ok").into_response();
let values: Vec<String> = response
.headers()
.get_all(SET_COOKIE)
.iter()
.map(|v| v.to_str().unwrap().to_string())
.collect();
assert_eq!(values, ["a=1", "b=2"]);
}
#[test]
fn raw_injection_becomes_the_typed_500_not_a_silent_drop() {
let bad = SetCookie::new("SID", "x\r\nSet-Cookie: evil=1")
.with_encoding(crate::ValueEncoding::Raw);
let response = bad.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(response.headers().get(SET_COOKIE).is_none());
}
#[test]
fn bad_set_cookie_display_never_echoes_cookie_bytes() {
let source = http::HeaderValue::from_str("a\r\nb").unwrap_err();
let error = BadSetCookie { source };
assert_eq!(
error.to_string(),
"a Set-Cookie value could not form a header value \
(a Raw-encoded value carrying a header-illegal byte)"
);
assert!(std::error::Error::source(&error).is_some());
}
}