use headers::{AcceptRanges, HeaderMap, HeaderMapExt, HeaderValue};
use hyper::{Body, Method, Response, StatusCode, header::CONTENT_ENCODING, header::CONTENT_LENGTH};
use std::fs::{File, Metadata};
use std::io;
use std::path::PathBuf;
use crate::Result;
use crate::conditional_headers::ConditionalHeaders;
use crate::fs::meta::{FileMetadata, try_metadata, try_metadata_with_html_suffix};
use crate::fs::path::{PathExt, sanitize_path};
use crate::http_ext::{HTTP_SUPPORTED_METHODS, MethodExt};
use crate::response::response_body;
#[cfg(feature = "experimental")]
use crate::mem_cache::{cache, cache::MemCacheOpts};
use crate::compression_static;
#[cfg(feature = "directory-listing")]
use crate::{
directory_listing,
directory_listing::{DirListFmt, DirListOpts},
};
#[cfg(feature = "directory-listing-download")]
use crate::directory_listing_download::{
DOWNLOAD_PARAM_KEY, DirDownloadFmt, DirDownloadOpts, archive_reply,
};
const DEFAULT_INDEX_FILES: &[&str; 1] = &["index.html"];
pub struct HandleOpts<'a> {
pub method: &'a Method,
#[cfg(feature = "experimental")]
pub memory_cache: Option<&'a MemCacheOpts>,
pub headers: &'a HeaderMap<HeaderValue>,
pub base_path: &'a PathBuf,
pub uri_path: &'a str,
pub index_files: &'a [&'a str],
pub uri_query: Option<&'a str>,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub dir_listing: bool,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub dir_listing_order: u8,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub dir_listing_format: &'a DirListFmt,
#[cfg(feature = "directory-listing-download")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
pub dir_listing_download: &'a [DirDownloadFmt],
pub redirect_trailing_slash: bool,
pub compression_static: bool,
pub ignore_hidden_files: bool,
pub disable_symlinks: bool,
}
pub struct StaticFileResponse {
pub resp: Response<Body>,
pub file_path: PathBuf,
}
pub async fn handle(opts: &HandleOpts<'_>) -> Result<StaticFileResponse, StatusCode> {
let method = opts.method;
if !method.is_allowed() {
return Err(StatusCode::METHOD_NOT_ALLOWED);
}
let uri_path = opts.uri_path;
let mut file_path = sanitize_path(opts.base_path, uri_path)?;
let headers_opt = opts.headers;
#[cfg(feature = "experimental")]
if opts.memory_cache.is_some() {
if opts.redirect_trailing_slash && uri_path.ends_with('/') {
file_path.push("index.html");
}
if let Some(result) = cache::get_or_acquire(file_path.as_path(), headers_opt).await {
return Ok(StaticFileResponse {
resp: result?,
file_path,
});
}
}
let FileMetadata {
file_path,
metadata,
is_dir,
precompressed_variant,
} = get_composed_file_metadata(
&mut file_path,
headers_opt,
opts.compression_static,
opts.index_files,
)?;
let mut file_path_temp = file_path.clone();
if is_dir {
file_path_temp.pop();
}
let file_path_relative = file_path_temp.strip_prefix(opts.base_path).map_err(|err| {
tracing::error!(
"unable to strip prefix from file path '{}': {}",
file_path.display(),
err,
);
StatusCode::NOT_FOUND
})?;
let file_path_resolved = file_path_temp.canonicalize().map_err(|err| {
tracing::error!(
"unable to resolve '{}' symlink path: {}",
file_path_temp.display(),
err,
);
StatusCode::NOT_FOUND
})?;
let base_path = opts.base_path.canonicalize().map_err(|err| {
tracing::error!(
"unable to resolve '{}' base path: {}",
opts.base_path.display(),
err,
);
StatusCode::NOT_FOUND
})?;
if !file_path_resolved.starts_with(base_path) {
tracing::error!(
"file path '{}' resolves outside of the base path, access denied",
file_path_resolved.display()
);
return Err(StatusCode::NOT_FOUND);
}
if opts.disable_symlinks {
let has_symlink = file_path_relative
.contains_symlink(opts.base_path)
.map_err(|err| {
tracing::error!(
"unable to check if file path '{}' contains symlink: {}",
file_path_relative.display(),
err,
);
StatusCode::NOT_FOUND
})?;
if has_symlink {
tracing::warn!(
"file path '{}' contains a symlink, access denied",
file_path.display()
);
return Err(StatusCode::FORBIDDEN);
}
}
if opts.ignore_hidden_files && file_path_relative.is_hidden() {
tracing::trace!(
"considering hidden file {} as not found",
file_path.display()
);
return Err(StatusCode::NOT_FOUND);
}
let resp_file_path = file_path.to_owned();
if is_dir && opts.redirect_trailing_slash && !uri_path.ends_with('/') {
let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
let uri = [uri_path, "/", query.as_str()].concat();
let loc = match HeaderValue::from_str(uri.as_str()) {
Ok(val) => val,
Err(err) => {
tracing::error!("invalid header value from current uri: {:?}", err);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let mut resp = Response::new(Body::empty());
resp.headers_mut().insert(hyper::header::LOCATION, loc);
*resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
tracing::trace!("uri doesn't end with a slash so redirecting permanently");
return Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
});
}
if method.is_options() {
let mut resp = Response::new(Body::empty());
*resp.status_mut() = StatusCode::NO_CONTENT;
resp.headers_mut()
.typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS.clone()));
resp.headers_mut().typed_insert(AcceptRanges::bytes());
return Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
});
}
#[cfg(feature = "directory-listing")]
if is_dir && opts.dir_listing && !file_path.exists() {
#[cfg(feature = "directory-listing-download")]
if !opts.dir_listing_download.is_empty()
&& let Some((_k, _dl_archive_opt)) =
form_urlencoded::parse(opts.uri_query.unwrap_or("").as_bytes())
.find(|(k, _v)| k == DOWNLOAD_PARAM_KEY)
{
let mut fp = file_path.clone();
fp.pop();
if let Some(filename) = fp.file_name() {
let resp = archive_reply(
filename,
&fp,
DirDownloadOpts {
method,
disable_symlinks: opts.disable_symlinks,
ignore_hidden_files: opts.ignore_hidden_files,
},
);
return Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
});
} else {
tracing::error!("Unable to get filename from {}", fp.to_string_lossy());
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
let resp = directory_listing::auto_index(DirListOpts {
root_path: opts.base_path.as_path(),
method,
current_path: uri_path,
uri_query: opts.uri_query,
filepath: file_path,
dir_listing_order: opts.dir_listing_order,
dir_listing_format: opts.dir_listing_format,
ignore_hidden_files: opts.ignore_hidden_files,
disable_symlinks: opts.disable_symlinks,
#[cfg(feature = "directory-listing-download")]
dir_listing_download: opts.dir_listing_download,
})?;
return Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
});
}
if let Some(precompressed_meta) = precompressed_variant {
let (precomp_path, precomp_encoding) = precompressed_meta;
let mut resp = file_reply(
headers_opt,
file_path,
&metadata,
Some(precomp_path),
#[cfg(feature = "experimental")]
opts.memory_cache,
)?;
resp.headers_mut().remove(CONTENT_LENGTH);
let encoding = match HeaderValue::from_str(precomp_encoding.as_str()) {
Ok(val) => val,
Err(err) => {
tracing::error!(
"unable to parse header value from content encoding: {:?}",
err
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
resp.headers_mut().insert(CONTENT_ENCODING, encoding);
return Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
});
}
#[cfg(feature = "experimental")]
let resp = file_reply(headers_opt, file_path, &metadata, None, opts.memory_cache)?;
#[cfg(not(feature = "experimental"))]
let resp = file_reply(headers_opt, file_path, &metadata, None)?;
Ok(StaticFileResponse {
resp,
file_path: resp_file_path,
})
}
fn get_composed_file_metadata<'a>(
mut file_path: &'a mut PathBuf,
headers: &'a HeaderMap<HeaderValue>,
compression_static: bool,
mut index_files: &'a [&'a str],
) -> Result<FileMetadata<'a>, StatusCode> {
tracing::trace!("getting metadata for file {}", file_path.display());
match try_metadata(file_path) {
Ok((mut metadata, is_dir)) => {
if is_dir {
if index_files.is_empty() {
index_files = DEFAULT_INDEX_FILES;
}
let mut index_found = false;
for index in index_files {
tracing::debug!("dir: appending {} to the directory path", index);
file_path.push(index);
if compression_static
&& let Some(p) =
compression_static::precompressed_variant(file_path, headers)
{
return Ok(FileMetadata {
file_path,
metadata: p.metadata,
is_dir: false,
precompressed_variant: Some((p.file_path, p.encoding)),
});
}
if let Ok(meta_res) = try_metadata(file_path) {
(metadata, _) = meta_res;
index_found = true;
break;
}
file_path.pop();
let new_meta: Option<Metadata>;
(file_path, new_meta) = try_metadata_with_html_suffix(file_path);
if let Some(new_meta) = new_meta {
metadata = new_meta;
index_found = true;
break;
}
}
if !index_found && !index_files.is_empty() {
file_path.push(index_files.last().unwrap());
}
}
let precompressed_variant = compression_static
.then(|| compression_static::precompressed_variant(file_path, headers))
.flatten()
.map(|p| (p.file_path, p.encoding));
Ok(FileMetadata {
file_path,
metadata,
is_dir,
precompressed_variant,
})
}
Err(err) => {
if compression_static
&& let Some(p) = compression_static::precompressed_variant(file_path, headers)
{
return Ok(FileMetadata {
file_path,
metadata: p.metadata,
is_dir: false,
precompressed_variant: Some((p.file_path, p.encoding)),
});
}
let new_meta: Option<Metadata>;
(file_path, new_meta) = try_metadata_with_html_suffix(file_path);
#[cfg(any(
feature = "compression",
feature = "compression-deflate",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd"
))]
match new_meta {
Some(new_meta) => {
return Ok(FileMetadata {
file_path,
metadata: new_meta,
is_dir: false,
precompressed_variant: None,
});
}
_ => {
if compression_static
&& let Some(p) =
compression_static::precompressed_variant(file_path, headers)
{
return Ok(FileMetadata {
file_path,
metadata: p.metadata,
is_dir: false,
precompressed_variant: Some((p.file_path, p.encoding)),
});
}
}
}
#[cfg(not(feature = "compression"))]
if let Some(new_meta) = new_meta {
return Ok(FileMetadata {
file_path,
metadata: new_meta,
is_dir: false,
precompressed_variant: None,
});
}
Err(err)
}
}
}
fn file_reply<'a>(
headers: &'a HeaderMap<HeaderValue>,
path: &'a PathBuf,
meta: &'a Metadata,
path_precompressed: Option<PathBuf>,
#[cfg(feature = "experimental")] memory_cache: Option<&'a MemCacheOpts>,
) -> Result<Response<Body>, StatusCode> {
let conditionals = ConditionalHeaders::new(headers);
let file_path = path_precompressed.as_ref().unwrap_or(path);
match File::open(file_path) {
Ok(file) => {
#[cfg(feature = "experimental")]
let resp = response_body(file, path, meta, conditionals, memory_cache);
#[cfg(not(feature = "experimental"))]
let resp = response_body(file, path, meta, conditionals);
resp
}
Err(err) => {
let status = match err.kind() {
io::ErrorKind::NotFound => {
tracing::debug!("file can't be opened or not found: {:?}", path.display());
StatusCode::NOT_FOUND
}
io::ErrorKind::PermissionDenied => {
tracing::warn!("file permission denied: {:?}", path.display());
StatusCode::FORBIDDEN
}
_ => {
tracing::error!("file open error (path={:?}): {} ", path.display(), err);
StatusCode::INTERNAL_SERVER_ERROR
}
};
Err(status)
}
}
}