"get_if_addrs",
"headers",
"hyper",
+ "lazy_static",
+ "md5",
"mime_guess",
"percent-encoding",
"rustls",
"tokio-rustls",
"tokio-stream",
"tokio-util",
+ "uuid",
]
[[package]]
"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"
"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"
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"
"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"
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"
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
- 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
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.
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,
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");
--- /dev/null
+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()
+}
mod args;
+mod auth;
mod server;
pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
+use crate::auth::{generate_www_auth, valid_digest};
use crate::{Args, BoxResult};
use async_walkdir::WalkDir;
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>;
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 {
return Ok(());
}
},
- None => 1,
+ None => 0,
};
let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
if depth > 0 {
}
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(())
}
}
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
}