packages feed

keter-2.3.0: src/Keter/Config/Middleware.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

module Keter.Config.Middleware where

import Data.Aeson
import GHC.Generics
import Network.Wai
import Prelude

import Control.Arrow ((***))
import Control.Monad

-- various Middlewares
import Network.Wai.Middleware.AcceptOverride (acceptOverride)
import Network.Wai.Middleware.AddHeaders (addHeaders)
import Network.Wai.Middleware.Autohead (autohead)
import Network.Wai.Middleware.HttpAuth (basicAuth)
import Network.Wai.Middleware.Jsonp (jsonp)
import Network.Wai.Middleware.Local (local)
import Network.Wai.Middleware.MethodOverride (methodOverride)
import Network.Wai.Middleware.MethodOverridePost (methodOverridePost)

import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L

import Data.String (fromString)
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy.Encoding as TL
import qualified Keter.Aeson.KeyHelper as AK (empty, toKey, toList, toText)

-- Rate limiter (updated API: buildEnvFromConfig + buildRateLimiterWithEnv)
import Keter.RateLimiter.WAI
  ( RateLimiterConfig(..)
  , buildEnvFromConfig
  , buildRateLimiterWithEnv
  )

data MiddlewareConfig = AcceptOverride
                      | Autohead
                      | Jsonp
                      | MethodOverride
                      | MethodOverridePost
                      | AddHeaders ![(S.ByteString, S.ByteString)]
                      | BasicAuth !String ![(S.ByteString, S.ByteString)] 
                         -- ^ Realm [(username,password)]
                      | Local !Int !L.ByteString
                         -- ^ Status Message
                      | RateLimiter !RateLimiterConfig
                         -- ^ Rate Limiter
          deriving (Show, Generic)

instance FromJSON MiddlewareConfig where
  parseJSON (String "accept-override"     ) = pure AcceptOverride
  parseJSON (String "autohead"            ) = pure Autohead
  parseJSON (String "jsonp"               ) = pure Jsonp
  parseJSON (String "method-override"     ) = pure MethodOverride
  parseJSON (String "method-override-post") = pure MethodOverridePost
  parseJSON (Object o) =
    case AK.toList o of
      [("basic-auth", Object o')] ->
        BasicAuth  <$> o' .:? "realm" .!= "keter"
                   <*> (map ((T.encodeUtf8 . AK.toText) *** T.encodeUtf8)
                       . AK.toList <$> o' .:? "creds" .!= AK.empty)

      [("headers", Object _ )] ->
        AddHeaders . map ((T.encodeUtf8 . AK.toText) *** T.encodeUtf8)
          . AK.toList <$> o .:? "headers" .!= AK.empty

      [("local", Object o')] ->
        Local <$> o' .:? "status" .!= 401
              <*> (TL.encodeUtf8 <$> o' .:? "message"
                   .!= "Unauthorized Accessing from Localhost ONLY")

      [("rate-limiter", v)] -> RateLimiter <$> parseJSON v

      _ -> mzero
  parseJSON _ = mzero

instance ToJSON MiddlewareConfig where
  toJSON AcceptOverride     = "accept-override"
  toJSON Autohead           = "autohead"
  toJSON Jsonp              = "jsonp"
  toJSON MethodOverride     = "method-override"
  toJSON MethodOverridePost = "method-override-post"
  toJSON (BasicAuth realm cred) =
    object [ "basic-auth" .= object
              [ "realm" .= realm
              , "creds" .= object
                  (map ((AK.toKey . T.decodeUtf8) *** (String . T.decodeUtf8)) cred)
              ] ]
  toJSON (AddHeaders headers) =
    object [ "headers" .= object
              (map ((AK.toKey . T.decodeUtf8) *** String . T.decodeUtf8) headers) ]
  toJSON (Local sc msg) =
    object [ "local" .= object [ "status" .= sc
                               , "message" .= TL.decodeUtf8 msg ] ]
  toJSON (RateLimiter rl) = object [ "rate-limiter" .= toJSON rl ]

{-- Still missing
-- Clean path
-- Gzip
-- RequestLogger
-- Rewrite
-- Vhost
--}

-- Build middlewares in IO, because the RateLimiter needs mutable state.
processMiddlewareIO :: [MiddlewareConfig] -> IO Middleware
processMiddlewareIO cfgs = do
  mws <- mapM toMiddlewareIO cfgs
  pure (composeMiddleware mws)

processMiddleware :: [MiddlewareConfig] -> Middleware
processMiddleware = composeMiddleware . map toMiddlewarePure
  where
    toMiddlewarePure (RateLimiter _) = id
    toMiddlewarePure x               = unsafeToMiddleware x

composeMiddleware :: [Middleware] -> Middleware
composeMiddleware = foldl (flip (.)) id

unsafeToMiddleware :: MiddlewareConfig -> Middleware
unsafeToMiddleware AcceptOverride     = acceptOverride
unsafeToMiddleware Autohead           = autohead
unsafeToMiddleware Jsonp              = jsonp
unsafeToMiddleware MethodOverride     = methodOverride
unsafeToMiddleware MethodOverridePost = methodOverridePost
unsafeToMiddleware (Local s c)        = local (responseLBS (toEnum s) [] c)
unsafeToMiddleware (BasicAuth realm cred) =
  basicAuth (\u p -> pure $ (Just p ==) $ lookup u cred) (fromString realm)
unsafeToMiddleware (AddHeaders headers) = addHeaders headers
unsafeToMiddleware (RateLimiter _)      = id

toMiddlewareIO :: MiddlewareConfig -> IO Middleware
toMiddlewareIO AcceptOverride     = pure acceptOverride
toMiddlewareIO Autohead           = pure autohead
toMiddlewareIO Jsonp              = pure jsonp
toMiddlewareIO MethodOverride     = pure methodOverride
toMiddlewareIO MethodOverridePost = pure methodOverridePost
toMiddlewareIO (Local s c)        = pure $ local (responseLBS (toEnum s) [] c)
toMiddlewareIO (BasicAuth realm cred) =
  pure $ basicAuth (\u p -> pure $ (Just p ==) $ lookup u cred) (fromString realm)
toMiddlewareIO (AddHeaders headers) = pure $ addHeaders headers
toMiddlewareIO (RateLimiter rl) = do
  env <- buildEnvFromConfig rl
  pure (buildRateLimiterWithEnv env)