Browse Source

implemented sigoden's suggestions

pull/60/head
Joe Koop 3 years ago
parent
commit
e799117959
No known key found for this signature in database
GPG Key ID: B2D0C6242D5AC1FF
  1. 21
      src/args.rs
  2. 224
      src/auth.rs
  3. 10
      src/server.rs
  4. 92
      tests/auth.rs

21
src/args.rs

@ -5,6 +5,7 @@ use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::auth::AccessControl; use crate::auth::AccessControl;
use crate::auth::AuthMethod;
use crate::tls::{load_certs, load_private_key}; use crate::tls::{load_certs, load_private_key};
use crate::BoxResult; use crate::BoxResult;
@ -48,10 +49,12 @@ fn app() -> Command<'static> {
.help("Specify an url path prefix"), .help("Specify an url path prefix"),
) )
.arg( .arg(
Arg::new("basic-auth") Arg::new("auth-method")
.short('B') .long("auth-method")
.long("basic-auth") .help("Choose auth method")
.help("Use HTTP basic auth instead of digest auth"), .possible_values(["basic", "digest"])
.default_value("digest")
.value_name("value"),
) )
.arg( .arg(
Arg::new("auth") Arg::new("auth")
@ -129,7 +132,7 @@ pub struct Args {
pub path_is_file: bool, pub path_is_file: bool,
pub path_prefix: String, pub path_prefix: String,
pub uri_prefix: String, pub uri_prefix: String,
pub basic_auth: bool, pub auth_method: AuthMethod,
pub auth: AccessControl, pub auth: AccessControl,
pub allow_upload: bool, pub allow_upload: bool,
pub allow_delete: bool, pub allow_delete: bool,
@ -169,7 +172,11 @@ impl Args {
.values_of("auth") .values_of("auth")
.map(|v| v.collect()) .map(|v| v.collect())
.unwrap_or_default(); .unwrap_or_default();
let basic_auth = matches.is_present("basic-auth"); let auth_method = match matches.value_of("auth-method").unwrap() {
"basic" => AuthMethod::Basic,
"digest" => AuthMethod::Digest,
_ => todo!(),
};
let auth = AccessControl::new(&auth, &uri_prefix)?; let auth = AccessControl::new(&auth, &uri_prefix)?;
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload"); 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"); let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
@ -193,7 +200,7 @@ impl Args {
path_is_file, path_is_file,
path_prefix, path_prefix,
uri_prefix, uri_prefix,
basic_auth, auth_method,
auth, auth,
enable_cors, enable_cors,
allow_delete, allow_delete,

224
src/auth.rs

@ -76,7 +76,7 @@ impl AccessControl {
path: &str, path: &str,
method: &Method, method: &Method,
authorization: Option<&HeaderValue>, authorization: Option<&HeaderValue>,
basic_auth: bool, auth_method: AuthMethod,
) -> GuardType { ) -> GuardType {
if self.rules.is_empty() { if self.rules.is_empty() {
return GuardType::ReadWrite; return GuardType::ReadWrite;
@ -87,11 +87,10 @@ impl AccessControl {
controls.push(control); controls.push(control);
if let Some(authorization) = authorization { if let Some(authorization) = authorization {
let Account { user, pass } = &control.readwrite; let Account { user, pass } = &control.readwrite;
if basic_auth { if auth_method
if valid_basic_auth(authorization, user, pass).is_some() { .validate(authorization, method.as_str(), user, pass)
return GuardType::ReadWrite; .is_some()
} {
} else if valid_digest(authorization, method.as_str(), user, pass).is_some() {
return GuardType::ReadWrite; return GuardType::ReadWrite;
} }
} }
@ -104,11 +103,10 @@ impl AccessControl {
} }
if let Some(authorization) = authorization { if let Some(authorization) = authorization {
if let Some(Account { user, pass }) = &control.readonly { if let Some(Account { user, pass }) = &control.readonly {
if basic_auth { if auth_method
if valid_basic_auth(authorization, user, pass).is_some() { .validate(authorization, method.as_str(), user, pass)
return GuardType::ReadOnly; .is_some()
} {
} else if valid_digest(authorization, method.as_str(), user, pass).is_some() {
return GuardType::ReadOnly; return GuardType::ReadOnly;
} }
} }
@ -176,115 +174,127 @@ impl Account {
} }
} }
pub fn generate_www_auth(stale: bool, basic_auth: bool) -> String { #[derive(Debug, Clone)]
if basic_auth { pub enum AuthMethod {
format!("Basic realm=\"{}\"", REALM) Basic,
} else { Digest,
let str_stale = if stale { "stale=true," } else { "" };
format!(
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
REALM,
create_nonce(),
str_stale
)
}
} }
pub fn valid_basic_auth( impl AuthMethod {
authorization: &HeaderValue, pub fn www_auth(&self, stale: bool) -> String {
auth_user: &str, match self {
auth_pass: &str, AuthMethod::Basic => {
) -> Option<()> { format!("Basic realm=\"{}\"", REALM)
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(); AuthMethod::Digest => {
let str_stale = if stale { "stale=true," } else { "" };
if parts[0] != auth_user { format!(
return None; "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();
let mut h = Context::new(); if parts[0] != auth_user {
h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes()); return None;
}
let http_pass = format!("{:x}", h.compute()); let mut h = Context::new();
h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes());
if http_pass == auth_pass { let http_pass = format!("{:x}", h.compute());
return Some(());
}
return None; if http_pass == auth_pass {
} return Some(());
}
pub fn valid_digest( None
authorization: &HeaderValue, }
method: &str, AuthMethod::Digest => {
auth_user: &str, let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
auth_pass: &str, let user_vals = to_headermap(digest_value).ok()?;
) -> Option<()> { if let (Some(username), Some(nonce), Some(user_response)) = (
let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?; user_vals
let user_vals = to_headermap(digest_value).ok()?; .get(b"username".as_ref())
if let (Some(username), Some(nonce), Some(user_response)) = ( .and_then(|b| std::str::from_utf8(*b).ok()),
user_vals user_vals.get(b"nonce".as_ref()),
.get(b"username".as_ref()) user_vals.get(b"response".as_ref()),
.and_then(|b| std::str::from_utf8(*b).ok()), ) {
user_vals.get(b"nonce".as_ref()), match validate_nonce(nonce) {
user_vals.get(b"response".as_ref()), Ok(true) => {}
) { _ => return None,
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 auth_user != username {
if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) { return None;
c.consume(cnonce);
} }
c.consume(b":"); let mut ha = Context::new();
c.consume(qop); ha.consume(method);
c.consume(b":"); ha.consume(b":");
c.consume(&*ha); if let Some(uri) = user_vals.get(b"uri".as_ref()) {
format!("{:x}", c.compute()) ha.consume(uri);
}); }
} let ha = format!("{:x}", ha.compute());
} let mut correct_response = None;
let correct_response = match correct_response { if let Some(qop) = user_vals.get(b"qop".as_ref()) {
Some(r) => r, if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
None => { correct_response = Some({
let mut c = Context::new(); let mut c = Context::new();
c.consume(&auth_pass); c.consume(&auth_pass);
c.consume(b":"); c.consume(b":");
c.consume(nonce); c.consume(nonce);
c.consume(b":"); c.consume(b":");
c.consume(&*ha); if let Some(nc) = user_vals.get(b"nc".as_ref()) {
format!("{:x}", c.compute()) 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. /// Check if a nonce is still valid.

10
src/server.rs

@ -1,4 +1,3 @@
use crate::auth::generate_www_auth;
use crate::streamer::Streamer; use crate::streamer::Streamer;
use crate::utils::{decode_uri, encode_uri}; use crate::utils::{decode_uri, encode_uri};
use crate::{Args, BoxResult}; use crate::{Args, BoxResult};
@ -96,7 +95,12 @@ impl Server {
} }
let authorization = headers.get(AUTHORIZATION); let authorization = headers.get(AUTHORIZATION);
let guard_type = self.args.auth.guard(req_path, &method, authorization, self.args.basic_auth); let guard_type = self.args.auth.guard(
req_path,
&method,
authorization,
self.args.auth_method.clone(),
);
if guard_type.is_reject() { if guard_type.is_reject() {
self.auth_reject(&mut res); self.auth_reject(&mut res);
return Ok(res); return Ok(res);
@ -720,7 +724,7 @@ const DATA =
} }
fn auth_reject(&self, res: &mut Response) { fn auth_reject(&self, res: &mut Response) {
let value = generate_www_auth(false, self.args.basic_auth); let value = self.args.auth_method.www_auth(false);
set_webdav_headers(res); set_webdav_headers(res);
res.headers_mut().typed_insert(Connection::close()); res.headers_mut().typed_insert(Connection::close());
res.headers_mut() res.headers_mut()

92
tests/auth.rs

@ -6,7 +6,7 @@ use fixtures::{server, Error, TestServer};
use rstest::rstest; use rstest::rstest;
#[rstest] #[rstest]
fn no_auth_digest(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
assert!(resp.headers().contains_key("www-authenticate")); assert!(resp.headers().contains_key("www-authenticate"));
@ -17,7 +17,7 @@ fn no_auth_digest(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer)
} }
#[rstest] #[rstest]
fn auth_digest(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
@ -29,14 +29,14 @@ fn auth_digest(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) ->
} }
#[rstest] #[rstest]
fn auth_skip_digest(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> { fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
} }
#[rstest] #[rstest]
fn auth_readonly_digest( fn auth_readonly(
#[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer, #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
@ -53,7 +53,7 @@ fn auth_readonly_digest(
} }
#[rstest] #[rstest]
fn auth_nest_digest( fn auth_nest(
#[with(&["--auth", "/@user:pass@user2:pass2", "--auth", "/dira@user3:pass3", "-A"])] #[with(&["--auth", "/@user:pass@user2:pass2", "--auth", "/dira@user3:pass3", "-A"])]
server: TestServer, server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -72,7 +72,7 @@ fn auth_nest_digest(
} }
#[rstest] #[rstest]
fn auth_nest_share_digest( fn auth_nest_share(
#[with(&["--auth", "/@user:pass@*", "--auth", "/dira@user3:pass3", "-A"])] server: TestServer, #[with(&["--auth", "/@user:pass@*", "--auth", "/dira@user3:pass3", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
@ -80,83 +80,3 @@ fn auth_nest_share_digest(
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
} }
#[rstest]
fn no_auth_basic(#[with(&["--basic-auth", "--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 401);
assert!(resp.headers().contains_key("www-authenticate"));
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401);
Ok(())
}
#[rstest]
fn auth_basic(#[with(&["--basic-auth", "--auth", "/@user:pass", "-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(())
}
#[rstest]
fn auth_skip_basic(#[with(&["--basic-auth", "--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn auth_readonly_basic(
#[with(&["--basic-auth", "--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", &url).basic_auth("user2", Some("pass2")).send()?;
assert_eq!(resp.status(), 200);
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec())
.basic_auth("user2", Some("pass2"))
.send()?;
assert_eq!(resp.status(), 401);
Ok(())
}
#[rstest]
fn auth_nest_basic(
#[with(&["--basic-auth", "--auth", "/@user:pass@user2:pass2", "--auth", "/dira@user3:pass3", "-A"])]
server: TestServer,
) -> Result<(), Error> {
let url = format!("{}dira/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("user3", Some("pass3"))
.send()?;
assert_eq!(resp.status(), 201);
let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec())
.basic_auth("user", Some("pass"))
.send()?;
assert_eq!(resp.status(), 201);
Ok(())
}
#[rstest]
fn auth_nest_share_basic(
#[with(&["--basic-auth", "--auth", "/@user:pass@*", "--auth", "/dira@user3:pass3", "-A"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 200);
Ok(())
}

Loading…
Cancel
Save