1use std::collections::HashMap;
2
3pub struct CtlConfig {
5 pub host: String,
7 pub port: u16,
9 pub auth_token: Option<String>,
11 pub disable_auth: bool,
13 pub config_path: Option<String>,
15 pub daemon_mode: bool,
17 pub tls_cert: Option<String>,
19 pub tls_key: Option<String>,
21}
22
23impl Default for CtlConfig {
24 fn default() -> Self {
25 CtlConfig {
26 host: "127.0.0.1".into(),
27 port: 8080,
28 auth_token: None,
29 disable_auth: false,
30 config_path: None,
31 daemon_mode: false,
32 tls_cert: None,
33 tls_key: None,
34 }
35 }
36}
37
38pub struct Args {
40 pub flags: HashMap<String, String>,
41 pub verbosity: u8,
42}
43
44impl Args {
45 pub fn parse() -> Self {
46 Self::parse_from(std::env::args().skip(1).collect())
47 }
48
49 pub fn parse_from(args: Vec<String>) -> Self {
50 let mut flags = HashMap::new();
51 let mut verbosity: u8 = 0;
52 let mut iter = args.into_iter();
53
54 while let Some(arg) = iter.next() {
55 if arg.starts_with("--") {
56 let key = arg[2..].to_string();
57 if let Some(eq_pos) = key.find('=') {
58 let (k, v) = key.split_at(eq_pos);
59 flags.insert(k.to_string(), v[1..].to_string());
60 } else {
61 match key.as_str() {
62 "help" | "daemon" | "disable-auth" | "version" => {
63 flags.insert(key, "true".into());
64 }
65 _ => {
66 if let Some(val) = iter.next() {
67 flags.insert(key, val);
68 } else {
69 flags.insert(key, "true".into());
70 }
71 }
72 }
73 }
74 } else if arg.starts_with('-') && arg.len() > 1 {
75 for c in arg[1..].chars() {
76 match c {
77 'v' => verbosity = verbosity.saturating_add(1),
78 'h' => {
79 flags.insert("help".into(), "true".into());
80 }
81 'd' => {
82 flags.insert("daemon".into(), "true".into());
83 }
84 _ => {
85 if let Some(val) = iter.next() {
87 flags.insert(c.to_string(), val);
88 } else {
89 flags.insert(c.to_string(), "true".into());
90 }
91 }
92 }
93 }
94 }
95 }
96
97 Args { flags, verbosity }
98 }
99
100 pub fn get(&self, key: &str) -> Option<&str> {
101 self.flags.get(key).map(|s| s.as_str())
102 }
103
104 pub fn has(&self, key: &str) -> bool {
105 self.flags.contains_key(key)
106 }
107}
108
109pub fn from_args_and_env(args: &Args) -> CtlConfig {
111 let mut cfg = CtlConfig::default();
112
113 cfg.host = args
115 .get("host")
116 .or_else(|| args.get("H"))
117 .map(String::from)
118 .or_else(|| std::env::var("RNSCTL_HOST").ok())
119 .unwrap_or(cfg.host);
120
121 cfg.port = args
122 .get("port")
123 .or_else(|| args.get("p"))
124 .and_then(|s| s.parse().ok())
125 .or_else(|| {
126 std::env::var("RNSCTL_HTTP_PORT")
127 .ok()
128 .and_then(|s| s.parse().ok())
129 })
130 .unwrap_or(cfg.port);
131
132 cfg.auth_token = args
133 .get("token")
134 .or_else(|| args.get("t"))
135 .map(String::from)
136 .or_else(|| std::env::var("RNSCTL_AUTH_TOKEN").ok());
137
138 cfg.disable_auth = args.has("disable-auth")
139 || std::env::var("RNSCTL_DISABLE_AUTH")
140 .map(|v| v == "true" || v == "1")
141 .unwrap_or(false);
142
143 cfg.config_path = args
144 .get("config")
145 .or_else(|| args.get("c"))
146 .map(String::from)
147 .or_else(|| std::env::var("RNSCTL_CONFIG_PATH").ok());
148
149 cfg.daemon_mode = args.has("daemon");
150
151 cfg.tls_cert = args
152 .get("tls-cert")
153 .map(String::from)
154 .or_else(|| std::env::var("RNSCTL_TLS_CERT").ok());
155
156 cfg.tls_key = args
157 .get("tls-key")
158 .map(String::from)
159 .or_else(|| std::env::var("RNSCTL_TLS_KEY").ok());
160
161 cfg
162}
163
164pub fn print_help() {
165 println!(
166 "rns-ctl - HTTP/WebSocket control interface for Reticulum
167
168USAGE:
169 rns-ctl [OPTIONS]
170
171OPTIONS:
172 -c, --config PATH Path to RNS config directory
173 -p, --port PORT HTTP port (default: 8080, env: RNSCTL_HTTP_PORT)
174 -H, --host HOST Bind host (default: 127.0.0.1, env: RNSCTL_HOST)
175 -t, --token TOKEN Auth bearer token (env: RNSCTL_AUTH_TOKEN)
176 -d, --daemon Connect as client to running rnsd
177 --disable-auth Disable authentication
178 --tls-cert PATH TLS certificate file (env: RNSCTL_TLS_CERT, requires 'tls' feature)
179 --tls-key PATH TLS private key file (env: RNSCTL_TLS_KEY, requires 'tls' feature)
180 -v Increase verbosity (repeat for more)
181 -h, --help Show this help
182 --version Show version"
183 );
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn args(s: &[&str]) -> Args {
191 Args::parse_from(s.iter().map(|s| s.to_string()).collect())
192 }
193
194 #[test]
195 fn parse_basic() {
196 let a = args(&["--port", "9090", "--host", "0.0.0.0", "-vv"]);
197 assert_eq!(a.get("port"), Some("9090"));
198 assert_eq!(a.get("host"), Some("0.0.0.0"));
199 assert_eq!(a.verbosity, 2);
200 }
201
202 #[test]
203 fn parse_short_config() {
204 let a = args(&["-c", "/tmp/rns"]);
205 assert_eq!(a.get("c"), Some("/tmp/rns"));
206 }
207
208 #[test]
209 fn parse_daemon() {
210 let a = args(&["-d"]);
211 assert!(a.has("daemon"));
212 }
213
214 #[test]
215 fn parse_disable_auth() {
216 let a = args(&["--disable-auth"]);
217 assert!(a.has("disable-auth"));
218 }
219
220 #[test]
221 fn parse_help() {
222 let a = args(&["--help"]);
223 assert!(a.has("help"));
224 let a = args(&["-h"]);
225 assert!(a.has("help"));
226 }
227
228 #[test]
229 fn config_from_args() {
230 let a = args(&["--port", "3000", "--host", "0.0.0.0", "--token", "secret", "--daemon"]);
231 let cfg = from_args_and_env(&a);
232 assert_eq!(cfg.port, 3000);
233 assert_eq!(cfg.host, "0.0.0.0");
234 assert_eq!(cfg.auth_token.as_deref(), Some("secret"));
235 assert!(cfg.daemon_mode);
236 }
237}