1use actix_web::http::StatusCode;
35use actix_web::web;
36use actix_web::HttpResponse;
37use actix_web::{dev::Payload, FromRequest, HttpRequest};
38use serde::de::DeserializeOwned;
39use serde::Serialize;
40use subtle::ConstantTimeEq;
41use uuid::Uuid;
42use zeroize::Zeroize;
43
44use crate::security::jwt::Claims;
45use crate::security::ratelimit::RateLimitError;
46use crate::AppState;
47use std::future::{ready, Ready};
48use std::net::IpAddr;
49
50pub mod alerts;
53pub mod auth;
54pub mod behavior;
55pub mod device;
56pub mod geo;
57pub mod network;
58pub mod sensors;
59pub mod smart_access;
60pub mod weather;
61
62#[derive(Serialize)]
63struct ApiErrorBody {
64 code: &'static str,
65 message: &'static str,
66 request_id: Option<String>,
67}
68
69pub fn api_error(status: StatusCode, code: &'static str, message: &'static str) -> HttpResponse {
70 HttpResponse::build(status).json(ApiErrorBody {
71 code,
72 message,
73 request_id: None,
74 })
75}
76
77pub fn api_error_with_request_id(
78 status: StatusCode,
79 code: &'static str,
80 message: &'static str,
81 request_id: &str,
82) -> HttpResponse {
83 HttpResponse::build(status)
84 .insert_header(("X-Request-ID", request_id.to_string()))
85 .json(ApiErrorBody {
86 code,
87 message,
88 request_id: Some(request_id.to_string()),
89 })
90}
91
92pub fn parse_json_payload<T: DeserializeOwned>(
93 payload_bytes: &web::Bytes,
94) -> Result<T, HttpResponse> {
95 serde_json::from_slice(payload_bytes).map_err(|_| {
96 api_error(
97 StatusCode::BAD_REQUEST,
98 "INVALID_JSON_PAYLOAD",
99 "Malformed JSON payload",
100 )
101 })
102}
103
104pub fn ok_json_with_trace<T: Serialize>(req: &HttpRequest, data: T) -> HttpResponse {
105 HttpResponse::Ok().json(serde_json::json!({
106 "trace_id": request_id(req),
107 "data": data,
108 }))
109}
110
111pub fn client_ip(req: &HttpRequest) -> IpAddr {
112 req.headers()
113 .get("X-Forwarded-For")
114 .and_then(|hv| hv.to_str().ok())
115 .and_then(|v| v.split(',').next())
116 .and_then(|s| s.trim().parse::<IpAddr>().ok())
117 .or_else(|| req.peer_addr().map(|a| a.ip()))
118 .unwrap_or(IpAddr::from([0, 0, 0, 0]))
119}
120
121pub fn request_id(req: &HttpRequest) -> String {
122 req.headers()
123 .get("X-Request-ID")
124 .and_then(|hv| hv.to_str().ok())
125 .map(str::trim)
126 .filter(|v| !v.is_empty() && v.len() <= 128)
127 .map(ToString::to_string)
128 .unwrap_or_else(|| Uuid::new_v4().to_string())
129}
130
131fn log_security_event(req_id: &str, ip: IpAddr, path: &str, code: &str, detail: &str) {
132 eprintln!(
133 "security_event code={} request_id={} ip={} path={} detail={}",
134 code, req_id, ip, path, detail
135 );
136}
137
138pub struct BearerToken(pub String);
141
142impl FromRequest for BearerToken {
143 type Error = actix_web::Error;
144 type Future = Ready<Result<Self, Self::Error>>;
145
146 fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
147 let token = req
148 .headers()
149 .get("Authorization")
150 .and_then(|hv| hv.to_str().ok())
151 .and_then(|s| s.strip_prefix("Bearer "))
152 .map(str::trim)
153 .filter(|s| !s.is_empty())
154 .map(ToString::to_string)
155 .unwrap_or_default();
156 ready(Ok(Self(token)))
157 }
158}
159
160pub async fn authorize_request(
161 app_state: &web::Data<AppState>,
162 req: &HttpRequest,
163 bearer: &BearerToken,
164 payload_bytes: &web::Bytes,
165) -> Result<Claims, HttpResponse> {
166 let ip = client_ip(req);
167 let req_id = request_id(req);
168 let path = req.path();
169
170 match app_state.rate_limiter.check(ip).await {
171 Ok(()) => {}
172 Err(RateLimitError::Blacklisted) => {
173 log_security_event(
174 &req_id,
175 ip,
176 path,
177 "IP_BLACKLISTED",
178 "request rejected by blacklist",
179 );
180 return Err(api_error_with_request_id(
181 StatusCode::FORBIDDEN,
182 "IP_BLACKLISTED",
183 "Client IP is blocked",
184 &req_id,
185 ));
186 }
187 Err(RateLimitError::LimitExceeded) => {
188 let retry_after = app_state.rate_limiter.retry_after_seconds(ip).await;
189 log_security_event(
190 &req_id,
191 ip,
192 path,
193 "RATE_LIMIT_EXCEEDED",
194 "request rejected by rate limit",
195 );
196 return Err(HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
197 .insert_header(("Retry-After", retry_after.to_string()))
198 .insert_header(("X-Request-ID", req_id.clone()))
199 .json(ApiErrorBody {
200 code: "RATE_LIMIT_EXCEEDED",
201 message: "Too many requests",
202 request_id: Some(req_id.clone()),
203 }));
204 }
205 }
206
207 if let Some(expected_api_key) = &app_state.api_key {
208 let provided = req
209 .headers()
210 .get("X-API-Key")
211 .and_then(|hv| hv.to_str().ok())
212 .map(str::trim)
213 .unwrap_or_default();
214
215 if provided.is_empty() {
216 log_security_event(
217 &req_id,
218 ip,
219 path,
220 "MISSING_API_KEY",
221 "request missing X-API-Key header",
222 );
223 return Err(api_error_with_request_id(
224 StatusCode::UNAUTHORIZED,
225 "MISSING_API_KEY",
226 "Missing API key",
227 &req_id,
228 ));
229 }
230
231 if provided
232 .as_bytes()
233 .ct_eq(expected_api_key.expose().as_bytes())
234 .unwrap_u8()
235 == 0
236 {
237 log_security_event(
238 &req_id,
239 ip,
240 path,
241 "INVALID_API_KEY",
242 "X-API-Key validation failed",
243 );
244 return Err(api_error_with_request_id(
245 StatusCode::UNAUTHORIZED,
246 "INVALID_API_KEY",
247 "Invalid API key",
248 &req_id,
249 ));
250 }
251 }
252
253 let mut token = bearer.0.clone();
254 if token.is_empty() {
255 log_security_event(
256 &req_id,
257 ip,
258 path,
259 "MISSING_BEARER_TOKEN",
260 "missing Authorization bearer token",
261 );
262 return Err(api_error_with_request_id(
263 StatusCode::UNAUTHORIZED,
264 "MISSING_BEARER_TOKEN",
265 "Missing Authorization token",
266 &req_id,
267 ));
268 }
269
270 let claims = app_state.jwt_manager.decode_token(&token).map_err(|_| {
271 log_security_event(
272 &req_id,
273 ip,
274 path,
275 "INVALID_OR_EXPIRED_TOKEN",
276 "JWT token validation failed",
277 );
278 api_error_with_request_id(
279 StatusCode::UNAUTHORIZED,
280 "INVALID_OR_EXPIRED_TOKEN",
281 "Invalid or expired token",
282 &req_id,
283 )
284 });
285 token.zeroize();
286 let claims = claims?;
287
288 let ua = req
289 .headers()
290 .get("User-Agent")
291 .and_then(|h| h.to_str().ok());
292 let ai_decision = app_state
293 .ai_guard
294 .evaluate_request(ip, req.path(), ua, payload_bytes)
295 .await;
296 if ai_decision.blocked {
297 let detail = format!(
298 "ai_block score={} reasons={}",
299 ai_decision.assessment.score,
300 ai_decision.assessment.reasons.join("|")
301 );
302 log_security_event(&req_id, ip, path, "AI_RISK_BLOCKED", &detail);
303 let mut builder = HttpResponse::build(StatusCode::FORBIDDEN);
304 builder.insert_header(("X-Request-ID", req_id.clone()));
305 if let Some(retry_after) = ai_decision.retry_after_seconds {
306 builder.insert_header(("Retry-After", retry_after.to_string()));
307 }
308 return Err(builder.json(serde_json::json!({
309 "code": "AI_RISK_BLOCKED",
310 "message": "Request blocked by adaptive AI security policy",
311 "request_id": req_id,
312 "risk_score": ai_decision.assessment.score,
313 "reasons": ai_decision.assessment.reasons,
314 "retry_after_seconds": ai_decision.retry_after_seconds,
315 })));
316 }
317
318 Ok(claims)
319}
320
321pub fn config(cfg: &mut web::ServiceConfig) {
324 cfg.service(
325 web::scope("/api")
326 .service(auth::get_user)
327 .service(geo::resolve_geo)
328 .service(device::resolve_device)
329 .service(behavior::analyze_behavior)
330 .service(sensors::analyze_sensors)
331 .service(network::analyze_network)
332 .service(alerts::trigger_alert)
333 .service(weather::weather_summary)
334 .service(smart_access::smart_access_verify),
335 );
336}