]> OzVa Git service - ozva-cloud/commitdiff
feat: use digest auth (#14)
authorsigoden <sigoden@gmail.com>
Sat, 4 Jun 2022 16:09:21 +0000 (00:09 +0800)
committerGitHub <noreply@github.com>
Sat, 4 Jun 2022 16:09:21 +0000 (00:09 +0800)
* feat: switch to digest auth

* implement digest auth

* cargo fmt

* no lock

Cargo.lock
Cargo.toml
README.md
src/args.rs
src/auth.rs [new file with mode: 0644]
src/main.rs
src/server.rs

index 88bed88012f82fc8ee2d9acdddfda1f236156f16..01b8badcf4e4a03a02980faee28a9e997edf63de 100644 (file)
@@ -297,6 +297,8 @@ dependencies = [
  "get_if_addrs",
  "headers",
  "hyper",
+ "lazy_static",
+ "md5",
  "mime_guess",
  "percent-encoding",
  "rustls",
@@ -307,6 +309,7 @@ dependencies = [
  "tokio-rustls",
  "tokio-stream",
  "tokio-util",
+ "uuid",
 ]
 
 [[package]]
@@ -484,6 +487,17 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.11.2"
@@ -656,6 +670,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
@@ -770,6 +790,12 @@ version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.39"
@@ -788,6 +814,36 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.20"
@@ -1098,6 +1154,16 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
+[[package]]
+name = "uuid"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238"
+dependencies = [
+ "getrandom",
+ "rand",
+]
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
index 40bef50cb180f6a4a1c316dacc0050c399e0d425..55999e262a3343ccbb814564560ed3af8903deae 100644 (file)
@@ -31,6 +31,9 @@ mime_guess = "2.0.4"
 get_if_addrs = "0.5.3"
 rustls = { version = "0.20", default-features = false, features = ["tls12"] }
 rustls-pemfile = "1"
+md5 = "0.7.0"
+lazy_static = "1.4.0"
+uuid = { version = "1.1.1", features = ["v4", "fast-rng"] }
 
 [profile.release]
 lto = true
index a6d7ac98f13046512343be801b3370780c3c7f23..a23165ba786a5d93834986f9a595ff76bdfbeaac 100644 (file)
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@ Duf is a simple file server. Support static serve, search, upload, webdav...
 - Download folder as zip file
 - Upload files and folders (Drag & Drop)
 - Search files
-- Basic authentication
 - Partial responses (Parallel/Resume download)
+- Authentication
 - Support https
 - Support webdav
 - Easy to use with curl
index 3341276130229844211ae17d287f0a88d051d736..55e70e8e290f58c7ab6ba90eea805c1ce4c3f7d7 100644 (file)
@@ -5,6 +5,7 @@ use std::net::SocketAddr;
 use std::path::{Path, PathBuf};
 use std::{env, fs, io};
 
+use crate::auth::parse_auth;
 use crate::BoxResult;
 
 const ABOUT: &str = concat!("\n", crate_description!()); // Add extra newline.
@@ -115,7 +116,7 @@ pub struct Args {
     pub path: PathBuf,
     pub path_prefix: String,
     pub uri_prefix: String,
-    pub auth: Option<String>,
+    pub auth: Option<(String, String)>,
     pub no_auth_access: bool,
     pub allow_upload: bool,
     pub allow_delete: bool,
@@ -145,7 +146,10 @@ impl Args {
             format!("/{}/", &path_prefix)
         };
         let cors = matches.is_present("cors");
-        let auth = matches.value_of("auth").map(|v| v.to_owned());
+        let auth = match matches.value_of("auth") {
+            Some(auth) => Some(parse_auth(auth)?),
+            None => None,
+        };
         let no_auth_access = matches.is_present("no-auth-access");
         let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
         let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
diff --git a/src/auth.rs b/src/auth.rs
new file mode 100644 (file)
index 0000000..d93b911
--- /dev/null
@@ -0,0 +1,209 @@
+use headers::HeaderValue;
+use lazy_static::lazy_static;
+use md5::Context;
+use std::{
+    collections::HashMap,
+    time::{SystemTime, UNIX_EPOCH},
+};
+use uuid::Uuid;
+
+use crate::BoxResult;
+
+const REALM: &str = "DUF";
+
+lazy_static! {
+    static ref NONCESTARTHASH: Context = {
+        let mut h = Context::new();
+        h.consume(Uuid::new_v4().as_bytes());
+        h.consume(std::process::id().to_be_bytes());
+        h
+    };
+}
+
+pub fn generate_www_auth(stale: bool) -> String {
+    let str_stale = if stale { "stale=true," } else { "" };
+    format!(
+        "Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\",algorithm=\"MD5\"",
+        REALM,
+        create_nonce(),
+        str_stale
+    )
+}
+
+pub fn parse_auth(auth: &str) -> BoxResult<(String, String)> {
+    let p: Vec<&str> = auth.trim().split(':').collect();
+    let err = "Invalid auth value";
+    if p.len() != 2 {
+        return Err(err.into());
+    }
+    let user = p[0];
+    let pass = p[1];
+    let mut h = Context::new();
+    h.consume(format!("{}:{}:{}", user, REALM, pass).as_bytes());
+    Ok((user.to_owned(), format!("{:x}", h.compute())))
+}
+
+pub fn valid_digest(
+    header_value: &HeaderValue,
+    method: &str,
+    auth_user: &str,
+    auth_pass: &str,
+) -> Option<()> {
+    let digest_value = strip_prefix(header_value.as_bytes(), b"Digest ")?;
+    let user_vals = to_headermap(digest_value).ok()?;
+    if let (Some(username), Some(nonce), Some(user_response)) = (
+        user_vals
+            .get(b"username".as_ref())
+            .and_then(|b| std::str::from_utf8(*b).ok()),
+        user_vals.get(b"nonce".as_ref()),
+        user_vals.get(b"response".as_ref()),
+    ) {
+        match validate_nonce(nonce) {
+            Ok(true) => {}
+            _ => return None,
+        }
+        if auth_user != username {
+            return None;
+        }
+        let mut ha = Context::new();
+        ha.consume(method);
+        ha.consume(b":");
+        if let Some(uri) = user_vals.get(b"uri".as_ref()) {
+            ha.consume(uri);
+        }
+        let ha = format!("{:x}", ha.compute());
+        let mut correct_response = None;
+        if let Some(qop) = user_vals.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) = user_vals.get(b"nc".as_ref()) {
+                        c.consume(nc);
+                    }
+                    c.consume(b":");
+                    if let Some(cnonce) = user_vals.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 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 {
+            // grant access
+            return Some(());
+        }
+    }
+    None
+}
+
+/// Check if a nonce is still valid.
+/// Return an error if it was never valid
+fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
+    if nonce.len() != 34 {
+        return Err(());
+    }
+    //parse hex
+    if let Ok(n) = std::str::from_utf8(nonce) {
+        //get time
+        if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
+            //check time
+            let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+            let secs_now = now.as_secs() as u32;
+
+            if let Some(dur) = secs_now.checked_sub(secs_nonce) {
+                //check hash
+                let mut h = NONCESTARTHASH.clone();
+                h.consume(secs_nonce.to_be_bytes());
+                let h = format!("{:x}", h.compute());
+                if h[..26] == n[8..34] {
+                    return Ok(dur < 300); // from the last 5min
+                                          //Authentication-Info ?
+                }
+            }
+        }
+    }
+    Err(())
+}
+
+fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
+    let l = prefix.len();
+    if search.len() < l {
+        return None;
+    }
+    if &search[..l] == prefix {
+        Some(&search[l..])
+    } else {
+        None
+    }
+}
+
+fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
+    let mut sep = Vec::new();
+    let mut asign = Vec::new();
+    let mut i: usize = 0;
+    let mut esc = false;
+    for c in header {
+        match (c, esc) {
+            (b'=', false) => asign.push(i),
+            (b',', false) => sep.push(i),
+            (b'"', false) => esc = true,
+            (b'"', true) => esc = false,
+            _ => {}
+        }
+        i += 1;
+    }
+    sep.push(i); // same len for both Vecs
+
+    i = 0;
+    let mut ret = HashMap::new();
+    for (&k, &a) in sep.iter().zip(asign.iter()) {
+        while header[i] == b' ' {
+            i += 1;
+        }
+        if a <= i || k <= 1 + a {
+            //keys and vals must contain one char
+            return Err(());
+        }
+        let key = &header[i..a];
+        let val = if header[1 + a] == b'"' && header[k - 1] == b'"' {
+            //escaped
+            &header[2 + a..k - 1]
+        } else {
+            //not escaped
+            &header[1 + a..k]
+        };
+        i = 1 + k;
+        ret.insert(key, val);
+    }
+    Ok(ret)
+}
+
+fn create_nonce() -> String {
+    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+    let secs = now.as_secs() as u32;
+    let mut h = NONCESTARTHASH.clone();
+    h.consume(secs.to_be_bytes());
+
+    let n = format!("{:08x}{:032x}", secs, h.compute());
+    n[..34].to_string()
+}
index e6c8e25c253a337aa14013664da1a84de338192a..6f2b9e29adc8ef78fa65be6c075d50a3e017a98b 100644 (file)
@@ -1,4 +1,5 @@
 mod args;
