Skip to main content

hatchery_node/
door.rs

1//! Local door for agents on this node: mail and task operations without ever touching a key.
2//!
3//! The node holds its identity key; agents (other users, other processes) do not. This loopback
4//! listener accepts `POST /door {"op","payload"}` from them, signs the call with the node identity
5//! and forwards it to C2, which applies the status policy. A node that is neutral, suspended or
6//! revoked simply gets "refused" back. The surface is exactly the `mail.*` and `task.*` operations.
7
8use hatchery_build::http;
9use hatchery_enroll::call::capability_for;
10use hatchery_enroll::NodeIdentity;
11use serde_json::{json, Value};
12use std::net::SocketAddr;
13use std::sync::Arc;
14use tokio::net::TcpListener;
15
16/// `session.wake` (handled here, never forwarded): `{"token","nick","from","event_id","body"}`,
17/// the letter event m4a's `M4A_LETTER_HOOK` emits plus the node's wake token.
18pub async fn serve(listener: TcpListener, c2_enroll: SocketAddr, identity: Arc<NodeIdentity>, node_id: String, wake: Option<crate::server::node_wake::NodeWake>) {
19    loop {
20        let Ok((mut stream, peer)) = listener.accept().await else { continue };
21        if !peer.ip().is_loopback() {
22            continue;
23        }
24        let (identity, node_id, wake) = (Arc::clone(&identity), node_id.clone(), wake.clone());
25        tokio::spawn(async move {
26            let Ok(Ok((method, path, _, body))) = tokio::time::timeout(std::time::Duration::from_secs(5), http::read_request(&mut stream, 64 * 1024)).await else {
27                let _ = http::respond(&mut stream, 400, br#"{"error":"bad request"}"#).await;
28                return;
29            };
30            if method != "POST" || path != "/door" {
31                let _ = http::respond(&mut stream, 404, br#"{"error":"not found"}"#).await;
32                return;
33            }
34            let Ok(request) = serde_json::from_slice::<Value>(&body) else {
35                let _ = http::respond(&mut stream, 400, br#"{"error":"malformed"}"#).await;
36                return;
37            };
38            let op = request.get("op").and_then(Value::as_str).unwrap_or_default();
39            if op == "session.wake" {
40                let p = request.get("payload").cloned().unwrap_or_else(|| json!({}));
41                let field = |k: &str| p.get(k).and_then(Value::as_str).unwrap_or_default().to_owned();
42                let (status, reply) = match &wake {
43                    None => (404, json!({"error":"letter wake is off on this node"})),
44                    Some(w) if !w.authorized(&field("token")) => (403, json!({"error":"refused"})),
45                    Some(w) => {
46                        let letter = crate::server::node_wake::NodeLetter { nick: field("nick"), from: field("from"), event_id: field("event_id"), body: field("body") };
47                        match w.wake(letter).await {
48                            Ok(done) => (200, json!({"ok": done})),
49                            Err(why) => (409, json!({"error": why})),
50                        }
51                    }
52                };
53                let _ = http::respond(&mut stream, status, reply.to_string().as_bytes()).await;
54                return;
55            }
56            if capability_for(op).is_none() {
57                let _ = http::respond(&mut stream, 404, br#"{"error":"unknown operation"}"#).await;
58                return;
59            }
60            let payload = request.get("payload").cloned().unwrap_or_else(|| json!({}));
61            let (status, reply) = match crate::enroll_client::node_call(c2_enroll, &identity, &node_id, op, &payload).await {
62                Ok(value) => (200, value.to_string()),
63                Err(crate::enroll_client::EnrollClientError::Refused) => (403, json!({"error":"refused"}).to_string()),
64                Err(_) => (502, json!({"error":"unavailable"}).to_string()),
65            };
66            let _ = http::respond(&mut stream, status, reply.as_bytes()).await;
67        });
68    }
69}