tower_http/services/fs/serve_dir/
open_file.rs

1use 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            // Might already at this point know a redirect or not found result should be
66            // returned which corresponds to a Some(output). Otherwise the path might be
67            // modified and proceed to the open file/metadata future.
68            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 there is any other amount of ranges than 1 we'll return an
136            // unsatisfiable later as there isn't yet support for multipart ranges
137            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    // Only applies to NULL bytes
155    if err.kind() == ErrorKind::InvalidInput {
156        return true;
157    }
158
159    // FIXME: Remove when MSRV >= 1.87.
160    // `io::ErrorKind::InvalidFilename` is stabilized in v1.87
161    #[cfg(windows)]
162    if let Some(raw_err) = err.raw_os_error() {
163        // https://github.com/rust-lang/rust/blob/70e2b4a4d197f154bed0eb3dcb5cac6a948ff3a3/library/std/src/sys/pal/windows/mod.rs
164        // Lines 81 and 115
165        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            // no last_modified means its always modified
194            .unwrap_or(false);
195        if unmodified {
196            return Some(OpenFileOutput::NotModified);
197        }
198    }
199
200    None
201}
202
203// Returns the preferred_encoding encoding and modifies the path extension
204// to the corresponding file extension for the encoding.
205fn 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
229// Attempts to open the file with any of the possible negotiated_encodings in the
230// preferred order. If none of the negotiated_encodings have a corresponding precompressed
231// file the uncompressed file is used as a fallback.
232async 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        // Get the preferred encoding among the negotiated ones.
238        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                // Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
243                // to reset the path before the next iteration.
244                path.set_extension(OsStr::new(""));
245                // Remove the encoding from the negotiated_encodings since the file doesn't exist
246                negotiated_encoding
247                    .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
248            }
249            (Err(err), _) => return Err(err),
250        }
251    };
252    Ok((file, encoding))
253}
254
255// Attempts to get the file metadata with any of the possible negotiated_encodings in the
256// preferred order. If none of the negotiated_encodings have a corresponding precompressed
257// file the uncompressed file is used as a fallback.
258async 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        // Get the preferred encoding among the negotiated ones.
264        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                // Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
269                // to reset the path before the next iteration.
270                path.set_extension(OsStr::new(""));
271                // Remove the encoding from the negotiated_encodings since the file doesn't exist
272                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}