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}