ricecoder_providers/
security_headers.rs1use std::collections::HashMap;
7
8pub struct SecurityHeadersBuilder {
10 headers: HashMap<String, String>,
11}
12
13impl SecurityHeadersBuilder {
14 pub fn new() -> Self {
16 let mut headers = HashMap::new();
17
18 headers.insert(
20 "X-Frame-Options".to_string(),
21 "DENY".to_string(),
22 );
23
24 headers.insert(
26 "X-Content-Type-Options".to_string(),
27 "nosniff".to_string(),
28 );
29
30 headers.insert(
32 "X-XSS-Protection".to_string(),
33 "1; mode=block".to_string(),
34 );
35
36 headers.insert(
38 "Referrer-Policy".to_string(),
39 "strict-origin-when-cross-origin".to_string(),
40 );
41
42 headers.insert(
44 "Permissions-Policy".to_string(),
45 "geolocation=(), microphone=(), camera=()".to_string(),
46 );
47
48 headers.insert(
50 "Strict-Transport-Security".to_string(),
51 "max-age=31536000; includeSubDomains".to_string(),
52 );
53
54 headers.insert(
56 "Content-Security-Policy".to_string(),
57 "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:".to_string(),
58 );
59
60 Self { headers }
61 }
62
63 pub fn add_header(&mut self, name: &str, value: &str) -> &mut Self {
65 self.headers.insert(name.to_string(), value.to_string());
66 self
67 }
68
69 pub fn remove_header(&mut self, name: &str) -> &mut Self {
71 self.headers.remove(name);
72 self
73 }
74
75 pub fn build(&self) -> HashMap<String, String> {
77 self.headers.clone()
78 }
79
80 pub fn get_header(&self, name: &str) -> Option<&str> {
82 self.headers.get(name).map(|s| s.as_str())
83 }
84
85 pub fn has_header(&self, name: &str) -> bool {
87 self.headers.contains_key(name)
88 }
89}
90
91impl Default for SecurityHeadersBuilder {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97pub struct SecurityHeadersValidator;
99
100impl SecurityHeadersValidator {
101 pub fn validate(headers: &HashMap<String, String>) -> Result<(), Vec<String>> {
103 let mut missing = Vec::new();
104
105 let required_headers = vec![
106 "X-Frame-Options",
107 "X-Content-Type-Options",
108 "Referrer-Policy",
109 "Strict-Transport-Security",
110 ];
111
112 for header in required_headers {
113 if !headers.contains_key(header) {
114 missing.push(format!("Missing required header: {}", header));
115 }
116 }
117
118 if missing.is_empty() {
119 Ok(())
120 } else {
121 Err(missing)
122 }
123 }
124
125 pub fn is_secure_header(name: &str, value: &str) -> bool {
127 match name {
128 "X-Frame-Options" => {
129 value == "DENY" || value == "SAMEORIGIN"
130 }
131 "X-Content-Type-Options" => {
132 value == "nosniff"
133 }
134 "Referrer-Policy" => {
135 matches!(
136 value,
137 "no-referrer"
138 | "no-referrer-when-downgrade"
139 | "same-origin"
140 | "origin"
141 | "strict-origin"
142 | "origin-when-cross-origin"
143 | "strict-origin-when-cross-origin"
144 | "unsafe-url"
145 )
146 }
147 "Strict-Transport-Security" => {
148 value.contains("max-age=") && value.contains("31536000")
149 }
150 "Content-Security-Policy" => {
151 !value.contains("unsafe-inline") || value.contains("'unsafe-inline'")
152 }
153 _ => true,
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_security_headers_builder_default() {
164 let builder = SecurityHeadersBuilder::new();
165 let headers = builder.build();
166
167 assert!(headers.contains_key("X-Frame-Options"));
168 assert!(headers.contains_key("X-Content-Type-Options"));
169 assert!(headers.contains_key("Referrer-Policy"));
170 assert!(headers.contains_key("Strict-Transport-Security"));
171 }
172
173 #[test]
174 fn test_security_headers_builder_add_header() {
175 let mut builder = SecurityHeadersBuilder::new();
176 builder.add_header("Custom-Header", "custom-value");
177
178 let headers = builder.build();
179 assert_eq!(headers.get("Custom-Header"), Some(&"custom-value".to_string()));
180 }
181
182 #[test]
183 fn test_security_headers_builder_remove_header() {
184 let mut builder = SecurityHeadersBuilder::new();
185 builder.remove_header("X-Frame-Options");
186
187 let headers = builder.build();
188 assert!(!headers.contains_key("X-Frame-Options"));
189 }
190
191 #[test]
192 fn test_security_headers_builder_get_header() {
193 let builder = SecurityHeadersBuilder::new();
194 assert_eq!(builder.get_header("X-Frame-Options"), Some("DENY"));
195 }
196
197 #[test]
198 fn test_security_headers_builder_has_header() {
199 let builder = SecurityHeadersBuilder::new();
200 assert!(builder.has_header("X-Frame-Options"));
201 assert!(!builder.has_header("Non-Existent-Header"));
202 }
203
204 #[test]
205 fn test_security_headers_validator_validate() {
206 let mut headers = HashMap::new();
207 headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
208 headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
209 headers.insert("Referrer-Policy".to_string(), "strict-origin-when-cross-origin".to_string());
210 headers.insert("Strict-Transport-Security".to_string(), "max-age=31536000".to_string());
211
212 let result = SecurityHeadersValidator::validate(&headers);
213 assert!(result.is_ok());
214 }
215
216 #[test]
217 fn test_security_headers_validator_missing_headers() {
218 let headers = HashMap::new();
219 let result = SecurityHeadersValidator::validate(&headers);
220 assert!(result.is_err());
221 let errors = result.unwrap_err();
222 assert!(!errors.is_empty());
223 }
224
225 #[test]
226 fn test_security_headers_validator_is_secure_header() {
227 assert!(SecurityHeadersValidator::is_secure_header("X-Frame-Options", "DENY"));
228 assert!(SecurityHeadersValidator::is_secure_header("X-Frame-Options", "SAMEORIGIN"));
229 assert!(!SecurityHeadersValidator::is_secure_header("X-Frame-Options", "ALLOW-FROM"));
230
231 assert!(SecurityHeadersValidator::is_secure_header("X-Content-Type-Options", "nosniff"));
232 assert!(!SecurityHeadersValidator::is_secure_header("X-Content-Type-Options", "sniff"));
233
234 assert!(SecurityHeadersValidator::is_secure_header("Referrer-Policy", "no-referrer"));
235 assert!(SecurityHeadersValidator::is_secure_header("Referrer-Policy", "strict-origin-when-cross-origin"));
236 }
237
238 #[test]
239 fn test_security_headers_builder_default_values() {
240 let builder = SecurityHeadersBuilder::new();
241 assert_eq!(builder.get_header("X-Frame-Options"), Some("DENY"));
242 assert_eq!(builder.get_header("X-Content-Type-Options"), Some("nosniff"));
243 assert!(builder.get_header("Strict-Transport-Security").unwrap().contains("31536000"));
244 }
245}