Skip to main content

rns_ctl/
config.rs

1use std::collections::HashMap;
2
3/// Configuration for rns-ctl.
4pub struct CtlConfig {
5    /// Bind host (default: "127.0.0.1").
6    pub host: String,
7    /// HTTP port (default: 8080).
8    pub port: u16,
9    /// Bearer token for auth. If None and !disable_auth, a random token is generated.
10    pub auth_token: Option<String>,
11    /// Skip auth entirely.
12    pub disable_auth: bool,
13    /// Path to RNS config directory.
14    pub config_path: Option<String>,
15    /// Connect as shared instance client (--daemon).
16    pub daemon_mode: bool,
17    /// TLS certificate path.
18    pub tls_cert: Option<String>,
19    /// TLS private key path.
20    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
38/// Parsed command-line arguments.
39pub 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                            // Short flag with value: -c /path, -p 8080, -t token
86                            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
109/// Build CtlConfig from CLI args + environment variables.
110pub fn from_args_and_env(args: &Args) -> CtlConfig {
111    let mut cfg = CtlConfig::default();
112
113    // CLI args take precedence over env vars
114    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}