enigma_node_registry/
routes.rs

1use std::sync::Arc;
2
3use actix_web::web::{self, Json, ServiceConfig};
4use actix_web::{get, post, HttpRequest, HttpResponse, Responder};
5use enigma_node_types::{
6    CheckUserResponse, NodesPayload, Presence, RegisterResponse, SyncRequest, SyncResponse, UserId,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::config::RegistryConfig;
11use crate::envelope::{EnvelopeCrypto, EnvelopeKeySet, EnvelopePublicKey, IdentityEnvelope};
12use crate::error::{RegistryError, RegistryResult};
13#[cfg(feature = "pow")]
14use crate::pow::PowChallenge;
15use crate::pow::PowManager as PowManagerStub;
16use crate::rate_limit::{RateLimiter, RateScope};
17use crate::store::Store;
18use crate::ttl::current_time_ms;
19
20#[derive(Clone)]
21pub struct AppState {
22    pub store: Arc<Store>,
23    pub keys: EnvelopeKeySet,
24    pub crypto: EnvelopeCrypto,
25    pub rate_limiter: RateLimiter,
26    pub pow: PowManagerStub,
27    pub presence_ttl: u64,
28    pub allow_sync: bool,
29    pub trusted_proxies: Arc<Vec<String>>,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct RegisterEnvelopeRequest {
35    pub handle: String,
36    pub envelope: IdentityEnvelope,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct ResolveRequest {
42    pub handle: String,
43    pub requester_ephemeral_pubkey_hex: String,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct ResolveResponse {
48    pub handle: String,
49    pub envelope: Option<IdentityEnvelope>,
50}
51
52#[derive(Debug, Serialize)]
53pub struct OkResponse {
54    pub ok: bool,
55}
56
57#[derive(Debug, Serialize)]
58pub struct MergedResponse {
59    pub merged: usize,
60}
61
62pub fn configure(_cfg: &RegistryConfig, state: AppState) -> impl FnOnce(&mut ServiceConfig) {
63    #[cfg(feature = "pow")]
64    let pow_enabled = state.pow.enabled();
65    move |service: &mut ServiceConfig| {
66        let data = web::Data::new(state.clone());
67        service
68            .app_data(data.clone())
69            .service(register)
70            .service(resolve)
71            .service(check_user)
72            .service(announce)
73            .service(sync)
74            .service(list_nodes)
75            .service(add_nodes)
76            .service(envelope_pubkey)
77            .service(envelope_pubkeys);
78        #[cfg(feature = "pow")]
79        if pow_enabled {
80            service.service(pow_challenge);
81        }
82    }
83}
84
85#[post("/register")]
86async fn register(
87    state: web::Data<AppState>,
88    req: HttpRequest,
89    payload: Json<RegisterEnvelopeRequest>,
90) -> RegistryResult<impl Responder> {
91    let ip = peer_ip(&req, &state.trusted_proxies);
92    state
93        .rate_limiter
94        .check(
95            &ip.unwrap_or_else(|| "unknown".to_string()),
96            RateScope::Register,
97        )
98        .await?;
99    let handle = parse_user_id(&payload.handle)?;
100    let now_ms = current_time_ms();
101    let key = state
102        .keys
103        .find_by_kid(&payload.envelope.kid, now_ms)
104        .ok_or_else(|| RegistryError::InvalidInput("unknown envelope key".to_string()))?;
105    let identity = state
106        .crypto
107        .decrypt_identity(&payload.envelope, &key, &handle, now_ms)?;
108    if identity.user_id != handle {
109        return Err(RegistryError::InvalidInput(
110            "handle does not match identity".to_string(),
111        ));
112    }
113    state.store.register(identity).await?;
114    Ok(HttpResponse::Ok().json(RegisterResponse { ok: true }))
115}
116
117#[post("/resolve")]
118async fn resolve(
119    state: web::Data<AppState>,
120    req: HttpRequest,
121    payload: Json<ResolveRequest>,
122) -> RegistryResult<impl Responder> {
123    let ip = peer_ip(&req, &state.trusted_proxies);
124    state
125        .rate_limiter
126        .check(
127            &ip.unwrap_or_else(|| "unknown".to_string()),
128            RateScope::Resolve,
129        )
130        .await?;
131    #[cfg(feature = "pow")]
132    {
133        state
134            .pow
135            .verify_header(
136                req.headers()
137                    .get("x-enigma-pow")
138                    .and_then(|v| v.to_str().ok()),
139            )
140            .await?;
141    }
142    let handle = parse_user_id(&payload.handle)?;
143    let requester_pubkey = parse_hex_array::<32>(
144        &payload.requester_ephemeral_pubkey_hex,
145        "requester_ephemeral_pubkey_hex",
146    )?;
147    let identity = state.store.resolve(&handle).await?;
148    let now_ms = current_time_ms();
149    let envelope = if let Some(identity) = identity {
150        let key = state
151            .keys
152            .active_key(now_ms)
153            .ok_or_else(|| RegistryError::Internal)?;
154        Some(state.crypto.encrypt_identity_for_peer(
155            &key,
156            &handle,
157            &identity,
158            requester_pubkey,
159            None,
160            now_ms,
161        )?)
162    } else {
163        None
164    };
165    Ok(HttpResponse::Ok().json(ResolveResponse {
166        handle: payload.handle.clone(),
167        envelope,
168    }))
169}
170
171#[get("/check_user/{handle}")]
172async fn check_user(
173    state: web::Data<AppState>,
174    req: HttpRequest,
175    path: web::Path<String>,
176) -> RegistryResult<impl Responder> {
177    let ip = peer_ip(&req, &state.trusted_proxies);
178    state
179        .rate_limiter
180        .check(
181            &ip.unwrap_or_else(|| "unknown".to_string()),
182            RateScope::CheckUser,
183        )
184        .await?;
185    #[cfg(feature = "pow")]
186    {
187        state
188            .pow
189            .verify_header(
190                req.headers()
191                    .get("x-enigma-pow")
192                    .and_then(|v| v.to_str().ok()),
193            )
194            .await?;
195    }
196    let handle = parse_user_id(&path.into_inner())?;
197    let exists = state.store.check_user(&handle).await?;
198    Ok(HttpResponse::Ok().json(CheckUserResponse { exists }))
199}
200
201#[post("/announce")]
202async fn announce(
203    state: web::Data<AppState>,
204    req: HttpRequest,
205    payload: Json<Presence>,
206) -> RegistryResult<impl Responder> {
207    let ip = peer_ip(&req, &state.trusted_proxies);
208    state
209        .rate_limiter
210        .check(
211            &ip.unwrap_or_else(|| "unknown".to_string()),
212            RateScope::Global,
213        )
214        .await?;
215    state.store.announce(payload.into_inner()).await?;
216    Ok(HttpResponse::Ok().json(OkResponse { ok: true }))
217}
218
219#[post("/sync")]
220async fn sync(
221    state: web::Data<AppState>,
222    req: HttpRequest,
223    payload: Json<SyncRequest>,
224) -> RegistryResult<impl Responder> {
225    if !state.allow_sync {
226        return Err(RegistryError::Unauthorized);
227    }
228    let ip = peer_ip(&req, &state.trusted_proxies);
229    state
230        .rate_limiter
231        .check(
232            &ip.unwrap_or_else(|| "unknown".to_string()),
233            RateScope::Global,
234        )
235        .await?;
236    let merged = state
237        .store
238        .sync_identities(payload.into_inner().identities)
239        .await?;
240    Ok(HttpResponse::Ok().json(SyncResponse { merged }))
241}
242
243#[get("/nodes")]
244async fn list_nodes(state: web::Data<AppState>) -> RegistryResult<impl Responder> {
245    let nodes = state.store.list_nodes().await?;
246    Ok(HttpResponse::Ok().json(NodesPayload { nodes }))
247}
248
249#[post("/nodes")]
250async fn add_nodes(
251    state: web::Data<AppState>,
252    payload: Json<NodesPayload>,
253) -> RegistryResult<impl Responder> {
254    let merged = state.store.add_nodes(payload.into_inner().nodes).await?;
255    Ok(HttpResponse::Ok().json(MergedResponse { merged }))
256}
257
258#[get("/envelope_pubkey")]
259async fn envelope_pubkey(state: web::Data<AppState>) -> RegistryResult<impl Responder> {
260    let now_ms = current_time_ms();
261    let key = state
262        .keys
263        .active_key(now_ms)
264        .ok_or_else(|| RegistryError::Internal)?;
265    let body = EnvelopePublicKey {
266        kid_hex: hex::encode(key.kid),
267        x25519_public_key_hex: hex::encode(key.public),
268        active: key.active,
269        not_after_epoch_ms: key.not_after,
270    };
271    Ok(HttpResponse::Ok().json(body))
272}
273
274#[get("/envelope_pubkeys")]
275async fn envelope_pubkeys(state: web::Data<AppState>) -> RegistryResult<impl Responder> {
276    let body = state.keys.public_keys();
277    Ok(HttpResponse::Ok().json(body))
278}
279
280#[cfg(feature = "pow")]
281#[get("/pow/challenge")]
282async fn pow_challenge(state: web::Data<AppState>) -> RegistryResult<impl Responder> {
283    if !state.pow.enabled() {
284        return Err(RegistryError::FeatureDisabled("pow".to_string()));
285    }
286    let challenge: PowChallenge = state.pow.issue().await?;
287    Ok(HttpResponse::Ok().json(challenge))
288}
289
290fn parse_user_id(input: &str) -> RegistryResult<UserId> {
291    UserId::from_hex(input).map_err(|_| RegistryError::InvalidInput("handle".to_string()))
292}
293
294fn parse_hex_array<const N: usize>(value: &str, field: &str) -> RegistryResult<[u8; N]> {
295    let bytes = hex::decode(value).map_err(|_| RegistryError::InvalidInput(field.to_string()))?;
296    if bytes.len() != N {
297        return Err(RegistryError::InvalidInput(field.to_string()));
298    }
299    let mut out = [0u8; N];
300    out.copy_from_slice(&bytes);
301    Ok(out)
302}
303
304fn peer_ip(req: &HttpRequest, trusted_proxies: &[String]) -> Option<String> {
305    if let Some(header) = req
306        .headers()
307        .get("x-forwarded-for")
308        .and_then(|v| v.to_str().ok())
309    {
310        let parts: Vec<&str> = header.split(',').collect();
311        for part in parts {
312            let candidate = part.trim();
313            if candidate.is_empty() {
314                continue;
315            }
316            if trusted_proxies.iter().any(|p| p == candidate) {
317                continue;
318            }
319            return Some(candidate.to_string());
320        }
321    }
322    req.peer_addr().map(|addr| addr.ip().to_string())
323}