ferron/setup/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4
5#[derive(Debug, Clone, PartialEq, ValueEnum)]
6pub enum ConfigAdapter {
7  Kdl,
8  #[cfg(feature = "config-yaml-legacy")]
9  YamlLegacy,
10  #[cfg(feature = "config-docker-auto")]
11  DockerAuto,
12}
13
14#[derive(ValueEnum, Debug, Clone, PartialEq)]
15pub enum LogOutput {
16  Stdout,
17  Stderr,
18  Off,
19}
20
21#[derive(Args, Debug, Clone, PartialEq)]
22pub struct ServeArgs {
23  /// The listening IP to use.
24  #[arg(short, long, default_value = "127.0.0.1")]
25  pub listen_ip: String,
26
27  /// The port to use.
28  #[arg(short, long, default_value = "3000")]
29  pub port: u16,
30
31  /// The root directory to serve.
32  #[arg(short, long, default_value = ".")]
33  pub root: PathBuf,
34
35  /// Basic authentication credentials for authorized users. The credential value must
36  /// be in the form "${user}:${hashed_password}" where the "${hashed_password}" is from
37  /// the ferron-passwd program or from any program using the password-auth generate_hash()
38  /// macro (see https://docs.rs/password-auth/latest/password_auth/fn.generate_hash.html).
39  #[arg(short, long)]
40  pub credential: Vec<String>,
41
42  /// Whether to disable brute-force password protection.
43  #[arg(long)]
44  pub disable_brute_protection: bool,
45
46  /// Whether to start the server as a forward proxy.
47  #[arg(long)]
48  pub forward_proxy: bool,
49
50  /// Where to output logs.
51  #[arg(long, default_value = "stdout")]
52  pub log: LogOutput,
53
54  /// Where to output error logs.
55  #[arg(long, default_value = "stderr")]
56  pub error_log: LogOutput,
57}
58
59#[derive(Subcommand, Debug, Clone, PartialEq)]
60pub enum Command {
61  /// Utility command to start up a basic HTTP server.
62  Serve(ServeArgs),
63}
64
65/// A fast, memory-safe web server written in Rust
66#[derive(Parser, Debug, PartialEq)]
67#[command(about, long_about = None)]
68pub struct FerronArgs {
69  /// The path to the server configuration file
70  #[arg(short, long, default_value = "./ferron.kdl")]
71  pub config: PathBuf,
72
73  /// The string containing the server configuration
74  #[arg(long)]
75  pub config_string: Option<String>,
76
77  /// The configuration adapter to use
78  #[arg(long, value_enum)]
79  pub config_adapter: Option<ConfigAdapter>,
80
81  /// Prints the used compile-time module configuration (`ferron-build.yaml` or `ferron-build-override.yaml` in the Ferron source) and exits
82  #[arg(long)]
83  pub module_config: bool,
84
85  /// Print version and build information
86  #[arg(short = 'V', long)]
87  pub version: bool,
88
89  #[command(subcommand)]
90  pub command: Option<Command>,
91}
92
93#[cfg(test)]
94mod tests {
95  use super::*;
96
97  // The hash here is for the password '123?45>6'.
98  const COMMON_TEST_PASSWORD: &str =
99    "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08";
100
101  #[test]
102  fn test_supported_args() {
103    let args = FerronArgs::parse_from(vec![
104      "ferron",
105      "--config",
106      "/dev/null",
107      "--config-adapter",
108      "kdl",
109      "--module-config",
110      "--version",
111    ]);
112    assert!(args.module_config);
113    assert!(args.version);
114    assert_eq!(PathBuf::from("/dev/null"), args.config);
115    assert_eq!(Some(ConfigAdapter::Kdl), args.config_adapter);
116    assert_eq!(None, args.command);
117  }
118
119  #[test]
120  fn test_supported_args_short_options() {
121    let args = FerronArgs::parse_from(vec![
122      "ferron",
123      "-c",
124      "/dev/null",
125      "--config-adapter",
126      "kdl",
127      "--module-config",
128      "-V",
129    ]);
130    assert!(args.module_config);
131    assert!(args.version);
132    assert_eq!(PathBuf::from("/dev/null"), args.config);
133    assert_eq!(None, args.config_string);
134    assert_eq!(Some(ConfigAdapter::Kdl), args.config_adapter);
135    assert_eq!(None, args.command);
136  }
137
138  #[test]
139  fn test_supported_optional_args() {
140    let args = FerronArgs::parse_from(vec!["ferron"]);
141    assert!(!args.module_config);
142    assert!(!args.version);
143    assert_eq!(PathBuf::from("./ferron.kdl"), args.config);
144    assert_eq!(None, args.config_string);
145    assert_eq!(None, args.config_adapter);
146    assert_eq!(None, args.command);
147  }
148
149  #[test]
150  fn test_supported_config_string_arg() {
151    let expected_string =
152      String::from(":8080 {\n  log \"/dev/stderr\"\n  error_log \"/dev/stderr\"\n  root \"/mnt/www\"\n}");
153    let args = FerronArgs::parse_from(vec!["ferron", "--config-string", &expected_string]);
154    assert!(!args.module_config);
155    assert!(!args.version);
156    assert_eq!(PathBuf::from("./ferron.kdl"), args.config);
157    assert_eq!(Some(expected_string), args.config_string);
158    assert_eq!(None, args.config_adapter);
159    assert_eq!(None, args.command);
160  }
161
162  #[test]
163  fn test_supported_http_serve_default_args() {
164    let args = FerronArgs::parse_from(vec!["ferron", "serve"]);
165    assert!(!args.module_config);
166    assert!(!args.version);
167    assert_eq!(PathBuf::from("./ferron.kdl"), args.config);
168    assert_eq!(None, args.config_string);
169    assert_eq!(None, args.config_adapter);
170    assert!(args.command.is_some());
171    match args.command.unwrap() {
172      Command::Serve(http_serve_args) => {
173        assert_eq!(String::from("127.0.0.1"), http_serve_args.listen_ip);
174        assert_eq!(3000, http_serve_args.port);
175        assert_eq!(PathBuf::from("."), http_serve_args.root);
176        assert_eq!(Vec::<String>::new(), http_serve_args.credential);
177        assert!(!http_serve_args.disable_brute_protection);
178        assert!(!http_serve_args.forward_proxy);
179        assert_eq!(LogOutput::Stdout, http_serve_args.log);
180        assert_eq!(LogOutput::Stderr, http_serve_args.error_log);
181      }
182    }
183  }
184
185  #[test]
186  fn test_supported_http_serve_args() {
187    let args = FerronArgs::parse_from(vec![
188      "ferron",
189      "serve",
190      "--listen-ip",
191      "0.0.0.0",
192      "--port",
193      "8080",
194      "--root",
195      "./wwwroot",
196      "--credential",
197      format!("test:{COMMON_TEST_PASSWORD}").as_str(),
198      "--credential",
199      format!("test2:{COMMON_TEST_PASSWORD}").as_str(),
200      "--disable-brute-protection",
201      "--forward-proxy",
202      "--log",
203      "off",
204      "--error-log",
205      "off",
206    ]);
207    assert!(!args.module_config);
208    assert!(!args.version);
209    assert_eq!(PathBuf::from("./ferron.kdl"), args.config);
210    assert_eq!(None, args.config_string);
211    assert_eq!(None, args.config_adapter);
212    assert!(args.command.is_some());
213    match args.command.unwrap() {
214      Command::Serve(http_serve_args) => {
215        assert_eq!(String::from("0.0.0.0"), http_serve_args.listen_ip);
216        assert_eq!(8080, http_serve_args.port);
217        assert_eq!(PathBuf::from("./wwwroot"), http_serve_args.root);
218        assert_eq!(
219          vec![
220            format!("test:{COMMON_TEST_PASSWORD}"),
221            format!("test2:{COMMON_TEST_PASSWORD}")
222          ],
223          http_serve_args.credential
224        );
225        assert!(http_serve_args.disable_brute_protection);
226        assert!(http_serve_args.forward_proxy);
227        assert_eq!(LogOutput::Off, http_serve_args.log);
228        assert_eq!(LogOutput::Off, http_serve_args.error_log);
229      }
230    }
231  }
232}