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