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