1use headers::{AcceptRanges, HeaderMap, HeaderMapExt, HeaderValue};
13use hyper::{Body, Method, Response, StatusCode, header::CONTENT_ENCODING, header::CONTENT_LENGTH};
14use std::fs::{File, Metadata};
15use std::io;
16use std::path::PathBuf;
17
18use crate::Result;
19use crate::conditional_headers::ConditionalHeaders;
20use crate::fs::meta::{FileMetadata, try_metadata, try_metadata_with_html_suffix};
21use crate::fs::path::{PathExt, sanitize_path};
22use crate::http_ext::{HTTP_SUPPORTED_METHODS, MethodExt};
23use crate::response::response_body;
24
25#[cfg(feature = "experimental")]
26use crate::mem_cache::{cache, cache::MemCacheOpts};
27
28use crate::compression_static;
29
30#[cfg(feature = "directory-listing")]
31use crate::{
32 directory_listing,
33 directory_listing::{DirListFmt, DirListOpts},
34};
35
36#[cfg(feature = "directory-listing-download")]
37use crate::directory_listing_download::{
38 DOWNLOAD_PARAM_KEY, DirDownloadFmt, DirDownloadOpts, archive_reply,
39};
40
41const DEFAULT_INDEX_FILES: &[&str; 1] = &["index.html"];
42
43pub struct HandleOpts<'a> {
45 pub method: &'a Method,
47 #[cfg(feature = "experimental")]
49 pub memory_cache: Option<&'a MemCacheOpts>,
50 pub headers: &'a HeaderMap<HeaderValue>,
52 pub base_path: &'a PathBuf,
54 pub uri_path: &'a str,
56 pub index_files: &'a [&'a str],
58 pub uri_query: Option<&'a str>,
60 #[cfg(feature = "directory-listing")]
62 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
63 pub dir_listing: bool,
64 #[cfg(feature = "directory-listing")]
66 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
67 pub dir_listing_order: u8,
68 #[cfg(feature = "directory-listing")]
70 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
71 pub dir_listing_format: &'a DirListFmt,
72 #[cfg(feature = "directory-listing-download")]
74 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
75 pub dir_listing_download: &'a [DirDownloadFmt],
76 pub redirect_trailing_slash: bool,
78 pub compression_static: bool,
80 pub ignore_hidden_files: bool,
82 pub disable_symlinks: bool,
84}
85
86pub struct StaticFileResponse {
88 pub resp: Response<Body>,
90 pub file_path: PathBuf,
92}
93
94pub async fn handle(opts: &HandleOpts<'_>) -> Result<StaticFileResponse, StatusCode> {
97 let method = opts.method;
98 if !method.is_allowed() {
100 return Err(StatusCode::METHOD_NOT_ALLOWED);
101 }
102
103 let uri_path = opts.uri_path;
104 let mut file_path = sanitize_path(opts.base_path, uri_path)?;
105
106 let headers_opt = opts.headers;
107
108 #[cfg(feature = "experimental")]
110 if opts.memory_cache.is_some() {
111 if opts.redirect_trailing_slash && uri_path.ends_with('/') {
114 file_path.push("index.html");
115 }
116
117 if let Some(result) = cache::get_or_acquire(file_path.as_path(), headers_opt).await {
118 return Ok(StaticFileResponse {
119 resp: result?,
120 file_path,
122 });
123 }
124 }
125
126 let FileMetadata {
127 file_path,
128 metadata,
129 is_dir,
130 precompressed_variant,
131 } = get_composed_file_metadata(
132 &mut file_path,
133 headers_opt,
134 opts.compression_static,
135 opts.index_files,
136 )?;
137
138 let mut file_path_temp = file_path.clone();
139 if is_dir {
140 file_path_temp.pop();
141 }
142
143 let file_path_relative = file_path_temp.strip_prefix(opts.base_path).map_err(|err| {
144 tracing::error!(
145 "unable to strip prefix from file path '{}': {}",
146 file_path.display(),
147 err,
148 );
149 StatusCode::NOT_FOUND
150 })?;
151
152 let file_path_resolved = file_path_temp.canonicalize().map_err(|err| {
153 tracing::error!(
154 "unable to resolve '{}' symlink path: {}",
155 file_path_temp.display(),
156 err,
157 );
158 StatusCode::NOT_FOUND
159 })?;
160
161 let base_path = opts.base_path.canonicalize().map_err(|err| {
162 tracing::error!(
163 "unable to resolve '{}' base path: {}",
164 opts.base_path.display(),
165 err,
166 );
167 StatusCode::NOT_FOUND
168 })?;
169
170 if !file_path_resolved.starts_with(base_path) {
171 tracing::error!(
172 "file path '{}' resolves outside of the base path, access denied",
173 file_path_resolved.display()
174 );
175 return Err(StatusCode::NOT_FOUND);
176 }
177
178 if opts.disable_symlinks {
179 let has_symlink = file_path_relative
182 .contains_symlink(opts.base_path)
183 .map_err(|err| {
184 tracing::error!(
185 "unable to check if file path '{}' contains symlink: {}",
186 file_path_relative.display(),
187 err,
188 );
189 StatusCode::NOT_FOUND
190 })?;
191
192 if has_symlink {
193 tracing::warn!(
194 "file path '{}' contains a symlink, access denied",
195 file_path.display()
196 );
197 return Err(StatusCode::FORBIDDEN);
198 }
199 }
200
201 if opts.ignore_hidden_files && file_path_relative.is_hidden() {
203 tracing::trace!(
204 "considering hidden file {} as not found",
205 file_path.display()
206 );
207 return Err(StatusCode::NOT_FOUND);
208 }
209
210 let resp_file_path = file_path.to_owned();
211
212 if is_dir && opts.redirect_trailing_slash && !uri_path.ends_with('/') {
215 let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
216 let uri = [uri_path, "/", query.as_str()].concat();
217 let loc = match HeaderValue::from_str(uri.as_str()) {
218 Ok(val) => val,
219 Err(err) => {
220 tracing::error!("invalid header value from current uri: {:?}", err);
221 return Err(StatusCode::INTERNAL_SERVER_ERROR);
222 }
223 };
224
225 let mut resp = Response::new(Body::empty());
226 resp.headers_mut().insert(hyper::header::LOCATION, loc);
227 *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
228
229 tracing::trace!("uri doesn't end with a slash so redirecting permanently");
230 return Ok(StaticFileResponse {
231 resp,
232 file_path: resp_file_path,
233 });
234 }
235
236 if method.is_options() {
238 let mut resp = Response::new(Body::empty());
239 *resp.status_mut() = StatusCode::NO_CONTENT;
240 resp.headers_mut()
241 .typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS.clone()));
242 resp.headers_mut().typed_insert(AcceptRanges::bytes());
243
244 return Ok(StaticFileResponse {
245 resp,
246 file_path: resp_file_path,
247 });
248 }
249
250 #[cfg(feature = "directory-listing")]
255 if is_dir && opts.dir_listing && !file_path.exists() {
256 #[cfg(feature = "directory-listing-download")]
261 if !opts.dir_listing_download.is_empty()
262 && let Some((_k, _dl_archive_opt)) =
263 form_urlencoded::parse(opts.uri_query.unwrap_or("").as_bytes())
264 .find(|(k, _v)| k == DOWNLOAD_PARAM_KEY)
265 {
266 let mut fp = file_path.clone();
268 fp.pop();
269 if let Some(filename) = fp.file_name() {
270 let resp = archive_reply(
271 filename,
272 &fp,
273 DirDownloadOpts {
274 method,
275 disable_symlinks: opts.disable_symlinks,
276 ignore_hidden_files: opts.ignore_hidden_files,
277 },
278 );
279 return Ok(StaticFileResponse {
280 resp,
281 file_path: resp_file_path,
282 });
283 } else {
284 tracing::error!("Unable to get filename from {}", fp.to_string_lossy());
285 return Err(StatusCode::INTERNAL_SERVER_ERROR);
286 }
287 }
288
289 let resp = directory_listing::auto_index(DirListOpts {
290 root_path: opts.base_path.as_path(),
291 method,
292 current_path: uri_path,
293 uri_query: opts.uri_query,
294 filepath: file_path,
295 dir_listing_order: opts.dir_listing_order,
296 dir_listing_format: opts.dir_listing_format,
297 ignore_hidden_files: opts.ignore_hidden_files,
298 disable_symlinks: opts.disable_symlinks,
299 #[cfg(feature = "directory-listing-download")]
300 dir_listing_download: opts.dir_listing_download,
301 })?;
302
303 return Ok(StaticFileResponse {
304 resp,
305 file_path: resp_file_path,
306 });
307 }
308
309 if let Some(precompressed_meta) = precompressed_variant {
311 let (precomp_path, precomp_encoding) = precompressed_meta;
312 let mut resp = file_reply(
313 headers_opt,
314 file_path,
315 &metadata,
316 Some(precomp_path),
317 #[cfg(feature = "experimental")]
318 opts.memory_cache,
319 )?;
320
321 resp.headers_mut().remove(CONTENT_LENGTH);
323 let encoding = match HeaderValue::from_str(precomp_encoding.as_str()) {
324 Ok(val) => val,
325 Err(err) => {
326 tracing::error!(
327 "unable to parse header value from content encoding: {:?}",
328 err
329 );
330 return Err(StatusCode::INTERNAL_SERVER_ERROR);
331 }
332 };
333 resp.headers_mut().insert(CONTENT_ENCODING, encoding);
334
335 return Ok(StaticFileResponse {
336 resp,
337 file_path: resp_file_path,
338 });
339 }
340
341 #[cfg(feature = "experimental")]
342 let resp = file_reply(headers_opt, file_path, &metadata, None, opts.memory_cache)?;
343
344 #[cfg(not(feature = "experimental"))]
345 let resp = file_reply(headers_opt, file_path, &metadata, None)?;
346
347 Ok(StaticFileResponse {
348 resp,
349 file_path: resp_file_path,
350 })
351}
352
353fn get_composed_file_metadata<'a>(
357 mut file_path: &'a mut PathBuf,
358 headers: &'a HeaderMap<HeaderValue>,
359 compression_static: bool,
360 mut index_files: &'a [&'a str],
361) -> Result<FileMetadata<'a>, StatusCode> {
362 tracing::trace!("getting metadata for file {}", file_path.display());
363
364 match try_metadata(file_path) {
366 Ok((mut metadata, is_dir)) => {
367 if is_dir {
368 if index_files.is_empty() {
370 index_files = DEFAULT_INDEX_FILES;
371 }
372 let mut index_found = false;
373 for index in index_files {
374 tracing::debug!("dir: appending {} to the directory path", index);
376 file_path.push(index);
377
378 if compression_static
379 && let Some(p) =
380 compression_static::precompressed_variant(file_path, headers)
381 {
382 return Ok(FileMetadata {
383 file_path,
384 metadata: p.metadata,
385 is_dir: false,
386 precompressed_variant: Some((p.file_path, p.encoding)),
387 });
388 }
389
390 if let Ok(meta_res) = try_metadata(file_path) {
394 (metadata, _) = meta_res;
395 index_found = true;
396 break;
397 }
398
399 file_path.pop();
401 let new_meta: Option<Metadata>;
402 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
403 if let Some(new_meta) = new_meta {
404 metadata = new_meta;
405 index_found = true;
406 break;
407 }
408 }
409
410 if !index_found && !index_files.is_empty() {
413 file_path.push(index_files.last().unwrap());
414 }
415 }
416
417 let precompressed_variant = compression_static
418 .then(|| compression_static::precompressed_variant(file_path, headers))
419 .flatten()
420 .map(|p| (p.file_path, p.encoding));
421
422 Ok(FileMetadata {
423 file_path,
424 metadata,
425 is_dir,
426 precompressed_variant,
427 })
428 }
429 Err(err) => {
430 if compression_static
432 && let Some(p) = compression_static::precompressed_variant(file_path, headers)
433 {
434 return Ok(FileMetadata {
435 file_path,
436 metadata: p.metadata,
437 is_dir: false,
438 precompressed_variant: Some((p.file_path, p.encoding)),
439 });
440 }
441
442 let new_meta: Option<Metadata>;
446 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
447
448 #[cfg(any(
449 feature = "compression",
450 feature = "compression-deflate",
451 feature = "compression-gzip",
452 feature = "compression-brotli",
453 feature = "compression-zstd"
454 ))]
455 match new_meta {
456 Some(new_meta) => {
457 return Ok(FileMetadata {
458 file_path,
459 metadata: new_meta,
460 is_dir: false,
461 precompressed_variant: None,
462 });
463 }
464 _ => {
465 if compression_static
467 && let Some(p) =
468 compression_static::precompressed_variant(file_path, headers)
469 {
470 return Ok(FileMetadata {
471 file_path,
472 metadata: p.metadata,
473 is_dir: false,
474 precompressed_variant: Some((p.file_path, p.encoding)),
475 });
476 }
477 }
478 }
479 #[cfg(not(feature = "compression"))]
480 if let Some(new_meta) = new_meta {
481 return Ok(FileMetadata {
482 file_path,
483 metadata: new_meta,
484 is_dir: false,
485 precompressed_variant: None,
486 });
487 }
488
489 Err(err)
490 }
491 }
492}
493
494fn file_reply<'a>(
501 headers: &'a HeaderMap<HeaderValue>,
502 path: &'a PathBuf,
503 meta: &'a Metadata,
504 path_precompressed: Option<PathBuf>,
505 #[cfg(feature = "experimental")] memory_cache: Option<&'a MemCacheOpts>,
506) -> Result<Response<Body>, StatusCode> {
507 let conditionals = ConditionalHeaders::new(headers);
508 let file_path = path_precompressed.as_ref().unwrap_or(path);
509
510 match File::open(file_path) {
511 Ok(file) => {
512 #[cfg(feature = "experimental")]
513 let resp = response_body(file, path, meta, conditionals, memory_cache);
514
515 #[cfg(not(feature = "experimental"))]
516 let resp = response_body(file, path, meta, conditionals);
517
518 resp
519 }
520 Err(err) => {
521 let status = match err.kind() {
522 io::ErrorKind::NotFound => {
523 tracing::debug!("file can't be opened or not found: {:?}", path.display());
524 StatusCode::NOT_FOUND
525 }
526 io::ErrorKind::PermissionDenied => {
527 tracing::warn!("file permission denied: {:?}", path.display());
528 StatusCode::FORBIDDEN
529 }
530 _ => {
531 tracing::error!("file open error (path={:?}): {} ", path.display(), err);
532 StatusCode::INTERNAL_SERVER_ERROR
533 }
534 };
535 Err(status)
536 }
537 }
538}