lio-simple-0.0.0.3: LIO/Web/Simple/TCB.hs
{-# LANGUAGE Unsafe #-}
{-# LANGUAGE FlexibleContexts #-}
{- |
This module exports a function 'run' for creating a runner that is
used to run a "Web.Simple" 'SimpleApplication' in the 'LIO' monad.
The runner is only available to trusted code since we do not impose
any policy on how requests and responses should be handled.
Middleware should be used on both ends to ensure safety. This module
provides several such Middleware.
-}
module LIO.Web.Simple.TCB (
-- * LIO applications
SimpleLIOApplication, SimpleLIOMiddleware
-- * Runners
, run, runP
-- * Middleware
, browserLabelGuard
, removeRequestHeaders
, removeResponseHeaders
-- * Templates
, lioGetTemplateTCB
) where
import safe Control.Monad
import Data.Text.Encoding
import safe Control.Applicative
import safe LIO
import safe LIO.Error
import LIO.TCB (ioTCB, getLIOStateTCB)
import safe qualified Data.List as List
import safe qualified Data.ByteString.Char8 as S8
import safe qualified Data.ByteString.Lazy.Char8 as L8
import safe Web.Simple
import safe Web.Simple.Controller.Trans
import safe Web.Simple.Templates.Language
import Network.HTTP.Types
import Network.Wai.Internal
import Network.Wai.Handler.Warp hiding (run)
import qualified Network.Wai.Handler.Warp as Warp
import safe System.FilePath
-- | An LIO simple aplpication is an 'LIO' computation mapping a set
-- of privileges and request to a response. While privileges can be
-- provided in terms of a e.g., 'Reader' monad, in certain cases not
-- having the privilege as part of the sate is cleaner.
type SimpleLIOApplication p l = Priv p -> SimpleApplication (LIO l)
-- | Simple LIO middleware.
type SimpleLIOMiddleware p l = SimpleLIOApplication p l -> SimpleLIOApplication p l
-- | Run an LIO web app wrapped by some middleware. Since web servers
-- can be quite messy it is important that you provide middleware to
-- sanitize responses to prevent data leakage.
--
-- Since security properties vary across applications, we do not
-- impose any conditions on the requests and reponses. The latter can
-- be sanitized by supplying a middleware, while the former can simply
-- be baked-into the app (as 'SimpleMiddleware'.
run :: Label l => Port -> Middleware -> SimpleApplication (LIO l) -> LIO l ()
run port middleware app = runP port middleware noPrivs (const app)
-- | Same as 'run', but run 'SimpleLIOApplication's, i.e.,
-- applications that take privileges.
runP :: (PrivDesc l p, Label l)
=> Port -> Middleware -> Priv p -> SimpleLIOApplication p l -> LIO l ()
runP port middleware priv app = do
state <- getLIOStateTCB
ioTCB $ Warp.run port $ middleware . filterFileResponses $
\req -> evalLIO (app priv req) state
-- | Remove any responses that were built with 'responseFile' or
-- 'responseSource'.
filterFileResponses :: Middleware
filterFileResponses app req = do
resp <- app req
return $ case resp of
ResponseBuilder _ _ _ -> resp
_ -> serverError $ L8.pack "App should not read directly from files."
-- | Middleware that ensures the 'Response' from the
-- application is readable by the client's browser (as determined by the
-- result label of the app computation and the label of the browser). If
-- the response is not readable by the browser, the middleware sends a
-- 403 (unauthorized) response instead.
browserLabelGuard :: MonadLIO l m => l -> SimpleMiddleware m
browserLabelGuard browserLabel app req = do
resp <- app req
resultLabel <- liftLIO $ getLabel
return $ if resultLabel `canFlowTo` browserLabel
then resp
else forbidden
-- | Remove certain headers from the request.
removeRequestHeaders :: Monad m => [HeaderName] -> SimpleMiddleware m
removeRequestHeaders headers app req = do
app $ foldr (\h r -> rmRequestHeader r h) req headers
where rmRequestHeader r h = r { requestHeaders = rm h (requestHeaders r) }
-- | Remove certain headers from the response, e.g., Set-Cookie.
removeResponseHeaders :: Monad m => [HeaderName] -> SimpleMiddleware m
removeResponseHeaders headers app req = do
resp <- app req
return $ foldr (\h r -> rmResponseHeader r h) resp headers
rmResponseHeader :: Response -> HeaderName -> Response
rmResponseHeader (ResponseFile x hs y z) h = ResponseFile x hs' y z where hs' = rm h hs
rmResponseHeader (ResponseBuilder x hs y ) h = ResponseBuilder x hs' y where hs' = rm h hs
rmResponseHeader (ResponseSource x hs y ) h = ResponseSource x hs' y where hs' = rm h hs
rmResponseHeader (ResponseRaw x r ) h = ResponseRaw x (rmResponseHeader r h)
rm :: HeaderName -> [Header] -> [Header]
rm h = List.filter ((/= h) . fst)
-- | Function to use to get a template. When the underlying monad is
-- 'LIO', it looks in the 'viewDirectory' for the given file name and
-- compiles the file into a template.
--
-- This function should be used only when the everything reachable
-- from the 'viewDirectory' is public.
--
-- To ensure that the function cannot be abused the function first
-- cleans up the file path: if it starts out with a @..@, we consider
-- this invalid as it can be used explore parts of the filesystem that
-- should otherwise be unaccessible. Similarly, we remove any @.@ from
-- the path.
--
-- Since this funciton does not use the 'lio-fs' filesystem @readFile@,
-- but rather the 'IO' @readFile@, it should not be exposed to
-- untrusted code.
lioGetTemplateTCB :: Label l => FilePath -> LIO l Template
lioGetTemplateTCB fp = do
fp' <- cleanUpPath fp
eres <- compileTemplate . decodeUtf8 <$> (ioTCB $ S8.readFile fp')
case eres of
Left str -> fail str
Right tmpl -> return tmpl
-- | Cleanup a file path, if it starts out with a @..@, we consider this
-- invalid as it can be used explore parts of the filesystem that should
-- otherwise be unaccessible. Similarly, we remove any @.@ from the path.
cleanUpPath :: Label l => FilePath -> LIO l FilePath
cleanUpPath path = withContext "cleanUpPath" $
doit . splitDirectories . normalise . stripSlash $ path
where doit [] = return []
doit ("..":_) = throwLIO $ userError "Path cannot contain .."
doit (_:"..":xs) = doit xs
doit (".":xs) = doit xs
doit (x:xs) = (x </>) `liftM` doit xs
-- | Remove any 'pathSeparator's from the front of a file path.
stripSlash :: FilePath -> FilePath
stripSlash [] = []
stripSlash xx@(x:xs) | x == pathSeparator = stripSlash xs
| otherwise = xx