Wai package:keter-rate-limiting-plugin

This file is a ported to Haskell language code with some simplifications of rack-attack https://github.com/rack/rack-attack/blob/main/lib/rack/attack.rb and is based on the structure of the original code of rack-attack, Copyright (c) 2016 by Kickstarter, PBC, under the MIT License. Oleksandr Zhabenko added several implementations of the window algorithm: tinyLRU, sliding window, token bucket window, leaky bucket window alongside with the initial count algorithm using AI chatbots. IP Zone functionality added to allow separate caches per IP zone. Overview ======== This module provides WAI middleware for declarative, IP-zone-aware rate limiting with multiple algorithms:
  • Fixed Window
  • Sliding Window
  • Token Bucket
  • Leaky Bucket
  • TinyLRU
Key points ----------
  • Plugin-friendly construction: build an environment once (Env) from RateLimiterConfig and produce a pure WAI Middleware. This matches common WAI patterns and avoids per-request setup or global mutable state.
  • Concurrency model: all shared structures inside Env use STM TVar, not IORef. This ensures thread-safe updates under GHC's lightweight (green) threads.
  • Zone-specific caches: per-IP-zone caches are stored in a HashMap keyed by zone identifiers. Zones are derived from a configurable strategy (ZoneBy), with a default.
  • No global caches in Keter: you can build one Env per compiled middleware chain and cache that chain externally (e.g., per-vhost + middleware-list), preserving counters/windows across requests.
Quick start ----------- 1) Declarative configuration (e.g., parsed from JSON/YAML):
let cfg = RateLimiterConfig
{ rlZoneBy = ZoneDefault
, rlThrottles =
[ RLThrottle "api"   1000 3600 FixedWindow IdIP Nothing
, RLThrottle "login" 5    300  TokenBucket IdIP (Just 600)
]
}
2) Build Env once and obtain a pure Middleware:
env <- buildEnvFromConfig cfg
let mw = buildRateLimiterWithEnv env
app = mw baseApplication
Alternatively:
mw <- buildRateLimiter cfg  -- convenience: Env creation + Middleware
app = mw baseApplication
Usage patterns -------------- Declarative approach (recommended):
import Keter.RateLimiter.WAI
import Keter.RateLimiter.Cache (Algorithm(..))

main = do
let config = RateLimiterConfig
{ rlZoneBy = ZoneIP
, rlThrottles = 
[ RLThrottle "api" 100 3600 FixedWindow IdIP Nothing
]
}
middleware <- buildRateLimiter config
let app = middleware baseApp
run 8080 app
Programmatic approach (advanced):
import Keter.RateLimiter.WAI
import Keter.RateLimiter.Cache (Algorithm(..))

main = do
env initConfig (\req - "zone1")
let throttleConfig = ThrottleConfig
{ throttleLimit = 100
, throttlePeriod = 3600
, throttleAlgorithm = FixedWindow
, throttleIdentifierBy = IdIP
, throttleTokenBucketTTL = Nothing
}
env' <- addThrottle env "api" throttleConfig
let middleware = buildRateLimiterWithEnv env'
app = middleware baseApp
run 8080 app
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 cookieName - 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
A type alias for a notifier specialized for WAI Requests. This simplifies the signature for notifiers that work directly with WAI, avoiding the need to manually convert the Request to Text beforehand. The conversion is handled internally using convertWAIRequest.
Lifts a generic Notifier into the WAI-specific domain, creating a WAINotifier. It works by using convertWAIRequest to transform the Request object into a summary Text before passing it to the underlying Notifier's action. The action verb is fixed to "blocked".

Example

let myWAINotifier = waiNotifier consoleNotifier
myWAINotifier "auth" request 10
-- Output: YYYY-MM-DD HH:MM:SS - auth blocked GET /login from 10.0.0.1:443 (limit: 10)
A WAINotifier that logs formatted request data to stdout. The log format includes timestamp, throttle name, action ("blocked"), formatted request information, and the limit that was exceeded:
YYYY-MM-DD HH:MM:SS - throttleName blocked METHOD PATH[?QUERY] from IP:PORT (limit: N)

Example output

2025-01-30 13:45:12 - api-global blocked GET v1users?id=123 from 192.0.2.1:54321 (limit: 1000)
2025-01-30 13:45:13 - auth blocked POST /login from 10.0.0.1:443 (limit: 10)
2025-01-30 13:45:14 - resource blocked DELETE resource123 from 127.0.0.1:9000 (limit: 50)
The notifier is thread-safe and handles various request types including IPv4, IPv6, and Unix socket connections appropriately.
Converts a WAI Request to a compact, single-line textual representation. The output format is: METHOD PATH[?QUERY] from IP:PORT Port handling: * For IPv4 and IPv6 addresses: includes the port number * For Unix sockets or unknown socket types: omits the port Query string handling: * Non-empty query strings: included with the leading ? * Empty query strings: omitted entirely

Examples

GET /index.html?lang=en from 127.0.0.1:8080
POST /login from 10.0.0.1:443
DELETE resource123 from 127.0.0.1:9000
GET apiusers?limit=10 from 2001:0db8:0000:0000:0000:0000:0000:0001:443
This function uses unsafePerformIO internally to resolve the client's IP address via getClientIP. This is considered acceptable for logging and notification purposes where the function's primary role is to produce a human-readable summary and performance is not critically impacted by a minor, contained impurity. The NOINLINE pragma is used to prevent the IO action from being duplicated by compiler optimizations.
A WAINotifier that performs no action. This is the WAI-specific equivalent of noopNotifier. It completes immediately without any side effects and is useful for testing or disabling WAI-specific notifications.

Example usage

-- Use in test environments or to disable logging
let notifier = if enableLogging then consoleWAINotifier else noopWAINotifier
notifier "test-throttle" request 100
A high-level wrapper to trigger a WAINotifier. This is a thin alias, defined for symmetry with notify. It simply calls the underlying WAINotifier function directly.

Example

notifyWAI consoleWAINotifier "api-throttle" request 100