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