packages feed

festung-0.9.1.1: src/Festung/Frontend.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE ViewPatterns      #-}

module Festung.Frontend
  ( App(..)
  , Widget
  , resourcesApp
  , vaultDirectory
  ) where

import           Data.Aeson.Types            as JT
import           Data.Text                   (Text)
import qualified Data.Text                   as T
import           Data.Word
import           Festung.Config
import           Festung.Frontend.Validators (validateVaultName)
import           Festung.Frontend.Converters (queryParser, errorObj, resultEncoder)
import           Festung.Utils               (getVersion)
import qualified Festung.Vault.Persistence   as P
import qualified Festung.Vault.VaultManager  as VM
import qualified Festung.Vault.VaultHandler  as VH
import qualified Festung.Vault               as V
import           System.Directory            (getDirectoryContents)
import           System.FilePath             (dropExtension)
import           Yesod
import qualified Data.ByteString             as BS
import qualified Data.ByteString.Base64      as B64
import           Data.CaseInsensitive        (CI)
import           Control.Monad
import           Data.Text.Encoding          (decodeUtf8)
import           Network.HTTP.Types.Status
import           Text.Read                   (readMaybe)


data App = App
    { config :: Config
    , vaultManager :: VM.VaultManager
    }


mkYesod "App" [parseRoutes|
    /        HomeR     GET
    /version VersionR  GET
    !/#Text  VaultR    POST DELETE
|]


addHeadersMiddleware :: Yesod site => [(Text, Text)] -> HandlerFor site res -> HandlerFor site res
addHeadersMiddleware headers handler = do
    forM_ headers $ uncurry addHeader
    handler


getDefaultHeaders :: [(Text, Text)]
getDefaultHeaders = [ ("X-Version", T.pack getVersion) ]


instance Yesod App where
    makeSessionBackend _ = return Nothing
    errorHandler NotFound =
        return $ toTypedContent $ errorObj "interface_error" "Invalid API endpoint."

    errorHandler (InvalidArgs args) =
        return $ toTypedContent $ errorObj "interface_error" message
            where message = T.unpack $ T.intercalate "\n" args

    errorHandler (PermissionDenied args) =
        return $ toTypedContent $ errorObj "interface_error" (T.unpack args)

    errorHandler err = defaultErrorHandler err -- REMOVEME
    yesodMiddleware = addHeadersMiddleware getDefaultHeaders . defaultYesodMiddleware


vaultDirectory :: App -> FilePath
vaultDirectory = dataDirectory . config


getVersionR :: Handler Value
getVersionR = return $ object [ "version" .= getVersion ]


-- FIXME(Antoine): This should actually be querying the vault manager
getHomeR :: Handler Value
getHomeR = do
    directory <- vaultDirectory <$> getYesod
    content   <- liftIO $ map dropExtension <$> listDirectory directory
    returnJson (content :: [String])
        where listDirectory directory = filter f <$> getDirectoryContents directory
              f name = V.isVault name && name `notElem` [".", ".."]


withValidName :: MonadHandler m => (Text -> m a) -> Text -> m a
withValidName view vaultName =
    case validateVaultName vaultName of
        Just err -> invalidArgs [err]
        Nothing  -> view vaultName

requireJsonBody' :: MonadHandler m => (Value -> JT.Parser a) -> m a
requireJsonBody' p = do
    res <- JT.parse p <$> requireJsonBody
    case res of
        JT.Error   err -> invalidArgs [T.pack err]
        JT.Success val -> return val


requireHeader :: MonadHandler m => CI BS.ByteString -> m BS.ByteString
requireHeader h = do
    res <- lookupHeader h
    case res of
        Just v  -> return v
        Nothing -> invalidArgs [T.pack $ "Missing header " ++ show h]


decodePassword :: BS.ByteString -> Either String [Word8]
decodePassword p = BS.unpack <$> B64.decode p


