]> OzVa Git service - ozva-cloud/commitdiff
feat: new auth (#218)
authorsigoden <sigoden@gmail.com>
Thu, 1 Jun 2023 10:52:05 +0000 (18:52 +0800)
committerGitHub <noreply@github.com>
Thu, 1 Jun 2023 10:52:05 +0000 (18:52 +0800)
The access level path control used by dufs has two disadvantages:

1. One path cannot support multiple users
2. It is very troublesome to set multiple paths for one user

So it needs to be refactored.
The new auth is account based, it closes #207, closes #208.

BREAKING CHANGE: new auth

Cargo.toml
src/args.rs
src/auth.rs
src/server.rs
tests/auth.rs
tests/log_http.rs
tests/single_file.rs

index 1839bfd814c574549caacae84533a6c8c13d0674..eb46756babe5957a099013fda36c11238c1daef6 100644 (file)
@@ -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
index 7cb3b37d65a6cddf69cf95d5176db5547ae388ab..fbd59e716058af5bc3ebe6132c4eae037359bb08 100644 (file)
@@ -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");
index ecd9f9061c7a7c80598630f92cf1f6a653248e61..881542b2d7139d3714bca2d07f3fc842e87be081 100644 (file)
@@ -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<String, PathControl>,
-}
-
-#[derive(Debug)]
-pub struct PathControl {
-    readwrite: Account,
-    readonly: Option<Account>,
-    share: bool,
+    users: IndexMap<String, (String, AccessPaths)>,
+    anony: Option<AccessPaths>,
 }
 
 impl AccessControl {
-    pub fn new(raw_rules: &[&str], uri_prefix: &str) -> Result<Self> {
-        let mut rules = HashMap::default();
+    pub fn new(raw_rules: &[&str]) -> Result<Self> {
         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));
+            }
+        }
+        for (path, perm) in anony_paths {
+            for (_, (_, paths)) in users.iter_mut() {
+                paths.add(path, perm)
             }
         }
-        Ok(Self { rules })
+        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;
-        }
-
-        if method == Method::OPTIONS {
-            return GuardType::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;
+    ) -> (Option<String>, Option<AccessPaths>) {
+        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
-                        .validate(authorization, method.as_str(), user, pass)
+                        .check(authorization, method.as_str(), &user, pass)
                         .is_some()
                     {
-                        return GuardType::ReadWrite;
+                        return (Some(user), paths.find(path, !is_readonly_method(method)));
+                    } else {
+                        return (None, None);
                     }
                 }
             }
         }
-        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;
-                        }
-                    }
+
+        if method == Method::OPTIONS {
+            return (None, Some(AccessPaths::new(AccessPerm::ReadOnly)));
+        }
+
+        if let Some(paths) = self.anony.as_ref() {
+            return (None, paths.find(path, !is_readonly_method(method)));
+        }
+
+        (None, None)
+    }
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct AccessPaths {
+    perm: AccessPerm,
+    children: IndexMap<String, AccessPaths>,
+}
+
+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<AccessPaths> {
+        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<AccessPaths> {
+        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<PathBuf> {
+        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<PathBuf>, 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)
+            }
         }
-        GuardType::Reject
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub enum GuardType {
-    Reject,
+#[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<Item = &str> {
-    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<Self> {
-        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<String> {
         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<String> {
     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::<Vec<_>>()
+        );
+        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::<Vec<_>>()
+            )
+        );
+        assert_eq!(paths.find("dir2", true), None);
+        assert!(paths.find("dir1/file", true).is_some());
+    }
+}
index ec3dcfafc42c3fddab9f99cde34b4f29223148d9..4213bac74cc7711a7db2c2328aad8d939612fcd2 100644 (file)
@@ -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<String, String> = 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?;
-                    } 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_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<String, String>,
         head_only: bool,
         user: Option<String>,
+        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<String, String>,
         head_only: bool,
         user: Option<String>,
+        access_paths: AccessPaths,
         res: &mut Response,
     ) -> Result<()> {
         let mut paths: Vec<PathItem> = 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<PathBuf> = 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();
+                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;
+                                }
                             }
-                            Err(_) => {
-                                continue;
+                        }
+                        if is_hidden(&hidden, base_name, is_dir_type) {
+                            if file_type.is_dir() {
+                                it.skip_current_dir();
                             }
+                            continue;
                         }
