+use anyhow::{anyhow, bail, Result};
use base64::{engine::general_purpose, Engine as _};
use headers::HeaderValue;
use hyper::Method;
use lazy_static::lazy_static;
use md5::Context;
-use std::{
- collections::HashMap,
- time::{SystemTime, UNIX_EPOCH},
-};
+use std::collections::HashMap;
use uuid::Uuid;
-use crate::utils::encode_uri;
-use crate::BoxResult;
+use crate::utils::{encode_uri, unix_now};
const REALM: &str = "DUFS";
const DIGEST_AUTH_TIMEOUT: u32 = 86400;
}
impl AccessControl {
- pub fn new(raw_rules: &[&str], uri_prefix: &str) -> BoxResult<Self> {
+ pub fn new(raw_rules: &[&str], uri_prefix: &str) -> Result<Self> {
let mut rules = HashMap::default();
if raw_rules.is_empty() {
return Ok(Self { rules });
}
for rule in raw_rules {
let parts: Vec<&str> = rule.split('@').collect();
- let create_err = || format!("Invalid auth `{rule}`").into();
+ let create_err = || anyhow!("Invalid auth `{rule}`");
match parts.as_slice() {
[path, readwrite] => {
let control = PathControl {
}
impl AuthMethod {
- pub fn www_auth(&self, stale: bool) -> String {
+ pub fn www_auth(&self, stale: bool) -> Result<String> {
match self {
- AuthMethod::Basic => {
- format!("Basic realm=\"{REALM}\"")
- }
+ AuthMethod::Basic => Ok(format!("Basic realm=\"{REALM}\"")),
AuthMethod::Digest => {
let str_stale = if stale { "stale=true," } else { "" };
- format!(
+ Ok(format!(
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
REALM,
- create_nonce(),
+ create_nonce()?,
str_stale
- )
+ ))
}
}
}
/// Check if a nonce is still valid.
/// Return an error if it was never valid
-fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
+fn validate_nonce(nonce: &[u8]) -> Result<bool> {
if nonce.len() != 34 {
- return Err(());
+ bail!("invalid nonce");
}
//parse hex
if let Ok(n) = std::str::from_utf8(nonce) {
//get time
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
//check time
- let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+ let now = unix_now()?;
let secs_now = now.as_secs() as u32;
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
}
}
}
- Err(())
+ bail!("invalid nonce");
}
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
Ok(ret)
}
-fn create_nonce() -> String {
- let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+fn create_nonce() -> Result<String> {
+ let now = unix_now()?;
let secs = now.as_secs() as u32;
let mut h = NONCESTARTHASH.clone();
h.consume(secs.to_be_bytes());
let n = format!("{:08x}{:032x}", secs, h.compute());
- n[..34].to_string()
+ Ok(n[..34].to_string())
}
#[cfg(feature = "tls")]
use crate::tls::{TlsAcceptor, TlsStream};
+use anyhow::{anyhow, Result};
use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[cfg(feature = "tls")]
use rustls::ServerConfig;
-pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
-
#[tokio::main]
async fn main() {
- run().await.unwrap_or_else(handle_err)
+ run().await.unwrap_or_else(|err| {
+ eprintln!("error: {err}");
+ std::process::exit(1);
+ })
}
-async fn run() -> BoxResult<()> {
- logger::init().map_err(|e| format!("Failed to init logger, {e}"))?;
+async fn run() -> Result<()> {
+ logger::init().map_err(|e| anyhow!("Failed to init logger, {e}"))?;
let cmd = build_cli();
let matches = cmd.get_matches();
if let Some(generator) = matches.get_one::<Shell>("completions") {
fn serve(
args: Arc<Args>,
running: Arc<AtomicBool>,
-) -> BoxResult<Vec<JoinHandle<Result<(), hyper::Error>>>> {
- let inner = Arc::new(Server::new(args.clone(), running));
+) -> Result<Vec<JoinHandle<Result<(), hyper::Error>>>> {
+ let inner = Arc::new(Server::init(args.clone(), running)?);
let mut handles = vec![];
let port = args.port;
for bind_addr in args.addrs.iter() {
match bind_addr {
BindAddr::Address(ip) => {
let incoming = create_addr_incoming(SocketAddr::new(*ip, port))
- .map_err(|e| format!("Failed to bind `{ip}:{port}`, {e}"))?;
+ .map_err(|e| anyhow!("Failed to bind `{ip}:{port}`, {e}"))?;
match args.tls.as_ref() {
#[cfg(feature = "tls")]
Some((certs, key)) => {
#[cfg(unix)]
{
let listener = tokio::net::UnixListener::bind(path)
- .map_err(|e| format!("Failed to bind `{}`, {}", path.display(), e))?;
+ .map_err(|e| anyhow!("Failed to bind `{}`, {e}", path.display()))?;
let acceptor = unix::UnixAcceptor::from_listener(listener);
let new_service = make_service_fn(move |_| serve_func(None));
let server = tokio::spawn(hyper::Server::builder(acceptor).serve(new_service));
Ok(handles)
}
-fn create_addr_incoming(addr: SocketAddr) -> BoxResult<AddrIncoming> {
+fn create_addr_incoming(addr: SocketAddr) -> Result<AddrIncoming> {
use socket2::{Domain, Protocol, Socket, Type};
let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;
if addr.is_ipv6() {
Ok(incoming)
}
-fn print_listening(args: Arc<Args>) -> BoxResult<()> {
+fn print_listening(args: Arc<Args>) -> Result<()> {
let mut bind_addrs = vec![];
let (mut ipv4, mut ipv6) = (false, false);
for bind_addr in args.addrs.iter() {
}
if ipv4 || ipv6 {
let ifaces = if_addrs::get_if_addrs()
- .map_err(|e| format!("Failed to get local interface addresses: {e}"))?;
+ .map_err(|e| anyhow!("Failed to get local interface addresses: {e}"))?;
for iface in ifaces.into_iter() {
let local_ip = iface.ip();
if ipv4 && local_ip.is_ipv4() {
Ok(())
}
-fn handle_err<T>(err: Box<dyn std::error::Error>) -> T {
- eprintln!("error: {err}");
- std::process::exit(1);
-}
-
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
use crate::streamer::Streamer;
use crate::utils::{decode_uri, encode_uri, get_file_name, glob, try_get_file_name};
-use crate::{Args, BoxResult};
+use crate::Args;
+use anyhow::{anyhow, Result};
use walkdir::WalkDir;
use xml::escape::escape_str_pcdata;
use async_zip::write::ZipFileWriter;
use async_zip::{Compression, ZipEntryBuilder};
-use chrono::{TimeZone, Utc};
+use chrono::{LocalResult, TimeZone, Utc};
use futures::TryStreamExt;
use headers::{
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, Connection,
}
impl Server {
- pub fn new(args: Arc<Args>, running: Arc<AtomicBool>) -> Self {
+ pub fn init(args: Arc<Args>, running: Arc<AtomicBool>) -> Result<Self> {
let assets_prefix = format!("{}__dufs_v{}_", args.uri_prefix, env!("CARGO_PKG_VERSION"));
let single_file_req_paths = if args.path_is_file {
vec![
vec![]
};
let html = match args.assets_path.as_ref() {
- Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html")).unwrap()),
+ Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
None => Cow::Borrowed(INDEX_HTML),
};
- Self {
+ Ok(Self {
args,
running,
single_file_req_paths,
assets_prefix,
html,
- }
+ })
}
pub async fn call(
Ok(res)
}
- pub async fn handle(self: Arc<Self>, req: Request) -> BoxResult<Response> {
+ pub async fn handle(self: Arc<Self>, req: Request) -> Result<Response> {
let mut res = Response::default();
let req_path = req.uri().path();
self.args.auth_method.clone(),
);
if guard_type.is_reject() {
- self.auth_reject(&mut res);
+ self.auth_reject(&mut res)?;
return Ok(res);
}
Ok(res)
}
- async fn handle_upload(
- &self,
- path: &Path,
- mut req: Request,
- res: &mut Response,
- ) -> BoxResult<()> {
+ async fn handle_upload(&self, path: &Path, mut req: Request, res: &mut Response) -> Result<()> {
ensure_path_parent(path).await?;
let mut file = match fs::File::create(&path).await {
Ok(())
}
- async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> BoxResult<()> {
+ async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> Result<()> {
match is_dir {
true => fs::remove_dir_all(path).await?,
false => fs::remove_file(path).await?,
head_only: bool,
user: Option<String>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let mut paths = vec![];
if exist {
paths = match self.list_dir(path, path).await {
head_only: bool,
user: Option<String>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let mut paths: Vec<PathItem> = vec![];
- let search = query_params.get("q").unwrap().to_lowercase();
+ let search = query_params
+ .get("q")
+ .ok_or_else(|| anyhow!("invalid q"))?
+ .to_lowercase();
if !search.is_empty() {
let path_buf = path.to_path_buf();
let hidden = Arc::new(self.args.hidden.to_vec());
self.send_index(path, paths, true, query_params, head_only, user, res)
}
- async fn handle_zip_dir(
- &self,
- path: &Path,
- head_only: bool,
- res: &mut Response,
- ) -> BoxResult<()> {
+ async fn handle_zip_dir(&self, path: &Path, head_only: bool, res: &mut Response) -> Result<()> {
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
let filename = try_get_file_name(path)?;
res.headers_mut().insert(
HeaderValue::from_str(&format!(
"attachment; filename=\"{}.zip\"",
encode_uri(filename),
- ))
- .unwrap(),
+ ))?,
);
res.headers_mut()
.insert("content-type", HeaderValue::from_static("application/zip"));
head_only: bool,
user: Option<String>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let index_path = path.join(INDEX_NAME);
if fs::metadata(&index_path)
.await
headers: &HeaderMap<HeaderValue>,
head_only: bool,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
if path.extension().is_none() {
let path = self.args.path.join(INDEX_NAME);
self.handle_send_file(&path, headers, head_only, res)
req_path: &str,
headers: &HeaderMap<HeaderValue>,
res: &mut Response,
- ) -> BoxResult<bool> {
+ ) -> Result<bool> {
if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
match self.args.assets_path.as_ref() {
Some(assets_path) => {
headers: &HeaderMap<HeaderValue>,
head_only: bool,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
let (mut file, meta) = (file?, meta?);
let mut use_range = true;
let filename = try_get_file_name(path)?;
res.headers_mut().insert(
CONTENT_DISPOSITION,
- HeaderValue::from_str(&format!("inline; filename=\"{}\"", encode_uri(filename),))
- .unwrap(),
+ HeaderValue::from_str(&format!("inline; filename=\"{}\"", encode_uri(filename),))?,
);
res.headers_mut().typed_insert(AcceptRanges::bytes());
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let content_range = format!("bytes {}-{}/{}", range.start, end, size);
res.headers_mut()
- .insert(CONTENT_RANGE, content_range.parse().unwrap());
+ .insert(CONTENT_RANGE, content_range.parse()?);
res.headers_mut()
- .insert(CONTENT_LENGTH, format!("{part_size}").parse().unwrap());
+ .insert(CONTENT_LENGTH, format!("{part_size}").parse()?);
if head_only {
return Ok(());
}
} else {
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
res.headers_mut()
- .insert(CONTENT_RANGE, format!("bytes */{size}").parse().unwrap());
+ .insert(CONTENT_RANGE, format!("bytes */{size}").parse()?);
}
} else {
res.headers_mut()
- .insert(CONTENT_LENGTH, format!("{size}").parse().unwrap());
+ .insert(CONTENT_LENGTH, format!("{size}").parse()?);
if head_only {
return Ok(());
}
head_only: bool,
user: Option<String>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
let (file, meta) = (file?, meta?);
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let output = self
.html
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
- .replace("__INDEX_DATA__", &serde_json::to_string(&data).unwrap());
+ .replace("__INDEX_DATA__", &serde_json::to_string(&data)?);
res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64));
if head_only {
path: &Path,
headers: &HeaderMap<HeaderValue>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
let depth: u32 = match headers.get("depth") {
Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
Some(v) => v,
},
None => 1,
};
- let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
+ let mut paths = match self.to_pathitem(path, &self.args.path).await? {
+ Some(v) => vec![v],
+ None => vec![],
+ };
if depth != 0 {
match self.list_dir(path, &self.args.path).await {
Ok(child) => paths.extend(child),
Ok(())
}
- async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
+ async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
} else {
Ok(())
}
- async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
+ async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> Result<()> {
fs::create_dir_all(path).await?;
*res.status_mut() = StatusCode::CREATED;
Ok(())
}
- async fn handle_copy(&self, path: &Path, req: &Request, res: &mut Response) -> BoxResult<()> {
+ async fn handle_copy(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
let dest = match self.extract_dest(req, res) {
Some(dest) => dest,
None => {
Ok(())
}
- async fn handle_move(&self, path: &Path, req: &Request, res: &mut Response) -> BoxResult<()> {
+ async fn handle_move(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
let dest = match self.extract_dest(req, res) {
Some(dest) => dest,
None => {
Ok(())
}
- async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> BoxResult<()> {
+ async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> Result<()> {
let token = if auth {
format!("opaquelocktoken:{}", Uuid::new_v4())
} else {
HeaderValue::from_static("application/xml; charset=utf-8"),
);
res.headers_mut()
- .insert("lock-token", format!("<{token}>").parse().unwrap());
+ .insert("lock-token", format!("<{token}>").parse()?);
*res.body_mut() = Body::from(format!(
r#"<?xml version="1.0" encoding="utf-8"?>
Ok(())
}
- async fn handle_proppatch(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
+ async fn handle_proppatch(&self, req_path: &str, res: &mut Response) -> Result<()> {
let output = format!(
r#"<D:response>
<D:href>{req_path}</D:href>
head_only: bool,
user: Option<String>,
res: &mut Response,
- ) -> BoxResult<()> {
+ ) -> Result<()> {
if let Some(sort) = query_params.get("sort") {
if sort == "name" {
paths.sort_by(|v1, v2| {
let output = if query_params.contains_key("json") {
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
- serde_json::to_string_pretty(&data).unwrap()
+ serde_json::to_string_pretty(&data)?
} else {
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
self.html
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
- .replace("__INDEX_DATA__", &serde_json::to_string(&data).unwrap())
+ .replace("__INDEX_DATA__", &serde_json::to_string(&data)?)
};
res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64));
Ok(())
}
- fn auth_reject(&self, res: &mut Response) {
- let value = self.args.auth_method.www_auth(false);
+ fn auth_reject(&self, res: &mut Response) -> Result<()> {
+ let value = self.args.auth_method.www_auth(false)?;
set_webdav_headers(res);
res.headers_mut().typed_insert(Connection::close());
- res.headers_mut()
- .insert(WWW_AUTHENTICATE, value.parse().unwrap());
+ res.headers_mut().insert(WWW_AUTHENTICATE, value.parse()?);
*res.status_mut() = StatusCode::UNAUTHORIZED;
+ Ok(())
}
async fn is_root_contained(&self, path: &Path) -> bool {
}
}
- async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> BoxResult<Vec<PathItem>> {
+ async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> Result<Vec<PathItem>> {
let mut paths: Vec<PathItem> = vec![];
let mut rd = fs::read_dir(entry_path).await?;
while let Ok(Some(entry)) = rd.next_entry().await {
Ok(paths)
}
- async fn to_pathitem<P: AsRef<Path>>(
- &self,
- path: P,
- base_path: P,
- ) -> BoxResult<Option<PathItem>> {
+ async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
let path = path.as_ref();
let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path));
let (meta, meta2) = (meta?, meta2?);
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
- let rel_path = path.strip_prefix(base_path).unwrap();
+ let rel_path = path.strip_prefix(base_path)?;
let name = normalize_path(rel_path);
Ok(Some(PathItem {
path_type,
}
pub fn to_dav_xml(&self, prefix: &str) -> String {
- let mtime = Utc
- .timestamp_millis_opt(self.mtime as i64)
- .unwrap()
- .to_rfc2822();
+ let mtime = match Utc.timestamp_millis_opt(self.mtime as i64) {
+ LocalResult::Single(v) => v.to_rfc2822(),
+ _ => String::new(),
+ };
let mut href = encode_uri(&format!("{}{}", prefix, &self.name));
if self.is_dir() && !href.ends_with('/') {
href.push('/');
fn to_timestamp(time: &SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH)
- .unwrap()
+ .unwrap_or_default()
.as_millis() as u64
}
}
}
-async fn ensure_path_parent(path: &Path) -> BoxResult<()> {
+async fn ensure_path_parent(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
if fs::symlink_metadata(parent).await.is_err() {
fs::create_dir_all(&parent).await?;
dir: &Path,
hidden: &[String],
running: Arc<AtomicBool>,
-) -> BoxResult<()> {
+) -> Result<()> {
let mut writer = ZipFileWriter::new(writer);
let hidden = Arc::new(hidden.to_vec());
let hidden = hidden.clone();
let mtime = meta.modified().ok()?;
let timestamp = to_timestamp(&mtime);
let size = meta.len();
- let etag = format!(r#""{timestamp}-{size}""#).parse::<ETag>().unwrap();
+ let etag = format!(r#""{timestamp}-{size}""#).parse::<ETag>().ok()?;
let last_modified = LastModified::from(mtime);
Some((etag, last_modified))
}
let range_hdr = headers.get(RANGE)?;
let hdr = range_hdr.to_str().ok()?;
let mut sp = hdr.splitn(2, '=');
- let units = sp.next().unwrap();
+ let units = sp.next()?;
if units == "bytes" {
let range = sp.next()?;
let mut sp_range = range.splitn(2, '-');
- let start: u64 = sp_range.next().unwrap().parse().ok()?;
+ let start: u64 = sp_range.next()?.parse().ok()?;
let end: Option<u64> = if let Some(end) = sp_range.next() {
if end.is_empty() {
None