headers/common/
strict_transport_security.rs

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