Skip to main content

http_security_headers/
config.rs

1//! Security headers configuration.
2//!
3//! This module provides the main configuration type and builder for security headers.
4
5use crate::error::{Error, Result};
6use crate::policy::*;
7
8/// Main security headers configuration.
9///
10/// This struct holds all configured security headers and provides a builder pattern
11/// for ergonomic construction.
12///
13/// # Examples
14///
15/// ```
16/// use http_security_headers::SecurityHeaders;
17/// use std::time::Duration;
18///
19/// let headers = SecurityHeaders::builder()
20///     .strict_transport_security(Duration::from_secs(31536000), true, false)
21///     .x_frame_options_deny()
22///     .referrer_policy_no_referrer()
23///     .build()
24///     .unwrap();
25/// ```
26#[derive(Debug, Clone)]
27pub struct SecurityHeaders {
28    pub(crate) content_security_policy: Option<ContentSecurityPolicy>,
29    pub(crate) strict_transport_security: Option<StrictTransportSecurity>,
30    pub(crate) x_frame_options: Option<XFrameOptions>,
31    pub(crate) x_content_type_options: bool,
32    pub(crate) referrer_policy: Option<ReferrerPolicy>,
33    pub(crate) cross_origin_opener_policy: Option<CrossOriginOpenerPolicy>,
34    pub(crate) cross_origin_embedder_policy: Option<CrossOriginEmbedderPolicy>,
35    pub(crate) cross_origin_resource_policy: Option<CrossOriginResourcePolicy>,
36}
37
38impl SecurityHeaders {
39    /// Creates a new builder for SecurityHeaders.
40    pub fn builder() -> SecurityHeadersBuilder {
41        SecurityHeadersBuilder::default()
42    }
43
44    /// Returns the Content-Security-Policy if configured.
45    pub fn content_security_policy(&self) -> Option<&ContentSecurityPolicy> {
46        self.content_security_policy.as_ref()
47    }
48
49    /// Returns the Strict-Transport-Security policy if configured.
50    pub fn strict_transport_security(&self) -> Option<&StrictTransportSecurity> {
51        self.strict_transport_security.as_ref()
52    }
53
54    /// Returns the X-Frame-Options policy if configured.
55    pub fn x_frame_options(&self) -> Option<XFrameOptions> {
56        self.x_frame_options
57    }
58
59    /// Returns whether X-Content-Type-Options: nosniff is enabled.
60    pub fn x_content_type_options_enabled(&self) -> bool {
61        self.x_content_type_options
62    }
63
64    /// Returns the Referrer-Policy if configured.
65    pub fn referrer_policy(&self) -> Option<ReferrerPolicy> {
66        self.referrer_policy
67    }
68
69    /// Returns the Cross-Origin-Opener-Policy if configured.
70    pub fn cross_origin_opener_policy(&self) -> Option<CrossOriginOpenerPolicy> {
71        self.cross_origin_opener_policy
72    }
73
74    /// Returns the Cross-Origin-Embedder-Policy if configured.
75    pub fn cross_origin_embedder_policy(&self) -> Option<CrossOriginEmbedderPolicy> {
76        self.cross_origin_embedder_policy
77    }
78
79    /// Returns the Cross-Origin-Resource-Policy if configured.
80    pub fn cross_origin_resource_policy(&self) -> Option<CrossOriginResourcePolicy> {
81        self.cross_origin_resource_policy
82    }
83}
84
85/// Builder for SecurityHeaders.
86///
87/// Provides a fluent interface for configuring security headers.
88#[derive(Debug, Default)]
89pub struct SecurityHeadersBuilder {
90    content_security_policy: Option<ContentSecurityPolicy>,
91    strict_transport_security: Option<StrictTransportSecurity>,
92    x_frame_options: Option<XFrameOptions>,
93    x_content_type_options: bool,
94    referrer_policy: Option<ReferrerPolicy>,
95    cross_origin_opener_policy: Option<CrossOriginOpenerPolicy>,
96    cross_origin_embedder_policy: Option<CrossOriginEmbedderPolicy>,
97    cross_origin_resource_policy: Option<CrossOriginResourcePolicy>,
98}
99
100impl SecurityHeadersBuilder {
101    /// Sets the Content-Security-Policy.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use http_security_headers::{SecurityHeaders, ContentSecurityPolicy};
107    ///
108    /// let csp = ContentSecurityPolicy::new()
109    ///     .default_src(vec!["'self'"])
110    ///     .script_src(vec!["'self'", "'unsafe-inline'"]);
111    ///
112    /// let headers = SecurityHeaders::builder()
113    ///     .content_security_policy(csp)
114    ///     .build()
115    ///     .unwrap();
116    /// ```
117    pub fn content_security_policy(mut self, policy: ContentSecurityPolicy) -> Self {
118        self.content_security_policy = Some(policy);
119        self
120    }
121
122    /// Sets the Strict-Transport-Security header.
123    ///
124    /// # Arguments
125    ///
126    /// * `max_age` - Duration for the max-age directive
127    /// * `include_subdomains` - Whether to include the includeSubDomains directive
128    /// * `preload` - Whether to include the preload directive
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use http_security_headers::SecurityHeaders;
134    /// use std::time::Duration;
135    ///
136    /// let headers = SecurityHeaders::builder()
137    ///     .strict_transport_security(Duration::from_secs(31536000), true, false)
138    ///     .build()
139    ///     .unwrap();
140    /// ```
141    pub fn strict_transport_security(
142        mut self,
143        max_age: std::time::Duration,
144        include_subdomains: bool,
145        preload: bool,
146    ) -> Self {
147        let mut hsts = StrictTransportSecurity::new(max_age);
148        if include_subdomains {
149            hsts = hsts.include_subdomains(true);
150        }
151        if preload {
152            hsts = hsts.preload(true);
153        }
154        self.strict_transport_security = Some(hsts);
155        self
156    }
157
158    /// Sets the Strict-Transport-Security header with a custom policy.
159    pub fn strict_transport_security_policy(mut self, policy: StrictTransportSecurity) -> Self {
160        self.strict_transport_security = Some(policy);
161        self
162    }
163
164    /// Sets X-Frame-Options to DENY.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use http_security_headers::SecurityHeaders;
170    ///
171    /// let headers = SecurityHeaders::builder()
172    ///     .x_frame_options_deny()
173    ///     .build()
174    ///     .unwrap();
175    /// ```
176    pub fn x_frame_options_deny(mut self) -> Self {
177        self.x_frame_options = Some(XFrameOptions::Deny);
178        self
179    }
180
181    /// Sets X-Frame-Options to SAMEORIGIN.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use http_security_headers::SecurityHeaders;
187    ///
188    /// let headers = SecurityHeaders::builder()
189    ///     .x_frame_options_sameorigin()
190    ///     .build()
191    ///     .unwrap();
192    /// ```
193    pub fn x_frame_options_sameorigin(mut self) -> Self {
194        self.x_frame_options = Some(XFrameOptions::SameOrigin);
195        self
196    }
197
198    /// Sets the X-Frame-Options header with a custom value.
199    pub fn x_frame_options(mut self, policy: XFrameOptions) -> Self {
200        self.x_frame_options = Some(policy);
201        self
202    }
203
204    /// Enables X-Content-Type-Options: nosniff.
205    ///
206    /// This is enabled by default in preset configurations.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use http_security_headers::SecurityHeaders;
212    ///
213    /// let headers = SecurityHeaders::builder()
214    ///     .x_content_type_options_nosniff()
215    ///     .build()
216    ///     .unwrap();
217    /// ```
218    pub fn x_content_type_options_nosniff(mut self) -> Self {
219        self.x_content_type_options = true;
220        self
221    }
222
223    /// Sets the Referrer-Policy header.
224    pub fn referrer_policy(mut self, policy: ReferrerPolicy) -> Self {
225        self.referrer_policy = Some(policy);
226        self
227    }
228
229    /// Sets Referrer-Policy to no-referrer.
230    pub fn referrer_policy_no_referrer(mut self) -> Self {
231        self.referrer_policy = Some(ReferrerPolicy::NoReferrer);
232        self
233    }
234
235    /// Sets Referrer-Policy to strict-origin-when-cross-origin.
236    pub fn referrer_policy_strict_origin_when_cross_origin(mut self) -> Self {
237        self.referrer_policy = Some(ReferrerPolicy::StrictOriginWhenCrossOrigin);
238        self
239    }
240
241    /// Sets the Cross-Origin-Opener-Policy header.
242    pub fn cross_origin_opener_policy(mut self, policy: CrossOriginOpenerPolicy) -> Self {
243        self.cross_origin_opener_policy = Some(policy);
244        self
245    }
246
247    /// Sets the Cross-Origin-Embedder-Policy header.
248    pub fn cross_origin_embedder_policy(mut self, policy: CrossOriginEmbedderPolicy) -> Self {
249        self.cross_origin_embedder_policy = Some(policy);
250        self
251    }
252
253    /// Sets the Cross-Origin-Resource-Policy header.
254    pub fn cross_origin_resource_policy(mut self, policy: CrossOriginResourcePolicy) -> Self {
255        self.cross_origin_resource_policy = Some(policy);
256        self
257    }
258
259    /// Builds the SecurityHeaders configuration.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the configuration is invalid.
264    pub fn build(self) -> Result<SecurityHeaders> {
265        if let Some(csp) = &self.content_security_policy {
266            csp.to_header_value()?;
267        }
268
269        if let Some(hsts) = &self.strict_transport_security {
270            hsts.to_header_value()?;
271        }
272
273        // Validate that at least one header is configured
274        if self.content_security_policy.is_none()
275            && self.strict_transport_security.is_none()
276            && self.x_frame_options.is_none()
277            && !self.x_content_type_options
278            && self.referrer_policy.is_none()
279            && self.cross_origin_opener_policy.is_none()
280            && self.cross_origin_embedder_policy.is_none()
281            && self.cross_origin_resource_policy.is_none()
282        {
283            return Err(Error::ValidationFailed(
284                "At least one security header must be configured".to_string(),
285            ));
286        }
287
288        Ok(SecurityHeaders {
289            content_security_policy: self.content_security_policy,
290            strict_transport_security: self.strict_transport_security,
291            x_frame_options: self.x_frame_options,
292            x_content_type_options: self.x_content_type_options,
293            referrer_policy: self.referrer_policy,
294            cross_origin_opener_policy: self.cross_origin_opener_policy,
295            cross_origin_embedder_policy: self.cross_origin_embedder_policy,
296            cross_origin_resource_policy: self.cross_origin_resource_policy,
297        })
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::time::Duration;
305
306    #[test]
307    fn test_builder_empty_fails() {
308        let result = SecurityHeaders::builder().build();
309        assert!(result.is_err());
310    }
311
312    #[test]
313    fn test_builder_with_hsts() {
314        let headers = SecurityHeaders::builder()
315            .strict_transport_security(Duration::from_secs(31536000), true, false)
316            .build()
317            .unwrap();
318
319        assert!(headers.strict_transport_security().is_some());
320        let hsts = headers.strict_transport_security().unwrap();
321        assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
322        assert!(hsts.includes_subdomains());
323        assert!(!hsts.is_preload());
324    }
325
326    #[test]
327    fn test_builder_with_frame_options() {
328        let headers = SecurityHeaders::builder()
329            .x_frame_options_deny()
330            .build()
331            .unwrap();
332
333        assert_eq!(headers.x_frame_options(), Some(XFrameOptions::Deny));
334    }
335
336    #[test]
337    fn test_builder_with_referrer_policy() {
338        let headers = SecurityHeaders::builder()
339            .referrer_policy_no_referrer()
340            .build()
341            .unwrap();
342
343        assert_eq!(headers.referrer_policy(), Some(ReferrerPolicy::NoReferrer));
344    }
345
346    #[test]
347    fn test_builder_with_multiple_headers() {
348        let csp = ContentSecurityPolicy::new().default_src(vec!["'self'"]);
349
350        let headers = SecurityHeaders::builder()
351            .content_security_policy(csp)
352            .strict_transport_security(Duration::from_secs(31536000), true, false)
353            .x_frame_options_deny()
354            .x_content_type_options_nosniff()
355            .referrer_policy_no_referrer()
356            .cross_origin_opener_policy(CrossOriginOpenerPolicy::SameOrigin)
357            .cross_origin_embedder_policy(CrossOriginEmbedderPolicy::RequireCorp)
358            .cross_origin_resource_policy(CrossOriginResourcePolicy::SameOrigin)
359            .build()
360            .unwrap();
361
362        assert!(headers.content_security_policy().is_some());
363        assert!(headers.strict_transport_security().is_some());
364        assert!(headers.x_frame_options().is_some());
365        assert!(headers.x_content_type_options_enabled());
366        assert!(headers.referrer_policy().is_some());
367        assert!(headers.cross_origin_opener_policy().is_some());
368        assert!(headers.cross_origin_embedder_policy().is_some());
369        assert!(headers.cross_origin_resource_policy().is_some());
370    }
371
372    #[test]
373    fn test_builder_with_empty_csp_fails() {
374        let result = SecurityHeaders::builder()
375            .content_security_policy(ContentSecurityPolicy::new())
376            .build();
377
378        assert!(result.is_err());
379    }
380}