diff options
author | Lukas Stabe <lukas@stabe.de> | 2025-02-05 23:03:05 +0000 |
---|---|---|
committer | Lukas Stabe <lukas@stabe.de> | 2025-02-05 23:03:05 +0000 |
commit | 317bd6a5d42a83c9c5e874788282a6e76f638211 (patch) | |
tree | 0907869057fea553c26601aaca6d2443fcc9de97 /src | |
parent | Merge pull request #1470 from svenstaro/dependabot/cargo/all-dependencies-184... (diff) | |
download | miniserve-317bd6a5d42a83c9c5e874788282a6e76f638211.tar.gz miniserve-317bd6a5d42a83c9c5e874788282a6e76f638211.zip |
add read-only webdav support
Diffstat (limited to '')
-rw-r--r-- | src/args.rs | 6 | ||||
-rw-r--r-- | src/config.rs | 4 | ||||
-rw-r--r-- | src/errors.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 51 | ||||
-rw-r--r-- | src/webdav_fs.rs | 83 |
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))) + } + } +} |