feat: support config file with --config option (#281)

This commit is contained in:
sigoden 2023-11-04 16:58:19 +08:00 committed by GitHub
parent 5782c5f413
commit 4ef07737e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 434 additions and 180 deletions

28
Cargo.lock generated
View file

@ -481,6 +481,7 @@ dependencies = [
"rustls-pemfile", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"socket2 0.5.3", "socket2 0.5.3",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -1488,18 +1489,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.186" version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.186" version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1529,6 +1530,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.5" version = "0.10.5"
@ -1808,6 +1822,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View file

@ -45,6 +45,7 @@ anyhow = "1.0"
chardetng = "0.1" chardetng = "0.1"
glob = "0.3.1" glob = "0.3.1"
indexmap = "2.0" indexmap = "2.0"
serde_yaml = "0.9.27"
[features] [features]
default = ["tls"] default = ["tls"]

View file

@ -48,16 +48,17 @@ Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip
``` ```
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
Usage: dufs [OPTIONS] [serve_path] Usage: dufs [OPTIONS] [serve-path]
Arguments: Arguments:
[serve_path] Specific path to serve [default: .] [serve-path] Specific path to serve [default: .]
Options: Options:
-c, --config <config> Specify configuration file
-b, --bind <addrs> Specify bind address or unix socket -b, --bind <addrs> Specify bind address or unix socket
-p, --port <port> Specify port to listen on [default: 5000] -p, --port <port> Specify port to listen on [default: 5000]
--path-prefix <path> Specify a path prefix --path-prefix <path> Specify a path prefix
--hidden <value> Hide paths from directory listings, separated by `,` --hidden <value> Hide paths from directory listings, e.g. tmp,*.log,*.lock
-a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2 -a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2
-A, --allow-all Allow all operations -A, --allow-all Allow all operations
--allow-upload Allow upload files/folders --allow-upload Allow upload files/folders
@ -69,11 +70,11 @@ Options:
--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
--render-spa Serve SPA(Single Page Application) --render-spa Serve SPA(Single Page Application)
--assets <path> Use custom assets to override builtin assets --assets <path> Set the path to the assets directory for overriding the built-in assets
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
--tls-key <path> Path to the SSL/TLS certificate's private key
--log-format <format> Customize http log format --log-format <format> Customize http log format
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh] --completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
--tls-key <path> Path to the SSL/TLS certificate's private key
-h, --help Print help -h, --help Print help
-V, --version Print version -V, --version Print version
``` ```
@ -308,7 +309,7 @@ dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admi
All options can be set using environment variables prefixed with `DUFS_`. All options can be set using environment variables prefixed with `DUFS_`.
``` ```
[SERVE_PATH] DUFS_SERVE_PATH=/dir [serve-path] DUFS_SERVE_PATH=/dir
-b, --bind <addrs> DUFS_BIND=0.0.0.0 -b, --bind <addrs> DUFS_BIND=0.0.0.0
-p, --port <port> DUFS_PORT=5000 -p, --port <port> DUFS_PORT=5000
--path-prefix <path> DUFS_PATH_PREFIX=/path --path-prefix <path> DUFS_PATH_PREFIX=/path
@ -325,9 +326,44 @@ All options can be set using environment variables prefixed with `DUFS_`.
--render-try-index DUFS_RENDER_TRY_INDEX=true --render-try-index DUFS_RENDER_TRY_INDEX=true
--render-spa DUFS_RENDER_SPA=true --render-spa DUFS_RENDER_SPA=true
--assets <path> DUFS_ASSETS=/assets --assets <path> DUFS_ASSETS=/assets
--log-format <format> DUFS_LOG_FORMAT=""
--tls-cert <path> DUFS_TLS_CERT=cert.pem --tls-cert <path> DUFS_TLS_CERT=cert.pem
--tls-key <path> DUFS_TLS_KEY=key.pem --tls-key <path> DUFS_TLS_KEY=key.pem
--log-format <format> DUFS_LOG_FORMAT="" ```
## Configuration File
You can specify and use the configuration file by selecting the option `--config <path-to-config.yaml>`.
The following are the configuration items:
```yaml
server-path: '.'
bind:
- 192.168.8.10
port: 5000
path-prefix: /dufs
hidden:
- tmp
- '*.log'
- '*.lock'
auth:
- admin:admin@/:rw
- user:pass@/src:rw,/share
allow-all: false
allow-upload: true
allow-delete: true
allow-search: true
allow-symlink: true
allow-archive: true
enable-cors: true
render-index: true
render-try-index: true
render-spa: true
assets: ./assets/
log-format: '$remote_addr "$request" $status $http_user_agent'
tls-cert: tests/data/cert.pem
tls-key: tests/data/key_pkcs1.pem
``` ```
### Customize UI ### Customize UI

View file

@ -2,16 +2,13 @@ use anyhow::{bail, Context, Result};
use clap::builder::PossibleValuesParser; use clap::builder::PossibleValuesParser;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use clap_complete::{generate, Generator, Shell}; use clap_complete::{generate, Generator, Shell};
#[cfg(feature = "tls")] use serde::{Deserialize, Deserializer};
use rustls::{Certificate, PrivateKey};
use std::env; use std::env;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::auth::AccessControl; use crate::auth::AccessControl;
use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT}; use crate::log_http::LogHttp;
#[cfg(feature = "tls")]
use crate::tls::{load_certs, load_private_key};
use crate::utils::encode_uri; use crate::utils::encode_uri;
pub fn build_cli() -> Command { pub fn build_cli() -> Command {
@ -24,12 +21,20 @@ pub fn build_cli() -> Command {
env!("CARGO_PKG_REPOSITORY") env!("CARGO_PKG_REPOSITORY")
)) ))
.arg( .arg(
Arg::new("serve_path") Arg::new("serve-path")
.env("DUFS_SERVE_PATH") .env("DUFS_SERVE_PATH")
.hide_env(true) .hide_env(true)
.default_value(".")
.value_parser(value_parser!(PathBuf)) .value_parser(value_parser!(PathBuf))
.help("Specific path to serve"), .help("Specific path to serve [default: .]"),
)
.arg(
Arg::new("config")
.env("DUFS_SERVE_PATH")
.hide_env(true)
.short('c')
.long("config")
.value_parser(value_parser!(PathBuf))
.help("Specify configuration file"),
) )
.arg( .arg(
Arg::new("bind") Arg::new("bind")
@ -48,9 +53,8 @@ pub fn build_cli() -> Command {
.hide_env(true) .hide_env(true)
.short('p') .short('p')
.long("port") .long("port")
.default_value("5000")
.value_parser(value_parser!(u16)) .value_parser(value_parser!(u16))
.help("Specify port to listen on") .help("Specify port to listen on [default: 5000]")
.value_name("port"), .value_name("port"),
) )
.arg( .arg(
@ -66,7 +70,7 @@ pub fn build_cli() -> Command {
.env("DUFS_HIDDEN") .env("DUFS_HIDDEN")
.hide_env(true) .hide_env(true)
.long("hidden") .long("hidden")
.help("Hide paths from directory listings, separated by `,`") .help("Hide paths from directory listings, e.g. tmp,*.log,*.lock")
.value_name("value"), .value_name("value"),
) )
.arg( .arg(
@ -177,9 +181,24 @@ pub fn build_cli() -> Command {
.env("DUFS_ASSETS") .env("DUFS_ASSETS")
.hide_env(true) .hide_env(true)
.long("assets") .long("assets")
.help("Use custom assets to override builtin assets") .help("Set the path to the assets directory for overriding the built-in assets")
.value_parser(value_parser!(PathBuf)) .value_parser(value_parser!(PathBuf))
.value_name("path") .value_name("path")
)
.arg(
Arg::new("log-format")
.env("DUFS_LOG_FORMAT")
.hide_env(true)
.long("log-format")
.value_name("format")
.help("Customize http log format"),
)
.arg(
Arg::new("completions")
.long("completions")
.value_name("shell")
.value_parser(value_parser!(Shell))
.help("Print shell completion script for <shell>"),
); );
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
@ -203,37 +222,32 @@ pub fn build_cli() -> Command {
.help("Path to the SSL/TLS certificate's private key"), .help("Path to the SSL/TLS certificate's private key"),
); );
app.arg( app
Arg::new("log-format")
.env("DUFS_LOG_FORMAT")
.hide_env(true)
.long("log-format")
.value_name("format")
.help("Customize http log format"),
)
.arg(
Arg::new("completions")
.long("completions")
.value_name("shell")
.value_parser(value_parser!(Shell))
.help("Print shell completion script for <shell>"),
)
} }
pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) { pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
} }
#[derive(Debug)] #[derive(Debug, Deserialize, Default)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct Args { pub struct Args {
#[serde(default = "default_serve_path")]
pub serve_path: PathBuf,
#[serde(deserialize_with = "deserialize_bind_addrs")]
#[serde(rename = "bind")]
pub addrs: Vec<BindAddr>, pub addrs: Vec<BindAddr>,
pub port: u16, pub port: u16,
pub path: PathBuf, #[serde(skip)]
pub path_is_file: bool, pub path_is_file: bool,
pub path_prefix: String, pub path_prefix: String,
#[serde(skip)]
pub uri_prefix: String, pub uri_prefix: String,
pub hidden: Vec<String>, pub hidden: Vec<String>,
#[serde(deserialize_with = "deserialize_access_control")]
pub auth: AccessControl, pub auth: AccessControl,
pub allow_all: bool,
pub allow_upload: bool, pub allow_upload: bool,
pub allow_delete: bool, pub allow_delete: bool,
pub allow_search: bool, pub allow_search: bool,
@ -243,12 +257,12 @@ pub struct Args {
pub render_spa: bool, pub render_spa: bool,
pub render_try_index: bool, pub render_try_index: bool,
pub enable_cors: bool, pub enable_cors: bool,
pub assets_path: Option<PathBuf>, pub assets: Option<PathBuf>,
#[serde(deserialize_with = "deserialize_log_http")]
#[serde(rename = "log-format")]
pub log_http: LogHttp, pub log_http: LogHttp,
#[cfg(feature = "tls")] pub tls_cert: Option<PathBuf>,
pub tls: Option<(Vec<Certificate>, PrivateKey)>, pub tls_key: Option<PathBuf>,
#[cfg(not(feature = "tls"))]
pub tls: Option<()>,
} }
impl Args { impl Args {
@ -257,90 +271,164 @@ impl Args {
/// If a parsing error occurred, exit the process and print out informative /// If a parsing error occurred, exit the process and print out informative
/// error message to user. /// error message to user.
pub fn parse(matches: ArgMatches) -> Result<Args> { pub fn parse(matches: ArgMatches) -> Result<Args> {
let port = *matches.get_one::<u16>("port").unwrap(); let mut args = Self {
let addrs = matches serve_path: default_serve_path(),
.get_many::<String>("bind") addrs: BindAddr::parse_addrs(&["0.0.0.0", "::"]).unwrap(),
.map(|bind| bind.map(|v| v.as_str()).collect()) port: 5000,
.unwrap_or_else(|| vec!["0.0.0.0", "::"]); ..Default::default()
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?; };
let path = Args::parse_path(matches.get_one::<PathBuf>("serve_path").unwrap())?;
let path_is_file = path.metadata()?.is_file(); if let Some(config_path) = matches.get_one::<PathBuf>("config") {
let path_prefix = matches let contents = std::fs::read_to_string(config_path)
.get_one::<String>("path-prefix") .with_context(|| format!("Failed to read config at {}", config_path.display()))?;
.map(|v| v.trim_matches('/').to_owned()) args = serde_yaml::from_str(&contents)
.unwrap_or_default(); .with_context(|| format!("Failed to load config at {}", config_path.display()))?;
let uri_prefix = if path_prefix.is_empty() { }
if let Some(path) = matches.get_one::<PathBuf>("serve-path") {
args.serve_path = path.clone()
}
args.serve_path = Self::sanitize_path(args.serve_path)?;
if let Some(port) = matches.get_one::<u16>("port") {
args.port = *port
}
if let Some(addrs) = matches.get_many::<String>("bind") {
let addrs: Vec<_> = addrs.map(|v| v.as_str()).collect();
args.addrs = BindAddr::parse_addrs(&addrs)?;
}
args.path_is_file = args.serve_path.metadata()?.is_file();
if let Some(path_prefix) = matches.get_one::<String>("path-prefix") {
args.path_prefix = path_prefix.clone();
}
args.path_prefix = args.path_prefix.trim_matches('/').to_string();
args.uri_prefix = if args.path_prefix.is_empty() {
"/".to_owned() "/".to_owned()
} else { } else {
format!("/{}/", &encode_uri(&path_prefix)) format!("/{}/", &encode_uri(&args.path_prefix))
}; };
let hidden: Vec<String> = matches
if let Some(hidden) = matches
.get_one::<String>("hidden") .get_one::<String>("hidden")
.map(|v| v.split(',').map(|x| x.to_string()).collect()) .map(|v| v.split(',').map(|x| x.to_string()).collect())
.unwrap_or_default(); {
let enable_cors = matches.get_flag("enable-cors"); args.hidden = hidden;
let auth: Vec<&str> = matches }
.get_many::<String>("auth")
.map(|auth| auth.map(|v| v.as_str()).collect())
.unwrap_or_default();
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");
let allow_symlink = matches.get_flag("allow-all") || matches.get_flag("allow-symlink");
let allow_archive = matches.get_flag("allow-all") || matches.get_flag("allow-archive");
let render_index = matches.get_flag("render-index");
let render_try_index = matches.get_flag("render-try-index");
let render_spa = matches.get_flag("render-spa");
#[cfg(feature = "tls")]
let tls = match (
matches.get_one::<PathBuf>("tls-cert"),
matches.get_one::<PathBuf>("tls-key"),
) {
(Some(certs_file), Some(key_file)) => {
let certs = load_certs(certs_file)?;
let key = load_private_key(key_file)?;
Some((certs, key))
}
_ => None,
};
#[cfg(not(feature = "tls"))]
let tls = None;
let log_http: LogHttp = matches
.get_one::<String>("log-format")
.map(|v| v.as_str())
.unwrap_or(DEFAULT_LOG_FORMAT)
.parse()?;
let assets_path = match matches.get_one::<PathBuf>("assets") {
Some(v) => Some(Args::parse_assets_path(v)?),
None => None,
};
Ok(Args { if !args.enable_cors {
addrs, args.enable_cors = matches.get_flag("enable-cors");
port, }
path,
path_is_file, if let Some(rules) = matches.get_many::<String>("auth") {
path_prefix, let rules: Vec<_> = rules.map(|v| v.as_str()).collect();
uri_prefix, args.auth = AccessControl::new(&rules)?;
hidden, }
auth,
enable_cors, if !args.allow_all {
allow_delete, args.allow_all = matches.get_flag("allow-all");
allow_upload, }
allow_search,
allow_symlink, let allow_all = args.allow_all;
allow_archive,
render_index, if !args.allow_upload {
render_try_index, args.allow_upload = allow_all || matches.get_flag("allow-upload");
render_spa, }
tls, if !args.allow_delete {
log_http, args.allow_delete = allow_all || matches.get_flag("allow-delete");
assets_path, }
}) if !args.allow_search {
args.allow_search = allow_all || matches.get_flag("allow-search");
}
if !args.allow_symlink {
args.allow_symlink = allow_all || matches.get_flag("allow-symlink");
}
if !args.allow_archive {
args.allow_archive = allow_all || matches.get_flag("allow-archive");
}
if !args.render_index {
args.render_index = matches.get_flag("render-index");
}
if !args.render_try_index {
args.render_try_index = matches.get_flag("render-try-index");
}
if !args.render_spa {
args.render_spa = matches.get_flag("render-spa");
}
if let Some(log_format) = matches.get_one::<String>("log-format") {
args.log_http = log_format.parse()?;
}
if let Some(assets_path) = matches.get_one::<PathBuf>("assets") {
args.assets = Some(assets_path.clone());
}
if let Some(assets_path) = &args.assets {
args.assets = Some(Args::sanitize_assets_path(assets_path)?);
}
#[cfg(feature = "tls")]
{
if let Some(tls_cert) = matches.get_one::<PathBuf>("tls-cert") {
args.tls_cert = Some(tls_cert.clone())
}
if let Some(tls_key) = matches.get_one::<PathBuf>("tls-key") {
args.tls_key = Some(tls_key.clone())
}
match (&args.tls_cert, &args.tls_key) {
(Some(_), Some(_)) => {}
(Some(_), _) => bail!("No tls-key set"),
(_, Some(_)) => bail!("No tls-cert set"),
(None, None) => {}
}
}
#[cfg(not(feature = "tls"))]
{
args.tls_cert = None;
args.tls_key = None;
}
Ok(args)
} }
fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> { fn sanitize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
if !path.exists() {
bail!("Path `{}` doesn't exist", path.display());
}
env::current_dir()
.and_then(|mut p| {
p.push(path); // If path is absolute, it replaces the current path.
std::fs::canonicalize(p)
})
.with_context(|| format!("Failed to access path `{}`", path.display()))
}
fn sanitize_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = Self::sanitize_path(path)?;
if !path.join("index.html").exists() {
bail!("Path `{}` doesn't contains index.html", path.display());
}
Ok(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BindAddr {
Address(IpAddr),
Path(PathBuf),
}
impl BindAddr {
fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
let mut bind_addrs = vec![]; let mut bind_addrs = vec![];
let mut invalid_addrs = vec![]; let mut invalid_addrs = vec![];
for addr in addrs { for addr in addrs {
@ -362,32 +450,32 @@ impl Args {
} }
Ok(bind_addrs) Ok(bind_addrs)
} }
fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
if !path.exists() {
bail!("Path `{}` doesn't exist", path.display());
}
env::current_dir()
.and_then(|mut p| {
p.push(path); // If path is absolute, it replaces the current path.
std::fs::canonicalize(p)
})
.with_context(|| format!("Failed to access path `{}`", path.display()))
}
fn parse_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = Self::parse_path(path)?;
if !path.join("index.html").exists() {
bail!("Path `{}` doesn't contains index.html", path.display());
}
Ok(path)
}
} }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, D::Error>
pub enum BindAddr { where
Address(IpAddr), D: Deserializer<'de>,
Path(PathBuf), {
let addrs: Vec<&str> = Vec::deserialize(deserializer)?;
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
}
fn deserialize_access_control<'de, D>(deserializer: D) -> Result<AccessControl, D::Error>
where
D: Deserializer<'de>,
{
let rules: Vec<&str> = Vec::deserialize(deserializer)?;
AccessControl::new(&rules).map_err(serde::de::Error::custom)
}
fn deserialize_log_http<'de, D>(deserializer: D) -> Result<LogHttp, D::Error>
where
D: Deserializer<'de>,
{
let value: String = Deserialize::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
fn default_serve_path() -> PathBuf {
PathBuf::from(".")
} }

View file

@ -25,21 +25,26 @@ lazy_static! {
}; };
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct AccessControl { pub struct AccessControl {
users: IndexMap<String, (String, AccessPaths)>, users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>, anony: Option<AccessPaths>,
} }
impl Default for AccessControl {
fn default() -> Self {
AccessControl {
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
}
}
}
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(AccessControl { return Ok(Default::default());
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
});
} }
let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
let mut anony = None; let mut anony = None;
let mut anony_paths = vec![]; let mut anony_paths = vec![];

