1use serde_json::{json, Value};
2
3const DEFAULT_HOOK_TYPE: &str = {
4 #[cfg(feature = "rns-hooks-native")]
5 {
6 "native"
7 }
8 #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
9 {
10 "wasm"
11 }
12 #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
13 {
14 "wasm"
15 }
16};
17
18use rns_crypto::identity::Identity;
19use rns_net::{
20 event::LifecycleState, DestHash, Destination, IdentityHash, ProofStrategy, QueryRequest,
21 QueryResponse, RnsNode,
22};
23
24use crate::auth::check_auth;
25use crate::encode::{from_base64, hex_to_array, to_base64, to_hex};
26use crate::http::{parse_query, HttpRequest, HttpResponse};
27use crate::state::{
28 lock_node_handle, read_state, ControlPlaneConfigHandle, DestinationEntry, SharedState,
29};
30use crate::stats_api;
31
32pub type NodeHandle = std::sync::Arc<std::sync::Mutex<Option<RnsNode>>>;
34
35fn with_node<F>(node: &NodeHandle, f: F) -> HttpResponse
37where
38 F: FnOnce(&RnsNode) -> HttpResponse,
39{
40 let guard = lock_node_handle(node);
41 match guard.as_ref() {
42 Some(n) => f(n),
43 None => HttpResponse::internal_error("Node is shutting down"),
44 }
45}
46
47fn with_active_node<F>(node: &NodeHandle, f: F) -> HttpResponse
48where
49 F: FnOnce(&RnsNode) -> HttpResponse,
50{
51 with_node(node, |n| match n.query(QueryRequest::DrainStatus) {
52 Ok(QueryResponse::DrainStatus(status))
53 if !matches!(status.state, LifecycleState::Active) =>
54 {
55 HttpResponse::conflict(
56 status
57 .detail
58 .as_deref()
59 .unwrap_or("Node is draining and not accepting new work"),
60 )
61 }
62 _ => f(n),
63 })
64}
65
66pub fn handle_request(
68 req: &HttpRequest,
69 node: &NodeHandle,
70 state: &SharedState,
71 config: &ControlPlaneConfigHandle,
72) -> HttpResponse {
73 if req.method == "GET" && (req.path == "/" || req.path == "/ui") {
74 return HttpResponse::html(index_html(config));
75 }
76 if req.method == "GET" && req.path == "/assets/app.css" {
77 return HttpResponse::bytes(
78 200,
79 "OK",
80 "text/css; charset=utf-8",
81 include_str!("../assets/app.css").as_bytes().to_vec(),
82 );
83 }
84 if req.method == "GET" && req.path == "/assets/app.js" {
85 return HttpResponse::bytes(
86 200,
87 "OK",
88 "application/javascript; charset=utf-8",
89 include_str!("../assets/app.js").as_bytes().to_vec(),
90 );
91 }
92
93 if req.method == "GET" && req.path == "/health" {
95 return HttpResponse::ok(json!({"status": "healthy"}));
96 }
97
98 if let Err(resp) = check_auth(req, config) {
100 return resp;
101 }
102
103 match (req.method.as_str(), req.path.as_str()) {
104 ("GET", "/api/node") => handle_node(node, state),
106 ("GET", "/api/config") => handle_config(state),
107 ("GET", "/api/config/schema") => handle_config_schema(state),
108 ("GET", "/api/config/status") => handle_config_status(state),
109 ("GET", "/api/processes") => handle_processes(state),
110 ("GET", "/api/process_events") => handle_process_events(state),
111 ("GET", "/api/stats/summary") => stats_api::handle_summary(req, state),
112 ("GET", "/api/stats/announces") => stats_api::handle_announces(req, state),
113 ("GET", "/api/stats/interfaces") => stats_api::handle_interfaces(req, state),
114 ("GET", "/api/stats/destinations") => stats_api::handle_destinations(req, state),
115 ("GET", "/api/stats/packets") => stats_api::handle_packets(req, state),
116 ("GET", "/api/stats/packets/series") => stats_api::handle_packet_series(req, state),
117 ("GET", "/api/stats/links") => stats_api::handle_links(req, state),
118 ("GET", "/api/stats/system") => stats_api::handle_system(req, state),
119 ("GET", path) if path.starts_with("/api/processes/") && path.ends_with("/logs") => {
120 handle_process_logs(path, req, state)
121 }
122 ("GET", "/api/info") => handle_info(node, state),
123 ("GET", "/api/interfaces") => handle_interfaces(node),
124 ("GET", "/api/destinations") => handle_destinations(node, state),
125 ("GET", "/api/paths") => handle_paths(req, node),
126 ("GET", "/api/links") => handle_links(node),
127 ("GET", "/api/resources") => handle_resources(node),
128 ("GET", "/api/announces") => handle_event_list(req, state, "announces"),
129 ("GET", "/api/packets") => handle_event_list(req, state, "packets"),
130 ("GET", "/api/proofs") => handle_event_list(req, state, "proofs"),
131 ("GET", "/api/link_events") => handle_event_list(req, state, "link_events"),
132 ("GET", "/api/resource_events") => handle_event_list(req, state, "resource_events"),
133
134 ("GET", path) if path.starts_with("/api/identity/") => {
136 let hash_str = &path["/api/identity/".len()..];
137 handle_recall_identity(hash_str, node)
138 }
139
140 ("POST", "/api/destination") => handle_post_destination(req, node, state),
142 ("POST", "/api/announce") => handle_post_announce(req, node, state),
143 ("POST", "/api/send") => handle_post_send(req, node, state),
144 ("POST", "/api/config/validate") => handle_config_validate(req, state),
145 ("POST", "/api/config") => {
146 handle_config_mutation(req, state, crate::state::ServerConfigMutationMode::Save)
147 }
148 ("POST", "/api/config/apply") => {
149 handle_config_mutation(req, state, crate::state::ServerConfigMutationMode::Apply)
150 }
151 ("POST", "/api/link") => handle_post_link(req, node),
152 ("POST", "/api/link/send") => handle_post_link_send(req, node),
153 ("POST", "/api/link/close") => handle_post_link_close(req, node),
154 ("POST", "/api/channel") => handle_post_channel(req, node),
155 ("POST", "/api/resource") => handle_post_resource(req, node),
156 ("POST", "/api/path/request") => handle_post_path_request(req, node),
157 ("POST", "/api/direct_connect") => handle_post_direct_connect(req, node),
158 ("POST", "/api/announce_queues/clear") => handle_post_clear_announce_queues(node),
159 ("POST", path) if path.starts_with("/api/processes/") && path.ends_with("/restart") => {
160 handle_process_control(path, state, "restart")
161 }
162 ("POST", path) if path.starts_with("/api/processes/") && path.ends_with("/start") => {
163 handle_process_control(path, state, "start")
164 }
165 ("POST", path) if path.starts_with("/api/processes/") && path.ends_with("/stop") => {
166 handle_process_control(path, state, "stop")
167 }
168
169 ("GET", "/api/backbone/peers") => handle_backbone_peers(req, node),
171 ("POST", "/api/backbone/blacklist") => handle_backbone_blacklist(req, node),
172
173 ("GET", "/api/hooks") => handle_list_hooks(node),
175 ("POST", "/api/hook/load") => handle_load_hook(req, node),
176 ("POST", "/api/hook/unload") => handle_unload_hook(req, node),
177 ("POST", "/api/hook/reload") => handle_reload_hook(req, node),
178 ("POST", "/api/hook/enable") => handle_set_hook_enabled(req, node, true),
179 ("POST", "/api/hook/disable") => handle_set_hook_enabled(req, node, false),
180 ("POST", "/api/hook/priority") => handle_set_hook_priority(req, node),
181
182 _ => HttpResponse::not_found(),
183 }
184}
185
186fn index_html(_config: &ControlPlaneConfigHandle) -> &'static str {
187 include_str!("../assets/index_auth.html")
188}
189
190fn handle_node(node: &NodeHandle, state: &SharedState) -> HttpResponse {
193 let (transport_id, drain_status) = {
194 let guard = lock_node_handle(node);
195 let Some(node) = guard.as_ref() else {
196 return HttpResponse::internal_error("Node is shutting down");
197 };
198 let transport_id = match node.query(QueryRequest::TransportIdentity) {
199 Ok(QueryResponse::TransportIdentity(id)) => id,
200 _ => None,
201 };
202 let drain_status = match node.query(QueryRequest::DrainStatus) {
203 Ok(QueryResponse::DrainStatus(status)) => Some(status),
204 _ => None,
205 };
206 (transport_id, drain_status)
207 };
208
209 let s = read_state(state);
210 HttpResponse::ok(json!({
211 "server_mode": s.server_mode,
212 "uptime_seconds": s.uptime_seconds(),
213 "transport_id": transport_id.map(|h| to_hex(&h)),
214 "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
215 "process_count": s.processes.len(),
216 "processes_running": s.processes.values().filter(|p| p.status == "running").count(),
217 "processes_ready": s.processes.values().filter(|p| p.ready).count(),
218 "drain": drain_status.map(|status| json!({
219 "state": format!("{:?}", status.state).to_lowercase(),
220 "drain_age_seconds": status.drain_age_seconds,
221 "deadline_remaining_seconds": status.deadline_remaining_seconds,
222 "drain_complete": status.drain_complete,
223 "interface_writer_queued_frames": status.interface_writer_queued_frames,
224 "provider_backlog_events": status.provider_backlog_events,
225 "provider_consumer_queued_events": status.provider_consumer_queued_events,
226 "detail": status.detail,
227 })),
228 }))
229}
230
231fn handle_config(state: &SharedState) -> HttpResponse {
232 let s = read_state(state);
233 match &s.server_config {
234 Some(config) => HttpResponse::ok(json!({ "config": config })),
235 None => HttpResponse::ok(json!({ "config": null })),
236 }
237}
238
239fn handle_config_schema(state: &SharedState) -> HttpResponse {
240 let s = read_state(state);
241 match &s.server_config_schema {
242 Some(schema) => HttpResponse::ok(json!({ "schema": schema })),
243 None => HttpResponse::ok(json!({ "schema": null })),
244 }
245}
246
247fn handle_config_status(state: &SharedState) -> HttpResponse {
248 let s = read_state(state);
249 HttpResponse::ok(json!({
250 "status": s.server_config_status.snapshot(),
251 }))
252}
253
254fn handle_config_validate(req: &HttpRequest, state: &SharedState) -> HttpResponse {
255 let validator = {
256 let s = read_state(state);
257 s.server_config_validator.clone()
258 };
259
260 match validator {
261 Some(validator) => match validator(&req.body) {
262 Ok(result) => HttpResponse::ok(json!({ "result": result })),
263 Err(err) => HttpResponse::bad_request(&err),
264 },
265 None => HttpResponse::internal_error("Server config validation is not enabled"),
266 }
267}
268
269fn handle_config_mutation(
270 req: &HttpRequest,
271 state: &SharedState,
272 mode: crate::state::ServerConfigMutationMode,
273) -> HttpResponse {
274 let mutator = {
275 let s = read_state(state);
276 s.server_config_mutator.clone()
277 };
278
279 match mutator {
280 Some(mutator) => match mutator(mode, &req.body) {
281 Ok(result) => HttpResponse::ok(json!({ "result": result })),
282 Err(err) => HttpResponse::bad_request(&err),
283 },
284 None => HttpResponse::internal_error("Server config mutation is not enabled"),
285 }
286}
287
288fn handle_info(node: &NodeHandle, state: &SharedState) -> HttpResponse {
289 with_node(node, |n| {
290 let transport_id = match n.query(QueryRequest::TransportIdentity) {
291 Ok(QueryResponse::TransportIdentity(id)) => id,
292 _ => None,
293 };
294 let s = read_state(state);
295 HttpResponse::ok(json!({
296 "transport_id": transport_id.map(|h| to_hex(&h)),
297 "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
298 "uptime_seconds": s.uptime_seconds(),
299 }))
300 })
301}
302
303fn handle_processes(state: &SharedState) -> HttpResponse {
304 let s = read_state(state);
305 let mut processes: Vec<&crate::state::ManagedProcessState> = s.processes.values().collect();
306 processes.sort_by(|a, b| a.name.cmp(&b.name));
307 HttpResponse::ok(json!({
308 "processes": processes
309 .into_iter()
310 .map(|p| json!({
311 "name": p.name,
312 "status": p.status,
313 "ready": p.ready,
314 "ready_state": p.ready_state,
315 "pid": p.pid,
316 "last_exit_code": p.last_exit_code,
317 "restart_count": p.restart_count,
318 "drain_ack_count": p.drain_ack_count,
319 "forced_kill_count": p.forced_kill_count,
320 "last_error": p.last_error,
321 "status_detail": p.status_detail,
322 "durable_log_path": p.durable_log_path,
323 "last_log_age_seconds": p.last_log_age_seconds(),
324 "recent_log_lines": p.recent_log_lines,
325 "uptime_seconds": p.uptime_seconds(),
326 "last_transition_seconds": p.last_transition_seconds(),
327 }))
328 .collect::<Vec<Value>>(),
329 }))
330}
331
332fn handle_process_events(state: &SharedState) -> HttpResponse {
333 let s = read_state(state);
334 let events: Vec<Value> = s
335 .process_events
336 .iter()
337 .rev()
338 .take(20)
339 .map(|event| {
340 json!({
341 "process": event.process,
342 "event": event.event,
343 "detail": event.detail,
344 "age_seconds": event.recorded_at.elapsed().as_secs_f64(),
345 })
346 })
347 .collect();
348 HttpResponse::ok(json!({ "events": events }))
349}
350
351fn handle_process_logs(path: &str, req: &HttpRequest, state: &SharedState) -> HttpResponse {
352 let Some(name) = path
353 .strip_prefix("/api/processes/")
354 .and_then(|rest| rest.strip_suffix("/logs"))
355 else {
356 return HttpResponse::bad_request("Invalid process logs path");
357 };
358
359 let limit = parse_query(&req.query)
360 .get("limit")
361 .and_then(|value| value.parse::<usize>().ok())
362 .map(|value| value.min(500))
363 .unwrap_or(200);
364
365 let s = read_state(state);
366 let Some(logs) = s.process_logs.get(name) else {
367 return HttpResponse::not_found();
368 };
369
370 let lines: Vec<Value> = logs
371 .iter()
372 .rev()
373 .take(limit)
374 .map(|entry| {
375 json!({
376 "process": entry.process,
377 "stream": entry.stream,
378 "line": entry.line,
379 "age_seconds": entry.recorded_at.elapsed().as_secs_f64(),
380 })
381 })
382 .collect();
383
384 HttpResponse::ok(json!({
385 "process": name,
386 "durable_log_path": s.processes.get(name).and_then(|p| p.durable_log_path.clone()),
387 "last_log_age_seconds": s.processes.get(name).and_then(|p| p.last_log_age_seconds()),
388 "recent_log_lines": s.processes.get(name).map(|p| p.recent_log_lines).unwrap_or(0),
389 "lines": lines,
390 }))
391}
392
393fn handle_process_control(path: &str, state: &SharedState, action: &str) -> HttpResponse {
394 let Some(name) = path.strip_prefix("/api/processes/").and_then(|rest| {
395 rest.strip_suffix("/restart")
396 .or_else(|| rest.strip_suffix("/start"))
397 .or_else(|| rest.strip_suffix("/stop"))
398 }) else {
399 return HttpResponse::bad_request("Invalid process control path");
400 };
401
402 let tx = {
403 let s = read_state(state);
404 s.control_tx.clone()
405 };
406
407 match tx {
408 Some(tx) => {
409 let process_name = name.to_string();
410 let command = match action {
411 "restart" => crate::state::ProcessControlCommand::Restart(process_name.clone()),
412 "start" => crate::state::ProcessControlCommand::Start(process_name.clone()),
413 "stop" => crate::state::ProcessControlCommand::Stop(process_name.clone()),
414 _ => return HttpResponse::bad_request("Unknown process action"),
415 };
416 match tx.send(command) {
417 Ok(()) => HttpResponse::ok(json!({
418 "ok": true,
419 "queued": true,
420 "action": action,
421 "process": process_name,
422 })),
423 Err(_) => HttpResponse::internal_error("Process control channel is unavailable"),
424 }
425 }
426 None => HttpResponse::internal_error("Process control is not enabled"),
427 }
428}
429
430fn handle_interfaces(node: &NodeHandle) -> HttpResponse {
431 with_node(node, |n| match n.query(QueryRequest::InterfaceStats) {
432 Ok(QueryResponse::InterfaceStats(stats)) => {
433 let ifaces: Vec<Value> = stats
434 .interfaces
435 .iter()
436 .map(|i| {
437 json!({
438 "id": i.id,
439 "name": i.name,
440 "status": if i.status { "up" } else { "down" },
441 "mode": i.mode,
442 "interface_type": i.interface_type,
443 "rxb": i.rxb,
444 "txb": i.txb,
445 "rx_packets": i.rx_packets,
446 "tx_packets": i.tx_packets,
447 "bitrate": i.bitrate,
448 "started": i.started,
449 "ia_freq": i.ia_freq,
450 "oa_freq": i.oa_freq,
451 })
452 })
453 .collect();
454 let backbone_peer_pool = stats.backbone_peer_pool.as_ref().map(|pool| {
455 json!({
456 "max_connected": pool.max_connected,
457 "active_count": pool.active_count,
458 "standby_count": pool.standby_count,
459 "cooldown_count": pool.cooldown_count,
460 "members": pool.members.iter().map(|member| {
461 json!({
462 "name": member.name,
463 "remote": member.remote,
464 "source": member.source,
465 "priority": member.priority,
466 "state": member.state,
467 "interface_id": member.interface_id,
468 "failure_count": member.failure_count,
469 "last_error": member.last_error,
470 "cooldown_remaining_seconds": member.cooldown_remaining_seconds,
471 })
472 }).collect::<Vec<_>>(),
473 })
474 });
475 HttpResponse::ok(json!({
476 "interfaces": ifaces,
477 "transport_enabled": stats.transport_enabled,
478 "transport_uptime": stats.transport_uptime,
479 "total_rxb": stats.total_rxb,
480 "total_txb": stats.total_txb,
481 "backbone_peer_pool": backbone_peer_pool,
482 }))
483 }
484 _ => HttpResponse::internal_error("Query failed"),
485 })
486}
487
488fn handle_destinations(node: &NodeHandle, state: &SharedState) -> HttpResponse {
489 with_node(node, |n| match n.query(QueryRequest::LocalDestinations) {
490 Ok(QueryResponse::LocalDestinations(dests)) => {
491 let s = read_state(state);
492 let list: Vec<Value> = dests
493 .iter()
494 .map(|d| {
495 let name = s
496 .destinations
497 .get(&d.hash)
498 .map(|e| e.full_name.as_str())
499 .unwrap_or("");
500 json!({
501 "hash": to_hex(&d.hash),
502 "type": d.dest_type,
503 "name": name,
504 })
505 })
506 .collect();
507 HttpResponse::ok(json!({"destinations": list}))
508 }
509 _ => HttpResponse::internal_error("Query failed"),
510 })
511}
512
513fn handle_paths(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
514 let params = parse_query(&req.query);
515 let filter_hash: Option<[u8; 16]> = params.get("dest_hash").and_then(|s| hex_to_array(s));
516
517 with_node(node, |n| {
518 match n.query(QueryRequest::PathTable { max_hops: None }) {
519 Ok(QueryResponse::PathTable(paths)) => {
520 let list: Vec<Value> = paths
521 .iter()
522 .filter(|p| filter_hash.is_none_or(|h| p.hash == h))
523 .map(|p| {
524 json!({
525 "hash": to_hex(&p.hash),
526 "via": to_hex(&p.via),
527 "hops": p.hops,
528 "expires": p.expires,
529 "interface": p.interface_name,
530 "timestamp": p.timestamp,
531 })
532 })
533 .collect();
534 HttpResponse::ok(json!({"paths": list}))
535 }
536 _ => HttpResponse::internal_error("Query failed"),
537 }
538 })
539}
540
541fn handle_links(node: &NodeHandle) -> HttpResponse {
542 with_node(node, |n| match n.query(QueryRequest::Links) {
543 Ok(QueryResponse::Links(links)) => {
544 let list: Vec<Value> = links
545 .iter()
546 .map(|l| {
547 json!({
548 "link_id": to_hex(&l.link_id),
549 "state": l.state,
550 "is_initiator": l.is_initiator,
551 "dest_hash": to_hex(&l.dest_hash),
552 "remote_identity": l.remote_identity.map(|h| to_hex(&h)),
553 "rtt": l.rtt,
554 "channel_window": l.channel_window,
555 "channel_outstanding": l.channel_outstanding,
556 "pending_channel_packets": l.pending_channel_packets,
557 "channel_send_ok": l.channel_send_ok,
558 "channel_send_not_ready": l.channel_send_not_ready,
559 "channel_send_too_big": l.channel_send_too_big,
560 "channel_send_other_error": l.channel_send_other_error,
561 "channel_messages_received": l.channel_messages_received,
562 "channel_proofs_sent": l.channel_proofs_sent,
563 "channel_proofs_received": l.channel_proofs_received,
564 })
565 })
566 .collect();
567 HttpResponse::ok(json!({"links": list}))
568 }
569 _ => HttpResponse::internal_error("Query failed"),
570 })
571}
572
573fn handle_resources(node: &NodeHandle) -> HttpResponse {
574 with_node(node, |n| match n.query(QueryRequest::Resources) {
575 Ok(QueryResponse::Resources(resources)) => {
576 let list: Vec<Value> = resources
577 .iter()
578 .map(|r| {
579 json!({
580 "link_id": to_hex(&r.link_id),
581 "direction": r.direction,
582 "total_parts": r.total_parts,
583 "transferred_parts": r.transferred_parts,
584 "complete": r.complete,
585 })
586 })
587 .collect();
588 HttpResponse::ok(json!({"resources": list}))
589 }
590 _ => HttpResponse::internal_error("Query failed"),
591 })
592}
593
594fn handle_event_list(req: &HttpRequest, state: &SharedState, kind: &str) -> HttpResponse {
595 let params = parse_query(&req.query);
596 let clear = params.get("clear").is_some_and(|v| v == "true");
597
598 let mut s = crate::state::write_state(state);
599 let items: Vec<Value> = match kind {
600 "announces" => {
601 let v: Vec<Value> = s
602 .announces
603 .iter()
604 .map(|r| serde_json::to_value(r).unwrap_or_default())
605 .collect();
606 if clear {
607 s.announces.clear();
608 }
609 v
610 }
611 "packets" => {
612 let v: Vec<Value> = s
613 .packets
614 .iter()
615 .map(|r| serde_json::to_value(r).unwrap_or_default())
616 .collect();
617 if clear {
618 s.packets.clear();
619 }
620 v
621 }
622 "proofs" => {
623 let v: Vec<Value> = s
624 .proofs
625 .iter()
626 .map(|r| serde_json::to_value(r).unwrap_or_default())
627 .collect();
628 if clear {
629 s.proofs.clear();
630 }
631 v
632 }
633 "link_events" => {
634 let v: Vec<Value> = s
635 .link_events
636 .iter()
637 .map(|r| serde_json::to_value(r).unwrap_or_default())
638 .collect();
639 if clear {
640 s.link_events.clear();
641 }
642 v
643 }
644 "resource_events" => {
645 let v: Vec<Value> = s
646 .resource_events
647 .iter()
648 .map(|r| serde_json::to_value(r).unwrap_or_default())
649 .collect();
650 if clear {
651 s.resource_events.clear();
652 }
653 v
654 }
655 _ => Vec::new(),
656 };
657
658 let mut obj = serde_json::Map::new();
659 obj.insert(kind.to_string(), Value::Array(items));
660 HttpResponse::ok(Value::Object(obj))
661}
662
663fn handle_recall_identity(hash_str: &str, node: &NodeHandle) -> HttpResponse {
664 let dest_hash: [u8; 16] = match hex_to_array(hash_str) {
665 Some(h) => h,
666 None => return HttpResponse::bad_request("Invalid dest_hash hex (expected 32 hex chars)"),
667 };
668
669 with_node(node, |n| match n.recall_identity(&DestHash(dest_hash)) {
670 Ok(Some(ai)) => HttpResponse::ok(json!({
671 "dest_hash": to_hex(&ai.dest_hash.0),
672 "identity_hash": to_hex(&ai.identity_hash.0),
673 "public_key": to_hex(&ai.public_key),
674 "app_data": ai.app_data.as_ref().map(|d| to_base64(d)),
675 "hops": ai.hops,
676 "received_at": ai.received_at,
677 })),
678 Ok(None) => HttpResponse::not_found(),
679 Err(_) => HttpResponse::internal_error("Query failed"),
680 })
681}
682
683fn parse_json_body(req: &HttpRequest) -> Result<Value, HttpResponse> {
686 serde_json::from_slice(&req.body)
687 .map_err(|e| HttpResponse::bad_request(&format!("Invalid JSON: {}", e)))
688}
689
690fn handle_post_destination(
691 req: &HttpRequest,
692 node: &NodeHandle,
693 state: &SharedState,
694) -> HttpResponse {
695 let body = match parse_json_body(req) {
696 Ok(v) => v,
697 Err(r) => return r,
698 };
699
700 let dest_type_str = body["type"].as_str().unwrap_or("");
701 let app_name = match body["app_name"].as_str() {
702 Some(s) => s,
703 None => return HttpResponse::bad_request("Missing app_name"),
704 };
705 let aspects: Vec<&str> = body["aspects"]
706 .as_array()
707 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
708 .unwrap_or_default();
709
710 let (identity_hash, identity_prv_key, identity_pub_key) = {
711 let s = read_state(state);
712 let ih = s.identity_hash;
713 let prv = s.identity.as_ref().and_then(|i| i.get_private_key());
714 let pubk = s.identity.as_ref().and_then(|i| i.get_public_key());
715 (ih, prv, pubk)
716 };
717
718 let (dest, signing_key) = match dest_type_str {
719 "single" => {
720 let direction = body["direction"].as_str().unwrap_or("in");
721 match direction {
722 "in" => {
723 let ih = match identity_hash {
724 Some(h) => IdentityHash(h),
725 None => return HttpResponse::internal_error("No identity loaded"),
726 };
727 let dest = Destination::single_in(app_name, &aspects, ih)
728 .set_proof_strategy(parse_proof_strategy(&body));
729 (dest, identity_prv_key)
730 }
731 "out" => {
732 let dh_str = match body["dest_hash"].as_str() {
733 Some(s) => s,
734 None => {
735 return HttpResponse::bad_request(
736 "OUT single requires dest_hash of remote",
737 )
738 }
739 };
740 let dh: [u8; 16] = match hex_to_array(dh_str) {
741 Some(h) => h,
742 None => return HttpResponse::bad_request("Invalid dest_hash"),
743 };
744 return with_node(node, |n| {
745 match n.recall_identity(&DestHash(dh)) {
746 Ok(Some(recalled)) => {
747 let dest = Destination::single_out(app_name, &aspects, &recalled);
748 let full_name = format_dest_name(app_name, &aspects);
750 let mut s = crate::state::write_state(state);
751 s.destinations.insert(
752 dest.hash.0,
753 DestinationEntry {
754 destination: dest.clone(),
755 full_name: full_name.clone(),
756 },
757 );
758 HttpResponse::created(json!({
759 "dest_hash": to_hex(&dest.hash.0),
760 "name": full_name,
761 "type": "single",
762 "direction": "out",
763 }))
764 }
765 Ok(None) => {
766 HttpResponse::bad_request("No recalled identity for dest_hash")
767 }
768 Err(_) => HttpResponse::internal_error("Query failed"),
769 }
770 });
771 }
772 _ => return HttpResponse::bad_request("direction must be 'in' or 'out'"),
773 }
774 }
775 "plain" => {
776 let dest = Destination::plain(app_name, &aspects)
777 .set_proof_strategy(parse_proof_strategy(&body));
778 (dest, None)
779 }
780 "group" => {
781 let mut dest = Destination::group(app_name, &aspects)
782 .set_proof_strategy(parse_proof_strategy(&body));
783 if let Some(key_b64) = body["group_key"].as_str() {
784 match from_base64(key_b64) {
785 Some(key) => {
786 if let Err(e) = dest.load_private_key(key) {
787 return HttpResponse::bad_request(&format!("Invalid group key: {}", e));
788 }
789 }
790 None => return HttpResponse::bad_request("Invalid base64 group_key"),
791 }
792 } else {
793 dest.create_keys();
794 }
795 (dest, None)
796 }
797 _ => return HttpResponse::bad_request("type must be 'single', 'plain', or 'group'"),
798 };
799
800 with_node(node, |n| {
801 match n.register_destination_with_proof(&dest, signing_key) {
802 Ok(()) => {
803 if dest_type_str == "single" && body["direction"].as_str().unwrap_or("in") == "in" {
806 if let (Some(prv), Some(pubk)) = (identity_prv_key, identity_pub_key) {
807 let mut sig_prv = [0u8; 32];
808 sig_prv.copy_from_slice(&prv[32..64]);
809 let mut sig_pub = [0u8; 32];
810 sig_pub.copy_from_slice(&pubk[32..64]);
811 let _ = n.register_link_destination(dest.hash.0, sig_prv, sig_pub, 0);
812 }
813 }
814
815 let full_name = format_dest_name(app_name, &aspects);
816 let hash_hex = to_hex(&dest.hash.0);
817 let group_key_b64 = dest.get_private_key().map(to_base64);
818 let mut s = crate::state::write_state(state);
819 s.destinations.insert(
820 dest.hash.0,
821 DestinationEntry {
822 destination: dest,
823 full_name: full_name.clone(),
824 },
825 );
826 let mut resp = json!({
827 "dest_hash": hash_hex,
828 "name": full_name,
829 "type": dest_type_str,
830 });
831 if let Some(gk) = group_key_b64 {
832 resp["group_key"] = Value::String(gk);
833 }
834 HttpResponse::created(resp)
835 }
836 Err(_) => HttpResponse::internal_error("Failed to register destination"),
837 }
838 })
839}
840
841fn handle_post_announce(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
842 let body = match parse_json_body(req) {
843 Ok(v) => v,
844 Err(r) => return r,
845 };
846
847 let dh_str = match body["dest_hash"].as_str() {
848 Some(s) => s,
849 None => return HttpResponse::bad_request("Missing dest_hash"),
850 };
851 let dh: [u8; 16] = match hex_to_array(dh_str) {
852 Some(h) => h,
853 None => return HttpResponse::bad_request("Invalid dest_hash"),
854 };
855
856 let app_data: Option<Vec<u8>> = body["app_data"].as_str().and_then(from_base64);
857
858 let (dest, identity) = {
859 let s = read_state(state);
860 let dest = match s.destinations.get(&dh) {
861 Some(entry) => entry.destination.clone(),
862 None => return HttpResponse::bad_request("Destination not registered via API"),
863 };
864 let identity = match s.identity.as_ref().and_then(|i| i.get_private_key()) {
865 Some(prv) => Identity::from_private_key(&prv),
866 None => return HttpResponse::internal_error("No identity loaded"),
867 };
868 (dest, identity)
869 };
870
871 with_active_node(node, |n| {
872 match n.announce(&dest, &identity, app_data.as_deref()) {
873 Ok(()) => HttpResponse::ok(json!({"status": "announced", "dest_hash": dh_str})),
874 Err(_) => HttpResponse::internal_error("Announce failed"),
875 }
876 })
877}
878
879fn handle_post_send(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
880 let body = match parse_json_body(req) {
881 Ok(v) => v,
882 Err(r) => return r,
883 };
884
885 let dh_str = match body["dest_hash"].as_str() {
886 Some(s) => s,
887 None => return HttpResponse::bad_request("Missing dest_hash"),
888 };
889 let dh: [u8; 16] = match hex_to_array(dh_str) {
890 Some(h) => h,
891 None => return HttpResponse::bad_request("Invalid dest_hash"),
892 };
893 let data = match body["data"].as_str().and_then(from_base64) {
894 Some(d) => d,
895 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
896 };
897
898 let s = read_state(state);
899 let dest = match s.destinations.get(&dh) {
900 Some(entry) => entry.destination.clone(),
901 None => return HttpResponse::bad_request("Destination not registered via API"),
902 };
903 drop(s);
904
905 let max_len = match dest.dest_type {
906 rns_core::types::DestinationType::Plain => rns_core::constants::PLAIN_MDU,
907 rns_core::types::DestinationType::Single | rns_core::types::DestinationType::Group => {
908 rns_core::constants::ENCRYPTED_MDU
909 }
910 };
911 if data.len() > max_len {
912 return HttpResponse::bad_request(&format!(
913 "Payload too large for single-packet send: {} bytes > {} byte limit",
914 data.len(),
915 max_len
916 ));
917 }
918
919 with_active_node(node, |n| match n.send_packet(&dest, &data) {
920 Ok(ph) => HttpResponse::ok(json!({
921 "status": "sent",
922 "packet_hash": to_hex(&ph.0),
923 })),
924 Err(_) => HttpResponse::internal_error("Send failed"),
925 })
926}
927
928fn handle_post_link(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
929 let body = match parse_json_body(req) {
930 Ok(v) => v,
931 Err(r) => return r,
932 };
933
934 let dh_str = match body["dest_hash"].as_str() {
935 Some(s) => s,
936 None => return HttpResponse::bad_request("Missing dest_hash"),
937 };
938 let dh: [u8; 16] = match hex_to_array(dh_str) {
939 Some(h) => h,
940 None => return HttpResponse::bad_request("Invalid dest_hash"),
941 };
942
943 with_active_node(node, |n| {
944 let recalled = match n.recall_identity(&DestHash(dh)) {
946 Ok(Some(ai)) => ai,
947 Ok(None) => return HttpResponse::bad_request("No recalled identity for dest_hash"),
948 Err(_) => return HttpResponse::internal_error("Query failed"),
949 };
950 let mut sig_pub = [0u8; 32];
952 sig_pub.copy_from_slice(&recalled.public_key[32..64]);
953
954 match n.create_link(dh, sig_pub) {
955 Ok(link_id) => HttpResponse::created(json!({
956 "link_id": to_hex(&link_id),
957 })),
958 Err(_) => HttpResponse::internal_error("Create link failed"),
959 }
960 })
961}
962
963fn handle_post_link_send(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
964 let body = match parse_json_body(req) {
965 Ok(v) => v,
966 Err(r) => return r,
967 };
968
969 let link_id: [u8; 16] = match body["link_id"].as_str().and_then(|s| hex_to_array(s)) {
970 Some(h) => h,
971 None => return HttpResponse::bad_request("Missing or invalid link_id"),
972 };
973 let data = match body["data"].as_str().and_then(from_base64) {
974 Some(d) => d,
975 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
976 };
977 let context = body["context"].as_u64().unwrap_or(0) as u8;
978
979 with_active_node(node, |n| match n.send_on_link(link_id, data, context) {
980 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
981 Err(_) => HttpResponse::internal_error("Send on link failed"),
982 })
983}
984
985fn handle_post_link_close(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
986 let body = match parse_json_body(req) {
987 Ok(v) => v,
988 Err(r) => return r,
989 };
990
991 let link_id: [u8; 16] = match body["link_id"].as_str().and_then(|s| hex_to_array(s)) {
992 Some(h) => h,
993 None => return HttpResponse::bad_request("Missing or invalid link_id"),
994 };
995
996 with_node(node, |n| match n.teardown_link(link_id) {
997 Ok(()) => HttpResponse::ok(json!({"status": "closed"})),
998 Err(_) => HttpResponse::internal_error("Teardown link failed"),
999 })
1000}
1001
1002fn handle_post_channel(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1003 let body = match parse_json_body(req) {
1004 Ok(v) => v,
1005 Err(r) => return r,
1006 };
1007
1008 let link_id: [u8; 16] = match body["link_id"].as_str().and_then(|s| hex_to_array(s)) {
1009 Some(h) => h,
1010 None => return HttpResponse::bad_request("Missing or invalid link_id"),
1011 };
1012 let msgtype = body["msgtype"].as_u64().unwrap_or(0) as u16;
1013 let payload = match body["payload"].as_str().and_then(from_base64) {
1014 Some(d) => d,
1015 None => return HttpResponse::bad_request("Missing or invalid base64 payload"),
1016 };
1017
1018 with_active_node(node, |n| {
1019 match n.send_channel_message(link_id, msgtype, payload) {
1020 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
1021 Err(_) => HttpResponse::bad_request("Channel message failed"),
1022 }
1023 })
1024}
1025
1026fn handle_post_resource(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1027 let body = match parse_json_body(req) {
1028 Ok(v) => v,
1029 Err(r) => return r,
1030 };
1031
1032 let link_id: [u8; 16] = match body["link_id"].as_str().and_then(|s| hex_to_array(s)) {
1033 Some(h) => h,
1034 None => return HttpResponse::bad_request("Missing or invalid link_id"),
1035 };
1036 let data = match body["data"].as_str().and_then(from_base64) {
1037 Some(d) => d,
1038 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
1039 };
1040 let metadata = body["metadata"].as_str().and_then(from_base64);
1041
1042 with_active_node(node, |n| match n.send_resource(link_id, data, metadata) {
1043 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
1044 Err(_) => HttpResponse::internal_error("Resource send failed"),
1045 })
1046}
1047
1048fn handle_post_path_request(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1049 let body = match parse_json_body(req) {
1050 Ok(v) => v,
1051 Err(r) => return r,
1052 };
1053
1054 let dh_str = match body["dest_hash"].as_str() {
1055 Some(s) => s,
1056 None => return HttpResponse::bad_request("Missing dest_hash"),
1057 };
1058 let dh: [u8; 16] = match hex_to_array(dh_str) {
1059 Some(h) => h,
1060 None => return HttpResponse::bad_request("Invalid dest_hash"),
1061 };
1062
1063 with_active_node(node, |n| match n.request_path(&DestHash(dh)) {
1064 Ok(()) => HttpResponse::ok(json!({"status": "requested"})),
1065 Err(_) => HttpResponse::internal_error("Path request failed"),
1066 })
1067}
1068
1069fn handle_post_direct_connect(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1070 let body = match parse_json_body(req) {
1071 Ok(v) => v,
1072 Err(r) => return r,
1073 };
1074
1075 let lid_str = match body["link_id"].as_str() {
1076 Some(s) => s,
1077 None => return HttpResponse::bad_request("Missing link_id"),
1078 };
1079 let link_id: [u8; 16] = match hex_to_array(lid_str) {
1080 Some(h) => h,
1081 None => return HttpResponse::bad_request("Invalid link_id"),
1082 };
1083
1084 with_active_node(node, |n| match n.propose_direct_connect(link_id) {
1085 Ok(()) => HttpResponse::ok(json!({"status": "proposed"})),
1086 Err(_) => HttpResponse::internal_error("Direct connect proposal failed"),
1087 })
1088}
1089
1090fn handle_post_clear_announce_queues(node: &NodeHandle) -> HttpResponse {
1091 with_node(node, |n| match n.query(QueryRequest::DropAnnounceQueues) {
1092 Ok(QueryResponse::DropAnnounceQueues) => HttpResponse::ok(json!({"status": "ok"})),
1093 _ => HttpResponse::internal_error("Query failed"),
1094 })
1095}
1096
1097fn handle_backbone_peers(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1100 let params = parse_query(&req.path);
1101 let interface_name = params.get("interface").map(|s| s.to_string());
1102 with_node(node, |n| {
1103 match n.query(QueryRequest::BackbonePeerState { interface_name }) {
1104 Ok(QueryResponse::BackbonePeerState(entries)) => {
1105 let peers: Vec<Value> = entries
1106 .iter()
1107 .map(|e| {
1108 json!({
1109 "interface": e.interface_name,
1110 "ip": e.peer_ip.to_string(),
1111 "connected_count": e.connected_count,
1112 "blacklisted_remaining_secs": e.blacklisted_remaining_secs,
1113 "blacklist_reason": e.blacklist_reason,
1114 "reject_count": e.reject_count,
1115 })
1116 })
1117 .collect();
1118 HttpResponse::ok(json!({ "peers": peers }))
1119 }
1120 _ => HttpResponse::internal_error("Query failed"),
1121 }
1122 })
1123}
1124
1125fn handle_backbone_blacklist(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1126 let body: Value = match serde_json::from_slice(&req.body) {
1127 Ok(v) => v,
1128 Err(_) => return HttpResponse::bad_request("Invalid JSON body"),
1129 };
1130 let interface_name = match body.get("interface").and_then(|v| v.as_str()) {
1131 Some(s) => s.to_string(),
1132 None => return HttpResponse::bad_request("Missing 'interface' field"),
1133 };
1134 let ip = match body.get("ip").and_then(|v| v.as_str()) {
1135 Some(s) => match s.parse::<std::net::IpAddr>() {
1136 Ok(addr) => addr,
1137 Err(_) => return HttpResponse::bad_request("Invalid IP address"),
1138 },
1139 None => return HttpResponse::bad_request("Missing 'ip' field"),
1140 };
1141 let duration_secs = match body.get("duration_secs").and_then(|v| v.as_u64()) {
1142 Some(d) => d,
1143 None => return HttpResponse::bad_request("Missing 'duration_secs' field"),
1144 };
1145 let reason = body
1146 .get("reason")
1147 .and_then(|v| v.as_str())
1148 .unwrap_or("sentinel blacklist")
1149 .to_string();
1150 let penalty_level = body
1151 .get("penalty_level")
1152 .and_then(|v| v.as_u64())
1153 .unwrap_or(0)
1154 .min(u8::MAX as u64) as u8;
1155 with_node(node, |n| {
1156 match n.query(QueryRequest::BlacklistBackbonePeer {
1157 interface_name,
1158 peer_ip: ip,
1159 duration: std::time::Duration::from_secs(duration_secs),
1160 reason,
1161 penalty_level,
1162 }) {
1163 Ok(QueryResponse::BlacklistBackbonePeer(true)) => {
1164 HttpResponse::ok(json!({"status": "ok"}))
1165 }
1166 Ok(QueryResponse::BlacklistBackbonePeer(false)) => HttpResponse::not_found(),
1167 _ => HttpResponse::internal_error("Query failed"),
1168 }
1169 })
1170}
1171
1172fn handle_list_hooks(node: &NodeHandle) -> HttpResponse {
1175 with_node(node, |n| match n.list_hooks() {
1176 Ok(hooks) => {
1177 let list: Vec<Value> = hooks
1178 .iter()
1179 .map(|h| {
1180 json!({
1181 "name": h.name,
1182 "attach_point": h.attach_point,
1183 "type": h.hook_type,
1184 "priority": h.priority,
1185 "enabled": h.enabled,
1186 "consecutive_traps": h.consecutive_traps,
1187 })
1188 })
1189 .collect();
1190 HttpResponse::ok(json!({"hooks": list}))
1191 }
1192 Err(_) => HttpResponse::internal_error("Query failed"),
1193 })
1194}
1195
1196fn handle_load_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1202 let body = match parse_json_body(req) {
1203 Ok(v) => v,
1204 Err(r) => return r,
1205 };
1206
1207 let name = match body["name"].as_str() {
1208 Some(s) => s.to_string(),
1209 None => return HttpResponse::bad_request("Missing name"),
1210 };
1211 let path = match body["path"].as_str() {
1212 Some(s) => s,
1213 None => return HttpResponse::bad_request("Missing path"),
1214 };
1215 let attach_point = match body["attach_point"].as_str() {
1216 Some(s) => s.to_string(),
1217 None => return HttpResponse::bad_request("Missing attach_point"),
1218 };
1219 let priority = body["priority"].as_i64().unwrap_or(0) as i32;
1220 let hook_type = body["type"]
1221 .as_str()
1222 .unwrap_or(DEFAULT_HOOK_TYPE)
1223 .to_string();
1224
1225 if hook_type == "wasm" {
1226 let wasm_bytes = match std::fs::read(path) {
1227 Ok(b) => b,
1228 Err(e) => {
1229 return HttpResponse::bad_request(&format!("Failed to read WASM file: {}", e));
1230 }
1231 };
1232
1233 with_node(node, |n| {
1234 match n.load_hook(name, wasm_bytes, attach_point, priority) {
1235 Ok(Ok(())) => HttpResponse::ok(json!({"status": "loaded"})),
1236 Ok(Err(e)) => HttpResponse::bad_request(&e),
1237 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1238 }
1239 })
1240 } else if hook_type == "builtin" {
1241 let builtin_id = body["builtin_id"]
1242 .as_str()
1243 .or_else(|| body["id"].as_str())
1244 .unwrap_or(path)
1245 .to_string();
1246 with_node(node, |n| {
1247 match n.load_builtin_hook(name, builtin_id, attach_point, priority) {
1248 Ok(Ok(())) => HttpResponse::ok(json!({"status": "loaded"})),
1249 Ok(Err(e)) => HttpResponse::bad_request(&e),
1250 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1251 }
1252 })
1253 } else {
1254 with_node(node, |n| {
1255 match n.load_hook_file(name, path.to_string(), hook_type, attach_point, priority) {
1256 Ok(Ok(())) => HttpResponse::ok(json!({"status": "loaded"})),
1257 Ok(Err(e)) => HttpResponse::bad_request(&e),
1258 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1259 }
1260 })
1261 }
1262}
1263
1264fn handle_unload_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1265 let body = match parse_json_body(req) {
1266 Ok(v) => v,
1267 Err(r) => return r,
1268 };
1269
1270 let name = match body["name"].as_str() {
1271 Some(s) => s.to_string(),
1272 None => return HttpResponse::bad_request("Missing name"),
1273 };
1274 let attach_point = match body["attach_point"].as_str() {
1275 Some(s) => s.to_string(),
1276 None => return HttpResponse::bad_request("Missing attach_point"),
1277 };
1278
1279 with_node(node, |n| match n.unload_hook(name, attach_point) {
1280 Ok(Ok(())) => HttpResponse::ok(json!({"status": "unloaded"})),
1281 Ok(Err(e)) => HttpResponse::bad_request(&e),
1282 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1283 })
1284}
1285
1286fn handle_reload_hook(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1287 let body = match parse_json_body(req) {
1288 Ok(v) => v,
1289 Err(r) => return r,
1290 };
1291
1292 let name = match body["name"].as_str() {
1293 Some(s) => s.to_string(),
1294 None => return HttpResponse::bad_request("Missing name"),
1295 };
1296 let path = match body["path"].as_str() {
1297 Some(s) => s,
1298 None => return HttpResponse::bad_request("Missing path"),
1299 };
1300 let attach_point = match body["attach_point"].as_str() {
1301 Some(s) => s.to_string(),
1302 None => return HttpResponse::bad_request("Missing attach_point"),
1303 };
1304 let hook_type = body["type"]
1305 .as_str()
1306 .unwrap_or(DEFAULT_HOOK_TYPE)
1307 .to_string();
1308
1309 if hook_type == "wasm" {
1310 let wasm_bytes = match std::fs::read(path) {
1311 Ok(b) => b,
1312 Err(e) => {
1313 return HttpResponse::bad_request(&format!("Failed to read WASM file: {}", e));
1314 }
1315 };
1316
1317 with_node(node, |n| {
1318 match n.reload_hook(name, attach_point, wasm_bytes) {
1319 Ok(Ok(())) => HttpResponse::ok(json!({"status": "reloaded"})),
1320 Ok(Err(e)) => HttpResponse::bad_request(&e),
1321 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1322 }
1323 })
1324 } else if hook_type == "builtin" {
1325 let builtin_id = body["builtin_id"]
1326 .as_str()
1327 .or_else(|| body["id"].as_str())
1328 .unwrap_or(path)
1329 .to_string();
1330 with_node(node, |n| {
1331 match n.reload_builtin_hook(name, attach_point, builtin_id) {
1332 Ok(Ok(())) => HttpResponse::ok(json!({"status": "reloaded"})),
1333 Ok(Err(e)) => HttpResponse::bad_request(&e),
1334 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1335 }
1336 })
1337 } else {
1338 with_node(node, |n| {
1339 match n.reload_hook_file(name, attach_point, path.to_string(), hook_type) {
1340 Ok(Ok(())) => HttpResponse::ok(json!({"status": "reloaded"})),
1341 Ok(Err(e)) => HttpResponse::bad_request(&e),
1342 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1343 }
1344 })
1345 }
1346}
1347
1348fn handle_set_hook_enabled(req: &HttpRequest, node: &NodeHandle, enabled: bool) -> HttpResponse {
1349 let body = match parse_json_body(req) {
1350 Ok(v) => v,
1351 Err(r) => return r,
1352 };
1353
1354 let name = match body["name"].as_str() {
1355 Some(s) => s.to_string(),
1356 None => return HttpResponse::bad_request("Missing name"),
1357 };
1358 let attach_point = match body["attach_point"].as_str() {
1359 Some(s) => s.to_string(),
1360 None => return HttpResponse::bad_request("Missing attach_point"),
1361 };
1362
1363 with_node(node, |n| {
1364 match n.set_hook_enabled(name, attach_point, enabled) {
1365 Ok(Ok(())) => HttpResponse::ok(json!({
1366 "status": if enabled { "enabled" } else { "disabled" }
1367 })),
1368 Ok(Err(e)) => HttpResponse::bad_request(&e),
1369 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1370 }
1371 })
1372}
1373
1374fn handle_set_hook_priority(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
1375 let body = match parse_json_body(req) {
1376 Ok(v) => v,
1377 Err(r) => return r,
1378 };
1379
1380 let name = match body["name"].as_str() {
1381 Some(s) => s.to_string(),
1382 None => return HttpResponse::bad_request("Missing name"),
1383 };
1384 let attach_point = match body["attach_point"].as_str() {
1385 Some(s) => s.to_string(),
1386 None => return HttpResponse::bad_request("Missing attach_point"),
1387 };
1388 let priority = match body["priority"].as_i64() {
1389 Some(v) => v as i32,
1390 None => return HttpResponse::bad_request("Missing priority"),
1391 };
1392
1393 with_node(node, |n| {
1394 match n.set_hook_priority(name, attach_point, priority) {
1395 Ok(Ok(())) => HttpResponse::ok(json!({"status": "priority_updated"})),
1396 Ok(Err(e)) => HttpResponse::bad_request(&e),
1397 Err(_) => HttpResponse::internal_error("Driver unavailable"),
1398 }
1399 })
1400}
1401
1402fn format_dest_name(app_name: &str, aspects: &[&str]) -> String {
1405 if aspects.is_empty() {
1406 app_name.to_string()
1407 } else {
1408 format!("{}.{}", app_name, aspects.join("."))
1409 }
1410}
1411
1412fn parse_proof_strategy(body: &Value) -> ProofStrategy {
1413 match body["proof_strategy"].as_str() {
1414 Some("all") => ProofStrategy::ProveAll,
1415 Some("app") => ProofStrategy::ProveApp,
1416 _ => ProofStrategy::ProveNone,
1417 }
1418}