Skip to main content

rns_ctl/
api.rs

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
15/// Handle for the node, wrapped so shutdown() can consume it.
16pub type NodeHandle = std::sync::Arc<std::sync::Mutex<Option<RnsNode>>>;
17
18/// Execute a closure with a reference to the node, returning an error response if the node is gone.
19fn 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
30/// Route dispatch: match method + path and call the appropriate handler.
31pub fn handle_request(
32    req: &HttpRequest,
33    node: &NodeHandle,
34    state: &SharedState,
35    config: &CtlConfig,
36) -> HttpResponse {
37    // Health check — no auth required
38    if req.method == "GET" && req.path == "/health" {
39        return HttpResponse::ok(json!({"status": "healthy"}));
40    }
41
42    // All other endpoints require auth
43    if let Err(resp) = check_auth(req, config) {
44        return resp;
45    }
46
47    match (req.method.as_str(), req.path.as_str()) {
48        // Read endpoints
49        ("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        // Identity recall: /api/identity/<dest_hash>
62        ("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        // Action endpoints
68        ("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        // Hook management
80        ("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
89// --- Read handlers ---
90
91fn 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
338// --- Action handlers ---
339
340fn 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                                // Register in state
399                                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                // For inbound single dests, also register with link manager
449                // so incoming LINKREQUEST packets are accepted.
450                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        // Recall identity to get signing public key
582        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        // Extract Ed25519 public key (second 32 bytes of public_key)
588        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
751// --- Hook handlers ---
752
753fn 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
776/// Load a WASM hook from a filesystem path.
777///
778/// The `path` field in the JSON body refers to a file on the **server's** local
779/// filesystem. This means the CLI and the HTTP server must have access to the
780/// same filesystem for the path to resolve correctly.
781fn 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    // Read WASM file
802    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
873// --- Helpers ---
874
875fn 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}