Compare commits

...

12 Commits

Author SHA1 Message Date
sigoden
a118c1348e fix: ensure symlink inside serve root (#670) 2026-02-05 22:08:04 +08:00
sigoden
db7a0530a2 fix: escape filename in ?simple output (#669) 2026-02-05 19:13:10 +08:00
sigoden
bc27c8c479 fix: some search results missing due to broken symlinks (#665) 2026-01-21 12:00:57 +08:00
sigoden
2b2c7bd5f7 feat: add option --allow-hash to allow/disallow file hashing (#657) 2026-01-09 16:43:18 +08:00
sigoden
ca18df1a36 refactor: improve UI botton titles (#656) 2026-01-09 16:21:40 +08:00
sigoden
7cfb97dfdf refactor: update deps (#655) 2026-01-08 12:02:41 +08:00
sigoden
23619033ae chore: release v0.45.0 (#626) 2025-09-04 18:10:54 +08:00
sigoden
db75ba4357 fix: verify token length (#627) 2025-09-02 18:48:17 +08:00
sigoden
4016715187 fix: login btn does not work for readonly annoymous (#620) 2025-08-19 08:58:59 +08:00
sigoden
f8a7873582 fix: perms on dufs -A -a @/:ro (#619) 2025-08-19 07:51:52 +08:00
sigoden
7f8269881d feat: log decoded uri (#615) 2025-08-18 09:00:18 +08:00
sigoden
b2f244a4cf feat: make dir urls inherit ?noscript params (#614) 2025-08-16 07:36:19 +08:00
22 changed files with 861 additions and 536 deletions

View File

@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.45.0] - 2025-09-03
### Bug Fixes
- Perms on `dufs -A -a @/:ro` ([#619](https://github.com/sigoden/dufs/issues/619))
- Login btn does not work for readonly anonymous ([#620](https://github.com/sigoden/dufs/issues/620))
- Verify token length ([#627](https://github.com/sigoden/dufs/issues/627))
### Features
- Make dir urls inherit `?noscript` params ([#614](https://github.com/sigoden/dufs/issues/614))
- Log decoded uri ([#615](https://github.com/sigoden/dufs/issues/615))
## [0.44.0] - 2025-08-02 ## [0.44.0] - 2025-08-02
### Bug Fixes ### Bug Fixes

1103
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dufs" name = "dufs"
version = "0.44.0" version = "0.45.0"
edition = "2021" edition = "2021"
authors = ["sigoden <sigoden@gmail.com>"] authors = ["sigoden <sigoden@gmail.com>"]
description = "Dufs is a distinctive utility file server" description = "Dufs is a distinctive utility file server"
@@ -25,13 +25,12 @@ async_zip = { version = "0.0.18", default-features = false, features = ["deflate
headers = "0.4" headers = "0.4"
mime_guess = "2.0" mime_guess = "2.0"
if-addrs = "0.14" if-addrs = "0.14"
rustls-pemfile = { version = "2.0", optional = true } tokio-rustls = { version = "0.26", optional = true }
tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring", "tls12"]}
md5 = "0.8" md5 = "0.8"
lazy_static = "1.4" lazy_static = "1.4"
uuid = { version = "1.7", features = ["v4", "fast-rng"] } uuid = { version = "1.7", features = ["v4", "fast-rng"] }
urlencoding = "2.1" urlencoding = "2.1"
xml-rs = "0.8" xml-rs = "1.0.0"
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
socket2 = "0.6" socket2 = "0.6"
async-stream = "0.3" async-stream = "0.3"
@@ -58,11 +57,11 @@ hex = "0.4.3"
[features] [features]
default = ["tls"] default = ["tls"]
tls = ["rustls-pemfile", "tokio-rustls"] tls = ["tokio-rustls"]
[dev-dependencies] [dev-dependencies]
assert_cmd = "2" assert_cmd = "2"
reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false } reqwest = { version = "0.13", features = ["blocking", "multipart", "rustls"], default-features = false }
assert_fs = "1" assert_fs = "1"
port_check = "0.3" port_check = "0.3"
rstest = "0.26.1" rstest = "0.26.1"

View File

@@ -67,6 +67,7 @@ Options:
--allow-search Allow search files/folders --allow-search Allow search files/folders
--allow-symlink Allow symlink to files/folders outside root directory --allow-symlink Allow symlink to files/folders outside root directory
--allow-archive Allow download folders as archive file --allow-archive Allow download folders as archive file
--allow-hash Allow ?hash query to get file sha256 hash
--enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *` --enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *`
--render-index Serve index.html when requesting a directory, returns 404 if not found index.html --render-index Serve index.html when requesting a directory, returns 404 if not found index.html
--render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html --render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
@@ -346,6 +347,7 @@ All options can be set using environment variables prefixed with `DUFS_`.
--allow-search DUFS_ALLOW_SEARCH=true --allow-search DUFS_ALLOW_SEARCH=true
--allow-symlink DUFS_ALLOW_SYMLINK=true --allow-symlink DUFS_ALLOW_SYMLINK=true
--allow-archive DUFS_ALLOW_ARCHIVE=true --allow-archive DUFS_ALLOW_ARCHIVE=true
--allow-hash DUFS_ALLOW_HASH=true
--enable-cors DUFS_ENABLE_CORS=true --enable-cors DUFS_ENABLE_CORS=true
--render-index DUFS_RENDER_INDEX=true --render-index DUFS_RENDER_INDEX=true
--render-try-index DUFS_RENDER_TRY_INDEX=true --render-try-index DUFS_RENDER_TRY_INDEX=true
@@ -383,6 +385,7 @@ allow-delete: true
allow-search: true allow-search: true
allow-symlink: true allow-symlink: true
allow-archive: true allow-archive: true
allow-hash: true
enable-cors: true enable-cors: true
render-index: true render-index: true
render-try-index: true render-try-index: true

View File

@@ -23,7 +23,7 @@
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" /> d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" />
</svg> </svg>
</a> </a>
<div class="control move-file hidden" title="Move to new path"> <div class="control move-file hidden" title="Move & Rename">
<svg class="icon-move" width="16" height="16" viewBox="0 0 16 16"> <svg class="icon-move" width="16" height="16" viewBox="0 0 16 16">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"> d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z">
@@ -38,7 +38,7 @@
d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z" /> d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z" />
</svg> </svg>
</div> </div>
<div class="control upload-file hidden" title="Upload files"> <div class="control upload-file hidden" title="Upload files/folders">
<label for="file"> <label for="file">
<svg width="16" height="16" viewBox="0 0 16 16"> <svg width="16" height="16" viewBox="0 0 16 16">
<path <path
@@ -47,7 +47,7 @@
d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" /> d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
</svg> </svg>
</label> </label>
<input type="file" id="file" title="Upload files" name="file" multiple> <input type="file" id="file" title="Upload files/folders" name="file" multiple>
</div> </div>
<div class="control new-folder hidden" title="New folder"> <div class="control new-folder hidden" title="New folder">
<svg width="16" height="16" viewBox="0 0 16 16"> <svg width="16" height="16" viewBox="0 0 16 16">

View File

@@ -465,7 +465,7 @@ function addPath(file, index) {
} }
if (DATA.allow_delete) { if (DATA.allow_delete) {
if (DATA.allow_upload) { if (DATA.allow_upload) {
actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move to new path">${ICONS.move}</div>`; actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move & Rename">${ICONS.move}</div>`;
if (!isDir) { if (!isDir) {
actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`; actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`;
} }
@@ -534,7 +534,7 @@ async function setupAuth() {
$loginBtn.classList.remove("hidden"); $loginBtn.classList.remove("hidden");
$loginBtn.addEventListener("click", async () => { $loginBtn.addEventListener("click", async () => {
try { try {
await checkAuth(); await checkAuth("login");
} catch { } } catch { }
location.reload(); location.reload();
}); });
@@ -782,9 +782,10 @@ async function saveChange() {
} }
} }
async function checkAuth() { async function checkAuth(variant) {
if (!DATA.auth) return; if (!DATA.auth) return;
const res = await fetch(baseUrl(), { const qs = variant ? `?${variant}` : "";
const res = await fetch(baseUrl() + qs, {
method: "CHECKAUTH", method: "CHECKAUTH",
}); });
await assertResOK(res); await assertResOK(res);

View File

@@ -148,6 +148,14 @@ pub fn build_cli() -> Command {
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
.help("Allow download folders as archive file"), .help("Allow download folders as archive file"),
) )
.arg(
Arg::new("allow-hash")
.env("DUFS_ALLOW_HASH")
.hide_env(true)
.long("allow-hash")
.action(ArgAction::SetTrue)
.help("Allow ?hash query to get file sha256 hash"),
)
.arg( .arg(
Arg::new("enable-cors") Arg::new("enable-cors")
.env("DUFS_ENABLE_CORS") .env("DUFS_ENABLE_CORS")
@@ -281,6 +289,7 @@ pub struct Args {
pub allow_search: bool, pub allow_search: bool,
pub allow_symlink: bool, pub allow_symlink: bool,
pub allow_archive: bool, pub allow_archive: bool,
pub allow_hash: bool,
pub render_index: bool, pub render_index: bool,
pub render_spa: bool, pub render_spa: bool,
pub render_try_index: bool, pub render_try_index: bool,
@@ -375,6 +384,9 @@ impl Args {
if !args.allow_symlink { if !args.allow_symlink {
args.allow_symlink = allow_all || matches.get_flag("allow-symlink"); args.allow_symlink = allow_all || matches.get_flag("allow-symlink");
} }
if !args.allow_hash {
args.allow_hash = allow_all || matches.get_flag("allow-hash");
}
if !args.allow_archive { if !args.allow_archive {
args.allow_archive = allow_all || matches.get_flag("allow-archive"); args.allow_archive = allow_all || matches.get_flag("allow-archive");
} }
@@ -492,21 +504,16 @@ impl BindAddr {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Compress { pub enum Compress {
None, None,
#[default]
Low, Low,
Medium, Medium,
High, High,
} }
impl Default for Compress {
fn default() -> Self {
Self::Low
}
}
impl ValueEnum for Compress { impl ValueEnum for Compress {
fn value_variants<'a>() -> &'a [Self] { fn value_variants<'a>() -> &'a [Self] {
&[Self::None, Self::Low, Self::Medium, Self::High] &[Self::None, Self::Low, Self::Medium, Self::High]

View File

@@ -30,6 +30,7 @@ lazy_static! {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct AccessControl { pub struct AccessControl {
empty: bool,
use_hashed_password: bool, use_hashed_password: bool,
users: IndexMap<String, (String, AccessPaths)>, users: IndexMap<String, (String, AccessPaths)>,
anonymous: Option<AccessPaths>, anonymous: Option<AccessPaths>,
@@ -38,6 +39,7 @@ pub struct AccessControl {
impl Default for AccessControl { impl Default for AccessControl {
fn default() -> Self { fn default() -> Self {
AccessControl { AccessControl {
empty: true,
use_hashed_password: false, use_hashed_password: false,
users: IndexMap::new(), users: IndexMap::new(),
anonymous: Some(AccessPaths::new(AccessPerm::ReadWrite)), anonymous: Some(AccessPaths::new(AccessPerm::ReadWrite)),
@@ -48,7 +50,7 @@ impl Default for AccessControl {
impl AccessControl { impl AccessControl {
pub fn new(raw_rules: &[&str]) -> Result<Self> { pub fn new(raw_rules: &[&str]) -> Result<Self> {
if raw_rules.is_empty() { if raw_rules.is_empty() {
return Ok(Default::default()); return Ok(Self::default());
} }
let new_raw_rules = split_rules(raw_rules); let new_raw_rules = split_rules(raw_rules);
let mut use_hashed_password = false; let mut use_hashed_password = false;
@@ -93,13 +95,14 @@ impl AccessControl {
} }
Ok(Self { Ok(Self {
empty: false,
use_hashed_password, use_hashed_password,
users, users,
anonymous, anonymous,
}) })
} }
pub fn exist(&self) -> bool { pub fn has_users(&self) -> bool {
!self.users.is_empty() !self.users.is_empty()
} }
@@ -111,7 +114,7 @@ impl AccessControl {
token: Option<&String>, token: Option<&String>,
guard_options: bool, guard_options: bool,
) -> (Option<String>, Option<AccessPaths>) { ) -> (Option<String>, Option<AccessPaths>) {
if self.users.is_empty() { if self.empty {
return (None, Some(AccessPaths::new(AccessPerm::ReadWrite))); return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
} }
@@ -170,6 +173,10 @@ impl AccessControl {
fn verify_token<'a>(&'a self, token: &str, path: &str) -> Result<(String, &'a AccessPaths)> { fn verify_token<'a>(&'a self, token: &str, path: &str) -> Result<(String, &'a AccessPaths)> {
let raw = hex::decode(token)?; let raw = hex::decode(token)?;
if raw.len() < 72 {
bail!("Invalid token");
}
let sig_bytes = &raw[..64]; let sig_bytes = &raw[..64];
let exp_bytes = &raw[64..72]; let exp_bytes = &raw[64..72];
let user_bytes = &raw[72..]; let user_bytes = &raw[72..];

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use crate::{auth::get_auth_user, server::Request}; use crate::{auth::get_auth_user, server::Request, utils::decode_uri};
pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#; pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
@@ -29,7 +29,9 @@ impl HttpLogger {
match element { match element {
LogElement::Variable(name) => match name.as_str() { LogElement::Variable(name) => match name.as_str() {
"request" => { "request" => {
data.insert(name.to_string(), format!("{} {}", req.method(), req.uri())); let uri = req.uri().to_string();
let uri = decode_uri(&uri).map(|s| s.to_string()).unwrap_or(uri);
data.insert(name.to_string(), format!("{} {uri}", req.method()));
} }
"remote_user" => { "remote_user" => {
if let Some(user) = if let Some(user) =
@@ -50,6 +52,7 @@ impl HttpLogger {
} }
data data
} }
pub fn log(&self, data: &HashMap<String, String>, err: Option<String>) { pub fn log(&self, data: &HashMap<String, String>, err: Option<String>) {
if self.elements.is_empty() { if self.elements.is_empty() {
return; return;

View File

@@ -55,17 +55,20 @@ pub fn generate_noscript_html(data: &IndexData) -> Result<String> {
fn render_parent() -> String { fn render_parent() -> String {
let value = "../"; let value = "../";
format!("<tr><td><a href=\"{value}\">{value}</a></td><td></td><td></td></tr>") format!("<tr><td><a href=\"{value}?noscript\">{value}</a></td><td></td><td></td></tr>")
} }
fn render_path_item(path: &PathItem) -> String { fn render_path_item(path: &PathItem) -> String {
let href = encode_uri(&path.name); let mut href = encode_uri(&path.name);
let suffix = if path.path_type.is_dir() { "/" } else { "" }; let mut name = escape_str_pcdata(&path.name).to_string();
let name = escape_str_pcdata(&path.name); if path.path_type.is_dir() {
href.push_str("/?noscript");
name.push('/');
};
let mtime = format_mtime(path.mtime).unwrap_or_default(); let mtime = format_mtime(path.mtime).unwrap_or_default();
let size = format_size(path.size, path.path_type); let size = format_size(path.size, path.path_type);
format!("<tr><td><a href=\"{href}{suffix}\">{name}{suffix}</a></td><td>{mtime}</td><td>{size}</td></tr>") format!("<tr><td><a href=\"{href}\">{name}</a></td><td>{mtime}</td><td>{size}</td></tr>")
} }
fn format_mtime(mtime: u64) -> Option<String> { fn format_mtime(mtime: u64) -> Option<String> {

View File

@@ -211,7 +211,18 @@ impl Server {
} }
if method.as_str() == "CHECKAUTH" { if method.as_str() == "CHECKAUTH" {
*res.body_mut() = body_full(user.clone().unwrap_or_default()); match user.clone() {
Some(user) => {
*res.body_mut() = body_full(user);
}
None => {
if has_query_flag(&query_params, "login") || !access_paths.perm().readwrite() {
self.auth_reject(&mut res)?
} else {
*res.body_mut() = body_full("");
}
}
}
return Ok(res); return Ok(res);
} else if method.as_str() == "LOGOUT" { } else if method.as_str() == "LOGOUT" {
self.auth_reject(&mut res)?; self.auth_reject(&mut res)?;
@@ -261,7 +272,7 @@ impl Server {
let render_spa = self.args.render_spa; let render_spa = self.args.render_spa;
let render_try_index = self.args.render_try_index; let render_try_index = self.args.render_try_index;
if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await { if self.guard_root_contained(path).await {
status_not_found(&mut res); status_not_found(&mut res);
return Ok(res); return Ok(res);
} }
@@ -347,7 +358,11 @@ impl Server {
self.handle_edit_file(path, DataKind::View, head_only, user, &mut res) self.handle_edit_file(path, DataKind::View, head_only, user, &mut res)
.await?; .await?;
} else if has_query_flag(&query_params, "hash") { } else if has_query_flag(&query_params, "hash") {
self.handle_hash_file(path, head_only, &mut res).await?; if self.args.allow_hash {
self.handle_hash_file(path, head_only, &mut res).await?;
} else {
status_forbid(&mut res);
}
} else { } else {
self.handle_send_file(path, headers, head_only, &mut res) self.handle_send_file(path, headers, head_only, &mut res)
.await?; .await?;
@@ -962,7 +977,7 @@ impl Server {
uri_prefix: self.args.uri_prefix.clone(), uri_prefix: self.args.uri_prefix.clone(),
allow_upload: self.args.allow_upload, allow_upload: self.args.allow_upload,
allow_delete: self.args.allow_delete, allow_delete: self.args.allow_delete,
auth: self.args.auth.exist(), auth: self.args.auth.has_users(),
user, user,
editable, editable,
}; };
@@ -1099,6 +1114,11 @@ impl Server {
ensure_path_parent(&dest).await?; ensure_path_parent(&dest).await?;
if self.guard_root_contained(&dest).await {
status_bad_request(res, "Invalid Destination");
return Ok(());
}
fs::copy(path, &dest).await?; fs::copy(path, &dest).await?;
status_no_content(res); status_no_content(res);
@@ -1115,6 +1135,11 @@ impl Server {
ensure_path_parent(&dest).await?; ensure_path_parent(&dest).await?;
if self.guard_root_contained(&dest).await {
status_bad_request(res, "Invalid Destination");
return Ok(());
}
fs::rename(path, &dest).await?; fs::rename(path, &dest).await?;
status_no_content(res); status_no_content(res);
@@ -1194,10 +1219,11 @@ impl Server {
let output = paths let output = paths
.into_iter() .into_iter()
.map(|v| { .map(|v| {
let displayname = escape_str_pcdata(&v.name);
if v.is_dir() { if v.is_dir() {
format!("{}/\n", v.name) format!("{}/\n", displayname)
} else { } else {
format!("{}\n", v.name) format!("{}\n", displayname)
} }
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
@@ -1226,7 +1252,7 @@ impl Server {
allow_search: self.args.allow_search, allow_search: self.args.allow_search,
allow_archive: self.args.allow_archive, allow_archive: self.args.allow_archive,
dir_exists: exist, dir_exists: exist,
auth: self.args.auth.exist(), auth: self.args.auth.has_users(),
user, user,
paths, paths,
}; };
@@ -1273,6 +1299,21 @@ impl Server {
Ok(()) Ok(())
} }
async fn guard_root_contained(&self, path: &Path) -> bool {
if self.args.allow_symlink {
return false;
}
let path = if !fs::try_exists(path).await.unwrap_or_default() {
match path.parent() {
Some(parent) => parent.to_path_buf(),
None => return true,
}
} else {
path.to_path_buf()
};
!self.is_root_contained(path.as_path()).await
}
async fn is_root_contained(&self, path: &Path) -> bool { async fn is_root_contained(&self, path: &Path) -> bool {
fs::canonicalize(path) fs::canonicalize(path)
.await .await
@@ -1866,10 +1907,14 @@ where
for dir in access_paths.entry_paths(&path) { for dir in access_paths.entry_paths(&path) {
let mut it = WalkDir::new(&dir).follow_links(true).into_iter(); let mut it = WalkDir::new(&dir).follow_links(true).into_iter();
it.next(); it.next();
while let Some(Ok(entry)) = it.next() { while let Some(entry) = it.next() {
if !running.load(atomic::Ordering::SeqCst) { if !running.load(atomic::Ordering::SeqCst) {
break; break;
} }
let entry = match entry {
Ok(v) => v,
Err(_) => continue,
};
let entry_path = entry.path(); let entry_path = entry.path();
let base_name = get_file_name(entry_path); let base_name = get_file_name(entry_path);
let is_dir = entry.file_type().is_dir(); let is_dir = entry.file_type().is_dir();

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use rustls_pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
use std::{ use std::{
borrow::Cow, borrow::Cow,
path::Path, path::Path,
@@ -62,42 +62,40 @@ pub fn glob(pattern: &str, target: &str) -> bool {
// Load public certificate from file. // Load public certificate from file.
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
pub fn load_certs<T: AsRef<Path>>(filename: T) -> Result<Vec<CertificateDer<'static>>> { pub fn load_certs<T: AsRef<Path>>(file_name: T) -> Result<Vec<CertificateDer<'static>>> {
// Open certificate file.
let cert_file = std::fs::File::open(filename.as_ref())
.with_context(|| format!("Failed to access `{}`", filename.as_ref().display()))?;
let mut reader = std::io::BufReader::new(cert_file);
// Load and return certificate.
let mut certs = vec![]; let mut certs = vec![];
for cert in rustls_pemfile::certs(&mut reader) { for cert in CertificateDer::pem_file_iter(file_name.as_ref()).with_context(|| {
let cert = cert.with_context(|| "Failed to load certificate")?; format!(
"Failed to load cert file at `{}`",
file_name.as_ref().display()
)
})? {
let cert = cert.with_context(|| {
format!(
"Invalid certificate data in file `{}`",
file_name.as_ref().display()
)
})?;
certs.push(cert) certs.push(cert)
} }
if certs.is_empty() { if certs.is_empty() {
anyhow::bail!("No supported certificate in file"); anyhow::bail!(
"No supported certificate in file `{}`",
file_name.as_ref().display()
);
} }
Ok(certs) Ok(certs)
} }
// Load private key from file. // Load private key from file.
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
pub fn load_private_key<T: AsRef<Path>>(filename: T) -> Result<PrivateKeyDer<'static>> { pub fn load_private_key<T: AsRef<Path>>(file_name: T) -> Result<PrivateKeyDer<'static>> {
let key_file = std::fs::File::open(filename.as_ref()) PrivateKeyDer::from_pem_file(file_name.as_ref()).with_context(|| {
.with_context(|| format!("Failed to access `{}`", filename.as_ref().display()))?; format!(
let mut reader = std::io::BufReader::new(key_file); "Failed to load key file at `{}`",
file_name.as_ref().display()
// Load and return a single private key. )
for key in rustls_pemfile::read_all(&mut reader) { })
let key = key.with_context(|| "There was a problem with reading private key")?;
match key {
rustls_pemfile::Item::Pkcs1Key(key) => return Ok(PrivateKeyDer::Pkcs1(key)),
rustls_pemfile::Item::Pkcs8Key(key) => return Ok(PrivateKeyDer::Pkcs8(key)),
rustls_pemfile::Item::Sec1Key(key) => return Ok(PrivateKeyDer::Sec1(key)),
_ => {}
}
}
anyhow::bail!("No supported private key in file");
} }
pub fn parse_range(range: &str, size: u64) -> Option<Vec<(u64, u64)>> { pub fn parse_range(range: &str, size: u64) -> Option<Vec<(u64, u64)>> {

View File

@@ -1,7 +1,6 @@
mod fixtures; mod fixtures;
mod utils; mod utils;
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir; use assert_fs::fixture::TempDir;
use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer, DIR_ASSETS}; use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer, DIR_ASSETS};
use rstest::rstest; use rstest::rstest;
@@ -101,7 +100,7 @@ fn asset_js_with_prefix(
#[rstest] #[rstest]
fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> { fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -125,11 +125,29 @@ fn auth_skip_if_no_auth_user(server: TestServer) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[rstest]
fn auth_no_skip_if_anonymous(
#[with(&["--auth", "@/:ro"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url)
.basic_auth("user", Some("pass"))
.send()?;
assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 200);
let resp = fetch!(b"DELETE", &url)
.basic_auth("user", Some("pass"))
.send()?;
assert_eq!(resp.status(), 401);
Ok(())
}
#[rstest] #[rstest]
fn auth_check( fn auth_check(
#[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}", server.url());
let resp = fetch!(b"CHECKAUTH", &url).send()?; let resp = fetch!(b"CHECKAUTH", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?; let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
@@ -143,7 +161,7 @@ fn auth_check(
fn auth_check2( fn auth_check2(
#[with(&["--auth", "user:pass@/:rw|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!("{}", server.url());
let resp = fetch!(b"CHECKAUTH", &url).send()?; let resp = fetch!(b"CHECKAUTH", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?; let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
@@ -153,6 +171,18 @@ fn auth_check2(
Ok(()) Ok(())
} }
#[rstest]
fn auth_check3(
#[with(&["--auth", "user:pass@/:rw", "--auth", "@/dir1:rw", "-A"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}dir1/", server.url());
let resp = fetch!(b"CHECKAUTH", &url).send()?;
assert_eq!(resp.status(), 200);
let resp = fetch!(b"CHECKAUTH", format!("{url}?login")).send()?;
assert_eq!(resp.status(), 401);
Ok(())
}
#[rstest] #[rstest]
fn auth_logout( fn auth_logout(
#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer,

View File

@@ -12,7 +12,7 @@ use std::process::{Command, Stdio};
#[rstest] #[rstest]
#[case(&["-b", "20.205.243.166"])] #[case(&["-b", "20.205.243.166"])]
fn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> { fn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
Command::cargo_bin("dufs")? Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())
@@ -49,7 +49,7 @@ fn bind_ipv4_ipv6(
#[case(&[] as &[&str])] #[case(&[] as &[&str])]
#[case(&["--path-prefix", "/prefix"])] #[case(&["--path-prefix", "/prefix"])]
fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> { fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -11,7 +11,10 @@ use std::process::Command;
#[test] #[test]
/// Show help and exit. /// Show help and exit.
fn help_shows() -> Result<(), Error> { fn help_shows() -> Result<(), Error> {
Command::cargo_bin("dufs")?.arg("-h").assert().success(); Command::new(assert_cmd::cargo::cargo_bin!())
.arg("-h")
.assert()
.success();
Ok(()) Ok(())
} }
@@ -21,7 +24,7 @@ fn help_shows() -> Result<(), Error> {
fn print_completions() -> Result<(), Error> { fn print_completions() -> Result<(), Error> {
// let shell_enums = EnumValueParser::<Shell>::new(); // let shell_enums = EnumValueParser::<Shell>::new();
for shell in Shell::value_variants() { for shell in Shell::value_variants() {
Command::cargo_bin("dufs")? Command::new(assert_cmd::cargo::cargo_bin!())
.arg("--completions") .arg("--completions")
.arg(shell.to_string()) .arg(shell.to_string())
.assert() .assert()

View File

@@ -2,7 +2,6 @@ mod digest_auth_util;
mod fixtures; mod fixtures;
mod utils; mod utils;
use assert_cmd::prelude::*;
use assert_fs::TempDir; use assert_fs::TempDir;
use digest_auth_util::send_with_digest_auth; use digest_auth_util::send_with_digest_auth;
use fixtures::{port, tmpdir, wait_for_port, Error}; use fixtures::{port, tmpdir, wait_for_port, Error};
@@ -13,7 +12,7 @@ use std::process::{Command, Stdio};
#[rstest] #[rstest]
fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> { fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let config_path = get_config_path().display().to_string(); let config_path = get_config_path().display().to_string();
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -1,4 +1,3 @@
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir; use assert_fs::fixture::TempDir;
use assert_fs::prelude::*; use assert_fs::prelude::*;
use port_check::free_local_port; use port_check::free_local_port;
@@ -129,8 +128,7 @@ where
{ {
let port = port(); let port = port();
let tmpdir = tmpdir(); let tmpdir = tmpdir();
let child = Command::cargo_bin("dufs") let child = Command::new(assert_cmd::cargo::cargo_bin!())
.expect("Couldn't find test binary")
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -203,7 +203,7 @@ fn head_file(server: TestServer) -> Result<(), Error> {
} }
#[rstest] #[rstest]
fn hash_file(server: TestServer) -> Result<(), Error> { fn hash_file(#[with(&["--allow-hash"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}index.html?hash", server.url()))?; let resp = reqwest::blocking::get(format!("{}index.html?hash", server.url()))?;
assert_eq!( assert_eq!(
resp.headers().get("content-type").unwrap(), resp.headers().get("content-type").unwrap(),
@@ -217,6 +217,13 @@ fn hash_file(server: TestServer) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[rstest]
fn no_hash_file(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}index.html?hash", server.url()))?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest] #[rstest]
fn get_file_404(server: TestServer) -> Result<(), Error> { fn get_file_404(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404", server.url()))?; let resp = reqwest::blocking::get(format!("{}404", server.url()))?;

View File

@@ -5,7 +5,6 @@ mod utils;
use digest_auth_util::send_with_digest_auth; use digest_auth_util::send_with_digest_auth;
use fixtures::{port, tmpdir, wait_for_port, Error}; use fixtures::{port, tmpdir, wait_for_port, Error};
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir; use assert_fs::fixture::TempDir;
use rstest::rstest; use rstest::rstest;
use std::io::Read; use std::io::Read;
@@ -20,7 +19,7 @@ fn log_remote_user(
#[case] args: &[&str], #[case] args: &[&str],
#[case] is_basic: bool, #[case] is_basic: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())
@@ -55,7 +54,7 @@ fn log_remote_user(
#[rstest] #[rstest]
#[case(&["--log-format", ""])] #[case(&["--log-format", ""])]
fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> { fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path()) .arg(tmpdir.path())
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -3,7 +3,6 @@
mod fixtures; mod fixtures;
mod utils; mod utils;
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir; use assert_fs::fixture::TempDir;
use fixtures::{port, tmpdir, wait_for_port, Error}; use fixtures::{port, tmpdir, wait_for_port, Error};
use rstest::rstest; use rstest::rstest;
@@ -12,7 +11,7 @@ use std::process::{Command, Stdio};
#[rstest] #[rstest]
#[case("index.html")] #[case("index.html")]
fn single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> { fn single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path().join(file)) .arg(tmpdir.path().join(file))
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())
@@ -35,7 +34,7 @@ fn single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Err
#[rstest] #[rstest]
#[case("index.html")] #[case("index.html")]
fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> { fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")? let mut child = Command::new(assert_cmd::cargo::cargo_bin!())
.arg(tmpdir.path().join(file)) .arg(tmpdir.path().join(file))
.arg("-p") .arg("-p")
.arg(port.to_string()) .arg(port.to_string())

View File

@@ -1,7 +1,6 @@
mod fixtures; mod fixtures;
mod utils; mod utils;
use assert_cmd::Command;
use fixtures::{server, Error, TestServer}; use fixtures::{server, Error, TestServer};
use predicates::str::contains; use predicates::str::contains;
use reqwest::blocking::ClientBuilder; use reqwest::blocking::ClientBuilder;
@@ -25,7 +24,7 @@ use crate::fixtures::port;
]))] ]))]
fn tls_works(#[case] server: TestServer) -> Result<(), Error> { fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
let client = ClientBuilder::new() let client = ClientBuilder::new()
.danger_accept_invalid_certs(true) .tls_danger_accept_invalid_certs(true)
.build()?; .build()?;
let resp = client.get(server.url()).send()?.error_for_status()?; let resp = client.get(server.url()).send()?.error_for_status()?;
assert_resp_paths!(resp); assert_resp_paths!(resp);
@@ -36,7 +35,7 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
#[rstest] #[rstest]
fn wrong_path_cert() -> Result<(), Error> { fn wrong_path_cert() -> Result<(), Error> {
let port = port().to_string(); let port = port().to_string();
Command::cargo_bin("dufs")? assert_cmd::cargo::cargo_bin_cmd!()
.args([ .args([
"--tls-cert", "--tls-cert",
"wrong", "wrong",
@@ -47,7 +46,7 @@ fn wrong_path_cert() -> Result<(), Error> {
]) ])
.assert() .assert()
.failure() .failure()
.stderr(contains("Failed to access `wrong`")); .stderr(contains("Failed to load cert file at `wrong`"));
Ok(()) Ok(())
} }
@@ -56,7 +55,7 @@ fn wrong_path_cert() -> Result<(), Error> {
#[rstest] #[rstest]
fn wrong_path_key() -> Result<(), Error> { fn wrong_path_key() -> Result<(), Error> {
let port = port().to_string(); let port = port().to_string();
Command::cargo_bin("dufs")? assert_cmd::cargo::cargo_bin_cmd!()
.args([ .args([
"--tls-cert", "--tls-cert",
"tests/data/cert.pem", "tests/data/cert.pem",
@@ -67,7 +66,7 @@ fn wrong_path_key() -> Result<(), Error> {
]) ])
.assert() .assert()
.failure() .failure()
.stderr(contains("Failed to access `wrong`")); .stderr(contains("Failed to load key file at `wrong`"));
Ok(()) Ok(())
} }