mockforge_http/middleware/
deceptive_canary.rs

1//! Deceptive Canary Middleware
2//!
3//! Middleware that routes a percentage of team traffic to deceptive deploys.
4
5use axum::extract::Request;
6use axum::http::{HeaderMap, Uri};
7use axum::middleware::Next;
8use axum::response::Response;
9use mockforge_core::deceptive_canary::DeceptiveCanaryRouter;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tracing::debug;
13
14/// Deceptive canary middleware state
15#[derive(Clone)]
16pub struct DeceptiveCanaryState {
17    /// Router for canary routing decisions
18    pub router: Arc<DeceptiveCanaryRouter>,
19}
20
21impl DeceptiveCanaryState {
22    /// Create new deceptive canary state
23    pub fn new(router: DeceptiveCanaryRouter) -> Self {
24        Self {
25            router: Arc::new(router),
26        }
27    }
28}
29
30/// Deceptive canary middleware
31///
32/// Intercepts requests and routes a percentage to deceptive deploy endpoints
33/// based on team identification criteria.
34pub async fn deceptive_canary_middleware(req: Request, next: Next) -> Response {
35    // Extract state from extensions (set by router)
36    let state = req.extensions().get::<DeceptiveCanaryState>().cloned().unwrap_or_else(|| {
37        // Return default state if not found (canary disabled)
38        DeceptiveCanaryState::new(DeceptiveCanaryRouter::default())
39    });
40    // Extract request information
41    let user_agent = req
42        .headers()
43        .get("user-agent")
44        .and_then(|h| h.to_str().ok())
45        .map(|s| s.to_string());
46
47    // Extract IP address from headers or connection info
48    let ip_address = req
49        .extensions()
50        .get::<std::net::SocketAddr>()
51        .map(|addr| addr.ip().to_string())
52        .or_else(|| {
53            req.headers()
54                .get("x-forwarded-for")
55                .or_else(|| req.headers().get("x-real-ip"))
56                .and_then(|h| h.to_str().ok())
57                .map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
58        });
59
60    // Extract headers
61    let mut headers_map = HashMap::new();
62    for (key, value) in req.headers() {
63        let key_str = key.as_str().to_string();
64        if let Ok(value_str) = value.to_str() {
65            headers_map.insert(key_str, value_str.to_string());
66        }
67    }
68
69    // Extract query parameters
70    let mut query_params = HashMap::new();
71    if let Some(query) = req.uri().query() {
72        for pair in query.split('&') {
73            if let Some((key, value)) = pair.split_once('=') {
74                query_params.insert(key.to_string(), value.to_string());
75            }
76        }
77    }
78
79    // Extract user ID from headers (if available)
80    let user_id = req
81        .headers()
82        .get("x-user-id")
83        .or_else(|| req.headers().get("authorization"))
84        .and_then(|h| h.to_str().ok())
85        .map(|s| s.to_string());
86
87    // Check if request should be routed to canary
88    let should_route = state.router.should_route_to_canary(
89        user_agent.as_deref(),
90        ip_address.as_deref(),
91        &headers_map,
92        &query_params,
93        user_id.as_deref(),
94    );
95
96    if should_route {
97        debug!("Routing request to deceptive canary: {} {}", req.method(), req.uri().path());
98
99        // Get deceptive deploy URL from router config
100        let canary_url = &state.router.config().deceptive_deploy_url;
101
102        if !canary_url.is_empty() {
103            // Proxy request to deceptive deploy
104            // For now, we'll just add a header indicating canary routing
105            // Full proxying would require more complex logic
106            let mut response = next.run(req).await;
107            response.headers_mut().insert("X-Deceptive-Canary", "true".parse().unwrap());
108            return response;
109        }
110    }
111
112    // Continue with normal request processing
113    next.run(req).await
114}