foxy-io 0.3.12

A configuration-driven and hyper-extensible HTTP proxy library
Documentation
# Extension Guide

This guide provides examples for using Foxy's trait-based system to create custom `Filters`, `Predicates`, and `SecurityProviders`. It covers implementing the necessary traits, registering your components, and enabling them through the proxy configuration.

## Custom Filters

Filters allow you to inspect and modify HTTP requests and responses as they pass through the proxy.

### Example: `AddCorrelationIdFilter`

Let's create a filter that adds a unique `X-Correlation-ID` header to every incoming request.

`src/my_filters.rs`:
```rust
use foxy::{Filter, FilterType, ProxyRequest, ProxyError};
use async_trait::async_trait;
use uuid::Uuid;

#[derive(Debug)]
pub struct AddCorrelationIdFilter;

impl AddCorrelationIdFilter {
    // The constructor receives the JSON config block from your foxy.json/yaml.
    // We don't need any config for this simple filter.
    pub fn new(_config: serde_json::Value) -> Result<Self, ProxyError> {
        Ok(Self)
    }
}

#[async_trait]
impl Filter for AddCorrelationIdFilter {
    // This is a pre-filter, so it runs before the request is sent upstream.
    fn filter_type(&self) -> FilterType {
        FilterType::Pre
    }

    // The name used in the configuration file.
    fn name(&self) -> &str {
        "add_correlation_id"
    }

    async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
        let correlation_id = Uuid::new_v4().to_string();
        
        // Add the header to the request.
        request.headers.insert(
            "X-Correlation-ID",
            correlation_id.parse().unwrap(),
        );
        
        println!("Added X-Correlation-ID: {}", correlation_id);

        Ok(request)
    }
}
```

### Registration and Usage

Register your filter in main.rs before building the Foxy instance.

`src/main.rs`:
```rust
use foxy::Foxy;
use foxy::filters::register_filter;
use std::sync::Arc;
// Import your custom filter
mod my_filters;
use my_filters::AddCorrelationIdFilter;

#[tokio::main]
async fn main() {
    // 1. Register the filter
    register_filter("add_correlation_id", |config| {
        Ok(Arc::new(AddCorrelationIdFilter::new(config)?))
    });

    // 2. Build Foxy as usual
    let foxy = Foxy::loader()
        .with_config_file("config.json")
        .build().await
        .unwrap();

    foxy.start().await.unwrap();
}
```

Now, you can use add_correlation_id in your configuration.

`config.json`:
```json
{
  "proxy": {
    "global_filters": [
      {
        "type": "add_correlation_id",
        "config": {}
      }
    ]
  },
  "routes": [
    // ... your routes
  ]
}
```

## Custom Predicates

Predicates are used to create custom routing rules. A route is selected only if all of its predicates match the incoming request.

### Example: UserAgentPredicate

Here's a predicate that matches requests from a specific user agent.

`src/my_predicates.rs`:
```rust
use foxy::{Predicate, ProxyRequest, ProxyError};
use async_trait::async_trait;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct UserAgentPredicateConfig {
    pattern: String,
}

#[derive(Debug)]
pub struct UserAgentPredicate {
    pattern: String,
}

impl UserAgentPredicate {
    pub fn new(config: serde_json::Value) -> Result<Self, ProxyError> {
        let config: UserAgentPredicateConfig = serde_json::from_value(config)
            .map_err(|e| ProxyError::ConfigError(e.to_string()))?;
        Ok(Self { pattern: config.pattern })
    }
}

#[async_trait]
impl Predicate for UserAgentPredicate {
    fn predicate_type(&self) -> &str {
        "user_agent"
    }

    async fn matches(&self, request: &ProxyRequest) -> bool {
        request.headers
            .get("User-Agent")
            .and_then(|v| v.to_str().ok())
            .map_or(false, |ua| ua.contains(&self.pattern))
    }
}
```

### Registration and Usage

`src/main.rs`:
```rust
use foxy::Foxy;
use foxy::router::register_predicate;
use std::sync::Arc;
// Import your custom predicate
mod my_predicates;
use my_predicates::UserAgentPredicate;

#[tokio::main]
async fn main() {
    // 1. Register the predicate
    register_predicate("user_agent", |config| {
        Ok(Arc::new(UserAgentPredicate::new(config)?))
    });

    // 2. Build Foxy
    // ...
}
```

`config.json`:
```json
{
  "routes": [
    {
      "id": "mobile-api-route",
      "target": "http://mobile-backend:8080",
      "priority": 100,
      "predicates": [
        {
          "type": "path",
          "config": { "pattern": "/api/mobile/*" }
        },
        {
          "type": "user_agent",
          "config": {
            "pattern": "FoxyMobileClient"
          }
        }
      ]
    }
  ]
}
```

## Custom Security Providers

Security providers are used to implement custom authentication and authorization logic, such as API key validation, custom JWT schemes, or HMAC signature verification.

### Example: ApiKeyProvider

This provider checks for a valid X-API-Key header.

`src/my_security.rs`:
```rust
use foxy::{SecurityProvider, SecurityStage, ProxyRequest, ProxyError};
use async_trait::async_trait;
use serde::Deserialize;
use std::collections::HashSet;
use std::future::Future;
use std::pin::Pin;

#[derive(Debug, Deserialize)]
struct ApiKeyProviderConfig {
    valid_keys: Vec<String>,
}

#[derive(Debug)]
pub struct ApiKeyProvider {
    valid_keys: HashSet<String>,
}

impl ApiKeyProvider {
    // Security Provider constructors are async because some, like OIDC,
    // may need to perform network requests for discovery.
    pub fn new(config: serde_json::Value) -> Pin<Box<dyn Future<Output = Result<Self, ProxyError>> + Send>> {
        Box::pin(async {
            let config: ApiKeyProviderConfig = serde_json::from_value(config)
                .map_err(|e| ProxyError::ConfigError(e.to_string()))?;

            let valid_keys = config.valid_keys.into_iter().collect();
            Ok(Self { valid_keys })
        })
    }
}

#[async_trait]
impl SecurityProvider for ApiKeyProvider {
    fn name(&self) -> &str {
        "api_key"
    }

    fn stage(&self) -> SecurityStage {
        SecurityStage::Pre
    }

    async fn pre(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
        if let Some(key) = request.headers.get("X-API-Key").and_then(|v| v.to_str().ok()) {
            if self.valid_keys.contains(key) {
                // Key is valid, allow the request to proceed.
                return Ok(request);
            }
        }
        // Key is missing or invalid.
        Err(ProxyError::SecurityError("Invalid or missing API Key".to_string()))
    }
}
```

### Registration and Usage

`src/main.rs`:
```rust
use foxy::Foxy;
use foxy::security::register_security_provider;
use std::sync::Arc;
// Import your custom provider
mod my_security;
use my_security::ApiKeyProvider;

#[tokio::main]
async fn main() {
    // 1. Register the security provider
    register_security_provider("api_key", |config| {
        Box::pin(async {
            let provider = ApiKeyProvider::new(config).await?;
            Ok(Arc::new(provider) as Arc<dyn foxy::SecurityProvider>)
        })
    });
    
    // 2. Build Foxy
    // ...
}
```

`config.json`:
```json
{
  "proxy": {
    "security_chain": [
      {
        "type": "api_key",
        "config": {
          "valid_keys": [
            "secret-key-1",
            "secret-key-2"
          ]
        }
      }
    ]
  },
  "routes": [
    // ... your routes
  ]
}
```