Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mbg/wai-rate-limit
Rate limiting for Servant and as WAI middleware
https://github.com/mbg/wai-rate-limit
haskell haskell-servant rate-limiting wai-middleware
Last synced: 2 months ago
JSON representation
Rate limiting for Servant and as WAI middleware
- Host: GitHub
- URL: https://github.com/mbg/wai-rate-limit
- Owner: mbg
- License: mit
- Created: 2020-11-09T18:29:35.000Z (about 4 years ago)
- Default Branch: main
- Last Pushed: 2024-01-18T13:09:27.000Z (11 months ago)
- Last Synced: 2024-10-04T16:16:01.306Z (3 months ago)
- Topics: haskell, haskell-servant, rate-limiting, wai-middleware
- Language: Haskell
- Homepage:
- Size: 87.9 KB
- Stars: 12
- Watchers: 3
- Forks: 3
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# Rate limiting for Servant and as WAI middleware
![MIT](https://img.shields.io/github/license/mbg/wai-rate-limit)
![CI](https://github.com/mbg/wai-rate-limit/workflows/CI/badge.svg?branch=main)
![stackage-nightly](https://github.com/mbg/wai-rate-limit/workflows/stackage-nightly/badge.svg)| Package | Version |
|---------|---------|
| `wai-rate-limit` | [![Hackage](https://img.shields.io/hackage/v/wai-rate-limit)](https://hackage.haskell.org/package/wai-rate-limit) |
| `wai-rate-limit-redis` | [![Hackage](https://img.shields.io/hackage/v/wai-rate-limit-redis)](https://hackage.haskell.org/package/wai-rate-limit-redis) |
| `servant-rate-limit` | [![Hackage](https://img.shields.io/hackage/v/servant-rate-limit)](https://hackage.haskell.org/package/servant-rate-limit) |This repository contains WAI middleware for rate limiting as well as a library which provides Servant combinators for including rate limiting strategies in API type specifications. The main library is [`wai-rate-limit`](https://hackage.haskell.org/package/wai-rate-limit) which provides the WAI middleware as well as implementations of different rate limiting strategies. For use with Servant, the [`servant-rate-limit`](https://hackage.haskell.org/package/servant-rate-limit) library provides the required combinators as well as type class instances.
To limit dependencies introduced by [`wai-rate-limit`](https://hackage.haskell.org/package/wai-rate-limit), storage backends are split up into their own packages:
- A Redis backend is provided by [`wai-rate-limit-redis`](https://hackage.haskell.org/package/wai-rate-limit-redis)
- A PostgreSQL backend is provided by [`wai-rate-limit-postgres`](https://hackage.haskell.org/package/wai-rate-limit-postgres)To limit dependencies for `servant-rate-limit`, build flags control the inclusion of different Servant dependencies and modules. All flags are on by default, but can be toggled off:
- `servant-rate-limit:server` is the flag that controls the inclusion of a dependency on `servant-server` and the `Servant.RateLimit.Server` module.
- `servant-rate-limit:client` is the flag that controls the inclusion of a dependency on `servant-client` and the `Servant.RateLimit.Client` module.## Usage as Middleware
### Sliding Window
The following example demonstrates how to use the middleware with a sliding window strategy and a Redis backend. The resulting middleware will limit requests to 50 requests per sliding window of 29 seconds based on keys derived from the client's IP address. In other words, if we receive a request, we check whether the limit of 50 has been exceed for the client based on their IP and if not, the request is allowed, the request count is increased, and the window is extended by 29 seconds. If the limit is exceeded, the client will always have to wait 29 seconds before making another request.
```haskell
import qualified Data.ByteString.Char8 as C8import Database.Redis as Redis
import Network.Wai.RateLimit
import Network.Wai.RateLimit.Strategy
import Network.Wai.RateLimit.Redismiddleware :: Redis.Connection -> Middleware
middleware conn = rateLimiting strategy
where backend = redisBackend conn
getKey = pure . C8.pack . show . remoteHost
strategy = slidingWindow backend 29 50 getKey
```The behaviour described above can be changed by altering the parameters to `slidingWindow` accordingly. In particular, for e.g. REST APIs, you may wish to use e.g. API keys or other user identifiers in place of IP addresses.
### Fixed Window
The following example demonstrates how to use the middleware with a fixed window strategy and a Redis backend. The resulting middleware will limit requests to 50 requests per window of 29 seconds based on keys derived from the client's IP address.
```haskell
import qualified Data.ByteString.Char8 as C8import Database.Redis as Redis
import Network.Wai.RateLimit
import Network.Wai.RateLimit.Strategy
import Network.Wai.RateLimit.Redismiddleware :: Redis.Connection -> Middleware
middleware conn = rateLimiting strategy
where backend = redisBackend conn
getKey = pure . C8.pack . show . remoteHost
strategy = fixedWindow backend 29 50 getKey
```The behaviour described above can be changed by altering the parameters to `fixedWindow` accordingly. In particular, for e.g. REST APIs, you may wish to use e.g. API keys or other user identifiers in place of IP addresses.
### Custom strategies
In addition to the provided strategies, you can implement your own `Strategy` values or customise existing ones. The `Strategy` type is currently defines as follows, so a custom strategy is essentially a function `Request -> IO Bool` which should return `True` if the request should proceed or `False` if it should be rejected:
```haskell
-- | Represents rate limiting strategies.
data Strategy = MkStrategy {
-- | 'strategyOnRequest' @request@ is a computation which determines
-- whether the request should be allowed or not, based on the rate
-- limiting strategy.
strategyOnRequest :: Request -> IO Bool
}
```Modifying existing strategies makes it relatively easy to e.g. selectively apply rate limiting to some paths:
```haskell
import qualified Data.ByteString.Char8 as C8import Database.Redis as Redis
import Network.Wai.RateLimit
import Network.Wai.RateLimit.Strategy
import Network.Wai.RateLimit.Redismiddleware :: Redis.Connection -> Middleware
middleware conn = rateLimiting strategy{ strategyOnRequest = customHandler }
where backend = redisBackend conn
getKey = pure . C8.pack . show . remoteHost
strategy = fixedWindow backend 29 50 getKey
customHandler req =
if rawPathInfo req == "/index.html"
then pure True -- always allow access to /index.html
else strategyOnRequest strategy req
```## Usage with Servant
The `Servant.RateLimit` module exports types for describing rate-limiting strategies and policies at the type-level. Consider the following API type specification:
```haskell
import Data.Time.TypeLevelimport Servant
import Servant.RateLimittype TestAPI
= RateLimit (FixedWindow ('Second 2) 50) (IPAddressPolicy "fixed:") :>
"fixed-window" :>
Get '[JSON] String
:<|> RateLimit (SlidingWindow ('Second 2) 50) (IPAddressPolicy "sliding:") :>
"sliding-window" :>
Get '[JSON] String
:<|> "unrestricted" :>
Get '[JSON] String
```We have three API endpoints:
- `/fixed-window` to which we apply a fixed window strategy which ensures that no more than 50 requests are made in a 2 second window.
- `/sliding-window` to which we apply a sliding window strategy which extends the window during which up to 50 requests may be made by 2 seconds every time there is a successful request.
- `/unrestricted` to which no rate limiting strategy is applied.For the two restricted endpoints, we use `IPAddressPolicy` to identify clients. This is a rate limiting policy which identifies client by their IP address. The string parameter is used to identify different scopes. I.e. the rate limits for each of the two endpoints are separate. This policy is OK for testing purposes, but for use in production you may wish to implement a more sophisticated policy which applies the rate limit based on identifiers that are available as a result of authentication. For example:
```haskell
import qualified Data.Vault.Lazy as Vtype UserId = ByteString -- for simplicity
{-# NOINLINE userKey #-}
userKey :: V.Key UserId
userKey = unsafePerformIO newKeydata MyPolicy
instance HasRateLimitPolicy ctx MyPolicy where
type RateLimitPolicyKey ctx MyPolicy = ByteStringpolicyGetIdentifier _ req =
fromMaybe (error "expected to have a user id in the vault") $
V.lookup userKey (vault req)
```Rate limiting policies have access to the Servant context. For example, assuming that a value of a custom `ApiKey` type is stored in the context, we can retrieve it in a custom `HasRateLimitPolicy` as follows:
```haskell
newtype ApiKey = MkApiKey { getApiKey :: ByteString }data ApiKeyPolicy
instance HasContextEntry ctx ApiKey => HasRateLimitPolicy ctx ApiKeyPolicy where
type RateLimitPolicyKey ctx ApiKeyPolicy = ByteStringpolicyGetIdentifier ctx _ =
pure $ getApiKey $ getContextEntry ctx
```