rama_http_headers/common/
strict_transport_security.rs1use std::fmt;
2use std::time::Duration;
3
4use rama_http_types::{HeaderName, HeaderValue};
5
6use crate::util::{self, IterExt, Seconds};
7use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
8
9#[derive(Clone, Debug, PartialEq)]
46pub struct StrictTransportSecurity {
47 include_subdomains: bool,
50
51 preload: bool,
55
56 max_age: Seconds,
60}
61
62impl StrictTransportSecurity {
63 #[must_use]
69 pub fn including_subdomains_for_max_seconds(max_age: u64) -> Self {
70 Self {
71 max_age: Seconds::new(max_age),
72 include_subdomains: true,
73 preload: false,
74 }
75 }
76
77 #[must_use]
83 pub fn including_subdomains_for_max_duration_rounded(dur: Duration) -> Self {
84 Self {
85 max_age: Seconds::from_duration_rounded(dur),
86 include_subdomains: true,
87 preload: false,
88 }
89 }
90
91 #[must_use]
99 pub fn including_subdomains_for_max_duration(dur: Duration) -> Option<Self> {
100 Seconds::try_from_duration(dur).map(|max_age| Self {
101 max_age,
102 include_subdomains: true,
103 preload: false,
104 })
105 }
106
107 #[must_use]
109 pub fn excluding_subdomains_for_max_seconds(max_age: u64) -> Self {
110 Self {
111 max_age: Seconds::new(max_age),
112 include_subdomains: false,
113 preload: false,
114 }
115 }
116
117 #[must_use]
123 pub fn excluding_subdomains_for_max_duration_rounded(dur: Duration) -> Self {
124 Self {
125 max_age: Seconds::from_duration_rounded(dur),
126 include_subdomains: false,
127 preload: false,
128 }
129 }
130
131 #[must_use]
139 pub fn excluding_subdomains_for_max_duration(dur: Duration) -> Option<Self> {
140 Seconds::try_from_duration(dur).map(|max_age| Self {
141 max_age,
142 include_subdomains: false,
143 preload: false,
144 })
145 }
146
147 rama_utils::macros::generate_set_and_with! {
148 pub fn preload(mut self, preload: bool) -> Self {
157 self.preload = preload;
158 self
159 }
160 }
161
162 #[must_use]
166 pub fn include_subdomains(&self) -> bool {
167 self.include_subdomains
168 }
169
170 #[must_use]
172 pub fn preload(&self) -> bool {
173 self.preload
174 }
175
176 #[must_use]
178 pub fn max_age(&self) -> Duration {
179 self.max_age.into()
180 }
181}
182
183enum Directive {
184 MaxAge(u64),
185 IncludeSubdomains,
186 Preload,
187 Unknown,
188}
189
190fn from_str(s: &str) -> Result<StrictTransportSecurity, Error> {
191 s.split(';')
192 .map(str::trim)
193 .map(|sub| {
194 if sub.eq_ignore_ascii_case("includeSubdomains") {
195 Some(Directive::IncludeSubdomains)
196 } else if sub.eq_ignore_ascii_case("preload") {
197 Some(Directive::Preload)
198 } else {
199 let mut sub = sub.splitn(2, '=');
200 match (sub.next(), sub.next()) {
201 (Some(left), Some(right)) if left.trim().eq_ignore_ascii_case("max-age") => {
202 right
203 .trim()
204 .trim_matches('"')
205 .parse()
206 .ok()
207 .map(Directive::MaxAge)
208 }
209 _ => Some(Directive::Unknown),
210 }
211 }
212 })
213 .try_fold((None, None, None), |res, dir| match (res, dir) {
214 ((None, sub, pre), Some(Directive::MaxAge(age))) => Some((Some(age), sub, pre)),
215 ((age, None, pre), Some(Directive::IncludeSubdomains)) => Some((age, Some(()), pre)),
216 ((age, sub, None), Some(Directive::Preload)) => Some((age, sub, Some(()))),
217 ((Some(_), _, _), Some(Directive::MaxAge(_)))
218 | ((_, Some(_), _), Some(Directive::IncludeSubdomains))
219 | ((_, _, Some(_)), Some(Directive::Preload))
220 | (_, None) => None,
221 (res, _) => Some(res),
222 })
223 .and_then(|res| match res {
224 (Some(age), sub, pre) => Some(StrictTransportSecurity {
225 max_age: Seconds::new(age),
226 include_subdomains: sub.is_some(),
227 preload: pre.is_some(),
228 }),
229 _ => None,
230 })
231 .ok_or_else(Error::invalid)
232}
233
234impl TypedHeader for StrictTransportSecurity {
235 fn name() -> &'static HeaderName {
236 &::rama_http_types::header::STRICT_TRANSPORT_SECURITY
237 }
238}
239
240impl HeaderDecode for StrictTransportSecurity {
241 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
242 values
243 .just_one()
244 .and_then(|v| v.to_str().ok())
245 .map(from_str)
246 .unwrap_or_else(|| Err(Error::invalid()))
247 }
248}
249
250impl HeaderEncode for StrictTransportSecurity {
251 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
252 struct Adapter<'a>(&'a StrictTransportSecurity);
253
254 impl fmt::Display for Adapter<'_> {
255 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
256 write!(f, "max-age={}", self.0.max_age)?;
257 if self.0.include_subdomains {
258 f.write_str("; includeSubDomains")?;
259 }
260 if self.0.preload {
261 f.write_str("; preload")?;
262 }
263 Ok(())
264 }
265 }
266
267 values.extend(::std::iter::once(util::fmt(Adapter(self))));
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::super::test_decode;
274 use super::*;
275
276 #[test]
277 fn test_parse_max_age() {
278 let h = test_decode::<StrictTransportSecurity>(&["max-age=31536000"]).unwrap();
279 assert_eq!(
280 h,
281 StrictTransportSecurity {
282 include_subdomains: false,
283 preload: false,
284 max_age: Seconds::new(31536000),
285 }
286 );
287 }
288
289 #[test]
290 fn test_parse_max_age_no_value() {
291 assert_eq!(test_decode::<StrictTransportSecurity>(&["max-age"]), None,);
292 }
293
294 #[test]
295 fn test_parse_quoted_max_age() {
296 let h = test_decode::<StrictTransportSecurity>(&["max-age=\"31536000\""]).unwrap();
297 assert_eq!(
298 h,
299 StrictTransportSecurity {
300 include_subdomains: false,
301 preload: false,
302 max_age: Seconds::new(31536000),
303 }
304 );
305 }
306
307 #[test]
308 fn test_parse_spaces_max_age() {
309 let h = test_decode::<StrictTransportSecurity>(&["max-age = 31536000"]).unwrap();
310 assert_eq!(
311 h,
312 StrictTransportSecurity {
313 include_subdomains: false,
314 preload: false,
315 max_age: Seconds::new(31536000),
316 }
317 );
318 }
319
320 #[test]
321 fn test_parse_include_subdomains() {
322 let h = test_decode::<StrictTransportSecurity>(&["max-age=15768000 ; includeSubDomains"])
323 .unwrap();
324 assert_eq!(
325 h,
326 StrictTransportSecurity {
327 include_subdomains: true,
328 preload: false,
329 max_age: Seconds::new(15768000),
330 }
331 );
332 }
333
334 #[test]
335 fn test_parse_no_max_age() {
336 assert_eq!(
337 test_decode::<StrictTransportSecurity>(&["includeSubdomains"]),
338 None,
339 );
340 }
341
342 #[test]
343 fn test_parse_max_age_nan() {
344 assert_eq!(
345 test_decode::<StrictTransportSecurity>(&["max-age = izzy"]),
346 None,
347 );
348 }
349
350 #[test]
351 fn test_parse_duplicate_directives() {
352 assert_eq!(
353 test_decode::<StrictTransportSecurity>(&["max-age=1; max-age=2"]),
354 None,
355 );
356 }
357
358 #[test]
359 fn test_parse_preload() {
360 for raw in [
361 "max-age=31536000; includeSubDomains; preload",
362 "max-age=31536000; includeSubDomains; Preload",
363 "max-age=31536000; includeSubDomains; PRELOAD",
364 ] {
365 let h = test_decode::<StrictTransportSecurity>(&[raw]).unwrap_or_else(|| {
366 panic!("failed to decode {raw}");
367 });
368 assert_eq!(
369 h,
370 StrictTransportSecurity {
371 include_subdomains: true,
372 preload: true,
373 max_age: Seconds::new(31536000),
374 }
375 );
376 }
377 }
378
379 #[test]
380 fn test_parse_preload_without_subdomains() {
381 let h = test_decode::<StrictTransportSecurity>(&["max-age=31536000; preload"]).unwrap();
382 assert_eq!(
383 h,
384 StrictTransportSecurity {
385 include_subdomains: false,
386 preload: true,
387 max_age: Seconds::new(31536000),
388 }
389 );
390 }
391
392 #[test]
393 fn test_parse_duplicate_preload_rejected() {
394 assert_eq!(
395 test_decode::<StrictTransportSecurity>(&["max-age=1; preload; preload"]),
396 None,
397 );
398 }
399
400 #[test]
401 fn test_encode_canonical_order() {
402 let sts = StrictTransportSecurity::including_subdomains_for_max_seconds(31_536_000)
403 .with_preload(true);
404 let map = super::super::test_encode(sts);
405 let raw = map
406 .get(StrictTransportSecurity::name())
407 .expect("header set")
408 .to_str()
409 .unwrap()
410 .to_owned();
411 assert_eq!(raw, "max-age=31536000; includeSubDomains; preload");
412 }
413
414 #[test]
415 fn test_encode_preload_excluding_subdomains() {
416 let sts = StrictTransportSecurity::excluding_subdomains_for_max_seconds(31_536_000)
417 .with_preload(true);
418 let map = super::super::test_encode(sts);
419 let raw = map
420 .get(StrictTransportSecurity::name())
421 .expect("header set")
422 .to_str()
423 .unwrap()
424 .to_owned();
425 assert_eq!(raw, "max-age=31536000; preload");
426 }
427
428 #[test]
429 fn test_preload_round_trip_idempotent() {
430 let sts = StrictTransportSecurity::including_subdomains_for_max_seconds(31_536_000)
431 .with_preload(true);
432 let map = super::super::test_encode(sts.clone());
433 let raw = map
434 .get(StrictTransportSecurity::name())
435 .expect("header set")
436 .to_str()
437 .unwrap()
438 .to_owned();
439 let parsed = test_decode::<StrictTransportSecurity>(&[raw.as_str()])
440 .expect("re-decode of canonical form");
441 assert_eq!(parsed, sts);
442 }
443}
444
445