headers/common/
strict_transport_security.rs
1use std::fmt;
2use std::time::Duration;
3
4use util::{self, IterExt, Seconds};
5
6#[derive(Clone, Debug, PartialEq)]
42pub struct StrictTransportSecurity {
43 include_subdomains: bool,
46
47 max_age: Seconds,
51}
52
53impl StrictTransportSecurity {
54 pub fn including_subdomains(max_age: Duration) -> StrictTransportSecurity {
60 StrictTransportSecurity {
61 max_age: max_age.into(),
62 include_subdomains: true,
63 }
64 }
65
66 pub fn excluding_subdomains(max_age: Duration) -> StrictTransportSecurity {
68 StrictTransportSecurity {
69 max_age: max_age.into(),
70 include_subdomains: false,
71 }
72 }
73
74 pub fn include_subdomains(&self) -> bool {
78 self.include_subdomains
79 }
80
81 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