tower_http/services/fs/serve_dir/
open_file.rs1use super::{
2 headers::{IfModifiedSince, IfUnmodifiedSince, LastModified},
3 ServeVariant,
4};
5use crate::content_encoding::{Encoding, QValue};
6use bytes::Bytes;
7use http::{header, HeaderValue, Method, Request, Uri};
8use http_body_util::Empty;
9use http_range_header::RangeUnsatisfiableError;
10use std::{
11 ffi::OsStr,
12 fs::Metadata,
13 io::{self, ErrorKind, SeekFrom},
14 ops::RangeInclusive,
15 path::{Path, PathBuf},
16};
17use tokio::{fs::File, io::AsyncSeekExt};
18
19pub(super) enum OpenFileOutput {
20 FileOpened(Box<FileOpened>),
21 Redirect { location: HeaderValue },
22 FileNotFound,
23 PreconditionFailed,
24 NotModified,
25 InvalidRedirectUri,
26 InvalidFilename,
27}
28
29pub(super) struct FileOpened {
30 pub(super) extent: FileRequestExtent,
31 pub(super) chunk_size: usize,
32 pub(super) mime_header_value: HeaderValue,
33 pub(super) maybe_encoding: Option<Encoding>,
34 pub(super) maybe_range: Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>>,
35 pub(super) last_modified: Option<LastModified>,
36}
37
38pub(super) enum FileRequestExtent {
39 Full(File, Metadata),
40 Head(Metadata),
41}
42
43pub(super) async fn open_file(
44 variant: ServeVariant,
45 mut path_to_file: PathBuf,
46 req: Request<Empty<Bytes>>,
47 negotiated_encodings: Vec<(Encoding, QValue)>,
48 range_header: Option<String>,
49 buf_chunk_size: usize,
50) -> io::Result<OpenFileOutput> {
51 let if_unmodified_since = req
52 .headers()
53 .get(header::IF_UNMODIFIED_SINCE)
54 .and_then(IfUnmodifiedSince::from_header_value);
55
56 let if_modified_since = req
57 .headers()
58 .get(header::IF_MODIFIED_SINCE)
59 .and_then(IfModifiedSince::from_header_value);
60
61 let mime = match variant {
62 ServeVariant::Directory {
63 append_index_html_on_directories,
64 } => {
65 if let Some(output) = maybe_redirect_or_append_path(
69 &mut path_to_file,
70 req.uri(),
71 append_index_html_on_directories,
72 )
73 .await
74 {
75 return Ok(output);
76 }
77
78 mime_guess::from_path(&path_to_file)
79 .first_raw()
80 .map(HeaderValue::from_static)
81 .unwrap_or_else(|| {
82 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
83 })
84 }
85
86 ServeVariant::SingleFile { mime } => mime,
87 };
88
89 if req.method() == Method::HEAD {
90 let (meta, maybe_encoding) =
91 file_metadata_with_fallback(path_to_file, negotiated_encodings).await?;
92
93 let last_modified = meta.modified().ok().map(LastModified::from);
94 if let Some(output) = check_modified_headers(
95 last_modified.as_ref(),
96 if_unmodified_since,
97 if_modified_since,
98 ) {
99 return Ok(output);
100 }
101
102 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
103
104 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
105 extent: FileRequestExtent::Head(meta),
106 chunk_size: buf_chunk_size,
107 mime_header_value: mime,
108 maybe_encoding,
109 maybe_range,
110 last_modified,
111 })))
112 } else {
113 let (mut file, maybe_encoding) =
114 match open_file_with_fallback(path_to_file, negotiated_encodings).await {
115 Ok(result) => result,
116
117 Err(err) if is_invalid_filename_error(&err) => {
118 return Ok(OpenFileOutput::InvalidFilename)
119 }
120 Err(err) => return Err(err),
121 };
122
123 let meta = file.metadata().await?;
124 let last_modified = meta.modified().ok().map(LastModified::from);
125 if let Some(output) = check_modified_headers(
126 last_modified.as_ref(),
127 if_unmodified_since,
128 if_modified_since,
129 ) {
130 return Ok(output);
131 }
132
133 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
134 if let Some(Ok(ranges)) = maybe_range.as_ref() {
135 if ranges.len() == 1 {
138 file.seek(SeekFrom::Start(*ranges[0].start())).await?;
139 }
140 }
141
142 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
143 extent: FileRequestExtent::Full(file, meta),
144 chunk_size: buf_chunk_size,
145 mime_header_value: mime,
146 maybe_encoding,
147 maybe_range,
148 last_modified,
149 })))
150 }
151}
152
153fn is_invalid_filename_error(err: &io::Error) -> bool {
154 if err.kind() == ErrorKind::InvalidInput {
156 return true;
157 }
158
159 #[cfg(windows)]
162 if let Some(raw_err) = err.raw_os_error() {
163 if (raw_err == 123) || (raw_err == 161) || (raw_err == 206) {
166 return true;
167 }
168 }
169
170 false
171}
172
173fn check_modified_headers(
174 modified: Option<&LastModified>,
175 if_unmodified_since: Option<IfUnmodifiedSince>,
176 if_modified_since: Option<IfModifiedSince>,
177) -> Option<OpenFileOutput> {
178 if let Some(since) = if_unmodified_since {
179 let precondition = modified
180 .as_ref()
181 .map(|time| since.precondition_passes(time))
182 .unwrap_or(false);
183
184 if !precondition {
185 return Some(OpenFileOutput::PreconditionFailed);
186 }
187 }
188
189 if let Some(since) = if_modified_since {
190 let unmodified = modified
191 .as_ref()
192 .map(|time| !since.is_modified(time))
193 .unwrap_or(false);
195 if unmodified {
196 return Some(OpenFileOutput::NotModified);
197 }
198 }
199
200 None
201}
202
203fn preferred_encoding(
206 path: &mut PathBuf,
207 negotiated_encoding: &[(Encoding, QValue)],
208) -> Option<Encoding> {
209 let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding.iter().copied());
210
211 if let Some(file_extension) =
212 preferred_encoding.and_then(|encoding| encoding.to_file_extension())
213 {
214 let new_file_name = path
215 .file_name()
216 .map(|file_name| {
217 let mut os_string = file_name.to_os_string();
218 os_string.push(file_extension);
219 os_string
220 })
221 .unwrap_or_else(|| file_extension.to_os_string());
222
223 path.set_file_name(new_file_name);
224 }
225
226 preferred_encoding
227}
228
229async fn open_file_with_fallback(
233 mut path: PathBuf,
234 mut negotiated_encoding: Vec<(Encoding, QValue)>,
235) -> io::Result<(File, Option<Encoding>)> {
236 let (file, encoding) = loop {
237 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
239 match (File::open(&path).await, encoding) {
240 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
241 (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
242 path.set_extension(OsStr::new(""));
245 negotiated_encoding
247 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
248 }
249 (Err(err), _) => return Err(err),
250 }
251 };
252 Ok((file, encoding))
253}
254
255async fn file_metadata_with_fallback(
259 mut path: PathBuf,
260 mut negotiated_encoding: Vec<(Encoding, QValue)>,
261) -> io::Result<(Metadata, Option<Encoding>)> {
262 let (file, encoding) = loop {
263 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
265 match (tokio::fs::metadata(&path).await, encoding) {
266 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
267 (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
268 path.set_extension(OsStr::new(""));
271 negotiated_encoding
273 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
274 }
275 (Err(err), _) => return Err(err),
276 }
277 };
278 Ok((file, encoding))
279}
280
281async fn maybe_redirect_or_append_path(
282 path_to_file: &mut PathBuf,
283 uri: &Uri,
284 append_index_html_on_directories: bool,
285) -> Option<OpenFileOutput> {
286 if !is_dir(path_to_file).await {
287 return None;
288 }
289
290 if !append_index_html_on_directories {
291 return Some(OpenFileOutput::FileNotFound);
292 }
293
294 if uri.path().ends_with('/') {
295 path_to_file.push("index.html");
296 None
297 } else {
298 let uri = match append_slash_on_path(uri.clone()) {
299 Ok(uri) => uri,
300 Err(err) => return Some(err),
301 };
302 let location = HeaderValue::from_str(&uri.to_string()).unwrap();
303 Some(OpenFileOutput::Redirect { location })
304 }
305}
306
307fn try_parse_range(
308 maybe_range_ref: Option<&str>,
309 file_size: u64,
310) -> Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>> {
311 maybe_range_ref.map(|header_value| {
312 http_range_header::parse_range_header(header_value)
313 .and_then(|first_pass| first_pass.validate(file_size))
314 })
315}
316
317async fn is_dir(path_to_file: &Path) -> bool {
318 tokio::fs::metadata(path_to_file)
319 .await
320 .map_or(false, |meta_data| meta_data.is_dir())
321}
322
323fn append_slash_on_path(uri: Uri) -> Result<Uri, OpenFileOutput> {
324 let http::uri::Parts {
325 scheme,
326 authority,
327 path_and_query,
328 ..
329 } = uri.into_parts();
330
331 let mut uri_builder = Uri::builder();
332
333 if let Some(scheme) = scheme {
334 uri_builder = uri_builder.scheme(scheme);
335 }
336
337 if let Some(authority) = authority {
338 uri_builder = uri_builder.authority(authority);
339 }
340
341 let uri_builder = if let Some(path_and_query) = path_and_query {
342 if let Some(query) = path_and_query.query() {
343 uri_builder.path_and_query(format!("{}/?{}", path_and_query.path(), query))
344 } else {
345 uri_builder.path_and_query(format!("{}/", path_and_query.path()))
346 }
347 } else {
348 uri_builder.path_and_query("/")
349 };
350
351 uri_builder.build().map_err(|err| {
352 tracing::error!(?err, "redirect uri failed to build");
353 OpenFileOutput::InvalidRedirectUri
354 })
355}
356
357#[test]
358fn preferred_encoding_with_extension() {
359 let mut path = PathBuf::from("hello.txt");
360 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
361 assert_eq!(path, PathBuf::from("hello.txt.gz"));
362}
363
364#[test]
365fn preferred_encoding_without_extension() {
366 let mut path = PathBuf::from("hello");
367 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
368 assert_eq!(path, PathBuf::from("hello.gz"));
369}