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 = + 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(()) +}