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
This commit is contained in:
Joe Koop
2022-06-19 22:25:09 -05:00
committed by GitHub
parent 0d3acb8ae6
commit deb6365a28
5 changed files with 175 additions and 89 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -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()