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