use std::{
io::{Error as IoError, ErrorKind as IoErrorKind},
ops::BitAnd,
path::PathBuf,
sync::Arc,
time::SystemTime,
};
use http::{header, HeaderValue, Method, Request};
use mime_guess::MimeGuess;
use tokio::fs::File;
use crate::{
util::RequestedPath,
vfs::{FileOpener, FileWithMetadata, TokioFileOpener},
};
#[derive(Debug)]
pub struct ResolvedFile<F = File> {
pub handle: F,
pub path: PathBuf,
pub size: u64,
pub modified: Option<SystemTime>,
pub content_type: Option<String>,
pub encoding: Option<Encoding>,
}
impl<F> ResolvedFile<F> {
fn new(
file: FileWithMetadata<F>,
path: PathBuf,
content_type: Option<String>,
encoding: Option<Encoding>,
) -> Self {
Self {
handle: file.handle,
path,
size: file.size,
modified: file.modified,
content_type,
encoding,
}
}
}
pub struct Resolver<O = TokioFileOpener> {
pub opener: Arc<O>,
pub allowed_encodings: AcceptEncoding,
}
#[derive(Debug)]
pub enum ResolveResult<F = File> {
MethodNotMatched,
NotFound,
PermissionDenied,
IsDirectory {
redirect_to: String,
},
Found(ResolvedFile<F>),
}
fn map_open_err<F>(err: IoError) -> Result<ResolveResult<F>, IoError> {
match err.kind() {
IoErrorKind::NotFound => Ok(ResolveResult::NotFound),
IoErrorKind::PermissionDenied => Ok(ResolveResult::PermissionDenied),
_ => Err(err),
}
}
impl Resolver<TokioFileOpener> {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self::with_opener(TokioFileOpener::new(root))
}
}
impl<O: FileOpener> Resolver<O> {
pub fn with_opener(opener: O) -> Self {
Self {
opener: Arc::new(opener),
allowed_encodings: AcceptEncoding::none(),
}
}
pub async fn resolve_request<B>(
&self,
req: &Request<B>,
) -> Result<ResolveResult<O::File>, IoError> {
match *req.method() {
Method::HEAD | Method::GET => {}
_ => {
return Ok(ResolveResult::MethodNotMatched);
}
}
let accept_encoding = self.allowed_encodings
& req
.headers()
.get(header::ACCEPT_ENCODING)
.map(AcceptEncoding::from_header_value)
.unwrap_or(AcceptEncoding::none());
self.resolve_path(req.uri().path(), accept_encoding).await
}
pub async fn resolve_path(
&self,
request_path: &str,
accept_encoding: AcceptEncoding,
) -> Result<ResolveResult<O::File>, IoError> {
let RequestedPath {
sanitized: mut path,
is_dir_request,
} = RequestedPath::resolve(request_path);
let file = match self.opener.open(&path).await {
Ok(pair) => pair,
Err(err) => return map_open_err(err),
};
if is_dir_request && !file.is_dir {
return Ok(ResolveResult::NotFound);
}
if !is_dir_request && file.is_dir {
let mut target = String::with_capacity(path.as_os_str().len() + 2);
target.push('/');
for component in path.components() {
target.push_str(&component.as_os_str().to_string_lossy());
target.push('/');
}
return Ok(ResolveResult::IsDirectory {
redirect_to: target,
});
}
if !is_dir_request {
return self.resolve_final(file, path, accept_encoding).await;
}
path.push("index.html");
let file = match self.opener.open(&path).await {
Ok(pair) => pair,
Err(err) => return map_open_err(err),
};
if file.is_dir {
return Ok(ResolveResult::NotFound);
}
self.resolve_final(file, path, accept_encoding).await
}
async fn resolve_final(
&self,
file: FileWithMetadata<O::File>,
path: PathBuf,
accept_encoding: AcceptEncoding,
) -> Result<ResolveResult<O::File>, IoError> {
let mime = MimeGuess::from_path(&path)
.first()
.map(|mime| mime.to_string());
if accept_encoding.br {
let mut br_path = path.clone().into_os_string();
br_path.push(".br");
if let Ok(file) = self.opener.open(br_path.as_ref()).await {
return Ok(ResolveResult::Found(ResolvedFile::new(
file,
br_path.into(),
mime,
Some(Encoding::Br),
)));
}
}
if accept_encoding.gzip {
let mut gzip_path = path.clone().into_os_string();
gzip_path.push(".gz");
if let Ok(file) = self.opener.open(gzip_path.as_ref()).await {
return Ok(ResolveResult::Found(ResolvedFile::new(
file,
gzip_path.into(),
mime,
Some(Encoding::Gzip),
)));
}
}
Ok(ResolveResult::Found(ResolvedFile::new(
file, path, mime, None,
)))
}
}
impl<O> Clone for Resolver<O> {
fn clone(&self) -> Self {
Self {
opener: self.opener.clone(),
allowed_encodings: self.allowed_encodings,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Encoding {
Gzip,
Br,
}
impl Encoding {
pub fn to_header_value(&self) -> HeaderValue {
HeaderValue::from_static(match self {
Encoding::Gzip => "gzip",
Encoding::Br => "br",
})
}
}
#[derive(Debug, Copy, Clone)]
pub struct AcceptEncoding {
pub gzip: bool,
pub br: bool,
}
impl AcceptEncoding {
pub const fn all() -> Self {
Self {
gzip: true,
br: true,
}
}
pub const fn none() -> Self {
Self {
gzip: false,
br: false,
}
}
pub fn from_header_value(value: &HeaderValue) -> Self {
let mut res = Self::none();
if let Ok(value) = value.to_str() {
for enc in value.split(',') {
match enc.split(';').next().unwrap().trim() {
"gzip" => res.gzip = true,
"br" => res.br = true,
_ => {}
}
}
}
res
}
}
impl BitAnd for AcceptEncoding {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self {
gzip: self.gzip && rhs.gzip,
br: self.br && rhs.br,
}
}
}