wai-rate-limit 0.1.0.0 → 0.2.0.0
raw patch · 5 files changed
+155/−65 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Network.Wai.RateLimit.Strategy: data Strategy
+ Network.Wai.RateLimit.Backend: BackendError :: e -> BackendError
+ Network.Wai.RateLimit.Backend: data BackendError
+ Network.Wai.RateLimit.Backend: instance GHC.Exception.Type.Exception Network.Wai.RateLimit.Backend.BackendError
+ Network.Wai.RateLimit.Backend: instance GHC.Show.Show Network.Wai.RateLimit.Backend.BackendError
+ Network.Wai.RateLimit.Strategy: newtype Strategy
- Network.Wai.RateLimit.Backend: MkBackend :: (key -> IO (Either err Integer)) -> (key -> Integer -> IO (Either err Integer)) -> (key -> Integer -> IO (Either err ())) -> Backend key err
+ Network.Wai.RateLimit.Backend: MkBackend :: (key -> IO Integer) -> (key -> Integer -> IO Integer) -> (key -> Integer -> IO ()) -> Backend key
- Network.Wai.RateLimit.Backend: [backendExpireIn] :: Backend key err -> key -> Integer -> IO (Either err ())
+ Network.Wai.RateLimit.Backend: [backendExpireIn] :: Backend key -> key -> Integer -> IO ()
- Network.Wai.RateLimit.Backend: [backendGetUsage] :: Backend key err -> key -> IO (Either err Integer)
+ Network.Wai.RateLimit.Backend: [backendGetUsage] :: Backend key -> key -> IO Integer
- Network.Wai.RateLimit.Backend: [backendIncAndGetUsage] :: Backend key err -> key -> Integer -> IO (Either err Integer)
+ Network.Wai.RateLimit.Backend: [backendIncAndGetUsage] :: Backend key -> key -> Integer -> IO Integer
- Network.Wai.RateLimit.Backend: data Backend key err
+ Network.Wai.RateLimit.Backend: data Backend key
- Network.Wai.RateLimit.Strategy: fixedWindow :: Backend key err -> Integer -> Integer -> (Request -> IO key) -> Strategy
+ Network.Wai.RateLimit.Strategy: fixedWindow :: Backend key -> Integer -> Integer -> (Request -> IO key) -> Strategy
- Network.Wai.RateLimit.Strategy: slidingWindow :: Backend key err -> Integer -> Integer -> (Request -> IO key) -> Strategy
+ Network.Wai.RateLimit.Strategy: slidingWindow :: Backend key -> Integer -> Integer -> (Request -> IO key) -> Strategy
Files
- CHANGELOG.md +9/−0
- README.md +73/−14
- src/Network/Wai/RateLimit/Backend.hs +31/−9
- src/Network/Wai/RateLimit/Strategy.hs +35/−36
- wai-rate-limit.cabal +7/−6
+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Changelog for `wai-rate-limit`++## 0.2++- Change how errors are handled by `Backend`s to use exceptions++## 0.1++- Support for fixed and sliding window strategies
README.md view
@@ -1,16 +1,28 @@-# Rate limiting as WAI middleware+# Rate limiting for Servant and as WAI middleware -++ -This repository contains WAI middleware for rate limiting. The main library is `wai-rate-limit` which provides the WAI middleware as well as implementations of different rate limiting strategies. +| Package | Version |+|---------|---------|+| `wai-rate-limit` | [](https://hackage.haskell.org/package/wai-rate-limit) |+| `wai-rate-limit-redis` | [](https://hackage.haskell.org/package/wai-rate-limit-redis) |+| `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` which provides the WAI middleware as well as implementations of different rate limiting strategies. For use with Servant, the `servant-rate-limit` library provides the required combinators as well as type class instances.+ To limit dependencies introduced by `wai-rate-limit`, storage backends are split up into their own packages: -- A Redis backend is provided by `wai-rate-limit-redis`. +- A Redis backend is provided by `wai-rate-limit-redis` -## Usage+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.@@ -25,8 +37,8 @@ import Network.Wai.RateLimit.Redis middleware :: Redis.Connection -> Middleware-middleware conn = rateLimiting strategy - where backend = redisBackend conn +middleware conn = rateLimiting strategy+ where backend = redisBackend conn getKey = pure . C8.pack . show . remoteHost strategy = slidingWindow backend 29 50 getKey ```@@ -35,7 +47,7 @@ ### 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. +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 C8@@ -47,8 +59,8 @@ import Network.Wai.RateLimit.Redis middleware :: Redis.Connection -> Middleware-middleware conn = rateLimiting strategy - where backend = redisBackend conn +middleware conn = rateLimiting strategy+ where backend = redisBackend conn getKey = pure . C8.pack . show . remoteHost strategy = fixedWindow backend 29 50 getKey ```@@ -81,12 +93,59 @@ import Network.Wai.RateLimit.Redis middleware :: Redis.Connection -> Middleware-middleware conn = rateLimiting strategy{ strategyOnRequest = customHandler } - where backend = redisBackend conn +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" + 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 Servant+import Servant.RateLimit++type TestAPI+ = RateLimit (FixedWindow 2 50) (IPAddressPolicy "fixed:") :>+ "fixed-window" :>+ Get '[JSON] String+ :<|> RateLimit (SlidingWindow 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 V++type UserId = ByteString -- for simplicity++{-# NOINLINE userKey #-}+userKey :: V.Key UserId+userKey = unsafePerformIO newKey++data MyPolicy++instance HasRateLimitPolicy MyPolicy where+ type RateLimitPolicyKey MyPolicy = ByteString++ policyGetIdentifier req =+ fromMaybe (error "expected to have a user id in the vault") $+ V.lookup userKey (vault req)+ ```
src/Network/Wai/RateLimit/Backend.hs view
@@ -5,23 +5,45 @@ -- file in the root directory of this source tree. -- -------------------------------------------------------------------------------- +{-# LANGUAGE ExistentialQuantification #-}+ module Network.Wai.RateLimit.Backend (+ BackendError(..), Backend(..) ) where -------------------------------------------------------------------------------- +import Control.Exception++--------------------------------------------------------------------------------++-- | Represents a base type for exceptions that occur in `Backend`s.+data BackendError = forall e . Exception e => BackendError e++instance Show BackendError where+ showsPrec p (BackendError e) = showsPrec p e++instance Exception BackendError+ -- | Represents storage backends for the rate limiting middleware.-data Backend key err = MkBackend {- -- | 'backendGetUsage' @key@ is a computation which gets the usage - -- associated with @key@.- backendGetUsage :: key -> IO (Either err Integer),- -- | 'backendIncAndGetUsage' @key usage@ is a computation which increments +data Backend key = MkBackend {+ -- | `backendGetUsage` @key@ is a computation which gets the usage+ -- associated with @key@. This computation may raise a `BackendError`+ -- exception if an error occurs while trying to retrieve the @key@'s+ -- usage from the backend.+ backendGetUsage :: key -> IO Integer,+ -- | `backendIncAndGetUsage` @key usage@ is a computation which increments -- the usage associated with @key@ by @usage@ and returns the result.- backendIncAndGetUsage :: key -> Integer -> IO (Either err Integer),- -- | 'backendExpireIn' @key seconds@ is a computation which makes @key@- -- expire in @seconds@ from now.- backendExpireIn :: key -> Integer -> IO (Either err ())+ -- This computation may raise a `BaackendError` exception if an error+ -- occurs while trying to increment and retrieve the usage associated+ -- with @key@.+ backendIncAndGetUsage :: key -> Integer -> IO Integer,+ -- | `backendExpireIn` @key seconds@ is a computation which makes @key@+ -- expire in @seconds@ from now. This computation may raise a+ -- `BackendError` exception if an error occurs while trying to set @key@+ -- to expire.+ backendExpireIn :: key -> Integer -> IO () } --------------------------------------------------------------------------------
src/Network/Wai/RateLimit/Strategy.hs view
@@ -21,68 +21,67 @@ -------------------------------------------------------------------------------- -- | Represents rate limiting strategies.-data Strategy = MkStrategy {+newtype 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 } - -- | 'windowStrategy'-windowStrategy :: Backend key err - -> Integer - -> Integer - -> (Request -> IO key) - -> (Integer -> Bool)- -> Request- -> IO Bool+windowStrategy+ :: Backend key+ -> Integer+ -> Integer+ -> (Request -> IO key)+ -> (Integer -> Bool)+ -> Request+ -> IO Bool windowStrategy MkBackend{..} seconds capacity getKey cond req = do -- get a key to identify the usage bucket for the request: this is -- up the application and may be comprised of e.g. the IP of the client -- or a unique user id, followed by e.g. a timestamp- key <- getKey req + key <- getKey req -- get usage for the key and increment it by 1- result <- backendIncAndGetUsage key 1+ used <- backendIncAndGetUsage key 1 - case result of - -- a backend error occurred, deny the request- Left err -> pure False - -- we got back the current usage: check whether it is within the- -- acceptable limit and, if so, add to the expiry timer- Right used - | used <= capacity -> do- when (cond used) $ void $ backendExpireIn key seconds- pure True - | otherwise -> pure False+ -- we got back the current usage: check whether it is within the+ -- acceptable limit and, if so, add to the expiry timer+ if used <= capacity+ then do+ when (cond used) $ void $ backendExpireIn key seconds+ pure True+ else pure False -- | 'fixedWindow' @seconds limit@ is a 'Strategy' which limits the number--- of requests made by a client to @limit@ within a window of @seconds@. -fixedWindow :: Backend key err - -> Integer - -> Integer - -> (Request -> IO key) - -> Strategy+-- of requests made by a client to @limit@ within a window of @seconds@.+fixedWindow+ :: Backend key+ -> Integer+ -> Integer+ -> (Request -> IO key)+ -> Strategy fixedWindow backend seconds capacity getKey = MkStrategy{- strategyOnRequest = + strategyOnRequest = let cond = (==) 1 in windowStrategy backend seconds capacity getKey cond } -- | 'slidingWindow' @seconds limit@ is a 'Strategy' which limits the number--- of requests made by a client to @limit@ within a sliding window of +-- of requests made by a client to @limit@ within a sliding window of -- @seconds@. That is, for every successful request, the window is extended by--- @seconds@ so that a "break" of @seconds@ is required after @limit@-many +-- @seconds@ so that a "break" of @seconds@ is required after @limit@-many -- requests have been made in a period during which the timeout has never -- been exceeded.-slidingWindow :: Backend key err - -> Integer - -> Integer - -> (Request -> IO key) - -> Strategy+slidingWindow+ :: Backend key+ -> Integer+ -> Integer+ -> (Request -> IO key)+ -> Strategy slidingWindow backend seconds capacity getKey = MkStrategy{- strategyOnRequest = + strategyOnRequest = let cond = const True in windowStrategy backend seconds capacity getKey cond }
wai-rate-limit.cabal view
@@ -1,26 +1,25 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: cc5d5ebcfe3ffeb74b04de46a5e0c91cebb17900f85fbd1939ca33fc1593a9b3 name: wai-rate-limit-version: 0.1.0.0+version: 0.2.0.0 synopsis: Rate limiting as WAI middleware description: A Haskell library which implements rate limiting as WAI middleware category: Security homepage: https://github.com/mbg/wai-rate-limit#readme bug-reports: https://github.com/mbg/wai-rate-limit/issues author: Michael B. Gale-maintainer: m.gale@warwick.ac.uk+maintainer: github@michael-gale.co.uk copyright: Copyright (c) Michael B. Gale license: MIT license-file: LICENSE build-type: Simple extra-source-files: README.md+ CHANGELOG.md source-repository head type: git@@ -35,7 +34,9 @@ Paths_wai_rate_limit hs-source-dirs: src- default-extensions: OverloadedStrings RecordWildCards+ default-extensions:+ OverloadedStrings+ RecordWildCards ghc-options: -W build-depends: base >=4.8 && <5