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.get("tls-cert").map(String::from);
152 cfg.tls_key = args.get("tls-key").map(String::from);
153
154 cfg
155}
156
157pub fn print_help() {
158 println!(
159 "rns-ctl - HTTP/WebSocket control interface for Reticulum
160
161USAGE:
162 rns-ctl [OPTIONS]
163
164OPTIONS:
165 -c, --config PATH Path to RNS config directory
166 -p, --port PORT HTTP port (default: 8080, env: RNSCTL_HTTP_PORT)
167 -H, --host HOST Bind host (default: 127.0.0.1, env: RNSCTL_HOST)
168 -t, --token TOKEN Auth bearer token (env: RNSCTL_AUTH_TOKEN)
169 -d, --daemon Connect as client to running rnsd
170 --disable-auth Disable authentication
171 --tls-cert PATH TLS certificate file (requires 'tls' feature)
172 --tls-key PATH TLS private key file (requires 'tls' feature)
173 -v Increase verbosity (repeat for more)
174 -h, --help Show this help
175 --version Show version"
176 );
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn args(s: &[&str]) -> Args {
184 Args::parse_from(s.iter().map(|s| s.to_string()).collect())
185 }
186
187 #[test]
188 fn parse_basic() {
189 let a = args(&["--port", "9090", "--host", "0.0.0.0", "-vv"]);
190 assert_eq!(a.get("port"), Some("9090"));
191 assert_eq!(a.get("host"), Some("0.0.0.0"));
192 assert_eq!(a.verbosity, 2);
193 }
194
195 #[test]
196 fn parse_short_config() {
197 let a = args(&["-c", "/tmp/rns"]);
198 assert_eq!(a.get("c"), Some("/tmp/rns"));
199 }
200
201 #[test]
202 fn parse_daemon() {
203 let a = args(&["-d"]);
204 assert!(a.has("daemon"));
205 }
206
207 #[test]
208 fn parse_disable_auth() {
209 let a = args(&["--disable-auth"]);
210 assert!(a.has("disable-auth"));
211 }
212
213 #[test]
214 fn parse_help() {
215 let a = args(&["--help"]);
216 assert!(a.has("help"));
217 let a = args(&["-h"]);
218 assert!(a.has("help"));
219 }
220
221 #[test]
222 fn config_from_args() {
223 let a = args(&["--port", "3000", "--host", "0.0.0.0", "--token", "secret", "--daemon"]);
224 let cfg = from_args_and_env(&a);
225 assert_eq!(cfg.port, 3000);
226 assert_eq!(cfg.host, "0.0.0.0");
227 assert_eq!(cfg.auth_token.as_deref(), Some("secret"));
228 assert!(cfg.daemon_mode);
229 }
230}