foxy-io 0.3.10

A configuration-driven and hyper-extensible HTTP proxy library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Routing DSL – *predicates* & helper logic.
//!
//! A [`PredicateRouter`] owns an ordered vector of [`Route`]s.  
//! The first route whose **predicate stack** returns `true` wins and its
//! filter-chain is executed.
//!
//! ### Built-in predicates
//! | type              | configuration key     | example                              |
//! |-------------------|-----------------------|--------------------------------------|
//! | `MethodPredicate` | `method`              | `"GET"`                              |
//! | `PathPredicate`   | `path` (regex)        | `"/api/v1/.*"`                       |
//! | `HeaderPredicate` | `header.<NAME>`       | `"X-Request-Id" = "^[0-9a-f-]{36}$"` |
//! | `QueryPredicate`  | `query.<NAME>`        | `"tenant"` = `"acme-corp"`           |

mod predicates;

#[cfg(test)]
#[path = "../../tests/unit/router/tests.rs"]
mod tests;

pub use predicates::*;

use async_trait::async_trait;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock as StdRwLock;
use tokio::sync::RwLock;

use crate::config::Config;
use crate::core::{ProxyError, ProxyRequest, Route};
use crate::{FilterFactory, debug_fmt, error_fmt, trace_fmt, warn_fmt};

/// Configuration for a route.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteConfig {
    /// The ID of the route (for logging and reference)
    pub id: String,
    /// The base URL of the target
    pub target: String,
    /// Filters to apply to this route
    #[serde(default)]
    pub filters: Vec<FilterConfig>,
    /// Priority of the route (higher means higher priority)
    #[serde(default = "default_priority")]
    pub priority: i32,
    /// Predicates for this route
    #[serde(default)]
    pub predicates: Vec<PredicateConfig>,
}

/// Configuration for a filter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
    /// The type of filter
    #[serde(rename = "type")]
    pub type_: String,
    /// The configuration for the filter
    pub config: serde_json::Value,
}

fn default_priority() -> i32 {
    0
}

/// Configuration for a predicate.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredicateConfig {
    /// The type of predicate
    pub type_: String,
    /// The configuration for the predicate
    pub config: serde_json::Value,
}

/// A predicate that determines if a request matches a route.
#[async_trait]
pub trait Predicate: Send + Sync + std::fmt::Debug {
    /// Check if the request matches this predicate.
    async fn matches(&self, request: &ProxyRequest) -> bool;

    /// Get the predicate type.
    fn predicate_type(&self) -> &str;
}

/// Constructor signature every dynamic predicate must implement
pub type PredicateConstructor = fn(serde_json::Value) -> Result<Arc<dyn Predicate>, ProxyError>;

/// Global registry – `register_predicate()` writes to it,
/// `PredicateFactory::create_predicate()` reads from it.
static PREDICATE_REGISTRY: Lazy<StdRwLock<HashMap<String, PredicateConstructor>>> =
    Lazy::new(|| StdRwLock::new(HashMap::new()));

/// Register a predicate under a unique name.
pub fn register_predicate(name: &str, ctor: PredicateConstructor) {
    PREDICATE_REGISTRY
        .write()
        .expect("PREDICATE_REGISTRY poisoned")
        .insert(name.to_string(), ctor);
}

/// Internal helper – fetch a constructor if somebody registered one.
fn get_registered_predicate(name: &str) -> Option<PredicateConstructor> {
    PREDICATE_REGISTRY
        .read()
        .expect("PREDICATE_REGISTRY poisoned")
        .get(name)
        .copied()
}

/// The predicable router implementation.
#[derive(Debug)]
pub struct PredicateRouter {
    /// Routes managed by this router, stored by ID
    routes: RwLock<HashMap<String, RouteWithPredicates>>,
    /// Sorted list of routes by priority
    sorted_routes: RwLock<Vec<RouteWithPredicates>>,
    /// Configuration for the router
    config: Arc<Config>,
}

