Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deprecate --auth-method, as both options are available #279

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down
8 changes: 1 addition & 7 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -329,7 +324,6 @@ impl Args {
path_prefix,
uri_prefix,
hidden,
auth_method,
auth,
enable_cors,
allow_delete,
Expand Down
239 changes: 107 additions & 132 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 5 additions & 7 deletions src/log_http.rs
Original file line number Diff line number Diff line change
@@ -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"#;

Expand All @@ -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 {
Expand All @@ -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);
}
Expand Down