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, 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}
26
27pub(super) struct FileOpened {
28    pub(super) extent: FileRequestExtent,
29    pub(super) chunk_size: usize,
30    pub(super) mime_header_value: HeaderValue,
31    pub(super) maybe_encoding: Option<Encoding>,
32    pub(super) maybe_range: Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>>,
33    pub(super) last_modified: Option<LastModified>,
34}
35
36pub(super) enum FileRequestExtent {
37    Full(File, Metadata),
38    Head(Metadata),
39}
40
41pub(super) async fn open_file(
42    variant: ServeVariant,
43    mut path_to_file: PathBuf,
44    req: Request<Empty<Bytes>>,
45    negotiated_encodings: Vec<(Encoding, QValue)>,
46    range_header: Option<String>,
47    buf_chunk_size: usize,
48) -> io::Result<OpenFileOutput> {
49    let if_unmodified_since = req
50        .headers()
51        .get(header::IF_UNMODIFIED_SINCE)
52        .and_then(IfUnmodifiedSince::from_header_value);
53
54    let if_modified_since = req
55        .headers()
56        .get(header::IF_MODIFIED_SINCE)
57        .and_then(IfModifiedSince::from_header_value);
58
59    let mime = match variant {
60        ServeVariant::Directory {
61            append_index_html_on_directories,
62        } => {
63            // Might already at this point know a redirect or not found result should be
64            // returned which corresponds to a Some(output). Otherwise the path might be
65            // modified and proceed to the open file/metadata future.
66            if let Some(output) = maybe_redirect_or_append_path(
67                &mut path_to_file,
68                req.uri(),
69                append_index_html_on_directories,
70            )
71            .await
72            {
73                return Ok(output);
74            }
75
76            mime_guess::from_path(&path_to_file)
77                .first_raw()
78                .map(HeaderValue::from_static)
79                .unwrap_or_else(|| {
80                    HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
81                })
82        }
83
84        ServeVariant::SingleFile { mime } => mime,
85    };
86
87    if req.method() == Method::HEAD {
88        let (meta, maybe_encoding) =
89            file_metadata_with_fallback(path_to_file, negotiated_encodings).await?;
90
91        let last_modified = meta.modified().ok().map(LastModified::from);
92        if let Some(output) = check_modified_headers(
93            last_modified.as_ref(),
94            if_unmodified_since,
95            if_modified_since,
96        ) {
97            return Ok(output);
98        }
99
100        let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
101
102        Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
103            extent: FileRequestExtent::Head(meta),
104            chunk_size: buf_chunk_size,
105            mime_header_value: mime,
106            maybe_encoding,
107            maybe_range,
108            last_modified,
109        })))
110    } else {
111        let (mut file, maybe_encoding) =
112            open_file_with_fallback(path_to_file, negotiated_encodings).await?;
113        let meta = file.metadata().await?;
114        let last_modified = meta.modified().ok().map(LastModified::from);
115        if let Some(output) = check_modified_headers(
116            last_modified.as_ref(),
117            if_unmodified_since,
118            if_modified_since,
119        ) {
120            return Ok(output);
121        }
122
123        let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
124        if let Some(Ok(ranges)) = maybe_range.as_ref() {
125            // if there is any other amount of ranges than 1 we'll return an
126            // unsatisfiable later as there isn't yet support for multipart ranges
127            if ranges.len() == 1 {
128                file.seek(SeekFrom::Start(*ranges[0].start())).await?;
129            }
130        }
131
132        Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
133            extent: FileRequestExtent::Full(file, meta),
134            chunk_size: buf_chunk_size,
135            mime_header_value: mime,
136            maybe_encoding,
137            maybe_range,
138            last_modified,
139        })))
140    }
141}
142
143fn check_modified_headers(
144    modified: Option<&LastModified>,
145    if_unmodified_since: Option<IfUnmodifiedSince>,
146    if_modified_since: Option<IfModifiedSince>,
147) -> Option<OpenFileOutput> {
148    if let Some(since) = if_unmodified_since {
149        let precondition = modified
150            .as_ref()
151            .map(|time| since.precondition_passes(time))
152            .unwrap_or(false);
153
154        if !precondition {
155            return Some(OpenFileOutput::PreconditionFailed);
156        }
157    }
158
159    if let Some(since) = if_modified_since {
160        let unmodified = modified
161            .as_ref()
162            .map(|time| !since.is_modified(time))
163            // no last_modified means its always modified
164            .unwrap_or(false);
165        if unmodified {
166            return Some(OpenFileOutput::NotModified);
167        }
168    }
169
170    None
171}
172
173// Returns the preferred_encoding encoding and modifies the path extension
174// to the corresponding file extension for the encoding.
175fn preferred_encoding(
176    path: &mut PathBuf,
177    negotiated_encoding: &[(Encoding, QValue)],
178) -> Option<Encoding> {
179    let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding.iter().copied());
180
181    if let Some(file_extension) =
182        preferred_encoding.and_then(|encoding| encoding.to_file_extension())
183    {
184        let new_extension = path
185            .extension()
186            .map(|extension| {
187                let mut os_string = extension.to_os_string();
188                os_string.push(file_extension);
189                os_string
190            })
191            .unwrap_or_else(|| file_extension.to_os_string());
192
193        path.set_extension(new_extension);
194    }
195
196    preferred_encoding
197}
198
199// Attempts to open the file with any of the possible negotiated_encodings in the
200// preferred order. If none of the negotiated_encodings have a corresponding precompressed
201// file the uncompressed file is used as a fallback.
202async fn open_file_with_fallback(
203    mut path: PathBuf,
204    mut negotiated_encoding: Vec<(Encoding, QValue)>,
205) -> io::Result<(File, Option<Encoding>)> {
206    let (file, encoding) = loop {
207        // Get the preferred encoding among the negotiated ones.
208        let encoding = preferred_encoding(&mut path, &negotiated_encoding);
209        match (File::open(&path).await, encoding) {
210            (Ok(file), maybe_encoding) => break (file, maybe_encoding),
211            (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
212                // Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
213                // to reset the path before the next iteration.
214                path.set_extension(OsStr::new(""));
215                // Remove the encoding from the negotiated_encodings since the file doesn't exist
216                negotiated_encoding
217                    .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
218                continue;
219            }
220            (Err(err), _) => return Err(err),
221        };
222    };
223    Ok((file, encoding))
224}
225
226// Attempts to get the file metadata with any of the possible negotiated_encodings in the
227// preferred order. If none of the negotiated_encodings have a corresponding precompressed
228// file the uncompressed file is used as a fallback.
229async fn file_metadata_with_fallback(
230    mut path: PathBuf,
231    mut negotiated_encoding: Vec<(Encoding, QValue)>,
232) -> io::Result<(Metadata, Option<Encoding>)> {
233    let (file, encoding) = loop {
234        // Get the preferred encoding among the negotiated ones.
235        let encoding = preferred_encoding(&mut path, &negotiated_encoding);
236        match (tokio::fs::metadata(&path).await, encoding) {
237            (Ok(file), maybe_encoding) => break (file, maybe_encoding),
238            (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
239                // Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
240                // to reset the path before the next iteration.
241                path.set_extension(OsStr::new(""));
242                // Remove the encoding from the negotiated_encodings since the file doesn't exist
243                negotiated_encoding
244                    .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
245                continue;
246            }
247            (Err(err), _) => return Err(err),
248        };
249    };
250    Ok((file, encoding))
251}
252
253async fn maybe_redirect_or_append_path(
254    path_to_file: &mut PathBuf,
255    uri: &Uri,
256    append_index_html_on_directories: bool,
257) -> Option<OpenFileOutput> {
258    if !is_dir(path_to_file).await {
259        return None;
260    }
261
262    if !append_index_html_on_directories {
263        return Some(OpenFileOutput::FileNotFound);
264    }
265
266    if uri.path().ends_with('/') {
267        path_to_file.push("index.html");
268        None
269    } else {
270        let location =
271            HeaderValue::from_str(&append_slash_on_path(uri.clone()).to_string()).unwrap();
272        Some(OpenFileOutput::Redirect { location })
273    }
274}
275
276fn try_parse_range(
277    maybe_range_ref: Option<&str>,
278    file_size: u64,
279) -> Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>> {
280    maybe_range_ref.map(|header_value| {
281        http_range_header::parse_range_header(header_value)
282            .and_then(|first_pass| first_pass.validate(file_size))
283    })
284}
285
286async fn is_dir(path_to_file: &Path) -> bool {
287    tokio::fs::metadata(path_to_file)
288        .await
289        .map_or(false, |meta_data| meta_data.is_dir())
290}
291
292fn append_slash_on_path(uri: Uri) -> Uri {
293    let http::uri::Parts {
294        scheme,
295        authority,
296        path_and_query,
297        ..
298    } = uri.into_parts();
299
300    let mut uri_builder = Uri::builder();
301
302    if let Some(scheme) = scheme {
303        uri_builder = uri_builder.scheme(scheme);
304    }
305
306    if let Some(authority) = authority {
307        uri_builder = uri_builder.authority(authority);
308    }
309
310    let uri_builder = if let Some(path_and_query) = path_and_query {
311        if let Some(query) = path_and_query.query() {
312            uri_builder.path_and_query(format!("{}/?{}", path_and_query.path(), query))
313        } else {
314            uri_builder.path_and_query(format!("{}/", path_and_query.path()))
315        }
316    } else {
317        uri_builder.path_and_query("/")
318    };
319
320    uri_builder.build().unwrap()
321}