{-# 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