diff --git a/Cargo.toml b/Cargo.toml index 91960ba..0382f7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,38 @@ categories = ["filesystem"] [workspace] +[features] +# By default, the "pam" and "quota" features are enabled. +# +# Some systems do not have pam (like OpenBSD), so to compile this +# package without pam but with quota use: +# +# cargo build --release --no-default-features --features=quota +# +default = [ "pam", "quota" ] + +# dependencies for the feature. +pam = [ "pam-sandboxed" ] +quota = [ "fs-quota" ] + +# Include debug info in release builds. +[profile.release] +debug = true + +# Build dependencies in optimized mode, even for debug builds. +[profile.dev.package."*"] +opt-level = 3 + +# Build dev-dependencies in non-optimized mode, even for release builds. +[profile.dev.build-override] +opt-level = 0 + [dependencies] clap = "2.33.0" enum_from_str = "0.1.0" enum_from_str_derive = "0.1.0" env_logger = "0.7.1" -fs-quota = { path = "fs_quota", version = "0.1.0" } +fs-quota = { path = "fs_quota", version = "0.1.0", optional = true } futures = "0.3" handlebars = "2.0.2" headers = "0.3.0" @@ -34,7 +60,7 @@ lazy_static = "1.4.0" libc = "0.2.65" log = "0.4.8" net2 = "0.2.33" -pam-sandboxed = { path = "pam", version = "0.2.0" } +pam-sandboxed = { path = "pam", version = "0.2.0", optional = true } percent-encoding = "2.1.0" regex = "1.3.1" serde = { version = "1.0.102", features = ["derive"] } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..fcfc876 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,130 @@ +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; + +use crate::config::{AuthType, Config, Location}; + +use headers::{authorization::Basic, Authorization, HeaderMapExt}; +use http::status::StatusCode; + +type HttpRequest = http::Request; + +#[derive(Clone)] +pub struct Auth { + config: Arc, + #[cfg(feature = "pam")] + pam_auth: pam_sandboxed::PamAuth, +} + +impl Auth { + + pub fn new(config: Arc) -> io::Result { + + // initialize pam. + #[cfg(feature = "pam")] + let pam_auth = { + // set cache timeouts. + if let Some(timeout) = config.pam.cache_timeout { + cache::cached::set_pamcache_timeout(timeout); + } + pam_sandboxed::PamAuth::new(config.pam.threads.clone())? + }; + + Ok(Auth { + #[cfg(feature = "pam")] + pam_auth, + config, + }) + } + + // authenticate user. + pub async fn auth<'a>(&'a self, req: &'a HttpRequest, location: &Location, _remote_ip: SocketAddr) -> Result { + // we must have a login/pass + let basic = match req.headers().typed_get::>() { + Some(Authorization(basic)) => basic, + _ => return Err(StatusCode::UNAUTHORIZED), + }; + let user = basic.username(); + let pass = basic.password(); + + // match the auth type. + let auth_type = location.accounts.auth_type.as_ref().or(self.config.accounts.auth_type.as_ref()); + match auth_type { + #[cfg(feature = "pam")] + Some(&AuthType::Pam) => self.auth_pam(req, user, pass, _remote_ip).await, + Some(&AuthType::HtPasswd(ref ht)) => self.auth_htpasswd(user, pass, ht.as_str()).await, + None => { + debug!("need authentication, but auth-type is not set"); + Err(StatusCode::UNAUTHORIZED) + }, + } + } + + // authenticate user using PAM. + #[cfg(feature = "pam")] + async fn auth_pam<'a>(&'a self, req: &'a HttpRequest, user: &'a str, pass: &'a str, remote_ip: SocketAddr) -> Result { + // stringify the remote IP address. + let ip = remote_ip.ip(); + let ip_string = if ip.is_loopback() { + // if it's loopback, take the value from the x-forwarded-for + // header, if present. + req.headers() + .get("x-forwarded-for") + .and_then(|s| s.to_str().ok()) + .and_then(|s| s.split(',').next()) + .map(|s| s.trim().to_owned()) + } else { + Some(match ip { + IpAddr::V4(ip) => ip.to_string(), + IpAddr::V6(ip) => ip.to_string(), + }) + }; + let ip_ref = ip_string.as_ref().map(|s| s.as_str()); + + // authenticate. + let service = self.config.pam.service.as_str(); + let pam_auth = self.pam_auth.clone(); + match cache::cached::pam_auth(pam_auth, service, user, pass, ip_ref).await { + Ok(_) => Ok(user.to_string()), + Err(_) => { + debug!("auth_pam({}): authentication for {} ({:?}) failed", service, user, ip_ref); + Err(StatusCode::UNAUTHORIZED) + }, + } + } + + // authenticate user using htpasswd. + async fn auth_htpasswd<'a>(&'a self, user: &'a str, pass: &'a str, section: &'a str) -> Result { + + // Get the htpasswd.WHATEVER section from the config file. + let file = match self.config.htpasswd.get(section) { + Some(section) => section.htpasswd.as_str(), + None => return Err(StatusCode::UNAUTHORIZED), + }; + + // Read the file and split it into a bunch of lines. + tokio::task::block_in_place(move || { + let data = match std::fs::read_to_string(file) { + Ok(data) => data, + Err(e) => { + debug!("{}: {}", file, e); + return Err(StatusCode::UNAUTHORIZED); + }, + }; + let lines = data.split('\n').map(|s| s.trim()).filter(|s| !s.starts_with("#") && !s.is_empty()); + + // Check each line for a match. + for line in lines { + let mut fields = line.split(':'); + if let (Some(htuser), Some(htpass)) = (fields.next(), fields.next()) { + if htuser == user && pwhash::unix::verify(pass, htpass) { + return Ok(user.to_string()); + } + } + } + + debug!("auth_htpasswd: authentication for {} failed", user); + Err(StatusCode::UNAUTHORIZED) + }) + } +} diff --git a/src/cache.rs b/src/cache.rs index e149181..1a72b91 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -95,8 +95,6 @@ pub(crate) mod cached { // // Cached versions of Unix account lookup and Pam auth. // - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; use std::io; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -104,7 +102,6 @@ pub(crate) mod cached { use crate::cache; use crate::unixuser::{self, User}; use lazy_static::lazy_static; - use pam_sandboxed::{PamAuth, PamError}; struct Timeouts { pwcache: Duration, @@ -140,6 +137,7 @@ pub(crate) mod cached { timeouts.pamcache = Duration::new(secs as u64, 0); } + #[cfg(feature = "pam")] pub async fn pam_auth<'a>( pam_auth: PamAuth, service: &'a str, @@ -148,6 +146,10 @@ pub(crate) mod cached { remip: Option<&'a str>, ) -> Result<(), PamError> { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + use pam_sandboxed::{PamAuth, PamError}; + let mut s = DefaultHasher::new(); service.hash(&mut s); user.hash(&mut s); diff --git a/src/config.rs b/src/config.rs index fbb40a8..86da19e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,6 +124,7 @@ pub enum Auth { #[derive(Debug, Clone)] pub enum AuthType { + #[cfg(feature = "pam")] Pam, HtPasswd(String), } @@ -202,14 +203,16 @@ pub fn deserialize_authtype<'de, D>(deserializer: D) -> Result, where D: Deserializer<'de> { let s = String::deserialize(deserializer)?; if s.starts_with("htpasswd.") { - Ok(Some(AuthType::HtPasswd(s[9..].to_string()))) - } else if &s == "pam" { - Ok(Some(AuthType::Pam)) - } else if s == "" { - Ok(None) - } else { - Err(serde::de::Error::custom("unknown auth-type")) + return Ok(Some(AuthType::HtPasswd(s[9..].to_string()))); } + #[cfg(feature = "pam")] + if &s == "pam" { + return Ok(Some(AuthType::Pam)); + } + if s == "" { + return Ok(None); + } + Err(serde::de::Error::custom("unknown auth-type")) } pub fn deserialize_opt_enum<'de, D, E>(deserializer: D) -> Result, D::Error> @@ -265,6 +268,7 @@ pub fn build_routes(cfg: &str, config: &mut Config) -> io::Result<()> { } pub fn check(cfg: &str, config: &Config) { + #[cfg(feature = "pam")] if let Some(AuthType::Pam) = config.accounts.auth_type { if config.pam.service == "" { eprintln!("{}: missing section [pam]", cfg); diff --git a/src/main.rs b/src/main.rs index 27eb5e9..df7895f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ #[macro_use] extern crate log; +mod auth; mod cache; mod config; mod rootfs; @@ -25,7 +26,7 @@ mod userfs; use std::convert::TryFrom; use std::io; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::net::{SocketAddr, ToSocketAddrs}; use std::process::exit; use std::sync::Arc; @@ -38,11 +39,10 @@ use hyper::{ service::{make_service_fn, service_fn}, }; -use pam_sandboxed::PamAuth; use webdav_handler::{davpath::DavPath, DavConfig, DavHandler, DavMethod, DavMethodSet}; use webdav_handler::{fakels::FakeLs, fs::DavFileSystem, ls::DavLockSystem}; -use crate::config::{AcctType, Auth, AuthType, CaseInsensitive, Handler, Location, OnNotfound}; +use crate::config::{AcctType, Auth, CaseInsensitive, Handler, Location, OnNotfound}; use crate::rootfs::RootFs; use crate::router::MatchedRoute; use crate::suid::switch_ugid; @@ -54,7 +54,7 @@ static PROGNAME: &'static str = "webdav-server"; #[derive(Clone)] struct Server { dh: DavHandler, - pam_auth: PamAuth, + auth: auth::Auth, config: Arc, } @@ -64,105 +64,12 @@ type HttpRequest = http::Request; // Server implementation. impl Server { // Constructor. - pub fn new(config: Arc, auth: PamAuth) -> Self { + 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: dh, - pam_auth: auth, - config: config, - } - } - - // authenticate user. - async fn auth<'a>(&'a self, req: &'a HttpRequest, location: &Location, remote_ip: SocketAddr) -> Result { - // we must have a login/pass - let basic = match req.headers().typed_get::>() { - Some(Authorization(basic)) => basic, - _ => return Err(StatusCode::UNAUTHORIZED), - }; - let user = basic.username(); - let pass = basic.password(); - - // match the auth type. - let auth_type = location.accounts.auth_type.as_ref().or(self.config.accounts.auth_type.as_ref()); - match auth_type { - Some(&AuthType::Pam) => self.auth_pam(req, user, pass, remote_ip).await, - Some(&AuthType::HtPasswd(ref ht)) => self.auth_htpasswd(user, pass, ht.as_str()).await, - None => { - debug!("need authentication, but auth-type is not set"); - Err(StatusCode::UNAUTHORIZED) - }, - } - } - - // authenticate user using PAM. - async fn auth_pam<'a>(&'a self, req: &'a HttpRequest, user: &'a str, pass: &'a str, remote_ip: SocketAddr) -> Result { - // stringify the remote IP address. - let ip = remote_ip.ip(); - let ip_string = if ip.is_loopback() { - // if it's loopback, take the value from the x-forwarded-for - // header, if present. - req.headers() - .get("x-forwarded-for") - .and_then(|s| s.to_str().ok()) - .and_then(|s| s.split(',').next()) - .map(|s| s.trim().to_owned()) - } else { - Some(match ip { - IpAddr::V4(ip) => ip.to_string(), - IpAddr::V6(ip) => ip.to_string(), - }) - }; - let ip_ref = ip_string.as_ref().map(|s| s.as_str()); - - // authenticate. - let service = self.config.pam.service.as_str(); - let pam_auth = self.pam_auth.clone(); - match cache::cached::pam_auth(pam_auth, service, user, pass, ip_ref).await { - Ok(_) => Ok(user.to_string()), - Err(_) => { - debug!("auth_pam({}): authentication for {} ({:?}) failed", service, user, ip_ref); - Err(StatusCode::UNAUTHORIZED) - }, - } - } - - // authenticate user using htpasswd. - async fn auth_htpasswd<'a>(&'a self, user: &'a str, pass: &'a str, section: &'a str) -> Result { - - // Get the htpasswd.WHATEVER section from the config file. - let file = match self.config.htpasswd.get(section) { - Some(section) => section.htpasswd.as_str(), - None => return Err(StatusCode::UNAUTHORIZED), - }; - - // Read the file and split it into a bunch of lines. - tokio::task::block_in_place(move || { - let data = match std::fs::read_to_string(file) { - Ok(data) => data, - Err(e) => { - debug!("{}: {}", file, e); - return Err(StatusCode::UNAUTHORIZED); - }, - }; - let lines = data.split('\n').map(|s| s.trim()).filter(|s| !s.starts_with("#") && !s.is_empty()); - - // Check each line for a match. - for line in lines { - let mut fields = line.split(':'); - if let (Some(htuser), Some(htpass)) = (fields.next(), fields.next()) { - if htuser == user && pwhash::unix::verify(pass, htpass) { - return Ok(user.to_string()); - } - } - } - - debug!("auth_htpasswd: authentication for {} failed", user); - Err(StatusCode::UNAUTHORIZED) - }) + Server { dh, auth, config } } // check user account. @@ -328,7 +235,7 @@ impl Server { Some(Auth::Opportunistic) | None => auth_hdr.is_some(), }; let auth_user = if do_auth { - let user = match self.auth(&req, location, remote_ip).await { + let user = match self.auth.auth(&req, location, remote_ip).await { Ok(user) => user, Err(status) => return self.auth_error(status, location).await, }; @@ -540,8 +447,8 @@ fn main() -> Result<(), Box> { Ok(a) => a, }; - // initialize pam. - let pam = PamAuth::new(config.pam.threads.clone())?; + // initialize auth early. + let auth = auth::Auth::new(config.clone())?; // start tokio runtime and initialize the rest from within the runtime. let mut rt = tokio::runtime::Builder::new() @@ -552,7 +459,7 @@ fn main() -> Result<(), Box> { rt.block_on(async move { // build servers (one for each listen address). - let dav_server = Server::new(config.clone(), pam); + let dav_server = Server::new(config.clone(), auth); let mut servers = Vec::new(); for sockaddr in addrs { let listener = match make_listener(&sockaddr) { diff --git a/src/userfs.rs b/src/userfs.rs index cd0d63a..3a7d0cd 100644 --- a/src/userfs.rs +++ b/src/userfs.rs @@ -1,20 +1,11 @@ use std::any::Any; use std::path::{Path, PathBuf}; -use std::time::Duration; -use futures::future::FutureExt; use webdav_handler::davpath::DavPath; use webdav_handler::fs::*; use webdav_handler::localfs::LocalFs; -use crate::cache; use crate::suid::UgidSwitch; -use fs_quota::*; -use lazy_static::lazy_static; - -lazy_static! { - static ref QCACHE: cache::Cache = cache::Cache::new().maxage(Duration::new(30, 0)); -} #[derive(Clone)] pub struct UserFs { @@ -95,7 +86,18 @@ impl DavFileSystem for UserFs { self.fs.copy(from, to) } + #[cfg(feature = "quota")] fn get_quota<'a>(&'a self) -> FsFuture<(u64, Option)> { + + use std::time::Duration; + use futures::future::FutureExt; + use crate::cache; + use fs_quota::*; + + lazy_static::lazy_static! { + static ref QCACHE: cache::Cache = cache::Cache::new().maxage(Duration::new(30, 0)); + } + async move { let mut key = self.basedir.clone(); key.push(&self.uid.to_string());