mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 00:59:02 +03:00
feat: support downloading via token auth (#603)
This commit is contained in:
69
src/auth.rs
69
src/auth.rs
@@ -2,11 +2,13 @@ use crate::{args::Args, server::Response, utils::unix_now};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use ed25519_dalek::{ed25519::signature::SignerMut, Signature, SigningKey};
|
||||
use headers::HeaderValue;
|
||||
use hyper::{header::WWW_AUTHENTICATE, Method};
|
||||
use indexmap::IndexMap;
|
||||
use lazy_static::lazy_static;
|
||||
use md5::Context;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
@@ -14,7 +16,8 @@ use std::{
|
||||
use uuid::Uuid;
|
||||
|
||||
const REALM: &str = "DUFS";
|
||||
const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
|
||||
const DIGEST_AUTH_TIMEOUT: u32 = 60 * 60 * 24 * 7; // 7 days
|
||||
const TOKEN_EXPIRATION: u64 = 1000 * 60 * 60 * 24 * 3; // 3 days
|
||||
|
||||
lazy_static! {
|
||||
static ref NONCESTARTHASH: Context = {
|
||||
@@ -105,11 +108,21 @@ impl AccessControl {
|
||||
path: &str,
|
||||
method: &Method,
|
||||
authorization: Option<&HeaderValue>,
|
||||
token: Option<&String>,
|
||||
guard_options: bool,
|
||||
) -> (Option<String>, Option<AccessPaths>) {
|
||||
if self.users.is_empty() {
|
||||
return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
|
||||
}
|
||||
|
||||
if method == Method::GET {
|
||||
if let Some(token) = token {
|
||||
if let Ok((user, ap)) = self.verifty_token(token, path) {
|
||||
return (Some(user), ap.guard(path, method));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(authorization) = authorization {
|
||||
if let Some(user) = get_auth_user(authorization) {
|
||||
if let Some((pass, ap)) = self.users.get(&user) {
|
||||
@@ -135,6 +148,49 @@ impl AccessControl {
|
||||
|
||||
(None, None)
|
||||
}
|
||||
|
||||
pub fn generate_token(&self, path: &str, user: &str) -> Result<String> {
|
||||
let (pass, _) = self
|
||||
.users
|
||||
.get(user)
|
||||
.ok_or_else(|| anyhow!("Not found user '{user}'"))?;
|
||||
let exp = unix_now().as_millis() as u64 + TOKEN_EXPIRATION;
|
||||
let message = format!("{path}:{exp}");
|
||||
let mut signing_key = derive_secret_key(user, pass);
|
||||
let sig = signing_key.sign(message.as_bytes()).to_bytes();
|
||||
|
||||
let mut raw = Vec::with_capacity(64 + 8 + user.len());
|
||||
raw.extend_from_slice(&sig);
|
||||
raw.extend_from_slice(&exp.to_be_bytes());
|
||||
raw.extend_from_slice(user.as_bytes());
|
||||
|
||||
Ok(hex::encode(raw))
|
||||
}
|
||||
|
||||
fn verifty_token<'a>(&'a self, token: &str, path: &str) -> Result<(String, &'a AccessPaths)> {
|
||||
let raw = hex::decode(token)?;
|
||||
|
||||
let sig_bytes = &raw[..64];
|
||||
let exp_bytes = &raw[64..72];
|
||||
let user_bytes = &raw[72..];
|
||||
|
||||
let exp = u64::from_be_bytes(exp_bytes.try_into()?);
|
||||
if unix_now().as_millis() as u64 > exp {
|
||||
bail!("Token expired");
|
||||
}
|
||||
|
||||
let user = std::str::from_utf8(user_bytes)?;
|
||||
let (pass, ap) = self
|
||||
.users
|
||||
.get(user)
|
||||
.ok_or_else(|| anyhow!("Not found user '{user}'"))?;
|
||||
|
||||
let sig = Signature::from_bytes(&<[u8; 64]>::try_from(sig_bytes)?);
|
||||
|
||||
let message = format!("{path}:{exp}");
|
||||
derive_secret_key(user, pass).verify(message.as_bytes(), &sig)?;
|
||||
Ok((user.to_string(), ap))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
@@ -422,6 +478,13 @@ pub fn check_auth(
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_secret_key(user: &str, pass: &str) -> SigningKey {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{user}:{pass}").as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
SigningKey::from_bytes(&hash.into())
|
||||
}
|
||||
|
||||
/// Check if a nonce is still valid.
|
||||
/// Return an error if it was never valid
|
||||
fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
||||
@@ -433,7 +496,7 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
||||
//get time
|
||||
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
|
||||
//check time
|
||||
let now = unix_now()?;
|
||||
let now = unix_now();
|
||||
let secs_now = now.as_secs() as u32;
|
||||
|
||||
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
|
||||
@@ -513,7 +576,7 @@ fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
|
||||
}
|
||||
|
||||
fn create_nonce() -> Result<String> {
|
||||
let now = unix_now()?;
|
||||
let now = unix_now();
|
||||
let secs = now.as_secs() as u32;
|
||||
let mut h = NONCESTARTHASH.clone();
|
||||
h.consume(secs.to_be_bytes());
|
||||
|
||||
@@ -149,20 +149,6 @@ impl Server {
|
||||
let headers = req.headers();
|
||||
let method = req.method().clone();
|
||||
|
||||
let user_agent = headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_microsoft_webdav = user_agent.starts_with("microsoft-webdav-miniredir/");
|
||||
|
||||
if is_microsoft_webdav {
|
||||
// microsoft webdav requires this.
|
||||
res.headers_mut()
|
||||
.insert(CONNECTION, HeaderValue::from_static("close"));
|
||||
}
|
||||
|
||||
let relative_path = match self.resolve_path(req_path) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
@@ -179,11 +165,34 @@ impl Server {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let user_agent = headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_microsoft_webdav = user_agent.starts_with("microsoft-webdav-miniredir/");
|
||||
|
||||
if is_microsoft_webdav {
|
||||
// microsoft webdav requires this.
|
||||
res.headers_mut()
|
||||
.insert(CONNECTION, HeaderValue::from_static("close"));
|
||||
}
|
||||
|
||||
let authorization = headers.get(AUTHORIZATION);
|
||||
let guard =
|
||||
self.args
|
||||
.auth
|
||||
.guard(&relative_path, &method, authorization, is_microsoft_webdav);
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
let mut query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
let guard = self.args.auth.guard(
|
||||
&relative_path,
|
||||
&method,
|
||||
authorization,
|
||||
query_params.get("token"),
|
||||
is_microsoft_webdav,
|
||||
);
|
||||
|
||||
let (user, access_paths) = match guard {
|
||||
(None, None) => {
|
||||
@@ -197,11 +206,6 @@ impl Server {
|
||||
(x, Some(y)) => (x, y),
|
||||
};
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
let mut query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
if detect_noscript(&user_agent) {
|
||||
query_params.insert("noscript".to_string(), String::new());
|
||||
}
|
||||
@@ -214,6 +218,11 @@ impl Server {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
if has_query_flag(&query_params, "tokengen") {
|
||||
self.handle_tokengen(&relative_path, user, &mut res).await?;
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let head_only = method == Method::HEAD;
|
||||
|
||||
if self.args.path_is_file {
|
||||
@@ -996,6 +1005,24 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_tokengen(
|
||||
&self,
|
||||
relative_path: &str,
|
||||
user: Option<String>,
|
||||
res: &mut Response,
|
||||
) -> Result<()> {
|
||||
let output = self
|
||||
.args
|
||||
.auth
|
||||
.generate_token(relative_path, &user.unwrap_or_default())?;
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentType::from(mime_guess::mime::TEXT_PLAIN_UTF_8));
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentLength(output.len() as u64));
|
||||
*res.body_mut() = body_full(output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_propfind_dir(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -1271,7 +1298,7 @@ impl Server {
|
||||
let guard = self
|
||||
.args
|
||||
.auth
|
||||
.guard(&dest_path, req.method(), authorization, false);
|
||||
.guard(&dest_path, req.method(), authorization, None, false);
|
||||
|
||||
match guard {
|
||||
(_, Some(_)) => {}
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::{
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
pub fn unix_now() -> Result<Duration> {
|
||||
pub fn unix_now() -> Duration {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.with_context(|| "Invalid system time")
|
||||
.expect("Unable to get unix epoch time")
|
||||
}
|
||||
|
||||
pub fn encode_uri(v: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user