1use std::path::Path;
7use std::sync::mpsc;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use rns_net::{Callbacks, RnsNode};
12use rns_net::shared_client::SharedClientConfig;
13
14pub 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
32pub struct RemoteQueryResult {
34 pub data: Vec<u8>,
36}
37
38struct 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(&mut self, _dest_hash: rns_net::DestHash, _raw: Vec<u8>, _packet_hash: rns_net::PacketHash) {}
51
52 fn on_link_established(&mut self, link_id: rns_net::LinkId, _dest_hash: rns_net::DestHash, _rtt: f64, _is_initiator: bool) {
53 let _ = self.link_established_tx.send(link_id);
54 }
55
56 fn on_response(&mut self, _link_id: rns_net::LinkId, _request_id: [u8; 16], data: Vec<u8>) {
57 *self.response_data.lock().unwrap() = Some(data);
58 let _ = self.response_tx.send(());
59 }
60}
61
62pub fn remote_query(
72 dest_hash: [u8; 16],
73 dest_sig_pub: [u8; 32],
74 identity_prv_key: [u8; 64],
75 path: &str,
76 data: &[u8],
77 config_path: Option<&Path>,
78 timeout: Duration,
79) -> Option<RemoteQueryResult> {
80 let (link_tx, link_rx) = mpsc::channel();
81 let (resp_tx, resp_rx) = mpsc::channel();
82 let response_data = Arc::new(Mutex::new(None));
83
84 let callbacks = RemoteCallbacks {
85 link_established_tx: link_tx,
86 response_data: response_data.clone(),
87 response_tx: resp_tx,
88 };
89
90 let config_dir = rns_net::storage::resolve_config_dir(config_path);
92 let config_file = config_dir.join("config");
93 let rns_config = if config_file.exists() {
94 rns_net::config::parse_file(&config_file).ok()?
95 } else {
96 rns_net::config::parse("").ok()?
97 };
98
99 let shared_config = SharedClientConfig {
100 instance_name: rns_config.reticulum.instance_name.clone(),
101 port: rns_config.reticulum.shared_instance_port,
102 rpc_port: rns_config.reticulum.instance_control_port,
103 };
104
105 let node = RnsNode::connect_shared(shared_config, Box::new(callbacks)).ok()?;
106
107 std::thread::sleep(Duration::from_millis(500));
109
110 let link_id = node.create_link(dest_hash, dest_sig_pub).ok()?;
112
113 let _established_link_id = link_rx.recv_timeout(timeout).ok()?;
115
116 node.identify_on_link(link_id, identity_prv_key).ok()?;
118 std::thread::sleep(Duration::from_millis(200));
119
120 node.send_request(link_id, path, data).ok()?;
122
123 resp_rx.recv_timeout(timeout).ok()?;
125
126 let data = response_data.lock().unwrap().take()?;
127 node.shutdown();
128
129 Some(RemoteQueryResult { data })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn parse_hex_hash_valid() {
138 let hash = parse_hex_hash("0123456789abcdef0123456789abcdef").unwrap();
139 assert_eq!(hash[0], 0x01);
140 assert_eq!(hash[15], 0xef);
141 }
142
143 #[test]
144 fn parse_hex_hash_invalid() {
145 assert!(parse_hex_hash("short").is_none());
146 assert!(parse_hex_hash("0123456789abcdef0123456789abcdef00").is_none());
147 assert!(parse_hex_hash("xyz3456789abcdef0123456789abcdef").is_none());
148 }
149
150 #[test]
151 fn parse_hex_hash_trimmed() {
152 let hash = parse_hex_hash(" 0123456789abcdef0123456789abcdef ").unwrap();
153 assert_eq!(hash[0], 0x01);
154 }
155}