1use headers::{AcceptRanges, HeaderMap, HeaderMapExt, HeaderValue};
13use hyper::{header::CONTENT_ENCODING, header::CONTENT_LENGTH, Body, Method, Response, StatusCode};
14use std::fs::{File, Metadata};
15use std::io;
16use std::path::PathBuf;
17
18use crate::conditional_headers::ConditionalHeaders;
19use crate::fs::meta::{try_metadata, try_metadata_with_html_suffix, FileMetadata};
20use crate::fs::path::{sanitize_path, PathExt};
21use crate::http_ext::{MethodExt, HTTP_SUPPORTED_METHODS};
22use crate::response::response_body;
23use crate::Result;
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 archive_reply, DirDownloadFmt, DirDownloadOpts, DOWNLOAD_PARAM_KEY,
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 let FileMetadata {
134 file_path,
135 metadata,
136 is_dir,
137 precompressed_variant,
138 } = get_composed_file_metadata(
139 &mut file_path,
140 headers_opt,
141 opts.compression_static,
142 opts.index_files,
143 opts.disable_symlinks,
144 )?;
145
146 if opts.ignore_hidden_files && file_path.is_hidden() {
148 return Err(StatusCode::NOT_FOUND);
149 }
150
151 let resp_file_path = file_path.to_owned();
152
153 if is_dir && opts.redirect_trailing_slash && !uri_path.ends_with('/') {
156 let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
157 let uri = [uri_path, "/", query.as_str()].concat();
158 let loc = match HeaderValue::from_str(uri.as_str()) {
159 Ok(val) => val,
160 Err(err) => {
161 tracing::error!("invalid header value from current uri: {:?}", err);
162 return Err(StatusCode::INTERNAL_SERVER_ERROR);
163 }
164 };
165
166 let mut resp = Response::new(Body::empty());
167 resp.headers_mut().insert(hyper::header::LOCATION, loc);
168 *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
169
170 tracing::trace!("uri doesn't end with a slash so redirecting permanently");
171 return Ok(StaticFileResponse {
172 resp,
173 file_path: resp_file_path,
174 });
175 }
176
177 if method.is_options() {
179 let mut resp = Response::new(Body::empty());
180 *resp.status_mut() = StatusCode::NO_CONTENT;
181 resp.headers_mut()
182 .typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS.clone()));
183 resp.headers_mut().typed_insert(AcceptRanges::bytes());
184
185 return Ok(StaticFileResponse {
186 resp,
187 file_path: resp_file_path,
188 });
189 }
190
191 #[cfg(feature = "directory-listing")]
196 if is_dir && opts.dir_listing && !file_path.exists() {
197 #[cfg(feature = "directory-listing-download")]
202 if !opts.dir_listing_download.is_empty() {
203 if let Some((_k, _dl_archive_opt)) =
204 form_urlencoded::parse(opts.uri_query.unwrap_or("").as_bytes())
205 .find(|(k, _v)| k == DOWNLOAD_PARAM_KEY)
206 {
207 let mut fp = file_path.clone();
209 fp.pop();
210 if let Some(filename) = fp.file_name() {
211 let resp = archive_reply(
212 filename,
213 &fp,
214 DirDownloadOpts {
215 method,
216 disable_symlinks: opts.disable_symlinks,
217 ignore_hidden_files: opts.ignore_hidden_files,
218 },
219 );
220 return Ok(StaticFileResponse {
221 resp,
222 file_path: resp_file_path,
223 });
224 } else {
225 tracing::error!("Unable to get filename from {}", fp.to_string_lossy());
226 return Err(StatusCode::INTERNAL_SERVER_ERROR);
227 }
228 }
229 }
230
231 let resp = directory_listing::auto_index(DirListOpts {
232 method,
233 current_path: uri_path,
234 uri_query: opts.uri_query,
235 filepath: file_path,
236 dir_listing_order: opts.dir_listing_order,
237 dir_listing_format: opts.dir_listing_format,
238 ignore_hidden_files: opts.ignore_hidden_files,
239 disable_symlinks: opts.disable_symlinks,
240 #[cfg(feature = "directory-listing-download")]
241 dir_listing_download: opts.dir_listing_download,
242 })?;
243
244 return Ok(StaticFileResponse {
245 resp,
246 file_path: resp_file_path,
247 });
248 }
249
250 if let Some(precompressed_meta) = precompressed_variant {
252 let (precomp_path, precomp_encoding) = precompressed_meta;
253 let mut resp = file_reply(
254 headers_opt,
255 file_path,
256 &metadata,
257 Some(precomp_path),
258 #[cfg(feature = "experimental")]
259 opts.memory_cache,
260 )?;
261
262 resp.headers_mut().remove(CONTENT_LENGTH);
264 let encoding = match HeaderValue::from_str(precomp_encoding.as_str()) {
265 Ok(val) => val,
266 Err(err) => {
267 tracing::error!(
268 "unable to parse header value from content encoding: {:?}",
269 err
270 );
271 return Err(StatusCode::INTERNAL_SERVER_ERROR);
272 }
273 };
274 resp.headers_mut().insert(CONTENT_ENCODING, encoding);
275
276 return Ok(StaticFileResponse {
277 resp,
278 file_path: resp_file_path,
279 });
280 }
281
282 #[cfg(feature = "experimental")]
283 let resp = file_reply(headers_opt, file_path, &metadata, None, opts.memory_cache)?;
284
285 #[cfg(not(feature = "experimental"))]
286 let resp = file_reply(headers_opt, file_path, &metadata, None)?;
287
288 Ok(StaticFileResponse {
289 resp,
290 file_path: resp_file_path,
291 })
292}
293
294fn get_composed_file_metadata<'a>(
298 mut file_path: &'a mut PathBuf,
299 _headers: &'a HeaderMap<HeaderValue>,
300 _compression_static: bool,
301 mut index_files: &'a [&'a str],
302 disable_symlinks: bool,
303) -> Result<FileMetadata<'a>, StatusCode> {
304 tracing::trace!("getting metadata for file {}", file_path.display());
305
306 if disable_symlinks && file_path.is_symlink() {
308 tracing::warn!(
309 "file path {} is a symlink, access denied",
310 file_path.display()
311 );
312 return Err(StatusCode::FORBIDDEN);
313 }
314
315 match try_metadata(file_path) {
317 Ok((mut metadata, is_dir)) => {
318 if is_dir {
319 if index_files.is_empty() {
321 index_files = DEFAULT_INDEX_FILES;
322 }
323 let mut index_found = false;
324 for index in index_files {
325 tracing::debug!("dir: appending {} to the directory path", index);
327 file_path.push(index);
328
329 #[cfg(any(
331 feature = "compression",
332 feature = "compression-deflate",
333 feature = "compression-gzip",
334 feature = "compression-brotli",
335 feature = "compression-zstd"
336 ))]
337 if _compression_static {
338 if let Some(p) =
339 compression_static::precompressed_variant(file_path, _headers)
340 {
341 return Ok(FileMetadata {
342 file_path,
343 metadata: p.metadata,
344 is_dir: false,
345 precompressed_variant: Some((p.file_path, p.encoding)),
346 });
347 }
348 }
349
350 if let Ok(meta_res) = try_metadata(file_path) {
354 (metadata, _) = meta_res;
355 index_found = true;
356 break;
357 }
358
359 file_path.pop();
361 let new_meta: Option<Metadata>;
362 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
363 if let Some(new_meta) = new_meta {
364 metadata = new_meta;
365 index_found = true;
366 break;
367 }
368 }
369
370 if !index_found && !index_files.is_empty() {
373 file_path.push(index_files.last().unwrap());
374 }
375 } else {
376 #[cfg(any(
378 feature = "compression",
379 feature = "compression-deflate",
380 feature = "compression-gzip",
381 feature = "compression-brotli",
382 feature = "compression-zstd"
383 ))]
384 if _compression_static {
385 if let Some(p) = compression_static::precompressed_variant(file_path, _headers)
386 {
387 return Ok(FileMetadata {
388 file_path,
389 metadata: p.metadata,
390 is_dir: false,
391 precompressed_variant: Some((p.file_path, p.encoding)),
392 });
393 }
394 }
395 }
396
397 Ok(FileMetadata {
398 file_path,
399 metadata,
400 is_dir,
401 precompressed_variant: None,
402 })
403 }
404 Err(err) => {
405 #[cfg(any(
407 feature = "compression",
408 feature = "compression-deflate",
409 feature = "compression-gzip",
410 feature = "compression-brotli",
411 feature = "compression-zstd"
412 ))]
413 if _compression_static {
414 if let Some(p) = compression_static::precompressed_variant(file_path, _headers) {
415 return Ok(FileMetadata {
416 file_path,
417 metadata: p.metadata,
418 is_dir: false,
419 precompressed_variant: Some((p.file_path, p.encoding)),
420 });
421 }
422 }
423
424 let new_meta: Option<Metadata>;
428 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
429
430 #[cfg(any(
431 feature = "compression",
432 feature = "compression-deflate",
433 feature = "compression-gzip",
434 feature = "compression-brotli",
435 feature = "compression-zstd"
436 ))]
437 match new_meta {
438 Some(new_meta) => {
439 return Ok(FileMetadata {
440 file_path,
441 metadata: new_meta,
442 is_dir: false,
443 precompressed_variant: None,
444 })
445 }
446 _ => {
447 if _compression_static {
449 if let Some(p) =
450 compression_static::precompressed_variant(file_path, _headers)
451 {
452 return Ok(FileMetadata {
453 file_path,
454 metadata: p.metadata,
455 is_dir: false,
456 precompressed_variant: Some((p.file_path, p.encoding)),
457 });
458 }
459 }
460 }
461 }
462 #[cfg(not(feature = "compression"))]
463 if let Some(new_meta) = new_meta {
464 return Ok(FileMetadata {
465 file_path,
466 metadata: new_meta,
467 is_dir: false,
468 precompressed_variant: None,
469 });
470 }
471
472 Err(err)
473 }
474 }
475}
476
477fn file_reply<'a>(
484 headers: &'a HeaderMap<HeaderValue>,
485 path: &'a PathBuf,
486 meta: &'a Metadata,
487 path_precompressed: Option<PathBuf>,
488 #[cfg(feature = "experimental")] memory_cache: Option<&'a MemCacheOpts>,
489) -> Result<Response<Body>, StatusCode> {
490 let conditionals = ConditionalHeaders::new(headers);
491 let file_path = path_precompressed.as_ref().unwrap_or(path);
492
493 match File::open(file_path) {
494 Ok(file) => {
495 #[cfg(feature = "experimental")]
496 let resp = response_body(file, path, meta, conditionals, memory_cache);
497
498 #[cfg(not(feature = "experimental"))]
499 let resp = response_body(file, path, meta, conditionals);
500
501 resp
502 }
503 Err(err) => {
504 let status = match err.kind() {
505 io::ErrorKind::NotFound => {
506 tracing::debug!("file can't be opened or not found: {:?}", path.display());
507 StatusCode::NOT_FOUND
508 }
509 io::ErrorKind::PermissionDenied => {
510 tracing::warn!("file permission denied: {:?}", path.display());
511 StatusCode::FORBIDDEN
512 }
513 _ => {
514 tracing::error!("file open error (path={:?}): {} ", path.display(), err);
515 StatusCode::INTERNAL_SERVER_ERROR
516 }
517 };
518 Err(status)
519 }
520 }
521}