Skip to main content

rns_ctl/cmd/
http.rs

1//! HTTP/WebSocket control server subcommand.
2
3use std::net::SocketAddr;
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6
7use rns_crypto::Rng;
8use rns_crypto::identity::Identity;
9
10use crate::{bridge, config, encode, server, state};
11use crate::api;
12use crate::args::Args;
13
14pub fn run(args: Args) {
15    if args.has("help") {
16        print_help();
17        return;
18    }
19
20    if args.has("version") {
21        println!("rns-ctl {}", env!("FULL_VERSION"));
22        return;
23    }
24
25    // Init logging
26    let log_level = match args.verbosity {
27        0 => "info",
28        1 => "debug",
29        _ => "trace",
30    };
31    std::env::set_var("RUST_LOG", format!("rns_ctl={},rns_net={}", log_level, log_level));
32    env_logger::init();
33
34    let mut cfg = config::from_args_and_env(&args);
35
36    // Generate a random auth token if none provided and auth is not disabled
37    if cfg.auth_token.is_none() && !cfg.disable_auth {
38        let mut token_bytes = [0u8; 24];
39        rns_crypto::OsRng.fill_bytes(&mut token_bytes);
40        let token = encode::to_hex(&token_bytes);
41        log::info!("Generated auth token: {}", token);
42        println!("Auth token: {}", token);
43        cfg.auth_token = Some(token);
44    }
45
46    // Create shared state and broadcast registry
47    let shared_state = Arc::new(std::sync::RwLock::new(state::CtlState::new()));
48    let ws_broadcast: state::WsBroadcast = Arc::new(Mutex::new(Vec::new()));
49
50    // Create callbacks
51    let callbacks = Box::new(bridge::CtlCallbacks::new(
52        shared_state.clone(),
53        ws_broadcast.clone(),
54    ));
55
56    // Resolve config path
57    let config_path = cfg.config_path.as_deref().map(Path::new);
58
59    // Start the RNS node
60    log::info!("Starting RNS node...");
61    let node = if cfg.daemon_mode {
62        log::info!("Connecting as shared client (daemon mode)");
63        rns_net::RnsNode::connect_shared_from_config(config_path, callbacks)
64    } else {
65        rns_net::RnsNode::from_config(config_path, callbacks)
66    };
67
68    let node = match node {
69        Ok(n) => n,
70        Err(e) => {
71            log::error!("Failed to start node: {}", e);
72            std::process::exit(1);
73        }
74    };
75
76    // Get identity from the config dir
77    let config_dir = rns_net::storage::resolve_config_dir(config_path);
78    let paths = rns_net::storage::ensure_storage_dirs(&config_dir).ok();
79    let identity: Option<Identity> = paths
80        .as_ref()
81        .and_then(|p| rns_net::storage::load_or_create_identity(&p.identities).ok());
82
83    // Store identity info in shared state
84    {
85        let mut s = shared_state.write().unwrap();
86        if let Some(ref id) = identity {
87            s.identity_hash = Some(*id.hash());
88            // Identity doesn't impl Clone; copy via private key
89            if let Some(prv) = id.get_private_key() {
90                s.identity = Some(Identity::from_private_key(&prv));
91            }
92        }
93    }
94
95    // Wrap node for shared access
96    let node_handle: api::NodeHandle = Arc::new(Mutex::new(Some(node)));
97    let node_for_shutdown = node_handle.clone();
98
99    // Store node handle in shared state so callbacks can access it
100    {
101        let mut s = shared_state.write().unwrap();
102        s.node_handle = Some(node_handle.clone());
103    }
104
105    // Set up ctrl-c handler
106    let shutdown_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
107    let shutdown_flag_handler = shutdown_flag.clone();
108
109    ctrlc_handler(move || {
110        if shutdown_flag_handler.swap(true, std::sync::atomic::Ordering::SeqCst) {
111            // Second ctrl-c: force exit
112            std::process::exit(1);
113        }
114        log::info!("Shutting down...");
115        if let Some(node) = node_for_shutdown.lock().unwrap().take() {
116            node.shutdown();
117        }
118        std::process::exit(0);
119    });
120
121    // Validate and load TLS config
122    #[cfg(feature = "tls")]
123    let tls_config = {
124        match (&cfg.tls_cert, &cfg.tls_key) {
125            (Some(cert), Some(key)) => {
126                match crate::tls::load_tls_config(cert, key) {
127                    Ok(config) => {
128                        log::info!("TLS enabled with cert={} key={}", cert, key);
129                        Some(config)
130                    }
131                    Err(e) => {
132                        log::error!("Failed to load TLS config: {}", e);
133                        std::process::exit(1);
134                    }
135                }
136            }
137            (Some(_), None) | (None, Some(_)) => {
138                log::error!("Both --tls-cert and --tls-key must be provided together");
139                std::process::exit(1);
140            }
141            (None, None) => None,
142        }
143    };
144
145    #[cfg(not(feature = "tls"))]
146    {
147        if cfg.tls_cert.is_some() || cfg.tls_key.is_some() {
148            log::error!("TLS options require the 'tls' feature. Rebuild with: cargo build --features tls");
149            std::process::exit(1);
150        }
151    }
152
153    // Build server context
154    let ctx = Arc::new(server::ServerContext {
155        node: node_handle,
156        state: shared_state,
157        ws_broadcast,
158        config: cfg,
159        #[cfg(feature = "tls")]
160        tls_config,
161    });
162
163    let addr: SocketAddr = format!("{}:{}", ctx.config.host, ctx.config.port)
164        .parse()
165        .unwrap_or_else(|_| {
166            log::error!("Invalid bind address");
167            std::process::exit(1);
168        });
169
170    // Run server (blocks)
171    if let Err(e) = server::run_server(addr, ctx) {
172        log::error!("Server error: {}", e);
173        std::process::exit(1);
174    }
175}
176
177/// Set up a ctrl-c signal handler.
178fn ctrlc_handler<F: FnOnce() + Send + 'static>(handler: F) {
179    let handler = Mutex::new(Some(handler));
180    libc_signal(move || {
181        if let Some(f) = handler.lock().unwrap().take() {
182            f();
183        }
184    });
185}
186
187/// Register a SIGINT handler using libc, polling in a background thread.
188fn libc_signal<F: FnMut() + Send + 'static>(mut callback: F) {
189    std::thread::Builder::new()
190        .name("signal-handler".into())
191        .spawn(move || {
192            use std::sync::atomic::{AtomicBool, Ordering};
193            static SIGNALED: AtomicBool = AtomicBool::new(false);
194
195            #[cfg(unix)]
196            {
197                extern "C" fn sig_handler(_: i32) {
198                    SIGNALED.store(true, std::sync::atomic::Ordering::SeqCst);
199                }
200                unsafe {
201                    libc_ffi::signal(libc_ffi::SIGINT, sig_handler as *const () as usize);
202                }
203            }
204
205            loop {
206                std::thread::sleep(std::time::Duration::from_millis(100));
207                if SIGNALED.swap(false, Ordering::SeqCst) {
208                    callback();
209                    break;
210                }
211            }
212        })
213        .ok();
214}
215
216#[cfg(unix)]
217mod libc_ffi {
218    extern "C" {
219        pub fn signal(sig: i32, handler: usize) -> usize;
220    }
221    pub const SIGINT: i32 = 2;
222}
223
224fn print_help() {
225    println!(
226        "rns-ctl http - HTTP/WebSocket control interface for Reticulum
227
228USAGE:
229    rns-ctl http [OPTIONS]
230
231OPTIONS:
232    -c, --config PATH       Path to RNS config directory
233    -p, --port PORT         HTTP port (default: 8080, env: RNSCTL_HTTP_PORT)
234    -H, --host HOST         Bind host (default: 127.0.0.1, env: RNSCTL_HOST)
235    -t, --token TOKEN       Auth bearer token (env: RNSCTL_AUTH_TOKEN)
236    -d, --daemon            Connect as client to running rnsd
237        --disable-auth      Disable authentication
238        --tls-cert PATH     TLS certificate file (env: RNSCTL_TLS_CERT, requires 'tls' feature)
239        --tls-key PATH      TLS private key file (env: RNSCTL_TLS_KEY, requires 'tls' feature)
240    -v                      Increase verbosity (repeat for more)
241    -h, --help              Show this help
242        --version           Show version"
243    );
244}