http_security_headers/policy/
hsts.rs1use crate::error::{Error, Result};
7use std::time::Duration;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct StrictTransportSecurity {
28 max_age: Duration,
29 include_subdomains: bool,
30 preload: bool,
31}
32
33impl StrictTransportSecurity {
34 pub fn new(max_age: Duration) -> Self {
49 Self {
50 max_age,
51 include_subdomains: false,
52 preload: false,
53 }
54 }
55
56 pub fn include_subdomains(mut self, include: bool) -> Self {
60 self.include_subdomains = include;
61 self
62 }
63
64 pub fn preload(mut self, preload: bool) -> Self {
69 self.preload = preload;
70 self
71 }
72
73 pub fn max_age(&self) -> Duration {
75 self.max_age
76 }
77
78 pub fn includes_subdomains(&self) -> bool {
80 self.include_subdomains
81 }
82
83 pub fn is_preload(&self) -> bool {
85 self.preload
86 }
87
88 pub fn to_header_value(&self) -> Result<String> {
90 let max_age_secs = self.max_age.as_secs();
91
92 if max_age_secs == 0 {
93 return Err(Error::InvalidHsts(
94 "max-age must be greater than 0".to_string(),
95 ));
96 }
97
98 if self.preload {
99 if !self.include_subdomains {
100 return Err(Error::InvalidHsts(
101 "preload requires includeSubDomains to be enabled".to_string(),
102 ));
103 }
104
105 if max_age_secs < 31_536_000 {
106 return Err(Error::InvalidHsts(
107 "preload requires max-age to be at least 31536000 seconds (1 year)"
108 .to_string(),
109 ));
110 }
111 }
112
113 let mut value = format!("max-age={}", max_age_secs);
114
115 if self.include_subdomains {
116 value.push_str("; includeSubDomains");
117 }
118
119 if self.preload {
120 value.push_str("; preload");
121 }
122
123 Ok(value)
124 }
125
126 pub fn parse(value: &str) -> Result<Self> {
136 let mut max_age = None;
137 let mut include_subdomains = false;
138 let mut preload = false;
139
140 for directive in value.split(';').map(|s| s.trim()) {
141 if directive.starts_with("max-age=") {
142 let age_str = directive.trim_start_matches("max-age=");
143 let age_secs = age_str.parse::<u64>().map_err(|_| {
144 Error::InvalidHsts(format!("Invalid max-age value: '{}'", age_str))
145 })?;
146 max_age = Some(Duration::from_secs(age_secs));
147 } else if directive.eq_ignore_ascii_case("includeSubDomains") {
148 include_subdomains = true;
149 } else if directive.eq_ignore_ascii_case("preload") {
150 preload = true;
151 }
152 }
153
154 let max_age = max_age.ok_or_else(|| Error::InvalidHsts("Missing max-age directive".to_string()))?;
155
156 if preload && !include_subdomains {
157 return Err(Error::InvalidHsts(
158 "preload requires the includeSubDomains directive".to_string(),
159 ));
160 }
161
162 if preload && max_age.as_secs() < 31_536_000 {
163 return Err(Error::InvalidHsts(
164 "preload requires max-age to be at least 31536000 seconds (1 year)".to_string(),
165 ));
166 }
167
168 Ok(Self {
169 max_age,
170 include_subdomains,
171 preload,
172 })
173 }
174}
175
176impl std::fmt::Display for StrictTransportSecurity {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 write!(f, "{}", self.to_header_value().unwrap_or_default())
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_new() {
188 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000));
189 assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
190 assert!(!hsts.includes_subdomains());
191 assert!(!hsts.is_preload());
192 }
193
194 #[test]
195 fn test_builder() {
196 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000))
197 .include_subdomains(true)
198 .preload(true);
199
200 assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
201 assert!(hsts.includes_subdomains());
202 assert!(hsts.is_preload());
203 }
204
205 #[test]
206 fn test_to_header_value() {
207 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000));
208 assert_eq!(hsts.to_header_value().unwrap(), "max-age=31536000");
209
210 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000))
211 .include_subdomains(true);
212 assert_eq!(
213 hsts.to_header_value().unwrap(),
214 "max-age=31536000; includeSubDomains"
215 );
216
217 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000))
218 .include_subdomains(true)
219 .preload(true);
220 assert_eq!(
221 hsts.to_header_value().unwrap(),
222 "max-age=31536000; includeSubDomains; preload"
223 );
224 }
225
226 #[test]
227 fn test_to_header_value_zero_max_age() {
228 let hsts = StrictTransportSecurity::new(Duration::from_secs(0));
229 assert!(hsts.to_header_value().is_err());
230 }
231
232 #[test]
233 fn test_to_header_value_invalid_preload() {
234 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000)).preload(true);
235 assert!(hsts.to_header_value().is_err());
236
237 let hsts = StrictTransportSecurity::new(Duration::from_secs(60))
238 .include_subdomains(true)
239 .preload(true);
240 assert!(hsts.to_header_value().is_err());
241 }
242
243 #[test]
244 fn test_parse() {
245 let hsts = StrictTransportSecurity::parse("max-age=31536000").unwrap();
246 assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
247 assert!(!hsts.includes_subdomains());
248 assert!(!hsts.is_preload());
249
250 let hsts =
251 StrictTransportSecurity::parse("max-age=31536000; includeSubDomains").unwrap();
252 assert!(hsts.includes_subdomains());
253
254 let hsts = StrictTransportSecurity::parse("max-age=31536000; includeSubDomains; preload")
255 .unwrap();
256 assert!(hsts.includes_subdomains());
257 assert!(hsts.is_preload());
258 }
259
260 #[test]
261 fn test_parse_invalid() {
262 assert!(StrictTransportSecurity::parse("invalid").is_err());
263 assert!(StrictTransportSecurity::parse("max-age=invalid").is_err());
264 assert!(StrictTransportSecurity::parse("").is_err());
265 }
266
267 #[test]
268 fn test_parse_invalid_preload() {
269 assert!(StrictTransportSecurity::parse("max-age=31536000; preload").is_err());
270 assert!(StrictTransportSecurity::parse("max-age=100; includeSubDomains; preload").is_err());
271 }
272
273 #[test]
274 fn test_display() {
275 let hsts = StrictTransportSecurity::new(Duration::from_secs(31536000))
276 .include_subdomains(true);
277 assert_eq!(hsts.to_string(), "max-age=31536000; includeSubDomains");
278 }
279}