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