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 leak_bytes(
154 &self,
155 options: &'static ServeOptions,
156 ) -> (&'static [u8], &'static [u8], &'static [u8]) {
157 let mut uncompressed = self.bytes.unwrap_or_default();
158
159 if self.is_compressed {
160 uncompressed = Box::new(decompress_brotli(uncompressed).unwrap_or_default()).leak()
161 }
162
163 let gzip_bytes = if self.should_compress && options.enable_gzip {
164 Box::new(compress_gzip(uncompressed).unwrap_or_default()).leak()
165 } else {
166 Default::default()
167 };
168
169 let brotli_bytes = if self.should_compress && options.enable_brotli {
170 if self.is_compressed {
171 self.bytes.unwrap_or_default()
173 } else {
174 Box::new(compress_brotli(uncompressed).unwrap_or_default()).leak()
177 }
178 } else {
179 Default::default()
180 };
181
182 (uncompressed, brotli_bytes, gzip_bytes)
183 }
184
185 fn read_source_bytes(&self) -> Result<Vec<u8>, StatusCode> {
187 fs::read(self.path).map_err(|_| StatusCode::NOT_FOUND)
188 }
189
190 fn negotiate_dynamic_encoding(
192 &self,
193 headers: &HeaderMap,
194 options: &ServeOptions,
195 ) -> OnDemandEncoding {
196 if !self.should_compress {
197 return OnDemandEncoding::Identity;
198 }
199
200 let brotli_q = options
201 .enable_brotli
202 .then(|| encoding_qvalue(headers, BROTLI_ENCODING))
203 .flatten();
204 let gzip_q = options
205 .enable_gzip
206 .then(|| encoding_qvalue(headers, GZIP_ENCODING))
207 .flatten();
208
209 match (brotli_q, gzip_q) {
211 (Some(br), Some(gz)) if br >= gz => OnDemandEncoding::Brotli,
212 (Some(_), None) => OnDemandEncoding::Brotli,
213 (_, Some(_)) => OnDemandEncoding::Gzip,
214 _ => OnDemandEncoding::Identity,
215 }
216 }
217
218 fn encode_dynamic_bytes(&self, bytes: &[u8], encoding: OnDemandEncoding) -> (Vec<u8>, Vec<u8>) {
220 match encoding {
221 OnDemandEncoding::Brotli => (compress_brotli(bytes).unwrap_or_default(), Vec::new()),
222 OnDemandEncoding::Gzip => (Vec::new(), compress_gzip(bytes).unwrap_or_default()),
223 OnDemandEncoding::Identity => (Vec::new(), Vec::new()),
224 }
225 }
226
227 fn dynamic_handler(
229 &self,
230 headers: &HeaderMap,
231 status: StatusCode,
232 options: &ServeOptions,
233 ) -> Response {
234 let bytes = match self.read_source_bytes() {
235 Ok(bytes) => bytes,
236 Err(status) => return status.into_response(),
237 };
238
239 let encoding = self.negotiate_dynamic_encoding(headers, options);
240 let (brotli_bytes, gzip_bytes) = self.encode_dynamic_bytes(&bytes, encoding);
241
242 let etag = sha256::digest(&bytes);
243
244 AssetResponse {
245 options,
246 headers,
247 status,
248 asset: self,
249 etag: &etag,
250 bytes_len: bytes.len(),
251 bytes,
252 brotli_bytes_len: brotli_bytes.len(),
253 brotli_bytes,
254 gzip_bytes_len: gzip_bytes.len(),
255 gzip_bytes,
256 }
257 .into_response()
258 }
259
260 pub(super) fn handler(
262 &self,
263 headers: &HeaderMap,
264 status: StatusCode,
265 bytes: &'static [u8],
266 brotli_bytes: &'static [u8],
267 gzip_bytes: &'static [u8],
268 options: &ServeOptions,
269 ) -> Response {
270 if bytes.is_empty() {
271 debug!("using dynamic handler for {}", self.path);
272
273 return self.dynamic_handler(headers, status, options);
274 }
275
276 AssetResponse {
277 options,
278 headers,
279 status,
280 asset: self,
281 etag: self.etag,
282 bytes_len: bytes.len(),
283 bytes,
284 brotli_bytes_len: brotli_bytes.len(),
285 brotli_bytes,
286 gzip_bytes_len: gzip_bytes.len(),
287 gzip_bytes,
288 }
289 .into_response()
290 }
291}