1use axum::{
2 http::{
3 HeaderMap, HeaderName, HeaderValue, StatusCode,
4 header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH, VARY},
5 },
6 response::{IntoResponse, Response},
7};
8use fs_err as fs;
9use tracing::debug;
10
11use crate::{
12 options::ServeOptions,
13 util::{
14 compression::{compress_brotli, compress_gzip, decompress_brotli},
15 headers::{encoding_qvalue, if_none_match_matches},
16 },
17};
18
19const BROTLI_ENCODING: &str = "br";
20
21const BROTLI_HEADER: (HeaderName, HeaderValue) =
22 (CONTENT_ENCODING, HeaderValue::from_static(BROTLI_ENCODING));
23
24const GZIP_ENCODING: &str = "gzip";
25
26const GZIP_HEADER: (HeaderName, HeaderValue) =
27 (CONTENT_ENCODING, HeaderValue::from_static(GZIP_ENCODING));
28
29const VARY_HEADER: (HeaderName, HeaderValue) = (VARY, HeaderValue::from_static("accept-encoding"));
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum OnDemandEncoding {
34 Identity,
36 Brotli,
38 Gzip,
40}
41
42#[derive(Debug)]
44pub struct Asset {
45 pub route: &'static str,
47 pub path: &'static str,
49 pub etag: &'static str,
51 pub content_type: &'static str,
53 pub bytes: Option<&'static [u8]>,
55 pub is_compressed: bool,
57 pub should_compress: bool,
59}
60
61struct AssetResponse<'t, B> {
63 options: &'t ServeOptions,
64 headers: &'t HeaderMap,
65 status: StatusCode,
66 asset: &'t Asset,
67 etag: &'t str,
68 bytes: B,
69 bytes_len: usize,
70 brotli_bytes: B,
71 brotli_bytes_len: usize,
72 gzip_bytes: B,
73 gzip_bytes_len: usize,
74}
75
76impl<B: IntoResponse> AssetResponse<'_, B> {
77 fn into_response(self) -> Response {
79 let quoted_etag = format!("\"{}\"", self.etag);
81 let varies_by_encoding =
82 self.asset.should_compress && (self.options.enable_brotli || self.options.enable_gzip);
83
84 let mut headers = HeaderMap::new();
85 headers.extend([
86 self.asset.content_type(),
87 self.asset.cache_control(self.options),
88 (ETAG, HeaderValue::from_str("ed_etag).unwrap()),
89 ]);
90 if varies_by_encoding {
91 headers.extend([VARY_HEADER]);
92 }
93
94 if let Some(if_none_match) = self.headers.get(IF_NONE_MATCH)
95 && let Ok(if_none_match) = if_none_match.to_str()
96 && if_none_match_matches(if_none_match, "ed_etag)
97 {
98 return (StatusCode::NOT_MODIFIED, headers).into_response();
99 }
100
101 let brotli_q = (self.options.enable_brotli && self.brotli_bytes_len > 0)
104 .then(|| encoding_qvalue(self.headers, BROTLI_ENCODING))
105 .flatten();
106 let gzip_q = (self.options.enable_gzip && self.gzip_bytes_len > 0)
107 .then(|| encoding_qvalue(self.headers, GZIP_ENCODING))
108 .flatten();
109
110 let prefer_brotli = match (brotli_q, gzip_q) {
112 (Some(br), Some(gz)) => br >= gz,
113 (Some(_), None) => true,
114 _ => false,
115 };
116
117 if prefer_brotli {
118 headers.extend([
119 (CONTENT_LENGTH, HeaderValue::from(self.brotli_bytes_len)),
120 BROTLI_HEADER,
121 ]);
122 return (self.status, headers, self.brotli_bytes).into_response();
123 }
124
125 if gzip_q.is_some() {
126 headers.extend([
127 (CONTENT_LENGTH, HeaderValue::from(self.gzip_bytes_len)),
128 GZIP_HEADER,
129 ]);
130 return (self.status, headers, self.gzip_bytes).into_response();
131 }
132
133 headers.extend([(CONTENT_LENGTH, HeaderValue::from(self.bytes_len))]);
134 (self.status, headers, self.bytes).into_response()
135 }
136}
137
138impl Asset {
139 fn cache_control(&self, options: &ServeOptions) -> (HeaderName, HeaderValue) {
141 match self.content_type {
142 "text/html" => options.html_cache_control.as_header(),
143 _ => options.cache_control.as_header(),
144 }
145 }
146
147 fn content_type(&self) -> (HeaderName, HeaderValue) {
149 (CONTENT_TYPE, HeaderValue::from_static(self.content_type))
150 }
151
152 pub(crate) fn is_html(&self) -> bool {
154 self.content_type == "text/html"
155 }
156
157 pub(crate) fn content_hash(&self) -> Option<String> {
160 if !self.etag.is_empty() {
161 return Some(self.etag.to_string());
162 }
163
164 self.read_source_bytes()
165 .ok()
166 .map(|bytes| sha256::digest(&bytes))
167 }
168
169 pub(crate) fn leak_bytes(
171 &self,
172 options: &'static ServeOptions,
173 ) -> (&'static [u8], &'static [u8], &'static [u8]) {
174 let mut uncompressed = self.bytes.unwrap_or_default();
175
176 if self.is_compressed {
177 uncompressed = Box::new(decompress_brotli(uncompressed).unwrap_or_default()).leak()
178 }
179
180 let gzip_bytes = if self.should_compress && options.enable_gzip && !uncompressed.is_empty()
181 {
182 Box::new(compress_gzip(uncompressed).unwrap_or_default()).leak()
183 } else {
184 Default::default()
185 };
186
187 let brotli_bytes = if self.should_compress && options.enable_brotli {
188 if self.is_compressed {
189 self.bytes.unwrap_or_default()
191 } else if uncompressed.is_empty() {
192 Default::default()
193 } else {
194 Box::new(compress_brotli(uncompressed).unwrap_or_default()).leak()
197 }
198 } else {
199 Default::default()
200 };
201
202 (uncompressed, brotli_bytes, gzip_bytes)
203 }
204
205 fn read_source_bytes(&self) -> Result<Vec<u8>, StatusCode> {
207 fs::read(self.path).map_err(|_| StatusCode::NOT_FOUND)
208 }
209
210 fn negotiate_dynamic_encoding(
212 &self,
213 headers: &HeaderMap,
214 options: &ServeOptions,
215 ) -> OnDemandEncoding {
216 if !self.should_compress {
217 return OnDemandEncoding::Identity;
218 }
219
220 let brotli_q = options
221 .enable_brotli
222 .then(|| encoding_qvalue(headers, BROTLI_ENCODING))
223 .flatten();
224 let gzip_q = options
225 .enable_gzip
226 .then(|| encoding_qvalue(headers, GZIP_ENCODING))
227 .flatten();
228
229 match (brotli_q, gzip_q) {
231 (Some(br), Some(gz)) if br >= gz => OnDemandEncoding::Brotli,
232 (Some(_), None) => OnDemandEncoding::Brotli,
233 (_, Some(_)) => OnDemandEncoding::Gzip,
234 _ => OnDemandEncoding::Identity,
235 }
236 }
237
238 fn encode_dynamic_bytes(&self, bytes: &[u8], encoding: OnDemandEncoding) -> (Vec<u8>, Vec<u8>) {
240 match encoding {
241 OnDemandEncoding::Brotli => (compress_brotli(bytes).unwrap_or_default(), Vec::new()),
242 OnDemandEncoding::Gzip => (Vec::new(), compress_gzip(bytes).unwrap_or_default()),
243 OnDemandEncoding::Identity => (Vec::new(), Vec::new()),
244 }
245 }
246
247 fn dynamic_handler(
249 &self,
250 headers: &HeaderMap,
251 status: StatusCode,
252 options: &ServeOptions,
253 ) -> Response {
254 let bytes = match self.read_source_bytes() {
255 Ok(bytes) => bytes,
256 Err(status) => return status.into_response(),
257 };
258
259 let encoding = self.negotiate_dynamic_encoding(headers, options);
260 let (brotli_bytes, gzip_bytes) = self.encode_dynamic_bytes(&bytes, encoding);
261
262 let etag = sha256::digest(&bytes);
263
264 AssetResponse {
265 options,
266 headers,
267 status,
268 asset: self,
269 etag: &etag,
270 bytes_len: bytes.len(),
271 bytes,
272 brotli_bytes_len: brotli_bytes.len(),
273 brotli_bytes,
274 gzip_bytes_len: gzip_bytes.len(),
275 gzip_bytes,
276 }
277 .into_response()
278 }
279
280 pub(super) fn handler(
282 &self,
283 headers: &HeaderMap,
284 status: StatusCode,
285 bytes: &'static [u8],
286 brotli_bytes: &'static [u8],
287 gzip_bytes: &'static [u8],
288 options: &ServeOptions,
289 ) -> Response {
290 if bytes.is_empty() {
291 debug!("using dynamic handler for {}", self.path);
292
293 return self.dynamic_handler(headers, status, options);
294 }
295
296 AssetResponse {
297 options,
298 headers,
299 status,
300 asset: self,
301 etag: self.etag,
302 bytes_len: bytes.len(),
303 bytes,
304 brotli_bytes_len: brotli_bytes.len(),
305 brotli_bytes,
306 gzip_bytes_len: gzip_bytes.len(),
307 gzip_bytes,
308 }
309 .into_response()
310 }
311}