requirePassword :: MonadHandler m => m [Word8]
requirePassword = do
    res <- decodePassword <$> requireHeader "Authorization"
    case res of
        Left  err      -> invalidArgs [T.pack $ "Base64 Error " ++ show err]
        Right password -> return password


isWrongPassword :: Either VM.ManagerError a -> Bool
isWrongPassword (Left (VM.VaultError VH.CouldNotOpen)) = True
isWrongPassword _                                      = False


parseInteger :: BS.ByteString -> Maybe Integer
parseInteger = readMaybe . T.unpack . decodeUtf8


getKdfIter :: MonadHandler m => m (Maybe Integer)
getKdfIter = (>>= parseInteger) <$> lookupHeader "X-kdf-iter"


withValidOpener :: MonadHandler m => (VH.VaultOpener -> m a) -> Text -> m a
withValidOpener view = withValidName $ \ name -> do
    -- FIXME(Antoine): This should be done somewhere else. Since Festung.Vault has this constant
    -- FIXME(Antoine): T.unpack... Ew...
    let name'  = T.unpack name ++ ".sqlcipher"
    password   <- requirePassword
    kdfIter    <- getKdfIter
    let opener = (name', password, P.VaultParameters { P.kdfIter = kdfIter })
    view opener


handleError :: VM.ManagerError -> Handler Value
handleError VM.CouldNotReachManager =
    sendStatusJSON serviceUnavailable503 $
        errorObj "internal_error" $
            concat [ "This is should never happen. The vault "
                   , "manager very crashed..."
                   ]
handleError (VM.VaultError VH.CouldNotOpen) =
    sendStatusJSON forbidden403 $
        -- FIXME(Antoine): This could also be: "the vault is corrupt"
        errorObj "interface_error" "Could not open the vault"
handleError (VM.VaultError VH.CouldNotReach) =
    sendStatusJSON serviceUnavailable503 $
        errorObj "internal_error" $
            concat [ "This error should almost never happen. "
                   , "The vault was closed while trying to be accessed, "
                   , "we tried to re-open it multiple times and failed."
                   ]
handleError (VM.VaultError (VH.VaultError (P.InternalError d))) =
    sendStatusJSON badRequest400 $ errorObj "internal_error" d

handleError (VM.VaultError (VH.VaultError (P.NotSupportedError d))) =
    sendStatusJSON badRequest400 $ errorObj "not_supported" d

handleError (VM.VaultError (VH.VaultError (P.IntegrityError _ d))) =
    sendStatusJSON badRequest400 $ errorObj "integrity_error" d

handleError (VM.VaultError (VH.VaultError (P.OperationalError _ d))) =
    sendStatusJSON badRequest400 $ errorObj "operational_error" d

handleError (VM.VaultError (VH.VaultError (P.DatabaseError _ d))) =
    sendStatusJSON badRequest400 $ errorObj "database_error" d

handleError (VM.VaultError (VH.VaultError (P.DataError _ d))) =
    sendStatusJSON badRequest400 $ errorObj "data_error" d

handleError (VM.VaultError (VH.VaultError (P.ProgrammingError _ d))) =
    sendStatusJSON badRequest400 $ errorObj "programming_error" d


postVaultR :: Text -> Handler Value
postVaultR = withValidOpener $ \opener -> do
    manager       <- vaultManager <$> getYesod
    (sql, params) <- requireJsonBody' queryParser
    results       <- liftIO $ VM.parametrizedQuery opener manager sql params
    when (isWrongPassword results) $ permissionDenied "Wrong password."
    case results of
        Left  err -> handleError err
        Right res -> returnJson $ resultEncoder res

deleteVaultR :: Text -> Handler Value
deleteVaultR = withValidOpener $ \opener -> do
    manager <- vaultManager <$> getYesod
    results <- liftIO $ VM.deleteVault opener manager
    when (isWrongPassword results) $ permissionDenied "Wrong password."
    sendResponseStatus status204 ()