From 4ef07737e12b4b1ca001a979ddd2fb473eda0409 Mon Sep 17 00:00:00 2001 From: sigoden Date: Sat, 4 Nov 2023 16:58:19 +0800 Subject: [PATCH] feat: support config file with `--config` option (#281) --- Cargo.lock | 28 +++- Cargo.toml | 1 + README.md | 52 +++++- src/args.rs | 360 +++++++++++++++++++++++++---------------- src/auth.rs | 17 +- src/log_http.rs | 6 + src/main.rs | 24 +-- src/server.rs | 35 ++-- tests/config.rs | 56 +++++++ tests/data/config.yaml | 9 ++ tests/log_http.rs | 4 +- tests/tls.rs | 22 ++- 12 files changed, 434 insertions(+), 180 deletions(-) create mode 100644 tests/config.rs create mode 100644 tests/data/config.yaml diff --git a/Cargo.lock b/Cargo.lock index d139806..0d59ed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,6 +481,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "serde_yaml", "socket2 0.5.3", "tokio", "tokio-rustls", @@ -1488,18 +1489,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.186" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.186" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -1529,6 +1530,19 @@ dependencies = [ "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]] name = "sha1" version = "0.10.5" @@ -1808,6 +1822,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 76955e4..4ee1c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ anyhow = "1.0" chardetng = "0.1" glob = "0.3.1" indexmap = "2.0" +serde_yaml = "0.9.27" [features] default = ["tls"] diff --git a/README.md b/README.md index 25c5a23..4d0e141 100644 --- a/README.md +++ b/README.md @@ -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 -Usage: dufs [OPTIONS] [serve_path] +Usage: dufs [OPTIONS] [serve-path] Arguments: - [serve_path] Specific path to serve [default: .] + [serve-path] Specific path to serve [default: .] Options: + -c, --config Specify configuration file -b, --bind Specify bind address or unix socket -p, --port Specify port to listen on [default: 5000] --path-prefix Specify a path prefix - --hidden Hide paths from directory listings, separated by `,` + --hidden Hide paths from directory listings, e.g. tmp,*.log,*.lock -a, --auth Add auth roles, e.g. user:pass@/dir1:rw,/dir2 -A, --allow-all Allow all operations --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-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html --render-spa Serve SPA(Single Page Application) - --assets Use custom assets to override builtin assets - --tls-cert Path to an SSL/TLS certificate to serve with HTTPS - --tls-key Path to the SSL/TLS certificate's private key + --assets Set the path to the assets directory for overriding the built-in assets --log-format Customize http log format --completions Print shell completion script for [possible values: bash, elvish, fish, powershell, zsh] + --tls-cert Path to an SSL/TLS certificate to serve with HTTPS + --tls-key Path to the SSL/TLS certificate's private key -h, --help Print help -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_`. ``` - [SERVE_PATH] DUFS_SERVE_PATH=/dir + [serve-path] DUFS_SERVE_PATH=/dir -b, --bind DUFS_BIND=0.0.0.0 -p, --port DUFS_PORT=5000 --path-prefix 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-spa DUFS_RENDER_SPA=true --assets DUFS_ASSETS=/assets + --log-format DUFS_LOG_FORMAT="" --tls-cert DUFS_TLS_CERT=cert.pem --tls-key DUFS_TLS_KEY=key.pem - --log-format DUFS_LOG_FORMAT="" +``` + +## Configuration File + +You can specify and use the configuration file by selecting the option `--config `. + +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 diff --git a/src/args.rs b/src/args.rs index 8d4da3e..65fd58c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,16 +2,13 @@ use anyhow::{bail, Context, Result}; use clap::builder::PossibleValuesParser; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap_complete::{generate, Generator, Shell}; -#[cfg(feature = "tls")] -use rustls::{Certificate, PrivateKey}; +use serde::{Deserialize, Deserializer}; use std::env; use std::net::IpAddr; use std::path::{Path, PathBuf}; use crate::auth::AccessControl; -use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT}; -#[cfg(feature = "tls")] -use crate::tls::{load_certs, load_private_key}; +use crate::log_http::LogHttp; use crate::utils::encode_uri; pub fn build_cli() -> Command { @@ -24,12 +21,20 @@ pub fn build_cli() -> Command { env!("CARGO_PKG_REPOSITORY") )) .arg( - Arg::new("serve_path") + Arg::new("serve-path") .env("DUFS_SERVE_PATH") .hide_env(true) - .default_value(".") .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::new("bind") @@ -48,9 +53,8 @@ pub fn build_cli() -> Command { .hide_env(true) .short('p') .long("port") - .default_value("5000") .value_parser(value_parser!(u16)) - .help("Specify port to listen on") + .help("Specify port to listen on [default: 5000]") .value_name("port"), ) .arg( @@ -66,7 +70,7 @@ pub fn build_cli() -> Command { .env("DUFS_HIDDEN") .hide_env(true) .long("hidden") - .help("Hide paths from directory listings, separated by `,`") + .help("Hide paths from directory listings, e.g. tmp,*.log,*.lock") .value_name("value"), ) .arg( @@ -177,9 +181,24 @@ pub fn build_cli() -> Command { .env("DUFS_ASSETS") .hide_env(true) .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_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 "), ); #[cfg(feature = "tls")] @@ -203,37 +222,32 @@ pub fn build_cli() -> Command { .help("Path to the SSL/TLS certificate's private key"), ); - app.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 "), - ) + app } pub fn print_completions(gen: G, cmd: &mut Command) { 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 { + #[serde(default = "default_serve_path")] + pub serve_path: PathBuf, + #[serde(deserialize_with = "deserialize_bind_addrs")] + #[serde(rename = "bind")] pub addrs: Vec, pub port: u16, - pub path: PathBuf, + #[serde(skip)] pub path_is_file: bool, pub path_prefix: String, + #[serde(skip)] pub uri_prefix: String, pub hidden: Vec, + #[serde(deserialize_with = "deserialize_access_control")] pub auth: AccessControl, + pub allow_all: bool, pub allow_upload: bool, pub allow_delete: bool, pub allow_search: bool, @@ -243,12 +257,12 @@ pub struct Args { pub render_spa: bool, pub render_try_index: bool, pub enable_cors: bool, - pub assets_path: Option, + pub assets: Option, + #[serde(deserialize_with = "deserialize_log_http")] + #[serde(rename = "log-format")] pub log_http: LogHttp, - #[cfg(feature = "tls")] - pub tls: Option<(Vec, PrivateKey)>, - #[cfg(not(feature = "tls"))] - pub tls: Option<()>, + pub tls_cert: Option, + pub tls_key: Option, } impl Args { @@ -257,90 +271,164 @@ impl Args { /// If a parsing error occurred, exit the process and print out informative /// error message to user. pub fn parse(matches: ArgMatches) -> Result { - let port = *matches.get_one::("port").unwrap(); - let addrs = matches - .get_many::("bind") - .map(|bind| bind.map(|v| v.as_str()).collect()) - .unwrap_or_else(|| vec!["0.0.0.0", "::"]); - let addrs: Vec = Args::parse_addrs(&addrs)?; - let path = Args::parse_path(matches.get_one::("serve_path").unwrap())?; - let path_is_file = path.metadata()?.is_file(); - let path_prefix = matches - .get_one::("path-prefix") - .map(|v| v.trim_matches('/').to_owned()) - .unwrap_or_default(); - let uri_prefix = if path_prefix.is_empty() { + let mut args = Self { + serve_path: default_serve_path(), + addrs: BindAddr::parse_addrs(&["0.0.0.0", "::"]).unwrap(), + port: 5000, + ..Default::default() + }; + + if let Some(config_path) = matches.get_one::("config") { + let contents = std::fs::read_to_string(config_path) + .with_context(|| format!("Failed to read config at {}", config_path.display()))?; + args = serde_yaml::from_str(&contents) + .with_context(|| format!("Failed to load config at {}", config_path.display()))?; + } + + if let Some(path) = matches.get_one::("serve-path") { + args.serve_path = path.clone() + } + args.serve_path = Self::sanitize_path(args.serve_path)?; + + if let Some(port) = matches.get_one::("port") { + args.port = *port + } + + if let Some(addrs) = matches.get_many::("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::("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() } else { - format!("/{}/", &encode_uri(&path_prefix)) + format!("/{}/", &encode_uri(&args.path_prefix)) }; - let hidden: Vec = matches + + if let Some(hidden) = matches .get_one::("hidden") .map(|v| v.split(',').map(|x| x.to_string()).collect()) - .unwrap_or_default(); - let enable_cors = matches.get_flag("enable-cors"); - let auth: Vec<&str> = matches - .get_many::("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::("tls-cert"), - matches.get_one::("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::("log-format") - .map(|v| v.as_str()) - .unwrap_or(DEFAULT_LOG_FORMAT) - .parse()?; - let assets_path = match matches.get_one::("assets") { - Some(v) => Some(Args::parse_assets_path(v)?), - None => None, - }; + { + args.hidden = hidden; + } - Ok(Args { - addrs, - port, - path, - path_is_file, - path_prefix, - uri_prefix, - hidden, - auth, - enable_cors, - allow_delete, - allow_upload, - allow_search, - allow_symlink, - allow_archive, - render_index, - render_try_index, - render_spa, - tls, - log_http, - assets_path, - }) + if !args.enable_cors { + args.enable_cors = matches.get_flag("enable-cors"); + } + + if let Some(rules) = matches.get_many::("auth") { + let rules: Vec<_> = rules.map(|v| v.as_str()).collect(); + args.auth = AccessControl::new(&rules)?; + } + + if !args.allow_all { + args.allow_all = matches.get_flag("allow-all"); + } + + let allow_all = args.allow_all; + + if !args.allow_upload { + args.allow_upload = allow_all || matches.get_flag("allow-upload"); + } + if !args.allow_delete { + args.allow_delete = allow_all || matches.get_flag("allow-delete"); + } + 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::("log-format") { + args.log_http = log_format.parse()?; + } + + if let Some(assets_path) = matches.get_one::("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::("tls-cert") { + args.tls_cert = Some(tls_cert.clone()) + } + + if let Some(tls_key) = matches.get_one::("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> { + fn sanitize_path>(path: P) -> Result { + 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>(path: P) -> Result { + 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> { let mut bind_addrs = vec![]; let mut invalid_addrs = vec![]; for addr in addrs { @@ -362,32 +450,32 @@ impl Args { } Ok(bind_addrs) } - - fn parse_path>(path: P) -> Result { - 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>(path: P) -> Result { - 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)] -pub enum BindAddr { - Address(IpAddr), - Path(PathBuf), +fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + 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 +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 +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(".") } diff --git a/src/auth.rs b/src/auth.rs index 25bf4a2..9c64ea9 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -25,21 +25,26 @@ lazy_static! { }; } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct AccessControl { users: IndexMap, anony: Option, } +impl Default for AccessControl { + fn default() -> Self { + AccessControl { + anony: Some(AccessPaths::new(AccessPerm::ReadWrite)), + users: IndexMap::new(), + } + } +} + impl AccessControl { pub fn new(raw_rules: &[&str]) -> Result { if raw_rules.is_empty() { - return Ok(AccessControl { - anony: Some(AccessPaths::new(AccessPerm::ReadWrite)), - users: IndexMap::new(), - }); + return Ok(Default::default()); } - let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); let mut anony = None; let mut anony_paths = vec![]; diff --git a/src/log_http.rs b/src/log_http.rs index 1c46a26..49e712e 100644 --- a/src/log_http.rs +++ b/src/log_http.rs @@ -9,6 +9,12 @@ pub struct LogHttp { elements: Vec, } +impl Default for LogHttp { + fn default() -> Self { + DEFAULT_LOG_FORMAT.parse().unwrap() + } +} + #[derive(Debug)] enum LogElement { Variable(String), diff --git a/src/main.rs b/src/main.rs index 4487524..ebc38ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ extern crate log; use crate::args::{build_cli, print_completions, Args}; use crate::server::{Request, Server}; #[cfg(feature = "tls")] -use crate::tls::{TlsAcceptor, TlsStream}; +use crate::tls::{load_certs, load_private_key, TlsAcceptor, TlsStream}; use anyhow::{anyhow, Context, Result}; use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener}; @@ -88,9 +88,12 @@ fn serve( BindAddr::Address(ip) => { let incoming = create_addr_incoming(SocketAddr::new(*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")] - 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() .with_safe_defaults() .with_no_client_auth() @@ -105,11 +108,7 @@ fn serve( tokio::spawn(hyper::Server::builder(accepter).serve(new_service)); handles.push(server); } - #[cfg(not(feature = "tls"))] - Some(_) => { - unreachable!() - } - None => { + (None, None) => { let new_service = make_service_fn(move |socket: &AddrStream| { let remote_addr = socket.remote_addr(); serve_func(Some(remote_addr)) @@ -118,6 +117,9 @@ fn serve( tokio::spawn(hyper::Server::builder(incoming).serve(new_service)); handles.push(server); } + _ => { + unreachable!() + } }; } BindAddr::Path(path) => { @@ -195,7 +197,11 @@ fn print_listening(args: Arc) -> Result<()> { IpAddr::V4(_) => 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) } BindAddr::Path(path) => path.display().to_string(), diff --git a/src/server.rs b/src/server.rs index 85cbce1..c9f0b70 100644 --- a/src/server.rs +++ b/src/server.rs @@ -71,13 +71,13 @@ impl Server { encode_uri(&format!( "{}{}", &args.uri_prefix, - get_file_name(&args.path) + get_file_name(&args.serve_path) )), ] } else { 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"))?), None => Cow::Borrowed(INDEX_HTML), }; @@ -180,7 +180,7 @@ impl Server { .iter() .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?; } else { status_not_found(&mut res); @@ -620,7 +620,7 @@ impl Server { res: &mut Response, ) -> Result<()> { 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) .await?; } else { @@ -636,7 +636,7 @@ impl Server { res: &mut Response, ) -> Result { 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) => { let path = assets_path.join(name); self.handle_send_file(&path, headers, false, res).await?; @@ -776,7 +776,10 @@ impl Server { ) -> Result<()> { let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),); 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 = vec![]; file.take(1024).read_to_end(&mut buffer).await?; let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text(); @@ -822,12 +825,15 @@ impl Server { }, 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], None => vec![], }; 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), Err(_) => { status_forbid(res); @@ -847,7 +853,7 @@ impl Server { } 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())); } else { status_not_found(res); @@ -990,7 +996,10 @@ impl Server { } 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 data = IndexData { kind: DataKind::Index, @@ -1038,7 +1047,7 @@ impl Server { fs::canonicalize(path) .await .ok() - .map(|v| v.starts_with(&self.args.path)) + .map(|v| v.starts_with(&self.args.serve_path)) .unwrap_or_default() } @@ -1104,14 +1113,14 @@ impl Server { fn join_path(&self, path: &str) -> Option { if path.is_empty() { - return Some(self.args.path.clone()); + return Some(self.args.serve_path.clone()); } let path = if cfg!(windows) { path.replace('/', "\\") } else { path.to_string() }; - Some(self.args.path.join(path)) + Some(self.args.serve_path.join(path)) } async fn list_dir( diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 0000000..d380d83 --- /dev/null +++ b/tests/config.rs @@ -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 +} diff --git a/tests/data/config.yaml b/tests/data/config.yaml new file mode 100644 index 0000000..ad2478d --- /dev/null +++ b/tests/data/config.yaml @@ -0,0 +1,9 @@ +bind: + - 0.0.0.0 +path-prefix: dufs +hidden: + - dir3 + - test.txt +auth: + - user:pass@/:rw +allow-upload: true diff --git a/tests/log_http.rs b/tests/log_http.rs index f991291..a504f59 100644 --- a/tests/log_http.rs +++ b/tests/log_http.rs @@ -41,7 +41,7 @@ fn log_remote_user( assert_eq!(resp.status(), 200); - let mut buf = [0; 4096]; + let mut buf = [0; 2048]; let buf_len = stdout.read(&mut buf)?; 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()?; assert_eq!(resp.status(), 200); - let mut buf = [0; 4096]; + let mut buf = [0; 2048]; let buf_len = stdout.read(&mut buf)?; let output = std::str::from_utf8(&buf[0..buf_len])?; diff --git a/tests/tls.rs b/tests/tls.rs index 64b38fa..71463f0 100644 --- a/tests/tls.rs +++ b/tests/tls.rs @@ -7,6 +7,8 @@ use predicates::str::contains; use reqwest::blocking::ClientBuilder; use rstest::rstest; +use crate::fixtures::port; + /// Can start the server with TLS and receive encrypted responses. #[rstest] #[case(server(&[ @@ -33,8 +35,16 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> { /// Wrong path for cert throws error. #[rstest] fn wrong_path_cert() -> Result<(), Error> { + let port = port().to_string(); 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() .failure() .stderr(contains("Failed to access `wrong`")); @@ -45,8 +55,16 @@ fn wrong_path_cert() -> Result<(), Error> { /// Wrong paths for key throws errors. #[rstest] fn wrong_path_key() -> Result<(), Error> { + let port = port().to_string(); 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() .failure() .stderr(contains("Failed to access `wrong`"));