Skip to content

Commit 2f40313

Browse files
authored
feat: use digest auth (#14)
* feat: switch to digest auth * implement digest auth * cargo fmt * no lock
1 parent 05155aa commit 2f40313

7 files changed

Lines changed: 316 additions & 24 deletions

File tree

Cargo.lock

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ mime_guess = "2.0.4"
3131
get_if_addrs = "0.5.3"
3232
rustls = { version = "0.20", default-features = false, features = ["tls12"] }
3333
rustls-pemfile = "1"
34+
md5 = "0.7.0"
35+
lazy_static = "1.4.0"
36+
uuid = { version = "1.1.1", features = ["v4", "fast-rng"] }
3437

3538
[profile.release]
3639
lto = true

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Duf is a simple file server. Support static serve, search, upload, webdav...
1313
- Download folder as zip file
1414
- Upload files and folders (Drag & Drop)
1515
- Search files
16-
- Basic authentication
1716
- Partial responses (Parallel/Resume download)
17+
- Authentication
1818
- Support https
1919
- Support webdav
2020
- Easy to use with curl

src/args.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::net::SocketAddr;
55
use std::path::{Path, PathBuf};
66
use std::{env, fs, io};
77

8+
use crate::auth::parse_auth;
89
use crate::BoxResult;
910

1011
const ABOUT: &str = concat!("\n", crate_description!()); // Add extra newline.
@@ -115,7 +116,7 @@ pub struct Args {
115116
pub path: PathBuf,
116117
pub path_prefix: String,
117118
pub uri_prefix: String,
118-
pub auth: Option<String>,
119+
pub auth: Option<(String, String)>,
119120
pub no_auth_access: bool,
120121
pub allow_upload: bool,
121122
pub allow_delete: bool,
@@ -145,7 +146,10 @@ impl Args {
145146
format!("/{}/", &path_prefix)
146147
};
147148
let cors = matches.is_present("cors");
148-
let auth = matches.value_of("auth").map(|v| v.to_owned());
149+
let auth = match matches.value_of("auth") {
150+
Some(auth) => Some(parse_auth(auth)?),
151+
None => None,
152+
};
149153
let no_auth_access = matches.is_present("no-auth-access");
150154
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
151155
let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");

src/auth.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
use headers::HeaderValue;
2+
use lazy_static::lazy_static;
3+
use md5::Context;
4+
use std::{
5+
collections::HashMap,
6+
time::{SystemTime, UNIX_EPOCH},
7+
};
8+
use uuid::Uuid;
9+
10+
use crate::BoxResult;
11+
12+
const REALM: &str = "DUF";
13+
14+
lazy_static! {
15+
static ref NONCESTARTHASH: Context = {
16+
let mut h = Context::new();
17+
h.consume(Uuid::new_v4().as_bytes());
18+
h.consume(std::process::id().to_be_bytes());
19+
h
20+
};
21+
}
22+
23+
pub fn generate_www_auth(stale: bool) -> String {
24+
let str_stale = if stale { "stale=true," } else { "" };
25+
format!(
26+
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\",algorithm=\"MD5\"",
27+
REALM,
28+
create_nonce(),
29+
str_stale
30+
)
31+
}
32+
33+
pub fn parse_auth(auth: &str) -> BoxResult<(String, String)> {
34+
let p: Vec<&str> = auth.trim().split(':').collect();
35+
let err = "Invalid auth value";
36+
if p.len() != 2 {
37+
return Err(err.into());
38+
}
39+
let user = p[0];
40+
let pass = p[1];
41+
let mut h = Context::new();
42+
h.consume(format!("{}:{}:{}", user, REALM, pass).as_bytes());
43+
Ok((user.to_owned(), format!("{:x}", h.compute())))
44+
}
45+
46+
pub fn valid_digest(
47+
header_value: &HeaderValue,
48+
method: &str,
49+
auth_user: &str,
50+
auth_pass: &str,
51+
) -> Option<()> {
52+
let digest_value = strip_prefix(header_value.as_bytes(), b"Digest ")?;
53+
let user_vals = to_headermap(digest_value).ok()?;
54+
if let (Some(username), Some(nonce), Some(user_response)) = (
55+
user_vals
56+
.get(b"username".as_ref())
57+
.and_then(|b| std::str::from_utf8(*b).ok()),
58+
user_vals.get(b"nonce".as_ref()),
59+
user_vals.get(b"response".as_ref()),
60+
) {
61+
match validate_nonce(nonce) {
62+
Ok(true) => {}
63+
_ => return None,
64+
}
65+
if auth_user != username {
66+
return None;
67+
}
68+
let mut ha = Context::new();
69+
ha.consume(method);
70+
ha.consume(b":");
71+
if let Some(uri) = user_vals.get(b"uri".as_ref()) {
72+
ha.consume(uri);
73+
}
74+
let ha = format!("{:x}", ha.compute());
75+
let mut correct_response = None;
76+
if let Some(qop) = user_vals.get(b"qop".as_ref()) {
77+
if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
78+
correct_response = Some({
79+
let mut c = Context::new();
80+
c.consume(&auth_pass);
81+
c.consume(b":");
82+
c.consume(nonce);
83+
c.consume(b":");
84+
if let Some(nc) = user_vals.get(b"nc".as_ref()) {
85+
c.consume(nc);
86+
}
87+
c.consume(b":");
88+
if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) {
89+
c.consume(cnonce);
90+
}
91+
c.consume(b":");
92+
c.consume(qop);
93+
c.consume(b":");
94+
c.consume(&*ha);
95+
format!("{:x}", c.compute())
96+
});
97+
}
98+
}
99+
let correct_response = match correct_response {
100+
Some(r) => r,
101+
None => {
102+
let mut c = Context::new();
103+
c.consume(&auth_pass);
104+
c.consume(b":");
105+
c.consume(nonce);
106+
c.consume(b":");
107+
c.consume(&*ha);
108+
format!("{:x}", c.compute())
109+
}
110+
};
111+
if correct_response.as_bytes() == *user_response {
112+
// grant access
113+
return Some(());
114+
}
115+
}
116+
None
117+
}
118+
119+
/// Check if a nonce is still valid.
120+
/// Return an error if it was never valid
121+
fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
122+
if nonce.len() != 34 {
123+
return Err(());
124+
}
125+
//parse hex
126+
if let Ok(n) = std::str::from_utf8(nonce) {
127+
//get time
128+
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
129+
//check time
130+
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
131+
let secs_now = now.as_secs() as u32;
132+
133+
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
134+
//check hash
135+
let mut h = NONCESTARTHASH.clone();
136+
h.consume(secs_nonce.to_be_bytes());
137+
let h = format!("{:x}", h.compute());
138+
if h[..26] == n[8..34] {
139+
return Ok(dur < 300); // from the last 5min
140+
//Authentication-Info ?
141+
}
142+
}
143+
}
144+
}
145+
Err(())
146+
}
147+
148+
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
149+
let l = prefix.len();
150+
if search.len() < l {
151+
return None;
152+
}
153+
if &search[..l] == prefix {
154+
Some(&search[l..])
155+
} else {
156+
None
157+
}
158+
}
159+
160+
fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
161+
let mut sep = Vec::new();
162+
let mut asign = Vec::new();
163+
let mut i: usize = 0;
164+
let mut esc = false;
165+
for c in header {
166+
match (c, esc) {
167+
(b'=', false) => asign.push(i),
168+
(b',', false) => sep.push(i),
169+
(b'"', false) => esc = true,
170+
(b'"', true) => esc = false,
171+
_ => {}
172+
}
173+
i += 1;
174+
}
175+
sep.push(i); // same len for both Vecs
176+
177+
i = 0;
178+
let mut ret = HashMap::new();
179+
for (&k, &a) in sep.iter().zip(asign.iter()) {
180+
while header[i] == b' ' {
181+
i += 1;
182+
}
183+
if a <= i || k <= 1 + a {
184+
//keys and vals must contain one char
185+
return Err(());
186+
}
187+
let key = &header[i..a];
188+
let val = if header[1 + a] == b'"' && header[k - 1] == b'"' {
189+
//escaped
190+
&header[2 + a..k - 1]
191+
} else {
192+
//not escaped
193+
&header[1 + a..k]
194+
};
195+
i = 1 + k;
196+
ret.insert(key, val);
197+
}
198+
Ok(ret)
199+
}
200+
201+
fn create_nonce() -> String {
202+
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
203+
let secs = now.as_secs() as u32;
204+
let mut h = NONCESTARTHASH.clone();
205+
h.consume(secs.to_be_bytes());
206+
207+
let n = format!("{:08x}{:032x}", secs, h.compute());
208+
n[..34].to_string()
209+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod args;
2+
mod auth;
23
mod server;
34

45
pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;

0 commit comments

Comments
 (0)