hums-0.3.2: src/HttpMonad.hs
{-
hums - The Haskell UPnP Server
Copyright (C) 2009-2010 Bardur Arantsson <bardur@scientician.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module HttpMonad ( HttpT
, runHttp
, HttpResponseCode(..)
, addHeader
, setResponseCode
, setContentLength
, writeToBody
, writeFileToBody
, logDataLBS
, logMessage
, getRequest
, ifPrefix
, ifPath
) where
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.State.Strict
import Control.Monad.Trans.Reader
import Control.Applicative
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad (unless)
import Network.HTTP.Base (ResponseCode, Response(..), Request(..), urlDecode)
import Network.HTTP.Headers (Header(..), HeaderName(..))
import Network.StreamSocket()
import Network.URI (uriPath)
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.List (stripPrefix)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import SendFile (sendFile)
import Text.Printf (printf)
-- HTTP response codes.
data HttpResponseCode = OK
| PartialContent
| NotFound
| InternalServerError
| NotImplemented
-- Convert HTTP response code to a numeric code and string.
mapStatusCode :: HttpResponseCode -> (ResponseCode, String)
mapStatusCode OK = ((2,0,0), "OK")
mapStatusCode NotFound = ((4,0,4), "Not Found")
mapStatusCode InternalServerError = ((5,0,4), "Internal Server Error")
mapStatusCode NotImplemented = ((5,0,1), "Not Implemented")
mapStatusCode PartialContent = ((2,0,6), "Partial Content")
-- State of the HTTP writer monad.
data HttpState =
HttpState { hHeaders :: [Header]
, hHeadersFlushed :: Bool
, hStatusCode :: HttpResponseCode
, hOutput :: ByteString -> IO ()
}
type Req = Request String
-- Newtype wrapper for HttpT for implementation hiding.
newtype HttpT m a = HttpT { unHttp :: ReaderT Req (StateT HttpState m) a }
deriving (Functor, Monad)
-- Instances
instance MonadTrans HttpT where
lift m = HttpT $ (lift . lift) m
-- Run a Http computation.
runHttp :: (Functor m, Monad m, MonadIO m) => (ByteString -> IO ()) -> Request String -> HttpT m a -> m a
runHttp output request m = do
evalStateT (runReaderT (unHttp m') request) s0
where
s0 = HttpState [] False OK output
m' = do
a <- m
flushHeaders -- Just in case we haven't written any data.
return a
-- Add header to the response.
addHeader :: Monad m => Header -> HttpT m ()
addHeader header = HttpT $ do
lift $ modify $ \st -> st { hHeaders = header : hHeaders st }
-- Set content length.
setContentLength :: Monad m => Maybe Integer -> HttpT m ()
setContentLength (Just n) = addHeader (Header HdrContentLength $ printf "%d" n) -- FIXME: Replace!
setContentLength Nothing = return () -- FIXME: Remove!
-- Flush all the headers.
flushHeaders :: (Functor m, Monad m, MonadIO m) => HttpT m ()
flushHeaders = HttpT $ lift $ do
-- Ignore if already flushed.
alreadyFlushed <- hHeadersFlushed <$> get
unless alreadyFlushed $ do
-- Generate the headers.
output <- hOutput <$> get
headers <- hHeaders <$> get
(code,reason) <- fmap mapStatusCode $ hStatusCode <$> get
let sResponse = show $ Response { rspCode = code
, rspReason = reason
, rspHeaders = headers
, rspBody = "" }
-- Output the headers.
lift $ liftIO $ output $ encodeUtf8 $ T.pack sResponse
-- We've flushed the headers.
modify (\s -> s { hHeadersFlushed = True })
-- Set the response code.
setResponseCode :: Monad m => HttpResponseCode -> HttpT m ()
setResponseCode c = HttpT $ lift $ do
modify $ \st -> st { hStatusCode = c }
-- Write to body.
writeToBody :: (Functor m, MonadIO m, Monad m) => L.ByteString -> HttpT m ()
writeToBody buf = HttpT $ do
output <- lift $ hOutput <$> get
unHttp $ flushHeaders
lift $ liftIO $ output $ S.concat $ L.toChunks buf -- FIXME: Maybe change "output" to use lazy?
-- Write the contents of a file to body.
writeFileToBody :: (Functor m, MonadIO m, Monad m) => FilePath -> Integer -> Integer -> HttpT m ()
writeFileToBody buf offset count = HttpT $ do
output <- lift $ hOutput <$> get
unHttp $ flushHeaders
lift $ liftIO $ sendFile output buf offset count
-- Logging.
logMessage :: (Monad m, MonadIO m) => String -> HttpT m ()
logMessage s = HttpT $ liftIO $ putStrLn s
logDataLBS :: (Monad m, MonadIO m) => L.ByteString -> HttpT m ()
logDataLBS s = HttpT $ liftIO $ L.putStrLn s
-- Get request.
getRequest :: (Functor m, Monad m) => HttpT m (Request String)
getRequest = HttpT $ ask
-- Dispatch based on a test-and-forward function.
dispatchOn :: (Functor m, Monad m) => (String -> Maybe a) -> (a -> HttpT m ()) -> HttpT m () -> HttpT m ()
dispatchOn p ifMatch ifNoMatch = do
request <- getRequest
case p $ urlDecode $ uriPath $ rqURI request of
Just r -> ifMatch r
Nothing -> ifNoMatch
-- Match request prefix. The request path is stripped of the prefix
-- before being forwarded to the handler.
ifPrefix :: (Functor m, Monad m) => String -> (String -> HttpT m ()) -> HttpT m () -> HttpT m ()
ifPrefix p = dispatchOn (stripPrefix p)
-- Match full request path.
ifPath :: (Functor m, Monad m) => String -> HttpT m () -> HttpT m () -> HttpT m ()
ifPath p ifMatch ifNoMatch =
dispatchOn predicate ifMatch' ifNoMatch
where
predicate p' = if p == p' then Just () else Nothing
ifMatch' _ = ifMatch