anyhow = "1.0"
chardetng = "0.1"
glob = "0.3.1"
+indexmap = "1.9"
[features]
default = ["tls"]
url = "2"
diqwest = { version = "1", features = ["blocking"] }
predicates = "3"
-indexmap = "1.9"
[profile.release]
lto = true
.long("auth")
.help("Add auth for path")
.action(ArgAction::Append)
- .value_delimiter(',')
+ .value_delimiter('|')
.value_name("rules"),
)
.arg(
"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");
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;
};
}
-#[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");
+ }
+ 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));
}
- [path, readwrite, readonly] => {
- let (readonly, share) = if *readonly == "*" {
- (None, true)
- } 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);
+ 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));
}
- _ => return Err(create_err()),
+ users.insert(user.to_string(), (pass.to_string(), paths));
+ } else {
+ return Err(create_err(rule));
+ }
+ }
+ for (path, perm) in anony_paths {
+ for (_, (_, paths)) in users.iter_mut() {
+ paths.add(path, perm)
}
}
- Ok(Self { rules })
+ Ok(Self { users, anony })
}
pub fn valid(&self) -> bool {
- !self.rules.is_empty()
+ !self.users.is_empty() || self.anony.is_some()
}
pub fn guard(
method: &Method,
authorization: Option<&HeaderValue>,
auth_method: AuthMethod,
- ) -> GuardType {
- if self.rules.is_empty() {
- return GuardType::ReadWrite;
- }
-
- if method == Method::OPTIONS {
- return GuardType::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;
+ ) -> (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
- .validate(authorization, method.as_str(), user, pass)
+ .check(authorization, method.as_str(), &user, pass)
.is_some()
{
- return GuardType::ReadWrite;
+ return (Some(user), paths.find(path, !is_readonly_method(method)));
+ } else {
+ return (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;
- }
- }
+
+ if method == Method::OPTIONS {
+ return (None, Some(AccessPaths::new(AccessPerm::ReadOnly)));
+ }
+
+ if let Some(paths) = self.anony.as_ref() {
+ return (None, paths.find(path, !is_readonly_method(method)));
+ }
+
+ (None, None)
+ }
+}
+
+#[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)
+ }
}
- GuardType::Reject
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub enum GuardType {
- Reject,
+#[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)
-}
-
-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
- })
+ pub fn indexonly(&self) -> bool {
+ self == &AccessPerm::IndexOnly
+ }
}
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,
}
}
}
+
pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> {
match self {
AuthMethod::Basic => {
}
}
}
- pub fn validate(
+
+ fn check(
&self,
authorization: &HeaderValue,
method: &str,
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(());
}
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":");
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":");
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":");
}
};
if correct_response.as_bytes() == *user_response {
- // grant access
return Some(());
}
}
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());
+ }
+}
+#![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,
}
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() {
- self.auth_reject(&mut res)?;
- return Ok(res);
- }
+
+ 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())
}
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);
status_not_found(&mut res);
return Ok(res);
}
- self.handle_zip_dir(path, head_only, &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_zip_dir(path, head_only, access_paths, &mut res)
.await?;
+ } else if allow_search && query_params.contains_key("q") {
+ 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?;
status_not_found(&mut res);
return Ok(res);
}
- self.handle_zip_dir(path, head_only, &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_zip_dir(path, head_only, access_paths, &mut res)
.await?;
+ } else if allow_search && query_params.contains_key("q") {
+ 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)
- .await?;
+ 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 {
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)
- .await?;
+ self.handle_ls_dir(
+ path,
+ false,
+ &query_params,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
} else {
status_not_found(&mut res);
}
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 {
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);
query_params: &HashMap<String, String>,
head_only: bool,
user: Option<String>,
+ access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let mut paths: Vec<PathItem> = vec![];
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![];
- while let Some(Ok(entry)) = it.next() {
- if !running.load(Ordering::SeqCst) {
- break;
- }
- let entry_path = entry.path();
- let base_name = get_file_name(entry_path);
- let file_type = entry.file_type();
- let mut is_dir_type: bool = file_type.is_dir();
- if file_type.is_symlink() {
- match std::fs::symlink_metadata(entry_path) {
- Ok(meta) => {
- is_dir_type = meta.is_dir();
+ 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;
+ }
+ let entry_path = entry.path();
+ let base_name = get_file_name(entry_path);
+ let file_type = entry.file_type();
+ let mut is_dir_type: bool = file_type.is_dir();
+ if file_type.is_symlink() {
+ match std::fs::symlink_metadata(entry_path) {
+ Ok(meta) => {
+ is_dir_type = meta.is_dir();
+ }
+ Err(_) => {
+ continue;
+ }
}
- Err(_) => {
- continue;
+ }
+ if is_hidden(&hidden, base_name, is_dir_type) {
+ if file_type.is_dir() {
+ it.skip_current_dir();
}
+ continue;
}
- }
- if is_hidden(&hidden, base_name, is_dir_type) {
- if file_type.is_dir() {
- it.skip_current_dir();
+ if !base_name.to_lowercase().contains(&search) {
+ continue;
}
- continue;
- }
- if !base_name.to_lowercase().contains(&search) {
- continue;
+ paths.push(entry_path.to_path_buf());
}
- paths.push(entry_path.to_path_buf());
}
paths
})
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))?;
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);
}
});
headers: &HeaderMap<HeaderValue>,
head_only: bool,
user: Option<String>,
+ access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let index_path = path.join(INDEX_NAME);
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)
&self,
path: &Path,
headers: &HeaderMap<HeaderValue>,
+ access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
let depth: u32 = match headers.get("depth") {
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);
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() {
- status_forbid(res);
- return None;
- }
- let dest = match self.extract_path(&dest_path) {
+ match guard {
+ (_, Some(_)) => {}
+ _ => {
+ status_forbid(res);
+ return None;
+ }
+ };
+
+ let dest = match self.join_path(&relative_path) {
Some(dest) => dest,
None => {
*res.status_mut() = StatusCode::BAD_REQUEST;
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());
+ }
+ path.strip_prefix(prefix.trim_start_matches('/'))
+ .map(|v| v.trim_matches('/').to_string())
+ }
+
+ fn join_path(&self, path: &str) -> Option<PathBuf> {
+ if path.is_empty() {
+ return Some(self.args.path.clone());
}
- let decoded_path = decode_uri(slash_stripped_path)?;
- let slashes_switched = if cfg!(windows) {
- decoded_path.replace('/', "\\")
+ let path = if cfg!(windows) {
+ path.replace('/', "\\")
} else {
- decoded_path.into_owned()
+ path.to_string()
};
- let stripped_path = match self.strip_path_prefix(&slashes_switched) {
- Some(path) => path,
- None => return None,
- };
- Some(self.args.path.join(stripped_path))
+ Some(self.args.path.join(path))
}
- 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)
+ 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 {
- path.strip_prefix(&self.args.path_prefix).ok()
+ let mut rd = fs::read_dir(entry_path).await?;
+ while let Ok(Some(entry)) = rd.next_entry().await {
+ let entry_path = entry.path();
+ self.add_pathitem(&mut paths, base_path, &entry_path).await;
+ }
}
+ Ok(paths)
}
- async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> Result<Vec<PathItem>> {
- let mut paths: Vec<PathItem> = vec![];
- 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);
+ 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);
}
- Ok(paths)
}
async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
size,
}))
}
-
- fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option<String> {
- self.args.auth_method.get_user(authorization?)
- }
}
#[derive(Debug, Serialize)]
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![];
- while let Some(Ok(entry)) = it.next() {
- if !running.load(Ordering::SeqCst) {
- break;
- }
- let entry_path = entry.path();
- let base_name = get_file_name(entry_path);
- let file_type = entry.file_type();
- let mut is_dir_type: bool = file_type.is_dir();
- if file_type.is_symlink() {
- match std::fs::symlink_metadata(entry_path) {
- Ok(meta) => {
- is_dir_type = meta.is_dir();
+ 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;
+ }
+ let entry_path = entry.path();
+ let base_name = get_file_name(entry_path);
+ let file_type = entry.file_type();
+ let mut is_dir_type: bool = file_type.is_dir();
+ if file_type.is_symlink() {
+ match std::fs::symlink_metadata(entry_path) {
+ Ok(meta) => {
+ is_dir_type = meta.is_dir();
+ }
+ Err(_) => {
+ continue;
+ }
}
- Err(_) => {
- continue;
+ }
+ if is_hidden(&hidden, base_name, is_dir_type) {
+ if file_type.is_dir() {
+ it.skip_current_dir();
}
+ continue;
}
- }
- if is_hidden(&hidden, base_name, is_dir_type) {
- if file_type.is_dir() {
- it.skip_current_dir();
+ if entry.path().symlink_metadata().is_err() {
+ continue;
}
- continue;
- }
- if entry.path().symlink_metadata().is_err() {
- continue;
- }
- if !file_type.is_file() {
- continue;
+ if !file_type.is_file() {
+ continue;
+ }
+ paths.push(entry_path.to_path_buf());
}
- paths.push(entry_path.to_path_buf());
}
paths
})
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"));
}
#[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);
}
#[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(())
#[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()?;
#[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(())
#[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()?;
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());
#[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()?;
}
#[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,
#[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());
#[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());
#[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()?;
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(())
+}
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,
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(())