use hyper::StatusCode;
use std::cell::RefCell;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::fs::path::PathExt;
use super::opts::HandleOpts;
const CONTAINMENT_CACHE_CAP: usize = 1024;
const CONTAINMENT_CACHE_TTL: Duration = Duration::from_secs(60);
thread_local! {
static CONTAINMENT_CACHE: RefCell<ContainmentCache> =
RefCell::new(ContainmentCache::new());
}
struct ContainmentCache {
entries: HashSet<PathBuf>,
last_clear: Instant,
}
impl ContainmentCache {
fn new() -> Self {
Self {
entries: HashSet::with_capacity(64),
last_clear: Instant::now(),
}
}
fn contains(&self, probe: &Path) -> bool {
if self.last_clear.elapsed() > CONTAINMENT_CACHE_TTL {
return false;
}
self.entries.contains(probe)
}
fn insert(&mut self, probe: PathBuf) {
if self.last_clear.elapsed() > CONTAINMENT_CACHE_TTL
|| self.entries.len() >= CONTAINMENT_CACHE_CAP
{
self.entries.clear();
self.last_clear = Instant::now();
}
self.entries.insert(probe);
}
}
pub(super) fn enforce(
file_path: &Path,
is_dir: bool,
opts: &HandleOpts<'_>,
) -> Result<(), StatusCode> {
let mut probe = file_path.to_path_buf();
if is_dir {
probe.pop();
}
let relative = probe.strip_prefix(opts.base_path).map_err(|err| {
tracing::error!(
"unable to strip prefix from file path '{}': {}",
file_path.display(),
err,
);
StatusCode::NOT_FOUND
})?;
enforce_containment(&probe, opts.base_path)?;
if !opts.follow_symlinks {
enforce_symlink_policy(relative, opts.base_path, file_path)?;
}
if !opts.include_hidden && relative.is_hidden() {
tracing::trace!(
"considering hidden file {} as not found",
file_path.display()
);
return Err(StatusCode::NOT_FOUND);
}
Ok(())
}
fn enforce_containment(probe: &Path, base_path: &Path) -> Result<(), StatusCode> {
if CONTAINMENT_CACHE.with(|c| c.borrow().contains(probe)) {
return Ok(());
}
let file_path_resolved = probe.canonicalize().map_err(|err| {
tracing::error!(
"unable to resolve '{}' symlink path: {}",
probe.display(),
err,
);
StatusCode::NOT_FOUND
})?;
if file_path_resolved.starts_with(base_path) {
cache_safe_probe(probe);
return Ok(());
}
let base_path_resolved = base_path.canonicalize().map_err(|err| {
tracing::error!(
"unable to resolve '{}' base path: {}",
base_path.display(),
err,
);
StatusCode::NOT_FOUND
})?;
if !file_path_resolved.starts_with(base_path_resolved) {
tracing::error!(
"file path '{}' resolves outside of the base path, access denied",
file_path_resolved.display()
);
return Err(StatusCode::NOT_FOUND);
}
cache_safe_probe(probe);
Ok(())
}
#[inline]
fn cache_safe_probe(probe: &Path) {
CONTAINMENT_CACHE.with(|c| {
c.borrow_mut().insert(probe.to_path_buf());
});
}
fn enforce_symlink_policy(
relative: &Path,
base_path: &Path,
file_path: &Path,
) -> Result<(), StatusCode> {
let has_symlink = relative.contains_symlink(base_path).map_err(|err| {
tracing::error!(
"unable to check if file path '{}' contains symlink: {}",
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);
}
Ok(())
}