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