-                    }
-                    if is_hidden(&hidden, base_name, is_dir_type) {
-                        if file_type.is_dir() {
-                            it.skip_current_dir();
+                        if !base_name.to_lowercase().contains(&search) {
+                            continue;
                         }
-                        continue;
-                    }
-                    if !base_name.to_lowercase().contains(&search) {
-                        continue;
+                        paths.push(entry_path.to_path_buf());
                     }
-                    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<HeaderValue>,
         head_only: bool,
         user: Option<String>,
+        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<HeaderValue>,
+        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,47 +1065,59 @@ impl Server {
         Some(uri.path().to_string())
     }
 
-    fn extract_path(&self, path: &str) -> Option<PathBuf> {
-        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<String> {
+        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());
+        }
+        path.strip_prefix(prefix.trim_start_matches('/'))
+            .map(|v| v.trim_matches('/').to_string())
+    }
+
+    fn join_path(&self, path: &str) -> Option<PathBuf> {
+        if path.is_empty() {
+            return Some(self.args.path.clone());
         }
-        let decoded_path = decode_uri(slash_stripped_path)?;
-        let slashes_switched = if cfg!(windows) {
-            decoded_path.replace('/', "\\")
+        let path = if cfg!(windows) {
+            path.replace('/', "\\")
         } else {
-            decoded_path.into_owned()
+            path.to_string()
         };
-        let stripped_path = match self.strip_path_prefix(&slashes_switched) {
-            Some(path) => path,
-            None => return None,
-        };
-        Some(self.args.path.join(stripped_path))
+        Some(self.args.path.join(path))
     }
 
-    fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> {
-        let path = path.as_ref();
-        if self.args.path_prefix.is_empty() {
-            Some(path)
+    async fn list_dir(
+        &self,
+        entry_path: &Path,
+        base_path: &Path,
+        access_paths: AccessPaths,
+    ) -> Result<Vec<PathItem>> {
+        let mut paths: Vec<PathItem> = vec![];
+        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 {
-            path.strip_prefix(&self.args.path_prefix).ok()
+            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 list_dir(&self, entry_path: &Path, base_path: &Path) -> Result<Vec<PathItem>> {
-        let mut paths: Vec<PathItem> = 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);
+    async fn add_pathitem(&self, paths: &mut Vec<PathItem>, 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);
         }
-        Ok(paths)
     }
 
     async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
@@ -1066,10 +1149,6 @@ impl Server {
             size,
         }))
     }
-
-    fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option<String> {
-        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<W: AsyncWrite + Unpin>(
     writer: &mut W,
     dir: &Path,
+    access_paths: AccessPaths,
     hidden: &[String],
     running: Arc<AtomicBool>,
 ) -> 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<PathBuf> = 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();
+        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;
+                        }
                     }
-                    Err(_) => {
-                        continue;
+                }
+                if is_hidden(&hidden, base_name, is_dir_type) {
+                    if file_type.is_dir() {
+                        it.skip_current_dir();
                     }
+                    continue;
                 }
-            }
-            if is_hidden(&hidden, base_name, is_dir_type) {
-                if file_type.is_dir() {
-                    it.skip_current_dir();
+                if entry.path().symlink_metadata().is_err() {
+                    continue;
                 }
-                continue;
-            }
-            if entry.path().symlink_metadata().is_err() {
-                continue;
-            }
-            if !file_type.is_file() {
-                continue;
+                if !file_type.is_file() {
+                    continue;
+                }
+                paths.push(entry_path.to_path_buf());
             }
-            paths.push(entry_path.to_path_buf());
         }
         paths
     })
index 8d98b89b07798862edab5dddb6b2c7de4d8b25d7..7b973977f291fac6021366cbb965e89829b91664 100644 (file)
@@ -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(())
+}
index 143608e92a0f72e0f9afad6115fc02483fb92540..3cb9badd886dc86dd4ed5fd70018a1bae51e6829 100644 (file)
@@ -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,
index f2b3f8d17fe265b0bf8cd5714c8728cd926d2faa..915f72cc476d7cca3a31f0e611ff7af060e939c5 100644 (file)
@@ -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(())