]> OzVa Git service - ozva-cloud/commitdiff
feat: deprecate `--auth-method`, as both options are available (#279)
authorsigoden <sigoden@gmail.com>
Fri, 3 Nov 2023 12:36:23 +0000 (20:36 +0800)
committerGitHub <noreply@github.com>
Fri, 3 Nov 2023 12:36:23 +0000 (20:36 +0800)
* feat: deprecate `--auth-method`, both are avaiable

* send one www-authenticate with two schemes

README.md
src/args.rs
src/auth.rs
src/log_http.rs
src/server.rs
tests/auth.rs
tests/log_http.rs

index 916f64098e199bfa7cac96e23ec76270756844ed..e8b45dd635084730f4ee6ff47d9e9107acc9e37b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -59,7 +59,6 @@ Options:
       --path-prefix <path>   Specify a path prefix
       --hidden <value>       Hide paths from directory listings, separated by `,`
   -a, --auth <rules>         Add auth role
-      --auth-method <value>  Select auth method [default: digest] [possible values: basic, digest]
   -A, --allow-all            Allow all operations
       --allow-upload         Allow upload files/folders
       --allow-delete         Allow delete files/folders
@@ -194,8 +193,8 @@ curl http://127.0.0.1:5000?q=Dockerfile&simple    # search for files, just like
 With authorization
 
 ```
-curl --user user:pass --digest http://192.168.8.10:5000/file  # digest auth
-curl --user user:pass http://192.168.8.10:5000/file           # basic auth
+curl http://192.168.8.10:5000/file --user user:pass                 # basic auth
+curl http://192.168.8.10:5000/file --user user:pass --digest        # digest auth
 ```
 
 <details>
@@ -314,7 +313,6 @@ All options can be set using environment variables prefixed with `DUFS_`.
       --path-prefix <path>    DUFS_PATH_PREFIX=/path
       --hidden <value>        DUFS_HIDDEN=*.log
   -a, --auth <rules>          DUFS_AUTH="admin:admin@/:rw|@/" 
-      --auth-method <value>   DUFS_AUTH_METHOD=basic
   -A, --allow-all             DUFS_ALLOW_ALL=true
       --allow-upload          DUFS_ALLOW_UPLOAD=true
       --allow-delete          DUFS_ALLOW_DELETE=true
index 7a44a04bacda4e4472b017a712ee78cfcab1e460..8472fe9478c7ff284536dee3c8bca4d971b36aa1 100644 (file)
@@ -9,7 +9,6 @@ use std::net::IpAddr;
 use std::path::{Path, PathBuf};
 
 use crate::auth::AccessControl;
-use crate::auth::AuthMethod;
 use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT};
 #[cfg(feature = "tls")]
 use crate::tls::{load_certs, load_private_key};
