Skip to main content

rns_ctl/
remote.rs

1//! Remote management query helper.
2//!
3//! Connects as a shared client, creates a link to a remote management
4//! destination, sends a request, and returns the response data.
5
6use std::path::Path;
7use std::sync::mpsc;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use rns_net::shared_client::SharedClientConfig;
12use rns_net::{Callbacks, RnsNode};
13
14/// Parse a 32-hex-char destination hash.
15pub fn parse_hex_hash(s: &str) -> Option<[u8; 16]> {
16    let s = s.trim();
17    if s.len() != 32 {
18        return None;
19    }
20    let bytes: Vec<u8> = (0..s.len())
21        .step_by(2)
22        .filter_map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
23        .collect();
24    if bytes.len() != 16 {
25        return None;
26    }
27    let mut result = [0u8; 16];
28    result.copy_from_slice(&bytes);
29    Some(result)
30}
31
32/// Result from a remote management query.
33pub struct RemoteQueryResult {
34    /// Raw response data from the management request.
35    pub data: Vec<u8>,
36}
37
38/// Callbacks that capture link establishment and response data.
39struct RemoteCallbacks {
40    link_established_tx: mpsc::Sender<rns_net::LinkId>,
41    response_data: Arc<Mutex<Option<Vec<u8>>>>,
42    response_tx: mpsc::Sender<()>,
43}
44
45impl Callbacks for RemoteCallbacks {
46    fn on_announce(&mut self, _announced: rns_net::AnnouncedIdentity) {}
47
48    fn on_path_updated(&mut self, _dest_hash: rns_net::DestHash, _hops: u8) {}
49
50    fn on_local_delivery(
51        &mut self,
52        _dest_hash: rns_net::DestHash,
53        _raw: Vec<u8>,
54        _packet_hash: rns_net::PacketHash,
55    ) {
56    }
57
58    fn on_link_established(
59        &mut self,
60        link_id: rns_net::LinkId,
61        _dest_hash: rns_net::DestHash,
62        _rtt: f64,
63        _is_initiator: bool,
64    ) {
65        let _ = self.link_established_tx.send(link_id);
66    }
67
68    fn on_response(&mut self, _link_id: rns_net::LinkId, _request_id: [u8; 16], data: Vec<u8>) {
69        *self.response_data.lock().unwrap() = Some(data);
70        let _ = self.response_tx.send(());
71    }
72}
73
74/// Perform a remote management query.
75///
76/// 1. Connects as a shared client
77/// 2. Creates a link to the management destination
78/// 3. Identifies on the link
79/// 4. Sends a request to the specified path
80/// 5. Returns the response data
81///
82/// Returns `None` if the query fails or times out.
83pub fn remote_query(
84    dest_hash: [u8; 16],
85    dest_sig_pub: [u8; 32],
86    identity_prv_key: [u8; 64],
87    path: &str,
88    data: &[u8],
89    config_path: Option<&Path>,
90    timeout: Duration,
91) -> Option<RemoteQueryResult> {
92    let (link_tx, link_rx) = mpsc::channel();
93    let (resp_tx, resp_rx) = mpsc::channel();
94    let response_data = Arc::new(Mutex::new(None));
95
96    let callbacks = RemoteCallbacks {
97        link_established_tx: link_tx,
98        response_data: response_data.clone(),
99        response_tx: resp_tx,
100    };
101
102    // Load config for shared instance connection
103    let config_dir = rns_net::storage::resolve_config_dir(config_path);
104    let config_file = config_dir.join("config");
105    let rns_config = if config_file.exists() {
106        rns_net::config::parse_file(&config_file).ok()?
107    } else {
108        rns_net::config::parse("").ok()?
109    };
110
111    let shared_config = SharedClientConfig {
112        instance_name: rns_config.reticulum.instance_name.clone(),
113        port: rns_config.reticulum.shared_instance_port,
114        rpc_port: rns_config.reticulum.instance_control_port,
115    };
116
117    let node = RnsNode::connect_shared(shared_config, Box::new(callbacks)).ok()?;
118
119    // Wait briefly for connection
120    std::thread::sleep(Duration::from_millis(500));
121
122    // Create link to management destination
123    let link_id = node.create_link(dest_hash, dest_sig_pub).ok()?;
124
125    // Wait for link establishment
126    let _established_link_id = link_rx.recv_timeout(timeout).ok()?;
127
128    // Identify on the link
129    node.identify_on_link(link_id, identity_prv_key).ok()?;
130    std::thread::sleep(Duration::from_millis(200));
131
132    // Send the request
133    node.send_request(link_id, path, data).ok()?;
134
135    // Wait for response
136    resp_rx.recv_timeout(timeout).ok()?;
137
138    let data = response_data.lock().unwrap().take()?;
139    node.shutdown();
140
141    Some(RemoteQueryResult { data })
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn parse_hex_hash_valid() {
150        let hash = parse_hex_hash("0123456789abcdef0123456789abcdef").unwrap();
151        assert_eq!(hash[0], 0x01);
152        assert_eq!(hash[15], 0xef);
153    }
154
155    #[test]
156    fn parse_hex_hash_invalid() {
157        assert!(parse_hex_hash("short").is_none());
158        assert!(parse_hex_hash("0123456789abcdef0123456789abcdef00").is_none());
159        assert!(parse_hex_hash("xyz3456789abcdef0123456789abcdef").is_none());
160    }
161
162    #[test]
163    fn parse_hex_hash_trimmed() {
164        let hash = parse_hex_hash("  0123456789abcdef0123456789abcdef  ").unwrap();
165        assert_eq!(hash[0], 0x01);
166    }
167}