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"
chardetng = "0.1"
glob = "0.3.1"
indexmap = "1.9"
[features]
default = ["tls"]
@ -59,7 +60,6 @@ regex = "1"
url = "2"
diqwest = { version = "1", features = ["blocking"] }
predicates = "3"
indexmap = "1.9"
[profile.release]
lto = true

View file

@ -78,7 +78,7 @@ pub fn build_cli() -> Command {
.long("auth")
.help("Add auth for path")
.action(ArgAction::Append)
.value_delimiter(',')
.value_delimiter('|')
.value_name("rules"),
)
.arg(
@ -288,7 +288,7 @@ impl Args {
"basic" => AuthMethod::Basic,
_ => 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_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete");
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 headers::HeaderValue;
use hyper::Method;
use indexmap::IndexMap;
use lazy_static::lazy_static;
use md5::Context;
use std::collections::HashMap;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use uuid::Uuid;
use crate::utils::{encode_uri, unix_now};
use crate::utils::unix_now;
const REALM: &str = "DUFS";
const DIGEST_AUTH_TIMEOUT: u32 = 86400;
@ -21,57 +25,63 @@ lazy_static! {
};
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct AccessControl {
rules: HashMap<String, PathControl>,
}
#[derive(Debug)]
pub struct PathControl {
readwrite: Account,
readonly: Option<Account>,
share: bool,
users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>,
}
impl AccessControl {
pub fn new(raw_rules: &[&str], uri_prefix: &str) -> Result<Self> {
let mut rules = HashMap::default();
pub fn new(raw_rules: &[&str]) -> Result<Self> {
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 {
let parts: Vec<&str> = rule.split('@').collect();
let create_err = || anyhow!("Invalid auth `{rule}`");
match parts.as_slice() {
[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);
let (user, list) = rule.split_once('@').ok_or_else(|| create_err(rule))?;
if user.is_empty() && anony.is_some() {
bail!("Invalid auth, duplicate anonymous rules");
}
[path, readwrite, readonly] => {
let (readonly, share) = if *readonly == "*" {
(None, true)
let mut paths = AccessPaths::default();
for value in list.trim_matches(',').split(',') {
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 {
(Some(Account::new(readonly).ok_or_else(create_err)?), false)
};
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()),
return Err(create_err(rule));
}
}
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 {
!self.rules.is_empty()
!self.users.is_empty() || self.anony.is_some()
}
pub fn guard(
@ -80,81 +90,157 @@ impl AccessControl {
method: &Method,
authorization: Option<&HeaderValue>,
auth_method: AuthMethod,
) -> GuardType {
if self.rules.is_empty() {
return GuardType::ReadWrite;
) -> (Option<String>, Option<AccessPaths>) {
if let Some(authorization) = authorization {
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 {
return GuardType::ReadOnly;
return (None, Some(AccessPaths::new(AccessPerm::ReadOnly)));
}
let mut controls = vec![];
for path in walk_path(path) {
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;
if let Some(paths) = self.anony.as_ref() {
return (None, paths.find(path, !is_readonly_method(method)));
}
}
}
}
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
(None, None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum GuardType {
Reject,
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct AccessPaths {
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,
ReadOnly,
}
impl GuardType {
pub fn is_reject(&self) -> bool {
*self == GuardType::Reject
}
impl AccessPerm {
pub fn readwrite(&self) -> bool {
self == &AccessPerm::ReadWrite
}
fn sanitize_path(path: &str, uri_prefix: &str) -> String {
let new_path = match (uri_prefix, path) {
("/", "/") => "/".into(),
(_, "/") => uri_prefix.trim_end_matches('/').into(),
_ => format!("{}{}", uri_prefix, path.trim_matches('/')),
};
encode_uri(&new_path)
pub fn indexonly(&self) -> bool {
self == &AccessPerm::IndexOnly
}
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 {
@ -164,29 +250,6 @@ fn is_readonly_method(method: &Method) -> bool {
|| 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)]
pub enum AuthMethod {
Basic,
@ -208,6 +271,7 @@ impl AuthMethod {
}
}
}
pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> {
match self {
AuthMethod::Basic => {
@ -227,7 +291,8 @@ impl AuthMethod {
}
}
}
pub fn validate(
fn check(
&self,
authorization: &HeaderValue,
method: &str,
@ -245,12 +310,7 @@ impl AuthMethod {
return None;
}
let mut h = Context::new();
h.consume(format!("{}:{}:{}", parts[0], REALM, parts[1]).as_bytes());
let http_pass = format!("{:x}", h.compute());
if http_pass == auth_pass {
if parts[1] == auth_pass {
return Some(());
}
@ -273,6 +333,11 @@ impl AuthMethod {
if auth_user != username {
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();
ha.consume(method);
ha.consume(b":");
@ -285,7 +350,7 @@ impl AuthMethod {
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(&auth_pass);
c.consume(b":");
c.consume(nonce);
c.consume(b":");
@ -308,7 +373,7 @@ impl AuthMethod {
Some(r) => r,
None => {
let mut c = Context::new();
c.consume(auth_pass);
c.consume(&auth_pass);
c.consume(b":");
c.consume(nonce);
c.consume(b":");
@ -317,7 +382,6 @@ impl AuthMethod {
}
};
if correct_response.as_bytes() == *user_response {
// grant access
return Some(());
}
}
@ -417,3 +481,42 @@ fn create_nonce() -> Result<String> {
let n = format!("{:08x}{:032x}", secs, h.compute());
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::utils::{
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 guard_type = self.args.auth.guard(
req_path,
let relative_path = match self.resolve_path(req_path) {
Some(v) => v,
None => {
status_forbid(&mut res);
return Ok(res);
}
};
let guard = self.args.auth.guard(
&relative_path,
&method,
authorization,
self.args.auth_method.clone(),
);
if guard_type.is_reject() {
let (user, access_paths) = match guard {
(None, None) => {
self.auth_reject(&mut 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_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
@ -171,8 +190,7 @@ impl Server {
}
return Ok(res);
}
let path = match self.extract_path(req_path) {
let path = match self.join_path(&relative_path) {
Some(v) => v,
None => {
status_forbid(&mut res);
@ -209,31 +227,38 @@ impl Server {
status_not_found(&mut 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") {
let user = self.retrieve_user(authorization);
self.handle_search_dir(path, &query_params, head_only, user, &mut res)
self.handle_search_dir(
path,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?;
} else {
let user = self.retrieve_user(authorization);
self.handle_render_index(
path,
&query_params,
headers,
head_only,
user,
access_paths,
&mut res,
)
.await?;
}
} else if render_index || render_spa {
let user = self.retrieve_user(authorization);
self.handle_render_index(
path,
&query_params,
headers,
head_only,
user,
access_paths,
&mut res,
)
.await?;
@ -242,19 +267,32 @@ impl Server {
status_not_found(&mut 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") {
let user = self.retrieve_user(authorization);
self.handle_search_dir(path, &query_params, head_only, user, &mut res)
self.handle_search_dir(
path,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?;
} else {
let user = self.retrieve_user(authorization);
self.handle_ls_dir(path, true, &query_params, head_only, user, &mut res)
self.handle_ls_dir(
path,
true,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?;
}
} else if is_file {
if query_params.contains_key("edit") {
let user = self.retrieve_user(authorization);
self.handle_edit_file(path, head_only, user, &mut res)
.await?;
} else {
@ -265,8 +303,15 @@ impl Server {
self.handle_render_spa(path, headers, head_only, &mut res)
.await?;
} else if allow_upload && req_path.ends_with('/') {
let user = self.retrieve_user(authorization);
self.handle_ls_dir(path, false, &query_params, head_only, user, &mut res)
self.handle_ls_dir(
path,
false,
&query_params,
head_only,
user,
access_paths,
&mut res,
)
.await?;
} else {
status_not_found(&mut res);
@ -294,7 +339,8 @@ impl Server {
method => match method.as_str() {
"PROPFIND" => {
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 {
self.handle_propfind_file(path, &mut res).await?;
} else {
@ -401,11 +447,12 @@ impl Server {
query_params: &HashMap<String, String>,
head_only: bool,
user: Option<String>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let mut paths = vec![];
if exist {
paths = match self.list_dir(path, path).await {
paths = match self.list_dir(path, path, access_paths).await {
Ok(paths) => paths,
Err(_) => {
status_forbid(res);
@ -422,6 +469,7 @@ impl Server {
query_params: &HashMap<String, String>,
head_only: bool,
user: Option<String>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let mut paths: Vec<PathItem> = vec![];
@ -435,8 +483,9 @@ impl Server {
let hidden = hidden.clone();
let running = self.running.clone();
let search_paths = tokio::task::spawn_blocking(move || {
let mut it = WalkDir::new(&path_buf).into_iter();
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() {
if !running.load(Ordering::SeqCst) {
break;
@ -466,6 +515,7 @@ impl Server {
}
paths.push(entry_path.to_path_buf());
}
}
paths
})
.await?;
@ -478,7 +528,13 @@ impl Server {
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 filename = try_get_file_name(path)?;
set_content_diposition(res, false, &format!("{}.zip", filename))?;
@ -491,7 +547,7 @@ impl Server {
let hidden = self.args.hidden.clone();
let running = self.running.clone();
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);
}
});
@ -507,6 +563,7 @@ impl Server {
headers: &HeaderMap<HeaderValue>,
head_only: bool,
user: Option<String>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let index_path = path.join(INDEX_NAME);
@ -519,7 +576,7 @@ impl Server {
self.handle_send_file(&index_path, headers, head_only, res)
.await?;
} 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?;
} else {
status_not_found(res)
@ -724,6 +781,7 @@ impl Server {
&self,
path: &Path,
headers: &HeaderMap<HeaderValue>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let depth: u32 = match headers.get("depth") {
@ -741,7 +799,7 @@ impl Server {
None => vec![],
};
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),
Err(_) => {
status_forbid(res);
@ -965,19 +1023,32 @@ impl Server {
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 guard_type = self.args.auth.guard(
&dest_path,
let guard = self.args.auth.guard(
&relative_path,
req.method(),
authorization,
self.args.auth_method.clone(),
);
if guard_type.is_reject() {
match guard {
(_, Some(_)) => {}
_ => {
status_forbid(res);
return None;
}
};
let dest = match self.extract_path(&dest_path) {
let dest = match self.join_path(&relative_path) {
Some(dest) => dest,
None => {
*res.status_mut() = StatusCode::BAD_REQUEST;
@ -994,49 +1065,61 @@ impl Server {
Some(uri.path().to_string())
}
fn extract_path(&self, path: &str) -> Option<PathBuf> {
let mut slash_stripped_path = path;
while let Some(p) = slash_stripped_path.strip_prefix('/') {
slash_stripped_path = p
fn resolve_path(&self, path: &str) -> Option<String> {
let path = path.trim_matches('/');
let path = decode_uri(path)?;
let prefix = self.args.path_prefix.as_str();
if prefix == "/" {
return Some(path.to_string());
}
let decoded_path = decode_uri(slash_stripped_path)?;
let slashes_switched = if cfg!(windows) {
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))
path.strip_prefix(prefix.trim_start_matches('/'))
.map(|v| v.trim_matches('/').to_string())
}
fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> {
let path = path.as_ref();
if self.args.path_prefix.is_empty() {
Some(path)
} else {
path.strip_prefix(&self.args.path_prefix).ok()
fn join_path(&self, path: &str) -> Option<PathBuf> {
if path.is_empty() {
return Some(self.args.path.clone());
}
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![];
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?;
while let Ok(Some(entry)) = rd.next_entry().await {
let entry_path = entry.path();
let base_name = get_file_name(&entry_path);
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);
self.add_pathitem(&mut paths, base_path, &entry_path).await;
}
}
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>> {
let path = path.as_ref();
let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path));
@ -1066,10 +1149,6 @@ impl Server {
size,
}))
}
fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option<String> {
self.args.auth_method.get_user(authorization?)
}
}
#[derive(Debug, Serialize)]
@ -1237,16 +1316,18 @@ fn res_multistatus(res: &mut Response, content: &str) {
async fn zip_dir<W: AsyncWrite + Unpin>(
writer: &mut W,
dir: &Path,
access_paths: AccessPaths,
hidden: &[String],
running: Arc<AtomicBool>,
) -> Result<()> {
let mut writer = ZipFileWriter::new(writer);
let hidden = Arc::new(hidden.to_vec());
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 mut it = WalkDir::new(&dir_path_buf).into_iter();
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() {
if !running.load(Ordering::SeqCst) {
break;
@ -1279,6 +1360,7 @@ async fn zip_dir<W: AsyncWrite + Unpin>(
}
paths.push(entry_path.to_path_buf());
}
}
paths
})
.await?;

View file

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