rama_http_headers/
client_hints.rs

1macro_rules! client_hint {
2    (
3        #[doc = $ch_doc:literal]
4        pub enum ClientHint {
5            $(
6                #[doc = $doc:literal]
7                $name:ident($($str:literal),*),
8            )+
9        }
10    ) => {
11        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12        pub enum ClientHint {
13            $(
14                #[doc = $doc]
15                $name,
16            )+
17        }
18
19        impl ClientHint {
20            #[doc = "Checks if the client hint is low entropy, meaning that it will be send by default."]
21            pub fn is_low_entropy(&self) -> bool {
22                matches!(self, Self::SaveData | Self::Ua | Self::Mobile | Self::Platform)
23            }
24
25            #[inline]
26            #[doc = "Attempts to convert a `HeaderName` to a `ClientHint`."]
27            pub fn match_header_name(name: &::rama_http_types::HeaderName) -> Option<Self> {
28                name.try_into().ok()
29            }
30
31            #[doc = "Return an iterator of all header names for this client hint."]
32            pub fn iter_header_names(&self) -> impl Iterator<Item = ::rama_http_types::HeaderName> {
33                match self {
34                    $(
35                        Self::$name => vec![$(::rama_http_types::HeaderName::from_static($str),)+].into_iter(),
36                    )+
37                }
38            }
39
40            #[doc = "Returns the preferred string representation of the client hint."]
41            pub fn as_str(&self) -> &'static str {
42                match self {
43                    $(
44                        Self::$name => {
45                            const VARIANTS: &'static [&'static str] = &[$($str,)+];
46                            VARIANTS[0]
47                        },
48                    )+
49                }
50            }
51        }
52
53        rama_utils::macros::error::static_str_error! {
54            /// Client Hint Parsing Error
55            pub struct ClientHintParsingError;
56        }
57
58        impl TryFrom<&str> for ClientHint {
59            type Error = ClientHintParsingError;
60
61            fn try_from(name: &str) -> Result<Self, Self::Error> {
62                rama_utils::macros::match_ignore_ascii_case_str! {
63                    match (name) {
64                        $(
65                            $($str)|+ => Ok(Self::$name),
66                        )+
67                        _ => Err(ClientHintParsingError),
68                    }
69                }
70            }
71        }
72
73        impl TryFrom<String> for ClientHint {
74            type Error = ClientHintParsingError;
75
76            fn try_from(name: String) -> Result<Self, Self::Error> {
77                Self::try_from(name.as_str())
78            }
79        }
80
81        impl TryFrom<::rama_http_types::HeaderName> for ClientHint {
82            type Error = ClientHintParsingError;
83
84            fn try_from(name: ::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
85                Self::try_from(name.as_str())
86            }
87        }
88
89        impl TryFrom<&::rama_http_types::HeaderName> for ClientHint {
90            type Error = ClientHintParsingError;
91
92            fn try_from(name: &::rama_http_types::HeaderName) -> Result<Self, Self::Error> {
93                Self::try_from(name.as_str())
94            }
95        }
96
97        impl std::str::FromStr for ClientHint {
98            type Err = ClientHintParsingError;
99
100            #[inline]
101            fn from_str(s: &str) -> Result<Self, Self::Err> {
102                Self::try_from(s)
103            }
104        }
105
106        impl std::fmt::Display for ClientHint {
107            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108                write!(f, "{}", self.as_str())
109            }
110        }
111
112        impl serde::Serialize for ClientHint {
113            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114            where
115                S: serde::Serializer,
116            {
117                serializer.serialize_str(self.as_str())
118            }
119        }
120
121        impl<'de> serde::Deserialize<'de> for ClientHint {
122            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123            where
124                D: serde::Deserializer<'de>,
125            {
126                use serde::de::Error;
127                let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
128                Self::try_from(s.as_ref()).map_err(D::Error::custom)
129            }
130        }
131
132        #[doc = "Returns an iterator over all client hints."]
133        pub fn all_client_hints() -> impl Iterator<Item = ClientHint> {
134            [
135                $(
136                    ClientHint::$name,
137                )+
138            ].into_iter()
139        }
140
141        #[doc = "Returns an iterator over all client hint header name strings."]
142        pub fn all_client_hint_header_name_strings() -> impl Iterator<Item = &'static str> {
143            [
144                $(
145                    $($str,)+
146                )+
147            ].into_iter()
148        }
149
150        #[doc = "Returns an iterator over all client hint header names."]
151        pub fn all_client_hint_header_names() -> impl Iterator<Item = ::rama_http_types::HeaderName> {
152            all_client_hint_header_name_strings().map(::rama_http_types::HeaderName::from_static)
153        }
154    };
155}
156
157// NOTE: we are open to contributions to this module,
158// e.g. in case you wish typed headers for each or some of these client hint headers,
159// we gladly mentor and guide you in the process.
160
161client_hint! {
162    #[doc = "Client Hints are a set of HTTP Headers and a JavaScript API that allow web browsers to send detailed information about the client device and browser to web servers. They are designed to be a successor to User-Agent, and provide a standardized way for web servers to optimize content for the client without relying on unreliable user-agent string-based detection or browser fingerprinting techniques."]
163    pub enum ClientHint {
164        /// Sec-CH-UA represents a user agent's branding and version.
165        Ua("sec-ch-ua"),
166        /// Sec-CH-UA-Full-Version represents the user agent's full version.
167        FullVersion("sec-ch-ua-full-version"),
168        /// Sec-CH-UA-Full-Version-List represents the full version for each brand in its brands list.
169        FullVersionList("sec-ch-ua-full-version-list"),
170        /// Sec-CH-UA-Platform represents the platform on which a given user agent is executing.
171        Platform("sec-ch-ua-platform"),
172        /// Sec-CH-UA-Platform-Version represents the platform version on which a given user agent is executing.
173        PlatformVersion("sec-ch-ua-platform-version"),
174        /// Sec-CH-UA-Arch represents the architecture of the platform on which a given user agent is executing.
175        Arch("sec-ch-ua-arch"),
176        /// Sec-CH-UA-Bitness represents the bitness of the architecture of the platform on which a given user agent is executing.
177        Bitness("sec-ch-ua-bitness"),
178        /// Sec-CH-UA-WoW64 is used to detect whether or not a user agent binary is running in 32-bit mode on 64-bit Windows.
179        Wow64("sec-ch-ua-wow64"),
180        /// Sec-CH-UA-Model represents the device on which a given user agent is executing.
181        Model("sec-ch-ua-model"),
182        /// Sec-CH-UA-Mobile is used to detect whether or not a user agent prefers a «mobile» user experience.
183        Mobile("sec-ch-ua-mobile"),
184        /// Sec-CH-UA-Form-Factors represents the form-factors of a device, historically represented as a <deviceCompat> token in the User-Agent string.
185        FormFactor("sec-ch-ua-form-factors"),
186        /// Sec-CH-Lang  (or Lang) represents the user's language preference.
187        Lang("sec-ch-lang", "lang"),
188        /// Sec-CH-Save-Data (or Save-Data) represents the user agent's preference for reduced data usage.
189        SaveData("sec-ch-save-data", "save-data"),
190        /// Sec-CH-Width gives a server the layout width of the image.
191        Width("sec-ch-width"),
192        /// Sec-CH-Viewport-Width (or Viewport-Width) is the width of the user's viewport in CSS pixels.
193        ViewportWidth("sec-ch-viewport-width", "viewport-width"),
194        /// Sec-CH-Viewport-Height represents the user-agent's current viewport height.
195        ViewportHeight("sec-ch-viewport-height"),
196        /// Sec-CH-DPR (or DPR) reports the ratio of physical pixels to CSS pixels of the user's screen.
197        Dpr("sec-ch-dpr", "dpr"),
198        /// Sec-CH-Device-Memory (or Device-Memory) reveals the approximate amount of memory the current device has in GiB. Because this information could be used to fingerprint users, the value of Device-Memory is intentionally coarse. Valid values are 0.25, 0.5, 1, 2, 4, and 8.
199        DeviceMemory("sec-ch-device-memory", "device-memory"),
200        /// Sec-CH-RTT (or RTT) provides the approximate Round Trip Time, in milliseconds, on the application layer. The RTT hint, unlike transport layer RTT, includes server processing time. The value of RTT is rounded to the nearest 25 milliseconds to prevent fingerprinting.
201        Rtt("sec-ch-rtt", "rtt"),
202        /// Sec-CH-Downlink (or Downlink) expressed in megabits per second (Mbps), reveals the approximate downstream speed of the user's connection. The value is rounded to the nearest multiple of 25 kilobits per second. Because again, fingerprinting.
203        Downlink("sec-ch-downlink", "downlink"),
204        /// Sec-CH-ECT (or ECT) stands for Effective Connection Type. Its value is one of an enumerated list of connection types, each of which describes a connection within specified ranges of both RTT and Downlink values. Valid values for ECT are 4g, 3g, 2g, and slow-2g.
205        Ect("sec-ch-ect", "ect"),
206        /// Sec-CH-Prefers-Color-Scheme represents the user's preferred color scheme.
207        PrefersColorScheme("sec-ch-prefers-color-scheme"),
208        /// Sec-CH-Prefers-Reduced-Motion is used to detect if the user has requested the system minimize the amount of animation or motion it uses.
209        PrefersReducedMotion("sec-ch-prefers-reduced-motion"),
210        /// Sec-CH-Prefers-Reduced-Transparency is used to detect if the user has requested the system minimize the amount of transparent or translucent layer effects it uses.
211        PrefersReducedTransparency("sec-ch-prefers-reduced-transparency"),
212        /// Sec-CH-Prefers-Contrast is used to detect if the user has requested that the web content is presented with a higher (or lower) contrast.
213        PrefersContrast("sec-ch-prefers-contrast"),
214        /// Sec-CH-Forced-Colors is used to detect if the user agent has enabled a forced colors mode where it enforces a user-chosen limited color palette on the page.
215        ForcedColors("sec-ch-forced-colors"),
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_client_hint_ua_from_str() {
225        let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
226        assert_eq!(hint, ClientHint::Ua);
227    }
228
229    #[test]
230    fn test_client_hint_ua_from_str_lowercase() {
231        let hint = ClientHint::try_from("sec-ch-ua").unwrap();
232        assert_eq!(hint, ClientHint::Ua);
233    }
234
235    #[test]
236    fn test_client_hint_ua_from_str_uppercase() {
237        let hint = ClientHint::try_from("SEC-CH-UA").unwrap();
238        assert_eq!(hint, ClientHint::Ua);
239    }
240
241    #[test]
242    fn test_client_hint_ua_from_str_mixedcase() {
243        let hint = ClientHint::try_from("Sec-CH-UA").unwrap();
244        assert_eq!(hint, ClientHint::Ua);
245    }
246
247    #[test]
248    fn test_client_hint_low_entropy() {
249        let hints = [
250            "Sec-CH-UA",
251            "Sec-CH-UA-Mobile",
252            "Sec-CH-UA-Platform",
253            "Save-Data",
254            "Sec-CH-Save-Data",
255        ];
256
257        for hint in hints {
258            let hint = ClientHint::try_from(hint).expect(hint);
259            assert!(hint.is_low_entropy());
260        }
261    }
262
263    #[test]
264    fn test_client_hint_high_entropy() {
265        let hints = [
266            "Sec-CH-UA-Full-Version",
267            "Sec-CH-UA-Full-Version-List",
268            "Sec-CH-UA-Platform-Version",
269            "Sec-CH-UA-Arch",
270            "Sec-CH-UA-Bitness",
271            "Sec-CH-UA-WoW64",
272            "Sec-CH-UA-Model",
273            "Sec-CH-UA-Form-Factors",
274            "Sec-CH-Width",
275            "Sec-CH-Viewport-Width",
276            "Sec-CH-Viewport-Height",
277            "Sec-CH-DPR",
278            "Sec-CH-Device-Memory",
279            "Sec-CH-RTT",
280            "Sec-CH-Downlink",
281            "Sec-CH-ECT",
282            "Sec-CH-Prefers-Color-Scheme",
283            "Sec-CH-Prefers-Reduced-Motion",
284            "Sec-CH-Prefers-Reduced-Transparency",
285            "Sec-CH-Prefers-Contrast",
286            "Sec-CH-Forced-Colors",
287        ];
288
289        for hint in hints {
290            let hint = ClientHint::try_from(hint).expect(hint);
291            assert!(!hint.is_low_entropy());
292        }
293    }
294
295    #[test]
296    fn test_all_client_hint_header_name_strings_contains_some_hints() {
297        let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
298        assert!(strings.contains(&"sec-ch-ua"), "{:?}", strings);
299    }
300
301    #[test]
302    fn test_all_client_hint_header_names() {
303        let names = all_client_hint_header_names().collect::<Vec<_>>();
304        let strings = all_client_hint_header_name_strings().collect::<Vec<_>>();
305        assert_eq!(names.len(), strings.len());
306        for (name, string) in names.iter().zip(strings.iter()) {
307            assert_eq!(name.as_str(), *string);
308        }
309    }
310}