/// A route with associated predicates.
#[derive(Debug, Clone)]
struct RouteWithPredicates {
    /// The route
    route: Route,
    /// Predicates that must match for this route
    predicates: Vec<Arc<dyn Predicate>>,
    /// Priority of the route
    priority: i32,
}

impl PredicateRouter {
    /// Create a new predicate router with the given configuration.
    pub async fn new(config: Arc<Config>) -> Result<Self, ProxyError> {
        let router = Self {
            routes: RwLock::new(HashMap::new()),
            sorted_routes: RwLock::new(Vec::new()),
            config,
        };

        // Initialize routes from configuration
        router.load_routes_from_config().await?;

        Ok(router)
    }

    /// Load routes from the configuration.
    async fn load_routes_from_config(&self) -> Result<(), ProxyError> {
        // Get routes from configuration
        let route_configs: Option<Vec<RouteConfig>> = self.config.get("routes")?;

        if let Some(route_configs) = route_configs {
            // Add each route
            for route_config in route_configs {
                // Create predicates for this route
                let mut predicates = Vec::new();
                for predicate_config in &route_config.predicates {
                    let predicate = PredicateFactory::create_predicate(
                        &predicate_config.type_,
                        predicate_config.config.clone(),
                    )?;
                    predicates.push(predicate);
                }

                // Create filters for this route
                let mut filters = Vec::new();
                for filter_config in &route_config.filters {
                    let filter = FilterFactory::create_filter(
                        &filter_config.type_,
                        filter_config.config.clone(),
                    )?;
                    filters.push(filter);
                }

                // Find the first path predicate to use as the route pattern
                let path_pattern = route_config
                    .predicates
                    .iter()
                    .find(|p| p.type_ == "path")
                    .map(|p| {
                        p.config
                            .get("pattern")
                            .and_then(|v| v.as_str())
                            .unwrap_or("/*")
                    })
                    .unwrap_or("/*")
                    .to_string();

                let route = Route {
                    id: route_config.id.clone(),
                    target_base_url: route_config.target.clone(),
                    path_pattern,
                    filters: if filters.is_empty() {
                        None
                    } else {
                        Some(filters)
                    },
                };

                // Add the route with its predicates
                self.add_route_with_predicates(route, predicates, route_config.priority)
                    .await?;
            }
        }

        Ok(())
    }

    /// Add a route with predicates and priority.
    async fn add_route_with_predicates(
        &self,
        route: Route,
        predicates: Vec<Arc<dyn Predicate>>,
        priority: i32,
    ) -> Result<(), ProxyError> {
        let route_with_predicates = RouteWithPredicates {
            route: route.clone(),
            predicates,
            priority,
        };

        // Store the route
        {
            let mut routes = self.routes.write().await;
            routes.insert(route.id.clone(), route_with_predicates.clone());
        }

        // Update sorted routes
        {
            let mut sorted_routes = self.sorted_routes.write().await;
            sorted_routes.push(route_with_predicates);

            // Sort by priority (higher first)
            sorted_routes.sort_by(|a, b| b.priority.cmp(&a.priority));
        }

        Ok(())
    }
}

#[async_trait]
impl crate::core::Router for PredicateRouter {
    async fn route(&self, request: &ProxyRequest) -> Result<Route, ProxyError> {
        // Find the first route where all predicates match
        let sorted_routes = self.sorted_routes.read().await;
        trace_fmt!(
            "Router",
            "Routing request {} {} against {} routes",
            request.method,
            request.path,
            sorted_routes.len()
        );

        for route_with_predicates in sorted_routes.iter() {
            // Check all predicates for this route
            let mut all_match = true;
            let route_id = &route_with_predicates.route.id;

            trace_fmt!(
                "Router",
                "Checking route '{}' with {} predicates",
                route_id,
                route_with_predicates.predicates.len()
            );

            for predicate in &route_with_predicates.predicates {
                let predicate_type = predicate.predicate_type();
                let matches = predicate.matches(request).await;

                trace_fmt!(
                    "Router",
                    "  Predicate '{}' for route '{}': {}",
                    predicate_type,
                    route_id,
                    if matches { "match" } else { "no match" }
                );

                if !matches {
                    all_match = false;
                    break;
                }
            }

            // If all predicates match, use this route
            if all_match {
                debug_fmt!(
                    "Router",
                    "Route '{}' matched request {} {}",
                    route_id,
                    request.method,
                    request.path
                );
                return Ok(route_with_predicates.route.clone());
            }
        }

        // No route matched
        let err = ProxyError::RoutingError(format!(
            "No route matched the request: {} {}",
            request.method, request.path
        ));
        warn_fmt!("Router", "{}", err);
        Err(err)
    }

