packages feed

keter-rate-limiting-plugin-0.2.0.2: README.md

# keter-rate-limiting-plugin

**keter-rate-limiting-plugin** is a modern, high-performance, and highly customizable rate-limiting plugin for [Keter](https://github.com/snoyberg/keter). It addresses [issue \#301](https://github.com/snoyberg/keter/issues/301) and brings robust, production-grade request throttling to Haskell web applications, featuring efficient in-memory caching with HashMap-based lookups and IP zone isolation.

This library is inspired by [rack-attack](https://github.com/rack/rack-attack) and [Ruby on Rails](https://github.com/rails/rails) (for Keter.RateLimiter.Notifications) and provides a powerful middleware for Keter-managed applications, though it can be integrated with any WAI-compatible Haskell web stack.

## Features

- **Five window algorithms**:
    - Fixed Window
    - Sliding Window
    - Token Bucket
    - Leaky Bucket
    - TinyLRU (Least Recently Used)
- **IP Zone Support**: Isolate caches and throttling policies per IP zone, customer segment, or any other logical grouping with efficient HashMap-based zone lookups.
- **Declarative Configuration**: Define throttling rules using JSON/YAML configuration with automatic serialization support.
- **Flexible Client Identification**: Multiple strategies for identifying clients (IP, headers, cookies, combinations).
- **Configurable Zone Derivation**: Flexible strategies for deriving IP zones from requests.
- **WAI Middleware**: Integrates seamlessly as a middleware into any WAI application.
- **Convenient and Customizable API**:
    - Use declarative configuration for common scenarios with automatic setup.
    - Or, for advanced use, fully control cache key structure and throttling logic.
- **Memory-efficient**: Designed for large-scale, high-traffic deployments with automatic cleanup of expired entries and HashMap-based O(1) average-case lookups.
- **Easy Integration**: Minimal code changes are required to get started.

## Why Use This Plugin?

- **Scalability**: Per-zone caches with HashMap-based storage and flexible throttling allow you to scale from single-user apps to multi-tenant platforms.
- **Performance**: The in-memory backend is built on efficient STM-based containers with HashMap optimizations for high-concurrency workloads.
- **Security**: Protects your application from abusive clients and denial-of-service attacks.
- **Flexibility**: Choose between declarative configuration and full programmatic customization.
- **Production-Ready**: Inspired by industry-standard tools, thoroughly documented, and designed for reliability with efficient data structures.
- **Open Source**: MIT licensed and community-friendly.

## Installation

Add the package to your `build-depends` in your project's `.cabal` file or `package.yaml`.

**For Cabal:**

```cabal
build-depends:
  , keter-rate-limiting-plugin
```

**For Stack (`package.yaml`):**

```yaml
dependencies:
- keter-rate-limiting-plugin
```

Then, rebuild your project. No external C libraries are required.

## Quick Start

### Declarative Configuration (Recommended)

The recommended approach uses declarative configuration that can be loaded from JSON or YAML files:

```haskell
{-# LANGUAGE OverloadedStrings #-}

import Keter.RateLimiter.WAI
import Keter.RateLimiter.Cache (Algorithm(..))
import Network.Wai (responseLBS, Application)
import Network.HTTP.Types (status200)
import Network.Wai.Handler.Warp (run)

-- A simple application that runs behind the middleware.
myApp :: Application
myApp _ respond = respond $ responseLBS status200 [] "Hello, you are not rate limited!"

main :: IO ()
main = do
  -- 1. Define declarative configuration
  let config = RateLimiterConfig
        { rlZoneBy = ZoneIP  -- Separate zones by client IP
        , rlThrottles = 
            [ RLThrottle "api"   100 3600 FixedWindow IdIP Nothing        -- 100 requests/hour by IP
            , RLThrottle "login" 5   300  TokenBucket  IdIP (Just 600)    -- 5 login attempts/5min by IP with 10min idle timeout
            ]
        }

  -- 2. Build middleware from configuration
  middleware <- buildRateLimiter config

  -- 3. Apply middleware to your application
  let appWithMiddleware = middleware myApp

  putStrLn "Server starting on port 8080..."
  run 8080 appWithMiddleware
```

### JSON Configuration

You can also load configuration from JSON files:

```json
{
  "zone_by": "ip",
  "throttles": [
    {
      "name": "api",
      "limit": 100,
      "period": 3600,
      "algorithm": "fixed_window",
      "identifier_by": "ip"
    },
    {
      "name": "login",
      "limit": 5,
      "period": 300,
      "algorithm": "token_bucket",
      "identifier_by": "ip",
      "token_bucket_ttl": 600
    }
  ]
}
```

### Advanced Programmatic Configuration

For more control, you can build the environment programmatically:

```haskell
import Keter.RateLimiter.WAI
import Keter.RateLimiter.Cache (Algorithm(..))
import Keter.RateLimiter.IPZones (defaultIPZone)
import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Types (hHost)

main :: IO ()
main = do
  -- 1. Initialize environment with custom zone logic
  env <- initConfig $ \req -> 
    case lookup hHost (requestHeaders req) of
      Just "api.example.com" -> "api_zone"
      Just "admin.example.com" -> "admin_zone"
      _ -> defaultIPZone

  -- 2. Add throttle configurations
  let apiThrottle = ThrottleConfig
        { throttleLimit      = 1000
        , throttlePeriod     = 3600
        , throttleAlgorithm  = FixedWindow
        , throttleIdentifierBy = IdIP
        , throttleTokenBucketTTL = Nothing
        }

  let loginThrottle = ThrottleConfig
        { throttleLimit      = 5
        , throttlePeriod     = 300
        , throttleAlgorithm  = TokenBucket
        , throttleIdentifierBy = IdIP
        , throttleTokenBucketTTL = Just 600
        }

  env' <- addThrottle env "api" apiThrottle
  env'' <- addThrottle env' "login" loginThrottle

  -- 3. Create middleware
  let middleware = buildRateLimiterWithEnv env''
      appWithMiddleware = middleware myApp

  putStrLn "Server starting on port 8080..."
  run 8080 appWithMiddleware
```

## Configuration Reference

### Client Identification Strategies (`IdentifierBy`)

- `IdIP` - Identify by client IP address
- `IdIPAndPath` - Identify by IP address and request path
- `IdIPAndUA` - Identify by IP address and User-Agent header
- `IdHeader headerName` - Identify by custom header value
- `IdCookie "session_id"` - Identify by cookie value
- `IdHeaderAndIP headerName` - Identify by header value combined with IP

### Zone Derivation Strategies (`ZoneBy`)

- `ZoneDefault` - All requests use the same cache (no zone separation)
- `ZoneIP` - Separate zones by client IP address
- `ZoneHeader headerName` - Separate zones by custom header value

### Rate Limiting Algorithms

- **`FixedWindow`** - Traditional fixed-window counting
- **`SlidingWindow`** - Precise sliding-window with timestamp tracking
- **`TokenBucket`** - Allow bursts up to capacity, refill over time
- **`LeakyBucket`** - Smooth rate limiting with configurable leak rate
- **`TinyLRU`** - Least-recently-used eviction for memory efficiency

## Example Usage

## For the Keter Users (is expected to be introduced in keter-2.3.0, see the README there and / or Changelog file)

### Important notes

Configure middleware in app bundles (config/keter.yaml), 
not in the global Keter daemon config. The global keter-config.yaml 
remains for listeners, TLS, ip-from-header, healthcheck-path, etc. 
Requests to healthcheck-path are never rate-limited.

### Quick Start

Attach a rate-limiter to any stanza via a middleware list.

Example bundle config (config/keter.yaml):

```yaml
stanzas:
  - type: webapp
    exec: ./my-app
    hosts: ["www.example.com"]
    middleware:
      - rate-limiter:
          zone_by: default
          throttles:
            - name: "ip-basic"
              limit: 100
              period: 60
              algorithm: FixedWindow
              identifier_by: ip

  - type: reverse-proxy
    hosts: ["api.example.com"]
    to: "http://127.0.0.1:9000"
    middleware:
      - rate-limiter:
          zone_by: { header: "X-Tenant-ID" }
          throttles:
            - name: "tenant-api"
              limit: 1000
              period: 3600
              algorithm: SlidingWindow
              identifier_by: { header: "X-Api-Key" }

  - type: static-files
    hosts: ["static.example.com"]
    root: ./static
    middleware:
      - rate-limiter:
          zone_by: ip
          throttles:
            - name: "static-ip"
              limit: 300
              period: 60
              algorithm: LeakyBucket
              identifier_by: ip
```

Tip: You can stack multiple middleware blocks if you need 
different protections. They run in order.

### Field Reference

* `rate-limiter`: top-level middleware key.
* `zone_by`:
  1. `"default"`: counters are isolated per vhost (Host header). Good per-domain isolation.
  2. `"ip"`: counters are isolated per client IP zone. Good for IP fairness.
  3. `{ "header": "X-Header" }`: per-tenant/customer isolation via a header value.
* `throttles`: list of rules. Each rule:
  1. `name`: a label for logs/metrics.
  2. `limit`: integer capacity or max requests.
  3. `period`: seconds (window or refill/leak interval depending on algorithm).
  4. `algorithm`: one of `FixedWindow | SlidingWindow | TokenBucket | LeakyBucket | TinyLRU`.
  5. `identifier_by`:
     * `"ip"`: identify by client IP (honors global ip-from-header).
     * `"ip+path"`: combine IP and path for path-specific throttles (e.g., /login).
     * `"ip+ua"`: combine IP and User-Agent.
     * `{ "header": "X-User" }`: identify by a header value.
     * `{ "cookie": "session" }`: identify by a cookie value.
     * `{ "header+ip": "X-Key" }`: combine header and IP.
  6. `token_bucket_ttl`: optional seconds; TokenBucket only (evicts idle buckets).

### Choosing Algorithms

Rule of thumb for common scenarios:

* **FixedWindow**
  1. When: Simple quotas (e.g., 100 req/min per IP).
  2. Pros: Simple, low overhead.
  3. Cons: Window boundary bursts possible.
  4. Use for: Public pages, basic protections.

* **SlidingWindow**
  1. When: Smoother enforcement over time; avoid boundary spikes.
  2. Pros: More accurate rolling rate.
  3. Cons: More state churn than FixedWindow.
  4. Use for: API endpoints where fairness matters.

* **TokenBucket**
  1. When: Allow short bursts but control average rate.
  2. Pros: Classic API limiter; bursty but bounded.
  3. Cons: Requires sensible period; consider TTL for idle buckets.
  4. Use for: Developer APIs, webhook receivers.
  5. Tip: Set token_bucket_ttl (e.g., 1800s) to evict idle buckets.

* **LeakyBucket**
  1. When: Smooth out bursts to a steady outflow.
  2. Pros: Predictable, backpressure-like effect.
  3. Cons: Tuning capacity vs leak rate.
  4. Use for: Form submissions, login attempts.

* **TinyLRU**
  1. When: Lightweight micro-throttling with tiny memory footprint.
  2. Pros: Very small, simple.
  3. Cons: Coarser control than others.
  4. Use for: Edge micro-protection, complementary limits.

### Practical Patterns

* Path-specific throttles (e.g., login):

```yaml
middleware:
  - rate-limiter:
      zone_by: default
      throttles:
        - name: "login"
          limit: 5
          period: 60
          algorithm: SlidingWindow
          identifier_by: ip+path
```

* API key quotas per tenant:

```yaml
middleware:
  - rate-limiter:
      zone_by: { header: "X-Tenant-ID" }
      throttles:
        - name: "tenant-quota"
          limit: 1000
          period: 3600
          algorithm: TokenBucket
          identifier_by: { header: "X-Api-Key" }
          token_bucket_ttl: 1800
```

* Mixed protections on the same host:

```yaml
middleware:
  - rate-limiter:
      zone_by: default
      throttles:
        - { name: "global-ip", limit: 600, period: 600, algorithm: FixedWindow, identifier_by: ip }
  - rate-limiter:
      zone_by: default
      throttles:
        - { name: "login", limit: 5, period: 60, algorithm: SlidingWindow, identifier_by: ip+path }
```

* Static assets fairness:

```yaml
- type: static-files
  hosts: ["cdn.example.com"]
  root: ./public
  middleware:
    - rate-limiter:
        zone_by: ip
        throttles:
          - { name: "cdn-ip", limit: 300, period: 60, algorithm: LeakyBucket, identifier_by: ip }
```

### Global keter daemon settings impacting behavior (keter-config.yaml):

* `ip-from-header`: influences throttles with `identifier_by: ip`.
* `healthcheck-path`: this path is always allowed and never rate-limited.


### Operational Tips

* Start with SlidingWindow or TokenBucket for APIs; FixedWindow 
for simple pages; add a strict path-specific rule for sensitive 
endpoints (/login, /password-reset).
* Tune limit/period to real traffic; prefer longer periods with 
proportionally larger limits for smoother behavior.
* If behind a load balancer/proxy, set ip-from-header: true 
in keter-config.yaml to honor X-Forwarded-For.
* Keep healthcheck-path simple (e.g., /keter-health); it's always 
bypassed by the limiter.
* For multi-tenant apps, use zone_by: { header: "X-Tenant-ID" } 
so each tenant's counters are isolated; pair with header/cookie 
identifiers that match your auth.
* Use token_bucket_ttl to bound memory for TokenBucket.
* Stacking throttles is common; the most restrictive one effectively 
governs.
* Consider integrating limiter notifications with your logging/metrics.

### FAQ

* **Should I configure middleware in the global Keter config?**

No. Middleware is per-app in bundles (config/keter.yaml). The global file 
configures listeners, TLS, ip-from-header, and healthcheck-path.

* **Does it work with HTTPS and multiple listeners?**

Yes. The middleware is applied uniformly; rate limiting is agnostic 
to scheme.

* **How do vhosts interact with rate limits?**

With zone_by: default, counters are isolated per Host. Different hosts 
pointing to the same backend port don't share counters.

If you'd like help choosing safe defaults for your workloads, open 
an issue with a brief description of your traffic patterns and endpoints.

## Using the Convenient API

The `CacheWithZone` module provides helpers that automatically compose cache keys from the algorithm, zone, and user key, simplifying common use cases while leveraging efficient HashMap-based zone lookups.

```haskell
import Keter.RateLimiter.Cache
import Keter.RateLimiter.CacheWithZone

-- Create a store and cache for the Fixed Window algorithm
fixedWindowStore <- createInMemoryStore @'FixedWindow
let cache = newCache FixedWindow fixedWindowStore

-- Increment a counter for a user in a specific zone.
-- The key "rate_limiter:zoneX:userX" is created automatically.
-- The request is allowed if the count is within the limit.
-- Zone lookup uses HashMap for O(1) average performance.
isAllowed <- allowFixedWindowRequest cache "zoneX" "userX" 100 3600 -- 100 requests per hour
```

## Using the Customizable API

For more complex scenarios, you can manually construct cache keys and interact directly with the `Cache` module. This gives you full control over the key structure while still benefiting from HashMap-optimized storage.

```haskell
import Keter.RateLimiter.Cache

-- Use the same cache from the previous example.
let customKey = "rate_limiter:fixed_window:logins:zoneY:userY"

-- Manually increment the counter for the custom key.
newCount <- incrementCache cache customKey 60 -- TTL of 60 seconds

-- Manually read the value.
mVal <- readCache cache customKey :: IO (Maybe Int)
```

### Multi-Algorithm Configuration Example

```haskell
let config = RateLimiterConfig
      { rlZoneBy = ZoneHeader (hdr "X-Tenant-ID")  -- Separate by tenant
      , rlThrottles = 
          [ RLThrottle "api_burst"     100  60   TokenBucket   IdIP              (Just 300)
          , RLThrottle "api_sustained" 1000 3600 FixedWindow   IdIP              Nothing
          , RLThrottle "login"         5    300  LeakyBucket   IdIP              Nothing
          , RLThrottle "admin"         50   3600 SlidingWindow (IdHeader (hdr "X-Admin-Key")) Nothing
          , RLThrottle "lru_cache"     1000 60   TinyLRU       IdIPAndPath       Nothing
          ]
      }
```

## Performance Characteristics

This library is optimized for high-performance scenarios:

- **HashMap-based zone caches**: O(1) average-case lookup for IP zone cache resolution
- **HashMap-based throttle storage**: O(1) average-case retrieval of throttle configurations
- **STM-based concurrent access**: Thread-safe operations with minimal contention
- **Memory-efficient algorithms**: Automatic cleanup of expired entries across all rate limiting algorithms
- **Scalable architecture**: Designed to handle thousands of concurrent requests with minimal overhead

## Testing

This package includes an extensive test suite covering all supported rate-limiting algorithms, IP zone isolation, cache management, and HashMap-based performance optimizations.

To run the tests:

```bash
cabal test
```

or

```bash
stack test
```

## When to Use This Library

- You need robust and efficient request throttling for your Haskell web application.
- You want to protect your service from abuse and DoS attacks.
- You require per-zone or per-user isolation of throttling policies with efficient lookups.
- You value both declarative configuration and the ability to customize behavior as needed.
- You need high-performance rate limiting that can scale to handle large numbers of concurrent requests and zones.

## Migration from Earlier Versions

If you're upgrading from an earlier version that used the programmatic API, the declarative configuration approach is now recommended:

**Old approach:**
```haskell
env <- initConfig getZoneFunction
env' <- addThrottle env "api" throttleConfig
let middleware = attackMiddleware env'
```

**New recommended approach:**
```haskell
let config = RateLimiterConfig { ... }
middleware <- buildRateLimiter config
```

The old programmatic API is still fully supported for advanced use cases via `buildRateLimiterWithEnv` and related functions.

## License

MIT License © 2025 Oleksandr Zhabenko

## References

- [rack-attack (Ruby)](https://github.com/rack/rack-attack)
- [keter (Haskell)](https://github.com/snoyberg/keter)