wai-rate-limit-redis (empty) → 0.1.0.0
raw patch · 5 files changed
+346/−0 lines, 5 filesdep +basedep +bytestringdep +hedis
Dependencies added: base, bytestring, hedis, http-types, tasty, tasty-hedgehog, tasty-hunit, wai, wai-extra, wai-rate-limit, wai-rate-limit-redis, warp
Files
- LICENSE +21/−0
- README.md +92/−0
- src/Network/Wai/RateLimit/Redis.hs +59/−0
- test/Spec.hs +107/−0
- wai-rate-limit-redis.cabal +67/−0
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2020 Michael B. Gale++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,92 @@+# Rate limiting 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. ++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`. ++## Usage++### 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 C8++import Database.Redis as Redis++import Network.Wai.RateLimit+import Network.Wai.RateLimit.Strategy+import Network.Wai.RateLimit.Redis++middleware :: 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 C8++import Database.Redis as Redis++import Network.Wai.RateLimit+import Network.Wai.RateLimit.Strategy+import Network.Wai.RateLimit.Redis++middleware :: 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 C8++import Database.Redis as Redis++import Network.Wai.RateLimit+import Network.Wai.RateLimit.Strategy+import Network.Wai.RateLimit.Redis++middleware :: 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+```
+ src/Network/Wai/RateLimit/Redis.hs view
@@ -0,0 +1,59 @@+--------------------------------------------------------------------------------+-- 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. --+--------------------------------------------------------------------------------++module Network.Wai.RateLimit.Redis (+ RedisBackendError(..),+ redisBackend+) where++--------------------------------------------------------------------------------++import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C8++import Database.Redis as Redis++import Network.Wai.RateLimit.Backend++--------------------------------------------------------------------------------++-- | Represents reasons why requests made to the Redis backend have failed.+data RedisBackendError+ = RedisBackendReply Reply + | RedisBackendTxAborted+ | RedisBackendTxError String + deriving (Eq, Show) ++-- | 'redisBackend' @connection@ constructs a rate limiting 'Backend' for the+-- given redis @connection@.+redisBackend :: Connection -> Backend BS.ByteString RedisBackendError+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 + -- the key does not exist or is not a valid integer:+ -- no previous usage+ Nothing -> pure $ Right 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,+ 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,+ 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 ()+}++--------------------------------------------------------------------------------
+ test/Spec.hs view
@@ -0,0 +1,107 @@+--------------------------------------------------------------------------------+-- 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. --+--------------------------------------------------------------------------------++module Main ( main ) where++--------------------------------------------------------------------------------++import Control.Concurrent+import Control.Monad+import Control.Monad.IO.Class++import qualified Data.ByteString.Char8 as C8++import Database.Redis++import Network.HTTP.Types+import Network.Wai+import Network.Wai.Test+import Network.Wai.Handler.Warp++import Network.Wai.RateLimit+import Network.Wai.RateLimit.Backend+import Network.Wai.RateLimit.Strategy+import Network.Wai.RateLimit.Redis++import Test.Tasty+import Test.Tasty.HUnit++--------------------------------------------------------------------------------++-- | 'rateLimitedSession' makes 50 requests to the server (which should be the+-- 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 + let req = SRequest defaultRequest ""++ forM_ [0..49] $ const $ do+ res <- srequest req+ assertStatus 200 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, +-- which should succeed.+expirySession :: Session ()+expirySession = do + let req = SRequest defaultRequest ""++ -- make 50 requests+ forM_ [0..49] $ const $ do+ res <- srequest req+ assertStatus 200 res++ -- sleep for 3 seconds+ liftIO $ threadDelay (3 * 1000 * 1000)++ -- make another request+ res <- srequest req + assertStatus 200 res ++ pure ()++windowTest :: Strategy -> Session () -> Assertion+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 = testGroup "Redis tests"+ [ testCase "slidingWindow" $ + let strategy = slidingWindow backend 29 50 (getKey "sliding:")+ in windowTest strategy rateLimitedSession+ , testCase "slidingWindow (reset)" $ + let strategy = slidingWindow backend 2 50 (getKey "sliding-reset:")+ in windowTest strategy expirySession+ , testCase "fixedWindow" $ + let strategy = fixedWindow backend 29 50 (getKey "fixed:")+ in windowTest strategy rateLimitedSession+ , 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 + -- connect to the Redis server and construct a backend for the connection+ backend <- redisBackend <$> checkedConnect defaultConnectInfo++ -- run the tests with the Redis backend+ defaultMain $ redisTests backend+++--------------------------------------------------------------------------------
+ wai-rate-limit-redis.cabal view
@@ -0,0 +1,67 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: cd4e697e0fbf8000edd5e95c4e59b00c44215b9cc60291b472a0ef9b4dc52553++name: wai-rate-limit-redis+version: 0.1.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+copyright: Copyright (c) Michael B. Gale+license: MIT+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/mbg/wai-rate-limit++library+ exposed-modules:+ Network.Wai.RateLimit.Redis+ other-modules:+ Paths_wai_rate_limit_redis+ hs-source-dirs:+ src+ 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+ default-language: Haskell2010++test-suite wai-rate-limit-redis-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_wai_rate_limit_redis+ hs-source-dirs:+ test+ default-extensions: OverloadedStrings LambdaCase+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.8 && <5+ , bytestring >=0.9 && <0.11+ , hedis >=0.12 && <1+ , http-types+ , tasty+ , tasty-hedgehog+ , tasty-hunit+ , wai+ , wai-extra+ , wai-rate-limit+ , wai-rate-limit-redis+ , warp+ default-language: Haskell2010