dufs/src/args.rs

400 lines
13 KiB
Rust
Raw Normal View History

2023-02-21 12:23:24 +03:00
use anyhow::{anyhow, bail, Result};
2022-11-11 16:46:07 +03:00
use clap::builder::PossibleValuesParser;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
2022-07-06 07:11:00 +03:00
use clap_complete::{generate, Generator, Shell};
#[cfg(feature = "tls")]
2022-06-02 06:06:41 +03:00
use rustls::{Certificate, PrivateKey};
use std::env;
use std::net::IpAddr;
2022-05-26 11:17:55 +03:00
use std::path::{Path, PathBuf};
use crate::auth::AccessControl;
use crate::auth::AuthMethod;
use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT};
#[cfg(feature = "tls")]
use crate::tls::{load_certs, load_private_key};
use crate::utils::encode_uri;
2022-05-26 11:17:55 +03:00
2022-11-11 16:46:07 +03:00
pub fn build_cli() -> Command {
let app = Command::new(env!("CARGO_CRATE_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(concat!(
env!("CARGO_PKG_DESCRIPTION"),
" - ",
env!("CARGO_PKG_REPOSITORY")
))
2022-11-11 16:46:07 +03:00
.arg(
Arg::new("root")
.env("DUFS_ROOT")
.hide_env(true)
2022-11-11 16:46:07 +03:00
.default_value(".")
.value_parser(value_parser!(PathBuf))
.help("Specific path to serve"),
)
.arg(
Arg::new("bind")
.env("DUFS_BIND")
.hide_env(true)
.short('b')
.long("bind")
2022-11-11 03:57:44 +03:00
.help("Specify bind address or unix socket")
.action(ArgAction::Append)
2022-11-11 16:46:07 +03:00
.value_delimiter(',')
.value_name("addrs"),
)
.arg(
Arg::new("port")
.env("DUFS_PORT")
.hide_env(true)
.short('p')
.long("port")
.default_value("5000")
2022-11-11 16:46:07 +03:00
.value_parser(value_parser!(u16))
.help("Specify port to listen on")
.value_name("port"),
)
2022-06-02 03:32:31 +03:00
.arg(
Arg::new("path-prefix")
.env("DUFS_PATH_PREFIX")
.hide_env(true)
2022-06-02 03:32:31 +03:00
.long("path-prefix")
.value_name("path")
.help("Specify a path prefix"),
2022-06-02 03:32:31 +03:00
)
.arg(
Arg::new("hidden")
.env("DUFS_HIDDEN")
.hide_env(true)
.long("hidden")
2022-09-09 16:30:27 +03:00
.help("Hide paths from directory listings, separated by `,`")
.value_name("value"),
)
.arg(
Arg::new("auth")
.env("DUFS_AUTH")
.hide_env(true)
.short('a')
.long("auth")
.help("Add auth for path")
.action(ArgAction::Append)
.value_delimiter(',')
2022-11-11 16:46:07 +03:00
.value_name("rules"),
)
2022-06-20 10:11:39 +03:00
.arg(
Arg::new("auth-method")
.env("DUFS_AUTH_METHOD")
.hide_env(true)
2022-06-20 10:11:39 +03:00
.long("auth-method")
.help("Select auth method")
2022-11-11 16:46:07 +03:00
.value_parser(PossibleValuesParser::new(["basic", "digest"]))
2022-06-20 10:11:39 +03:00
.default_value("digest")
.value_name("value"),
)
.arg(
Arg::new("allow-all")
.env("DUFS_ALLOW_ALL")
.hide_env(true)
.short('A')
.long("allow-all")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
.help("Allow all operations"),
)
.arg(
Arg::new("allow-upload")
.env("DUFS_ALLOW_UPLOAD")
.hide_env(true)
.long("allow-upload")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
2022-06-02 14:32:19 +03:00
.help("Allow upload files/folders"),
)
.arg(
Arg::new("allow-delete")
.env("DUFS_ALLOW_DELETE")
.hide_env(true)
2022-05-31 17:38:00 +03:00
.long("allow-delete")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
2022-06-02 14:32:19 +03:00
.help("Allow delete files/folders"),
)
2022-06-21 02:23:20 +03:00
.arg(
Arg::new("allow-search")
.env("DUFS_ALLOW_SEARCH")
.hide_env(true)
2022-06-21 02:23:20 +03:00
.long("allow-search")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
2022-06-21 02:23:20 +03:00
.help("Allow search files/folders"),
)
2022-05-31 15:53:14 +03:00
.arg(
Arg::new("allow-symlink")
.env("DUFS_ALLOW_SYMLINK")
.hide_env(true)
2022-05-31 17:38:00 +03:00
.long("allow-symlink")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
2022-06-02 14:32:19 +03:00
.help("Allow symlink to files/folders outside root directory"),
2022-05-31 15:53:14 +03:00
)
.arg(
Arg::new("allow-archive")
.env("DUFS_ALLOW_ARCHIVE")
.hide_env(true)
.long("allow-archive")
.action(ArgAction::SetTrue)
.help("Allow zip archive generation"),
)
.arg(
Arg::new("enable-cors")
.env("DUFS_ENABLE_CORS")
.hide_env(true)
.long("enable-cors")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
.help("Enable CORS, sets `Access-Control-Allow-Origin: *`"),
)
.arg(
Arg::new("render-index")
.env("DUFS_RENDER_INDEX")
.hide_env(true)
.long("render-index")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
.help("Serve index.html when requesting a directory, returns 404 if not found index.html"),
)
.arg(
Arg::new("render-try-index")
.env("DUFS_RENDER_TRY_INDEX")
.hide_env(true)
.long("render-try-index")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
.help("Serve index.html when requesting a directory, returns directory listing if not found index.html"),
)
.arg(
Arg::new("render-spa")
.env("DUFS_RENDER_SPA")
.hide_env(true)
.long("render-spa")
2022-11-11 16:46:07 +03:00
.action(ArgAction::SetTrue)
.help("Serve SPA(Single Page Application)"),
)
.arg(
Arg::new("assets")
.env("DUFS_ASSETS")
.hide_env(true)
.long("assets")
.help("Use custom assets to override builtin assets")
2022-11-11 16:46:07 +03:00
.value_parser(value_parser!(PathBuf))
.value_name("path")
);
#[cfg(feature = "tls")]
let app = app
2022-06-02 06:06:41 +03:00
.arg(
Arg::new("tls-cert")
.env("DUFS_TLS_CERT")
.hide_env(true)
2022-06-02 06:06:41 +03:00
.long("tls-cert")
.value_name("path")
2022-11-11 16:46:07 +03:00
.value_parser(value_parser!(PathBuf))
2022-06-02 06:06:41 +03:00
.help("Path to an SSL/TLS certificate to serve with HTTPS"),
)
.arg(
Arg::new("tls-key")
.env("DUFS_TLS_KEY")
.hide_env(true)
2022-06-02 06:06:41 +03:00
.long("tls-key")
.value_name("path")
2022-11-11 16:46:07 +03:00
.value_parser(value_parser!(PathBuf))
2022-06-02 06:06:41 +03:00
.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>"),
)
2022-05-26 11:17:55 +03:00
}
2022-07-06 07:11:00 +03:00
pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
2022-05-26 11:17:55 +03:00
}
#[derive(Debug)]
2022-05-26 11:17:55 +03:00
pub struct Args {
2022-11-11 03:57:44 +03:00
pub addrs: Vec<BindAddr>,
pub port: u16,
2022-05-26 11:17:55 +03:00
pub path: PathBuf,
pub path_is_file: bool,
pub path_prefix: String,
pub uri_prefix: String,
pub hidden: Vec<String>,
pub auth_method: AuthMethod,
pub auth: AccessControl,
pub allow_upload: bool,
pub allow_delete: bool,
2022-06-21 02:23:20 +03:00
pub allow_search: bool,
2022-05-31 15:53:14 +03:00
pub allow_symlink: bool,
pub allow_archive: bool,
pub render_index: bool,
pub render_spa: bool,
2022-06-17 14:02:13 +03:00
pub render_try_index: bool,
pub enable_cors: bool,
pub assets_path: Option<PathBuf>,
pub log_http: LogHttp,
#[cfg(feature = "tls")]
2022-06-02 06:06:41 +03:00
pub tls: Option<(Vec<Certificate>, PrivateKey)>,
#[cfg(not(feature = "tls"))]
pub tls: Option<()>,
2022-05-26 11:17:55 +03:00
}
impl Args {
/// Parse command-line arguments.
///
/// If a parsing error occurred, exit the process and print out informative
2022-05-26 11:17:55 +03:00
/// error message to user.
2023-02-21 12:23:24 +03:00
pub fn parse(matches: ArgMatches) -> Result<Args> {
2022-11-11 16:46:07 +03:00
let port = *matches.get_one::<u16>("port").unwrap();
let addrs = matches
2022-11-11 16:46:07 +03:00
.get_many::<String>("bind")
.map(|bind| bind.map(|v| v.as_str()).collect())
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
2022-11-11 03:57:44 +03:00
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?;
2022-11-11 16:46:07 +03:00
let path = Args::parse_path(matches.get_one::<PathBuf>("root").unwrap())?;
let path_is_file = path.metadata()?.is_file();
2022-06-04 07:51:56 +03:00
let path_prefix = matches
2022-11-11 16:46:07 +03:00
.get_one::<String>("path-prefix")
.map(|v| v.trim_matches('/').to_owned())
.unwrap_or_default();
let uri_prefix = if path_prefix.is_empty() {
"/".to_owned()
} else {
format!("/{}/", &encode_uri(&path_prefix))
};
let hidden: Vec<String> = matches
2022-11-11 16:46:07 +03:00
.get_one::<String>("hidden")
2022-09-09 16:30:27 +03:00
.map(|v| v.split(',').map(|x| x.to_string()).collect())
.unwrap_or_default();
2022-11-11 16:46:07 +03:00
let enable_cors = matches.get_flag("enable-cors");
let auth: Vec<&str> = matches
2022-11-11 16:46:07 +03:00
.get_many::<String>("auth")
.map(|auth| auth.map(|v| v.as_str()).collect())
.unwrap_or_default();
2022-11-11 16:46:07 +03:00
let auth_method = match matches.get_one::<String>("auth-method").unwrap().as_str() {
"basic" => AuthMethod::Basic,
_ => AuthMethod::Digest,
};
let auth = AccessControl::new(&auth, &uri_prefix)?;
2022-11-11 16:46:07 +03:00
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");
2022-11-11 16:46:07 +03:00
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")]
2022-11-11 16:46:07 +03:00
let tls = match (
matches.get_one::<PathBuf>("tls-cert"),
matches.get_one::<PathBuf>("tls-key"),
) {
2022-06-02 06:06:41 +03:00
(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
2022-11-11 16:46:07 +03:00
.get_one::<String>("log-format")
.map(|v| v.as_str())
.unwrap_or(DEFAULT_LOG_FORMAT)
.parse()?;
2022-11-11 16:46:07 +03:00
let assets_path = match matches.get_one::<PathBuf>("assets") {
Some(v) => Some(Args::parse_assets_path(v)?),
None => None,
};
2022-05-26 11:17:55 +03:00
Ok(Args {
addrs,
port,
2022-05-26 11:17:55 +03:00
path,
path_is_file,
2022-06-02 03:32:31 +03:00
path_prefix,
uri_prefix,
hidden,
auth_method,
2022-05-26 13:06:52 +03:00
auth,
enable_cors,
allow_delete,
allow_upload,
2022-06-21 02:23:20 +03:00
allow_search,
2022-05-31 15:53:14 +03:00
allow_symlink,
allow_archive,
render_index,
2022-06-17 14:02:13 +03:00
render_try_index,
render_spa,
2022-06-02 06:06:41 +03:00
tls,
log_http,
assets_path,
2022-05-26 11:17:55 +03:00
})
}
2023-02-21 12:23:24 +03:00
fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
2022-11-11 03:57:44 +03:00
let mut bind_addrs = vec![];
let mut invalid_addrs = vec![];
for addr in addrs {
match addr.parse::<IpAddr>() {
Ok(v) => {
2022-11-11 03:57:44 +03:00
bind_addrs.push(BindAddr::Address(v));
}
Err(_) => {
2022-11-11 03:57:44 +03:00
if cfg!(unix) {
bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
} else {
invalid_addrs.push(*addr);
}
}
}
}
if !invalid_addrs.is_empty() {
2023-02-21 12:23:24 +03:00
bail!("Invalid bind address `{}`", invalid_addrs.join(","));
}
2022-11-11 03:57:44 +03:00
Ok(bind_addrs)
}
2023-02-21 12:23:24 +03:00
fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
2022-05-26 11:17:55 +03:00
let path = path.as_ref();
if !path.exists() {
2023-02-21 12:23:24 +03:00
bail!("Path `{}` doesn't exist", path.display());
2022-05-26 11:17:55 +03:00
}
env::current_dir()
.and_then(|mut p| {
p.push(path); // If path is absolute, it replaces the current path.
2022-05-31 17:38:00 +03:00
std::fs::canonicalize(p)
2022-05-26 11:17:55 +03:00
})
2023-02-21 12:23:24 +03:00
.map_err(|err| anyhow!("Failed to access path `{}`: {}", path.display(), err,))
2022-05-26 11:17:55 +03:00
}
2023-02-21 12:23:24 +03:00
fn parse_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = Self::parse_path(path)?;
if !path.join("index.html").exists() {
2023-02-21 12:23:24 +03:00
bail!("Path `{}` doesn't contains index.html", path.display());
}
Ok(path)
}
2022-06-06 05:52:12 +03:00
}
2022-11-11 03:57:44 +03:00
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BindAddr {
Address(IpAddr),
Path(PathBuf),
}