rwf2 / cookie-rs

HTTP cookie parsing and cookie jar management for Rust.
https://docs.rs/cookie
Apache License 2.0
312 stars 119 forks source link

Support for `__Host-` cookies #214

Closed jakajancar closed 1 year ago

jakajancar commented 1 year ago

According to the spec:

   If a cookie's name begins with "__Host-", the cookie MUST be:

   1.  Set with a "Secure" attribute
   2.  Set from a URI whose "scheme" is considered "secure" by the user
       agent.
   3.  Sent only to the host which set the cookie.  That is, a cookie
       named "__Host-cookie1" set from "https://example.com" MUST NOT
       contain a "Domain" attribute (and will therefore be sent only to
       "example.com", and not to "subdomain.example.com").
   4.  Sent to every request for a host.  That is, a cookie named
       "__Host-cookie1" MUST contain a "Path" attribute with a value of
       "/".

It would be nice if there was Cookie::new_host("name", "value") or similar shortcut which would create a valid host cookie, i.e. prepend __Host-, set the secure attribute, and set the path to /.

SergioBenitez commented 1 year ago

Note that this RFC is the current active draft. I agree it would be nice to support this. I don't think a new constructor is quite what we want, however. Maybe we want a Prefix enum that works a bit like SameSite. I'd be open to reviewing such a design.

I would suggest starting purely with the design (i.e, a set of interface changes and desired semantics) and then we can talk about implementation.

jakajancar commented 1 year ago

I’m not actually sure what a good API is. You probably also want name() to still equal the actual name of the cookie, incl. the prefix, so having prefix as completely separate (vs. just a convenience constructor/setter) is not a good idea.

SergioBenitez commented 1 year ago

Thinking about it more, I think the API we want is that of a prefixed child jar. This means we get the following API:

let mut jar: CookieJar; // some cookie jar
jar.prefixed(Host).add(Cookie::new("foo", "bar")); // prefix the name, set the required values, add to jar
jar.prefixed(Host).get("foo") // => cookie with ("foo", "bar")
jar.get("foo") // => cookie with ("__HOST-foo", "bar")

Here's an implementation (that I haven't tested or checked through thoroughly, at all):

use std::marker::PhantomData;
use std::borrow::{Borrow, BorrowMut, Cow};

use crate::{CookieJar, Cookie};

mod private {
    pub trait Sealed {}

    impl Sealed for super::Host {}
    impl Sealed for super::Secure {}
}

pub struct Host;

impl Prefix for Host {
    const PREFIX: &'static str = "__HOST-";

    fn polish(mut cookie: Cookie<'_>) -> Cookie<'_> {
        cookie.set_secure(true);
        cookie.set_path("/");
        cookie.unset_domain();
        cookie
    }
}

pub struct Secure;

impl Prefix for Secure {
    const PREFIX: &'static str = "__SECURE-";

    fn polish(mut cookie: Cookie<'_>) -> Cookie<'_> {
        cookie.set_secure(true);
        cookie
    }
}

pub trait Prefix: private::Sealed {
    #[doc(hidden)]
    const PREFIX: &'static str;

    fn polish(cookie: Cookie<'_>) -> Cookie<'_>;

    #[doc(hidden)]
    fn prefixed_name(name: &str) -> String {
        format!("{}{name}", Self::PREFIX)
    }

    #[doc(hidden)]
    fn prefix(mut cookie: Cookie<'_>) -> Cookie<'_> {
        use crate::CookieStr;

        cookie.name = CookieStr::Concrete(match cookie.name {
            CookieStr::Concrete(Cow::Owned(mut string)) => {
                string.insert_str(0, Self::PREFIX);
                string.into()
            }
            _ => Self::prefixed_name(cookie.name()).into(),
        });

        cookie
    }

    #[doc(hidden)]
    fn clip(mut cookie: Cookie<'_>) -> Cookie<'_> {
        use std::borrow::Cow::*;
        use crate::CookieStr::*;

        debug_assert!(cookie.name().starts_with(Self::PREFIX));
        if !cookie.name().starts_with(Self::PREFIX) {
            return cookie;
        }

        let len = Self::PREFIX.len();
        cookie.name = match cookie.name {
            Indexed(i, j) => Indexed(i + len, j),
            Concrete(Borrowed(v)) => Concrete(Borrowed(&v[len..])),
            Concrete(Owned(v)) => Concrete(Owned(v[len..].to_string())),
        };

        cookie
    }

    #[doc(hidden)]
    fn refine(cookie: Cookie<'_>) -> Cookie<'_> {
        Self::polish(Self::prefix(cookie))
    }
}

pub struct PrefixJar<P: Prefix, J> {
    parent: J,
    _prefix: PhantomData<P>,
}

impl<P: Prefix, J> PrefixJar<P, J> {
    pub(crate) fn new(parent: J) -> Self {
        Self { parent, _prefix: PhantomData }
    }
}

impl<P: Prefix, J: Borrow<CookieJar>> PrefixJar<P, J> {
    pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
        self.parent.borrow()
            .get(&P::prefixed_name(name))
            .map(|c| P::clip(c.clone()))
    }
}

impl<P: Prefix, J: BorrowMut<CookieJar>> PrefixJar<P, J> {
    pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
        self.parent.borrow_mut().add(P::refine(cookie.into()));
    }

    pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
        self.parent.borrow_mut().add_original(P::refine(cookie.into()));
    }

    pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
        self.parent.borrow_mut().remove(P::refine(cookie.into()));
    }
}
jakajancar commented 1 year ago

That is very clever, I agree. Like a virtual, transparently mapped namespace.