From c48d16a4f4311ecdaa0620b39e2334cd57b00e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hamal=20Dvo=C5=99=C3=A1k?= <mordae@anilinux.org> Date: Thu, 18 Jun 2020 01:12:17 +0200 Subject: [PATCH] Integrate with configurator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I am still not comfortable with this. Ideally there should not be such a tight coupling and a different configuration engine could be plugged in instead. On the other hand, configurator offers environment variable interpolation, has decent syntax and generally works. Signed-off-by: Jan Hamal Dvořák <mordae@anilinux.org> --- hikaru.cabal | 4 ++- lib/Hikaru/Action.hs | 62 +++++++++++++++++++++++++++++++++----------- lib/Hikaru/CSRF.hs | 35 +++++++------------------ lib/Hikaru/Form.hs | 2 +- package.yaml | 1 + 5 files changed, 62 insertions(+), 42 deletions(-) diff --git a/hikaru.cabal b/hikaru.cabal index 0cd8be6..2d65f29 100644 --- a/hikaru.cabal +++ b/hikaru.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 29d0cf827300a88d2c302f25025d2f8d70bf6580c115003871c8397c80211d86 +-- hash: abedad9337bb68e5ffcb0364bf2152ac0633c4810698dfc652cd122bedd3290c name: hikaru version: 0.1.0.0 @@ -56,6 +56,7 @@ library , binary >=0.8 && <0.9 , bytestring >=0.10 && <0.11 , case-insensitive >=1.2 && <1.3 + , configurator >=0.3 && <0.4 , containers >=0.6 && <0.7 , cookie >=0.4 && <0.5 , cryptonite >=0.26 && <0.27 @@ -94,6 +95,7 @@ test-suite spec , binary >=0.8 && <0.9 , bytestring >=0.10 && <0.11 , case-insensitive >=1.2 && <1.3 + , configurator >=0.3 && <0.4 , containers >=0.6 && <0.7 , cookie >=0.4 && <0.5 , cryptonite >=0.26 && <0.27 diff --git a/lib/Hikaru/Action.hs b/lib/Hikaru/Action.hs index ad29fbe..35a3b58 100644 --- a/lib/Hikaru/Action.hs +++ b/lib/Hikaru/Action.hs @@ -88,6 +88,10 @@ module Hikaru.Action , dropCache , dropCaches + -- ** Configuration + , getConfigMaybe + , getConfigDefault + -- ** Finalizing , registerFinalizer @@ -103,9 +107,11 @@ where import BasePrelude hiding (length) import qualified Data.ByteString.Lazy as LBS + import qualified Data.Configurator as Cfg + import qualified Data.Configurator.Types as Cfg + import qualified Data.Map.Strict as Map import qualified Data.Text.Lazy as LT import qualified Network.Wai.Parse as Parse - import qualified Data.Map.Strict as Map import Control.Monad.Trans import Control.Monad.Trans.Resource @@ -186,6 +192,7 @@ where data ActionEnv = ActionEnv { aeRequest :: Request + , aeConfig :: Cfg.Config , aeBody :: IORef RequestBody , aeRespStatus :: IORef Status , aeRespHeaders :: IORef ResponseHeaders @@ -205,9 +212,9 @@ where -- -- Whole operation is bracketed to ensure all finalizers are run. -- - respond :: (ActionEnv -> IO ()) -> Application - respond run req resp = do - env <- makeActionEnv req + respond :: (ActionEnv -> IO ()) -> Cfg.Config -> Application + respond run cfg req resp = do + env <- makeActionEnv cfg req bracket_ (return ()) (finalize env) do _ <- run env @@ -255,17 +262,19 @@ where -- | -- Create the initial action environment from the 'Request'. -- - makeActionEnv :: Request -> IO ActionEnv - makeActionEnv req = ActionEnv <$> pure req - <*> newIORef BodyUnparsed - <*> newIORef status200 - <*> newIORef [] - <*> newIORef (\st hs -> responseLBS st hs "") - <*> newIORef (return ()) - <*> newIORef (10 * 1024 * 1024) - <*> newIORef 0 - <*> newIORef [] - <*> newIORef Map.empty + makeActionEnv :: Cfg.Config -> Request -> IO ActionEnv + makeActionEnv cfg req = + ActionEnv <$> pure req + <*> pure cfg + <*> newIORef BodyUnparsed + <*> newIORef status200 + <*> newIORef [] + <*> newIORef (\st hs -> responseLBS st hs "") + <*> newIORef (return ()) + <*> newIORef (10 * 1024 * 1024) + <*> newIORef 0 + <*> newIORef [] + <*> newIORef Map.empty -- Inspecting Request ------------------------------------------------------ @@ -1081,6 +1090,29 @@ where modifyActionField aeCache (const Map.empty) + -- Configuration ----------------------------------------------------------- + + + -- | + -- Lookup a configuration value. + -- + getConfigMaybe :: (MonadAction m, Cfg.Configured a) => Text -> m (Maybe a) + getConfigMaybe name = do + cfg <- aeConfig <$> getActionEnv + liftIO do + Cfg.lookup cfg name + + + -- | + -- Lookup a configuration value with a fallback to return if not found. + -- + getConfigDefault :: (MonadAction m, Cfg.Configured a) => Text -> a -> m a + getConfigDefault name value = do + cfg <- aeConfig <$> getActionEnv + liftIO do + Cfg.lookupDefault value cfg name + + -- Finalizing -------------------------------------------------------------- diff --git a/lib/Hikaru/CSRF.hs b/lib/Hikaru/CSRF.hs index f44492a..67fc98c 100644 --- a/lib/Hikaru/CSRF.hs +++ b/lib/Hikaru/CSRF.hs @@ -7,18 +7,16 @@ Maintainer : mordae@anilinux.org Stability : unstable Portability : non-portable (ghc) -CSRF mitigation utilities. +This module provides CSRF mitigation utilities. -} module Hikaru.CSRF - ( MonadCsrf(..) - , generateToken + ( generateToken , isTokenValid ) where import BasePrelude - import Control.Monad.Trans import Crypto.Hash import Crypto.MAC.HMAC import Data.ByteString (ByteString) @@ -26,46 +24,33 @@ where import Data.Text (Text, splitOn) import Data.Time.Clock.POSIX (getPOSIXTime) - - class (MonadIO m) => MonadCsrf m where - csrfTokenValidity :: m Int64 - csrfTokenSecret :: m Text - - default csrfTokenValidity - :: (MonadTrans t, MonadCsrf n, m ~ t n) => m Int64 - csrfTokenValidity = lift csrfTokenValidity - {-# INLINE csrfTokenValidity #-} - - default csrfTokenSecret - :: (MonadTrans t, MonadCsrf n, m ~ t n) => m Text - csrfTokenSecret = lift csrfTokenSecret - {-# INLINE csrfTokenSecret #-} + import Hikaru.Action -- | - -- TODO + -- Generate an anti-CSRF token to be used with forms. -- - generateToken :: (MonadCsrf m) => m Text + generateToken :: (MonadAction m) => m Text generateToken = do now <- getTimestamp - secret <- csrfTokenSecret + secret <- getConfigDefault "csrf.secret" "" let signature = sign now secret in return $ mconcat [ cs (show now), ":", signature ] -- | - -- TODO + -- Verify that the anti-CSRF token is currently valid. -- - isTokenValid :: (MonadCsrf m) => Text -> m Bool + isTokenValid :: (MonadAction m) => Text -> m Bool isTokenValid token = do case splitOn ":" token of [time, signature] -> do case readMaybe (cs time) of Just (timestamp :: Int64) -> do now <- getTimestamp - valid <- csrfTokenValidity - secret <- csrfTokenSecret + valid <- getConfigDefault "csrf.validity" 86400 + secret <- getConfigDefault "csrf.secret" "" if timestamp + valid >= now then return (sign timestamp secret == signature) diff --git a/lib/Hikaru/Form.hs b/lib/Hikaru/Form.hs index b125785..e4d95e3 100644 --- a/lib/Hikaru/Form.hs +++ b/lib/Hikaru/Form.hs @@ -625,7 +625,7 @@ where -- -- TODO: Add an example. -- - token :: (MonadCsrf m, FromFormMessage l) => Text -> FormT l o m Text + token :: (MonadAction m, FromFormMessage l) => Text -> FormT l o m Text token name = hiddenValue name (Just <$> generateToken) do validate \case diff --git a/package.yaml b/package.yaml index 6e2a8b8..d697cb7 100644 --- a/package.yaml +++ b/package.yaml @@ -49,6 +49,7 @@ dependencies: - binary >= 0.8 && <0.9 - bytestring >= 0.10 && <0.11 - case-insensitive >= 1.2 && <1.3 + - configurator >= 0.3 && <0.4 - containers >= 0.6 && <0.7 - cookie >= 0.4 && <0.5 - cryptonite >= 0.26 && <0.27 -- GitLab