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, 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}
27
28pub(super) struct FileOpened {
29 pub(super) extent: FileRequestExtent,
30 pub(super) chunk_size: usize,
31 pub(super) mime_header_value: HeaderValue,
32 pub(super) maybe_encoding: Option<Encoding>,
33 pub(super) maybe_range: Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>>,
34 pub(super) last_modified: Option<LastModified>,
35}
36
37pub(super) enum FileRequestExtent {
38 Full(File, Metadata),
39 Head(Metadata),
40}
41
42pub(super) async fn open_file(
43 variant: ServeVariant,
44 mut path_to_file: PathBuf,
45 req: Request<Empty<Bytes>>,
46 negotiated_encodings: Vec<(Encoding, QValue)>,
47 range_header: Option<String>,
48 buf_chunk_size: usize,
49) -> io::Result<OpenFileOutput> {
50 let if_unmodified_since = req
51 .headers()
52 .get(header::IF_UNMODIFIED_SINCE)
53 .and_then(IfUnmodifiedSince::from_header_value);
54
55 let if_modified_since = req
56 .headers()
57 .get(header::IF_MODIFIED_SINCE)
58 .and_then(IfModifiedSince::from_header_value);
59
60 let mime = match variant {
61 ServeVariant::Directory {
62 append_index_html_on_directories,
63 } => {
64 if let Some(output) = maybe_redirect_or_append_path(
68 &mut path_to_file,
69 req.uri(),
70 append_index_html_on_directories,
71 )
72 .await
73 {
74 return Ok(output);
75 }
76
77 mime_guess::from_path(&path_to_file)
78 .first_raw()
79 .map(HeaderValue::from_static)
80 .unwrap_or_else(|| {
81 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
82 })
83 }
84
85 ServeVariant::SingleFile { mime } => mime,
86 };
87
88 if req.method() == Method::HEAD {
89 let (meta, maybe_encoding) =
90 file_metadata_with_fallback(path_to_file, negotiated_encodings).await?;
91
92 let last_modified = meta.modified().ok().map(LastModified::from);
93 if let Some(output) = check_modified_headers(
94 last_modified.as_ref(),
95 if_unmodified_since,
96 if_modified_since,
97 ) {
98 return Ok(output);
99 }
100
101 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
102
103 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
104 extent: FileRequestExtent::Head(meta),
105 chunk_size: buf_chunk_size,
106 mime_header_value: mime,
107 maybe_encoding,
108 maybe_range,
109 last_modified,
110 })))
111 } else {
112 let (mut file, maybe_encoding) =
113 open_file_with_fallback(path_to_file, negotiated_encodings).await?;
114 let meta = file.metadata().await?;
115 let last_modified = meta.modified().ok().map(LastModified::from);
116 if let Some(output) = check_modified_headers(
117 last_modified.as_ref(),
118 if_unmodified_since,
119 if_modified_since,
120 ) {
121 return Ok(output);
122 }
123
124 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
125 if let Some(Ok(ranges)) = maybe_range.as_ref() {
126 if ranges.len() == 1 {
129 file.seek(SeekFrom::Start(*ranges[0].start())).await?;
130 }
131 }
132
133 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
134 extent: FileRequestExtent::Full(file, meta),
135 chunk_size: buf_chunk_size,
136 mime_header_value: mime,
137 maybe_encoding,
138 maybe_range,
139 last_modified,
140 })))
141 }
142}
143
144fn check_modified_headers(
145 modified: Option<&LastModified>,
146 if_unmodified_since: Option<IfUnmodifiedSince>,
147 if_modified_since: Option<IfModifiedSince>,
148) -> Option<OpenFileOutput> {
149 if let Some(since) = if_unmodified_since {
150 let precondition = modified
151 .as_ref()
152 .map(|time| since.precondition_passes(time))
153 .unwrap_or(false);
154
155 if !precondition {
156 return Some(OpenFileOutput::PreconditionFailed);
157 }
158 }
159
160 if let Some(since) = if_modified_since {
161 let unmodified = modified
162 .as_ref()
163 .map(|time| !since.is_modified(time))
164 .unwrap_or(false);
166 if unmodified {
167 return Some(OpenFileOutput::NotModified);
168 }
169 }
170
171 None
172}
173
174fn preferred_encoding(
177 path: &mut PathBuf,
178 negotiated_encoding: &[(Encoding, QValue)],
179) -> Option<Encoding> {
180 let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding.iter().copied());
181
182 if let Some(file_extension) =
183 preferred_encoding.and_then(|encoding| encoding.to_file_extension())
184 {
185 let new_file_name = path
186 .file_name()
187 .map(|file_name| {
188 let mut os_string = file_name.to_os_string();
189 os_string.push(file_extension);
190 os_string
191 })
192 .unwrap_or_else(|| file_extension.to_os_string());
193
194 path.set_file_name(new_file_name);
195 }
196
197 preferred_encoding
198}
199
200async fn open_file_with_fallback(
204 mut path: PathBuf,
205 mut negotiated_encoding: Vec<(Encoding, QValue)>,
206) -> io::Result<(File, Option<Encoding>)> {
207 let (file, encoding) = loop {
208 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
210 match (File::open(&path).await, encoding) {
211 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
212 (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
213 path.set_extension(OsStr::new(""));
216 negotiated_encoding
218 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
219 }
220 (Err(err), _) => return Err(err),
221 }
222 };
223 Ok((file, encoding))
224}
225
226async 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 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 path.set_extension(OsStr::new(""));
242 negotiated_encoding
244 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
245 }
246 (Err(err), _) => return Err(err),
247 }
248 };
249 Ok((file, encoding))
250}
251
252async fn maybe_redirect_or_append_path(
253 path_to_file: &mut PathBuf,
254 uri: &Uri,
255 append_index_html_on_directories: bool,
256) -> Option<OpenFileOutput> {
257 if !is_dir(path_to_file).await {
258 return None;
259 }
260
261 if !append_index_html_on_directories {
262 return Some(OpenFileOutput::FileNotFound);
263 }
264
265 if uri.path().ends_with('/') {
266 path_to_file.push("index.html");
267 None
268 } else {
269 let uri = match append_slash_on_path(uri.clone()) {
270 Ok(uri) => uri,
271 Err(err) => return Some(err),
272 };
273 let location = HeaderValue::from_str(&uri.to_string()).unwrap();
274 Some(OpenFileOutput::Redirect { location })
275 }
276}
277
278fn try_parse_range(
279 maybe_range_ref: Option<&str>,
280 file_size: u64,
281) -> Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>> {
282 maybe_range_ref.map(|header_value| {
283 http_range_header::parse_range_header(header_value)
284 .and_then(|first_pass| first_pass.validate(file_size))
285 })
286}
287
288async fn is_dir(path_to_file: &Path) -> bool {
289 tokio::fs::metadata(path_to_file)
290 .await
291 .map_or(false, |meta_data| meta_data.is_dir())
292}
293
294fn append_slash_on_path(uri: Uri) -> Result<Uri, OpenFileOutput> {
295 let http::uri::Parts {
296 scheme,
297 authority,
298 path_and_query,
299 ..
300 } = uri.into_parts();
301
302 let mut uri_builder = Uri::builder();
303
304 if let Some(scheme) = scheme {
305 uri_builder = uri_builder.scheme(scheme);
306 }
307
308 if let Some(authority) = authority {
309 uri_builder = uri_builder.authority(authority);
310 }
311
312 let uri_builder = if let Some(path_and_query) = path_and_query {
313 if let Some(query) = path_and_query.query() {
314 uri_builder.path_and_query(format!("{}/?{}", path_and_query.path(), query))
315 } else {
316 uri_builder.path_and_query(format!("{}/", path_and_query.path()))
317 }
318 } else {
319 uri_builder.path_and_query("/")
320 };
321
322 uri_builder.build().map_err(|err| {
323 tracing::error!(?err, "redirect uri failed to build");
324 OpenFileOutput::InvalidRedirectUri
325 })
326}
327
328#[test]
329fn preferred_encoding_with_extension() {
330 let mut path = PathBuf::from("hello.txt");
331 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
332 assert_eq!(path, PathBuf::from("hello.txt.gz"));
333}
334
335#[test]
336fn preferred_encoding_without_extension() {
337 let mut path = PathBuf::from("hello");
338 preferred_encoding(&mut path, &[(Encoding::Gzip, QValue::one())]);
339 assert_eq!(path, PathBuf::from("hello.gz"));
340}