#![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) }