feat: support downloading via token auth (#603)

This commit is contained in:
sigoden
2025-08-02 14:37:49 +08:00
committed by GitHub
parent 089d30c5a5
commit 9c9fca75d3
7 changed files with 277 additions and 33 deletions

View File

@@ -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());

View File

@@ -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(_)) => {}

View File

@@ -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 {