hyperx/header/common/
strict_transport_security.rs

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