aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs6
-rw-r--r--src/config.rs4
-rw-r--r--src/errors.rs3
-rw-r--r--src/main.rs51
-rw-r--r--src/webdav_fs.rs83
5 files changed, 145 insertions, 2 deletions
diff --git a/src/args.rs b/src/args.rs
index 9ac6772..922e78b 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -309,6 +309,12 @@ pub struct CliArgs {
/// and return an error instead.
#[arg(short = 'I', long, env = "MINISERVE_DISABLE_INDEXING")]
pub disable_indexing: bool,
+
+ /// Enable read-only WebDAV support (PROPFIND requests)
+ ///
+ /// Currently incompatible with -P|--no-symlinks (see https://github.com/messense/dav-server-rs/issues/37)
+ #[arg(long, env = "MINISERVE_ENABLE_WEBDAV", conflicts_with = "no_symlinks")]
+ pub enable_webdav: bool,
}
/// Checks whether an interface is valid, i.e. it can be parsed into an IP address
diff --git a/src/config.rs b/src/config.rs
index 984f873..5d2d7e8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -149,6 +149,9 @@ pub struct MiniserveConfig {
/// If enabled, indexing is disabled.
pub disable_indexing: bool,
+ /// If enabled, respond to WebDAV requests (read-only).
+ pub webdav_enabled: bool,
+
/// If set, use provided rustls config for TLS
#[cfg(feature = "tls")]
pub tls_rustls_config: Option<rustls::ServerConfig>,
@@ -306,6 +309,7 @@ impl MiniserveConfig {
show_wget_footer: args.show_wget_footer,
readme: args.readme,
disable_indexing: args.disable_indexing,
+ webdav_enabled: args.enable_webdav,
tls_rustls_config: tls_rustls_server_config,
compress_response: args.compress_response,
})
diff --git a/src/errors.rs b/src/errors.rs
index 21f8f12..99c15ff 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -24,6 +24,9 @@ Please set an explicit serve path like: `miniserve /my/path`")]
/// In case miniserve was invoked with --no-symlinks but the serve path is a symlink
#[error("The -P|--no-symlinks option was provided but the serve path '{0}' is a symlink")]
NoSymlinksOptionWithSymlinkServePath(String),
+
+ #[error("The --enable-webdav option was provided, but the serve path '{0}' is a file")]
+ WebdavWithFileServePath(String),
}
#[derive(Debug, Error)]
diff --git a/src/main.rs b/src/main.rs
index 1434a0c..ccf611c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,13 +6,18 @@ use std::time::Duration;
use actix_files::NamedFile;
use actix_web::{
dev::{fn_service, ServiceRequest, ServiceResponse},
- http::header::ContentType,
+ guard,
+ http::{header::ContentType, Method},
middleware, web, App, HttpRequest, HttpResponse, Responder,
};
use actix_web_httpauth::middleware::HttpAuthentication;
use anyhow::Result;
use clap::{crate_version, CommandFactory, Parser};
use colored::*;
+use dav_server::{
+ actix::{DavRequest, DavResponse},
+ DavConfig, DavHandler, DavMethodSet,
+};
use fast_qr::QRBuilder;
use log::{error, warn};
@@ -27,9 +32,11 @@ mod file_utils;
mod listing;
mod pipe;
mod renderer;
+mod webdav_fs;
use crate::config::MiniserveConfig;
use crate::errors::{RuntimeError, StartupError};
+use crate::webdav_fs::RestrictedFs;
static STYLESHEET: &str = grass::include!("data/style.scss");
@@ -88,6 +95,12 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
));
}
+ if miniserve_config.webdav_enabled && miniserve_config.path.is_file() {
+ return Err(StartupError::WebdavWithFileServePath(
+ miniserve_config.path.to_string_lossy().to_string(),
+ ));
+ }
+
let inside_config = miniserve_config.clone();
let canon_path = miniserve_config
@@ -307,7 +320,9 @@ fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders {
/// This is where we configure the app to serve an index file, the file listing, or a single file.
fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
let dir_service = || {
- let mut files = actix_files::Files::new("", &conf.path);
+ // use routing guard so propfind and options requests fall through to the webdav handler
+ let mut files = actix_files::Files::new("", &conf.path)
+ .guard(guard::Any(guard::Get()).or(guard::Head()));
// Use specific index file if one was provided.
if let Some(ref index_file) = conf.index {
@@ -376,6 +391,38 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
// Handle directories
app.service(dir_service());
}
+
+ if conf.webdav_enabled {
+ let fs = RestrictedFs::new(&conf.path, conf.show_hidden);
+
+ let dav_server = DavHandler::builder()
+ .filesystem(fs)
+ .methods(DavMethodSet::WEBDAV_RO)
+ .hide_symlinks(conf.no_symlinks)
+ .strip_prefix(conf.route_prefix.to_owned())
+ .build_handler();
+
+ app.app_data(web::Data::new(dav_server.clone()));
+
+ app.service(
+ // actix requires tail segment to be named, even if unused
+ web::resource("/{tail}*")
+ .guard(
+ guard::Any(guard::Options())
+ .or(guard::Method(Method::from_bytes(b"PROPFIND").unwrap())),
+ )
+ .to(dav_handler),
+ );
+ }
+}
+
+async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
+ if let Some(prefix) = req.prefix() {
+ let config = DavConfig::new().strip_prefix(prefix);
+ davhandler.handle_with(config, req.request).await.into()
+ } else {
+ davhandler.handle(req.request).await.into()
+ }
}
async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
diff --git a/src/webdav_fs.rs b/src/webdav_fs.rs
new file mode 100644
index 0000000..63c9f94
--- /dev/null
+++ b/src/webdav_fs.rs
@@ -0,0 +1,83 @@
+//! Helper types and functions to allow configuring hidden files visibility
+//! for WebDAV handlers
+
+use dav_server::{davpath::DavPath, fs::*, localfs::LocalFs};
+use futures::{future::ready, StreamExt, TryFutureExt};
+use std::path::{Component, Path};
+
+/// A dav_server local filesystem backend that can be configured to deny access
+/// to files and directories with names starting with a dot.
+#[derive(Clone)]
+pub struct RestrictedFs {
+ local: Box<LocalFs>,
+ show_hidden: bool,
+}
+
+impl RestrictedFs {
+ /// Creates a new RestrictedFs serving the local path at "base".
+ /// If "show_hidden" is false, access to hidden files is prevented.
+ pub fn new<P: AsRef<Path>>(base: P, show_hidden: bool) -> Box<RestrictedFs> {
+ let local = LocalFs::new(base, false, false, false);
+ Box::new(RestrictedFs { local, show_hidden })
+ }
+}
+
+/// true if any normal component of path either starts with dot or can't be turned into a str
+fn path_has_hidden_components(path: &DavPath) -> bool {
+ path.as_pathbuf().components().any(|c| match c {
+ Component::Normal(name) => name.to_str().map_or(true, |s| s.starts_with('.')),
+ _ => false,
+ })
+}
+
+impl DavFileSystem for RestrictedFs {
+ fn open<'a>(
+ &'a self,
+ path: &'a DavPath,
+ options: OpenOptions,
+ ) -> FsFuture<'a, Box<dyn DavFile>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.open(path, options)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn read_dir<'a>(
+ &'a self,
+ path: &'a DavPath,
+ meta: ReadDirMeta,
+ ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
+ if self.show_hidden {
+ self.local.read_dir(path, meta)
+ } else if !path_has_hidden_components(path) {
+ Box::pin(self.local.read_dir(path, meta).map_ok(|stream| {
+ let dyn_stream: FsStream<Box<dyn DavDirEntry>> = Box::pin(stream.filter(|entry| {
+ ready(match entry {
+ Ok(ref e) => !e.name().starts_with(b"."),
+ _ => false,
+ })
+ }));
+ dyn_stream
+ }))
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.metadata(path)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.symlink_metadata(path)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+}