]> OzVa Git service - ozva-cloud/commitdiff
feat: support config file with `--config` option (#281)
authorsigoden <sigoden@gmail.com>
Sat, 4 Nov 2023 08:58:19 +0000 (16:58 +0800)
committerGitHub <noreply@github.com>
Sat, 4 Nov 2023 08:58:19 +0000 (16:58 +0800)
12 files changed:
Cargo.lock
Cargo.toml
README.md
src/args.rs
src/auth.rs
src/log_http.rs
src/main.rs
src/server.rs
tests/config.rs [new file with mode: 0644]
tests/data/config.yaml [new file with mode: 0644]
tests/log_http.rs
tests/tls.rs

index d139806e7c00435479f4c207e2aaeb0c38e97f1b..0d59ed5c823b28945244cfbad04146b430b32824 100644 (file)
@@ -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"
index 76955e45d7cebaa6dc740c36e2c0094db9c2449b..4ee1c3e50a7f56385caa2127d34d4519d78aec50 100644 (file)
@@ -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"]
index 25c5a23063b626e1503c98ab79609cebf8431f9a..4d0e14172823fbb19ff053e139b5d1613adfb700 100644 (file)
--- 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 <config>      Specify configuration file
   -b, --bind <addrs>         Specify bind address or unix socket
   -p, --port <port>          Specify port to listen on [default: 5000]
       --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, --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 <path>        Use custom assets to override builtin 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
+      --assets <path>        Set the path to the assets directory for overriding the built-in assets
       --log-format <format>  Customize http log format
       --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
   -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 <addrs>          DUFS_BIND=0.0.0.0
   -p, --port <port>           DUFS_PORT=5000
       --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-spa            DUFS_RENDER_SPA=true
       --assets <path>         DUFS_ASSETS=/assets
+      --log-format <format>   DUFS_LOG_FORMAT=""
       --tls-cert <path>       DUFS_TLS_CERT=cert.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
index 8d4da3efdcc2d364310022f90f016e9890bbdb2a..65fd58cb51cb5af2b7dbdb449c8878ccc1bd488d 100644 (file)
@@ -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 <shell>"),
         );
 
     #[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 <shell>"),
-    )
+    app
 }
 
 pub fn print_completions<G: Generator>(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<BindAddr>,
     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<String>,
+    #[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<PathBuf>,
+    pub assets: Option<PathBuf>,
+    #[serde(deserialize_with = "deserialize_log_http")]
+    #[serde(rename = "log-format")]
     pub log_http: LogHttp,
-    #[cfg(feature = "tls")]
-    pub tls: Option<(Vec<Certificate>, PrivateKey)>,
-    #[cfg(not(feature = "tls"))]
-    pub tls: Option<()>,
+    pub tls_cert: Option<PathBuf>,
+    pub tls_key: Option<PathBuf>,
 }
 
 impl Args {
@@ -257,113 +271,134 @@ impl Args {
     /// If a parsing error occurred, exit the process and print out informative
     /// error message to user.
     pub fn parse(matches: ArgMatches) -> Result<Args> {
-        let port = *matches.get_one::<u16>("port").unwrap();
-        let addrs = matches
-            .get_many::<String>("bind")
-            .map(|bind| bind.map(|v| v.as_str()).collect())
-            .unwrap_or_else(|| vec!["0.0.0.0", "::"]);
-        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();
-        let path_prefix = matches
-            .get_one::<String>("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::<PathBuf>("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::<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()
         } 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")
             .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::<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");
+        {
+            args.hidden = hidden;
+        }
+
+        if !args.enable_cors {
+            args.enable_cors = matches.get_flag("enable-cors");
+        }
+
+        if let Some(rules) = matches.get_many::<String>("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::<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")]
-        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))
+        {
+            if let Some(tls_cert) = matches.get_one::<PathBuf>("tls-cert") {
+                args.tls_cert = Some(tls_cert.clone())
             }
-            _ => 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 {
-            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 let Some(tls_key) = matches.get_one::<PathBuf>("tls-key") {
+                args.tls_key = Some(tls_key.clone())
+            }
 
-    fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
-        let mut bind_addrs = vec![];
-        let mut invalid_addrs = vec![];
-        for addr in addrs {
-            match addr.parse::<IpAddr>() {
-                Ok(v) => {
-                    bind_addrs.push(BindAddr::Address(v));
-                }
-                Err(_) => {
-                    if cfg!(unix) {
-                        bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
-                    } else {
-                        invalid_addrs.push(*addr);
-                    }
-                }
+            match (&args.tls_cert, &args.tls_key) {
+                (Some(_), Some(_)) => {}
+                (Some(_), _) => bail!("No tls-key set"),
+                (_, Some(_)) => bail!("No tls-cert set"),
+                (None, None) => {}
             }
         }
-        if !invalid_addrs.is_empty() {
-            bail!("Invalid bind address `{}`", invalid_addrs.join(","));
+        #[cfg(not(feature = "tls"))]
+        {
+            args.tls_cert = None;
+            args.tls_key = None;
         }
-        Ok(bind_addrs)
+
+        Ok(args)
     }
 
-    fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
+    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());
@@ -377,8 +412,8 @@ impl Args {
             .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)?;
+    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());
         }
