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:
parent
0d3acb8ae6
commit
deb6365a28
5 changed files with 175 additions and 89 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
src/args.rs
15
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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
|
201
src/auth.rs
201
src/auth.rs
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue