Skip to main content

topcoat_cookie/
prefix.rs

1use cookie::Cookie;
2
3use crate::Cookies;
4
5/// A [RFC 6265bis] cookie name prefix.
6///
7/// A prefix asks the browser to enforce extra constraints on a cookie based on
8/// its name. For example, apply `__Host-` with [`Cookies::override_prefix_host`].
9///
10/// [RFC 6265bis]: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Prefix {
13    /// The `__Host-` prefix: the cookie must be `Secure`, have `Path=/`, and no
14    /// `Domain`.
15    Host,
16    /// The `__Secure-` prefix: the cookie must be `Secure`.
17    Secure,
18}
19
20impl Prefix {
21    /// The literal prefix string prepended to cookie names.
22    #[must_use]
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Prefix::Host => "__Host-",
26            Prefix::Secure => "__Secure-",
27        }
28    }
29
30    /// Prepends the prefix to the cookie's name, unless it is already present.
31    fn apply_name(self, cookie: &mut Cookie<'static>) {
32        if !cookie.name().starts_with(self.as_str()) {
33            let name = format!("{}{}", self.as_str(), cookie.name());
34            cookie.set_name(name);
35        }
36    }
37
38    /// Strips the prefix from the cookie's name, if present.
39    fn strip_name(self, cookie: &mut Cookie<'static>) {
40        if let Some(stripped) = cookie.name().strip_prefix(self.as_str()) {
41            let stripped = stripped.to_owned();
42            cookie.set_name(stripped);
43        }
44    }
45
46    /// Applies the attributes the prefix requires.
47    ///
48    /// [`Conform::Override`] replaces attributes to meet the prefix's rules.
49    /// [`Conform::Default`] fills only unset attributes and preserves explicit
50    /// values, even when they conflict with those rules.
51    fn conform(self, cookie: &mut Cookie<'static>, mode: Conform) {
52        let force = matches!(mode, Conform::Override);
53
54        // Both prefixes require `Secure`.
55        if force || cookie.secure().is_none() {
56            cookie.set_secure(true);
57        }
58
59        // `__Host-` additionally requires `Path=/` and no `Domain`.
60        if self == Prefix::Host {
61            if force || cookie.path().is_none() {
62                cookie.set_path("/");
63            }
64            if force {
65                cookie.unset_domain();
66            }
67        }
68    }
69}
70
71/// Whether a prefix forces its required attributes or only fills them when
72/// unset. See [`Prefix::conform`].
73#[derive(Debug, Clone, Copy)]
74pub(crate) enum Conform {
75    /// Fill required attributes only when the cookie has not set them.
76    Default,
77    /// Force the required attributes, guaranteeing RFC compliance.
78    Override,
79}
80
81/// A [`Cookies`] adapter that scopes cookies to a name [`Prefix`].
82///
83/// On write it prepends the prefix and applies the prefix's required
84/// attributes; on read it looks the cookie up under its prefixed name and
85/// strips the prefix from the result. Created by
86/// [`Cookies::override_prefix_host`] and the related combinators.
87/// Names may be bare or already carry this adapter's prefix.
88#[derive(Debug, Clone, Copy)]
89pub struct Prefixed<J> {
90    inner: J,
91    prefix: Prefix,
92    conform: Conform,
93}
94
95impl<J> Prefixed<J> {
96    pub(crate) fn new(inner: J, prefix: Prefix, conform: Conform) -> Self {
97        Self {
98            inner,
99            prefix,
100            conform,
101        }
102    }
103
104    /// Prepends the prefix and applies its required attributes under `mode`.
105    fn prepare(&self, cookie: impl Into<Cookie<'static>>, mode: Conform) -> Cookie<'static> {
106        let mut cookie = cookie.into();
107        self.prefix.apply_name(&mut cookie);
108        self.prefix.conform(&mut cookie, mode);
109        cookie
110    }
111}
112
113impl<J: Cookies> Cookies for Prefixed<J> {
114    fn get(&self, name: &str) -> Option<Cookie<'static>> {
115        let mut cookie = if name.starts_with(self.prefix.as_str()) {
116            self.inner.get(name)?
117        } else {
118            self.inner
119                .get(&format!("{}{}", self.prefix.as_str(), name))?
120        };
121        self.prefix.strip_name(&mut cookie);
122        Some(cookie)
123    }
124
125    fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
126        self.inner.add(self.prepare(cookie, self.conform));
127    }
128
129    fn remove<C: Into<Cookie<'static>>>(&self, cookie: C) {
130        // A removal must force the required attributes (notably `Path=/` for
131        // `__Host-`) so the browser matches and clears it.
132        self.inner.remove(self.prepare(cookie, Conform::Override));
133    }
134}