use super::Reply;
use crate::head_ext::HeaderMapExt;
use crate::server::handler::Handler;
use crate::server::limit::ContentLengthRead;
use crate::server::peek::Peekable;
use crate::server::{ResponseBuilderExt, ServerRequestExt};
use crate::AsyncReadSeek;
use crate::AsyncRuntime;
use crate::Body;
use crate::Error;
use futures_util::io::AsyncSeekExt;
use http::Request;
use http::StatusCode;
use httpdate::{fmt_http_date, parse_http_date};
use std::future::Future;
use std::io;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::time::SystemTime;
#[derive(Debug)]
pub struct Static {
root: PathBuf,
use_path_param: bool,
index_file: Option<String>,
}
impl Static {
pub fn dir(path: impl AsRef<Path>) -> Self {
Static::new(path, true)
}
pub fn file(path: impl AsRef<Path>) -> Self {
Static::new(path, false)
}
pub async fn send_file(
req: &http::Request<Body>,
path: impl AsRef<Path>,
) -> Result<http::Response<Body>, Error> {
let st = Static::new("", false);
st.handle(req, Some(path.as_ref())).await
}
fn new(path: impl AsRef<Path>, use_path_param: bool) -> Self {
let path = path.as_ref();
let root = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().unwrap().join(path)
};
let index_file = Some(
if cfg!(target_os = "windows") {
"index.htm"
} else {
"index.html"
}
.to_string(),
);
Static {
root,
use_path_param,
index_file,
}
}
pub fn index_file(mut self, file: Option<&str>) -> Self {
self.index_file = file.map(|v| v.to_string());
self
}
fn resolve_path(&self, path: Option<&Path>) -> io::Result<PathBuf> {
let mut root = self.root.clone();
let root_canon = root.canonicalize()?;
if let Some(path) = path {
root.push(&path);
}
let absolute = root.canonicalize()?;
if !absolute.starts_with(&root_canon) {
debug!("Path not under base path: {:?}", path);
return Err(io::Error::new(io::ErrorKind::NotFound, "Base path"));
}
Ok(absolute)
}
async fn handle(
&self,
req: &Request<Body>,
path: Option<&Path>,
) -> Result<http::Response<Body>, Error> {
if req.method() != http::Method::GET && req.method() != http::Method::HEAD {
return Ok(err(http::StatusCode::METHOD_NOT_ALLOWED, "Use GET or HEAD"));
}
let mut absolute = match self.resolve_path(path) {
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
return Ok(err(StatusCode::NOT_FOUND, "Not found"));
} else {
warn!("Failed to canonicalize ({:?}): {:?}", path, e);
return Ok(err(StatusCode::BAD_REQUEST, "Bad request"));
}
}
Ok(v) => v,
};
if absolute.is_dir() {
if let Some(index) = &self.index_file {
absolute.push(index);
} else {
return Ok(err(StatusCode::NOT_FOUND, "Not found"));
}
}
let d = Dispatch::new(absolute, req);
Ok(d.into_response().await?)
}
}
impl Handler for Static {
fn call<'a>(&'a self, req: Request<Body>) -> Pin<Box<dyn Future<Output = Reply> + Send + 'a>> {
if self.use_path_param {
let params = req.path_params();
if params.is_empty() || params.len() > 1 {
let msg = "serve_dir() must be used with none path param. Example: /dir/*file";
warn!("{}", msg);
Box::pin(async move { err(StatusCode::INTERNAL_SERVER_ERROR, msg).into() })
} else {
let path: PathBuf = params[0].1.to_owned().into();
Box::pin(async move {
let ret = self.handle(&req, Some(&path)).await;
ret.into()
})
}
} else {
Box::pin(async move {
let ret = self.handle(&req, None).await;
ret.into()
})
}
}
}
fn err(status: http::StatusCode, msg: &str) -> http::Response<Body> {
http::Response::builder()
.status(status)
.body(msg.into())
.unwrap()
}
struct Dispatch {
file: PathBuf,
if_modified_since: Option<SystemTime>,
is_head: bool,
range: Option<(u64, u64)>,
}
impl Dispatch {
fn new(file: PathBuf, req: &http::Request<Body>) -> Self {
let if_modified_since = req
.headers()
.get_as::<String>("if-modified-since")
.and_then(|v| parse_http_date(&v).ok());
let is_head = req.method() == http::Method::HEAD;
let is_get = req.method() == http::Method::GET;
let range = if is_get {
req.headers()
.get("range")
.and_then(|v| v.to_str().ok())
.filter(|v| v.starts_with("bytes="))
.map(|v| &v[6..])
.and_then(|v| {
if let Some(i) = v.find('-') {
Some((&v[0..i], &v[i + 1..]))
} else {
None
}
})
.and_then(|(s, e)| match (s.parse::<u64>(), e.parse::<u64>()) {
(Ok(s), Ok(e)) => Some((s, e)),
_ => None,
})
.map(|(s, e)| (s, e + 1))
} else {
None
};
Dispatch {
file,
if_modified_since,
is_head,
range,
}
}
async fn into_response(self) -> Result<http::Response<Body>, Error> {
match self.into_response_io().await {
Ok(v) => Ok(v),
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Ok(err(StatusCode::NOT_FOUND, "File not found")),
io::ErrorKind::PermissionDenied => {
Ok(err(StatusCode::FORBIDDEN, "File permission denied"))
}
_ => Err(e.into()),
},
}
}
async fn into_response_io(self) -> io::Result<http::Response<Body>> {
let file = std::fs::File::open(&self.file)?;
let meta = file.metadata()?;
let length = meta.len();
let modified = meta.modified()?;
if let Some(since) = self.if_modified_since {
if let Ok(diff) = modified.duration_since(since) {
if diff.as_secs_f32() < 1.0 {
return Ok(http::Response::builder()
.status(http::StatusCode::NOT_MODIFIED)
.header("cache-control", "must-revalidate")
.header("last-modified", fmt_http_date(modified))
.body(Body::empty())
.unwrap());
}
}
}
let guess = mime_guess::from_path(&self.file);
let mut content_type = if let Some(mime) = guess.first() {
mime.to_string()
} else {
"application/octet-stream".to_string()
};
let read = AsyncRuntime::file_to_reader(file);
const PEEK_LEN: usize = 1024;
let mut peek = Peekable::new(read, PEEK_LEN);
if content_type.starts_with("text/") {
let max = (PEEK_LEN as u64).min(length);
let buf = peek.peek(max as usize).await?;
let mut det = chardetng::EncodingDetector::new();
det.feed(buf, length < PEEK_LEN as u64);
let enc = det.guess(None, true);
content_type.push_str(&format!("; charset={}", enc.name()));
}
let res = http::Response::builder()
.header("cache-control", "must-revalidate")
.header("accept-ranges", "bytes")
.header("content-type", content_type)
.charset_encode(false) .header("last-modified", httpdate::fmt_http_date(modified));
let (body, res) = self.create_body(length, peek, res).await?;
Ok(res.body(body).unwrap())
}
async fn create_body<Z: AsyncReadSeek + Unpin + Send + Sync + 'static>(
&self,
length: u64,
mut reader: Z,
mut res: http::response::Builder,
) -> io::Result<(Body, http::response::Builder)> {
let body = if self.is_head {
res = res.header("content-length", length.to_string());
Body::empty()
} else if let Some((start, end)) = self.range {
if end <= start || start >= length || end > length {
debug!("Bad range [{}..{}] of {}", start, end, length);
res = res.status(http::StatusCode::RANGE_NOT_SATISFIABLE);
Body::empty()
} else {
debug!("Serve range [{}..{}] of {}", start, end, length);
reader.seek(io::SeekFrom::Start(start)).await?;
let sub = end - start;
let limit = ContentLengthRead::new(reader, sub);
res = res.status(http::StatusCode::PARTIAL_CONTENT).header(
"content-range",
format!("bytes {}-{}/{}", start, end - 1, length),
);
Body::from_async_read(limit, Some(sub))
}
} else {
Body::from_async_read(reader, Some(length))
};
Ok((body, res))
}
}