headers_ext/common/
strict_transport_security.rs

1use std::fmt;
2use std::time::Duration;
3
4use util::{self, IterExt, Seconds};
5
6/// `StrictTransportSecurity` header, defined in [RFC6797](https://tools.ietf.org/html/rfc6797)
7///
8/// This specification defines a mechanism enabling web sites to declare
9/// themselves accessible only via secure connections and/or for users to be
10/// able to direct their user agent(s) to interact with given sites only over
11/// secure connections.  This overall policy is referred to as HTTP Strict
12/// Transport Security (HSTS).  The policy is declared by web sites via the
13/// Strict-Transport-Security HTTP response header field and/or by other means,
14/// such as user agent configuration, for example.
15///
16/// # ABNF
17///
18/// ```text
19///      [ directive ]  *( ";" [ directive ] )
20///
21///      directive                 = directive-name [ "=" directive-value ]
22///      directive-name            = token
23///      directive-value           = token | quoted-string
24///
25/// ```
26///
27/// # Example values
28///
29/// * `max-age=31536000`
30/// * `max-age=15768000 ; includeSubdomains`
31///
32/// # Example
33///
34/// ```
35/// # extern crate headers_ext as headers;
36/// use std::time::Duration;
37/// use headers::StrictTransportSecurity;
38///
39/// let sts = StrictTransportSecurity::including_subdomains(Duration::from_secs(31_536_000));
40/// ```
41#[derive(Clone, Debug, PartialEq)]
42pub struct StrictTransportSecurity {
43    /// Signals the UA that the HSTS Policy applies to this HSTS Host as well as
44    /// any subdomains of the host's domain name.
45    include_subdomains: bool,
46
47    /// Specifies the number of seconds, after the reception of the STS header
48    /// field, during which the UA regards the host (from whom the message was
49    /// received) as a Known HSTS Host.
50    max_age: Seconds,
51}
52
53impl StrictTransportSecurity {
54    // NOTE: The two constructors exist to make a user *have* to decide if
55    // subdomains can be included or not, instead of forgetting due to an
56    // incorrect assumption about a default.
57
58    /// Create an STS header that includes subdomains
59    pub fn including_subdomains(max_age: Duration) -> StrictTransportSecurity {
60        StrictTransportSecurity {
61            max_age: max_age.into(),
62            include_subdomains: true
63        }
64    }
65
66    /// Create an STS header that excludes subdomains
67    pub fn excluding_subdomains(max_age: Duration) -> StrictTransportSecurity {
68        StrictTransportSecurity {
69            max_age: max_age.into(),
70            include_subdomains: false
71        }
72    }
73}
74
75enum Directive {
76    MaxAge(u64),
77    IncludeSubdomains,
78    Unknown
79}
80
81fn from_str(s: &str) -> Result<StrictTransportSecurity, ::Error> {
82    s.split(';')
83        .map(str::trim)
84        .map(|sub| if sub.eq_ignore_ascii_case("includeSubdomains") {
85            Some(Directive::IncludeSubdomains)
86        } else {
87            let mut sub = sub.splitn(2, '=');
88            match (sub.next(), sub.next()) {
89                (Some(left), Some(right))
90                if left.trim().eq_ignore_ascii_case("max-age") => {
91                    right
92                        .trim()
93                        .trim_matches('"')
94                        .parse()
95                        .ok()
96                        .map(Directive::MaxAge)
97                },
98                _ => Some(Directive::Unknown)
99            }
100        })
101        .fold(Some((None, None)), |res, dir| match (res, dir) {
102            (Some((None, sub)), Some(Directive::MaxAge(age))) => Some((Some(age), sub)),
103            (Some((age, None)), Some(Directive::IncludeSubdomains)) => Some((age, Some(()))),
104            (Some((Some(_), _)), Some(Directive::MaxAge(_))) |
105            (Some((_, Some(_))), Some(Directive::IncludeSubdomains)) |
106            (_, None) => None,
107            (res, _) => res
108        })
109        .and_then(|res| match res {
110            (Some(age), sub) => Some(StrictTransportSecurity {
111                max_age: Duration::from_secs(age).into(),
112                include_subdomains: sub.is_some()
113            }),
114            _ => None
115        })
116        .ok_or_else(::Error::invalid)
117}
118
119impl ::Header for StrictTransportSecurity {
120    const NAME: &'static ::HeaderName = &::http::header::STRICT_TRANSPORT_SECURITY;
121
122    fn decode<'i, I: Iterator<Item = &'i ::HeaderValue>>(values: &mut I) -> Result<Self, ::Error> {
123        values
124            .just_one()
125            .and_then(|v| v.to_str().ok())
126            .map(from_str)
127            .unwrap_or_else(|| Err(::Error::invalid()))
128    }
129
130
131    fn encode<E: Extend<::HeaderValue>>(&self, values: &mut E) {
132        struct Adapter<'a>(&'a StrictTransportSecurity);
133
134        impl<'a> fmt::Display for Adapter<'a> {
135            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136                if self.0.include_subdomains {
137                    write!(f, "max-age={}; includeSubdomains", self.0.max_age)
138                } else {
139                    write!(f, "max-age={}", self.0.max_age)
140                }
141            }
142        }
143
144        values.extend(::std::iter::once(util::fmt(Adapter(self))));
145    }
146}
147
148
149#[cfg(test)]
150mod tests {
151    use std::time::Duration;
152    use super::StrictTransportSecurity;
153    use super::super::test_decode;
154
155    #[test]
156    fn test_parse_max_age() {
157        let h = test_decode::<StrictTransportSecurity>(&["max-age=31536000"]).unwrap();
158        assert_eq!(h, StrictTransportSecurity {
159            include_subdomains: false,
160            max_age: Duration::from_secs(31536000).into(),
161        });
162    }
163
164    #[test]
165    fn test_parse_max_age_no_value() {
166        assert_eq!(
167            test_decode::<StrictTransportSecurity>(&["max-age"]),
168            None,
169        );
170    }
171
172    #[test]
173    fn test_parse_quoted_max_age() {
174        let h = test_decode::<StrictTransportSecurity>(&["max-age=\"31536000\""]).unwrap();
175        assert_eq!(h, StrictTransportSecurity {
176            include_subdomains: false,
177            max_age: Duration::from_secs(31536000).into(),
178        });
179    }
180
181    #[test]
182    fn test_parse_spaces_max_age() {
183        let h = test_decode::<StrictTransportSecurity>(&["max-age = 31536000"]).unwrap();
184        assert_eq!(h, StrictTransportSecurity {
185            include_subdomains: false,
186            max_age: Duration::from_secs(31536000).into(),
187        });
188    }
189
190    #[test]
191    fn test_parse_include_subdomains() {
192        let h = test_decode::<StrictTransportSecurity>(&["max-age=15768000 ; includeSubDomains"]).unwrap();
193        assert_eq!(h, StrictTransportSecurity {
194            include_subdomains: true,
195            max_age: Duration::from_secs(15768000).into(),
196        });
197    }
198
199    #[test]
200    fn test_parse_no_max_age() {
201        assert_eq!(
202            test_decode::<StrictTransportSecurity>(&["includeSubdomains"]),
203            None,
204        );
205    }
206
207    #[test]
208    fn test_parse_max_age_nan() {
209        assert_eq!(
210            test_decode::<StrictTransportSecurity>(&["max-age = izzy"]),
211            None,
212        );
213    }
214
215    #[test]
216    fn test_parse_duplicate_directives() {
217        assert_eq!(
218            test_decode::<StrictTransportSecurity>(&["max-age=1; max-age=2"]),
219            None,
220        );
221    }
222}
223
224//bench_header!(bench, StrictTransportSecurity, { vec![b"max-age=15768000 ; includeSubDomains".to_vec()] });