packages feed

wai-rate-limit-redis 0.1.0.0 → 0.2.0.0

raw patch · 5 files changed

+129/−54 lines, 5 filesdep ~wai-rate-limitPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: wai-rate-limit

API changes (from Hackage documentation)

+ Network.Wai.RateLimit.Redis: instance GHC.Exception.Type.Exception Network.Wai.RateLimit.Redis.RedisBackendError
- Network.Wai.RateLimit.Redis: redisBackend :: Connection -> Backend ByteString RedisBackendError
+ Network.Wai.RateLimit.Redis: redisBackend :: Connection -> Backend ByteString

Files

+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Changelog for `wai-rate-limit-redis`++## 0.2++- Update to match revised error handling in `wai-rate-limit`++## 0.1++- Initial release
README.md view
@@ -1,16 +1,28 @@-# Rate limiting as WAI middleware+# 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=master)+![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) -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` | [![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` 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/Redis.hs view
@@ -12,6 +12,8 @@  -------------------------------------------------------------------------------- +import Control.Exception+ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8 @@ -23,37 +25,39 @@  -- | Represents reasons why requests made to the Redis backend have failed. data RedisBackendError-    = RedisBackendReply Reply +    = RedisBackendReply Reply     | RedisBackendTxAborted-    | RedisBackendTxError String -    deriving (Eq, Show) +    | RedisBackendTxError String+    deriving (Eq, Show) +instance Exception RedisBackendError+ -- | 'redisBackend' @connection@ constructs a rate limiting 'Backend' for the -- given redis @connection@.-redisBackend :: Connection -> Backend BS.ByteString RedisBackendError+redisBackend :: Connection -> Backend BS.ByteString redisBackend conn = MkBackend{     backendGetUsage = \key -> runRedis conn (get key) >>= \case-        Left err -> pure $ Left $ RedisBackendReply err-        Right mVal -> case mVal >>= \val -> C8.readInteger val of +        Left err -> throw $ BackendError $ RedisBackendReply err+        Right mVal -> case mVal >>= \val -> C8.readInteger val of             -- the key does not exist or is not a valid integer:             -- no previous usage-            Nothing -> pure $ Right 0 +            Nothing -> pure 0             -- the key exists: check that nothing follows the integer and then             -- return the current usage-            Just (n, xs) | BS.null xs -> pure $ Right n-                         | otherwise -> pure $ Right 0,+            Just (n, xs) | BS.null xs -> pure n+                         | otherwise -> pure 0,     backendIncAndGetUsage = \key val -> do         -- increment the value of the key by the specified amount         result <- runRedis conn $ incrby key val-        -        case result of -            Left err -> pure $ Left $ RedisBackendReply err-            Right n -> pure $ Right n,++        case result of+            Left err -> throw $ BackendError $ RedisBackendReply err+            Right n -> pure n,     backendExpireIn = \key seconds ->-        -- update the key to expire in the specified number of seconds -        runRedis conn (expire key seconds) >>= \case  -            Left err -> pure $ Left $ RedisBackendReply err -            Right _ -> pure $ Right ()+        -- update the key to expire in the specified number of seconds+        runRedis conn (expire key seconds) >>= \case+            Left err -> throw $ BackendError $ RedisBackendReply err+            Right _ -> pure () }  --------------------------------------------------------------------------------
test/Spec.hs view
@@ -36,23 +36,23 @@ -- rate limit), all of which should result in a 200 status code. It then makes -- another request which should result in a 429 status code. rateLimitedSession :: Session ()-rateLimitedSession = do +rateLimitedSession = do     let req = SRequest defaultRequest ""      forM_ [0..49] $ const $ do         res <- srequest req         assertStatus 200 res -    res <- srequest req -    assertStatus 429 res +    res <- srequest req+    assertStatus 429 res      pure ()  -- | 'expirySession' makes 50 requests to the server, which should hit the--- rate limit. It then sleeps for three seconds and makes another request, +-- rate limit. It then sleeps for three seconds and makes another request, -- which should succeed. expirySession :: Session ()-expirySession = do +expirySession = do     let req = SRequest defaultRequest ""      -- make 50 requests@@ -64,39 +64,39 @@     liftIO $ threadDelay (3 * 1000 * 1000)      -- make another request-    res <- srequest req -    assertStatus 200 res +    res <- srequest req+    assertStatus 200 res      pure ()  windowTest :: Strategy -> Session () -> Assertion-windowTest strategy tests = -    runSession tests $ rateLimiting strategy $ \req respond -> do +windowTest strategy tests =+    runSession tests $ rateLimiting strategy $ \req respond -> do         respond $ responseLBS status200 [] "OK"  getKey :: C8.ByteString -> Request -> IO C8.ByteString getKey prefix = pure . (<>) prefix . C8.pack . show . remoteHost  -- | 'redisTests' @backend@ constructs the tests using @backend@.-redisTests :: Backend C8.ByteString RedisBackendError -> TestTree+redisTests :: Backend C8.ByteString -> TestTree redisTests backend = testGroup "Redis tests"-    [ testCase "slidingWindow" $ +    [ testCase "slidingWindow" $         let strategy = slidingWindow backend 29 50 (getKey "sliding:")         in windowTest strategy rateLimitedSession-    , testCase "slidingWindow (reset)" $ +    , testCase "slidingWindow (reset)" $         let strategy = slidingWindow backend 2 50 (getKey "sliding-reset:")         in windowTest strategy expirySession-    , testCase "fixedWindow" $ +    , testCase "fixedWindow" $         let strategy = fixedWindow backend 29 50 (getKey "fixed:")         in windowTest strategy rateLimitedSession-    , testCase "fixedWindow (reset)" $ +    , testCase "fixedWindow (reset)" $         let strategy = fixedWindow backend 2 50 (getKey "fixed-reset:")         in windowTest strategy expirySession     ]  -- | 'main' is the main entry point for the test suite. main :: IO ()-main = do +main = do     -- connect to the Redis server and construct a backend for the connection     backend <- redisBackend <$> checkedConnect defaultConnectInfo 
wai-rate-limit-redis.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: cd4e697e0fbf8000edd5e95c4e59b00c44215b9cc60291b472a0ef9b4dc52553  name:           wai-rate-limit-redis-version:        0.1.0.0+version:        0.2.0.0 synopsis:       Redis backend for 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@@ -33,13 +32,15 @@       Paths_wai_rate_limit_redis   hs-source-dirs:       src-  default-extensions: OverloadedStrings LambdaCase+  default-extensions:+      OverloadedStrings+      LambdaCase   ghc-options: -W   build-depends:       base >=4.8 && <5     , bytestring >=0.9 && <0.11     , hedis >=0.12 && <1-    , wai-rate-limit >=0.1 && <1+    , wai-rate-limit >=0.2 && <1   default-language: Haskell2010  test-suite wai-rate-limit-redis-tests@@ -49,7 +50,9 @@       Paths_wai_rate_limit_redis   hs-source-dirs:       test-  default-extensions: OverloadedStrings LambdaCase+  default-extensions:+      OverloadedStrings+      LambdaCase   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:       base >=4.8 && <5