diff --git a/Cargo.toml b/Cargo.toml index 1839bfd..eb46756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ content_inspector = "0.2" anyhow = "1.0" chardetng = "0.1" glob = "0.3.1" +indexmap = "1.9" [features] default = ["tls"] @@ -59,7 +60,6 @@ regex = "1" url = "2" diqwest = { version = "1", features = ["blocking"] } predicates = "3" -indexmap = "1.9" [profile.release] lto = true diff --git a/src/args.rs b/src/args.rs index 7cb3b37..fbd59e7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -78,7 +78,7 @@ pub fn build_cli() -> Command { .long("auth") .help("Add auth for path") .action(ArgAction::Append) - .value_delimiter(',') + .value_delimiter('|') .value_name("rules"), ) .arg( @@ -288,7 +288,7 @@ impl Args { "basic" => AuthMethod::Basic, _ => AuthMethod::Digest, }; - let auth = AccessControl::new(&auth, &uri_prefix)?; + let auth = AccessControl::new(&auth)?; let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload"); let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete"); let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search"); diff --git a/src/auth.rs b/src/auth.rs index ecd9f90..881542b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,12 +2,16 @@ use anyhow::{anyhow, bail, Result}; use base64::{engine::general_purpose, Engine as _}; use headers::HeaderValue; use hyper::Method; +use indexmap::IndexMap; use lazy_static::lazy_static; use md5::Context; -use std::collections::HashMap; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use uuid::Uuid; -use crate::utils::{encode_uri, unix_now}; +use crate::utils::unix_now; const REALM: &str = "DUFS"; const DIGEST_AUTH_TIMEOUT: u32 = 86400; @@ -21,57 +25,63 @@ lazy_static! { }; } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct AccessControl { - rules: HashMap, -} - -#[derive(Debug)] -pub struct PathControl { - readwrite: Account, - readonly: Option, - share: bool, + users: IndexMap, + anony: Option, } impl AccessControl { - pub fn new(raw_rules: &[&str], uri_prefix: &str) -> Result { - let mut rules = HashMap::default(); + pub fn new(raw_rules: &[&str]) -> Result { if raw_rules.is_empty() { - return Ok(Self { rules }); + return Ok(AccessControl { + anony: Some(AccessPaths::new(AccessPerm::ReadWrite)), + users: IndexMap::new(), + }); } + + let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); + let mut anony = None; + let mut anony_paths = vec![]; + let mut users = IndexMap::new(); for rule in raw_rules { - let parts: Vec<&str> = rule.split('@').collect(); - let create_err = || anyhow!("Invalid auth `{rule}`"); - match parts.as_slice() { - [path, readwrite] => { - let control = PathControl { - readwrite: Account::new(readwrite).ok_or_else(create_err)?, - readonly: None, - share: false, - }; - rules.insert(sanitize_path(path, uri_prefix), control); + let (user, list) = rule.split_once('@').ok_or_else(|| create_err(rule))?; + if user.is_empty() && anony.is_some() { + bail!("Invalid auth, duplicate anonymous rules"); + } + let mut paths = AccessPaths::default(); + for value in list.trim_matches(',').split(',') { + let (path, perm) = match value.split_once(':') { + None => (value, AccessPerm::ReadOnly), + Some((path, "rw")) => (path, AccessPerm::ReadWrite), + _ => return Err(create_err(rule)), + }; + if user.is_empty() { + anony_paths.push((path, perm)); } - [path, readwrite, readonly] => { - let (readonly, share) = if *readonly == "*" { - (None, true) - } else { - (Some(Account::new(readonly).ok_or_else(create_err)?), false) - }; - let control = PathControl { - readwrite: Account::new(readwrite).ok_or_else(create_err)?, - readonly, - share, - }; - rules.insert(sanitize_path(path, uri_prefix), control); + paths.add(path, perm); + } + if user.is_empty() { + anony = Some(paths); + } else if let Some((user, pass)) = user.split_once(':') { + if user.is_empty() || pass.is_empty() { + return Err(create_err(rule)); } - _ => return Err(create_err()), + users.insert(user.to_string(), (pass.to_string(), paths)); + } else { + return Err(create_err(rule)); } } - Ok(Self { rules }) + for (path, perm) in anony_paths { + for (_, (_, paths)) in users.iter_mut() { + paths.add(path, perm) + } + } + Ok(Self { users, anony }) } pub fn valid(&self) -> bool { - !self.rules.is_empty() + !self.users.is_empty() || self.anony.is_some() } pub fn guard( @@ -80,81 +90,157 @@ impl AccessControl { method: &Method, authorization: Option<&HeaderValue>, auth_method: AuthMethod, - ) -> GuardType { - if self.rules.is_empty() { - return GuardType::ReadWrite; + ) -> (Option, Option) { + if let Some(authorization) = authorization { + if let Some(user) = auth_method.get_user(authorization) { + if let Some((pass, paths)) = self.users.get(&user) { + if method == Method::OPTIONS { + return (Some(user), Some(AccessPaths::new(AccessPerm::ReadOnly))); + } + if auth_method + .check(authorization, method.as_str(), &user, pass) + .is_some() + { + return (Some(user), paths.find(path, !is_readonly_method(method))); + } else { + return (None, None); + } + } + } } if method == Method::OPTIONS { - return GuardType::ReadOnly; + return (None, Some(AccessPaths::new(AccessPerm::ReadOnly))); } - let mut controls = vec![]; - for path in walk_path(path) { - if let Some(control) = self.rules.get(path) { - controls.push(control); - if let Some(authorization) = authorization { - let Account { user, pass } = &control.readwrite; - if auth_method - .validate(authorization, method.as_str(), user, pass) - .is_some() - { - return GuardType::ReadWrite; - } - } - } + if let Some(paths) = self.anony.as_ref() { + return (None, paths.find(path, !is_readonly_method(method))); } - if is_readonly_method(method) { - for control in controls.into_iter() { - if control.share { - return GuardType::ReadOnly; - } - if let Some(authorization) = authorization { - if let Some(Account { user, pass }) = &control.readonly { - if auth_method - .validate(authorization, method.as_str(), user, pass) - .is_some() - { - return GuardType::ReadOnly; - } - } - } - } - } - GuardType::Reject + + (None, None) } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum GuardType { - Reject, +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct AccessPaths { + perm: AccessPerm, + children: IndexMap, +} + +impl AccessPaths { + pub fn new(perm: AccessPerm) -> Self { + Self { + perm, + ..Default::default() + } + } + + pub fn perm(&self) -> AccessPerm { + self.perm + } + + fn set_perm(&mut self, perm: AccessPerm) { + if self.perm < perm { + self.perm = perm + } + } + + pub fn add(&mut self, path: &str, perm: AccessPerm) { + let path = path.trim_matches('/'); + if path.is_empty() { + self.set_perm(perm); + } else { + let parts: Vec<&str> = path.split('/').collect(); + self.add_impl(&parts, perm); + } + } + + fn add_impl(&mut self, parts: &[&str], perm: AccessPerm) { + let parts_len = parts.len(); + if parts_len == 0 { + self.set_perm(perm); + return; + } + let child = self.children.entry(parts[0].to_string()).or_default(); + child.add_impl(&parts[1..], perm) + } + + pub fn find(&self, path: &str, writable: bool) -> Option { + let parts: Vec<&str> = path + .trim_matches('/') + .split('/') + .filter(|v| !v.is_empty()) + .collect(); + let target = self.find_impl(&parts, self.perm)?; + if writable && !target.perm().readwrite() { + return None; + } + Some(target) + } + + fn find_impl(&self, parts: &[&str], perm: AccessPerm) -> Option { + let perm = self.perm.max(perm); + if parts.is_empty() { + if perm.indexonly() { + return Some(self.clone()); + } else { + return Some(AccessPaths::new(perm)); + } + } + let child = match self.children.get(parts[0]) { + Some(v) => v, + None => { + if perm.indexonly() { + return None; + } else { + return Some(AccessPaths::new(perm)); + } + } + }; + child.find_impl(&parts[1..], perm) + } + + pub fn child_paths(&self) -> Vec<&String> { + self.children.keys().collect() + } + + pub fn leaf_paths(&self, base: &Path) -> Vec { + if !self.perm().indexonly() { + return vec![base.to_path_buf()]; + } + let mut output = vec![]; + self.leaf_paths_impl(&mut output, base); + output + } + + fn leaf_paths_impl(&self, output: &mut Vec, base: &Path) { + for (name, child) in self.children.iter() { + let base = base.join(name); + if child.perm().indexonly() { + child.leaf_paths_impl(output, &base); + } else { + output.push(base) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum AccessPerm { + #[default] + IndexOnly, ReadWrite, ReadOnly, } -impl GuardType { - pub fn is_reject(&self) -> bool { - *self == GuardType::Reject +impl AccessPerm { + pub fn readwrite(&self) -> bool { + self == &AccessPerm::ReadWrite } -} -fn sanitize_path(path: &str, uri_prefix: &str) -> String { - let new_path = match (uri_prefix, path) { - ("/", "/") => "/".into(), - (_, "/") => uri_prefix.trim_end_matches('/').into(), - _ => format!("{}{}", uri_prefix, path.trim_matches('/')), - }; - encode_uri(&new_path) -} - -fn walk_path(path: &str) -> impl Iterator { - let mut idx = 0; - path.split('/').enumerate().map(move |(i, part)| { - let end = if i == 0 { 1 } else { idx + part.len() + i }; - let value = &path[..end]; - idx += part.len(); - value - }) + pub fn indexonly(&self) -> bool { + self == &AccessPerm::IndexOnly + } } fn is_readonly_method(method: &Method) -> bool { @@ -164,29 +250,6 @@ fn is_readonly_method(method: &Method) -> bool { || method.as_str() == "PROPFIND" } -#[derive(Debug, Clone)] -struct Account { - user: String, - pass: String, -} - -impl Account { - fn new(data: &str) -> Option { - let p: Vec<&str> = data.trim().split(':').collect(); - if p.len() != 2 { - return None; - } - let user = p[0]; - let pass = p[1]; - let mut h = Context::new(); - h.consume(format!("{user}:{REALM}:{pass}").as_bytes()); - Some(Account { - user: user.to_owned(), - pass: format!("{:x}", h.compute()), - }) - } -} - #[derive(Debug, Clone)] pub enum AuthMethod { Basic, @@ -208,6 +271,7 @@ impl AuthMethod { } } } + pub fn get_user(&self, authorization: &HeaderValue) -> Option { match self { AuthMethod::Basic => { @@ -227,7 +291,8 @@ impl AuthMethod { } } } - pub fn validate( + + fn check( &self, authorization: &HeaderValue, method: &str, @@ -245,12 +310,7 @@ impl AuthMethod { return None; } - let mut h = Context::new(); - h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes()); - - let http_pass = format!("{:x}", h.compute()); - - if http_pass == auth_pass { + if parts[1] == auth_pass { return Some(()); } @@ -273,6 +333,11 @@ impl AuthMethod { if auth_user != username { return None; } + + let mut h = Context::new(); + h.consume(format!("{}:{}:{}", auth_user, REALM, auth_pass).as_bytes()); + let auth_pass = format!("{:x}", h.compute()); + let mut ha = Context::new(); ha.consume(method); ha.consume(b":"); @@ -285,7 +350,7 @@ impl AuthMethod { if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() { correct_response = Some({ let mut c = Context::new(); - c.consume(auth_pass); + c.consume(&auth_pass); c.consume(b":"); c.consume(nonce); c.consume(b":"); @@ -308,7 +373,7 @@ impl AuthMethod { Some(r) => r, None => { let mut c = Context::new(); - c.consume(auth_pass); + c.consume(&auth_pass); c.consume(b":"); c.consume(nonce); c.consume(b":"); @@ -317,7 +382,6 @@ impl AuthMethod { } }; if correct_response.as_bytes() == *user_response { - // grant access return Some(()); } } @@ -417,3 +481,42 @@ fn create_nonce() -> Result { let n = format!("{:08x}{:032x}", secs, h.compute()); Ok(n[..34].to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_access_paths() { + let mut paths = AccessPaths::default(); + paths.add("/dir1", AccessPerm::ReadWrite); + paths.add("/dir2/dir1", AccessPerm::ReadWrite); + paths.add("/dir2/dir2", AccessPerm::ReadOnly); + paths.add("/dir2/dir3/dir1", AccessPerm::ReadWrite); + assert_eq!( + paths.leaf_paths(Path::new("/tmp")), + [ + "/tmp/dir1", + "/tmp/dir2/dir1", + "/tmp/dir2/dir2", + "/tmp/dir2/dir3/dir1" + ] + .iter() + .map(PathBuf::from) + .collect::>() + ); + assert_eq!( + paths + .find("dir2", false) + .map(|v| v.leaf_paths(Path::new("/tmp/dir2"))), + Some( + ["/tmp/dir2/dir1", "/tmp/dir2/dir2", "/tmp/dir2/dir3/dir1"] + .iter() + .map(PathBuf::from) + .collect::>() + ) + ); + assert_eq!(paths.find("dir2", true), None); + assert!(paths.find("dir1/file", true).is_some()); + } +} diff --git a/src/server.rs b/src/server.rs index ec3dcfa..4213bac 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,6 @@ +#![allow(clippy::too_many_arguments)] + +use crate::auth::AccessPaths; use crate::streamer::Streamer; use crate::utils::{ decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name, @@ -136,16 +139,32 @@ impl Server { } let authorization = headers.get(AUTHORIZATION); - let guard_type = self.args.auth.guard( - req_path, + let relative_path = match self.resolve_path(req_path) { + Some(v) => v, + None => { + status_forbid(&mut res); + return Ok(res); + } + }; + + let guard = self.args.auth.guard( + &relative_path, &method, authorization, self.args.auth_method.clone(), ); - if guard_type.is_reject() { - self.auth_reject(&mut res)?; - return Ok(res); - } + + let (user, access_paths) = match guard { + (None, None) => { + self.auth_reject(&mut res)?; + return Ok(res); + } + (Some(_), None) => { + status_forbid(&mut res); + return Ok(res); + } + (x, Some(y)) => (x, y), + }; let query = req.uri().query().unwrap_or_default(); let query_params: HashMap = form_urlencoded::parse(query.as_bytes()) @@ -171,8 +190,7 @@ impl Server { } return Ok(res); } - - let path = match self.extract_path(req_path) { + let path = match self.join_path(&relative_path) { Some(v) => v, None => { status_forbid(&mut res); @@ -209,31 +227,38 @@ impl Server { status_not_found(&mut res); return Ok(res); } - self.handle_zip_dir(path, head_only, &mut res).await?; - } else if allow_search && query_params.contains_key("q") { - let user = self.retrieve_user(authorization); - self.handle_search_dir(path, &query_params, head_only, user, &mut res) + self.handle_zip_dir(path, head_only, access_paths, &mut res) .await?; + } else if allow_search && query_params.contains_key("q") { + self.handle_search_dir( + path, + &query_params, + head_only, + user, + access_paths, + &mut res, + ) + .await?; } else { - let user = self.retrieve_user(authorization); self.handle_render_index( path, &query_params, headers, head_only, user, + access_paths, &mut res, ) .await?; } } else if render_index || render_spa { - let user = self.retrieve_user(authorization); self.handle_render_index( path, &query_params, headers, head_only, user, + access_paths, &mut res, ) .await?; @@ -242,19 +267,32 @@ impl Server { status_not_found(&mut res); return Ok(res); } - self.handle_zip_dir(path, head_only, &mut res).await?; + self.handle_zip_dir(path, head_only, access_paths, &mut res) + .await?; } else if allow_search && query_params.contains_key("q") { - let user = self.retrieve_user(authorization); - self.handle_search_dir(path, &query_params, head_only, user, &mut res) - .await?; + self.handle_search_dir( + path, + &query_params, + head_only, + user, + access_paths, + &mut res, + ) + .await?; } else { - let user = self.retrieve_user(authorization); - self.handle_ls_dir(path, true, &query_params, head_only, user, &mut res) - .await?; + self.handle_ls_dir( + path, + true, + &query_params, + head_only, + user, + access_paths, + &mut res, + ) + .await?; } } else if is_file { if query_params.contains_key("edit") { - let user = self.retrieve_user(authorization); self.handle_edit_file(path, head_only, user, &mut res) .await?; } else { @@ -265,9 +303,16 @@ impl Server { self.handle_render_spa(path, headers, head_only, &mut res) .await?; } else if allow_upload && req_path.ends_with('/') { - let user = self.retrieve_user(authorization); - self.handle_ls_dir(path, false, &query_params, head_only, user, &mut res) - .await?; + self.handle_ls_dir( + path, + false, + &query_params, + head_only, + user, + access_paths, + &mut res, + ) + .await?; } else { status_not_found(&mut res); } @@ -294,7 +339,8 @@ impl Server { method => match method.as_str() { "PROPFIND" => { if is_dir { - self.handle_propfind_dir(path, headers, &mut res).await?; + self.handle_propfind_dir(path, headers, access_paths, &mut res) + .await?; } else if is_file { self.handle_propfind_file(path, &mut res).await?; } else { @@ -401,11 +447,12 @@ impl Server { query_params: &HashMap, head_only: bool, user: Option, + access_paths: AccessPaths, res: &mut Response, ) -> Result<()> { let mut paths = vec![]; if exist { - paths = match self.list_dir(path, path).await { + paths = match self.list_dir(path, path, access_paths).await { Ok(paths) => paths, Err(_) => { status_forbid(res); @@ -422,6 +469,7 @@ impl Server { query_params: &HashMap, head_only: bool, user: Option, + access_paths: AccessPaths, res: &mut Response, ) -> Result<()> { let mut paths: Vec = vec![]; @@ -435,36 +483,38 @@ impl Server { let hidden = hidden.clone(); let running = self.running.clone(); let search_paths = tokio::task::spawn_blocking(move || { - let mut it = WalkDir::new(&path_buf).into_iter(); let mut paths: Vec = vec![]; - while let Some(Ok(entry)) = it.next() { - if !running.load(Ordering::SeqCst) { - break; - } - let entry_path = entry.path(); - let base_name = get_file_name(entry_path); - let file_type = entry.file_type(); - let mut is_dir_type: bool = file_type.is_dir(); - if file_type.is_symlink() { - match std::fs::symlink_metadata(entry_path) { - Ok(meta) => { - is_dir_type = meta.is_dir(); - } - Err(_) => { - continue; + for dir in access_paths.leaf_paths(&path_buf) { + let mut it = WalkDir::new(&dir).into_iter(); + while let Some(Ok(entry)) = it.next() { + if !running.load(Ordering::SeqCst) { + break; + } + let entry_path = entry.path(); + let base_name = get_file_name(entry_path); + let file_type = entry.file_type(); + let mut is_dir_type: bool = file_type.is_dir(); + if file_type.is_symlink() { + match std::fs::symlink_metadata(entry_path) { + Ok(meta) => { + is_dir_type = meta.is_dir(); + } + Err(_) => { + continue; + } } } - } - if is_hidden(&hidden, base_name, is_dir_type) { - if file_type.is_dir() { - it.skip_current_dir(); + if is_hidden(&hidden, base_name, is_dir_type) { + if file_type.is_dir() { + it.skip_current_dir(); + } + continue; } - continue; + if !base_name.to_lowercase().contains(&search) { + continue; + } + paths.push(entry_path.to_path_buf()); } - if !base_name.to_lowercase().contains(&search) { - continue; - } - paths.push(entry_path.to_path_buf()); } paths }) @@ -478,7 +528,13 @@ impl Server { self.send_index(path, paths, true, query_params, head_only, user, res) } - async fn handle_zip_dir(&self, path: &Path, head_only: bool, res: &mut Response) -> Result<()> { + async fn handle_zip_dir( + &self, + path: &Path, + head_only: bool, + access_paths: AccessPaths, + res: &mut Response, + ) -> Result<()> { let (mut writer, reader) = tokio::io::duplex(BUF_SIZE); let filename = try_get_file_name(path)?; set_content_diposition(res, false, &format!("{}.zip", filename))?; @@ -491,7 +547,7 @@ impl Server { let hidden = self.args.hidden.clone(); let running = self.running.clone(); tokio::spawn(async move { - if let Err(e) = zip_dir(&mut writer, &path, &hidden, running).await { + if let Err(e) = zip_dir(&mut writer, &path, access_paths, &hidden, running).await { error!("Failed to zip {}, {}", path.display(), e); } }); @@ -507,6 +563,7 @@ impl Server { headers: &HeaderMap, head_only: bool, user: Option, + access_paths: AccessPaths, res: &mut Response, ) -> Result<()> { let index_path = path.join(INDEX_NAME); @@ -519,7 +576,7 @@ impl Server { self.handle_send_file(&index_path, headers, head_only, res) .await?; } else if self.args.render_try_index { - self.handle_ls_dir(path, true, query_params, head_only, user, res) + self.handle_ls_dir(path, true, query_params, head_only, user, access_paths, res) .await?; } else { status_not_found(res) @@ -724,6 +781,7 @@ impl Server { &self, path: &Path, headers: &HeaderMap, + access_paths: AccessPaths, res: &mut Response, ) -> Result<()> { let depth: u32 = match headers.get("depth") { @@ -741,7 +799,7 @@ impl Server { None => vec![], }; if depth != 0 { - match self.list_dir(path, &self.args.path).await { + match self.list_dir(path, &self.args.path, access_paths).await { Ok(child) => paths.extend(child), Err(_) => { status_forbid(res); @@ -965,19 +1023,32 @@ impl Server { return None; } }; + + let relative_path = match self.resolve_path(&dest_path) { + Some(v) => v, + None => { + *res.status_mut() = StatusCode::BAD_REQUEST; + return None; + } + }; + let authorization = headers.get(AUTHORIZATION); - let guard_type = self.args.auth.guard( - &dest_path, + let guard = self.args.auth.guard( + &relative_path, req.method(), authorization, self.args.auth_method.clone(), ); - if guard_type.is_reject() { - status_forbid(res); - return None; - } - let dest = match self.extract_path(&dest_path) { + match guard { + (_, Some(_)) => {} + _ => { + status_forbid(res); + return None; + } + }; + + let dest = match self.join_path(&relative_path) { Some(dest) => dest, None => { *res.status_mut() = StatusCode::BAD_REQUEST; @@ -994,49 +1065,61 @@ impl Server { Some(uri.path().to_string()) } - fn extract_path(&self, path: &str) -> Option { - let mut slash_stripped_path = path; - while let Some(p) = slash_stripped_path.strip_prefix('/') { - slash_stripped_path = p + fn resolve_path(&self, path: &str) -> Option { + let path = path.trim_matches('/'); + let path = decode_uri(path)?; + let prefix = self.args.path_prefix.as_str(); + if prefix == "/" { + return Some(path.to_string()); } - let decoded_path = decode_uri(slash_stripped_path)?; - let slashes_switched = if cfg!(windows) { - decoded_path.replace('/', "\\") - } else { - decoded_path.into_owned() - }; - let stripped_path = match self.strip_path_prefix(&slashes_switched) { - Some(path) => path, - None => return None, - }; - Some(self.args.path.join(stripped_path)) + path.strip_prefix(prefix.trim_start_matches('/')) + .map(|v| v.trim_matches('/').to_string()) } - fn strip_path_prefix<'a, P: AsRef>(&self, path: &'a P) -> Option<&'a Path> { - let path = path.as_ref(); - if self.args.path_prefix.is_empty() { - Some(path) - } else { - path.strip_prefix(&self.args.path_prefix).ok() + fn join_path(&self, path: &str) -> Option { + if path.is_empty() { + return Some(self.args.path.clone()); } + let path = if cfg!(windows) { + path.replace('/', "\\") + } else { + path.to_string() + }; + Some(self.args.path.join(path)) } - async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> Result> { + async fn list_dir( + &self, + entry_path: &Path, + base_path: &Path, + access_paths: AccessPaths, + ) -> Result> { let mut paths: Vec = vec![]; - let mut rd = fs::read_dir(entry_path).await?; - while let Ok(Some(entry)) = rd.next_entry().await { - let entry_path = entry.path(); - let base_name = get_file_name(&entry_path); - if let Ok(Some(item)) = self.to_pathitem(entry_path.as_path(), base_path).await { - if is_hidden(&self.args.hidden, base_name, item.is_dir()) { - continue; - } - paths.push(item); + if access_paths.perm().indexonly() { + for name in access_paths.child_paths() { + let entry_path = entry_path.join(name); + self.add_pathitem(&mut paths, base_path, &entry_path).await; + } + } else { + let mut rd = fs::read_dir(entry_path).await?; + while let Ok(Some(entry)) = rd.next_entry().await { + let entry_path = entry.path(); + self.add_pathitem(&mut paths, base_path, &entry_path).await; } } Ok(paths) } + async fn add_pathitem(&self, paths: &mut Vec, base_path: &Path, entry_path: &Path) { + let base_name = get_file_name(entry_path); + if let Ok(Some(item)) = self.to_pathitem(entry_path, base_path).await { + if is_hidden(&self.args.hidden, base_name, item.is_dir()) { + return; + } + paths.push(item); + } + } + async fn to_pathitem>(&self, path: P, base_path: P) -> Result> { let path = path.as_ref(); let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path)); @@ -1066,10 +1149,6 @@ impl Server { size, })) } - - fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option { - self.args.auth_method.get_user(authorization?) - } } #[derive(Debug, Serialize)] @@ -1237,47 +1316,50 @@ fn res_multistatus(res: &mut Response, content: &str) { async fn zip_dir( writer: &mut W, dir: &Path, + access_paths: AccessPaths, hidden: &[String], running: Arc, ) -> Result<()> { let mut writer = ZipFileWriter::new(writer); let hidden = Arc::new(hidden.to_vec()); let hidden = hidden.clone(); - let dir_path_buf = dir.to_path_buf(); + let dir_clone = dir.to_path_buf(); let zip_paths = tokio::task::spawn_blocking(move || { - let mut it = WalkDir::new(&dir_path_buf).into_iter(); let mut paths: Vec = vec![]; - while let Some(Ok(entry)) = it.next() { - if !running.load(Ordering::SeqCst) { - break; - } - let entry_path = entry.path(); - let base_name = get_file_name(entry_path); - let file_type = entry.file_type(); - let mut is_dir_type: bool = file_type.is_dir(); - if file_type.is_symlink() { - match std::fs::symlink_metadata(entry_path) { - Ok(meta) => { - is_dir_type = meta.is_dir(); - } - Err(_) => { - continue; + for dir in access_paths.leaf_paths(&dir_clone) { + let mut it = WalkDir::new(&dir).into_iter(); + while let Some(Ok(entry)) = it.next() { + if !running.load(Ordering::SeqCst) { + break; + } + let entry_path = entry.path(); + let base_name = get_file_name(entry_path); + let file_type = entry.file_type(); + let mut is_dir_type: bool = file_type.is_dir(); + if file_type.is_symlink() { + match std::fs::symlink_metadata(entry_path) { + Ok(meta) => { + is_dir_type = meta.is_dir(); + } + Err(_) => { + continue; + } } } - } - if is_hidden(&hidden, base_name, is_dir_type) { - if file_type.is_dir() { - it.skip_current_dir(); + if is_hidden(&hidden, base_name, is_dir_type) { + if file_type.is_dir() { + it.skip_current_dir(); + } + continue; } - continue; + if entry.path().symlink_metadata().is_err() { + continue; + } + if !file_type.is_file() { + continue; + } + paths.push(entry_path.to_path_buf()); } - if entry.path().symlink_metadata().is_err() { - continue; - } - if !file_type.is_file() { - continue; - } - paths.push(entry_path.to_path_buf()); } paths }) diff --git a/tests/auth.rs b/tests/auth.rs index 8d98b89..7b97397 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -3,10 +3,11 @@ mod utils; use diqwest::blocking::WithDigestAuth; use fixtures::{server, Error, TestServer}; +use indexmap::IndexSet; use rstest::rstest; #[rstest] -fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { +fn no_auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Result<(), Error> { let resp = reqwest::blocking::get(server.url())?; assert_eq!(resp.status(), 401); assert!(resp.headers().contains_key("www-authenticate")); @@ -17,7 +18,7 @@ fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Resu } #[rstest] -fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { +fn auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Result<(), Error> { let url = format!("{}file1", server.url()); let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; assert_eq!(resp.status(), 401); @@ -29,7 +30,7 @@ fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result< } #[rstest] -fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> { +fn auth_skip(#[with(&["--auth", "@/"])] server: TestServer) -> Result<(), Error> { let resp = reqwest::blocking::get(server.url())?; assert_eq!(resp.status(), 200); Ok(()) @@ -37,7 +38,7 @@ fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result #[rstest] fn auth_skip_on_options_method( - #[with(&["--auth", "/@user:pass"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw"])] server: TestServer, ) -> Result<(), Error> { let url = format!("{}index.html", server.url()); let resp = fetch!(b"OPTIONS", &url).send()?; @@ -47,13 +48,13 @@ fn auth_skip_on_options_method( #[rstest] fn auth_check( - #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer, ) -> Result<(), Error> { let url = format!("{}index.html", server.url()); let resp = fetch!(b"WRITEABLE", &url).send()?; assert_eq!(resp.status(), 401); let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user2", "pass2")?; - assert_eq!(resp.status(), 401); + assert_eq!(resp.status(), 403); let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user", "pass")?; assert_eq!(resp.status(), 200); Ok(()) @@ -61,7 +62,7 @@ fn auth_check( #[rstest] fn auth_readonly( - #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer, ) -> Result<(), Error> { let url = format!("{}index.html", server.url()); let resp = fetch!(b"GET", &url).send()?; @@ -72,13 +73,13 @@ fn auth_readonly( let resp = fetch!(b"PUT", &url) .body(b"abc".to_vec()) .send_with_digest_auth("user2", "pass2")?; - assert_eq!(resp.status(), 401); + assert_eq!(resp.status(), 403); Ok(()) } #[rstest] fn auth_nest( - #[with(&["--auth", "/@user:pass@user2:pass2", "--auth", "/dir1@user3:pass3", "-A"])] + #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "--auth", "user3:pass3@/dir1:rw", "-A"])] server: TestServer, ) -> Result<(), Error> { let url = format!("{}dir1/file1", server.url()); @@ -97,7 +98,8 @@ fn auth_nest( #[rstest] fn auth_nest_share( - #[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, + #[with(&["--auth", "@/", "--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])] + server: TestServer, ) -> Result<(), Error> { let url = format!("{}index.html", server.url()); let resp = fetch!(b"GET", &url).send()?; @@ -106,8 +108,8 @@ fn auth_nest_share( } #[rstest] -#[case(server(&["--auth", "/@user:pass", "--auth-method", "basic", "-A"]), "user", "pass")] -#[case(server(&["--auth", "/@u1:p1", "--auth-method", "basic", "-A"]), "u1", "p1")] +#[case(server(&["--auth", "user:pass@/:rw", "--auth-method", "basic", "-A"]), "user", "pass")] +#[case(server(&["--auth", "u1:p1@/:rw", "--auth-method", "basic", "-A"]), "u1", "p1")] fn auth_basic( #[case] server: TestServer, #[case] user: &str, @@ -126,7 +128,8 @@ fn auth_basic( #[rstest] fn auth_webdav_move( - #[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])] + server: TestServer, ) -> Result<(), Error> { let origin_url = format!("{}dir1/test.html", server.url()); let new_url = format!("{}test2.html", server.url()); @@ -139,7 +142,8 @@ fn auth_webdav_move( #[rstest] fn auth_webdav_copy( - #[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])] + server: TestServer, ) -> Result<(), Error> { let origin_url = format!("{}dir1/test.html", server.url()); let new_url = format!("{}test2.html", server.url()); @@ -152,7 +156,7 @@ fn auth_webdav_copy( #[rstest] fn auth_path_prefix( - #[with(&["--auth", "/@user:pass", "--path-prefix", "xyz", "-A"])] server: TestServer, + #[with(&["--auth", "user:pass@/:rw", "--path-prefix", "xyz", "-A"])] server: TestServer, ) -> Result<(), Error> { let url = format!("{}xyz/index.html", server.url()); let resp = fetch!(b"GET", &url).send()?; @@ -161,3 +165,22 @@ fn auth_path_prefix( assert_eq!(resp.status(), 200); Ok(()) } + +#[rstest] +fn auth_partial_index( + #[with(&["--auth", "user:pass@/dir1:rw,/dir2:rw", "-A"])] server: TestServer, +) -> Result<(), Error> { + let resp = fetch!(b"GET", server.url()).send_with_digest_auth("user", "pass")?; + assert_eq!(resp.status(), 200); + let paths = utils::retrieve_index_paths(&resp.text()?); + assert_eq!(paths, IndexSet::from(["dir1/".into(), "dir2/".into()])); + let resp = fetch!(b"GET", format!("{}?q={}", server.url(), "test.html")) + .send_with_digest_auth("user", "pass")?; + assert_eq!(resp.status(), 200); + let paths = utils::retrieve_index_paths(&resp.text()?); + assert_eq!( + paths, + IndexSet::from(["dir1/test.html".into(), "dir2/test.html".into()]) + ); + Ok(()) +} diff --git a/tests/log_http.rs b/tests/log_http.rs index 143608e..3cb9bad 100644 --- a/tests/log_http.rs +++ b/tests/log_http.rs @@ -11,8 +11,8 @@ use std::io::Read; use std::process::{Command, Stdio}; #[rstest] -#[case(&["-a", "/@user:pass", "--log-format", "$remote_user"], false)] -#[case(&["-a", "/@user:pass", "--log-format", "$remote_user", "--auth-method", "basic"], true)] +#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], false)] +#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user", "--auth-method", "basic"], true)] fn log_remote_user( tmpdir: TempDir, port: u16, diff --git a/tests/single_file.rs b/tests/single_file.rs index f2b3f8d..915f72c 100644 --- a/tests/single_file.rs +++ b/tests/single_file.rs @@ -53,7 +53,7 @@ fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Re let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?; assert_eq!(resp.text()?, "This is index.html"); let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?; - assert_eq!(resp.status(), 404); + assert_eq!(resp.status(), 403); child.kill()?; Ok(())