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