packages feed

wai-rate-limit-0.3.0.0: src/Network/Wai/RateLimit/Strategy.hs

--------------------------------------------------------------------------------
-- Rate Limiting Middleware for WAI                                           --
--------------------------------------------------------------------------------
-- This source code is licensed under the MIT license found in the LICENSE    --
-- file in the root directory of this source tree.                            --
--------------------------------------------------------------------------------

-- | Exports functions which implement rate-limiting strategies.
module Network.Wai.RateLimit.Strategy (
    Strategy(..),
    fixedWindow,
    slidingWindow
) where

--------------------------------------------------------------------------------

import Control.Monad

import Data.Time.Units

import Network.Wai
import Network.Wai.RateLimit.Backend

--------------------------------------------------------------------------------

-- | Represents rate limiting strategies.
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` implements a general window-based rate limiting strategy.
windowStrategy
    :: Backend key -- ^ The storage backend to use.
    -> Second -- ^ The number of seconds after which recorded usage expires.
    -> Integer -- ^ How much capacity each key should have.
    -> (Request -> IO key) -- ^ A function which computes a key for the
                           -- request.
    -> (Integer -> Bool) -- ^ A predicate which determines whether the expiry
                         -- timer should be reset.
    -> Request -- ^ The request to apply the stragey to, used for deriving
               -- the key.
    -> 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

    -- get usage for the key and increment it by 1
    used <- backendIncAndGetUsage key 1

    -- 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 (toInteger 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
    -> Second
    -> Integer
    -> (Request -> IO key)
    -> Strategy
fixedWindow backend seconds capacity getKey = MkStrategy{
    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
-- @seconds@. That is, for every successful request, the window is extended by
-- @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
    -> Second
    -> Integer
    -> (Request -> IO key)
    -> Strategy
slidingWindow backend seconds capacity getKey = MkStrategy{
    strategyOnRequest =
        let cond = const True
        in windowStrategy backend seconds capacity getKey cond
}

--------------------------------------------------------------------------------