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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 89 deletions

View file

@ -1,9 +1,14 @@
html { html {
font-family: -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif; font-family: -apple-system,BlinkMacSystemFont,Roboto,Helvetica,Arial,sans-serif;
line-height: 1.5; line-height: 1.5;
color: #24292e; color: #24292e;
} }
body {
/* prevent premature breadcrumb wrapping on mobile */
min-width: 500px;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -49,6 +54,11 @@ html {
margin-right: 10px; margin-right: 10px;
} }
.toolbox > div {
/* vertically align with breadcrumb text */
height: 1.1rem;
}
.searchbar { .searchbar {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -116,11 +126,6 @@ html {
white-space: nowrap; white-space: nowrap;
} }
.uploaders-table .cell-name,
.paths-table .cell-name {
width: 500px;
}
.uploaders-table .cell-status { .uploaders-table .cell-status {
width: 80px; width: 80px;
padding-left: 0.6em; padding-left: 0.6em;
@ -143,7 +148,6 @@ html {
padding-left: 0.6em; padding-left: 0.6em;
} }
.path svg { .path svg {
height: 100%; height: 100%;
fill: rgba(3,47,98,0.5); fill: rgba(3,47,98,0.5);
@ -163,7 +167,7 @@ html {
display: block; display: block;
text-decoration: none; text-decoration: none;
max-width: calc(100vw - 375px); max-width: calc(100vw - 375px);
min-width: 400px; min-width: 200px;
} }
.path a:hover { .path a:hover {
@ -200,7 +204,8 @@ html {
} }
svg, svg,
.path svg { .path svg,
.breadcrumb svg {
fill: #fff; fill: #fff;
} }

View file

@ -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;
@ -47,6 +48,14 @@ fn app() -> Command<'static> {
.value_name("path") .value_name("path")
.help("Specify an url path prefix"), .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(
Arg::new("auth") Arg::new("auth")
.short('a') .short('a')
@ -123,6 +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 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,
@ -162,6 +172,10 @@ impl Args {
.values_of("auth") .values_of("auth")
.map(|v| v.collect()) .map(|v| v.collect())
.unwrap_or_default(); .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 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");
@ -185,6 +199,7 @@ impl Args {
path_is_file, path_is_file,
path_prefix, path_prefix,
uri_prefix, uri_prefix,
auth_method,
auth, auth,
enable_cors, enable_cors,
allow_delete, allow_delete,

View file

@ -76,6 +76,7 @@ impl AccessControl {
path: &str, path: &str,
method: &Method, method: &Method,
authorization: Option<&HeaderValue>, authorization: Option<&HeaderValue>,
auth_method: AuthMethod,
) -> GuardType { ) -> GuardType {
if self.rules.is_empty() { if self.rules.is_empty() {
return GuardType::ReadWrite; return GuardType::ReadWrite;
@ -86,7 +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 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; return GuardType::ReadWrite;
} }
} }
@ -99,7 +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 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; return GuardType::ReadOnly;
} }
} }
@ -167,87 +174,127 @@ impl Account {
} }
} }
pub fn generate_www_auth(stale: bool) -> String { #[derive(Debug, Clone)]
let str_stale = if stale { "stale=true," } else { "" }; pub enum AuthMethod {
format!( Basic,
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"", Digest,
REALM,
create_nonce(),
str_stale
)
} }
pub fn valid_digest( impl AuthMethod {
authorization: &HeaderValue, pub fn www_auth(&self, stale: bool) -> String {
method: &str, match self {
auth_user: &str, AuthMethod::Basic => {
auth_pass: &str, format!("Basic realm=\"{}\"", REALM)
) -> 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())
});
} }
} AuthMethod::Digest => {
let correct_response = match correct_response { let str_stale = if stale { "stale=true," } else { "" };
Some(r) => r, format!(
None => { "Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
let mut c = Context::new(); REALM,
c.consume(&auth_pass); create_nonce(),
c.consume(b":"); str_stale
c.consume(nonce); )
c.consume(b":"); }
c.consume(&*ha); }
format!("{:x}", c.compute()) }
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. /// 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::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); 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); 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()

View file

@ -80,3 +80,18 @@ fn auth_nest_share(
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) 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(())
}