Spock-0.7.0.0: src/Web/Spock/Safe.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
module Web.Spock.Safe
( -- * Spock's core
spock, SpockM, SpockAction
, spockT, SpockT, ActionT
, spockApp
-- * Defining routes
, Path, root, var, static, (</>)
-- * Rendering routes
, renderRoute
-- * Hooking routes
, subcomponent
, get, post, head, put, delete, patch, hookRoute
, Http.StdMethod (..)
-- * Handeling requests
, request, header, cookie, body, jsonBody, jsonBody', files, UploadedFile (..)
, params, param, param'
-- * Sending responses
, setStatus, setHeader, redirect, jumpNext, setCookie, setCookie', bytes, lazyBytes
, text, html, file, json, blaze
-- * Adding middleware
, middleware
-- * Database
, PoolOrConn (..), ConnBuilder (..), PoolCfg (..)
-- * Accessing Database and State
, HasSpock (runQuery, getState), SpockConn, SpockState, SpockSession
-- * Sessions
, SessionCfg (..)
, readSession, writeSession, modifySession, clearAllSessions
-- * Basic HTTP-Auth
, requireBasicAuth
-- * Safe actions
, SafeAction (..)
, safeActionPath
-- * Digestive Functors
, runForm
-- * Internals for extending Spock
, getSpockHeart, runSpockIO, WebStateM, WebState
)
where
import Web.Spock.Internal.CoreAction
import Web.Spock.Internal.Digestive
import Web.Spock.Internal.Monad
import Web.Spock.Internal.SessionManager
import Web.Spock.Internal.Types
import Web.Spock.Internal.Wrapper
import qualified Web.Spock.Internal.Wire as W
import qualified Web.Spock.Internal.Core as C
import Control.Applicative
import Control.Monad.Trans
import Data.Monoid
import Network.HTTP.Types.Method
import Prelude hiding (head)
import Web.Routing.SafeRouting
import qualified Data.Text as T
import qualified Network.HTTP.Types as Http
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
type SpockM conn sess st a = SpockT (WebStateM conn sess st) a
newtype SpockT m a
= SpockT { runSpockT :: C.SpockAllT (SafeRouter (ActionT m) ()) m a
} deriving (Monad, Functor, Applicative, MonadIO)
instance MonadTrans SpockT where
lift = SpockT . lift
-- | Run a spock application using the warp server, a given db storageLayer and an initial state.
-- Spock works with database libraries that already implement connection pooling and
-- with those that don't come with it out of the box. For more see the 'PoolOrConn' type.
spock :: Int -> SessionCfg sess -> PoolOrConn conn -> st -> SpockM conn sess st () -> IO ()
spock port sessCfg poolOrConn initSt spockAppl =
spockAll SafeRouter port sessCfg poolOrConn initSt (runSpockT spockAppl')
where
spockAppl' =
do hookSafeActions
spockAppl
-- | Run a raw spock application with custom underlying monad
spockT :: (MonadIO m)
=> Warp.Port
-> (forall a. m a -> IO a)
-> SpockT m ()
-> IO ()
spockT port liftFun (SpockT app) =
C.spockAllT SafeRouter port liftFun app
-- | Convert a Spock-App to a wai-application
spockApp :: (MonadIO m) => (forall a. m a -> IO a) -> SpockT m () -> IO Wai.Application
spockApp liftFun (SpockT app) =
W.buildApp SafeRouter liftFun app
-- | Specify an action that will be run when the HTTP verb 'GET' and the given route match
get :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
get = hookRoute GET
-- | Specify an action that will be run when the HTTP verb 'POST' and the given route match
post :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
post = hookRoute POST
-- | Specify an action that will be run when the HTTP verb 'HEAD' and the given route match
head :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
head = hookRoute HEAD
-- | Specify an action that will be run when the HTTP verb 'PUT' and the given route match
put :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
put = hookRoute PUT
-- | Specify an action that will be run when the HTTP verb 'DELETE' and the given route match
delete :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
delete = hookRoute DELETE
-- | Specify an action that will be run when the HTTP verb 'PATCH' and the given route match
patch :: MonadIO m => Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
patch = hookRoute PATCH
-- | Specify an action that will be run when a HTTP verb and the given route match
hookRoute :: Monad m => StdMethod -> Path xs -> HListElim xs (ActionT m ()) -> SpockT m ()
hookRoute m path action = SpockT $ C.hookRoute m (SafeRouterPath path) (HListElim' action)
-- | Define a subcomponent. Usage example:
--
-- > subcomponent "site" $
-- > do get "home" homeHandler
-- > get ("misc" <> var) $ -- ...
-- > subcomponent "admin" $
-- > do get "home" adminHomeHandler
--
-- The request "/site/home" will be routed to homeHandler and the
-- request "/admin/home" will be routed to adminHomeHandler
subcomponent :: Monad m => Path '[] -> SpockT m () -> SpockT m ()
subcomponent p (SpockT subapp) = SpockT $ C.subcomponent (SafeRouterPath p) subapp
-- | Hook wai middleware into Spock
middleware :: Monad m => Wai.Middleware -> SpockT m ()
middleware = SpockT . C.middleware
-- | Write to the current session. Note that all data is stored on the server.
-- The user only reciedes a sessionId to be identified.
writeSession :: sess -> SpockAction conn sess st ()
writeSession d =
do mgr <- getSessMgr
(sm_writeSession mgr) d
-- | Modify the stored session
modifySession :: (sess -> sess) -> SpockAction conn sess st ()
modifySession f =
do mgr <- getSessMgr
(sm_modifySession mgr) f
-- | Read the stored session
readSession :: SpockAction conn sess st sess
readSession =
do mgr <- getSessMgr
sm_readSession mgr
-- | Globally delete all existing sessions. This is useful for example if you want
-- to require all users to relogin
clearAllSessions :: SpockAction conn sess st ()
clearAllSessions =
do mgr <- getSessMgr
sm_clearAllSessions mgr
-- | Wire up a safe action: Safe actions are actions that are protected from
-- csrf attacks. Here's a usage example:
--
-- > newtype DeleteUser = DeleteUser Int deriving (Hashable, Typeable, Eq)
-- >
-- > instance SafeAction Connection () () DeleteUser where
-- > runSafeAction (DeleteUser i) =
-- > do runQuery $ deleteUserFromDb i
-- > redirect "/user-list"
-- >
-- > get "/user-details/:userId" $
-- > do userId <- param' "userId"
-- > deleteUrl <- safeActionPath (DeleteUser userId)
-- > html $ "Click <a href='" <> deleteUrl <> "'>here</a> to delete user!"
--
-- Note that safeActions currently only support GET and POST requests.
--
safeActionPath :: forall conn sess st a.
( SafeAction conn sess st a
, HasSpock(SpockAction conn sess st)
, SpockConn (SpockAction conn sess st) ~ conn
, SpockSession (SpockAction conn sess st) ~ sess
, SpockState (SpockAction conn sess st) ~ st)
=> a
-> SpockAction conn sess st T.Text
safeActionPath safeAction =
do mgr <- getSessMgr
hash <- (sm_addSafeAction mgr) (PackedSafeAction safeAction)
return $ "/h/" <> hash
hookSafeActions :: forall conn sess st.
( HasSpock (SpockAction conn sess st)
, SpockConn (SpockAction conn sess st) ~ conn
, SpockSession (SpockAction conn sess st) ~ sess
, SpockState (SpockAction conn sess st) ~ st)
=> SpockM conn sess st ()
hookSafeActions =
do get (static "h" </> var) run
post (static "h" </> var) run
where
run h =
do mgr <- getSessMgr
mAction <- (sm_lookupSafeAction mgr) h
case mAction of
Nothing ->
do setStatus Http.status404
text "File not found"
Just p@(PackedSafeAction action) ->
do runSafeAction action
(sm_removeSafeAction mgr) p