@@ -83,6 +82,7 @@ pub fn build_cli() -> Command {
         )
         .arg(
             Arg::new("auth-method")
+                .hide(true)
                 .env("DUFS_AUTH_METHOD")
                                .hide_env(true)
                 .long("auth-method")
@@ -233,7 +233,6 @@ pub struct Args {
     pub path_prefix: String,
     pub uri_prefix: String,
     pub hidden: Vec<String>,
-    pub auth_method: AuthMethod,
     pub auth: AccessControl,
     pub allow_upload: bool,
     pub allow_delete: bool,
@@ -284,10 +283,6 @@ impl Args {
             .get_many::<String>("auth")
             .map(|auth| auth.map(|v| v.as_str()).collect())
             .unwrap_or_default();
-        let auth_method = match matches.get_one::<String>("auth-method").unwrap().as_str() {
-            "basic" => AuthMethod::Basic,
-            _ => AuthMethod::Digest,
-        };
         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");
@@ -329,7 +324,6 @@ impl Args {
             path_prefix,
             uri_prefix,
             hidden,
-            auth_method,
             auth,
             enable_cors,
             allow_delete,
index dbeda8933cd6562091c6d65273db17f42aa47672..25bf4a2921963e0f89789f74aad6fdaa1d81e2ce 100644 (file)
@@ -14,7 +14,7 @@ use uuid::Uuid;
 use crate::utils::unix_now;
 
 const REALM: &str = "DUFS";
-const DIGEST_AUTH_TIMEOUT: u32 = 86400;
+const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
 
 lazy_static! {
     static ref NONCESTARTHASH: Context = {
@@ -89,18 +89,14 @@ impl AccessControl {
         path: &str,
         method: &Method,
         authorization: Option<&HeaderValue>,
-        auth_method: AuthMethod,
     ) -> (Option<String>, Option<AccessPaths>) {
         if let Some(authorization) = authorization {
-            if let Some(user) = auth_method.get_user(authorization) {
+            if let Some(user) = get_auth_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()
-                    {
+                    if check_auth(authorization, method.as_str(), &user, pass).is_some() {
                         return (Some(user), paths.find(path, !is_readonly_method(method)));
                     } else {
                         return (None, None);
@@ -243,147 +239,119 @@ impl AccessPerm {
     }
 }
 
-fn is_readonly_method(method: &Method) -> bool {
-    method == Method::GET
-        || method == Method::OPTIONS
-        || method == Method::HEAD
-        || method.as_str() == "PROPFIND"
+pub fn www_authenticate() -> Result<HeaderValue> {
+    let nonce = create_nonce()?;
+    let value = format!(
+        "Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"",
+        REALM, nonce, REALM
+    );
+    Ok(HeaderValue::from_str(&value)?)
 }
 
-#[derive(Debug, Clone)]
-pub enum AuthMethod {
-    Basic,
-    Digest,
+pub fn get_auth_user(authorization: &HeaderValue) -> Option<String> {
+    if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
+        let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
+        let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
+        Some(parts[0].to_string())
+    } else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
+        let digest_map = to_headermap(value).ok()?;
+        let username = digest_map.get(b"username".as_ref())?;
+        std::str::from_utf8(username).map(|v| v.to_string()).ok()
+    } else {
+        None
+    }
 }
 
-impl AuthMethod {
-    pub fn www_auth(&self) -> Result<String> {
-        match self {
-            AuthMethod::Basic => Ok(format!("Basic realm=\"{REALM}\"")),
-            AuthMethod::Digest => Ok(format!(
-                "Digest realm=\"{}\",nonce=\"{}\",qop=\"auth\"",
-                REALM,
-                create_nonce()?,
-            )),
+pub fn check_auth(
+    authorization: &HeaderValue,
+    method: &str,
+    auth_user: &str,
+    auth_pass: &str,
+) -> Option<()> {
+    if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
+        let basic_value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
+        let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect();
+
+        if parts[0] != auth_user {
+            return None;
         }
-    }
 
-    pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> {
-        match self {
-            AuthMethod::Basic => {
-                let value: Vec<u8> = general_purpose::STANDARD
-                    .decode(strip_prefix(authorization.as_bytes(), b"Basic ")?)
-                    .ok()?;
-                let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
-                Some(parts[0].to_string())
-            }
-            AuthMethod::Digest => {
-                let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
-                let digest_map = to_headermap(digest_value).ok()?;
-                digest_map
-                    .get(b"username".as_ref())
-                    .and_then(|b| std::str::from_utf8(b).ok())
-                    .map(|v| v.to_string())
-            }
+        if parts[1] == auth_pass {
+            return Some(());
         }
-    }
-
-    fn check(
-        &self,
-        authorization: &HeaderValue,
-        method: &str,
-        auth_user: &str,
-        auth_pass: &str,
-    ) -> Option<()> {
-        match self {
-            AuthMethod::Basic => {
-                let basic_value: Vec<u8> = general_purpose::STANDARD
-                    .decode(strip_prefix(authorization.as_bytes(), b"Basic ")?)
-                    .ok()?;
-                let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect();
-
-                if parts[0] != auth_user {
-                    return None;
-                }
 
-                if parts[1] == auth_pass {
-                    return Some(());
-                }
-
-                None
+        None
+    } else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
+        let digest_map = to_headermap(value).ok()?;
+        if let (Some(username), Some(nonce), Some(user_response)) = (
+            digest_map
+                .get(b"username".as_ref())
+                .and_then(|b| std::str::from_utf8(b).ok()),
+            digest_map.get(b"nonce".as_ref()),
+            digest_map.get(b"response".as_ref()),
+        ) {
+            match validate_nonce(nonce) {
+                Ok(true) => {}
+                _ => return None,
+            }
+            if auth_user != username {
+                return None;
             }
-            AuthMethod::Digest => {
-                let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
-                let digest_map = to_headermap(digest_value).ok()?;
-                if let (Some(username), Some(nonce), Some(user_response)) = (
-                    digest_map
-                        .get(b"username".as_ref())
-                        .and_then(|b| std::str::from_utf8(b).ok()),
-                    digest_map.get(b"nonce".as_ref()),
-                    digest_map.get(b"response".as_ref()),
-                ) {
-                    match validate_nonce(nonce) {
-                        Ok(true) => {}
-                        _ => return None,
-                    }
-                    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 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":");
-                    if let Some(uri) = digest_map.get(b"uri".as_ref()) {
-                        ha.consume(uri);
-                    }
-                    let ha = format!("{:x}", ha.compute());
-                    let mut correct_response = None;
-                    if let Some(qop) = digest_map.get(b"qop".as_ref()) {
-                        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(b":");
-                                c.consume(nonce);
-                                c.consume(b":");
-                                if let Some(nc) = digest_map.get(b"nc".as_ref()) {
-                                    c.consume(nc);
-                                }
-                                c.consume(b":");
-                                if let Some(cnonce) = digest_map.get(b"cnonce".as_ref()) {
-                                    c.consume(cnonce);
-                                }
-                                c.consume(b":");
-                                c.consume(qop);
-                                c.consume(b":");
-                                c.consume(&*ha);
-                                format!("{:x}", c.compute())
-                            });
+            let mut ha = Context::new();
+            ha.consume(method);
+            ha.consume(b":");
+            if let Some(uri) = digest_map.get(b"uri".as_ref()) {
+                ha.consume(uri);
+            }
+            let ha = format!("{:x}", ha.compute());
+            let mut correct_response = None;
+            if let Some(qop) = digest_map.get(b"qop".as_ref()) {
+                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(b":");
+                        c.consume(nonce);
+                        c.consume(b":");
+                        if let Some(nc) = digest_map.get(b"nc".as_ref()) {
+                            c.consume(nc);
                         }
-                    }
-                    let correct_response = match correct_response {
-                        Some(r) => r,
-                        None => {
-                            let mut c = Context::new();
-                            c.consume(&auth_pass);
-                            c.consume(b":");
-                            c.consume(nonce);
-                            c.consume(b":");
-                            c.consume(&*ha);
-                            format!("{:x}", c.compute())
+                        c.consume(b":");
+                        if let Some(cnonce) = digest_map.get(b"cnonce".as_ref()) {
+                            c.consume(cnonce);
                         }
-                    };
-                    if correct_response.as_bytes() == *user_response {
-                        return Some(());
-                    }
+                        c.consume(b":");
+                        c.consume(qop);
+                        c.consume(b":");
+                        c.consume(&*ha);
+                        format!("{:x}", c.compute())
+                    });
                 }
-                None
+            }
+            let correct_response = match correct_response {
+                Some(r) => r,
+                None => {
+                    let mut c = Context::new();
+                    c.consume(&auth_pass);
+                    c.consume(b":");
+                    c.consume(nonce);
+                    c.consume(b":");
+                    c.consume(&*ha);
+                    format!("{:x}", c.compute())
+                }
+            };
+            if correct_response.as_bytes() == *user_response {
+                return Some(());
             }
         }
+        None
+    } else {
+        None
     }
 }
 
@@ -415,6 +383,13 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool> {
     bail!("invalid nonce");
 }
 
+fn is_readonly_method(method: &Method) -> bool {
+    method == Method::GET
+        || method == Method::OPTIONS
+        || method == Method::HEAD
+        || method.as_str() == "PROPFIND"
+}
+
 fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
     let l = prefix.len();
     if search.len() < l {
index 505c30866f2d44e9afa7c301ffec64141282a046..1c46a26aed7f53ca40c23bae828f764b52fbb4d6 100644 (file)
@@ -1,6 +1,6 @@
-use std::{collections::HashMap, str::FromStr, sync::Arc};
+use std::{collections::HashMap, str::FromStr};
 
-use crate::{args::Args, server::Request};
+use crate::{auth::get_auth_user, server::Request};
 
 pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
 
@@ -17,7 +17,7 @@ enum LogElement {
 }
 
 impl LogHttp {
-    pub fn data(&self, req: &Request, args: &Arc<Args>) -> HashMap<String, String> {
+    pub fn data(&self, req: &Request) -> HashMap<String, String> {
         let mut data = HashMap::default();
         for element in self.elements.iter() {
             match element {
@@ -26,10 +26,8 @@ impl LogHttp {
                         data.insert(name.to_string(), format!("{} {}", req.method(), req.uri()));
                     }
                     "remote_user" => {
-                        if let Some(user) = req
-                            .headers()
-                            .get("authorization")
-                            .and_then(|v| args.auth_method.get_user(v))
+                        if let Some(user) =
+                            req.headers().get("authorization").and_then(get_auth_user)
                         {
                             data.insert(name.to_string(), user);
                         }
index e17e33cf623c74a0f0157e78859ca69e3d0daa2f..0cd0989154a7c1b4aa052a4f6af9c2035fa9e8a9 100644 (file)
@@ -1,6 +1,6 @@
 #![allow(clippy::too_many_arguments)]
 
-use crate::auth::{AccessPaths, AccessPerm};
+use crate::auth::{www_authenticate, AccessPaths, AccessPerm};
 use crate::streamer::Streamer;
 use crate::utils::{
     decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name,
@@ -98,7 +98,7 @@ impl Server {
         let uri = req.uri().clone();
         let assets_prefix = &self.assets_prefix;
         let enable_cors = self.args.enable_cors;
-        let mut http_log_data = self.args.log_http.data(&req, &self.args);
+        let mut http_log_data = self.args.log_http.data(&req);
         if let Some(addr) = addr {
             http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
         }
@@ -149,12 +149,7 @@ impl Server {
             }
         };
 
-        let guard = self.args.auth.guard(
-            &relative_path,
-            &method,
-            authorization,
-            self.args.auth_method.clone(),
-        );
+        let guard = self.args.auth.guard(&relative_path, &method, authorization);
 
         let (user, access_paths) = match guard {
             (None, None) => {
@@ -1026,9 +1021,9 @@ impl Server {
     }
 
     fn auth_reject(&self, res: &mut Response) -> Result<()> {
-        let value = self.args.auth_method.www_auth()?;
         set_webdav_headers(res);
-        res.headers_mut().insert(WWW_AUTHENTICATE, value.parse()?);
+        res.headers_mut()
+            .append(WWW_AUTHENTICATE, www_authenticate()?);
         // set 401 to make the browser pop up the login box
         *res.status_mut() = StatusCode::UNAUTHORIZED;
         Ok(())
@@ -1061,12 +1056,10 @@ impl Server {
         };
 
         let authorization = headers.get(AUTHORIZATION);
-        let guard = self.args.auth.guard(
-            &relative_path,
-            req.method(),
-            authorization,
-            self.args.auth_method.clone(),
-        );
+        let guard = self
+            .args
+            .auth
+            .guard(&relative_path, req.method(), authorization);
 
         match guard {
             (_, Some(_)) => {}
index 41b9436487688d7c13d9c75552d1b63253734c8c..5ad47f6eb7958ef1e5a768c113ddcde5b7a7dbf3 100644 (file)
@@ -125,8 +125,8 @@ fn auth_nest_share(
 }
 
 #[rstest]
-#[case(server(&["--auth", "user:pass@/:rw", "--auth-method", "basic", "-A"]), "user", "pass")]
-#[case(server(&["--auth", "u1:p1@/:rw", "--auth-method", "basic", "-A"]), "u1", "p1")]
+#[case(server(&["--auth", "user:pass@/:rw", "-A"]), "user", "pass")]
+#[case(server(&["--auth", "u1:p1@/:rw", "-A"]), "u1", "p1")]
 fn auth_basic(
     #[case] server: TestServer,
     #[case] user: &str,
@@ -216,7 +216,7 @@ fn no_auth_propfind_dir(
 
 #[rstest]
 fn auth_data(
-    #[with(&["--auth", "user:pass@/:rw|@/", "-A", "--auth-method", "basic"])] server: TestServer,
+    #[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer,
 ) -> Result<(), Error> {
     let resp = reqwest::blocking::get(server.url())?;
     let content = resp.text()?;
index d850ec6fca1d87f44ed7738f342d17d5986786fa..f991291e4d6b5cbac7f50c3b8d25c30b8c0096ef 100644 (file)
@@ -12,7 +12,7 @@ use std::process::{Command, Stdio};
 
 #[rstest]
 #[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], false)]
-#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user", "--auth-method", "basic"], true)]
+#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], true)]
 fn log_remote_user(
     tmpdir: TempDir,
     port: u16,
@@ -41,7 +41,7 @@ fn log_remote_user(
 
     assert_eq!(resp.status(), 200);
 
-    let mut buf = [0; 2000];
+    let mut buf = [0; 4096];
     let buf_len = stdout.read(&mut buf)?;
     let output = std::str::from_utf8(&buf[0..buf_len])?;
 
@@ -69,7 +69,7 @@ fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error
     let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?;
     assert_eq!(resp.status(), 200);
 
-    let mut buf = [0; 1000];
+    let mut buf = [0; 4096];
     let buf_len = stdout.read(&mut buf)?;
     let output = std::str::from_utf8(&buf[0..buf_len])?;