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