use headers::{AcceptRanges, HeaderMapExt, HeaderValue};
use hyper::{
Response, StatusCode,
header::{CONTENT_ENCODING, CONTENT_LENGTH},
};
use std::fs::{File, Metadata};
use std::io;
use std::path::{Path, PathBuf};
use crate::body::Body;
use crate::conditional_headers::ConditionalHeaders;
use crate::exts::headers::ContentCoding;
use crate::exts::http::HTTP_SUPPORTED_METHODS;
use crate::response::response_body;
use super::opts::HandleOpts;
pub(super) fn trailing_slash_redirect(
is_dir: bool,
opts: &HandleOpts<'_>,
) -> Result<Option<Response<Body>>, StatusCode> {
if !(is_dir && opts.redirect_trailing_slash && !opts.uri_path.ends_with('/')) {
return Ok(None);
}
let query = opts.uri_query.map_or(String::new(), |s| ["?", s].concat());
let uri = [opts.uri_path, "/", query.as_str()].concat();
let loc = HeaderValue::from_str(uri.as_str()).map_err(|err| {
tracing::error!("invalid header value from current uri: {:?}", err);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let mut resp = Response::new(crate::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");
Ok(Some(resp))
}
pub(super) fn options_reply() -> Response<Body> {
let mut resp = Response::new(crate::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());
resp
}
pub(super) fn file_or_precompressed(
opts: &HandleOpts<'_>,
file_path: &Path,
metadata: &Metadata,
precompressed_variant: Option<(PathBuf, ContentCoding)>,
pre_opened: Option<File>,
) -> Result<Response<Body>, StatusCode> {
if let Some((precomp_path, precomp_encoding)) = precompressed_variant {
drop(pre_opened);
return precompressed_reply(opts, file_path, metadata, precomp_path, precomp_encoding);
}
file_reply(opts, file_path, metadata, None, pre_opened)
}
fn precompressed_reply(
opts: &HandleOpts<'_>,
file_path: &Path,
metadata: &Metadata,
precomp_path: PathBuf,
precomp_encoding: ContentCoding,
) -> Result<Response<Body>, StatusCode> {
let mut resp = file_reply(opts, file_path, metadata, Some(precomp_path), None)?;
resp.headers_mut().remove(CONTENT_LENGTH);
let encoding = HeaderValue::from_str(precomp_encoding.as_str()).map_err(|err| {
tracing::error!(
"unable to parse header value from content encoding: {:?}",
err
);
StatusCode::INTERNAL_SERVER_ERROR
})?;
resp.headers_mut().insert(CONTENT_ENCODING, encoding);
Ok(resp)
}
fn file_reply(
opts: &HandleOpts<'_>,
path: &Path,
meta: &Metadata,
path_precompressed: Option<PathBuf>,
pre_opened: Option<File>,
) -> Result<Response<Body>, StatusCode> {
let conditionals = ConditionalHeaders::new(opts.headers);
let file_result = match (path_precompressed.as_deref(), pre_opened) {
(None, Some(file)) => Ok(file),
(Some(precomp_path), _) => File::open(precomp_path),
(None, None) => File::open(path),
};
let open_path: &Path = path_precompressed.as_deref().unwrap_or(path);
match file_result {
Ok(file) => {
#[cfg(feature = "mem-cache")]
{
let _ = open_path;
response_body(file, path, meta, conditionals, opts.etag, opts.memory_cache)
}
#[cfg(not(feature = "mem-cache"))]
{
let _ = open_path;
response_body(file, path, meta, conditionals, opts.etag)
}
}
Err(err) => Err(open_error_to_status(err, path)),
}
}
fn open_error_to_status(err: io::Error, path: &Path) -> StatusCode {
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
}
}
}