cogo_http/header/common/
strict_transport_security.rs

1use std::fmt;
2use std::str::{self, FromStr};
3
4use unicase::UniCase;
5
6use crate::header::{Header, HeaderFormat, 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/// ```plain
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/// * `max-age=31536000`
31/// * `max-age=15768000 ; includeSubDomains`
32///
33/// # Example
34/// ```
35/// # extern crate cogo_http;
36/// # fn main() {
37/// use cogo_http::header::{Headers, StrictTransportSecurity};
38///
39/// let mut headers = Headers::new();
40///
41/// headers.set(
42///    StrictTransportSecurity::including_subdomains(31536000u64)
43/// );
44/// # }
45/// ```
46#[derive(Clone, PartialEq, Debug)]
47pub struct StrictTransportSecurity {
48    /// Signals the UA that the HSTS Policy applies to this HSTS Host as well as
49    /// any subdomains of the host's domain name.
50    pub include_subdomains: bool,
51
52    /// Specifies the number of seconds, after the reception of the STS header
53    /// field, during which the UA regards the host (from whom the message was
54    /// received) as a Known HSTS Host.
55    pub max_age: u64
56}
57
58impl StrictTransportSecurity {
59    /// Create an STS header that includes subdomains
60    pub fn including_subdomains(max_age: u64) -> StrictTransportSecurity {
61        StrictTransportSecurity {
62            max_age: max_age,
63            include_subdomains: true
64        }
65    }
66
67    /// Create an STS header that excludes subdomains
68    pub fn excluding_subdomains(max_age: u64) -> StrictTransportSecurity {
69        StrictTransportSecurity {
70            max_age: max_age,
71            include_subdomains: false
72        }
73    }
74}
75
76enum Directive {
77    MaxAge(u64),
78    IncludeSubdomains,
79    Unknown
80}
81
82impl FromStr for StrictTransportSecurity {
83    type Err = crate::Error;
84
85    fn from_str(s: &str) -> crate::Result<StrictTransportSecurity> {
86        s.split(';')
87            .map(str::trim)
88            .map(|sub| if UniCase(sub) == UniCase("includeSubdomains") {
89                Ok(Directive::IncludeSubdomains)
90            } else {
91                let mut sub = sub.splitn(2, '=');
92                match (sub.next(), sub.next()) {
93                    (Some(left), Some(right))
94                    if UniCase(left.trim()) == UniCase("max-age") => {
95                        right
96                            .trim()
97                            .trim_matches('"')
98                            .parse()
99                            .map(Directive::MaxAge)
100                    },
101                    _ => Ok(Directive::Unknown)
102                }
103            })
104            .fold(Ok((None, None)), |res, dir| match (res, dir) {
105                (Ok((None, sub)), Ok(Directive::MaxAge(age))) => Ok((Some(age), sub)),
106                (Ok((age, None)), Ok(Directive::IncludeSubdomains)) => Ok((age, Some(()))),
107                (Ok((Some(_), _)), Ok(Directive::MaxAge(_))) => Err(crate::Error::Header),
108                (Ok((_, Some(_))), Ok(Directive::IncludeSubdomains)) => Err(crate::Error::Header),
109                (_, Err(_)) => Err(crate::Error::Header),
110                (res, _) => res
111            })
112            .and_then(|res| match res {
113                (Some(age), sub) => Ok(StrictTransportSecurity {
114                    max_age: age,
115                    include_subdomains: sub.is_some()
116                }),
117                _ => Err(crate::Error::Header)
118            })
119    }
120}
121
122impl Header for StrictTransportSecurity {
123    fn header_name() -> &'static str {
124        "Strict-Transport-Security"
125    }
126
127    fn parse_header(raw: &[Vec<u8>]) -> crate::Result<StrictTransportSecurity> {
128        parsing::from_one_raw_str(raw)
129    }
130}
131
132impl HeaderFormat for StrictTransportSecurity {
133    fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
134        fmt::Display::fmt(self, f)
135    }
136}
137
138impl fmt::Display for StrictTransportSecurity {
139    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140        if self.include_subdomains {
141            write!(f, "max-age={}; includeSubdomains", self.max_age)
142        } else {
143            write!(f, "max-age={}", self.max_age)
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::StrictTransportSecurity;
151    use crate::header::Header;
152
153    #[test]
154    fn test_parse_max_age() {
155        let h = Header::parse_header(&[b"max-age=31536000".to_vec()][..]);
156        assert_eq!(h.ok(), Some(StrictTransportSecurity { include_subdomains: false, max_age: 31536000u64 }));
157    }
158
159    #[test]
160    fn test_parse_max_age_no_value() {
161        let h: crate::Result<StrictTransportSecurity> = Header::parse_header(&[b"max-age".to_vec()][..]);
162        assert!(h.is_err());
163    }
164
165    #[test]
166    fn test_parse_quoted_max_age() {
167        let h = Header::parse_header(&[b"max-age=\"31536000\"".to_vec()][..]);
168        assert_eq!(h.ok(), Some(StrictTransportSecurity { include_subdomains: false, max_age: 31536000u64 }));
169    }
170
171    #[test]
172    fn test_parse_spaces_max_age() {
173        let h = Header::parse_header(&[b"max-age = 31536000".to_vec()][..]);
174        assert_eq!(h.ok(), Some(StrictTransportSecurity { include_subdomains: false, max_age: 31536000u64 }));
175    }
176
177    #[test]
178    fn test_parse_include_subdomains() {
179        let h = Header::parse_header(&[b"max-age=15768000 ; includeSubDomains".to_vec()][..]);
180        assert_eq!(h.ok(), Some(StrictTransportSecurity { include_subdomains: true, max_age: 15768000u64 }));
181    }
182
183    #[test]
184    fn test_parse_no_max_age() {
185        let h: crate::Result<StrictTransportSecurity> = Header::parse_header(&[b"includeSubDomains".to_vec()][..]);
186        assert!(h.is_err());
187    }
188
189    #[test]
190    fn test_parse_max_age_nan() {
191        let h: crate::Result<StrictTransportSecurity> = Header::parse_header(&[b"max-age = derp".to_vec()][..]);
192        assert!(h.is_err());
193    }
194
195    #[test]
196    fn test_parse_duplicate_directives() {
197        assert!(StrictTransportSecurity::parse_header(&[b"max-age=100; max-age=5; max-age=0".to_vec()][..]).is_err());
198    }
199}
200
201bench_header!(bench, StrictTransportSecurity, { vec![b"max-age=15768000 ; includeSubDomains".to_vec()] });