packages feed

servant-rate-limit (empty) → 0.1.0.0

raw patch · 9 files changed

+686/−0 lines, 9 filesdep +basedep +bytestringdep +hedis

Dependencies added: base, bytestring, hedis, http-client, http-types, servant, servant-client, servant-rate-limit, servant-server, tasty, tasty-hunit, wai, wai-extra, wai-rate-limit, wai-rate-limit-redis, warp

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Changelog for `servant-rate-limit`++## 0.1++- Initial release
+ 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,151 @@+# 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` 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`++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 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+```++## 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)++```
+ servant-rate-limit.cabal view
@@ -0,0 +1,118 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           servant-rate-limit+version:        0.1.0.0+synopsis:       Rate limiting for Servant+description:    A Haskell library which implements rate limiting for Servant+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:     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+  location: https://github.com/mbg/wai-rate-limit++flag client+  description: Enable servant-client support.+  manual: True+  default: True++flag server+  description: Enable servant-server support.+  manual: True+  default: True++library+  exposed-modules:+      Servant.RateLimit+      Servant.RateLimit.Types+  other-modules:+      Paths_servant_rate_limit+  hs-source-dirs:+      src+  default-extensions:+      DataKinds+      FlexibleContexts+      FlexibleInstances+      MultiParamTypeClasses+      OverloadedStrings+      RecordWildCards+      TypeApplications+      TypeFamilies+      TypeOperators+  ghc-options: -Wall+  build-depends:+      base >=4.8 && <5+    , bytestring+    , http-types <1+    , servant+    , wai >=3.0 && <4+    , wai-rate-limit >=0.2 && <1+  if flag(server)+    build-depends:+        servant-server+  if flag(client)+    build-depends:+        servant-client+  if flag(server)+    exposed-modules:+        Servant.RateLimit.Server+  if flag(client)+    exposed-modules:+        Servant.RateLimit.Client+  default-language: Haskell2010++test-suite servant-rate-limit-tests+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_servant_rate_limit+  hs-source-dirs:+      test+  default-extensions:+      DataKinds+      FlexibleContexts+      FlexibleInstances+      MultiParamTypeClasses+      OverloadedStrings+      RecordWildCards+      TypeApplications+      TypeFamilies+      TypeOperators+  build-depends:+      base >=4.8 && <5+    , bytestring+    , hedis+    , http-client+    , http-types <1+    , servant+    , servant-client+    , servant-rate-limit+    , servant-server+    , tasty+    , tasty-hunit+    , wai >=3.0 && <4+    , wai-extra+    , wai-rate-limit >=0.2 && <1+    , wai-rate-limit-redis+    , warp+  if flag(server)+    build-depends:+        servant-server+  if flag(client)+    build-depends:+        servant-client+  default-language: Haskell2010
+ src/Servant/RateLimit.hs view
@@ -0,0 +1,32 @@+--------------------------------------------------------------------------------+-- Rate Limiting Middleware for Servant                                       --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++-- | This library implements Servant combinators for applying rate limiting+-- strategies from "Network.Wai.RateLimit.Strategy". Different strategies may+-- be applied to different parts of the API. For example:+-- @+-- 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+-- @+module Servant.RateLimit (+    module RateLimit+) where++--------------------------------------------------------------------------------++import Servant.RateLimit.Types as RateLimit++--------------------------------------------------------------------------------
+ src/Servant/RateLimit/Client.hs view
@@ -0,0 +1,28 @@+--------------------------------------------------------------------------------+-- Rate Limiting Middleware for Servant                                       --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++{-# OPTIONS_GHC -Wno-orphans #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Servant.RateLimit.Client where++--------------------------------------------------------------------------------++import Servant+import Servant.Client+import Servant.RateLimit.Types++--------------------------------------------------------------------------------++instance HasClient m api => HasClient m (RateLimit st p :> api) where+    type Client m (RateLimit st p :> api) = Client m api++    hoistClientMonad mp _ = hoistClientMonad mp (Proxy :: Proxy api)++    clientWithRoute mp _ = clientWithRoute mp (Proxy :: Proxy api)++--------------------------------------------------------------------------------
+ src/Servant/RateLimit/Server.hs view
@@ -0,0 +1,76 @@+--------------------------------------------------------------------------------+-- Rate Limiting Middleware for Servant                                       --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++{-# OPTIONS_GHC -Wno-orphans #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++module Servant.RateLimit.Server where++--------------------------------------------------------------------------------++import Control.Monad+import Control.Monad.IO.Class++import Data.ByteString as BS+import Data.ByteString.Char8 as C8+import Data.Kind++import Network.Wai+import Network.Wai.RateLimit.Backend+import Network.Wai.RateLimit.Strategy++import Servant+import Servant.RateLimit.Types+import Servant.Server.Internal.Delayed+import Servant.Server.Internal.DelayedIO++--------------------------------------------------------------------------------++instance+    ( HasServer api ctx+    , HasContextEntry ctx (Backend key)+    , HasRateLimitStrategy strategy+    , HasRateLimitPolicy policy+    , key ~ RateLimitPolicyKey policy+    ) => HasServer (RateLimit strategy policy :> api) ctx+    where++    type ServerT (RateLimit strategy policy :> api) m = ServerT api m++    hoistServerWithContext _ pc nt s =+        hoistServerWithContext (Proxy :: Proxy api) pc nt s++    route _ context subserver = do+        -- retrieve the backend from the Servant context+        let backend = getContextEntry context++        -- retrieve the rate-limiting policy used to identify clients+        let policy = policyGetIdentifier @policy++        -- retrieve the rate-limiting strategy used to limit access+        let strategy = strategyValue @strategy @key backend policy++        let rateCheck = withRequest $ \req -> do+                -- apply the rate-limiting strategy to the request+                allowRequest <- liftIO $ strategyOnRequest strategy req++                -- fail if the rate limit has been exceeded+                unless allowRequest $ delayedFailFatal $ ServerError{+                    errHTTPCode = 429,+                    errReasonPhrase = "Rate limit exceeded",+                    errBody = "",+                    errHeaders = []+                }++        -- add the check for whether the rate limit has been exceeded to the+        -- server and return it+        route (Proxy :: Proxy api) context $+            subserver `addAcceptCheck` rateCheck++--------------------------------------------------------------------------------
+ src/Servant/RateLimit/Types.hs view
@@ -0,0 +1,114 @@+--------------------------------------------------------------------------------+-- Rate Limiting Middleware for Servant                                       --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Servant.RateLimit.Types (+    -- * Servant combinator+    RateLimit,++    -- * Rate-limiting strategies+    FixedWindow,+    SlidingWindow,+    HasRateLimitStrategy(..),++    -- * Rate-limiting policies+    IPAddressPolicy,+    HasRateLimitPolicy(..)+) where++--------------------------------------------------------------------------------++import GHC.TypeLits++import Data.ByteString.Char8 as C8+import Data.Kind+import Data.Proxy++import Network.Wai+import Network.Wai.RateLimit.Backend+import Network.Wai.RateLimit.Strategy++--------------------------------------------------------------------------------++-- | A type-level description for the parameters of the `fixedWindow` strategy.+data FixedWindow (secs :: Nat) (capacity :: Nat)++-- | A type-level description for the parameters of the `slidingWindow`+-- strategy.+data SlidingWindow (secs :: Nat) (capacity :: Nat)++-- | A class of types which are type-level descriptions of rate-limiting+-- strategies.+class HasRateLimitStrategy strategy where+    -- | `strategyValue` @backend getKey@ is a function which, given a+    -- @backend@ and a function @getKey@ used to compute the key using which+    -- the client should be identified, returns a rate-limiting `Strategy`.+    strategyValue :: Backend key -> (Request -> IO key) -> Strategy++instance (KnownNat secs, KnownNat capacity)+    => HasRateLimitStrategy (FixedWindow secs capacity)+    where++    strategyValue backend getKey = fixedWindow+        backend+        (fromInteger $ natVal (Proxy :: Proxy secs))+        (fromInteger $ natVal (Proxy :: Proxy capacity))+        getKey++instance (KnownNat secs, KnownNat capacity)+    => HasRateLimitStrategy (SlidingWindow secs capacity)+    where++    strategyValue backend getKey = slidingWindow+        backend+        (fromInteger $ natVal (Proxy :: Proxy secs))+        (fromInteger $ natVal (Proxy :: Proxy capacity))+        getKey++--------------------------------------------------------------------------------++-- | A simple rate-limiting policy which applies a rate-limiting strategy+-- based on the client's IP address. This policy is useful mainly for testing+-- purposes. For production use, you should implement your own policy based+-- on e.g. the current user, API key, etc. The @prefix@ parameter may be set+-- to the empty string if all API endpoints count towards the same rate limit,+-- but can be set to other values to have different rate limits for different+-- sets of endpoints.+data IPAddressPolicy (prefix :: Symbol)++-- | A class of types which are type-level descriptions of rate-limiting+-- policies.+class HasRateLimitPolicy policy where+    type RateLimitPolicyKey policy :: Type++    -- | `policyGetIdentifier` @request@ computes the key that should be+    -- used by the backend to identify the client to which the rate+    -- limiting policy should be applied to. This could be as simple+    -- as retrieving the IP address of the client from @request@+    -- (as is the case with `IPAddressPolicy`) or retrieving data from+    -- the @request@ vault. The computation runs in `IO` to allow policies+    -- to perform arbitrary effects.+    policyGetIdentifier :: Request -> IO (RateLimitPolicyKey policy)++instance KnownSymbol prefix => HasRateLimitPolicy (IPAddressPolicy prefix) where+    type RateLimitPolicyKey (IPAddressPolicy prefix) = ByteString++    policyGetIdentifier =+        pure . (C8.pack (symbolVal (Proxy :: Proxy prefix)) <>) .+        C8.pack . show . remoteHost++--------------------------------------------------------------------------------++-- | A generalised rate limiting combinator which combines type-level+-- descriptions of a rate-limiting strategy, such as `FixedWindow`, with a+-- type-level description of a rate-limiting policy, such as `IPAddressPolicy`.+data RateLimit strategy policy++--------------------------------------------------------------------------------
+ test/Spec.hs view
@@ -0,0 +1,141 @@+--------------------------------------------------------------------------------+-- Rate Limiting Middleware for Servant                                       --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++import Control.Monad++import Database.Redis++import Network.HTTP.Client (newManager, defaultManagerSettings)+import Network.Wai.Handler.Warp as Warp+import Network.Wai.RateLimit.Redis++import Servant+import Servant.Client+import Servant.Server+import Servant.RateLimit+import Servant.RateLimit.Client+import Servant.RateLimit.Server++import Test.Tasty+import Test.Tasty.HUnit+import Control.Monad.IO.Class+import Control.Concurrent++--------------------------------------------------------------------------------++-- | The API we use for out tests, which has endpoints for the different+-- rate limiting strategies as well as an unrestricted endpoint.+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++-- | `testApi` is a `Proxy` for `TestAPI`, as required by Servant.+testApi :: Proxy TestAPI+testApi = Proxy++-- | `server` implements a Servant `Server` for `TestAPI`.+server :: Server TestAPI+server =+    pure "Fixed window" :<|>+    pure "Sliding window" :<|>+    pure "Unrestricted"++getFixedWindow :: ClientM String+getSlidingWindow :: ClientM String+getUnrestricted :: ClientM String+getFixedWindow :<|> getSlidingWindow :<|> getUnrestricted = client testApi++--------------------------------------------------------------------------------++-- | `appTestCase` @app name handler@ is a wrapper around `testCase` which+-- constructs a `TestTree` named @name@, but for which we run a web server+-- running @app@. The test @handler@ is given a Servant `ClientEnv` which is+-- configured to communicate with the web server.+appTestCase :: Application -> TestName -> (ClientEnv -> Assertion) -> TestTree+appTestCase app name test =+    testCase name $+    Warp.testWithApplication (pure app) $ \port -> do++        manager <- newManager defaultManagerSettings++        let baseUrl = BaseUrl Http "localhost" port ""+        let env = mkClientEnv manager baseUrl{ baseUrlPort = port }++        test env++-- | `assertSuccess` @result@ fails if @result@ indicates an error.+assertSuccess :: Either ClientError a -> Assertion+assertSuccess (Left err) = assertFailure (show err)+assertSuccess _ = pure ()++-- | `assertFailed` @result@ fails if @result@ indicates success.+assertFailed :: Either ClientError a -> Assertion+assertFailed (Right _) =+    assertFailure "Excepted the request to fail, but it succeeded"+assertFailed _ = pure ()++rateLimitedSession :: ClientM a -> ClientEnv -> Assertion+rateLimitedSession endpoint env = do+    forM_ [0..49] $ const $ do+        res <- runClientM endpoint env+        assertSuccess res++    res <- runClientM endpoint env+    assertFailed res++    pure ()++expirySession :: ClientM a -> ClientEnv -> Assertion+expirySession endpoint env = do+    -- make 50 requests+    forM_ [0..49] $ const $ do+        res <- runClientM endpoint env+        assertSuccess res++    -- sleep for 3 seconds+    liftIO $ threadDelay (3 * 1000 * 1000)++    -- make another request+    res <- runClientM endpoint env+    assertSuccess res++tests :: Application -> TestTree+tests app = testGroup "Servant.RateLimiting"+    [ appTestCase app "Fixed window: gets rate limited" $+        rateLimitedSession getFixedWindow+    , appTestCase app "Fixed window: rate limit resets" $+        expirySession getFixedWindow+    , appTestCase app "Sliding window: gets rate limited" $+        rateLimitedSession getSlidingWindow+    , appTestCase app "Sliding window: rate limit resets" $+        expirySession getSlidingWindow+    ]++-- | `main` is the main entry point for the test suite in which we initialise+-- a Redis connection and the Servant server before running the tests.+main :: IO ()+main = do+    -- connect to the Redis server and construct a backend for the connection+    backend <- redisBackend <$> checkedConnect defaultConnectInfo++    -- stick the Redis backend into the Servant context so that we can access+    -- it when we try to apply rate limiting policies+    let ctx = backend :. EmptyContext++    -- construct the Servant application using the context+    let app = serveWithContext testApi ctx server++    -- run the tests+    defaultMain (tests app)++--------------------------------------------------------------------------------