1use 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
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(
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
74pub 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 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 std::thread::sleep(Duration::from_millis(500));
121
122 let link_id = node.create_link(dest_hash, dest_sig_pub).ok()?;
124
125 let _established_link_id = link_rx.recv_timeout(timeout).ok()?;
127
128 node.identify_on_link(link_id, identity_prv_key).ok()?;
130 std::thread::sleep(Duration::from_millis(200));
131
132 node.send_request(link_id, path, data).ok()?;
134
135 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}