headers/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;
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    // getters
75
76    /// Get whether this should include subdomains.
77    pub fn include_subdomains(&self) -> bool {
78        self.include_subdomains
79    }
80
81    /// Get the max-age.
82    pub fn max_age(&self) -> Duration {
83        self.max_age.into()
84    }
85}
86
87enum Directive {
88    MaxAge(u64),
89    IncludeSubdomains,
90    Unknown,
91}
92
93fn from_str(s: &str) -> Result<StrictTransportSecurity, ::Error> {
94    s.split(';')
95        .map(str::trim)
96        .map(|sub| {
97            if sub.eq_ignore_ascii_case("includeSubdomains") {
98                Some(Directive::IncludeSubdomains)
99            } else {
100                let mut sub = sub.splitn(2, '=');
101                match (sub.next(), sub.next()) {
102                    (Some(left), Some(right)) if left.trim().eq_ignore_ascii_case("max-age") => {
103                        right
104                            .trim()
105                            .trim_matches('"')
106                            .parse()
107                            .ok()
108                            .map(Directive::MaxAge)
109                    }
110                    _ => Some(Directive::Unknown),
111                }
112            }
113        })
114        .fold(Some((None, None)), |res, dir| match (res, dir) {
115            (Some((None, sub)), Some(Directive::MaxAge(age))) => Some((Some(age), sub)),
116            (Some((age, None)), Some(Directive::IncludeSubdomains)) => Some((age, Some(()))),
117            (Some((Some(_), _)), Some(Directive::MaxAge(_)))
118            | (Some((_, Some(_))), Some(Directive::IncludeSubdomains))
119            | (_, None) => None,
120            (res, _) => res,
121        })
122        .and_then(|res| match res {
123            (Some(age), sub) => Some(StrictTransportSecurity {
124                max_age: Duration::from_secs(age).into(),
125                include_subdomains: sub.is_some(),
126            }),
127            _ => None,
128        })
129        .ok_or_else(::Error::invalid)
130}
131
132impl ::Header for StrictTransportSecurity {
133    fn name() -> &'static ::HeaderName {
134        &::http::header::STRICT_TRANSPORT_SECURITY
135    }
136
137    fn decode<'i, I: Iterator<Item = &'i ::HeaderValue>>(values: &mut I) -> Result<Self, ::Error> {
138        values
139            .just_one()
140            .and_then(|v| v.to_str().ok())
141            .map(from_str)
142            .unwrap_or_else(|| Err(::Error::invalid()))
143    }
144
145    fn encode<E: Extend<::HeaderValue>>(&self, values: &mut E) {
146        struct Adapter<'a>(&'a StrictTransportSecurity);
147
148        impl<'a> fmt::Display for Adapter<'a> {
149            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150                if self.0.include_subdomains {
151                    write!(f, "max-age={}; includeSubdomains", self.0.max_age)
152                } else {
153                    write!(f, "max-age={}", self.0.max_age)
154                }
155            }
156        }
157
158        values.extend(::std::iter::once(util::fmt(Adapter(self))));
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::super::test_decode;
165    use super::StrictTransportSecurity;
166    use std::time::Duration;
167
168    #[test]
169    fn test_parse_max_age() {
170        let h = test_decode::<StrictTransportSecurity>(&["max-age=31536000"]).unwrap();
171        assert_eq!(
172            h,
173            StrictTransportSecurity {
174                include_subdomains: false,
175                max_age: Duration::from_secs(31536000).into(),
176            }
177        );
178    }
179
180    #[test]
181    fn test_parse_max_age_no_value() {
182        assert_eq!(test_decode::<StrictTransportSecurity>(&["max-age"]), None,);
183    }
184
185    #[test]
186    fn test_parse_quoted_max_age() {
187        let h = test_decode::<StrictTransportSecurity>(&["max-age=\"31536000\""]).unwrap();
188        assert_eq!(
189            h,
190            StrictTransportSecurity {
191                include_subdomains: false,
192                max_age: Duration::from_secs(31536000).into(),
193            }
194        );
195    }
196
197    #[test]
198    fn test_parse_spaces_max_age() {
199        let h = test_decode::<StrictTransportSecurity>(&["max-age = 31536000"]).unwrap();
200        assert_eq!(
201            h,
202            StrictTransportSecurity {
203                include_subdomains: false,
204                max_age: Duration::from_secs(31536000).into(),
205            }
206        );
207    }
208
209    #[test]
210    fn test_parse_include_subdomains() {
211        let h = test_decode::<StrictTransportSecurity>(&["max-age=15768000 ; includeSubDomains"])
212            .unwrap();
213        assert_eq!(
214            h,
215            StrictTransportSecurity {
216                include_subdomains: true,
217                max_age: Duration::from_secs(15768000).into(),
218            }
219        );
220    }
221
222    #[test]
223    fn test_parse_no_max_age() {
224        assert_eq!(
225            test_decode::<StrictTransportSecurity>(&["includeSubdomains"]),
226            None,
227        );
228    }
229
230    #[test]
231    fn test_parse_max_age_nan() {
232        assert_eq!(
233            test_decode::<StrictTransportSecurity>(&["max-age = izzy"]),
234            None,
235        );
236    }
237
238    #[test]
239    fn test_parse_duplicate_directives() {
240        assert_eq!(
241            test_decode::<StrictTransportSecurity>(&["max-age=1; max-age=2"]),
242            None,
243        );
244    }
245}
246
247//bench_header!(bench, StrictTransportSecurity, { vec![b"max-age=15768000 ; includeSubDomains".to_vec()] });