1use super::{
2 backend::{Backend, File as _, Metadata as _},
3 headers::{ETag, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince, LastModified},
4 ServeVariant,
5};
6use crate::content_encoding::{Encoding, QValue};
7use bytes::Bytes;
8use http::{header, HeaderValue, Method, Request, Uri};
9use http_body_util::Empty;
10use http_range_header::RangeUnsatisfiableError;
11use std::{
12 ffi::OsStr,
13 io::{self, ErrorKind, SeekFrom},
14 ops::RangeInclusive,
15 path::{Path, PathBuf},
16};
17use tokio::io::AsyncSeekExt;
18
19pub(super) enum OpenFileOutput {
20 FileOpened(Box<FileOpened>),
21 Redirect {
22 location: HeaderValue,
23 },
24 FileNotFound,
25 PreconditionFailed,
26 NotModified {
27 etag: Option<ETag>,
28 last_modified: Option<LastModified>,
29 },
30 InvalidRedirectUri,
31 InvalidFilename,
32}
33
34pub(super) struct FileOpened {
35 pub(super) extent: FileRequestExtent,
36 pub(super) chunk_size: usize,
37 pub(super) mime_header_value: HeaderValue,
38 pub(super) maybe_encoding: Option<Encoding>,
39 pub(super) maybe_range: Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>>,
40 pub(super) last_modified: Option<LastModified>,
41 pub(super) precompression_configured: bool,
42 pub(super) etag: Option<ETag>,
43}
44
45pub(super) enum FileRequestExtent {
46 Full(Box<dyn tokio::io::AsyncRead + Unpin + Send>, u64),
47 Head(u64),
48}
49
50pub(super) struct OpenFileRequest<B> {
51 pub(super) variant: ServeVariant,
52 pub(super) redirect_path_prefix: String,
53 pub(super) path_to_file: PathBuf,
54 pub(super) req: Request<Empty<Bytes>>,
55 pub(super) negotiated_encodings: Vec<(Encoding, QValue)>,
56 pub(super) range_header: Option<String>,
57 pub(super) buf_chunk_size: usize,
58 pub(super) precompression_configured: bool,
59 pub(super) backend: B,
60}
61
62pub(super) async fn open_file<B: Backend>(
63 request: OpenFileRequest<B>,
64) -> io::Result<OpenFileOutput> {
65 let OpenFileRequest {
66 variant,
67 redirect_path_prefix,
68 mut path_to_file,
69 req,
70 negotiated_encodings,
71 range_header,
72 buf_chunk_size,
73 precompression_configured,
74 backend,
75 } = request;
76 let preconditions = Preconditions {
77 if_match: req
78 .headers()
79 .get(header::IF_MATCH)
80 .and_then(IfMatch::from_header_value),
81 if_unmodified_since: req
82 .headers()
83 .get(header::IF_UNMODIFIED_SINCE)
84 .and_then(IfUnmodifiedSince::from_header_value),
85 if_none_match: req
86 .headers()
87 .get(header::IF_NONE_MATCH)
88 .and_then(IfNoneMatch::from_header_value),
89 if_modified_since: req
90 .headers()
91 .get(header::IF_MODIFIED_SINCE)
92 .and_then(IfModifiedSince::from_header_value),
93 };
94
95 let mime = match variant {
96 ServeVariant::Directory {
97 append_index_html_on_directories,
98 html_as_default_extension,
99 } => {
100 if let Some(output) = maybe_redirect_or_append_path(
104 &redirect_path_prefix,
105 &mut path_to_file,
106 req.uri(),
107 append_index_html_on_directories,
108 html_as_default_extension,
109 &backend,
110 )
111 .await
112 {
113 return Ok(output);
114 }
115
116 mime_guess::from_path(&path_to_file)
117 .first_raw()
118 .map(HeaderValue::from_static)
119 .unwrap_or_else(|| {
120 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
121 })
122 }
123
124 ServeVariant::SingleFile { mime } => mime,
125 };
126
127 if req.method() == Method::HEAD {
128 #[cfg(feature = "tracing")]
129 let _path_str = path_to_file.display().to_string();
130 let (meta, maybe_encoding) =
131 file_metadata_with_fallback(&backend, path_to_file, negotiated_encodings).await?;
132
133 let last_modified = meta.modified().ok().map(LastModified::from);
134 let etag = meta
135 .modified()
136 .ok()
137 .and_then(|mtime| ETag::from_metadata(meta.len(), mtime));
138
139 #[cfg(feature = "tracing")]
140 if etag.is_none() {
141 rate_limited!(
142 std::time::Duration::from_secs(60),
143 tracing::warn!(path = %_path_str, "ETag generation failed (mtime unavailable or pre-epoch)")
144 );
145 }
146
147 if let Some(output) = preconditions.check(etag.as_ref(), last_modified.as_ref()) {
148 return Ok(output);
149 }
150
151 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
152
153 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
154 extent: FileRequestExtent::Head(meta.len()),
155 chunk_size: buf_chunk_size,
156 mime_header_value: mime,
157 maybe_encoding,
158 maybe_range,
159 last_modified,
160 precompression_configured,
161 etag,
162 })))
163 } else {
164 #[cfg(feature = "tracing")]
165 let _path_str = path_to_file.display().to_string();
166 let (mut file, maybe_encoding) =
167 match open_file_with_fallback(&backend, path_to_file, negotiated_encodings).await {
168 Ok(result) => result,
169
170 Err(err) if is_invalid_filename_error(&err) => {
171 return Ok(OpenFileOutput::InvalidFilename)
172 }
173 Err(err) => return Err(err),
174 };
175
176 let meta = file.metadata().await?;
177
178 let last_modified = meta.modified().ok().map(LastModified::from);
179 let etag = meta
180 .modified()
181 .ok()
182 .and_then(|mtime| ETag::from_metadata(meta.len(), mtime));
183
184 #[cfg(feature = "tracing")]
185 if etag.is_none() {
186 rate_limited!(
187 std::time::Duration::from_secs(60),
188 tracing::warn!(path = %_path_str, "ETag generation failed (mtime unavailable or pre-epoch)")
189 );
190 }
191
192 if let Some(output) = preconditions.check(etag.as_ref(), last_modified.as_ref()) {
193 return Ok(output);
194 }
195
196 let size = meta.len();
197 let maybe_range = try_parse_range(range_header.as_deref(), size);
198 if let Some(Ok(ranges)) = maybe_range.as_ref() {
199 if ranges.len() == 1 {
202 file.seek(SeekFrom::Start(*ranges[0].start())).await?;
203 }
204 }
205
206 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
207 extent: FileRequestExtent::Full(Box::new(file), size),
208 chunk_size: buf_chunk_size,
209 mime_header_value: mime,
210 maybe_encoding,
211 maybe_range,
212 last_modified,
213 precompression_configured,
214 etag,
215 })))
216 }
217}
218
219fn is_invalid_filename_error(err: &io::Error) -> bool {
220 if err.kind() == ErrorKind::InvalidInput {
222 return true;
223 }
224
225 #[cfg(windows)]
228 if let Some(raw_err) = err.raw_os_error() {
229 if (raw_err == 123) || (raw_err == 161) || (raw_err == 206) {
232 return true;
233 }
234 }
235
236 false
237}
238
239struct Preconditions {
241 if_match: Option<IfMatch>,
242 if_unmodified_since: Option<IfUnmodifiedSince>,
243 if_none_match: Option<IfNoneMatch>,
244 if_modified_since: Option<IfModifiedSince>,
245}
246
247impl Preconditions {
248 fn check(
256 self,
257 etag: Option<&ETag>,
258 last_modified: Option<&LastModified>,
259 ) -> Option<OpenFileOutput> {
260 if let Some(if_match) = self.if_match {
262 let passes = etag
266 .map(|etag| if_match.precondition_passes(etag))
267 .unwrap_or(false);
268 if !passes {
269 return Some(OpenFileOutput::PreconditionFailed);
270 }
271 } else {
272 if let Some(since) = self.if_unmodified_since {
276 let passes = last_modified
277 .map(|lm| since.precondition_passes(lm))
278 .unwrap_or(true);
279 if !passes {
280 return Some(OpenFileOutput::PreconditionFailed);
281 }
282 }
283 }
284
285 if let Some(if_none_match) = self.if_none_match {
287 let passes = etag
289 .map(|etag| if_none_match.precondition_passes(etag))
290 .unwrap_or(true);
291 if !passes {
292 return Some(OpenFileOutput::NotModified {
293 etag: etag.cloned(),
294 last_modified: last_modified.map(|lm| LastModified(lm.0)),
295 });
296 }
297 } else {
298 if let Some(since) = self.if_modified_since {
301 let unmodified = last_modified
302 .map(|lm| !since.is_modified(lm))
303 .unwrap_or(false);
304 if unmodified {
305 return Some(OpenFileOutput::NotModified {
306 etag: etag.cloned(),
307 last_modified: last_modified.map(|lm| LastModified(lm.0)),
308 });
309 }
310 }
311 }
312
313 None
314 }
315}
316
317fn preferred_encoding(
320 path: &mut PathBuf,
321 negotiated_encoding: &[(Encoding, QValue)],
322) -> Option<Encoding> {
323 let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding.iter().copied());
324
325 if let Some(file_extension) =
326 preferred_encoding.and_then(|encoding| encoding.to_file_extension())
327 {
328 let new_file_name = path
329 .file_name()
330 .map(|file_name| {
331 let mut os_string = file_name.to_os_string();
332 os_string.push(file_extension);
333 os_string
334 })
335 .unwrap_or_else(|| file_extension.to_os_string());
336
337 path.set_file_name(new_file_name);
338 }
339
340 preferred_encoding
341}
342
343async fn open_file_with_fallback<B: Backend>(
347 backend: &B,
348 mut path: PathBuf,
349 mut negotiated_encoding: Vec<(Encoding, QValue)>,
350) -> io::Result<(B::File, Option<Encoding>)> {
351 let (file, encoding) = loop {
352 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
354 match (backend.open(path.clone()).await, encoding) {
355 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
356 (Err(err), Some(encoding))
357 if err.kind() == io::ErrorKind::NotFound && encoding != Encoding::Identity =>
358 {
359 path.set_extension(OsStr::new(""));
362 negotiated_encoding
364 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
365 }
366 (Err(err), _) => return Err(err),
367 }
368 };
369 Ok((file, encoding))
370}
371
372async fn file_metadata_with_fallback<B: Backend>(
376 backend: &B,
377 mut path: PathBuf,
378 mut negotiated_encoding: Vec<(Encoding, QValue)>,
379) -> io::Result<(B::Metadata, Option<Encoding>)> {
380 let (meta, encoding) = loop {
381 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
383 match (backend.metadata(path.clone()).await, encoding) {
384 (Ok(meta), maybe_encoding) => break (meta, maybe_encoding),
385 (Err(err), Some(encoding))
386 if err.kind() == io::ErrorKind::NotFound && encoding != Encoding::Identity =>
387 {
388 path.set_extension(OsStr::new(""));
391 negotiated_encoding
393 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
394 }
395 (Err(err), _) => return Err(err),
396 }
397 };
398 Ok((meta, encoding))
399}
400
401async fn maybe_redirect_or_append_path<B: Backend>(
402 redirect_path_prefix: &str,
403 path_to_file: &mut PathBuf,
404 uri: &Uri,
405 append_index_html_on_directories: bool,
406 html_as_default_extension: bool,
407 backend: &B,
408) -> Option<OpenFileOutput> {
409 let uri_path = uri.path();
410
411 let is_directory = is_dir(path_to_file, backend).await;
412
413 if uri_path.ends_with('/') && uri_path != "/" && is_directory != Some(true) {
414 return Some(OpenFileOutput::FileNotFound);
415 }
416
417 if html_as_default_extension && is_directory.is_none() && path_to_file.extension().is_none() {
419 path_to_file.set_extension("html");
420 return None;
421 }
422
423 if is_directory != Some(true) {
424 return None;
425 }
426
427 if !append_index_html_on_directories {
428 return Some(OpenFileOutput::FileNotFound);
429 }
430
431 if uri_path.ends_with('/') {
432 path_to_file.push("index.html");
433 None
434 } else {
435 let uri = match append_slash_on_path(uri.clone(), redirect_path_prefix) {
436 Ok(uri) => uri,
437 Err(err) => return Some(err),
438 };
439 let location = HeaderValue::from_str(&uri.to_string()).unwrap();
440 Some(OpenFileOutput::Redirect { location })
441 }
442}
443
444fn try_parse_range(
445 maybe_range_ref: Option<&str>,
446 file_size: u64,
447) -> Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>> {
448 maybe_range_ref.map(|header_value| {
449 http_range_header::parse_range_header(header_value)
450 .and_then(|first_pass| first_pass.validate(file_size))
451 })
452}
453
454async fn is_dir<B: Backend>(path_to_file: &Path, backend: &B) -> Option<bool> {
455 backend
456 .metadata(path_to_file.to_owned())
457 .await
458 .ok()
459 .map(|meta_data| meta_data.is_dir())
460}
461
462fn append_slash_on_path(uri: Uri, redirect_path_prefix: &str) -> Result<Uri, OpenFileOutput> {
463 let http::uri::Parts {
464 scheme,
465 authority,
466 path_and_query,
467 ..
468 } = uri.into_parts();
469
470 let mut uri_builder = Uri::builder();
471
472 if let Some(scheme) = scheme {
473 uri_builder = uri_builder.scheme(scheme);
474 }
475
476 if let Some(authority) = authority {
477 uri_builder = uri_builder.authority(authority);
478 }
479
480 let uri_builder = if let Some(path_and_query) = path_and_query {
481 if let Some(query) = path_and_query.query() {
482 uri_builder.path_and_query(format!(
483 "{redirect_path_prefix}{}/?{}",
484 path_and_query.path(),
485 query
486 ))
487 } else {
488 uri_builder.path_and_query(format!("{redirect_path_prefix}{}/", path_and_query.path()))
489 }
490 } else {
491 uri_builder.path_and_query(format!("{redirect_path_prefix}/"))
492 };
493
494 uri_builder.build().map_err(|_err| {
495 #[cfg(feature = "tracing")]
496 tracing::error!(err = ?_err, "redirect uri failed to build");
497
498 OpenFileOutput::InvalidRedirectUri
499 })
500}
501
502#[test]
503fn preferred_encoding_with_extension() {
504 let mut path = PathBuf::from("hello.txt");
505 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
506 assert_eq!(path, PathBuf::from("hello.txt.gz"));
507}
508
509#[test]
510fn preferred_encoding_without_extension() {
511 let mut path = PathBuf::from("hello");
512 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
513 assert_eq!(path, PathBuf::from("hello.gz"));
514}