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
28#[cfg(any(
29 feature = "compression",
30 feature = "compression-deflate",
31 feature = "compression-gzip",
32 feature = "compression-brotli",
33 feature = "compression-zstd"
34))]
35use crate::compression_static;
36
37#[cfg(feature = "directory-listing")]
38use crate::{
39 directory_listing,
40 directory_listing::{DirListFmt, DirListOpts},
41};
42
43#[cfg(feature = "directory-listing-download")]
44use crate::directory_listing_download::{
45 DOWNLOAD_PARAM_KEY, DirDownloadFmt, DirDownloadOpts, archive_reply,
46};
47
48const DEFAULT_INDEX_FILES: &[&str; 1] = &["index.html"];
49
50pub struct HandleOpts<'a> {
52 pub method: &'a Method,
54 #[cfg(feature = "experimental")]
56 pub memory_cache: Option<&'a MemCacheOpts>,
57 pub headers: &'a HeaderMap<HeaderValue>,
59 pub base_path: &'a PathBuf,
61 pub uri_path: &'a str,
63 pub index_files: &'a [&'a str],
65 pub uri_query: Option<&'a str>,
67 #[cfg(feature = "directory-listing")]
69 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
70 pub dir_listing: bool,
71 #[cfg(feature = "directory-listing")]
73 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
74 pub dir_listing_order: u8,
75 #[cfg(feature = "directory-listing")]
77 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
78 pub dir_listing_format: &'a DirListFmt,
79 #[cfg(feature = "directory-listing-download")]
81 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
82 pub dir_listing_download: &'a [DirDownloadFmt],
83 pub redirect_trailing_slash: bool,
85 pub compression_static: bool,
87 pub ignore_hidden_files: bool,
89 pub disable_symlinks: bool,
91}
92
93pub struct StaticFileResponse {
95 pub resp: Response<Body>,
97 pub file_path: PathBuf,
99}
100
101pub async fn handle(opts: &HandleOpts<'_>) -> Result<StaticFileResponse, StatusCode> {
104 let method = opts.method;
105 let uri_path = opts.uri_path;
106
107 if !method.is_allowed() {
109 return Err(StatusCode::METHOD_NOT_ALLOWED);
110 }
111
112 let headers_opt = opts.headers;
113 let mut file_path = sanitize_path(opts.base_path, uri_path)?;
114
115 #[cfg(feature = "experimental")]
117 if opts.memory_cache.is_some() {
118 if opts.redirect_trailing_slash && uri_path.ends_with('/') {
121 file_path.push("index.html");
122 }
123
124 if let Some(result) = cache::get_or_acquire(file_path.as_path(), headers_opt).await {
125 return Ok(StaticFileResponse {
126 resp: result?,
127 file_path,
129 });
130 }
131 }
132
133 if file_path.is_symlink() {
135 if opts.disable_symlinks {
136 tracing::warn!(
137 "file path {} is a symlink, access denied",
138 file_path.display()
139 );
140 return Err(StatusCode::FORBIDDEN);
141 }
142
143 let symlink_file_path = file_path.canonicalize().map_err(|err| {
144 tracing::error!(
145 "unable to resolve `{}` symlink path: {}",
146 file_path.display(),
147 err,
148 );
149 StatusCode::NOT_FOUND
150 })?;
151
152 let base_path = opts.base_path.canonicalize().map_err(|err| {
153 tracing::error!(
154 "unable to resolve `{}` symlink path: {}",
155 file_path.display(),
156 err,
157 );
158 StatusCode::NOT_FOUND
159 })?;
160
161 if !symlink_file_path.starts_with(base_path) {
162 tracing::error!(
163 "file path {} is a symlink, access denied",
164 symlink_file_path.display()
165 );
166 return Err(StatusCode::NOT_FOUND);
167 }
168 }
169
170 let FileMetadata {
171 file_path,
172 metadata,
173 is_dir,
174 precompressed_variant,
175 } = get_composed_file_metadata(
176 &mut file_path,
177 headers_opt,
178 opts.compression_static,
179 opts.index_files,
180 )?;
181
182 if opts.ignore_hidden_files && file_path.is_hidden() {
184 return Err(StatusCode::NOT_FOUND);
185 }
186
187 let resp_file_path = file_path.to_owned();
188
189 if is_dir && opts.redirect_trailing_slash && !uri_path.ends_with('/') {
192 let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
193 let uri = [uri_path, "/", query.as_str()].concat();
194 let loc = match HeaderValue::from_str(uri.as_str()) {
195 Ok(val) => val,
196 Err(err) => {
197 tracing::error!("invalid header value from current uri: {:?}", err);
198 return Err(StatusCode::INTERNAL_SERVER_ERROR);
199 }
200 };
201
202 let mut resp = Response::new(Body::empty());
203 resp.headers_mut().insert(hyper::header::LOCATION, loc);
204 *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
205
206 tracing::trace!("uri doesn't end with a slash so redirecting permanently");
207 return Ok(StaticFileResponse {
208 resp,
209 file_path: resp_file_path,
210 });
211 }
212
213 if method.is_options() {
215 let mut resp = Response::new(Body::empty());
216 *resp.status_mut() = StatusCode::NO_CONTENT;
217 resp.headers_mut()
218 .typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS.clone()));
219 resp.headers_mut().typed_insert(AcceptRanges::bytes());
220
221 return Ok(StaticFileResponse {
222 resp,
223 file_path: resp_file_path,
224 });
225 }
226
227 #[cfg(feature = "directory-listing")]
232 if is_dir && opts.dir_listing && !file_path.exists() {
233 #[cfg(feature = "directory-listing-download")]
238 if !opts.dir_listing_download.is_empty() {
239 if let Some((_k, _dl_archive_opt)) =
240 form_urlencoded::parse(opts.uri_query.unwrap_or("").as_bytes())
241 .find(|(k, _v)| k == DOWNLOAD_PARAM_KEY)
242 {
243 let mut fp = file_path.clone();
245 fp.pop();
246 if let Some(filename) = fp.file_name() {
247 let resp = archive_reply(
248 filename,
249 &fp,
250 DirDownloadOpts {
251 method,
252 disable_symlinks: opts.disable_symlinks,
253 ignore_hidden_files: opts.ignore_hidden_files,
254 },
255 );
256 return Ok(StaticFileResponse {
257 resp,
258 file_path: resp_file_path,
259 });
260 } else {
261 tracing::error!("Unable to get filename from {}", fp.to_string_lossy());
262 return Err(StatusCode::INTERNAL_SERVER_ERROR);
263 }
264 }
265 }
266
267 let resp = directory_listing::auto_index(DirListOpts {
268 root_path: opts.base_path.as_path(),
269 method,
270 current_path: uri_path,
271 uri_query: opts.uri_query,
272 filepath: file_path,
273 dir_listing_order: opts.dir_listing_order,
274 dir_listing_format: opts.dir_listing_format,
275 ignore_hidden_files: opts.ignore_hidden_files,
276 disable_symlinks: opts.disable_symlinks,
277 #[cfg(feature = "directory-listing-download")]
278 dir_listing_download: opts.dir_listing_download,
279 })?;
280
281 return Ok(StaticFileResponse {
282 resp,
283 file_path: resp_file_path,
284 });
285 }
286
287 if let Some(precompressed_meta) = precompressed_variant {
289 let (precomp_path, precomp_encoding) = precompressed_meta;
290 let mut resp = file_reply(
291 headers_opt,
292 file_path,
293 &metadata,
294 Some(precomp_path),
295 #[cfg(feature = "experimental")]
296 opts.memory_cache,
297 )?;
298
299 resp.headers_mut().remove(CONTENT_LENGTH);
301 let encoding = match HeaderValue::from_str(precomp_encoding.as_str()) {
302 Ok(val) => val,
303 Err(err) => {
304 tracing::error!(
305 "unable to parse header value from content encoding: {:?}",
306 err
307 );
308 return Err(StatusCode::INTERNAL_SERVER_ERROR);
309 }
310 };
311 resp.headers_mut().insert(CONTENT_ENCODING, encoding);
312
313 return Ok(StaticFileResponse {
314 resp,
315 file_path: resp_file_path,
316 });
317 }
318
319 #[cfg(feature = "experimental")]
320 let resp = file_reply(headers_opt, file_path, &metadata, None, opts.memory_cache)?;
321
322 #[cfg(not(feature = "experimental"))]
323 let resp = file_reply(headers_opt, file_path, &metadata, None)?;
324
325 Ok(StaticFileResponse {
326 resp,
327 file_path: resp_file_path,
328 })
329}
330
331fn get_composed_file_metadata<'a>(
335 mut file_path: &'a mut PathBuf,
336 _headers: &'a HeaderMap<HeaderValue>,
337 _compression_static: bool,
338 mut index_files: &'a [&'a str],
339) -> Result<FileMetadata<'a>, StatusCode> {
340 tracing::trace!("getting metadata for file {}", file_path.display());
341
342 match try_metadata(file_path) {
344 Ok((mut metadata, is_dir)) => {
345 if is_dir {
346 if index_files.is_empty() {
348 index_files = DEFAULT_INDEX_FILES;
349 }
350 let mut index_found = false;
351 for index in index_files {
352 tracing::debug!("dir: appending {} to the directory path", index);
354 file_path.push(index);
355
356 #[cfg(any(
358 feature = "compression",
359 feature = "compression-deflate",
360 feature = "compression-gzip",
361 feature = "compression-brotli",
362 feature = "compression-zstd"
363 ))]
364 if _compression_static {
365 if let Some(p) =
366 compression_static::precompressed_variant(file_path, _headers)
367 {
368 return Ok(FileMetadata {
369 file_path,
370 metadata: p.metadata,
371 is_dir: false,
372 precompressed_variant: Some((p.file_path, p.encoding)),
373 });
374 }
375 }
376
377 if let Ok(meta_res) = try_metadata(file_path) {
381 (metadata, _) = meta_res;
382 index_found = true;
383 break;
384 }
385
386 file_path.pop();
388 let new_meta: Option<Metadata>;
389 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
390 if let Some(new_meta) = new_meta {
391 metadata = new_meta;
392 index_found = true;
393 break;
394 }
395 }
396
397 if !index_found && !index_files.is_empty() {
400 file_path.push(index_files.last().unwrap());
401 }
402 } else {
403 #[cfg(any(
405 feature = "compression",
406 feature = "compression-deflate",
407 feature = "compression-gzip",
408 feature = "compression-brotli",
409 feature = "compression-zstd"
410 ))]
411 if _compression_static {
412 if let Some(p) = compression_static::precompressed_variant(file_path, _headers)
413 {
414 return Ok(FileMetadata {
415 file_path,
416 metadata: p.metadata,
417 is_dir: false,
418 precompressed_variant: Some((p.file_path, p.encoding)),
419 });
420 }
421 }
422 }
423
424 Ok(FileMetadata {
425 file_path,
426 metadata,
427 is_dir,
428 precompressed_variant: None,
429 })
430 }
431 Err(err) => {
432 #[cfg(any(
434 feature = "compression",
435 feature = "compression-deflate",
436 feature = "compression-gzip",
437 feature = "compression-brotli",
438 feature = "compression-zstd"
439 ))]
440 if _compression_static {
441 if let Some(p) = compression_static::precompressed_variant(file_path, _headers) {
442 return Ok(FileMetadata {
443 file_path,
444 metadata: p.metadata,
445 is_dir: false,
446 precompressed_variant: Some((p.file_path, p.encoding)),
447 });
448 }
449 }
450
451 let new_meta: Option<Metadata>;
455 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
456
457 #[cfg(any(
458 feature = "compression",
459 feature = "compression-deflate",
460 feature = "compression-gzip",
461 feature = "compression-brotli",
462 feature = "compression-zstd"
463 ))]
464 match new_meta {
465 Some(new_meta) => {
466 return Ok(FileMetadata {
467 file_path,
468 metadata: new_meta,
469 is_dir: false,
470 precompressed_variant: None,
471 });
472 }
473 _ => {
474 if _compression_static {
476 if let Some(p) =
477 compression_static::precompressed_variant(file_path, _headers)
478 {
479 return Ok(FileMetadata {
480 file_path,
481 metadata: p.metadata,
482 is_dir: false,
483 precompressed_variant: Some((p.file_path, p.encoding)),
484 });
485 }
486 }
487 }
488 }
489 #[cfg(not(feature = "compression"))]
490 if let Some(new_meta) = new_meta {
491 return Ok(FileMetadata {
492 file_path,
493 metadata: new_meta,
494 is_dir: false,
495 precompressed_variant: None,
496 });
497 }
498
499 Err(err)
500 }
501 }
502}
503
504fn file_reply<'a>(
511 headers: &'a HeaderMap<HeaderValue>,
512 path: &'a PathBuf,
513 meta: &'a Metadata,
514 path_precompressed: Option<PathBuf>,
515 #[cfg(feature = "experimental")] memory_cache: Option<&'a MemCacheOpts>,
516) -> Result<Response<Body>, StatusCode> {
517 let conditionals = ConditionalHeaders::new(headers);
518 let file_path = path_precompressed.as_ref().unwrap_or(path);
519
520 match File::open(file_path) {
521 Ok(file) => {
522 #[cfg(feature = "experimental")]
523 let resp = response_body(file, path, meta, conditionals, memory_cache);
524
525 #[cfg(not(feature = "experimental"))]
526 let resp = response_body(file, path, meta, conditionals);
527
528 resp
529 }
530 Err(err) => {
531 let status = match err.kind() {
532 io::ErrorKind::NotFound => {
533 tracing::debug!("file can't be opened or not found: {:?}", path.display());
534 StatusCode::NOT_FOUND
535 }
536 io::ErrorKind::PermissionDenied => {
537 tracing::warn!("file permission denied: {:?}", path.display());
538 StatusCode::FORBIDDEN
539 }
540 _ => {
541 tracing::error!("file open error (path={:?}): {} ", path.display(), err);
542 StatusCode::INTERNAL_SERVER_ERROR
543 }
544 };
545 Err(status)
546 }
547 }
548}