#![doc(html_root_url = "https://docs.rs/webdav-server/0.4.0")]
//! # `webdav-server` is a webdav server that handles user-accounts.
//!
//! This is a webdav server that allows access to a users home directory,
//! just like an ancient FTP server would (remember those?).
//!
//! This is an application. There is no API documentation here.
//! If you want to build your _own_ webdav server, use the `webdav-handler` crate.
//!
//! See the [GitHub repository](https://github.com/miquels/webdav-server-rs/)
//! for documentation on how to run the server.
//!
#[macro_use]
extern crate log;
mod auth;
mod cache;
mod config;
mod rootfs;
#[doc(hidden)]
pub mod router;
mod suid;
mod tls;
mod unixuser;
mod userfs;
use std::convert::TryFrom;
use std::io;
use std::net::{SocketAddr, ToSocketAddrs};
use std::os::unix::io::{FromRawFd, AsRawFd};
use std::process::exit;
use std::sync::Arc;
use clap::clap_app;
use headers::{authorization::Basic, Authorization, HeaderMapExt};
use http::status::StatusCode;
use hyper::{
self,
server::conn::{AddrIncoming, AddrStream},
service::{make_service_fn, service_fn},
};
use tls_listener::TlsListener;
use tokio_rustls::server::TlsStream;
use webdav_handler::{davpath::DavPath, DavConfig, DavHandler, DavMethod, DavMethodSet};
use webdav_handler::{fakels::FakeLs, fs::DavFileSystem, ls::DavLockSystem};
use crate::config::{AcctType, Auth, CaseInsensitive, Handler, Location, OnNotfound};
use crate::rootfs::RootFs;
use crate::router::MatchedRoute;
use crate::suid::proc_switch_ugid;
use crate::tls::tls_acceptor;
use crate::userfs::UserFs;
static PROGNAME: &'static str = "webdav-server";
// Contains "state" and a handle to the config.
#[derive(Clone)]
struct Server {
dh: DavHandler,
auth: auth::Auth,
config: Arc,
}
type HttpResult = Result, io::Error>;
type HttpRequest = http::Request;
// Server implementation.
impl Server {
// Constructor.
pub fn new(config: Arc, auth: auth::Auth) -> Self {
// mostly empty handler.
let ls = FakeLs::new() as Box;
let dh = DavHandler::builder().locksystem(ls).build_handler();
Server { dh, auth, config }
}
// check user account.
async fn acct<'a>(
&'a self,
location: &Location,
auth_user: Option<&'a String>,
user_param: Option<&'a str>,
) -> Result>, StatusCode>
{
// Get username - if any.
let user = match auth_user.map(|u| u.as_str()).or(user_param) {
Some(u) => u,
None => return Ok(None),
};
// If account is not set, fine.
let acct_type = location
.accounts
.acct_type
.as_ref()
.or(self.config.accounts.acct_type.as_ref());
match acct_type {
Some(&AcctType::Unix) => {},
None => return Ok(None),
};
// check if user exists.
let pwd = match cache::cached::unixuser(user, self.config.unix.aux_groups).await {
Ok(pwd) => pwd,
Err(_) => {
debug!("acct: unix: user {} not found", user);
return Err(StatusCode::UNAUTHORIZED);
},
};
// check minimum uid
if let Some(min_uid) = self.config.unix.min_uid {
if pwd.uid < min_uid {
debug!("acct: {}: uid {} too low (<{})", pwd.name, pwd.uid, min_uid);
return Err(StatusCode::FORBIDDEN);
}
}
Ok(Some(pwd))
}
// return a new response::Builder with the Server and CORS header set.
fn response_builder(&self) -> http::response::Builder {
let mut builder = hyper::Response::builder();
self.set_headers(builder.headers_mut().unwrap());
builder
}
// Set Server: webdav-server-rs header, and CORS.
fn set_headers(&self, headers: &mut http::HeaderMap) {
let id = self
.config
.server
.identification
.as_ref()
.map(|s| s.as_str())
.unwrap_or("webdav-server-rs");
if id != "" {
headers.insert("server", id.parse().unwrap());
}
if self.config.server.cors {
headers.insert("Access-Control-Allow-Origin", "*".parse().unwrap());
headers.insert("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,PROPFIND".parse().unwrap());
headers.insert("Access-Control-Allow-Headers", "DNT,Depth,Range".parse().unwrap());
}
}
// handle a request.
async fn route(&self, req: HttpRequest, remote_ip: SocketAddr) -> HttpResult {
// Get the URI path.
let davpath = match DavPath::from_uri(req.uri()) {
Ok(p) => p,
Err(_) => return self.error(StatusCode::BAD_REQUEST).await,
};
let path = davpath.as_bytes();
// Get the method.
let method = match DavMethod::try_from(req.method()) {
Ok(m) => m,
Err(_) => return self.error(http::StatusCode::METHOD_NOT_ALLOWED).await,
};
// Request is stored here.
let mut reqdata = Some(req);
let mut got_match = false;
// Match routes to one or more locations.
for route in self
.config
.router
.matches(path, method, &["user", "path"])
.drain(..)
{
got_match = true;
// Take the request from the option.
let req = reqdata.take().unwrap();
// if we might continue, store a clone of the request for the next round.
let location = &self.config.location[*route.data];
if let Some(OnNotfound::Continue) = location.on_notfound {
reqdata.get_or_insert(clone_httpreq(&req));
}
// handle request.
let res = self
.handle(req, method, path, route, location, remote_ip.clone())
.await?;
// no on_notfound? then this is final.
if reqdata.is_none() || res.status() != StatusCode::NOT_FOUND {
return Ok(res);
}
}
if !got_match {
debug!("route: no matching route for {:?}", davpath);
}
self.error(StatusCode::NOT_FOUND).await
}
// handle a request.
async fn handle<'a, 't: 'a, 'p: 'a>(
&'a self,
req: HttpRequest,
method: DavMethod,
path: &'a [u8],
route: MatchedRoute<'t, 'p, usize>,
location: &'a Location,
remote_ip: SocketAddr,
) -> HttpResult
{
// See if we matched a :user parameter
// If so, it must be valid UTF-8, or we return NOT_FOUND.
let user_param = match route.params[0].as_ref() {
Some(p) => {
match p.as_str() {
Some(p) => Some(p),
None => {
debug!("handle: invalid utf-8 in :user part of path");
return self.error(StatusCode::NOT_FOUND).await;
},
}
},
None => None,
};
// Do authentication if needed.
let auth_hdr = req.headers().typed_get::>();
let do_auth = match location.auth {
Some(Auth::True) => true,
Some(Auth::Write) => !DavMethodSet::WEBDAV_RO.contains(method) || auth_hdr.is_some(),
Some(Auth::False) => false,
Some(Auth::Opportunistic) | None => auth_hdr.is_some(),
};
let auth_user = if do_auth {
let user = match self.auth.auth(&req, location, remote_ip).await {
Ok(user) => user,
Err(status) => return self.auth_error(status, location).await,
};
// if there was a :user in the route, return error if it does not match.
if user_param.map(|u| u != &user).unwrap_or(false) {
debug!("handle: auth user and :user mismatch");
return self.auth_error(StatusCode::UNAUTHORIZED, location).await;
}
Some(user)
} else {
None
};
// Now see if we want to do a account lookup, for uid/gid/homedir.
let pwd = match self.acct(location, auth_user.as_ref(), user_param).await {
Ok(pwd) => pwd,
Err(status) => return self.auth_error(status, location).await,
};
// Expand "~" in the directory.
let dir = match expand_directory(location.directory.as_str(), pwd.as_ref()) {
Ok(d) => d,
Err(_) => return self.error(StatusCode::NOT_FOUND).await,
};
// If :path matched, we can calculate the prefix.
// If it didn't, the entire path _is_ the prefix.
let prefix = match route.params[1].as_ref() {
Some(p) => {
let mut start = p.start();
if start > 0 {
start -= 1;
}
&path[..start]
},
None => path,
};
let prefix = match std::str::from_utf8(prefix) {
Ok(p) => p.to_string(),
Err(_) => {
debug!("handle: prefix is non-UTF8");
return self.error(StatusCode::NOT_FOUND).await;
},
};
// Get User-Agent for user-agent specific modes.
let user_agent = req
.headers()
.get("user-agent")
.and_then(|s| s.to_str().ok())
.unwrap_or("");
// Case insensitivity wanted?
let case_insensitive = match location.case_insensitive {
Some(CaseInsensitive::True) => true,
Some(CaseInsensitive::Ms) => user_agent.contains("Microsoft"),
Some(CaseInsensitive::False) | None => false,
};
// macOS optimizations?
let macos = user_agent.contains("WebDAVFS/") && user_agent.contains("Darwin");
// Get the filesystem.
let auth_ugid = if location.setuid {
pwd.as_ref().map(|p| (p.uid, p.gid, p.groups.as_slice()))
} else {
None
};
let fs = match location.handler {
Handler::Virtroot => {
let auth_user = auth_user.as_ref().map(String::to_owned);
RootFs::new(dir, auth_user, auth_ugid) as Box
},
Handler::Filesystem => {
UserFs::new(dir, auth_ugid, true, case_insensitive, macos) as Box
},
};
// Build a handler.
let methods = location
.methods
.unwrap_or(DavMethodSet::from_vec(vec!["GET", "HEAD"]).unwrap());
let hide_symlinks = location.hide_symlinks.clone().unwrap_or(true);
let mut config = DavConfig::new()
.filesystem(fs)
.strip_prefix(prefix)
.methods(methods)
.hide_symlinks(hide_symlinks)
.autoindex(location.autoindex);
if let Some(auth_user) = auth_user {
config = config.principal(auth_user);
}
if let Some(indexfile) = location.indexfile.clone() {
config = config.indexfile(indexfile);
}
// All set.
self.run_davhandler(config, req).await
}
async fn build_error(&self, code: StatusCode, location: Option<&Location>) -> HttpResult {
let msg = format!(
"{} {} \n",
code.as_u16(),
code.canonical_reason().unwrap_or("")
);
let mut response = self
.response_builder()
.status(code)
.header("Content-Type", "text/xml");
if code == StatusCode::UNAUTHORIZED {
let realm = location.and_then(|location| location.accounts.realm.as_ref());
let realm = realm.or(self.config.accounts.realm.as_ref());
let realm = realm.map(|s| s.as_str()).unwrap_or("Webdav Server");
response = response.header("WWW-Authenticate", format!("Basic realm=\"{}\"", realm).as_str());
}
Ok(response.body(msg.into()).unwrap())
}
async fn auth_error(&self, code: StatusCode, location: &Location) -> HttpResult {
self.build_error(code, Some(location)).await
}
async fn error(&self, code: StatusCode) -> HttpResult {
self.build_error(code, None).await
}
// Call the davhandler, then add headers to the response.
async fn run_davhandler(&self, config: DavConfig, req: HttpRequest) -> HttpResult {
let resp = self.dh.handle_with(config, req).await;
let (mut parts, body) = resp.into_parts();
self.set_headers(&mut parts.headers);
Ok(http::Response::from_parts(parts, body))
}
}
fn main() -> Result<(), Box> {
// command line option processing.
let matches = clap_app!(webdav_server =>
(version: "0.3")
(@arg CFG: -c --config +takes_value "configuration file (/etc/webdav-server.toml)")
(@arg PORT: -p --port +takes_value "listen to this port on localhost only")
(@arg DBG: -D --debug "enable debug level logging")
)
.get_matches();
if matches.is_present("DBG") {
use env_logger::Env;
let level = "webdav_server=debug,webdav_handler=debug";
env_logger::Builder::from_env(Env::default().default_filter_or(level)).init();
} else {
env_logger::init();
}
let port = matches.value_of("PORT");
let cfg = matches.value_of("CFG").unwrap_or("/etc/webdav-server.toml");
// read config.
let mut config = match config::read(cfg.clone()) {
Err(e) => {
eprintln!("{}: {}: {}", PROGNAME, cfg, e);
exit(1);
},
Ok(c) => c,
};
config::check(cfg.clone(), &config);
// build routes.
if let Err(e) = config::build_routes(cfg.clone(), &mut config) {
eprintln!("{}: {}: {}", PROGNAME, cfg, e);
exit(1);
}
if let Some(port) = port {
let localhosts = vec![
("127.0.0.1:".to_string() + port).parse::().unwrap(),
("[::]:".to_string() + port).parse::().unwrap(),
];
config.server.listen = config::OneOrManyAddr::Many(localhosts);
}
let config = Arc::new(config);
// set cache timeouts.
if let Some(timeout) = config.unix.cache_timeout {
cache::cached::set_pwcache_timeout(timeout);
}
// resolve addresses.
let addrs = config.server.listen.clone().to_socket_addrs().unwrap_or_else(|e| {
eprintln!("{}: {}: [server] listen: {:?}", PROGNAME, cfg, e);
exit(1);
});
let tls_addrs = config.server.tls_listen.clone().to_socket_addrs().unwrap_or_else(|e| {
eprintln!("{}: {}: [server] listen: {:?}", PROGNAME, cfg, e);
exit(1);
});
// initialize auth early.
let auth = auth::Auth::new(config.clone())?;
// start tokio runtime and initialize the rest from within the runtime.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_io()
.enable_time()
.build()?;
rt.block_on(async move {
// build servers (one for each listen address).
let dav_server = Server::new(config.clone(), auth);
let mut servers = Vec::new();
let mut tls_servers = Vec::new();
// Plaintext servers.
for sockaddr in addrs {
let listener = match make_listener(sockaddr) {
Ok(l) => l,
Err(e) => {
eprintln!("{}: listener on {:?}: {}", PROGNAME, &sockaddr, e);
exit(1);
},
};
let dav_server = dav_server.clone();
let make_service = make_service_fn(move |socket: &AddrStream| {
let dav_server = dav_server.clone();
let remote_addr = socket.remote_addr();
async move {
let func = move |req| {
let dav_server = dav_server.clone();
async move { dav_server.route(req, remote_addr).await }
};
Ok::<_, hyper::Error>(service_fn(func))
}
});
let incoming = AddrIncoming::from_listener(listener)?;
let server = hyper::Server::builder(incoming);
println!("Listening on http://{:?}", sockaddr);
servers.push(async move {
if let Err(e) = server.serve(make_service).await {
eprintln!("{}: server error: {}", PROGNAME, e);
exit(1);
}
});
}
// TLS servers.
if tls_addrs.len() > 0 {
let tls_acceptor = tls_acceptor(&config.server)?;
for sockaddr in tls_addrs {
let tls_acceptor = tls_acceptor.clone();
let listener = make_listener(sockaddr).unwrap_or_else(|e| {
eprintln!("{}: listener on {:?}: {}", PROGNAME, &sockaddr, e);
exit(1);
});
let dav_server = dav_server.clone();
let make_service = make_service_fn(move |stream: &TlsStream| {
let dav_server = dav_server.clone();
let remote_addr = stream.get_ref().0.remote_addr();
async move {
let func = move |req| {
let dav_server = dav_server.clone();
async move { dav_server.route(req, remote_addr).await }
};
Ok::<_, hyper::Error>(service_fn(func))
}
});
// Since the server can exit when there's an error on the TlsStream,
// we run it in a loop. Every time the loop is entered we dup() the
// listening fd and create a new TcpListener. This way, we should
// not lose any pending connections during a restart.
let master_listen_fd = listener.as_raw_fd();
std::mem::forget(listener);
println!("Listening on https://{:?}", sockaddr);
tls_servers.push(async move {
loop {
// reuse the incoming socket after the server exits.
let listen_fd = match nix::unistd::dup(master_listen_fd) {
Ok(fd) => fd,
Err(e) => {
eprintln!("{}: server error: dup: {}", PROGNAME, e);
break;
}
};
// SAFETY: listen_fd is unique (we just dup'ed it).
let std_listen = unsafe { std::net::TcpListener::from_raw_fd(listen_fd) };
let listener = match tokio::net::TcpListener::from_std(std_listen) {
Ok(l) => l,
Err(e) => {
eprintln!("{}: server error: new TcpListener: {}", PROGNAME, e);
break;
}
};
let a_incoming = match AddrIncoming::from_listener(listener) {
Ok(a) => a,
Err(e) => {
eprintln!("{}: server error: new AddrIncoming: {}", PROGNAME, e);
break;
}
};
let incoming = TlsListener::new(tls_acceptor.clone(), a_incoming);
let server = hyper::Server::builder(incoming);
if let Err(e) = server.serve(make_service.clone()).await {
eprintln!("{}: server error: {} (retrying)", PROGNAME, e);
}
}
});
}
}
// drop privs.
match (&config.server.uid, &config.server.gid) {
(&Some(uid), &Some(gid)) => {
if !suid::have_suid_privs() {
eprintln!(
"{}: insufficent priviliges to switch uid/gid (not root).",
PROGNAME
);
exit(1);
}
let keep_privs = config.location.iter().any(|l| l.setuid);
proc_switch_ugid(uid, gid, keep_privs);
},
_ => {},
}
// spawn all servers, and wait for them to finish.
let mut tasks = Vec::new();
for server in servers.drain(..) {
tasks.push(tokio::spawn(server));
}
for server in tls_servers.drain(..) {
tasks.push(tokio::spawn(server));
}
for task in tasks.drain(..) {
let _ = task.await;
}
Ok::<_, Box>(())
})
}
// Clones a http request with an empty body.
fn clone_httpreq(req: &HttpRequest) -> HttpRequest {
let mut builder = http::Request::builder()
.method(req.method().clone())
.uri(req.uri().clone())
.version(req.version().clone());
for (name, value) in req.headers().iter() {
builder = builder.header(name, value);
}
builder.body(hyper::Body::empty()).unwrap()
}
fn expand_directory(dir: &str, pwd: Option<&Arc>) -> Result {
// If it doesn't start with "~", skip.
if !dir.starts_with("~") {
return Ok(dir.to_string());
}
// ~whatever doesn't work.
if dir.len() > 1 && !dir.starts_with("~/") {
debug!("expand_directory: rejecting {}", dir);
return Err(StatusCode::NOT_FOUND);
}
// must have a directory, and that dir must be UTF-8.
let pwd = match pwd {
Some(pwd) => pwd,
None => {
debug!("expand_directory: cannot expand {}: no account", dir);
return Err(StatusCode::NOT_FOUND);
},
};
let homedir = pwd.dir.to_str().ok_or(StatusCode::NOT_FOUND)?;
Ok(format!("{}/{}", homedir, &dir[1..]))
}
// Make a new TcpListener, and if it's a V6 listener, set the
// V6_V6ONLY socket option on it.
fn make_listener(addr: SocketAddr) -> io::Result {
use socket2::{Domain, SockAddr, Socket, Type, Protocol};
let s = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;
if addr.is_ipv6() {
s.set_only_v6(true)?;
}
s.set_nonblocking(true)?;
s.set_nodelay(true)?;
s.set_reuse_address(true)?;
let addr: SockAddr = addr.into();
s.bind(&addr)?;
s.listen(128)?;
let listener: std::net::TcpListener = s.into();
tokio::net::TcpListener::from_std(listener)
}