View file

@ -9,6 +9,12 @@ pub struct LogHttp {
elements: Vec<LogElement>, elements: Vec<LogElement>,
} }
impl Default for LogHttp {
fn default() -> Self {
DEFAULT_LOG_FORMAT.parse().unwrap()
}
}
#[derive(Debug)] #[derive(Debug)]
enum LogElement { enum LogElement {
Variable(String), Variable(String),

View file

@ -16,7 +16,7 @@ extern crate log;
use crate::args::{build_cli, print_completions, Args}; use crate::args::{build_cli, print_completions, Args};
use crate::server::{Request, Server}; use crate::server::{Request, Server};
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use crate::tls::{TlsAcceptor, TlsStream}; use crate::tls::{load_certs, load_private_key, TlsAcceptor, TlsStream};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener}; use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener};
@ -88,9 +88,12 @@ fn serve(
BindAddr::Address(ip) => { BindAddr::Address(ip) => {
let incoming = create_addr_incoming(SocketAddr::new(*ip, port)) let incoming = create_addr_incoming(SocketAddr::new(*ip, port))
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?; .with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
match args.tls.as_ref() {
match (&args.tls_cert, &args.tls_key) {
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
Some((certs, key)) => { (Some(cert_file), Some(key_file)) => {
let certs = load_certs(cert_file)?;
let key = load_private_key(key_file)?;
let config = ServerConfig::builder() let config = ServerConfig::builder()
.with_safe_defaults() .with_safe_defaults()
.with_no_client_auth() .with_no_client_auth()
@ -105,11 +108,7 @@ fn serve(
tokio::spawn(hyper::Server::builder(accepter).serve(new_service)); tokio::spawn(hyper::Server::builder(accepter).serve(new_service));
handles.push(server); handles.push(server);
} }
#[cfg(not(feature = "tls"))] (None, None) => {
Some(_) => {
unreachable!()
}
None => {
let new_service = make_service_fn(move |socket: &AddrStream| { let new_service = make_service_fn(move |socket: &AddrStream| {
let remote_addr = socket.remote_addr(); let remote_addr = socket.remote_addr();
serve_func(Some(remote_addr)) serve_func(Some(remote_addr))
@ -118,6 +117,9 @@ fn serve(
tokio::spawn(hyper::Server::builder(incoming).serve(new_service)); tokio::spawn(hyper::Server::builder(incoming).serve(new_service));
handles.push(server); handles.push(server);
} }
_ => {
unreachable!()
}
}; };
} }
BindAddr::Path(path) => { BindAddr::Path(path) => {
@ -195,7 +197,11 @@ fn print_listening(args: Arc<Args>) -> Result<()> {
IpAddr::V4(_) => format!("{}:{}", addr, args.port), IpAddr::V4(_) => format!("{}:{}", addr, args.port),
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port), IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
}; };
let protocol = if args.tls.is_some() { "https" } else { "http" }; let protocol = if args.tls_cert.is_some() {
"https"
} else {
"http"
};
format!("{}://{}{}", protocol, addr, args.uri_prefix) format!("{}://{}{}", protocol, addr, args.uri_prefix)
} }
BindAddr::Path(path) => path.display().to_string(), BindAddr::Path(path) => path.display().to_string(),

View file

@ -71,13 +71,13 @@ impl Server {
encode_uri(&format!( encode_uri(&format!(
"{}{}", "{}{}",
&args.uri_prefix, &args.uri_prefix,
get_file_name(&args.path) get_file_name(&args.serve_path)
)), )),
] ]
} else { } else {
vec![] vec![]
}; };
let html = match args.assets_path.as_ref() { let html = match args.assets.as_ref() {
Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?), Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
None => Cow::Borrowed(INDEX_HTML), None => Cow::Borrowed(INDEX_HTML),
}; };
@ -180,7 +180,7 @@ impl Server {
.iter() .iter()
.any(|v| v.as_str() == req_path) .any(|v| v.as_str() == req_path)
{ {
self.handle_send_file(&self.args.path, headers, head_only, &mut res) self.handle_send_file(&self.args.serve_path, headers, head_only, &mut res)
.await?; .await?;
} else { } else {
status_not_found(&mut res); status_not_found(&mut res);
@ -620,7 +620,7 @@ impl Server {
res: &mut Response, res: &mut Response,
) -> Result<()> { ) -> Result<()> {
if path.extension().is_none() { if path.extension().is_none() {
let path = self.args.path.join(INDEX_NAME); let path = self.args.serve_path.join(INDEX_NAME);
self.handle_send_file(&path, headers, head_only, res) self.handle_send_file(&path, headers, head_only, res)
.await?; .await?;
} else { } else {
@ -636,7 +636,7 @@ impl Server {
res: &mut Response, res: &mut Response,
) -> Result<bool> { ) -> Result<bool> {
if let Some(name) = req_path.strip_prefix(&self.assets_prefix) { if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
match self.args.assets_path.as_ref() { match self.args.assets.as_ref() {
Some(assets_path) => { Some(assets_path) => {
let path = assets_path.join(name); let path = assets_path.join(name);
self.handle_send_file(&path, headers, false, res).await?; self.handle_send_file(&path, headers, false, res).await?;
@ -776,7 +776,10 @@ impl Server {
) -> Result<()> { ) -> Result<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),); let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
let (file, meta) = (file?, meta?); let (file, meta) = (file?, meta?);
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?)); let href = format!(
"/{}",
normalize_path(path.strip_prefix(&self.args.serve_path)?)
);
let mut buffer: Vec<u8> = vec![]; let mut buffer: Vec<u8> = vec![];
file.take(1024).read_to_end(&mut buffer).await?; file.take(1024).read_to_end(&mut buffer).await?;
let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text(); let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
@ -822,12 +825,15 @@ impl Server {
}, },
None => 1, None => 1,
}; };
let mut paths = match self.to_pathitem(path, &self.args.path).await? { let mut paths = match self.to_pathitem(path, &self.args.serve_path).await? {
Some(v) => vec![v], Some(v) => vec![v],
None => vec![], None => vec![],
}; };
if depth != 0 { if depth != 0 {
match self.list_dir(path, &self.args.path, access_paths).await { match self
.list_dir(path, &self.args.serve_path, access_paths)
.await
{
Ok(child) => paths.extend(child), Ok(child) => paths.extend(child),
Err(_) => { Err(_) => {
status_forbid(res); status_forbid(res);
@ -847,7 +853,7 @@ impl Server {
} }
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> { async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? { if let Some(pathitem) = self.to_pathitem(path, &self.args.serve_path).await? {
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str())); res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
} else { } else {
status_not_found(res); status_not_found(res);
@ -990,7 +996,10 @@ impl Server {
} }
return Ok(()); return Ok(());
} }
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?)); let href = format!(
"/{}",
normalize_path(path.strip_prefix(&self.args.serve_path)?)
);
let readwrite = access_paths.perm().readwrite(); let readwrite = access_paths.perm().readwrite();
let data = IndexData { let data = IndexData {
kind: DataKind::Index, kind: DataKind::Index,
@ -1038,7 +1047,7 @@ impl Server {
fs::canonicalize(path) fs::canonicalize(path)
.await .await
.ok() .ok()
.map(|v| v.starts_with(&self.args.path)) .map(|v| v.starts_with(&self.args.serve_path))
.unwrap_or_default() .unwrap_or_default()
} }
@ -1104,14 +1113,14 @@ impl Server {
fn join_path(&self, path: &str) -> Option<PathBuf> { fn join_path(&self, path: &str) -> Option<PathBuf> {
if path.is_empty() { if path.is_empty() {
return Some(self.args.path.clone()); return Some(self.args.serve_path.clone());
} }
let path = if cfg!(windows) { let path = if cfg!(windows) {
path.replace('/', "\\") path.replace('/', "\\")
} else { } else {
path.to_string() path.to_string()
}; };
Some(self.args.path.join(path)) Some(self.args.serve_path.join(path))
} }
async fn list_dir( async fn list_dir(

56
tests/config.rs Normal file
View file

@ -0,0 +1,56 @@
mod fixtures;
mod utils;
use assert_cmd::prelude::*;
use assert_fs::TempDir;
use diqwest::blocking::WithDigestAuth;
use fixtures::{port, tmpdir, wait_for_port, Error};
use rstest::rstest;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[rstest]
fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let config_path = get_config_path().display().to_string();
let mut child = Command::cargo_bin("dufs")?
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(["--config", &config_path])
.stdout(Stdio::piped())
.spawn()?;
wait_for_port(port);
let url = format!("http://localhost:{port}/dufs/index.html");
let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 401);
let url = format!("http://localhost:{port}/dufs/index.html");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
assert_eq!(resp.text()?, "This is index.html");
let url = format!("http://localhost:{port}/dufs?simple");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
let text: String = resp.text().unwrap();
assert!(text.split('\n').any(|c| c == "dir1/"));
assert!(!text.split('\n').any(|c| c == "dir3/"));
assert!(!text.split('\n').any(|c| c == "test.txt"));
let url = format!("http://localhost:{port}/dufs/dir1/upload.txt");
let resp = fetch!(b"PUT", &url)
.body("Hello")
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201);
child.kill()?;
Ok(())
}
fn get_config_path() -> PathBuf {
let mut path = std::env::current_dir().expect("Failed to get current directory");
path.push("tests");
path.push("data");
path.push("config.yaml");
path
}

9
tests/data/config.yaml Normal file
View file

@ -0,0 +1,9 @@
bind:
- 0.0.0.0
path-prefix: dufs
hidden:
- dir3
- test.txt
auth:
- user:pass@/:rw
allow-upload: true

View file

@ -41,7 +41,7 @@ fn log_remote_user(
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let mut buf = [0; 4096]; let mut buf = [0; 2048];
let buf_len = stdout.read(&mut buf)?; let buf_len = stdout.read(&mut buf)?;
let output = std::str::from_utf8(&buf[0..buf_len])?; let output = std::str::from_utf8(&buf[0..buf_len])?;
@ -69,7 +69,7 @@ fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error
let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?; let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let mut buf = [0; 4096]; let mut buf = [0; 2048];
let buf_len = stdout.read(&mut buf)?; let buf_len = stdout.read(&mut buf)?;
let output = std::str::from_utf8(&buf[0..buf_len])?; let output = std::str::from_utf8(&buf[0..buf_len])?;

View file

@ -7,6 +7,8 @@ use predicates::str::contains;
use reqwest::blocking::ClientBuilder; use reqwest::blocking::ClientBuilder;
use rstest::rstest; use rstest::rstest;
use crate::fixtures::port;
/// Can start the server with TLS and receive encrypted responses. /// Can start the server with TLS and receive encrypted responses.
#[rstest] #[rstest]
#[case(server(&[ #[case(server(&[
@ -33,8 +35,16 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
/// Wrong path for cert throws error. /// Wrong path for cert throws error.
#[rstest] #[rstest]
fn wrong_path_cert() -> Result<(), Error> { fn wrong_path_cert() -> Result<(), Error> {
let port = port().to_string();
Command::cargo_bin("dufs")? Command::cargo_bin("dufs")?
.args(["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"]) .args([
"--tls-cert",
"wrong",
"--tls-key",
"tests/data/key.pem",
"--port",
&port,
])
.assert() .assert()
.failure() .failure()
.stderr(contains("Failed to access `wrong`")); .stderr(contains("Failed to access `wrong`"));
@ -45,8 +55,16 @@ fn wrong_path_cert() -> Result<(), Error> {
/// Wrong paths for key throws errors. /// Wrong paths for key throws errors.
#[rstest] #[rstest]
fn wrong_path_key() -> Result<(), Error> { fn wrong_path_key() -> Result<(), Error> {
let port = port().to_string();
Command::cargo_bin("dufs")? Command::cargo_bin("dufs")?
.args(["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"]) .args([
"--tls-cert",
"tests/data/cert.pem",
"--tls-key",
"wrong",
"--port",
&port,
])
.assert() .assert()
.failure() .failure()
.stderr(contains("Failed to access `wrong`")); .stderr(contains("Failed to access `wrong`"));