From deb6365a2830983f82be4b299a4175887928afd9 Mon Sep 17 00:00:00 2001
From: Joe Koop <joe@joekoop.com>
Date: Sun, 19 Jun 2022 22:25:09 -0500
Subject: [PATCH] feat: added basic auth (#60)

* some small css fixes and changes

* added basic auth
https://stackoverflow.com/a/9534652/3642588

* most tests are passing

* fixed all the tests

* maybe now CI will pass

* implemented sigoden's suggestions

* test basic auth

* fixed some little things
---
 assets/index.css |  23 +++---
 src/args.rs      |  15 ++++
 src/auth.rs      | 201 +++++++++++++++++++++++++++++------------------
 src/server.rs    |  10 ++-
 tests/auth.rs    |  15 ++++
 5 files changed, 175 insertions(+), 89 deletions(-)

diff --git a/assets/index.css b/assets/index.css
index 40e46c7..0c3820a 100644
--- a/assets/index.css
+++ b/assets/index.css
@@ -1,9 +1,14 @@
 html {
-  font-family: -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
+  font-family: -apple-system,BlinkMacSystemFont,Roboto,Helvetica,Arial,sans-serif;
   line-height: 1.5;
   color: #24292e;
 }
 
+body {
+  /* prevent premature breadcrumb wrapping on mobile */
+  min-width: 500px;
+}
+
 .hidden {
   display: none;
 }
@@ -49,6 +54,11 @@ html {
   margin-right: 10px;
 }
 
+.toolbox > div {
+  /* vertically align with breadcrumb text */
+  height: 1.1rem;
+}
+
 .searchbar {
   display: flex;
   flex-wrap: nowrap;
@@ -116,11 +126,6 @@ html {
   white-space: nowrap;
 }
 
-.uploaders-table .cell-name,
-.paths-table .cell-name {
-  width: 500px;
-}
-
 .uploaders-table .cell-status {
   width: 80px;
   padding-left: 0.6em;
@@ -143,7 +148,6 @@ html {
   padding-left: 0.6em;
 }
 
-
 .path svg {
   height: 100%;
   fill: rgba(3,47,98,0.5);
@@ -163,7 +167,7 @@ html {
   display: block;
   text-decoration: none;
   max-width: calc(100vw - 375px);
-  min-width: 400px;
+  min-width: 200px;
 }
 
 .path a:hover {
@@ -200,7 +204,8 @@ html {
   }
 
   svg,
-  .path svg {
+  .path svg,
+  .breadcrumb svg {
     fill: #fff;
   }
 
diff --git a/src/args.rs b/src/args.rs
index c4d29f3..34f8274 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -5,6 +5,7 @@ use std::net::IpAddr;
 use std::path::{Path, PathBuf};
 
 use crate::auth::AccessControl;
+use crate::auth::AuthMethod;
 use crate::tls::{load_certs, load_private_key};
 use crate::BoxResult;
 
@@ -47,6 +48,14 @@ fn app() -> Command<'static> {
                 .value_name("path")
                 .help("Specify an url path prefix"),
         )
+        .arg(
+            Arg::new("auth-method")
+                .long("auth-method")
+                .help("Choose auth method")
+                .possible_values(["basic", "digest"])
+                .default_value("digest")
+                .value_name("value"),
+        )
         .arg(
             Arg::new("auth")
                 .short('a')
@@ -123,6 +132,7 @@ pub struct Args {
     pub path_is_file: bool,
     pub path_prefix: String,
     pub uri_prefix: String,
+    pub auth_method: AuthMethod,
     pub auth: AccessControl,
     pub allow_upload: bool,
     pub allow_delete: bool,
@@ -162,6 +172,10 @@ impl Args {
             .values_of("auth")
             .map(|v| v.collect())
             .unwrap_or_default();
+        let auth_method = match matches.value_of("auth-method").unwrap() {
+            "basic" => AuthMethod::Basic,
+            _ => AuthMethod::Digest,
+        };
         let auth = AccessControl::new(&auth, &uri_prefix)?;
         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");
@@ -185,6 +199,7 @@ impl Args {
             path_is_file,
             path_prefix,
             uri_prefix,
+            auth_method,
             auth,
             enable_cors,
             allow_delete,
diff --git a/src/auth.rs b/src/auth.rs
index 683a9a3..c3e6958 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -76,6 +76,7 @@ impl AccessControl {
         path: &str,
         method: &Method,
         authorization: Option<&HeaderValue>,
+        auth_method: AuthMethod,
     ) -> GuardType {
         if self.rules.is_empty() {
             return GuardType::ReadWrite;
@@ -86,7 +87,10 @@ impl AccessControl {
                 controls.push(control);
                 if let Some(authorization) = authorization {
                     let Account { user, pass } = &control.readwrite;
-                    if valid_digest(authorization, method.as_str(), user, pass).is_some() {
+                    if auth_method
+                        .validate(authorization, method.as_str(), user, pass)
+                        .is_some()
+                    {
                         return GuardType::ReadWrite;
                     }
                 }
@@ -99,7 +103,10 @@ impl AccessControl {
                 }
                 if let Some(authorization) = authorization {
                     if let Some(Account { user, pass }) = &control.readonly {
-                        if valid_digest(authorization, method.as_str(), user, pass).is_some() {
+                        if auth_method
+                            .validate(authorization, method.as_str(), user, pass)
+                            .is_some()
+                        {
                             return GuardType::ReadOnly;
                         }
                     }
@@ -167,87 +174,127 @@ impl Account {
     }
 }
 
-pub fn generate_www_auth(stale: bool) -> String {
-    let str_stale = if stale { "stale=true," } else { "" };
-    format!(
-        "Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
-        REALM,
-        create_nonce(),
-        str_stale
-    )
+#[derive(Debug, Clone)]
+pub enum AuthMethod {
+    Basic,
+    Digest,
 }
 
-pub fn valid_digest(
-    authorization: &HeaderValue,
-    method: &str,
-    auth_user: &str,
-    auth_pass: &str,
-) -> Option<()> {
-    let digest_value = strip_prefix(authorization.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())
-                });
+impl AuthMethod {
+    pub fn www_auth(&self, stale: bool) -> String {
+        match self {
+            AuthMethod::Basic => {
+                format!("Basic realm=\"{}\"", REALM)
             }
-        }
-        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())
+            AuthMethod::Digest => {
+                let str_stale = if stale { "stale=true," } else { "" };
+                format!(
+                    "Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
+                    REALM,
+                    create_nonce(),
+                    str_stale
+                )
+            }
+        }
+    }
+    pub fn validate(
+        &self,
+        authorization: &HeaderValue,
+        method: &str,
+        auth_user: &str,
+        auth_pass: &str,
+    ) -> Option<()> {
+        match self {
+            AuthMethod::Basic => {
+                let value: Vec<u8> =
+                    base64::decode(strip_prefix(authorization.as_bytes(), b"Basic ").unwrap())
+                        .unwrap();
+                let parts: Vec<&str> = std::str::from_utf8(&value).unwrap().split(':').collect();
+
+                if parts[0] != auth_user {
+                    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 {
+                    return Some(());
+                }
+
+                None
+            }
+            AuthMethod::Digest => {
+                let digest_value = strip_prefix(authorization.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
             }
-        };
-        if correct_response.as_bytes() == *user_response {
-            // grant access
-            return Some(());
         }
     }
-    None
 }
 
 /// Check if a nonce is still valid.
diff --git a/src/server.rs b/src/server.rs
index 2c72ab9..4620b79 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,4 +1,3 @@
-use crate::auth::generate_www_auth;
 use crate::streamer::Streamer;
 use crate::utils::{decode_uri, encode_uri};
 use crate::{Args, BoxResult};
@@ -96,7 +95,12 @@ impl Server {
         }
 
         let authorization = headers.get(AUTHORIZATION);
-        let guard_type = self.args.auth.guard(req_path, &method, authorization);
+        let guard_type = self.args.auth.guard(
+            req_path,
+            &method,
+            authorization,
+            self.args.auth_method.clone(),
+        );
         if guard_type.is_reject() {
             self.auth_reject(&mut res);
             return Ok(res);
@@ -720,7 +724,7 @@ const DATA =
     }
 
     fn auth_reject(&self, res: &mut Response) {
-        let value = generate_www_auth(false);
+        let value = self.args.auth_method.www_auth(false);
         set_webdav_headers(res);
         res.headers_mut().typed_insert(Connection::close());
         res.headers_mut()
diff --git a/tests/auth.rs b/tests/auth.rs
index e95c239..c1fe0e7 100644
--- a/tests/auth.rs
+++ b/tests/auth.rs
@@ -80,3 +80,18 @@ fn auth_nest_share(
     assert_eq!(resp.status(), 200);
     Ok(())
 }
+
+#[rstest]
+fn auth_basic(
+    #[with(&["--auth", "/@user:pass", "--auth-method", "basic", "-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);
+    let resp = fetch!(b"PUT", &url)
+        .body(b"abc".to_vec())
+        .basic_auth("user", Some("pass"))
+        .send()?;
+    assert_eq!(resp.status(), 201);
+    Ok(())
+}