1use bytes::Bytes;
2use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
3use std::sync::Arc;
4
5const STATIC_SECURITY_HEADERS: [(HeaderName, &str); 6] = [
8 (header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
9 (header::X_FRAME_OPTIONS, "SAMEORIGIN"),
10 (header::REFERRER_POLICY, "strict-origin-when-cross-origin"),
11 (
12 header::STRICT_TRANSPORT_SECURITY,
13 "max-age=63072000; includeSubDomains",
14 ),
15 (
16 HeaderName::from_static("permissions-policy"),
17 "camera=(), microphone=(), geolocation=()",
18 ),
19 (HeaderName::from_static("x-dns-prefetch-control"), "off"),
20];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum Encoding {
24 Identity,
25 Gzip,
26 Brotli,
27 Zstd,
28}
29
30impl Encoding {
31 pub const ALL: [Self; 4] = [Self::Identity, Self::Gzip, Self::Brotli, Self::Zstd];
32
33 pub fn from_accept_encoding(accept: &str) -> Self {
37 let mut best = Self::Identity;
38 for part in accept.split(',') {
39 let mut segments = part.split(';');
40 let token = segments.next().unwrap_or("").trim();
41
42 let rejected = segments.any(|s| {
44 s.trim()
45 .strip_prefix("q=")
46 .and_then(|v| v.trim().parse::<f32>().ok())
47 .is_some_and(|q| q == 0.0)
48 });
49 if rejected {
50 continue;
51 }
52
53 match token {
54 "br" => return Self::Brotli, "zstd" => best = Self::Zstd,
56 "gzip" if !matches!(best, Self::Zstd) => best = Self::Gzip,
57 _ => {}
58 }
59 }
60 best
61 }
62}
63
64#[derive(Debug, Clone)]
65pub struct ResponseBuffer {
66 pub body: Bytes,
67 pub content_type: Arc<str>,
68 pub content_encoding: Option<&'static str>,
69 pub etag: Arc<str>,
70 pub last_modified: Arc<str>,
71 pub cache_control: Arc<str>,
72 pub content_length: Arc<str>,
73 pub vary_encoding: bool,
75 pub headers: HeaderMap,
79}
80
81impl ResponseBuffer {
82 pub fn new(
83 body: Bytes,
84 content_type: Arc<str>,
85 content_encoding: Option<&'static str>,
86 etag: Arc<str>,
87 last_modified: Arc<str>,
88 cache_control: Arc<str>,
89 vary_encoding: bool,
90 ) -> Self {
91 let content_length: Arc<str> = Arc::from(body.len().to_string().as_str());
92 let headers = build_headers(
93 &content_type,
94 content_encoding,
95 &etag,
96 &last_modified,
97 &cache_control,
98 &content_length,
99 vary_encoding,
100 );
101 Self {
102 body,
103 content_type,
104 content_encoding,
105 etag,
106 last_modified,
107 cache_control,
108 content_length,
109 vary_encoding,
110 headers,
111 }
112 }
113}
114
115fn build_headers(
119 content_type: &str,
120 content_encoding: Option<&'static str>,
121 etag: &str,
122 last_modified: &str,
123 cache_control: &str,
124 content_length: &str,
125 vary_encoding: bool,
126) -> HeaderMap {
127 let mut h = HeaderMap::with_capacity(13);
128 let val = |s: &str| HeaderValue::from_str(s).expect("server-controlled header value");
129
130 h.insert(header::CONTENT_TYPE, val(content_type));
131 h.insert(header::ETAG, val(etag));
132 h.insert(header::LAST_MODIFIED, val(last_modified));
133 h.insert(header::CACHE_CONTROL, val(cache_control));
134 h.insert(header::CONTENT_LENGTH, val(content_length));
135 if let Some(encoding) = content_encoding {
136 h.insert(header::CONTENT_ENCODING, HeaderValue::from_static(encoding));
137 }
138 if vary_encoding {
139 h.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
140 }
141 for (name, value) in STATIC_SECURITY_HEADERS {
142 h.insert(name, HeaderValue::from_static(value));
143 }
144 h
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_encoding_priority() {
153 assert_eq!(
154 Encoding::from_accept_encoding("gzip, br, zstd"),
155 Encoding::Brotli
156 );
157 assert_eq!(Encoding::from_accept_encoding("br"), Encoding::Brotli);
158 assert_eq!(Encoding::from_accept_encoding("gzip, zstd"), Encoding::Zstd);
159 assert_eq!(Encoding::from_accept_encoding("zstd"), Encoding::Zstd);
160 assert_eq!(Encoding::from_accept_encoding("gzip"), Encoding::Gzip);
161 assert_eq!(
162 Encoding::from_accept_encoding("deflate"),
163 Encoding::Identity
164 );
165 assert_eq!(Encoding::from_accept_encoding(""), Encoding::Identity);
166 }
167
168 #[test]
169 fn test_encoding_no_substring_false_positives() {
170 assert_eq!(
171 Encoding::from_accept_encoding("vibrant"),
172 Encoding::Identity
173 );
174 assert_eq!(Encoding::from_accept_encoding("broken"), Encoding::Identity);
175 }
176
177 #[test]
178 fn test_encoding_with_quality_values() {
179 assert_eq!(
180 Encoding::from_accept_encoding("gzip;q=1.0, br;q=0.8"),
181 Encoding::Brotli
182 );
183 assert_eq!(
184 Encoding::from_accept_encoding("gzip;q=0.5, zstd;q=1.0"),
185 Encoding::Zstd
186 );
187 }
188
189 #[test]
190 fn test_encoding_respects_q_zero() {
191 assert_eq!(
193 Encoding::from_accept_encoding("br;q=0, gzip"),
194 Encoding::Gzip
195 );
196 assert_eq!(
197 Encoding::from_accept_encoding("br;q=0, zstd;q=0, gzip"),
198 Encoding::Gzip
199 );
200 assert_eq!(
201 Encoding::from_accept_encoding("br;q=0, zstd;q=0, gzip;q=0"),
202 Encoding::Identity
203 );
204 assert_eq!(
205 Encoding::from_accept_encoding("gzip;q=0"),
206 Encoding::Identity
207 );
208 }
209}