1use crate::core::{Error, Result};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Header {
14 pub name: String,
16 pub value: String,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SameSite {
23 Strict,
25 Lax,
27 None,
29}
30
31pub struct SafeHeader;
33
34impl SafeHeader {
35 pub fn has_crlf(s: &str) -> bool {
37 s.contains('\r') || s.contains('\n')
38 }
39
40 pub fn is_valid_name(name: &str) -> bool {
42 if name.is_empty() || name.len() > 256 {
43 return false;
44 }
45 name.chars().all(Self::is_valid_token_char)
46 }
47
48 fn is_valid_token_char(c: char) -> bool {
50 matches!(c,
52 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' |
53 '^' | '_' | '`' | '|' | '~' |
54 'a'..='z' | 'A'..='Z' | '0'..='9'
55 )
56 }
57
58 const DANGEROUS_HEADERS: &'static [&'static str] = &[
60 "proxy-authorization",
61 "proxy-authenticate",
62 "proxy-connection",
63 "transfer-encoding",
64 "content-length",
65 "host",
66 "connection",
67 "keep-alive",
68 "upgrade",
69 "te",
70 "trailer",
71 ];
72
73 pub fn is_dangerous(name: &str) -> bool {
75 let lower = name.to_lowercase();
76 Self::DANGEROUS_HEADERS.contains(&lower.as_str())
77 }
78
79 pub fn make(name: &str, value: &str) -> Result<Header> {
81 let trimmed_name = name.trim();
82 let trimmed_value = value.trim();
83
84 if !Self::is_valid_name(trimmed_name) {
85 return Err(Error::InvalidFormat("Invalid header name".into()));
86 }
87
88 if Self::has_crlf(trimmed_value) {
89 return Err(Error::InvalidFormat("Header value contains CRLF".into()));
90 }
91
92 if trimmed_value.len() > 8192 {
93 return Err(Error::TooLong("Header value too long".into()));
94 }
95
96 Ok(Header {
97 name: trimmed_name.to_string(),
98 value: trimmed_value.to_string(),
99 })
100 }
101
102 pub fn make_safe(name: &str, value: &str) -> Result<Header> {
104 if Self::is_dangerous(name) {
105 return Err(Error::InvalidFormat("Dangerous header not allowed".into()));
106 }
107 Self::make(name, value)
108 }
109
110 pub fn render(header: &Header) -> String {
112 format!("{}: {}", header.name, header.value)
113 }
114
115 pub fn build_hsts(max_age: u64, include_subdomains: bool, preload: bool) -> String {
117 let mut value = format!("max-age={}", max_age);
118 if include_subdomains {
119 value.push_str("; includeSubDomains");
120 }
121 if preload {
122 value.push_str("; preload");
123 }
124 value
125 }
126
127 pub fn build_csp(directives: &[(String, Vec<String>)]) -> String {
129 directives
130 .iter()
131 .map(|(name, sources)| {
132 if sources.is_empty() {
133 name.clone()
134 } else {
135 format!("{} {}", name, sources.join(" "))
136 }
137 })
138 .collect::<Vec<_>>()
139 .join("; ")
140 }
141
142 pub fn security_headers() -> Vec<Header> {
144 vec![
145 Header {
146 name: "X-Frame-Options".to_string(),
147 value: "DENY".to_string(),
148 },
149 Header {
150 name: "X-Content-Type-Options".to_string(),
151 value: "nosniff".to_string(),
152 },
153 Header {
154 name: "Referrer-Policy".to_string(),
155 value: "strict-origin-when-cross-origin".to_string(),
156 },
157 Header {
158 name: "X-XSS-Protection".to_string(),
159 value: "1; mode=block".to_string(),
160 },
161 ]
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_has_crlf() {
171 assert!(!SafeHeader::has_crlf("application/json"));
172 assert!(SafeHeader::has_crlf("text\r\nX-Injected: evil"));
173 assert!(SafeHeader::has_crlf("value\ninjected"));
174 }
175
176 #[test]
177 fn test_is_valid_name() {
178 assert!(SafeHeader::is_valid_name("Content-Type"));
179 assert!(SafeHeader::is_valid_name("X-Custom-Header"));
180 assert!(!SafeHeader::is_valid_name("Content:Type"));
181 assert!(!SafeHeader::is_valid_name(""));
182 }
183
184 #[test]
185 fn test_is_dangerous() {
186 assert!(SafeHeader::is_dangerous("Host"));
187 assert!(SafeHeader::is_dangerous("Transfer-Encoding"));
188 assert!(!SafeHeader::is_dangerous("X-Custom-Header"));
189 }
190
191 #[test]
192 fn test_build_hsts() {
193 let hsts = SafeHeader::build_hsts(31536000, true, true);
194 assert_eq!(hsts, "max-age=31536000; includeSubDomains; preload");
195 }
196
197 #[test]
198 fn test_make_header() {
199 let header = SafeHeader::make("Content-Type", "application/json").unwrap();
200 assert_eq!(header.name, "Content-Type");
201 assert_eq!(header.value, "application/json");
202 }
203
204 #[test]
205 fn test_reject_crlf() {
206 let result = SafeHeader::make("Content-Type", "text\r\nX-Injected: evil");
207 assert!(result.is_err());
208 }
209}