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 ("GET", "/api/link_events") => handle_event_list(req, state, "link_events"),
59 ("GET", "/api/resource_events") => handle_event_list(req, state, "resource_events"),
60
61 ("GET", path) if path.starts_with("/api/identity/") => {
63 let hash_str = &path["/api/identity/".len()..];
64 handle_recall_identity(hash_str, node)
65 }
66
67 ("POST", "/api/destination") => handle_post_destination(req, node, state),
69 ("POST", "/api/announce") => handle_post_announce(req, node, state),
70 ("POST", "/api/send") => handle_post_send(req, node, state),
71 ("POST", "/api/link") => handle_post_link(req, node),
72 ("POST", "/api/link/send") => handle_post_link_send(req, node),
73 ("POST", "/api/link/close") => handle_post_link_close(req, node),
74 ("POST", "/api/channel") => handle_post_channel(req, node),
75 ("POST", "/api/resource") => handle_post_resource(req, node),
76 ("POST", "/api/path/request") => handle_post_path_request(req, node),
77 ("POST", "/api/direct_connect") => handle_post_direct_connect(req, node),
78
79 ("GET", "/api/hooks") => handle_list_hooks(node),
81 ("POST", "/api/hook/load") => handle_load_hook(req, node),
82 ("POST", "/api/hook/unload") => handle_unload_hook(req, node),
83 ("POST", "/api/hook/reload") => handle_reload_hook(req, node),
84
85 _ => HttpResponse::not_found(),
86 }
87}
88
89fn handle_info(node: &NodeHandle, state: &SharedState) -> HttpResponse {
92 with_node(node, |n| {
93 let transport_id = match n.query(QueryRequest::TransportIdentity) {
94 Ok(QueryResponse::TransportIdentity(id)) => id,
95 _ => None,
96 };
97 let s = state.read().unwrap();
98 HttpResponse::ok(json!({
99 "transport_id": transport_id.map(|h| to_hex(&h)),
100 "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
101 "uptime_seconds": s.uptime_seconds(),
102 }))
103 })
104}
105
106fn handle_interfaces(node: &NodeHandle) -> HttpResponse {
107 with_node(node, |n| {
108 match n.query(QueryRequest::InterfaceStats) {
109 Ok(QueryResponse::InterfaceStats(stats)) => {
110 let ifaces: Vec<Value> = stats
111 .interfaces
112 .iter()
113 .map(|i| {
114 json!({
115 "name": i.name,
116 "status": if i.status { "up" } else { "down" },
117 "mode": i.mode,
118 "interface_type": i.interface_type,
119 "rxb": i.rxb,
120 "txb": i.txb,
121 "rx_packets": i.rx_packets,
122 "tx_packets": i.tx_packets,
123 "bitrate": i.bitrate,
124 "started": i.started,
125 "ia_freq": i.ia_freq,
126 "oa_freq": i.oa_freq,
127 })
128 })
129 .collect();
130 HttpResponse::ok(json!({
131 "interfaces": ifaces,
132 "transport_enabled": stats.transport_enabled,
133 "transport_uptime": stats.transport_uptime,
134 "total_rxb": stats.total_rxb,
135 "total_txb": stats.total_txb,
136 }))
137 }
138 _ => HttpResponse::internal_error("Query failed"),
139 }
140 })
141}
142
143fn handle_destinations(node: &NodeHandle, state: &SharedState) -> HttpResponse {
144 with_node(node, |n| {
145 match n.query(QueryRequest::LocalDestinations) {
146 Ok(QueryResponse::LocalDestinations(dests)) => {
147 let s = state.read().unwrap();
148 let list: Vec<Value> = dests
149 .iter()
150 .map(|d| {
151 let name = s
152 .destinations
153 .get(&d.hash)
154 .map(|e| e.full_name.as_str())
155 .unwrap_or("");
156 json!({
157 "hash": to_hex(&d.hash),
158 "type": d.dest_type,
159 "name": name,
160 })
161 })
162 .collect();
163 HttpResponse::ok(json!({"destinations": list}))
164 }
165 _ => HttpResponse::internal_error("Query failed"),
166 }
167 })
168}
169
170fn handle_paths(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
171 let params = parse_query(&req.query);
172 let filter_hash: Option<[u8; 16]> = params
173 .get("dest_hash")
174 .and_then(|s| hex_to_array(s));
175
176 with_node(node, |n| {
177 match n.query(QueryRequest::PathTable { max_hops: None }) {
178 Ok(QueryResponse::PathTable(paths)) => {
179 let list: Vec<Value> = paths
180 .iter()
181 .filter(|p| filter_hash.map_or(true, |h| p.hash == h))
182 .map(|p| {
183 json!({
184 "hash": to_hex(&p.hash),
185 "via": to_hex(&p.via),
186 "hops": p.hops,
187 "expires": p.expires,
188 "interface": p.interface_name,
189 "timestamp": p.timestamp,
190 })
191 })
192 .collect();
193 HttpResponse::ok(json!({"paths": list}))
194 }
195 _ => HttpResponse::internal_error("Query failed"),
196 }
197 })
198}
199
200fn handle_links(node: &NodeHandle) -> HttpResponse {
201 with_node(node, |n| {
202 match n.query(QueryRequest::Links) {
203 Ok(QueryResponse::Links(links)) => {
204 let list: Vec<Value> = links
205 .iter()
206 .map(|l| {
207 json!({
208 "link_id": to_hex(&l.link_id),
209 "state": l.state,
210 "is_initiator": l.is_initiator,
211 "dest_hash": to_hex(&l.dest_hash),
212 "remote_identity": l.remote_identity.map(|h| to_hex(&h)),
213 "rtt": l.rtt,
214 })
215 })
216 .collect();
217 HttpResponse::ok(json!({"links": list}))
218 }
219 _ => HttpResponse::internal_error("Query failed"),
220 }
221 })
222}
223
224fn handle_resources(node: &NodeHandle) -> HttpResponse {
225 with_node(node, |n| {
226 match n.query(QueryRequest::Resources) {
227 Ok(QueryResponse::Resources(resources)) => {
228 let list: Vec<Value> = resources
229 .iter()
230 .map(|r| {
231 json!({
232 "link_id": to_hex(&r.link_id),
233 "direction": r.direction,
234 "total_parts": r.total_parts,
235 "transferred_parts": r.transferred_parts,
236 "complete": r.complete,
237 })
238 })
239 .collect();
240 HttpResponse::ok(json!({"resources": list}))
241 }
242 _ => HttpResponse::internal_error("Query failed"),
243 }
244 })
245}
246
247fn handle_event_list(req: &HttpRequest, state: &SharedState, kind: &str) -> HttpResponse {
248 let params = parse_query(&req.query);
249 let clear = params.get("clear").map_or(false, |v| v == "true");
250
251 let mut s = state.write().unwrap();
252 let items: Vec<Value> = match kind {
253 "announces" => {
254 let v: Vec<Value> = s
255 .announces
256 .iter()
257 .map(|r| serde_json::to_value(r).unwrap_or_default())
258 .collect();
259 if clear {
260 s.announces.clear();
261 }
262 v
263 }
264 "packets" => {
265 let v: Vec<Value> = s
266 .packets
267 .iter()
268 .map(|r| serde_json::to_value(r).unwrap_or_default())
269 .collect();
270 if clear {
271 s.packets.clear();
272 }
273 v
274 }
275 "proofs" => {
276 let v: Vec<Value> = s
277 .proofs
278 .iter()
279 .map(|r| serde_json::to_value(r).unwrap_or_default())
280 .collect();
281 if clear {
282 s.proofs.clear();
283 }
284 v
285 }
286 "link_events" => {
287 let v: Vec<Value> = s
288 .link_events
289 .iter()
290 .map(|r| serde_json::to_value(r).unwrap_or_default())
291 .collect();
292 if clear {
293 s.link_events.clear();
294 }
295 v
296 }
297 "resource_events" => {
298 let v: Vec<Value> = s
299 .resource_events
300 .iter()
301 .map(|r| serde_json::to_value(r).unwrap_or_default())
302 .collect();
303 if clear {
304 s.resource_events.clear();
305 }
306 v
307 }
308 _ => Vec::new(),
309 };
310
311 let mut obj = serde_json::Map::new();
312 obj.insert(kind.to_string(), Value::Array(items));
313 HttpResponse::ok(Value::Object(obj))
314}
315
316fn handle_recall_identity(hash_str: &str, node: &NodeHandle) -> HttpResponse {
317 let dest_hash: [u8; 16] = match hex_to_array(hash_str) {
318 Some(h) => h,
319 None => return HttpResponse::bad_request("Invalid dest_hash hex (expected 32 hex chars)"),
320 };
321
322 with_node(node, |n| {
323 match n.recall_identity(&DestHash(dest_hash)) {
324 Ok(Some(ai)) => HttpResponse::ok(json!({
325 "dest_hash": to_hex(&ai.dest_hash.0),
326 "identity_hash": to_hex(&ai.identity_hash.0),
327 "public_key": to_hex(&ai.public_key),
328 "app_data": ai.app_data.as_ref().map(|d| to_base64(d)),
329 "hops": ai.hops,
330 "received_at": ai.received_at,
331 })),
332 Ok(None) => HttpResponse::not_found(),
333 Err(_) => HttpResponse::internal_error("Query failed"),
334 }
335 })
336}
337
338fn parse_json_body(req: &HttpRequest) -> Result<Value, HttpResponse> {
341 serde_json::from_slice(&req.body).map_err(|e| HttpResponse::bad_request(&format!("Invalid JSON: {}", e)))
342}
343
344fn handle_post_destination(
345 req: &HttpRequest,
346 node: &NodeHandle,
347 state: &SharedState,
348) -> HttpResponse {
349 let body = match parse_json_body(req) {
350 Ok(v) => v,
351 Err(r) => return r,
352 };
353
354 let dest_type_str = body["type"].as_str().unwrap_or("");
355 let app_name = match body["app_name"].as_str() {
356 Some(s) => s,
357 None => return HttpResponse::bad_request("Missing app_name"),
358 };
359 let aspects: Vec<&str> = body["aspects"]
360 .as_array()
361 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
362 .unwrap_or_default();
363
364 let (identity_hash, identity_prv_key, identity_pub_key) = {
365 let s = state.read().unwrap();
366 let ih = s.identity_hash;
367 let prv = s.identity.as_ref().and_then(|i| i.get_private_key());
368 let pubk = s.identity.as_ref().and_then(|i| i.get_public_key());
369 (ih, prv, pubk)
370 };
371
372 let (dest, signing_key) = match dest_type_str {
373 "single" => {
374 let direction = body["direction"].as_str().unwrap_or("in");
375 match direction {
376 "in" => {
377 let ih = match identity_hash {
378 Some(h) => IdentityHash(h),
379 None => return HttpResponse::internal_error("No identity loaded"),
380 };
381 let dest = Destination::single_in(app_name, &aspects, ih)
382 .set_proof_strategy(parse_proof_strategy(&body));
383 (dest, identity_prv_key)
384 }
385 "out" => {
386 let dh_str = match body["dest_hash"].as_str() {
387 Some(s) => s,
388 None => return HttpResponse::bad_request("OUT single requires dest_hash of remote"),
389 };
390 let dh: [u8; 16] = match hex_to_array(dh_str) {
391 Some(h) => h,
392 None => return HttpResponse::bad_request("Invalid dest_hash"),
393 };
394 return with_node(node, |n| {
395 match n.recall_identity(&DestHash(dh)) {
396 Ok(Some(recalled)) => {
397 let dest = Destination::single_out(app_name, &aspects, &recalled);
398 let full_name = format_dest_name(app_name, &aspects);
400 let mut s = state.write().unwrap();
401 s.destinations.insert(dest.hash.0, DestinationEntry {
402 destination: dest.clone(),
403 full_name: full_name.clone(),
404 });
405 HttpResponse::created(json!({
406 "dest_hash": to_hex(&dest.hash.0),
407 "name": full_name,
408 "type": "single",
409 "direction": "out",
410 }))
411 }
412 Ok(None) => HttpResponse::bad_request("No recalled identity for dest_hash"),
413 Err(_) => HttpResponse::internal_error("Query failed"),
414 }
415 });
416 }
417 _ => return HttpResponse::bad_request("direction must be 'in' or 'out'"),
418 }
419 }
420 "plain" => {
421 let dest = Destination::plain(app_name, &aspects)
422 .set_proof_strategy(parse_proof_strategy(&body));
423 (dest, None)
424 }
425 "group" => {
426 let mut dest = Destination::group(app_name, &aspects)
427 .set_proof_strategy(parse_proof_strategy(&body));
428 if let Some(key_b64) = body["group_key"].as_str() {
429 match from_base64(key_b64) {
430 Some(key) => {
431 if let Err(e) = dest.load_private_key(key) {
432 return HttpResponse::bad_request(&format!("Invalid group key: {}", e));
433 }
434 }
435 None => return HttpResponse::bad_request("Invalid base64 group_key"),
436 }
437 } else {
438 dest.create_keys();
439 }
440 (dest, None)
441 }
442 _ => return HttpResponse::bad_request("type must be 'single', 'plain', or 'group'"),
443 };
444
445 with_node(node, |n| {
446 match n.register_destination_with_proof(&dest, signing_key) {
447 Ok(()) => {
448 if dest_type_str == "single"
451 && body["direction"].as_str().unwrap_or("in") == "in"
452 {
453 if let (Some(prv), Some(pubk)) = (identity_prv_key, identity_pub_key) {
454 let mut sig_prv = [0u8; 32];
455 sig_prv.copy_from_slice(&prv[32..64]);
456 let mut sig_pub = [0u8; 32];
457 sig_pub.copy_from_slice(&pubk[32..64]);
458 let _ = n.register_link_destination(dest.hash.0, sig_prv, sig_pub, 0);
459 }
460 }
461
462 let full_name = format_dest_name(app_name, &aspects);
463 let hash_hex = to_hex(&dest.hash.0);
464 let group_key_b64 = dest.get_private_key().map(to_base64);
465 let mut s = state.write().unwrap();
466 s.destinations.insert(
467 dest.hash.0,
468 DestinationEntry {
469 destination: dest,
470 full_name: full_name.clone(),
471 },
472 );
473 let mut resp = json!({
474 "dest_hash": hash_hex,
475 "name": full_name,
476 "type": dest_type_str,
477 });
478 if let Some(gk) = group_key_b64 {
479 resp["group_key"] = Value::String(gk);
480 }
481 HttpResponse::created(resp)
482 }
483 Err(_) => HttpResponse::internal_error("Failed to register destination"),
484 }
485 })
486}
487
488fn handle_post_announce(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
489 let body = match parse_json_body(req) {
490 Ok(v) => v,
491 Err(r) => return r,
492 };
493
494 let dh_str = match body["dest_hash"].as_str() {
495 Some(s) => s,
496 None => return HttpResponse::bad_request("Missing dest_hash"),
497 };
498 let dh: [u8; 16] = match hex_to_array(dh_str) {
499 Some(h) => h,
500 None => return HttpResponse::bad_request("Invalid dest_hash"),
501 };
502
503 let app_data: Option<Vec<u8>> = body["app_data"]
504 .as_str()
505 .and_then(from_base64);
506
507 let (dest, identity) = {
508 let s = state.read().unwrap();
509 let dest = match s.destinations.get(&dh) {
510 Some(entry) => entry.destination.clone(),
511 None => return HttpResponse::bad_request("Destination not registered via API"),
512 };
513 let identity = match s.identity.as_ref().and_then(|i| i.get_private_key()) {
514 Some(prv) => Identity::from_private_key(&prv),
515 None => return HttpResponse::internal_error("No identity loaded"),
516 };
517 (dest, identity)
518 };
519
520 with_node(node, |n| {
521 match n.announce(&dest, &identity, app_data.as_deref()) {
522 Ok(()) => HttpResponse::ok(json!({"status": "announced", "dest_hash": dh_str})),
523 Err(_) => HttpResponse::internal_error("Announce failed"),
524 }
525 })
526}
527
528fn handle_post_send(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
529 let body = match parse_json_body(req) {
530 Ok(v) => v,
531 Err(r) => return r,
532 };
533
534 let dh_str = match body["dest_hash"].as_str() {
535 Some(s) => s,
536 None => return HttpResponse::bad_request("Missing dest_hash"),
537 };
538 let dh: [u8; 16] = match hex_to_array(dh_str) {
539 Some(h) => h,
540 None => return HttpResponse::bad_request("Invalid dest_hash"),
541 };
542 let data = match body["data"].as_str().and_then(from_base64) {
543 Some(d) => d,
544 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
545 };
546
547 let s = state.read().unwrap();
548 let dest = match s.destinations.get(&dh) {
549 Some(entry) => entry.destination.clone(),
550 None => return HttpResponse::bad_request("Destination not registered via API"),
551 };
552 drop(s);
553
554 with_node(node, |n| {
555 match n.send_packet(&dest, &data) {
556 Ok(ph) => HttpResponse::ok(json!({
557 "status": "sent",
558 "packet_hash": to_hex(&ph.0),
559 })),
560 Err(_) => HttpResponse::internal_error("Send failed"),
561 }
562 })
563}
564
565fn handle_post_link(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
566 let body = match parse_json_body(req) {
567 Ok(v) => v,
568 Err(r) => return r,
569 };
570
571 let dh_str = match body["dest_hash"].as_str() {
572 Some(s) => s,
573 None => return HttpResponse::bad_request("Missing dest_hash"),
574 };
575 let dh: [u8; 16] = match hex_to_array(dh_str) {
576 Some(h) => h,
577 None => return HttpResponse::bad_request("Invalid dest_hash"),
578 };
579
580 with_node(node, |n| {
581 let recalled = match n.recall_identity(&DestHash(dh)) {
583 Ok(Some(ai)) => ai,
584 Ok(None) => return HttpResponse::bad_request("No recalled identity for dest_hash"),
585 Err(_) => return HttpResponse::internal_error("Query failed"),
586 };
587 let mut sig_pub = [0u8; 32];
589 sig_pub.copy_from_slice(&recalled.public_key[32..64]);
590
591 match n.create_link(dh, sig_pub) {
592 Ok(link_id) => HttpResponse::created(json!({
593 "link_id": to_hex(&link_id),
594 })),
595 Err(_) => HttpResponse::internal_error("Create link failed"),
596 }
597 })
598}
599
600fn handle_post_link_send(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
601 let body = match parse_json_body(req) {
602 Ok(v) => v,
603 Err(r) => return r,
604 };
605
606 let link_id: [u8; 16] = match body["link_id"]
607 .as_str()
608 .and_then(|s| hex_to_array(s))
609 {
610 Some(h) => h,
611 None => return HttpResponse::bad_request("Missing or invalid link_id"),
612 };
613 let data = match body["data"].as_str().and_then(from_base64) {
614 Some(d) => d,
615 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
616 };
617 let context = body["context"].as_u64().unwrap_or(0) as u8;
618
619 with_node(node, |n| {
620 match n.send_on_link(link_id, data, context) {
621 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
622 Err(_) => HttpResponse::internal_error("Send on link failed"),
623 }
624 })
625}
626
627fn handle_post_link_close(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
628 let body = match parse_json_body(req) {
629 Ok(v) => v,
630 Err(r) => return r,
631 };
632
633 let link_id: [u8; 16] = match body["link_id"]
634 .as_str()
635 .and_then(|s| hex_to_array(s))
636 {
637 Some(h) => h,
638 None => return HttpResponse::bad_request("Missing or invalid link_id"),
639 };
640
641 with_node(node, |n| {
642 match n.teardown_link(link_id) {
643 Ok(()) => HttpResponse::ok(json!({"status": "closed"})),
644 Err(_) => HttpResponse::internal_error("Teardown link failed"),
645 }
646 })
647}
648
649fn handle_post_channel(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
650 let body = match parse_json_body(req) {
651 Ok(v) => v,
652 Err(r) => return r,
653 };
654
655 let link_id: [u8; 16] = match body["link_id"]
656 .as_str()
657 .and_then(|s| hex_to_array(s))
658 {
659 Some(h) => h,
660 None => return HttpResponse::bad_request("Missing or invalid link_id"),
661 };
662 let msgtype = body["msgtype"].as_u64().unwrap_or(0) as u16;
663 let payload = match body["payload"].as_str().and_then(from_base64) {
664 Some(d) => d,
665 None => return HttpResponse::bad_request("Missing or invalid base64 payload"),
666 };
667
668 with_node(node, |n| {
669 match n.send_channel_message(link_id, msgtype, payload) {
670 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
671 Err(_) => HttpResponse::internal_error("Channel message failed"),
672 }
673 })
674}
675
676fn handle_post_resource(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
677 let body = match parse_json_body(req) {
678 Ok(v) => v,
679 Err(r) => return r,
680 };
681
682 let link_id: [u8; 16] = match body["link_id"]
683 .as_str()
684 .and_then(|s| hex_to_array(s))
685 {
686 Some(h) => h,
687 None => return HttpResponse::bad_request("Missing or invalid link_id"),
688 };
689 let data = match body["data"].as_str().and_then(from_base64) {
690 Some(d) => d,
691 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
692 };
693 let metadata = body["metadata"]
694 .as_str()
695 .and_then(from_base64);
696
697 with_node(node, |n| {
698 match n.send_resource(link_id, data, metadata) {
699 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
700 Err(_) => HttpResponse::internal_error("Resource send failed"),
701 }
702 })
703}
704
705fn handle_post_path_request(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
706 let body = match parse_json_body(req) {
707 Ok(v) => v,
708 Err(r) => return r,
709 };
710
711 let dh_str = match body["dest_hash"].as_str() {
712 Some(s) => s,
713 None => return HttpResponse::bad_request("Missing dest_hash"),
714 };
715 let dh: [u8; 16] = match hex_to_array(dh_str) {
716 Some(h) => h,
717 None => return HttpResponse::bad_request("Invalid dest_hash"),
718 };
719
720 with_node(node, |n| {
721 match n.request_path(&DestHash(dh)) {
722 Ok(()) => HttpResponse::ok(json!({"status": "requested"})),
723 Err(_) => HttpResponse::internal_error("Path request failed"),
724 }
725 })
726}
727
728fn handle_post_direct_connect(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
729 let body = match parse_json_body(req) {
730 Ok(v) => v,
731 Err(r) => return r,
732 };
733
734 let lid_str = match body["link_id"].as_str() {
735 Some(s) => s,
736 None => return HttpResponse::bad_request("Missing link_id"),
737 };
738 let link_id: [u8; 16] = match hex_to_array(lid_str) {
739 Some(h) => h,
740 None => return HttpResponse::bad_request("Invalid link_id"),
741 };
742
743 with_node(node, |n| {
744 match n.propose_direct_connect(link_id) {
745 Ok(()) => HttpResponse::ok(json!({"status": "proposed"})),
746 Err(_) => HttpResponse::internal_error("Direct connect proposal failed"),
747 }
748 })
749}
750
751fn handle_list_hooks(node: &NodeHandle) -> HttpResponse {
754 with_node(node, |n| {
755 match n.list_hooks() {
756 Ok(hooks) => {
757 let list: Vec<Value> = hooks
758 .iter()
759 .map(|h| {
760 json!({
761 "name": h.name,
762 "attach_point": h.attach_point,
763 "priority": h.priority,
764 "enabled": h.enabled,
765 "consecutive_traps": h.consecutive_traps,
766 })
767 })
768 .collect();
769 HttpResponse::ok(json!({"hooks": list}))
770 }
771 Err(_) => HttpResponse::internal_error("Query failed"),
772 }
773 })
774}
775
776fn handle_load_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
782 let body = match parse_json_body(req) {
783 Ok(v) => v,
784 Err(r) => return r,
785 };
786
787 let name = match body["name"].as_str() {
788 Some(s) => s.to_string(),
789 None => return HttpResponse::bad_request("Missing name"),
790 };
791 let path = match body["path"].as_str() {
792 Some(s) => s,
793 None => return HttpResponse::bad_request("Missing path"),
794 };
795 let attach_point = match body["attach_point"].as_str() {
796 Some(s) => s.to_string(),
797 None => return HttpResponse::bad_request("Missing attach_point"),
798 };
799 let priority = body["priority"].as_i64().unwrap_or(0) as i32;
800
801 let wasm_bytes = match std::fs::read(path) {
803 Ok(b) => b,
804 Err(e) => return HttpResponse::bad_request(&format!("Failed to read WASM file: {}", e)),
805 };
806
807 with_node(node, |n| {
808 match n.load_hook(name, wasm_bytes, attach_point, priority) {
809 Ok(Ok(())) => HttpResponse::ok(json!({"status": "loaded"})),
810 Ok(Err(e)) => HttpResponse::bad_request(&e),
811 Err(_) => HttpResponse::internal_error("Driver unavailable"),
812 }
813 })
814}
815
816fn handle_unload_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
817 let body = match parse_json_body(req) {
818 Ok(v) => v,
819 Err(r) => return r,
820 };
821
822 let name = match body["name"].as_str() {
823 Some(s) => s.to_string(),
824 None => return HttpResponse::bad_request("Missing name"),
825 };
826 let attach_point = match body["attach_point"].as_str() {
827 Some(s) => s.to_string(),
828 None => return HttpResponse::bad_request("Missing attach_point"),
829 };
830
831 with_node(node, |n| {
832 match n.unload_hook(name, attach_point) {
833 Ok(Ok(())) => HttpResponse::ok(json!({"status": "unloaded"})),
834 Ok(Err(e)) => HttpResponse::bad_request(&e),
835 Err(_) => HttpResponse::internal_error("Driver unavailable"),
836 }
837 })
838}
839
840fn handle_reload_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
841 let body = match parse_json_body(req) {
842 Ok(v) => v,
843 Err(r) => return r,
844 };
845
846 let name = match body["name"].as_str() {
847 Some(s) => s.to_string(),
848 None => return HttpResponse::bad_request("Missing name"),
849 };
850 let path = match body["path"].as_str() {
851 Some(s) => s,
852 None => return HttpResponse::bad_request("Missing path"),
853 };
854 let attach_point = match body["attach_point"].as_str() {
855 Some(s) => s.to_string(),
856 None => return HttpResponse::bad_request("Missing attach_point"),
857 };
858
859 let wasm_bytes = match std::fs::read(path) {
860 Ok(b) => b,
861 Err(e) => return HttpResponse::bad_request(&format!("Failed to read WASM file: {}", e)),
862 };
863
864 with_node(node, |n| {
865 match n.reload_hook(name, attach_point, wasm_bytes) {
866 Ok(Ok(())) => HttpResponse::ok(json!({"status": "reloaded"})),
867 Ok(Err(e)) => HttpResponse::bad_request(&e),
868 Err(_) => HttpResponse::internal_error("Driver unavailable"),
869 }
870 })
871}
872
873fn format_dest_name(app_name: &str, aspects: &[&str]) -> String {
876 if aspects.is_empty() {
877 app_name.to_string()
878 } else {
879 format!("{}.{}", app_name, aspects.join("."))
880 }
881}
882
883fn parse_proof_strategy(body: &Value) -> ProofStrategy {
884 match body["proof_strategy"].as_str() {
885 Some("all") => ProofStrategy::ProveAll,
886 Some("app") => ProofStrategy::ProveApp,
887 _ => ProofStrategy::ProveNone,
888 }
889}