1use serde_json::{json, Value};
2
3use rns_net::{
4 Destination, QueryRequest, QueryResponse, RnsNode,
5 DestHash, IdentityHash, ProofStrategy,
6};
7use rns_crypto::identity::Identity;
8
9use crate::auth::check_auth;
10use crate::config::CtlConfig;
11use crate::encode::{from_base64, hex_to_array, to_base64, to_hex};
12use crate::http::{parse_query, HttpRequest, HttpResponse};
13use crate::state::{DestinationEntry, SharedState};
14
15pub type NodeHandle = std::sync::Arc<std::sync::Mutex<Option<RnsNode>>>;
17
18fn with_node<F>(node: &NodeHandle, f: F) -> HttpResponse
20where
21 F: FnOnce(&RnsNode) -> HttpResponse,
22{
23 let guard = node.lock().unwrap();
24 match guard.as_ref() {
25 Some(n) => f(n),
26 None => HttpResponse::internal_error("Node is shutting down"),
27 }
28}
29
30pub fn handle_request(
32 req: &HttpRequest,
33 node: &NodeHandle,
34 state: &SharedState,
35 config: &CtlConfig,
36) -> HttpResponse {
37 if req.method == "GET" && req.path == "/health" {
39 return HttpResponse::ok(json!({"status": "healthy"}));
40 }
41
42 if let Err(resp) = check_auth(req, config) {
44 return resp;
45 }
46
47 match (req.method.as_str(), req.path.as_str()) {
48 ("GET", "/api/info") => handle_info(node, state),
50 ("GET", "/api/interfaces") => handle_interfaces(node),
51 ("GET", "/api/destinations") => handle_destinations(node, state),
52 ("GET", "/api/paths") => handle_paths(req, node),
53 ("GET", "/api/links") => handle_links(node),
54 ("GET", "/api/resources") => handle_resources(node),
55 ("GET", "/api/announces") => handle_event_list(req, state, "announces"),
56 ("GET", "/api/packets") => handle_event_list(req, state, "packets"),
57 ("GET", "/api/proofs") => handle_event_list(req, state, "proofs"),
58
59 ("GET", path) if path.starts_with("/api/identity/") => {
61 let hash_str = &path["/api/identity/".len()..];
62 handle_recall_identity(hash_str, node)
63 }
64
65 ("POST", "/api/destination") => handle_post_destination(req, node, state),
67 ("POST", "/api/announce") => handle_post_announce(req, node, state),
68 ("POST", "/api/send") => handle_post_send(req, node, state),
69 ("POST", "/api/link") => handle_post_link(req, node),
70 ("POST", "/api/link/send") => handle_post_link_send(req, node),
71 ("POST", "/api/link/close") => handle_post_link_close(req, node),
72 ("POST", "/api/channel") => handle_post_channel(req, node),
73 ("POST", "/api/resource") => handle_post_resource(req, node),
74 ("POST", "/api/path/request") => handle_post_path_request(req, node),
75
76 _ => HttpResponse::not_found(),
77 }
78}
79
80fn handle_info(node: &NodeHandle, state: &SharedState) -> HttpResponse {
83 with_node(node, |n| {
84 let transport_id = match n.query(QueryRequest::TransportIdentity) {
85 Ok(QueryResponse::TransportIdentity(id)) => id,
86 _ => None,
87 };
88 let s = state.read().unwrap();
89 HttpResponse::ok(json!({
90 "transport_id": transport_id.map(|h| to_hex(&h)),
91 "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
92 "uptime_seconds": s.uptime_seconds(),
93 }))
94 })
95}
96
97fn handle_interfaces(node: &NodeHandle) -> HttpResponse {
98 with_node(node, |n| {
99 match n.query(QueryRequest::InterfaceStats) {
100 Ok(QueryResponse::InterfaceStats(stats)) => {
101 let ifaces: Vec<Value> = stats
102 .interfaces
103 .iter()
104 .map(|i| {
105 json!({
106 "name": i.name,
107 "status": if i.status { "up" } else { "down" },
108 "mode": i.mode,
109 "interface_type": i.interface_type,
110 "rxb": i.rxb,
111 "txb": i.txb,
112 "rx_packets": i.rx_packets,
113 "tx_packets": i.tx_packets,
114 "bitrate": i.bitrate,
115 "started": i.started,
116 "ia_freq": i.ia_freq,
117 "oa_freq": i.oa_freq,
118 })
119 })
120 .collect();
121 HttpResponse::ok(json!({
122 "interfaces": ifaces,
123 "transport_enabled": stats.transport_enabled,
124 "transport_uptime": stats.transport_uptime,
125 "total_rxb": stats.total_rxb,
126 "total_txb": stats.total_txb,
127 }))
128 }
129 _ => HttpResponse::internal_error("Query failed"),
130 }
131 })
132}
133
134fn handle_destinations(node: &NodeHandle, state: &SharedState) -> HttpResponse {
135 with_node(node, |n| {
136 match n.query(QueryRequest::LocalDestinations) {
137 Ok(QueryResponse::LocalDestinations(dests)) => {
138 let s = state.read().unwrap();
139 let list: Vec<Value> = dests
140 .iter()
141 .map(|d| {
142 let name = s
143 .destinations
144 .get(&d.hash)
145 .map(|e| e.full_name.as_str())
146 .unwrap_or("");
147 json!({
148 "hash": to_hex(&d.hash),
149 "type": d.dest_type,
150 "name": name,
151 })
152 })
153 .collect();
154 HttpResponse::ok(json!({"destinations": list}))
155 }
156 _ => HttpResponse::internal_error("Query failed"),
157 }
158 })
159}
160
161fn handle_paths(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
162 let params = parse_query(&req.query);
163 let filter_hash: Option<[u8; 16]> = params
164 .get("dest_hash")
165 .and_then(|s| hex_to_array(s));
166
167 with_node(node, |n| {
168 match n.query(QueryRequest::PathTable { max_hops: None }) {
169 Ok(QueryResponse::PathTable(paths)) => {
170 let list: Vec<Value> = paths
171 .iter()
172 .filter(|p| filter_hash.map_or(true, |h| p.hash == h))
173 .map(|p| {
174 json!({
175 "hash": to_hex(&p.hash),
176 "via": to_hex(&p.via),
177 "hops": p.hops,
178 "expires": p.expires,
179 "interface": p.interface_name,
180 "timestamp": p.timestamp,
181 })
182 })
183 .collect();
184 HttpResponse::ok(json!({"paths": list}))
185 }
186 _ => HttpResponse::internal_error("Query failed"),
187 }
188 })
189}
190
191fn handle_links(node: &NodeHandle) -> HttpResponse {
192 with_node(node, |n| {
193 match n.query(QueryRequest::Links) {
194 Ok(QueryResponse::Links(links)) => {
195 let list: Vec<Value> = links
196 .iter()
197 .map(|l| {
198 json!({
199 "link_id": to_hex(&l.link_id),
200 "state": l.state,
201 "is_initiator": l.is_initiator,
202 "dest_hash": to_hex(&l.dest_hash),
203 "remote_identity": l.remote_identity.map(|h| to_hex(&h)),
204 "rtt": l.rtt,
205 })
206 })
207 .collect();
208 HttpResponse::ok(json!({"links": list}))
209 }
210 _ => HttpResponse::internal_error("Query failed"),
211 }
212 })
213}
214
215fn handle_resources(node: &NodeHandle) -> HttpResponse {
216 with_node(node, |n| {
217 match n.query(QueryRequest::Resources) {
218 Ok(QueryResponse::Resources(resources)) => {
219 let list: Vec<Value> = resources
220 .iter()
221 .map(|r| {
222 json!({
223 "link_id": to_hex(&r.link_id),
224 "direction": r.direction,
225 "total_parts": r.total_parts,
226 "transferred_parts": r.transferred_parts,
227 "complete": r.complete,
228 })
229 })
230 .collect();
231 HttpResponse::ok(json!({"resources": list}))
232 }
233 _ => HttpResponse::internal_error("Query failed"),
234 }
235 })
236}
237
238fn handle_event_list(req: &HttpRequest, state: &SharedState, kind: &str) -> HttpResponse {
239 let params = parse_query(&req.query);
240 let clear = params.get("clear").map_or(false, |v| v == "true");
241
242 let mut s = state.write().unwrap();
243 let items: Vec<Value> = match kind {
244 "announces" => {
245 let v: Vec<Value> = s
246 .announces
247 .iter()
248 .map(|r| serde_json::to_value(r).unwrap_or_default())
249 .collect();
250 if clear {
251 s.announces.clear();
252 }
253 v
254 }
255 "packets" => {
256 let v: Vec<Value> = s
257 .packets
258 .iter()
259 .map(|r| serde_json::to_value(r).unwrap_or_default())
260 .collect();
261 if clear {
262 s.packets.clear();
263 }
264 v
265 }
266 "proofs" => {
267 let v: Vec<Value> = s
268 .proofs
269 .iter()
270 .map(|r| serde_json::to_value(r).unwrap_or_default())
271 .collect();
272 if clear {
273 s.proofs.clear();
274 }
275 v
276 }
277 _ => Vec::new(),
278 };
279
280 let mut obj = serde_json::Map::new();
281 obj.insert(kind.to_string(), Value::Array(items));
282 HttpResponse::ok(Value::Object(obj))
283}
284
285fn handle_recall_identity(hash_str: &str, node: &NodeHandle) -> HttpResponse {
286 let dest_hash: [u8; 16] = match hex_to_array(hash_str) {
287 Some(h) => h,
288 None => return HttpResponse::bad_request("Invalid dest_hash hex (expected 32 hex chars)"),
289 };
290
291 with_node(node, |n| {
292 match n.recall_identity(&DestHash(dest_hash)) {
293 Ok(Some(ai)) => HttpResponse::ok(json!({
294 "dest_hash": to_hex(&ai.dest_hash.0),
295 "identity_hash": to_hex(&ai.identity_hash.0),
296 "public_key": to_hex(&ai.public_key),
297 "app_data": ai.app_data.as_ref().map(|d| to_base64(d)),
298 "hops": ai.hops,
299 "received_at": ai.received_at,
300 })),
301 Ok(None) => HttpResponse::not_found(),
302 Err(_) => HttpResponse::internal_error("Query failed"),
303 }
304 })
305}
306
307fn parse_json_body(req: &HttpRequest) -> Result<Value, HttpResponse> {
310 serde_json::from_slice(&req.body).map_err(|e| HttpResponse::bad_request(&format!("Invalid JSON: {}", e)))
311}
312
313fn handle_post_destination(
314 req: &HttpRequest,
315 node: &NodeHandle,
316 state: &SharedState,
317) -> HttpResponse {
318 let body = match parse_json_body(req) {
319 Ok(v) => v,
320 Err(r) => return r,
321 };
322
323 let dest_type_str = body["type"].as_str().unwrap_or("");
324 let app_name = match body["app_name"].as_str() {
325 Some(s) => s,
326 None => return HttpResponse::bad_request("Missing app_name"),
327 };
328 let aspects: Vec<&str> = body["aspects"]
329 .as_array()
330 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
331 .unwrap_or_default();
332
333 let (identity_hash, identity_prv_key) = {
334 let s = state.read().unwrap();
335 let ih = s.identity_hash;
336 let prv = s.identity.as_ref().and_then(|i| i.get_private_key());
337 (ih, prv)
338 };
339
340 let (dest, signing_key) = match dest_type_str {
341 "single" => {
342 let direction = body["direction"].as_str().unwrap_or("in");
343 match direction {
344 "in" => {
345 let ih = match identity_hash {
346 Some(h) => IdentityHash(h),
347 None => return HttpResponse::internal_error("No identity loaded"),
348 };
349 let dest = Destination::single_in(app_name, &aspects, ih)
350 .set_proof_strategy(parse_proof_strategy(&body));
351 (dest, identity_prv_key)
352 }
353 "out" => {
354 let dh_str = match body["dest_hash"].as_str() {
355 Some(s) => s,
356 None => return HttpResponse::bad_request("OUT single requires dest_hash of remote"),
357 };
358 let dh: [u8; 16] = match hex_to_array(dh_str) {
359 Some(h) => h,
360 None => return HttpResponse::bad_request("Invalid dest_hash"),
361 };
362 return with_node(node, |n| {
363 match n.recall_identity(&DestHash(dh)) {
364 Ok(Some(recalled)) => {
365 let dest = Destination::single_out(app_name, &aspects, &recalled);
366 let full_name = format_dest_name(app_name, &aspects);
368 let mut s = state.write().unwrap();
369 s.destinations.insert(dest.hash.0, DestinationEntry {
370 destination: dest.clone(),
371 full_name: full_name.clone(),
372 });
373 HttpResponse::created(json!({
374 "dest_hash": to_hex(&dest.hash.0),
375 "name": full_name,
376 "type": "single",
377 "direction": "out",
378 }))
379 }
380 Ok(None) => HttpResponse::bad_request("No recalled identity for dest_hash"),
381 Err(_) => HttpResponse::internal_error("Query failed"),
382 }
383 });
384 }
385 _ => return HttpResponse::bad_request("direction must be 'in' or 'out'"),
386 }
387 }
388 "plain" => {
389 let dest = Destination::plain(app_name, &aspects)
390 .set_proof_strategy(parse_proof_strategy(&body));
391 (dest, None)
392 }
393 "group" => {
394 let mut dest = Destination::group(app_name, &aspects)
395 .set_proof_strategy(parse_proof_strategy(&body));
396 if let Some(key_b64) = body["group_key"].as_str() {
397 match from_base64(key_b64) {
398 Some(key) => {
399 if let Err(e) = dest.load_private_key(key) {
400 return HttpResponse::bad_request(&format!("Invalid group key: {}", e));
401 }
402 }
403 None => return HttpResponse::bad_request("Invalid base64 group_key"),
404 }
405 } else {
406 dest.create_keys();
407 }
408 (dest, None)
409 }
410 _ => return HttpResponse::bad_request("type must be 'single', 'plain', or 'group'"),
411 };
412
413 with_node(node, |n| {
414 match n.register_destination_with_proof(&dest, signing_key) {
415 Ok(()) => {
416 let full_name = format_dest_name(app_name, &aspects);
417 let hash_hex = to_hex(&dest.hash.0);
418 let group_key_b64 = dest.get_private_key().map(to_base64);
419 let mut s = state.write().unwrap();
420 s.destinations.insert(
421 dest.hash.0,
422 DestinationEntry {
423 destination: dest,
424 full_name: full_name.clone(),
425 },
426 );
427 let mut resp = json!({
428 "dest_hash": hash_hex,
429 "name": full_name,
430 "type": dest_type_str,
431 });
432 if let Some(gk) = group_key_b64 {
433 resp["group_key"] = Value::String(gk);
434 }
435 HttpResponse::created(resp)
436 }
437 Err(_) => HttpResponse::internal_error("Failed to register destination"),
438 }
439 })
440}
441
442fn handle_post_announce(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
443 let body = match parse_json_body(req) {
444 Ok(v) => v,
445 Err(r) => return r,
446 };
447
448 let dh_str = match body["dest_hash"].as_str() {
449 Some(s) => s,
450 None => return HttpResponse::bad_request("Missing dest_hash"),
451 };
452 let dh: [u8; 16] = match hex_to_array(dh_str) {
453 Some(h) => h,
454 None => return HttpResponse::bad_request("Invalid dest_hash"),
455 };
456
457 let app_data: Option<Vec<u8>> = body["app_data"]
458 .as_str()
459 .and_then(from_base64);
460
461 let (dest, identity) = {
462 let s = state.read().unwrap();
463 let dest = match s.destinations.get(&dh) {
464 Some(entry) => entry.destination.clone(),
465 None => return HttpResponse::bad_request("Destination not registered via API"),
466 };
467 let identity = match s.identity.as_ref().and_then(|i| i.get_private_key()) {
468 Some(prv) => Identity::from_private_key(&prv),
469 None => return HttpResponse::internal_error("No identity loaded"),
470 };
471 (dest, identity)
472 };
473
474 with_node(node, |n| {
475 match n.announce(&dest, &identity, app_data.as_deref()) {
476 Ok(()) => HttpResponse::ok(json!({"status": "announced", "dest_hash": dh_str})),
477 Err(_) => HttpResponse::internal_error("Announce failed"),
478 }
479 })
480}
481
482fn handle_post_send(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
483 let body = match parse_json_body(req) {
484 Ok(v) => v,
485 Err(r) => return r,
486 };
487
488 let dh_str = match body["dest_hash"].as_str() {
489 Some(s) => s,
490 None => return HttpResponse::bad_request("Missing dest_hash"),
491 };
492 let dh: [u8; 16] = match hex_to_array(dh_str) {
493 Some(h) => h,
494 None => return HttpResponse::bad_request("Invalid dest_hash"),
495 };
496 let data = match body["data"].as_str().and_then(from_base64) {
497 Some(d) => d,
498 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
499 };
500
501 let s = state.read().unwrap();
502 let dest = match s.destinations.get(&dh) {
503 Some(entry) => entry.destination.clone(),
504 None => return HttpResponse::bad_request("Destination not registered via API"),
505 };
506 drop(s);
507
508 with_node(node, |n| {
509 match n.send_packet(&dest, &data) {
510 Ok(ph) => HttpResponse::ok(json!({
511 "status": "sent",
512 "packet_hash": to_hex(&ph.0),
513 })),
514 Err(_) => HttpResponse::internal_error("Send failed"),
515 }
516 })
517}
518
519fn handle_post_link(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
520 let body = match parse_json_body(req) {
521 Ok(v) => v,
522 Err(r) => return r,
523 };
524
525 let dh_str = match body["dest_hash"].as_str() {
526 Some(s) => s,
527 None => return HttpResponse::bad_request("Missing dest_hash"),
528 };
529 let dh: [u8; 16] = match hex_to_array(dh_str) {
530 Some(h) => h,
531 None => return HttpResponse::bad_request("Invalid dest_hash"),
532 };
533
534 with_node(node, |n| {
535 let recalled = match n.recall_identity(&DestHash(dh)) {
537 Ok(Some(ai)) => ai,
538 Ok(None) => return HttpResponse::bad_request("No recalled identity for dest_hash"),
539 Err(_) => return HttpResponse::internal_error("Query failed"),
540 };
541 let mut sig_pub = [0u8; 32];
543 sig_pub.copy_from_slice(&recalled.public_key[32..64]);
544
545 match n.create_link(dh, sig_pub) {
546 Ok(link_id) => HttpResponse::created(json!({
547 "link_id": to_hex(&link_id),
548 })),
549 Err(_) => HttpResponse::internal_error("Create link failed"),
550 }
551 })
552}
553
554fn handle_post_link_send(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
555 let body = match parse_json_body(req) {
556 Ok(v) => v,
557 Err(r) => return r,
558 };
559
560 let link_id: [u8; 16] = match body["link_id"]
561 .as_str()
562 .and_then(|s| hex_to_array(s))
563 {
564 Some(h) => h,
565 None => return HttpResponse::bad_request("Missing or invalid link_id"),
566 };
567 let data = match body["data"].as_str().and_then(from_base64) {
568 Some(d) => d,
569 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
570 };
571 let context = body["context"].as_u64().unwrap_or(0) as u8;
572
573 with_node(node, |n| {
574 match n.send_on_link(link_id, data, context) {
575 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
576 Err(_) => HttpResponse::internal_error("Send on link failed"),
577 }
578 })
579}
580
581fn handle_post_link_close(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
582 let body = match parse_json_body(req) {
583 Ok(v) => v,
584 Err(r) => return r,
585 };
586
587 let link_id: [u8; 16] = match body["link_id"]
588 .as_str()
589 .and_then(|s| hex_to_array(s))
590 {
591 Some(h) => h,
592 None => return HttpResponse::bad_request("Missing or invalid link_id"),
593 };
594
595 with_node(node, |n| {
596 match n.teardown_link(link_id) {
597 Ok(()) => HttpResponse::ok(json!({"status": "closed"})),
598 Err(_) => HttpResponse::internal_error("Teardown link failed"),
599 }
600 })
601}
602
603fn handle_post_channel(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
604 let body = match parse_json_body(req) {
605 Ok(v) => v,
606 Err(r) => return r,
607 };
608
609 let link_id: [u8; 16] = match body["link_id"]
610 .as_str()
611 .and_then(|s| hex_to_array(s))
612 {
613 Some(h) => h,
614 None => return HttpResponse::bad_request("Missing or invalid link_id"),
615 };
616 let msgtype = body["msgtype"].as_u64().unwrap_or(0) as u16;
617 let payload = match body["payload"].as_str().and_then(from_base64) {
618 Some(d) => d,
619 None => return HttpResponse::bad_request("Missing or invalid base64 payload"),
620 };
621
622 with_node(node, |n| {
623 match n.send_channel_message(link_id, msgtype, payload) {
624 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
625 Err(_) => HttpResponse::internal_error("Channel message failed"),
626 }
627 })
628}
629
630fn handle_post_resource(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
631 let body = match parse_json_body(req) {
632 Ok(v) => v,
633 Err(r) => return r,
634 };
635
636 let link_id: [u8; 16] = match body["link_id"]
637 .as_str()
638 .and_then(|s| hex_to_array(s))
639 {
640 Some(h) => h,
641 None => return HttpResponse::bad_request("Missing or invalid link_id"),
642 };
643 let data = match body["data"].as_str().and_then(from_base64) {
644 Some(d) => d,
645 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
646 };
647 let metadata = body["metadata"]
648 .as_str()
649 .and_then(from_base64);
650
651 with_node(node, |n| {
652 match n.send_resource(link_id, data, metadata) {
653 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
654 Err(_) => HttpResponse::internal_error("Resource send failed"),
655 }
656 })
657}
658
659fn handle_post_path_request(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
660 let body = match parse_json_body(req) {
661 Ok(v) => v,
662 Err(r) => return r,
663 };
664
665 let dh_str = match body["dest_hash"].as_str() {
666 Some(s) => s,
667 None => return HttpResponse::bad_request("Missing dest_hash"),
668 };
669 let dh: [u8; 16] = match hex_to_array(dh_str) {
670 Some(h) => h,
671 None => return HttpResponse::bad_request("Invalid dest_hash"),
672 };
673
674 with_node(node, |n| {
675 match n.request_path(&DestHash(dh)) {
676 Ok(()) => HttpResponse::ok(json!({"status": "requested"})),
677 Err(_) => HttpResponse::internal_error("Path request failed"),
678 }
679 })
680}
681
682fn format_dest_name(app_name: &str, aspects: &[&str]) -> String {
685 if aspects.is_empty() {
686 app_name.to_string()
687 } else {
688 format!("{}.{}", app_name, aspects.join("."))
689 }
690}
691
692fn parse_proof_strategy(body: &Value) -> ProofStrategy {
693 match body["proof_strategy"].as_str() {
694 Some("all") => ProofStrategy::ProveAll,
695 Some("app") => ProofStrategy::ProveApp,
696 _ => ProofStrategy::ProveNone,
697 }
698}