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
43const DEFAULT_INDEX_FILES: &[&str; 1] = &["index.html"];
44
45pub struct HandleOpts<'a> {
47 pub method: &'a Method,
49 #[cfg(feature = "experimental")]
51 pub memory_cache: Option<&'a MemCacheOpts>,
52 pub headers: &'a HeaderMap<HeaderValue>,
54 pub base_path: &'a PathBuf,
56 pub uri_path: &'a str,
58 pub index_files: &'a [&'a str],
60 pub uri_query: Option<&'a str>,
62 #[cfg(feature = "directory-listing")]
64 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
65 pub dir_listing: bool,
66 #[cfg(feature = "directory-listing")]
68 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
69 pub dir_listing_order: u8,
70 #[cfg(feature = "directory-listing")]
72 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
73 pub dir_listing_format: &'a DirListFmt,
74 pub redirect_trailing_slash: bool,
76 pub compression_static: bool,
78 pub ignore_hidden_files: bool,
80 pub disable_symlinks: bool,
82}
83
84pub struct StaticFileResponse {
86 pub resp: Response<Body>,
88 pub file_path: PathBuf,
90}
91
92pub async fn handle(opts: &HandleOpts<'_>) -> Result<StaticFileResponse, StatusCode> {
95 let method = opts.method;
96 let uri_path = opts.uri_path;
97
98 if !method.is_allowed() {
100 return Err(StatusCode::METHOD_NOT_ALLOWED);
101 }
102
103 let headers_opt = opts.headers;
104 let mut file_path = sanitize_path(opts.base_path, uri_path)?;
105
106 #[cfg(feature = "experimental")]
108 if opts.memory_cache.is_some() {
109 if opts.redirect_trailing_slash && uri_path.ends_with('/') {
112 file_path.push("index.html");
113 }
114
115 if let Some(result) = cache::get_or_acquire(file_path.as_path(), headers_opt).await {
116 return Ok(StaticFileResponse {
117 resp: result?,
118 file_path,
120 });
121 }
122 }
123
124 let FileMetadata {
125 file_path,
126 metadata,
127 is_dir,
128 precompressed_variant,
129 } = get_composed_file_metadata(
130 &mut file_path,
131 headers_opt,
132 opts.compression_static,
133 opts.index_files,
134 opts.disable_symlinks,
135 )?;
136
137 if opts.ignore_hidden_files && file_path.is_hidden() {
139 return Err(StatusCode::NOT_FOUND);
140 }
141
142 let resp_file_path = file_path.to_owned();
143
144 if is_dir && opts.redirect_trailing_slash && !uri_path.ends_with('/') {
147 let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
148 let uri = [uri_path, "/", query.as_str()].concat();
149 let loc = match HeaderValue::from_str(uri.as_str()) {
150 Ok(val) => val,
151 Err(err) => {
152 tracing::error!("invalid header value from current uri: {:?}", err);
153 return Err(StatusCode::INTERNAL_SERVER_ERROR);
154 }
155 };
156
157 let mut resp = Response::new(Body::empty());
158 resp.headers_mut().insert(hyper::header::LOCATION, loc);
159 *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
160
161 tracing::trace!("uri doesn't end with a slash so redirecting permanently");
162 return Ok(StaticFileResponse {
163 resp,
164 file_path: resp_file_path,
165 });
166 }
167
168 if method.is_options() {
170 let mut resp = Response::new(Body::empty());
171 *resp.status_mut() = StatusCode::NO_CONTENT;
172 resp.headers_mut()
173 .typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS.clone()));
174 resp.headers_mut().typed_insert(AcceptRanges::bytes());
175
176 return Ok(StaticFileResponse {
177 resp,
178 file_path: resp_file_path,
179 });
180 }
181
182 #[cfg(feature = "directory-listing")]
187 if is_dir && opts.dir_listing && !file_path.exists() {
188 let resp = directory_listing::auto_index(DirListOpts {
189 method,
190 current_path: uri_path,
191 uri_query: opts.uri_query,
192 filepath: file_path,
193 dir_listing_order: opts.dir_listing_order,
194 dir_listing_format: opts.dir_listing_format,
195 ignore_hidden_files: opts.ignore_hidden_files,
196 disable_symlinks: opts.disable_symlinks,
197 })?;
198
199 return Ok(StaticFileResponse {
200 resp,
201 file_path: resp_file_path,
202 });
203 }
204
205 if let Some(precompressed_meta) = precompressed_variant {
207 let (precomp_path, precomp_encoding) = precompressed_meta;
208 let mut resp = file_reply(
209 headers_opt,
210 file_path,
211 &metadata,
212 Some(precomp_path),
213 #[cfg(feature = "experimental")]
214 opts.memory_cache,
215 )?;
216
217 resp.headers_mut().remove(CONTENT_LENGTH);
219 let encoding = match HeaderValue::from_str(precomp_encoding.as_str()) {
220 Ok(val) => val,
221 Err(err) => {
222 tracing::error!(
223 "unable to parse header value from content encoding: {:?}",
224 err
225 );
226 return Err(StatusCode::INTERNAL_SERVER_ERROR);
227 }
228 };
229 resp.headers_mut().insert(CONTENT_ENCODING, encoding);
230
231 return Ok(StaticFileResponse {
232 resp,
233 file_path: resp_file_path,
234 });
235 }
236
237 #[cfg(feature = "experimental")]
238 let resp = file_reply(headers_opt, file_path, &metadata, None, opts.memory_cache)?;
239
240 #[cfg(not(feature = "experimental"))]
241 let resp = file_reply(headers_opt, file_path, &metadata, None)?;
242
243 Ok(StaticFileResponse {
244 resp,
245 file_path: resp_file_path,
246 })
247}
248
249fn get_composed_file_metadata<'a>(
253 mut file_path: &'a mut PathBuf,
254 _headers: &'a HeaderMap<HeaderValue>,
255 _compression_static: bool,
256 mut index_files: &'a [&'a str],
257 disable_symlinks: bool,
258) -> Result<FileMetadata<'a>, StatusCode> {
259 tracing::trace!("getting metadata for file {}", file_path.display());
260
261 if disable_symlinks && file_path.is_symlink() {
263 tracing::warn!(
264 "file path {} is a symlink, access denied",
265 file_path.display()
266 );
267 return Err(StatusCode::FORBIDDEN);
268 }
269
270 match try_metadata(file_path) {
272 Ok((mut metadata, is_dir)) => {
273 if is_dir {
274 if index_files.is_empty() {
276 index_files = DEFAULT_INDEX_FILES;
277 }
278 let mut index_found = false;
279 for index in index_files {
280 tracing::debug!("dir: appending {} to the directory path", index);
282 file_path.push(index);
283
284 #[cfg(any(
286 feature = "compression",
287 feature = "compression-deflate",
288 feature = "compression-gzip",
289 feature = "compression-brotli",
290 feature = "compression-zstd"
291 ))]
292 if _compression_static {
293 if let Some(p) =
294 compression_static::precompressed_variant(file_path, _headers)
295 {
296 return Ok(FileMetadata {
297 file_path,
298 metadata: p.metadata,
299 is_dir: false,
300 precompressed_variant: Some((p.file_path, p.encoding)),
301 });
302 }
303 }
304
305 if let Ok(meta_res) = try_metadata(file_path) {
309 (metadata, _) = meta_res;
310 index_found = true;
311 break;
312 }
313
314 file_path.pop();
316 let new_meta: Option<Metadata>;
317 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
318 if let Some(new_meta) = new_meta {
319 metadata = new_meta;
320 index_found = true;
321 break;
322 }
323 }
324
325 if !index_found && !index_files.is_empty() {
328 file_path.push(index_files.last().unwrap());
329 }
330 } else {
331 #[cfg(any(
333 feature = "compression",
334 feature = "compression-deflate",
335 feature = "compression-gzip",
336 feature = "compression-brotli",
337 feature = "compression-zstd"
338 ))]
339 if _compression_static {
340 if let Some(p) = compression_static::precompressed_variant(file_path, _headers)
341 {
342 return Ok(FileMetadata {
343 file_path,
344 metadata: p.metadata,
345 is_dir: false,
346 precompressed_variant: Some((p.file_path, p.encoding)),
347 });
348 }
349 }
350 }
351
352 Ok(FileMetadata {
353 file_path,
354 metadata,
355 is_dir,
356 precompressed_variant: None,
357 })
358 }
359 Err(err) => {
360 #[cfg(any(
362 feature = "compression",
363 feature = "compression-deflate",
364 feature = "compression-gzip",
365 feature = "compression-brotli",
366 feature = "compression-zstd"
367 ))]
368 if _compression_static {
369 if let Some(p) = compression_static::precompressed_variant(file_path, _headers) {
370 return Ok(FileMetadata {
371 file_path,
372 metadata: p.metadata,
373 is_dir: false,
374 precompressed_variant: Some((p.file_path, p.encoding)),
375 });
376 }
377 }
378
379 let new_meta: Option<Metadata>;
383 (file_path, new_meta) = try_metadata_with_html_suffix(file_path);
384
385 #[cfg(any(
386 feature = "compression",
387 feature = "compression-deflate",
388 feature = "compression-gzip",
389 feature = "compression-brotli",
390 feature = "compression-zstd"
391 ))]
392 match new_meta {
393 Some(new_meta) => {
394 return Ok(FileMetadata {
395 file_path,
396 metadata: new_meta,
397 is_dir: false,
398 precompressed_variant: None,
399 })
400 }
401 _ => {
402 if _compression_static {
404 if let Some(p) =
405 compression_static::precompressed_variant(file_path, _headers)
406 {
407 return Ok(FileMetadata {
408 file_path,
409 metadata: p.metadata,
410 is_dir: false,
411 precompressed_variant: Some((p.file_path, p.encoding)),
412 });
413 }
414 }
415 }
416 }
417 #[cfg(not(feature = "compression"))]
418 if let Some(new_meta) = new_meta {
419 return Ok(FileMetadata {
420 file_path,
421 metadata: new_meta,
422 is_dir: false,
423 precompressed_variant: None,
424 });
425 }
426
427 Err(err)
428 }
429 }
430}
431
432fn file_reply<'a>(
439 headers: &'a HeaderMap<HeaderValue>,
440 path: &'a PathBuf,
441 meta: &'a Metadata,
442 path_precompressed: Option<PathBuf>,
443 #[cfg(feature = "experimental")] memory_cache: Option<&'a MemCacheOpts>,
444) -> Result<Response<Body>, StatusCode> {
445 let conditionals = ConditionalHeaders::new(headers);
446 let file_path = path_precompressed.as_ref().unwrap_or(path);
447
448 match File::open(file_path) {
449 Ok(file) => {
450 #[cfg(feature = "experimental")]
451 let resp = response_body(file, path, meta, conditionals, memory_cache);
452
453 #[cfg(not(feature = "experimental"))]
454 let resp = response_body(file, path, meta, conditionals);
455
456 resp
457 }
458 Err(err) => {
459 let status = match err.kind() {
460 io::ErrorKind::NotFound => {
461 tracing::debug!("file can't be opened or not found: {:?}", path.display());
462 StatusCode::NOT_FOUND
463 }
464 io::ErrorKind::PermissionDenied => {
465 tracing::warn!("file permission denied: {:?}", path.display());
466 StatusCode::FORBIDDEN
467 }
468 _ => {
469 tracing::error!("file open error (path={:?}): {} ", path.display(), err);
470 StatusCode::INTERNAL_SERVER_ERROR
471 }
472 };
473 Err(status)
474 }
475 }
476}