+mod auth;
 mod server;
 
 pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
index 5fe8360dc2aaaeabff82d5dfdd612201e4cc60ff..982022f53335c8007f3d1fd7f820e0f2f0c3e2c8 100644 (file)
@@ -1,3 +1,4 @@
+use crate::auth::{generate_www_auth, valid_digest};
 use crate::{Args, BoxResult};
 
 use async_walkdir::WalkDir;
@@ -34,6 +35,7 @@ use tokio::{fs, io};
 use tokio_rustls::TlsAcceptor;
 use tokio_util::codec::{BytesCodec, FramedRead};
 use tokio_util::io::{ReaderStream, StreamReader};
+use uuid::Uuid;
 
 type Request = hyper::Request<Body>;
 type Response = hyper::Response<Body>;
@@ -364,7 +366,10 @@ impl InnerService {
 
     async fn handle_zip_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
         let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
-        let filename = path.file_name().unwrap().to_str().unwrap();
+        let filename = path
+            .file_name()
+            .and_then(|v| v.to_str())
+            .ok_or_else(|| format!("Failed to get name of `{}`", path.display()))?;
         let path = path.to_owned();
         tokio::spawn(async move {
             if let Err(e) = zip_dir(&mut writer, &path).await {
@@ -509,7 +514,7 @@ impl InnerService {
                     return Ok(());
                 }
             },
-            None => 1,
+            None => 0,
         };
         let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
         if depth > 0 {
@@ -598,22 +603,26 @@ impl InnerService {
     }
 
     async fn handle_lock(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
-        let now = Utc::now().timestamp();
+        let token = if self.args.auth.is_none() {
+            Utc::now().timestamp().to_string()
+        } else {
+            format!("opaquelocktoken:{}", Uuid::new_v4())
+        };
+
         res.headers_mut().insert(
             "content-type",
             "application/xml; charset=utf-8".parse().unwrap(),
         );
         res.headers_mut()
-            .insert("lock-token", format!("<{}>", now).parse().unwrap());
+            .insert("lock-token", format!("<{}>", token).parse().unwrap());
+
         *res.body_mut() = Body::from(format!(
             r#"<?xml version="1.0" encoding="utf-8"?>
 <D:prop xmlns:D="DAV:"><D:lockdiscovery><D:activelock>
-       <D:locktype><D:write/></D:locktype>
-       <D:lockscope><D:exclusive/></D:lockscope>
        <D:locktoken><D:href>{}</D:href></D:locktoken>
        <D:lockroot><D:href>{}</D:href></D:lockroot>
 </D:activelock></D:lockdiscovery></D:prop>"#,
-            now, req_path
+            token, req_path
         ));
         Ok(())
     }
@@ -656,29 +665,29 @@ impl InnerService {
     }
 
     fn auth_guard(&self, req: &Request, res: &mut Response) -> bool {
+        let method = req.method();
         let pass = {
             match &self.args.auth {
                 None => true,
-                Some(auth) => match req.headers().get(AUTHORIZATION) {
-                    Some(value) => match value.to_str().ok().map(|v| {
-                        let mut it = v.split(' ');
-                        (it.next(), it.next())
-                    }) {
-                        Some((Some("Basic"), Some(tail))) => base64::decode(tail)
-                            .ok()
-                            .and_then(|v| String::from_utf8(v).ok())
-                            .map(|v| v.as_str() == auth)
-                            .unwrap_or_default(),
-                        _ => false,
-                    },
-                    None => self.args.no_auth_access && req.method() == Method::GET,
+                Some((user, pass)) => match req.headers().get(AUTHORIZATION) {
+                    Some(value) => {
+                        valid_digest(value, method.as_str(), user.as_str(), pass.as_str()).is_some()
+                    }
+                    None => {
+                        self.args.no_auth_access
+                            && (method == Method::GET
+                                || method == Method::OPTIONS
+                                || method == Method::HEAD
+                                || method.as_str() == "PROPFIND")
+                    }
                 },
             }
         };
         if !pass {
+            let value = generate_www_auth(false);
             status!(res, StatusCode::UNAUTHORIZED);
             res.headers_mut()
-                .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Basic"));
+                .insert(WWW_AUTHENTICATE, value.parse().unwrap());
         }
         pass
     }