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; @@ -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;
@ -48,10 +49,12 @@ fn app() -> Command<'static> { @@ -48,10 +49,12 @@ fn app() -> Command<'static> {
.help("Specify an url path prefix"),
)
.arg(
Arg::new("basic-auth")
.short('B')
.long("basic-auth")
.help("Use HTTP basic auth instead of digest auth"),
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")
@ -129,7 +132,7 @@ pub struct Args { @@ -129,7 +132,7 @@ pub struct Args {
pub path_is_file: bool,
pub path_prefix: String,
pub uri_prefix: String,
pub basic_auth: bool,
pub auth_method: AuthMethod,
pub auth: AccessControl,
pub allow_upload: bool,
pub allow_delete: bool,
@ -169,7 +172,11 @@ impl Args { @@ -169,7 +172,11 @@ impl Args {
.values_of("auth")
.map(|v| v.collect())
.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 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");
@ -193,7 +200,7 @@ impl Args { @@ -193,7 +200,7 @@ impl Args {
path_is_file,
path_prefix,
uri_prefix,
basic_auth,
auth_method,
auth,
enable_cors,
allow_delete,

224
src/auth.rs

@ -76,7 +76,7 @@ impl AccessControl { @@ -76,7 +76,7 @@ impl AccessControl {
path: &str,
method: &Method,
authorization: Option<&HeaderValue>,
basic_auth: bool,
auth_method: AuthMethod,
) -> GuardType {
if self.rules.is_empty() {
return GuardType::ReadWrite;
@ -87,11 +87,10 @@ impl AccessControl { @@ -87,11 +87,10 @@ impl AccessControl {
controls.push(control);
if let Some(authorization) = authorization {
let Account { user, pass } = &control.readwrite;
if basic_auth {
if valid_basic_auth(authorization, user, pass).is_some() {
return GuardType::ReadWrite;
}
} else 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;
}
}
@ -104,11 +103,10 @@ impl AccessControl { @@ -104,11 +103,10 @@ impl AccessControl {
}
if let Some(authorization) = authorization {
if let Some(Account { user, pass }) = &control.readonly {
if basic_auth {
if valid_basic_auth(authorization, user, pass).is_some() {
return GuardType::ReadOnly;
}
} else 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;
}
}
@ -176,115 +174,127 @@ impl Account { @@ -176,115 +174,127 @@ impl Account {
}
}
pub fn generate_www_auth(stale: bool, basic_auth: bool) -> String {
if basic_auth {
format!("Basic realm=\"{}\"", REALM)
} else {
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_basic_auth(
authorization: &HeaderValue,
auth_user: &str,
auth_pass: &str,
) -> Option<()> {
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;
impl AuthMethod {
pub fn www_auth(&self, stale: bool) -> String {
match self {
AuthMethod::Basic => {
format!("Basic realm=\"{}\"", REALM)
}
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();
let mut h = Context::new();
h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes());
if parts[0] != auth_user {
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 {
return Some(());
}
let http_pass = format!("{:x}", h.compute());
return None;
}
if http_pass == auth_pass {
return Some(());
}
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);
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,
}
c.consume(b":");
if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) {
c.consume(cnonce);
if auth_user != username {
return None;
}
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())
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.

10
src/server.rs

@ -1,4 +1,3 @@ @@ -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 { @@ -96,7 +95,12 @@ impl Server {
}
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() {
self.auth_reject(&mut res);
return Ok(res);
@ -720,7 +724,7 @@ const DATA = @@ -720,7 +724,7 @@ const DATA =
}
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);
res.headers_mut().typed_insert(Connection::close());
res.headers_mut()

92
tests/auth.rs

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