@@ -391,3 +426,56 @@ pub enum BindAddr {
     Address(IpAddr),
     Path(PathBuf),
 }
+
+impl BindAddr {
+    fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
+        let mut bind_addrs = vec![];
+        let mut invalid_addrs = vec![];
+        for addr in addrs {
+            match addr.parse::<IpAddr>() {
+                Ok(v) => {
+                    bind_addrs.push(BindAddr::Address(v));
+                }
+                Err(_) => {
+                    if cfg!(unix) {
+                        bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
+                    } else {
+                        invalid_addrs.push(*addr);
+                    }
+                }
+            }
+        }
+        if !invalid_addrs.is_empty() {
+            bail!("Invalid bind address `{}`", invalid_addrs.join(","));
+        }
+        Ok(bind_addrs)
+    }
+}
+
+fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, 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<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(".")
+}
index 25bf4a2921963e0f89789f74aad6fdaa1d81e2ce..9c64ea975251110ab32682702a031f55bb3f4c91 100644 (file)
@@ -25,21 +25,26 @@ lazy_static! {
     };
 }
 
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct AccessControl {
     users: IndexMap<String, (String, AccessPaths)>,
     anony: Option<AccessPaths>,
 }
 
+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<Self> {
         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![];
index 1c46a26aed7f53ca40c23bae828f764b52fbb4d6..49e712eaf4a76ecebd8ab4292155577c2037a7ea 100644 (file)
@@ -9,6 +9,12 @@ pub struct LogHttp {
     elements: Vec<LogElement>,
 }
 
+impl Default for LogHttp {
+    fn default() -> Self {
+        DEFAULT_LOG_FORMAT.parse().unwrap()
+    }
+}
+
 #[derive(Debug)]
 enum LogElement {
     Variable(String),
index 4487524a3ecf07ca9a46f62209289956cb555b8f..ebc38ee8d17f77e72caac430ed043c111aa35c99 100644 (file)
@@ -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<Args>) -> 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(),
index 85cbce15d4fec219b013c6c3cf093a0efababc78..c9f0b70f2efa45d36da34f6a273220e20861329c 100644 (file)
@@ -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<bool> {
         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<u8> = 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<PathBuf> {
         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 (file)
index 0000000..d380d83
--- /dev/null
@@ -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 (file)
index 0000000..ad2478d
--- /dev/null
@@ -0,0 +1,9 @@
+bind:
+  - 0.0.0.0
+path-prefix: dufs
+hidden:
+  - dir3
+  - test.txt
+auth: 
+  - user:pass@/:rw
+allow-upload: true
index f991291e4d6b5cbac7f50c3b8d25c30b8c0096ef..a504f59fe508bcbc4f853351101710668a7164e1 100644 (file)
@@ -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])?;
 
index 64b38fa04a47b5522a34c6fdfb04b6c4dc5440fd..71463f0208a27073a343a703bb9bf18813650e81 100644 (file)
@@ -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`"));