    async fn get_routes(&self) -> Vec<Route> {
        let routes = self.routes.read().await;
        routes.values().map(|r| r.route.clone()).collect()
    }

    async fn add_route(&self, route: Route) -> Result<(), ProxyError> {
        // Create an empty predicate list - this is not the recommended way to add routes
        // Users should use add_route_with_predicates instead
        self.add_route_with_predicates(route, Vec::new(), 0).await
    }

    async fn remove_route(&self, route_id: &str) -> Result<(), ProxyError> {
        // Remove the route from the routes map
        {
            let mut routes = self.routes.write().await;
            if routes.remove(route_id).is_none() {
                return Err(ProxyError::RoutingError(format!(
                    "Route not found: {route_id}"
                )));
            }
        }

        // Remove the route from the sorted list
        {
            let mut sorted_routes = self.sorted_routes.write().await;
            sorted_routes.retain(|r| r.route.id != route_id);
        }

        Ok(())
    }
}

/// Factory for creating predicates based on configuration.
#[derive(Debug)]
pub struct PredicateFactory;

impl PredicateFactory {
    /// Create a predicate based on the predicate type and configuration.
    pub fn create_predicate(
        predicate_type: &str,
        config: serde_json::Value,
    ) -> Result<Arc<dyn Predicate>, ProxyError> {
        debug_fmt!(
            "Router",
            "Creating predicate of type '{}' with config: {}",
            predicate_type,
            config
        );

        // See if we've got an external predicate registered of that name
        if let Some(ctor) = get_registered_predicate(predicate_type) {
            return ctor(config);
        }

        match predicate_type {
            "path" => {
                let path_config: PathPredicateConfig =
                    serde_json::from_value(config).map_err(|e| {
                        let err =
                            ProxyError::RoutingError(format!("Invalid path predicate config: {e}"));
                        error_fmt!("Router", "{}", err);
                        err
                    })?;

                match PathPredicate::new(path_config) {
                    Ok(predicate) => Ok(Arc::new(predicate)),
                    Err(error) => Err(error),
                }
            }
            "method" => {
                let method_config: MethodPredicateConfig =
                    serde_json::from_value(config).map_err(|e| {
                        let err = ProxyError::RoutingError(format!(
                            "Invalid method predicate config: {e}"
                        ));
                        error_fmt!("Router", "{}", err);
                        err
                    })?;
                Ok(Arc::new(MethodPredicate::new(method_config)))
            }
            "header" => {
                let header_config: HeaderPredicateConfig =
                    serde_json::from_value(config).map_err(|e| {
                        let err = ProxyError::RoutingError(format!(
                            "Invalid header predicate config: {e}"
                        ));
                        error_fmt!("Router", "{}", err);
                        err
                    })?;
                Ok(Arc::new(HeaderPredicate::new(header_config)))
            }
            "query" => {
                let query_config: QueryPredicateConfig =
                    serde_json::from_value(config).map_err(|e| {
                        let err = ProxyError::RoutingError(format!(
                            "Invalid query predicate config: {e}"
                        ));
                        error_fmt!("Router", "{}", err);
                        err
                    })?;
                Ok(Arc::new(QueryPredicate::new(query_config)))
            }
            _ => {
                let err =
                    ProxyError::RoutingError(format!("Unknown predicate type: {predicate_type}"));
                error_fmt!("Router", "{}", err);
                Err(err)
            }
        }
    }
}