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        // Validate that at least one header is configured
270        if self.content_security_policy.is_none()
271            && self.strict_transport_security.is_none()
272            && self.x_frame_options.is_none()
273            && !self.x_content_type_options
274            && self.referrer_policy.is_none()
275            && self.cross_origin_opener_policy.is_none()
276            && self.cross_origin_embedder_policy.is_none()
277            && self.cross_origin_resource_policy.is_none()
278        {
279            return Err(Error::ValidationFailed(
280                "At least one security header must be configured".to_string(),
281            ));
282        }
283
284        Ok(SecurityHeaders {
285            content_security_policy: self.content_security_policy,
286            strict_transport_security: self.strict_transport_security,
287            x_frame_options: self.x_frame_options,
288            x_content_type_options: self.x_content_type_options,
289            referrer_policy: self.referrer_policy,
290            cross_origin_opener_policy: self.cross_origin_opener_policy,
291            cross_origin_embedder_policy: self.cross_origin_embedder_policy,
292            cross_origin_resource_policy: self.cross_origin_resource_policy,
293        })
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use std::time::Duration;
301
302    #[test]
303    fn test_builder_empty_fails() {
304        let result = SecurityHeaders::builder().build();
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_builder_with_hsts() {
310        let headers = SecurityHeaders::builder()
311            .strict_transport_security(Duration::from_secs(31536000), true, false)
312            .build()
313            .unwrap();
314
315        assert!(headers.strict_transport_security().is_some());
316        let hsts = headers.strict_transport_security().unwrap();
317        assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
318        assert!(hsts.includes_subdomains());
319        assert!(!hsts.is_preload());
320    }
321
322    #[test]
323    fn test_builder_with_frame_options() {
324        let headers = SecurityHeaders::builder()
325            .x_frame_options_deny()
326            .build()
327            .unwrap();
328
329        assert_eq!(headers.x_frame_options(), Some(XFrameOptions::Deny));
330    }
331
332    #[test]
333    fn test_builder_with_referrer_policy() {
334        let headers = SecurityHeaders::builder()
335            .referrer_policy_no_referrer()
336            .build()
337            .unwrap();
338
339        assert_eq!(headers.referrer_policy(), Some(ReferrerPolicy::NoReferrer));
340    }
341
342    #[test]
343    fn test_builder_with_multiple_headers() {
344        let csp = ContentSecurityPolicy::new().default_src(vec!["'self'"]);
345
346        let headers = SecurityHeaders::builder()
347            .content_security_policy(csp)
348            .strict_transport_security(Duration::from_secs(31536000), true, false)
349            .x_frame_options_deny()
350            .x_content_type_options_nosniff()
351            .referrer_policy_no_referrer()
352            .cross_origin_opener_policy(CrossOriginOpenerPolicy::SameOrigin)
353            .cross_origin_embedder_policy(CrossOriginEmbedderPolicy::RequireCorp)
354            .cross_origin_resource_policy(CrossOriginResourcePolicy::SameOrigin)
355            .build()
356            .unwrap();
357
358        assert!(headers.content_security_policy().is_some());
359        assert!(headers.strict_transport_security().is_some());
360        assert!(headers.x_frame_options().is_some());
361        assert!(headers.x_content_type_options_enabled());
362        assert!(headers.referrer_policy().is_some());
363        assert!(headers.cross_origin_opener_policy().is_some());
364        assert!(headers.cross_origin_embedder_policy().is_some());
365        assert!(headers.cross_origin_resource_policy().is_some());
366    }
367
368    #[test]
369    fn test_builder_with_empty_csp_fails() {
370        let result = SecurityHeaders::builder()
371            .content_security_policy(ContentSecurityPolicy::new())
372            .build();
373
374        assert!(result.is_err());
375    }
376}