headers/common/
strict_transport_security.rs1use std::fmt;
2use std::time::Duration;
3
4use http::{HeaderName, HeaderValue};
5
6use crate::util::{self, IterExt, Seconds};
7use crate::{Error, Header};
8
9#[derive(Clone, Debug, PartialEq)]
44pub struct StrictTransportSecurity {
45 include_subdomains: bool,
48
49 max_age: Seconds,
53}
54
55impl StrictTransportSecurity {
56 pub fn including_subdomains(max_age: Duration) -> StrictTransportSecurity {
62 StrictTransportSecurity {
63 max_age: max_age.into(),
64 include_subdomains: true,
65 }
66 }
67
68 pub fn excluding_subdomains(max_age: Duration) -> StrictTransportSecurity {
70 StrictTransportSecurity {
71 max_age: max_age.into(),
72 include_subdomains: false,
73 }
74 }
75
76 pub fn include_subdomains(&self) -> bool {
80 self.include_subdomains
81 }
82
83 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