feat: use digest auth (#14)
* feat: switch to digest auth * implement digest auth * cargo fmt * no lock
This commit is contained in:
parent
05155aa532
commit
2f40313a54
7 changed files with 316 additions and 24 deletions
66
Cargo.lock
generated
66
Cargo.lock
generated
|
@ -297,6 +297,8 @@ dependencies = [
|
|||
"get_if_addrs",
|
||||
"headers",
|
||||
"hyper",
|
||||
"lazy_static",
|
||||
"md5",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
|
@ -307,6 +309,7 @@ dependencies = [
|
|||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -484,6 +487,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
|
@ -656,6 +670,12 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
|
@ -770,6 +790,12 @@ version = "0.3.25"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
|
@ -788,6 +814,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
|
@ -1098,6 +1154,16 @@ version = "0.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
|
|
@ -31,6 +31,9 @@ mime_guess = "2.0.4"
|
|||
get_if_addrs = "0.5.3"
|
||||
rustls = { version = "0.20", default-features = false, features = ["tls12"] }
|
||||
rustls-pemfile = "1"
|
||||
md5 = "0.7.0"
|
||||
lazy_static = "1.4.0"
|
||||
uuid = { version = "1.1.1", features = ["v4", "fast-rng"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -13,8 +13,8 @@ Duf is a simple file server. Support static serve, search, upload, webdav...
|
|||
- Download folder as zip file
|
||||
- Upload files and folders (Drag & Drop)
|
||||
- Search files
|
||||
- Basic authentication
|
||||
- Partial responses (Parallel/Resume download)
|
||||
- Authentication
|
||||
- Support https
|
||||
- Support webdav
|
||||
- Easy to use with curl
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::net::SocketAddr;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs, io};
|
||||
|
||||
use crate::auth::parse_auth;
|
||||
use crate::BoxResult;
|
||||
|
||||
const ABOUT: &str = concat!("\n", crate_description!()); // Add extra newline.
|
||||
|
@ -115,7 +116,7 @@ pub struct Args {
|
|||
pub path: PathBuf,
|
||||
pub path_prefix: String,
|
||||
pub uri_prefix: String,
|
||||
pub auth: Option<String>,
|
||||
pub auth: Option<(String, String)>,
|
||||
pub no_auth_access: bool,
|
||||
pub allow_upload: bool,
|
||||
pub allow_delete: bool,
|
||||
|
@ -145,7 +146,10 @@ impl Args {
|
|||
format!("/{}/", &path_prefix)
|
||||
};
|
||||
let cors = matches.is_present("cors");
|
||||
let auth = matches.value_of("auth").map(|v| v.to_owned());
|
||||
let auth = match matches.value_of("auth") {
|
||||
Some(auth) => Some(parse_auth(auth)?),
|
||||
None => None,
|
||||
};
|
||||
let no_auth_access = matches.is_present("no-auth-access");
|
||||
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");
|
||||
|
|
209
src/auth.rs
Normal file
209
src/auth.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use headers::HeaderValue;
|
||||
use lazy_static::lazy_static;
|
||||
use md5::Context;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::BoxResult;
|
||||
|
||||
const REALM: &str = "DUF";
|
||||
|
||||
lazy_static! {
|
||||
static ref NONCESTARTHASH: Context = {
|
||||
let mut h = Context::new();
|
||||
h.consume(Uuid::new_v4().as_bytes());
|
||||
h.consume(std::process::id().to_be_bytes());
|
||||
h
|
||||
};
|
||||
}
|
||||
|
||||
pub fn generate_www_auth(stale: bool) -> String {
|
||||
let str_stale = if stale { "stale=true," } else { "" };
|
||||
format!(
|
||||
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\",algorithm=\"MD5\"",
|
||||
REALM,
|
||||
create_nonce(),
|
||||
str_stale
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse_auth(auth: &str) -> BoxResult<(String, String)> {
|
||||
let p: Vec<&str> = auth.trim().split(':').collect();
|
||||
let err = "Invalid auth value";
|
||||
if p.len() != 2 {
|
||||
return Err(err.into());
|
||||
}
|
||||
let user = p[0];
|
||||
let pass = p[1];
|
||||
let mut h = Context::new();
|
||||
h.consume(format!("{}:{}:{}", user, REALM, pass).as_bytes());
|
||||
Ok((user.to_owned(), format!("{:x}", h.compute())))
|
||||
}
|
||||
|
||||
pub fn valid_digest(
|
||||
header_value: &HeaderValue,
|
||||
method: &str,
|
||||
auth_user: &str,
|
||||
auth_pass: &str,
|
||||
) -> Option<()> {
|
||||
let digest_value = strip_prefix(header_value.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
|
||||
}
|
||||
|
||||
/// Check if a nonce is still valid.
|
||||
/// Return an error if it was never valid
|
||||
fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
|
||||
if nonce.len() != 34 {
|
||||
return Err(());
|
||||
}
|
||||
//parse hex
|
||||
if let Ok(n) = std::str::from_utf8(nonce) {
|
||||
//get time
|
||||
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
|
||||
//check time
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let secs_now = now.as_secs() as u32;
|
||||
|
||||
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
|
||||
//check hash
|
||||
let mut h = NONCESTARTHASH.clone();
|
||||
h.consume(secs_nonce.to_be_bytes());
|
||||
let h = format!("{:x}", h.compute());
|
||||
if h[..26] == n[8..34] {
|
||||
return Ok(dur < 300); // from the last 5min
|
||||
//Authentication-Info ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
||||
let l = prefix.len();
|
||||
if search.len() < l {
|
||||
return None;
|
||||
}
|
||||
if &search[..l] == prefix {
|
||||
Some(&search[l..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
|
||||
let mut sep = Vec::new();
|
||||
let mut asign = Vec::new();
|
||||
let mut i: usize = 0;
|
||||
let mut esc = false;
|
||||
for c in header {
|
||||
match (c, esc) {
|
||||
(b'=', false) => asign.push(i),
|
||||
(b',', false) => sep.push(i),
|
||||
(b'"', false) => esc = true,
|
||||
(b'"', true) => esc = false,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
sep.push(i); // same len for both Vecs
|
||||
|
||||
i = 0;
|
||||
let mut ret = HashMap::new();
|
||||
for (&k, &a) in sep.iter().zip(asign.iter()) {
|
||||
while header[i] == b' ' {
|
||||
i += 1;
|
||||
}
|
||||
if a <= i || k <= 1 + a {
|
||||
//keys and vals must contain one char
|
||||
return Err(());
|
||||
}
|
||||
let key = &header[i..a];
|
||||
let val = if header[1 + a] == b'"' && header[k - 1] == b'"' {
|
||||
//escaped
|
||||
&header[2 + a..k - 1]
|
||||
} else {
|
||||
//not escaped
|
||||
&header[1 + a..k]
|
||||
};
|
||||
i = 1 + k;
|
||||
ret.insert(key, val);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn create_nonce() -> String {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let secs = now.as_secs() as u32;
|
||||
let mut h = NONCESTARTHASH.clone();
|
||||
h.consume(secs.to_be_bytes());
|
||||
|
||||
let n = format!("{:08x}{:032x}", secs, h.compute());
|
||||
n[..34].to_string()
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod args;
|
||||
mod auth;
|
||||
mod server;
|
||||
|
||||
pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::auth::{generate_www_auth, valid_digest};
|
||||
use crate::{Args, BoxResult};
|
||||
|
||||
use async_walkdir::WalkDir;
|
||||
|
@ -34,6 +35,7 @@ use tokio::{fs, io};
|
|||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use tokio_util::io::{ReaderStream, StreamReader};
|
||||
use uuid::Uuid;
|
||||
|
||||
type Request = hyper::Request<Body>;
|
||||
type Response = hyper::Response<Body>;
|
||||
|
@ -364,7 +366,10 @@ impl InnerService {
|
|||
|
||||
async fn handle_zip_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.ok_or_else(|| format!("Failed to get name of `{}`", path.display()))?;
|
||||
let path = path.to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = zip_dir(&mut writer, &path).await {
|
||||
|
@ -509,7 +514,7 @@ impl InnerService {
|
|||
return Ok(());
|
||||
}
|
||||
},
|
||||
None => 1,
|
||||
None => 0,
|
||||
};
|
||||
let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
|
||||
if depth > 0 {
|
||||
|
@ -598,22 +603,26 @@ impl InnerService {
|
|||
}
|
||||
|
||||
async fn handle_lock(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
|
||||
let now = Utc::now().timestamp();
|
||||
let token = if self.args.auth.is_none() {
|
||||
Utc::now().timestamp().to_string()
|
||||
} else {
|
||||
format!("opaquelocktoken:{}", Uuid::new_v4())
|
||||
};
|
||||
|
||||
res.headers_mut().insert(
|
||||
"content-type",
|
||||
"application/xml; charset=utf-8".parse().unwrap(),
|
||||
);
|
||||
res.headers_mut()
|
||||
.insert("lock-token", format!("<{}>", now).parse().unwrap());
|
||||
.insert("lock-token", format!("<{}>", token).parse().unwrap());
|
||||
|
||||
*res.body_mut() = Body::from(format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:prop xmlns:D="DAV:"><D:lockdiscovery><D:activelock>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktoken><D:href>{}</D:href></D:locktoken>
|
||||
<D:lockroot><D:href>{}</D:href></D:lockroot>
|
||||
</D:activelock></D:lockdiscovery></D:prop>"#,
|
||||
now, req_path
|
||||
token, req_path
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
@ -656,29 +665,29 @@ impl InnerService {
|
|||
}
|
||||
|
||||
fn auth_guard(&self, req: &Request, res: &mut Response) -> bool {
|
||||
let method = req.method();
|
||||
let pass = {
|
||||
match &self.args.auth {
|
||||
None => true,
|
||||
Some(auth) => match req.headers().get(AUTHORIZATION) {
|
||||
Some(value) => match value.to_str().ok().map(|v| {
|
||||
let mut it = v.split(' ');
|
||||
(it.next(), it.next())
|
||||
}) {
|
||||
Some((Some("Basic"), Some(tail))) => base64::decode(tail)
|
||||
.ok()
|
||||
.and_then(|v| String::from_utf8(v).ok())
|
||||
.map(|v| v.as_str() == auth)
|
||||
.unwrap_or_default(),
|
||||
_ => false,
|
||||
},
|
||||
None => self.args.no_auth_access && req.method() == Method::GET,
|
||||
Some((user, pass)) => match req.headers().get(AUTHORIZATION) {
|
||||
Some(value) => {
|
||||
valid_digest(value, method.as_str(), user.as_str(), pass.as_str()).is_some()
|
||||
}
|
||||
None => {
|
||||
self.args.no_auth_access
|
||||
&& (method == Method::GET
|
||||
|| method == Method::OPTIONS
|
||||
|| method == Method::HEAD
|
||||
|| method.as_str() == "PROPFIND")
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
if !pass {
|
||||
let value = generate_www_auth(false);
|
||||
status!(res, StatusCode::UNAUTHORIZED);
|
||||
res.headers_mut()
|
||||
.insert(WWW_AUTHENTICATE, HeaderValue::from_static("Basic"));
|
||||
.insert(WWW_AUTHENTICATE, value.parse().unwrap());
|
||||
}
|
||||
pass
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue