feat: new auth (#218)

The access level path control used by dufs has two disadvantages:

1. One path cannot support multiple users
2. It is very troublesome to set multiple paths for one user

So it needs to be refactored.
The new auth is account based, it closes #207, closes #208.

BREAKING CHANGE: new auth
This commit is contained in:
sigoden 2023-06-01 18:52:05 +08:00 committed by GitHub
parent 2890b3929d
commit f8ea41638f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 488 additions and 280 deletions

View file

@ -44,6 +44,7 @@ content_inspector = "0.2"
anyhow = "1.0" anyhow = "1.0"
chardetng = "0.1" chardetng = "0.1"
glob = "0.3.1" glob = "0.3.1"
indexmap = "1.9"
[features] [features]
default = ["tls"] default = ["tls"]
@ -59,7 +60,6 @@ regex = "1"
url = "2" url = "2"
diqwest = { version = "1", features = ["blocking"] } diqwest = { version = "1", features = ["blocking"] }
predicates = "3" predicates = "3"
indexmap = "1.9"
[profile.release] [profile.release]
lto = true lto = true

View file

@ -78,7 +78,7 @@ pub fn build_cli() -> Command {
.long("auth") .long("auth")
.help("Add auth for path") .help("Add auth for path")
.action(ArgAction::Append) .action(ArgAction::Append)
.value_delimiter(',') .value_delimiter('|')
.value_name("rules"), .value_name("rules"),
) )
.arg( .arg(
@ -288,7 +288,7 @@ impl Args {
"basic" => AuthMethod::Basic, "basic" => AuthMethod::Basic,
_ => AuthMethod::Digest, _ => AuthMethod::Digest,
}; };
let auth = AccessControl::new(&auth, &uri_prefix)?; let auth = AccessControl::new(&auth)?;
let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload"); let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload");
let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete"); let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete");
let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search"); let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search");

View file

@ -2,12 +2,16 @@ use anyhow::{anyhow, bail, Result};
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use headers::HeaderValue; use headers::HeaderValue;
use hyper::Method; use hyper::Method;
use indexmap::IndexMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use md5::Context; use md5::Context;
use std::collections::HashMap; use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use uuid::Uuid; use uuid::Uuid;
use crate::utils::{encode_uri, unix_now}; use crate::utils::unix_now;
const REALM: &str = "DUFS"; const REALM: &str = "DUFS";
const DIGEST_AUTH_TIMEOUT: u32 = 86400; const DIGEST_AUTH_TIMEOUT: u32 = 86400;
@ -21,57 +25,63 @@ lazy_static! {
}; };
} }
#[derive(Debug)] #[derive(Debug, Default)]
pub struct AccessControl { pub struct AccessControl {
rules: HashMap<String, PathControl>, users: IndexMap<String, (String, AccessPaths)>,
} anony: Option<AccessPaths>,
#[derive(Debug)]
pub struct PathControl {
readwrite: Account,
readonly: Option<Account>,
share: bool,
} }
impl AccessControl { impl AccessControl {
pub fn new(raw_rules: &[&str], uri_prefix: &str) -> Result<Self> { pub fn new(raw_rules: &[&str]) -> Result<Self> {
let mut rules = HashMap::default();
if raw_rules.is_empty() { if raw_rules.is_empty() {
return Ok(Self { rules }); return Ok(AccessControl {
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
});
} }
let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
let mut anony = None;
let mut anony_paths = vec![];
let mut users = IndexMap::new();
for rule in raw_rules { for rule in raw_rules {
let parts: Vec<&str> = rule.split('@').collect(); let (user, list) = rule.split_once('@').ok_or_else(|| create_err(rule))?;
let create_err = || anyhow!("Invalid auth `{rule}`"); if user.is_empty() && anony.is_some() {
match parts.as_slice() { bail!("Invalid auth, duplicate anonymous rules");
[path, readwrite] => {
let control = PathControl {
readwrite: Account::new(readwrite).ok_or_else(create_err)?,
readonly: None,
share: false,
};
rules.insert(sanitize_path(path, uri_prefix), control);
} }
[path, readwrite, readonly] => { let mut paths = AccessPaths::default();
let (readonly, share) = if *readonly == "*" { for value in list.trim_matches(',').split(',') {
(None, true) let (path, perm) = match value.split_once(':') {
None => (value, AccessPerm::ReadOnly),
Some((path, "rw")) => (path, AccessPerm::ReadWrite),
_ => return Err(create_err(rule)),
};
if user.is_empty() {
anony_paths.push((path, perm));
}
paths.add(path, perm);
}
if user.is_empty() {
anony = Some(paths);
} else if let Some((user, pass)) = user.split_once(':') {
if user.is_empty() || pass.is_empty() {
return Err(create_err(rule));
}
users.insert(user.to_string(), (pass.to_string(), paths));
} else { } else {
(Some(Account::new(readonly).ok_or_else(create_err)?), false) return Err(create_err(rule));
};
let control = PathControl {
readwrite: Account::new(readwrite).ok_or_else(create_err)?,
readonly,
share,
};
rules.insert(sanitize_path(path, uri_prefix), control);
}
_ => return Err(create_err()),
} }
} }
Ok(Self { rules }) for (path, perm) in anony_paths {
for (_, (_, paths)) in users.iter_mut() {
paths.add(path, perm)
}
}
Ok(Self { users, anony })
} }
pub fn valid(&self) -> bool { pub fn valid(&self) -> bool {
!self.rules.is_empty() !self.users.is_empty() || self.anony.is_some()
} }
pub fn guard( pub fn guard(
@ -80,81 +90,157 @@ impl AccessControl {
method: &Method, method: &Method,
authorization: Option<&HeaderValue>, authorization: Option<&HeaderValue>,
auth_method: AuthMethod, auth_method: AuthMethod,
) -> GuardType { ) -> (Option<String>, Option<AccessPaths>) {
if self.rules.is_empty() { if let Some(authorization) = authorization {
return GuardType::ReadWrite; if let Some(user) = auth_method.get_user(authorization) {
if let Some((pass, paths)) = self.users.get(&user) {
if method == Method::OPTIONS {
return (Some(user), Some(AccessPaths::new(AccessPerm::ReadOnly)));
}
if auth_method
.check(authorization, method.as_str(), &user, pass)
.is_some()
{
return (Some(user), paths.find(path, !is_readonly_method(method)));
} else {
return (None, None);
}
}
}
} }
if method == Method::OPTIONS { if method == Method::OPTIONS {
return GuardType::ReadOnly; return (None, Some(AccessPaths::new(AccessPerm::ReadOnly)));
} }
let mut controls = vec![]; if let Some(paths) = self.anony.as_ref() {
for path in walk_path(path) { return (None, paths.find(path, !is_readonly_method(method)));
if let Some(control) = self.rules.get(path) {
controls.push(control);
if let Some(authorization) = authorization {
let Account { user, pass } = &control.readwrite;
if auth_method
.validate(authorization, method.as_str(), user, pass)
.is_some()
{
return GuardType::ReadWrite;
} }
}
} (None, None)
}
if is_readonly_method(method) {
for control in controls.into_iter() {
if control.share {
return GuardType::ReadOnly;
}
if let Some(authorization) = authorization {
if let Some(Account { user, pass }) = &control.readonly {
if auth_method
.validate(authorization, method.as_str(), user, pass)
.is_some()
{
return GuardType::ReadOnly;
}
}
}
}
}
GuardType::Reject
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum GuardType { pub struct AccessPaths {
Reject, perm: AccessPerm,
children: IndexMap<String, AccessPaths>,
}
impl AccessPaths {
pub fn new(perm: AccessPerm) -> Self {
Self {
perm,
..Default::default()
}
}
pub fn perm(&self) -> AccessPerm {
self.perm
}
fn set_perm(&mut self, perm: AccessPerm) {
if self.perm < perm {
self.perm = perm
}
}
pub fn add(&mut self, path: &str, perm: AccessPerm) {
let path = path.trim_matches('/');
if path.is_empty() {
self.set_perm(perm);
} else {
let parts: Vec<&str> = path.split('/').collect();
self.add_impl(&parts, perm);
}
}
fn add_impl(&mut self, parts: &[&str], perm: AccessPerm) {
let parts_len = parts.len();
if parts_len == 0 {
self.set_perm(perm);
return;
}
let child = self.children.entry(parts[0].to_string()).or_default();
child.add_impl(&parts[1..], perm)
}
pub fn find(&self, path: &str, writable: bool) -> Option<AccessPaths> {
let parts: Vec<&str> = path
.trim_matches('/')
.split('/')
.filter(|v| !v.is_empty())
.collect();
let target = self.find_impl(&parts, self.perm)?;
if writable && !target.perm().readwrite() {
return None;
}
Some(target)
}
fn find_impl(&self, parts: &[&str], perm: AccessPerm) -> Option<AccessPaths> {
let perm = self.perm.max(perm);
if parts.is_empty() {
if perm.indexonly() {
return Some(self.clone());
} else {
return Some(AccessPaths::new(perm));
}
}
let child = match self.children.get(parts[0]) {
Some(v) => v,
None => {
if perm.indexonly() {
return None;
} else {
return Some(AccessPaths::new(perm));
}
}
};
child.find_impl(&parts[1..], perm)
}
pub fn child_paths(&self) -> Vec<&String> {
self.children.keys().collect()
}
pub fn leaf_paths(&self, base: &Path) -> Vec<PathBuf> {
if !self.perm().indexonly() {
return vec![base.to_path_buf()];
}
let mut output = vec![];
self.leaf_paths_impl(&mut output, base);
output
}
fn leaf_paths_impl(&self, output: &mut Vec<PathBuf>, base: &Path) {
for (name, child) in self.children.iter() {
let base = base.join(name);
if child.perm().indexonly() {
child.leaf_paths_impl(output, &base);
} else {
output.push(base)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum AccessPerm {
#[default]
IndexOnly,
ReadWrite, ReadWrite,
ReadOnly, ReadOnly,
} }
impl GuardType { impl AccessPerm {
pub fn is_reject(&self) -> bool { pub fn readwrite(&self) -> bool {
*self == GuardType::Reject self == &AccessPerm::ReadWrite
}
} }
fn sanitize_path(path: &str, uri_prefix: &str) -> String { pub fn indexonly(&self) -> bool {
let new_path = match (uri_prefix, path) { self == &AccessPerm::IndexOnly
("/", "/") => "/".into(),
(_, "/") => uri_prefix.trim_end_matches('/').into(),
_ => format!("{}{}", uri_prefix, path.trim_matches('/')),
};
encode_uri(&new_path)
} }
fn walk_path(path: &str) -> impl Iterator<Item = &str> {
let mut idx = 0;
path.split('/').enumerate().map(move |(i, part)| {
let end = if i == 0 { 1 } else { idx + part.len() + i };
let value = &path[..end];
idx += part.len();
value
})
} }
fn is_readonly_method(method: &Method) -> bool { fn is_readonly_method(method: &Method) -> bool {
@ -164,29 +250,6 @@ fn is_readonly_method(method: &Method) -> bool {
|| method.as_str() == "PROPFIND" || method.as_str() == "PROPFIND"
} }
#[derive(Debug, Clone)]
struct Account {
user: String,
pass: String,
}
impl Account {
fn new(data: &str) -> Option<Self> {
let p: Vec<&str> = data.trim().split(':').collect();
if p.len() != 2 {
return None;
}
let user = p[0];
let pass = p[1];
let mut h = Context::new();
h.consume(format!("{user}:{REALM}:{pass}").as_bytes());
Some(Account {
user: user.to_owned(),
pass: format!("{:x}", h.compute()),
})
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum AuthMethod { pub enum AuthMethod {
Basic, Basic,
@ -208,6 +271,7 @@ impl AuthMethod {
} }
} }
} }
pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> { pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> {
match self { match self {
AuthMethod::Basic => { AuthMethod::Basic => {
@ -227,7 +291,8 @@ impl AuthMethod {
} }
} }
} }
pub fn validate(
fn check(
&self, &self,
authorization: &HeaderValue, authorization: &HeaderValue,
method: &str, method: &str,
@ -245,12 +310,7 @@ impl AuthMethod {
return None; return None;
} }
let mut h = Context::new(); if parts[1] == auth_pass {
h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes());
let http_pass = format!("{:x}", h.compute());
if http_pass == auth_pass {
return Some(()); return Some(());
} }
@ -273,6 +333,11 @@ impl AuthMethod {
if auth_user != username { if auth_user != username {
return None; return None;
} }
let mut h = Context::new();
h.consume(format!("{}:{}:{}", auth_user, REALM, auth_pass).as_bytes());
let auth_pass = format!("{:x}", h.compute());
let mut ha = Context::new(); let mut ha = Context::new();
ha.consume(method); ha.consume(method);
ha.consume(b":"); ha.consume(b":");
@ -285,7 +350,7 @@ impl AuthMethod {
if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() { if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
correct_response = Some({ correct_response = Some({
let mut c = Context::new(); let mut c = Context::new();
c.consume(auth_pass); c.consume(&auth_pass);
c.consume(b":"); c.consume(b":");
c.consume(nonce); c.consume(nonce);
c.consume(b":"); c.consume(b":");
@ -308,7 +373,7 @@ impl AuthMethod {
Some(r) => r, Some(r) => r,
None => { None => {
let mut c = Context::new(); let mut c = Context::new();
c.consume(auth_pass); c.consume(&auth_pass);
c.consume(b":"); c.consume(b":");
c.consume(nonce); c.consume(nonce);
c.consume(b":"); c.consume(b":");
@ -317,7 +382,6 @@ impl AuthMethod {
} }
}; };
if correct_response.as_bytes() == *user_response { if correct_response.as_bytes() == *user_response {
// grant access
return Some(()); return Some(());
} }
} }
@ -417,3 +481,42 @@ fn create_nonce() -> Result<String> {
let n = format!("{:08x}{:032x}", secs, h.compute()); let n = format!("{:08x}{:032x}", secs, h.compute());
Ok(n[..34].to_string()) Ok(n[..34].to_string())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_access_paths() {
let mut paths = AccessPaths::default();
paths.add("/dir1", AccessPerm::ReadWrite);
paths.add("/dir2/dir1", AccessPerm::ReadWrite);
paths.add("/dir2/dir2", AccessPerm::ReadOnly);
paths.add("/dir2/dir3/dir1", AccessPerm::ReadWrite);
assert_eq!(
paths.leaf_paths(Path::new("/tmp")),
[
"/tmp/dir1",
"/tmp/dir2/dir1",
"/tmp/dir2/dir2",
"/tmp/dir2/dir3/dir1"
]
.iter()
.map(PathBuf::from)
.collect::<Vec<_>>()
);
assert_eq!(
paths
.find("dir2", false)
.map(|v| v.leaf_paths(Path::new("/tmp/dir2"))),
Some(
["/tmp/dir2/dir1", "/tmp/dir2/dir2", "/tmp/dir2/dir3/dir1"]
.iter()
.map(PathBuf::from)
.collect::<Vec<_>>()
)
);
assert_eq!(paths.find("dir2", true), None);
assert!(paths.find("dir1/file", true).is_some());
}
}

View file

@ -1,3 +1,6 @@
#![allow(clippy::too_many_arguments)]
use crate::auth::AccessPaths;
use crate::streamer::Streamer; use crate::streamer::Streamer;
use crate::utils::{ use crate::utils::{
decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name, decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name,
@ -136,16 +139,32 @@ impl Server {
} }
let authorization = headers.get(AUTHORIZATION); let authorization = headers.get(AUTHORIZATION);
let guard_type = self.args.auth.guard( let relative_path = match self.resolve_path(req_path) {
req_path, Some(v) => v,
None => {
status_forbid(&mut res);
return Ok(res);
}
};
let guard = self.args.auth.guard(
&relative_path,
&method, &method,
authorization, authorization,
self.args.auth_method.clone(), self.args.auth_method.clone(),
); );
if guard_type.is_reject() {
let (user, access_paths) = match guard {
(None, None) => {
self.auth_reject(&mut res)?; self.auth_reject(&mut res)?;
return Ok(res); return Ok(res);
} }
(Some(_), None) => {
status_forbid(&mut res);
return Ok(res);
}
(x, Some(y)) => (x, y),
};
let query = req.uri().query().unwrap_or_default(); let query = req.uri().query().unwrap_or_default();
let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes()) let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
@ -171,8 +190,7 @@ impl Server {
} }
return Ok(res); return Ok(res);
} }
let path = match self.join_path(&relative_path) {
let path = match self.extract_path(req_path) {
Some(v) => v, Some(v) => v,
None => { None => {
status_forbid(&mut res); status_forbid(&mut res);
@ -209,31 +227,38 @@ impl Server {
status_not_found(&mut res); status_not_found(&mut res);
return Ok(res); return Ok(res);
} }
self.handle_zip_dir(path, head_only, &mut res).await?; self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
} else if allow_search && query_params.contains_key("q") { } else if allow_search && query_params.contains_key("q") {
let user = self.retrieve_user(authorization); self.handle_search_dir(
self.handle_search_dir(path, &query_params, head_only, user, &mut res) path,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?; .await?;
} else { } else {
let user = self.retrieve_user(authorization);
self.handle_render_index( self.handle_render_index(
path, path,
&query_params, &query_params,
headers, headers,
head_only, head_only,
user, user,
access_paths,
&mut res, &mut res,
) )
.await?; .await?;
} }
} else if render_index || render_spa { } else if render_index || render_spa {
let user = self.retrieve_user(authorization);
self.handle_render_index( self.handle_render_index(
path, path,
&query_params, &query_params,
headers, headers,
head_only, head_only,
user, user,
access_paths,
&mut res, &mut res,
) )
.await?; .await?;
@ -242,19 +267,32 @@ impl Server {
status_not_found(&mut res); status_not_found(&mut res);
return Ok(res); return Ok(res);
} }
self.handle_zip_dir(path, head_only, &mut res).await?; self.handle_zip_dir(path, head_only, access_paths, &mut res)
.await?;
} else if allow_search && query_params.contains_key("q") { } else if allow_search && query_params.contains_key("q") {
let user = self.retrieve_user(authorization); self.handle_search_dir(
self.handle_search_dir(path, &query_params, head_only, user, &mut res) path,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?; .await?;
} else { } else {
let user = self.retrieve_user(authorization); self.handle_ls_dir(
self.handle_ls_dir(path, true, &query_params, head_only, user, &mut res) path,
true,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?; .await?;
} }
} else if is_file { } else if is_file {
if query_params.contains_key("edit") { if query_params.contains_key("edit") {
let user = self.retrieve_user(authorization);
self.handle_edit_file(path, head_only, user, &mut res) self.handle_edit_file(path, head_only, user, &mut res)
.await?; .await?;
} else { } else {
@ -265,8 +303,15 @@ impl Server {
self.handle_render_spa(path, headers, head_only, &mut res) self.handle_render_spa(path, headers, head_only, &mut res)
.await?; .await?;
} else if allow_upload && req_path.ends_with('/') { } else if allow_upload && req_path.ends_with('/') {
let user = self.retrieve_user(authorization); self.handle_ls_dir(
self.handle_ls_dir(path, false, &query_params, head_only, user, &mut res) path,
false,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?; .await?;
} else { } else {
status_not_found(&mut res); status_not_found(&mut res);
@ -294,7 +339,8 @@ impl Server {
method => match method.as_str() { method => match method.as_str() {
"PROPFIND" => { "PROPFIND" => {
if is_dir { if is_dir {
self.handle_propfind_dir(path, headers, &mut res).await?; self.handle_propfind_dir(path, headers, access_paths, &mut res)
.await?;
} else if is_file { } else if is_file {
self.handle_propfind_file(path, &mut res).await?; self.handle_propfind_file(path, &mut res).await?;
} else { } else {
@ -401,11 +447,12 @@ impl Server {
query_params: &HashMap<String, String>, query_params: &HashMap<String, String>,
head_only: bool, head_only: bool,
user: Option<String>, user: Option<String>,
access_paths: AccessPaths,
res: &mut Response, res: &mut Response,
) -> Result<()> { ) -> Result<()> {
let mut paths = vec![]; let mut paths = vec![];
if exist { if exist {
paths = match self.list_dir(path, path).await { paths = match self.list_dir(path, path, access_paths).await {
Ok(paths) => paths, Ok(paths) => paths,
Err(_) => { Err(_) => {
status_forbid(res); status_forbid(res);
@ -422,6 +469,7 @@ impl Server {
query_params: &HashMap<String, String>, query_params: &HashMap<String, String>,
head_only: bool, head_only: bool,
user: Option<String>, user: Option<String>,
access_paths: AccessPaths,
res: &mut Response, res: &mut Response,
) -> Result<()> { ) -> Result<()> {
let mut paths: Vec<PathItem> = vec![]; let mut paths: Vec<PathItem> = vec![];
@ -435,8 +483,9 @@ impl Server {
let hidden = hidden.clone(); let hidden = hidden.clone();
let running = self.running.clone(); let running = self.running.clone();
let search_paths = tokio::task::spawn_blocking(move || { let search_paths = tokio::task::spawn_blocking(move || {
let mut it = WalkDir::new(&path_buf).into_iter();
let mut paths: Vec<PathBuf> = vec![]; let mut paths: Vec<PathBuf> = vec![];
for dir in access_paths.leaf_paths(&path_buf) {
let mut it = WalkDir::new(&dir).into_iter();
while let Some(Ok(entry)) = it.next() { while let Some(Ok(entry)) = it.next() {
if !running.load(Ordering::SeqCst) { if !running.load(Ordering::SeqCst) {
break; break;
@ -466,6 +515,7 @@ impl Server {
} }
paths.push(entry_path.to_path_buf()); paths.push(entry_path.to_path_buf());
} }
}
paths paths
}) })
.await?; .await?;
@ -478,7 +528,13 @@ impl Server {
self.send_index(path, paths, true, query_params, head_only, user, res) self.send_index(path, paths, true, query_params, head_only, user, res)
} }
async fn handle_zip_dir(&self, path: &Path, head_only: bool, res: &mut Response) -> Result<()> { async fn handle_zip_dir(
&self,
path: &Path,
head_only: bool,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE); let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
let filename = try_get_file_name(path)?; let filename = try_get_file_name(path)?;
set_content_diposition(res, false, &format!("{}.zip", filename))?; set_content_diposition(res, false, &format!("{}.zip", filename))?;
@ -491,7 +547,7 @@ impl Server {
let hidden = self.args.hidden.clone(); let hidden = self.args.hidden.clone();
let running = self.running.clone(); let running = self.running.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = zip_dir(&mut writer, &path, &hidden, running).await { if let Err(e) = zip_dir(&mut writer, &path, access_paths, &hidden, running).await {
error!("Failed to zip {}, {}", path.display(), e); error!("Failed to zip {}, {}", path.display(), e);
} }
}); });
@ -507,6 +563,7 @@ impl Server {
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
head_only: bool, head_only: bool,
user: Option<String>, user: Option<String>,
access_paths: AccessPaths,
res: &mut Response, res: &mut Response,
) -> Result<()> { ) -> Result<()> {
let index_path = path.join(INDEX_NAME); let index_path = path.join(INDEX_NAME);
@ -519,7 +576,7 @@ impl Server {
self.handle_send_file(&index_path, headers, head_only, res) self.handle_send_file(&index_path, headers, head_only, res)
.await?; .await?;
} else if self.args.render_try_index { } else if self.args.render_try_index {
self.handle_ls_dir(path, true, query_params, head_only, user, res) self.handle_ls_dir(path, true, query_params, head_only, user, access_paths, res)
.await?; .await?;
} else { } else {
status_not_found(res) status_not_found(res)
@ -724,6 +781,7 @@ impl Server {
&self, &self,
path: &Path, path: &Path,
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,
access_paths: AccessPaths,
res: &mut Response, res: &mut Response,
) -> Result<()> { ) -> Result<()> {
let depth: u32 = match headers.get("depth") { let depth: u32 = match headers.get("depth") {
@ -741,7 +799,7 @@ impl Server {
None => vec![], None => vec![],
}; };
if depth != 0 { if depth != 0 {
match self.list_dir(path, &self.args.path).await { match self.list_dir(path, &self.args.path, access_paths).await {
Ok(child) => paths.extend(child), Ok(child) => paths.extend(child),
Err(_) => { Err(_) => {
status_forbid(res); status_forbid(res);
@ -965,19 +1023,32 @@ impl Server {
return None; return None;
} }
}; };
let relative_path = match self.resolve_path(&dest_path) {
Some(v) => v,
None => {
*res.status_mut() = StatusCode::BAD_REQUEST;
return None;
}
};
let authorization = headers.get(AUTHORIZATION); let authorization = headers.get(AUTHORIZATION);
let guard_type = self.args.auth.guard( let guard = self.args.auth.guard(
&dest_path, &relative_path,
req.method(), req.method(),
authorization, authorization,
self.args.auth_method.clone(), self.args.auth_method.clone(),
); );
if guard_type.is_reject() {
match guard {
(_, Some(_)) => {}
_ => {
status_forbid(res); status_forbid(res);
return None; return None;
} }
};
let dest = match self.extract_path(&dest_path) { let dest = match self.join_path(&relative_path) {
Some(dest) => dest, Some(dest) => dest,
None => { None => {
*res.status_mut() = StatusCode::BAD_REQUEST; *res.status_mut() = StatusCode::BAD_REQUEST;
@ -994,49 +1065,61 @@ impl Server {
Some(uri.path().to_string()) Some(uri.path().to_string())
} }
fn extract_path(&self, path: &str) -> Option<PathBuf> { fn resolve_path(&self, path: &str) -> Option<String> {
let mut slash_stripped_path = path; let path = path.trim_matches('/');
while let Some(p) = slash_stripped_path.strip_prefix('/') { let path = decode_uri(path)?;
slash_stripped_path = p let prefix = self.args.path_prefix.as_str();
if prefix == "/" {
return Some(path.to_string());
} }
let decoded_path = decode_uri(slash_stripped_path)?; path.strip_prefix(prefix.trim_start_matches('/'))
let slashes_switched = if cfg!(windows) { .map(|v| v.trim_matches('/').to_string())
decoded_path.replace('/', "\\")
} else {
decoded_path.into_owned()
};
let stripped_path = match self.strip_path_prefix(&slashes_switched) {
Some(path) => path,
None => return None,
};
Some(self.args.path.join(stripped_path))
} }
fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> { fn join_path(&self, path: &str) -> Option<PathBuf> {
let path = path.as_ref(); if path.is_empty() {
if self.args.path_prefix.is_empty() { return Some(self.args.path.clone());
Some(path)
} else {
path.strip_prefix(&self.args.path_prefix).ok()
} }
let path = if cfg!(windows) {
path.replace('/', "\\")
} else {
path.to_string()
};
Some(self.args.path.join(path))
} }
async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> Result<Vec<PathItem>> { async fn list_dir(
&self,
entry_path: &Path,
base_path: &Path,
access_paths: AccessPaths,
) -> Result<Vec<PathItem>> {
let mut paths: Vec<PathItem> = vec![]; let mut paths: Vec<PathItem> = vec![];
if access_paths.perm().indexonly() {
for name in access_paths.child_paths() {
let entry_path = entry_path.join(name);
self.add_pathitem(&mut paths, base_path, &entry_path).await;
}
} else {
let mut rd = fs::read_dir(entry_path).await?; let mut rd = fs::read_dir(entry_path).await?;
while let Ok(Some(entry)) = rd.next_entry().await { while let Ok(Some(entry)) = rd.next_entry().await {
let entry_path = entry.path(); let entry_path = entry.path();
let base_name = get_file_name(&entry_path); self.add_pathitem(&mut paths, base_path, &entry_path).await;
if let Ok(Some(item)) = self.to_pathitem(entry_path.as_path(), base_path).await {
if is_hidden(&self.args.hidden, base_name, item.is_dir()) {
continue;
}
paths.push(item);
} }
} }
Ok(paths) Ok(paths)
} }
async fn add_pathitem(&self, paths: &mut Vec<PathItem>, base_path: &Path, entry_path: &Path) {
let base_name = get_file_name(entry_path);
if let Ok(Some(item)) = self.to_pathitem(entry_path, base_path).await {
if is_hidden(&self.args.hidden, base_name, item.is_dir()) {
return;
}
paths.push(item);
}
}
async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> { async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
let path = path.as_ref(); let path = path.as_ref();
let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path)); let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path));
@ -1066,10 +1149,6 @@ impl Server {
size, size,
})) }))
} }
fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option<String> {
self.args.auth_method.get_user(authorization?)
}
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -1237,16 +1316,18 @@ fn res_multistatus(res: &mut Response, content: &str) {
async fn zip_dir<W: AsyncWrite + Unpin>( async fn zip_dir<W: AsyncWrite + Unpin>(
writer: &mut W, writer: &mut W,
dir: &Path, dir: &Path,
access_paths: AccessPaths,
hidden: &[String], hidden: &[String],
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
) -> Result<()> { ) -> Result<()> {
let mut writer = ZipFileWriter::new(writer); let mut writer = ZipFileWriter::new(writer);
let hidden = Arc::new(hidden.to_vec()); let hidden = Arc::new(hidden.to_vec());
let hidden = hidden.clone(); let hidden = hidden.clone();
let dir_path_buf = dir.to_path_buf(); let dir_clone = dir.to_path_buf();
let zip_paths = tokio::task::spawn_blocking(move || { let zip_paths = tokio::task::spawn_blocking(move || {
let mut it = WalkDir::new(&dir_path_buf).into_iter();
let mut paths: Vec<PathBuf> = vec![]; let mut paths: Vec<PathBuf> = vec![];
for dir in access_paths.leaf_paths(&dir_clone) {
let mut it = WalkDir::new(&dir).into_iter();
while let Some(Ok(entry)) = it.next() { while let Some(Ok(entry)) = it.next() {
if !running.load(Ordering::SeqCst) { if !running.load(Ordering::SeqCst) {
break; break;
@ -1279,6 +1360,7 @@ async fn zip_dir<W: AsyncWrite + Unpin>(
} }
paths.push(entry_path.to_path_buf()); paths.push(entry_path.to_path_buf());
} }
}
paths paths
}) })
.await?; .await?;

View file

@ -3,10 +3,11 @@ mod utils;
use diqwest::blocking::WithDigestAuth; use diqwest::blocking::WithDigestAuth;
use fixtures::{server, Error, TestServer}; use fixtures::{server, Error, TestServer};
use indexmap::IndexSet;
use rstest::rstest; use rstest::rstest;
#[rstest] #[rstest]
fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { fn no_auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
assert!(resp.headers().contains_key("www-authenticate")); assert!(resp.headers().contains_key("www-authenticate"));
@ -17,7 +18,7 @@ fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Resu
} }
#[rstest] #[rstest]
fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> { fn auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
@ -29,7 +30,7 @@ fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<
} }
#[rstest] #[rstest]
fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> { fn auth_skip(#[with(&["--auth", "@/"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
@ -37,7 +38,7 @@ fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result
#[rstest] #[rstest]
fn auth_skip_on_options_method( fn auth_skip_on_options_method(
#[with(&["--auth", "/@user:pass"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"OPTIONS", &url).send()?; let resp = fetch!(b"OPTIONS", &url).send()?;
@ -47,13 +48,13 @@ fn auth_skip_on_options_method(
#[rstest] #[rstest]
fn auth_check( fn auth_check(
#[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"WRITEABLE", &url).send()?; let resp = fetch!(b"WRITEABLE", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user2", "pass2")?; let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user2", "pass2")?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 403);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user", "pass")?; let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
@ -61,7 +62,7 @@ fn auth_check(
#[rstest] #[rstest]
fn auth_readonly( fn auth_readonly(
#[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
@ -72,13 +73,13 @@ fn auth_readonly(
let resp = fetch!(b"PUT", &url) let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec()) .body(b"abc".to_vec())
.send_with_digest_auth("user2", "pass2")?; .send_with_digest_auth("user2", "pass2")?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 403);
Ok(()) Ok(())
} }
#[rstest] #[rstest]
fn auth_nest( fn auth_nest(
#[with(&["--auth", "/@user:pass@user2:pass2", "--auth", "/dir1@user3:pass3", "-A"])] #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "--auth", "user3:pass3@/dir1:rw", "-A"])]
server: TestServer, server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}dir1/file1", server.url()); let url = format!("{}dir1/file1", server.url());
@ -97,7 +98,8 @@ fn auth_nest(
#[rstest] #[rstest]
fn auth_nest_share( fn auth_nest_share(
#[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, #[with(&["--auth", "@/", "--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])]
server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
@ -106,8 +108,8 @@ fn auth_nest_share(
} }
#[rstest] #[rstest]
#[case(server(&["--auth", "/@user:pass", "--auth-method", "basic", "-A"]), "user", "pass")] #[case(server(&["--auth", "user:pass@/:rw", "--auth-method", "basic", "-A"]), "user", "pass")]
#[case(server(&["--auth", "/@u1:p1", "--auth-method", "basic", "-A"]), "u1", "p1")] #[case(server(&["--auth", "u1:p1@/:rw", "--auth-method", "basic", "-A"]), "u1", "p1")]
fn auth_basic( fn auth_basic(
#[case] server: TestServer, #[case] server: TestServer,
#[case] user: &str, #[case] user: &str,
@ -126,7 +128,8 @@ fn auth_basic(
#[rstest] #[rstest]
fn auth_webdav_move( fn auth_webdav_move(
#[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])]
server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let origin_url = format!("{}dir1/test.html", server.url()); let origin_url = format!("{}dir1/test.html", server.url());
let new_url = format!("{}test2.html", server.url()); let new_url = format!("{}test2.html", server.url());
@ -139,7 +142,8 @@ fn auth_webdav_move(
#[rstest] #[rstest]
fn auth_webdav_copy( fn auth_webdav_copy(
#[with(&["--auth", "/@user:pass@*", "--auth", "/dir1@user3:pass3", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "--auth", "user3:pass3@/dir1:rw", "-A"])]
server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let origin_url = format!("{}dir1/test.html", server.url()); let origin_url = format!("{}dir1/test.html", server.url());
let new_url = format!("{}test2.html", server.url()); let new_url = format!("{}test2.html", server.url());
@ -152,7 +156,7 @@ fn auth_webdav_copy(
#[rstest] #[rstest]
fn auth_path_prefix( fn auth_path_prefix(
#[with(&["--auth", "/@user:pass", "--path-prefix", "xyz", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "--path-prefix", "xyz", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}xyz/index.html", server.url()); let url = format!("{}xyz/index.html", server.url());
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
@ -161,3 +165,22 @@ fn auth_path_prefix(
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
} }
#[rstest]
fn auth_partial_index(
#[with(&["--auth", "user:pass@/dir1:rw,/dir2:rw", "-A"])] server: TestServer,
) -> Result<(), Error> {
let resp = fetch!(b"GET", server.url()).send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?);
assert_eq!(paths, IndexSet::from(["dir1/".into(), "dir2/".into()]));
let resp = fetch!(b"GET", format!("{}?q={}", server.url(), "test.html"))
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?);
assert_eq!(
paths,
IndexSet::from(["dir1/test.html".into(), "dir2/test.html".into()])
);
Ok(())
}

View file

@ -11,8 +11,8 @@ use std::io::Read;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
#[rstest] #[rstest]
#[case(&["-a", "/@user:pass", "--log-format", "$remote_user"], false)] #[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], false)]
#[case(&["-a", "/@user:pass", "--log-format", "$remote_user", "--auth-method", "basic"], true)] #[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user", "--auth-method", "basic"], true)]
fn log_remote_user( fn log_remote_user(
tmpdir: TempDir, tmpdir: TempDir,
port: u16, port: u16,

View file

@ -53,7 +53,7 @@ fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Re
let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?; let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?;
assert_eq!(resp.text()?, "This is index.html"); assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?; let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?;
assert_eq!(resp.status(), 404); assert_eq!(resp.status(), 403);
child.kill()?; child.kill()?;
Ok(()) Ok(())