hums-0.3.3: src/Handlers.hs
{-
hums - The Haskell UPnP Server
Copyright (C) 2009 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 Handlers ( rootDescriptionHandler
, staticHandler
, serviceControlHandler
, contentHandler
, fallbackHandler
, State
) where
import Soap
import Configuration
import Service
import Text.Printf
import Action
import Blaze.ByteString.Builder (insertByteString)
import System.IO (withFile, hFileSize, IOMode(..))
import MimeType
import Object
import HttpExtra
import System.FilePath
import Data.ByteString (ByteString, isInfixOf)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as B8
import Data.IORef
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Network.Wai
import Data.Enumerator (Iteratee, ($$), ($=))
import Data.Enumerator.Binary (enumFileRange)
import qualified Data.Enumerator as E
import qualified Data.Enumerator.List as EL
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Network.HTTP.Types (Status, Header, status206, statusServerError, statusOK, statusNotFound, headerContentType, headerContentLength, headerConnection)
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
type State = (Configuration, MediaServerConfiguration, ApplicationInformation, [DeviceType], IORef Objects)
{-
RFC2616 (HTTP/1.1) compliance issues:
- No conditional range support.
- Can only handle single ranges.
- Negative range indexes are not supported.
- Handling of invalid range specifications is non-compliant.
It does work well enough for the PS3 though :).
-}
fileSize :: FilePath -> IO Integer
fileSize fp =
withFile fp ReadMode $ \h -> hFileSize h
serveStaticFile :: Request -> ByteString -> FilePath -> Iteratee ByteString IO Response
serveStaticFile req mimeType fp = do
logMessage $ printf "Serving file '%s'..." fp
-- Do we have a range header?
let ranges = case lookup rangeHeader $ requestHeaders req of
Just value -> parseRangeHeader $ B8.unpack value
Nothing -> [(Nothing, Nothing)] -- whole file
-- Serve the ranges.
fsz <- lift $ fileSize fp
serveFile fsz ranges
where
serveFile fsz [(l,h)] = do
let l' = maybe 0 id l
let h' = maybe (fsz-1) id h
let n = (h' - l' + 1)
return $ ResponseEnumerator $ \f ->
E.run_ $ (enumFileRange fp l h $= EL.map insertByteString) $$ f status206
[ hdrContentLength n
, headerContentType mimeType
, hdrContentRange l' h' fsz
, hdrAcceptRangesBytes
, hdrConnectionClose
]
serveFile _ _ = do
-- This requires multipart/byteranges, but we don't support that as of yet.
sendError statusServerError
-- Regular expressions for avoiding relative URLs. These
-- are overly conservative, but what the heck...
dotDotSlash :: ByteString
dotDotSlash = "../"
slashDotDot :: ByteString
slashDotDot = "/.."
-- Handler for the root description.
rootDescriptionHandler :: State -> Iteratee ByteString IO Response
rootDescriptionHandler (c,mc,ai,s,_) = do
logMessage "Got request for root description."
let xml = generateDescriptionXml c mc s
return $ responseLBS statusOK [ hdrConnectionClose
, hdrContentLength (L.length xml)
, headerContentType "text/xml"
] xml
-- Handle static files.
staticHandler :: Request -> String -> ByteString -> Iteratee ByteString IO Response
staticHandler req root p = do
logMessage $ "Got request for static content: " ++ (show p)
if dotDotSlash `isInfixOf` p || -- Reject relative URLs.
slashDotDot `isInfixOf` p then
sendError statusServerError
else
serveStaticFile req mimeType fp
where
fp = root </> (T.unpack $ decodeUtf8 p)
mimeType = guessMimeType fp
-- Handle requests for content.
contentHandler :: Request -> State -> ByteString -> Iteratee ByteString IO Response
contentHandler req (c,mc,ai,s,objects_) oid = do
objects <- lift $ readIORef objects_ -- Current snapshot of object tree.
logMessage $ printf "Got request for CONTENT for objectId=%s" (B8.unpack oid)
-- Serve the file which the object maps to.
case findByObjectId oid objects of
Just o ->
serveStaticFile req mt fp
where
od = getObjectData o
fp = objectFileName od
mt = objectMimeType od
Nothing ->
sendError statusNotFound
-- Handle requests for device CONTROL urls.
serviceControlHandler :: State -> DeviceType -> ByteString -> Iteratee ByteString IO Response
serviceControlHandler (c,mc,ai,s,objects_) deviceType _ = do
objects <- lift $ readIORef objects_ -- Current snapshot of object tree.
logMessage $ printf "Got request for CONTROL for service '%s'" $ deviceTypeToString deviceType
-- Parse the SOAP request
requestXml <- fmap S.concat EL.consume
logMessage $ "Request: " ++ (show requestXml)
action <- lift $ parseControlSoapXml $ T.unpack $ decodeUtf8 requestXml
logMessage $ "Action: " ++ (show action)
-- Deal with the action
case action of
Just a -> do
xml_ <- case a of
ContentDirectoryAction_ cda -> handleCDA deviceType cda objects
ConnectionManagerAction_ cma -> handleCMA cma
return xml_
Nothing ->
sendError statusNotFound
where
handleCDA st a objects = do
sendXml $ generateActionResponseXml c st objects a
handleCMA _ =
-- TODO: This should really be implemented as it is required by
-- the specification. However, the PS3 doesn't seem to use it at
-- all so I don't have any way to test an implementation anyway.
sendError statusNotFound
-- Last resort handler.
fallbackHandler :: Iteratee ByteString IO Response
fallbackHandler = do
return $ responseLBS statusNotFound [] ""
-- Send an empty error response.
sendError :: Monad m => Status -> Iteratee ByteString m Response
sendError s = return $
responseLBS s [ hdrConnectionClose
, hdrContentLength (0 :: Integer)
] ""
-- Send generated XML.
sendXml :: (MonadIO m, Functor m) => L.ByteString -> Iteratee ByteString m Response
sendXml xml = return $
responseLBS statusOK [ hdrConnectionClose
, hdrContentLength (L.length xml)
, headerContentType "text/xml"
] xml
logMessage :: (MonadIO m, Functor m) => String -> Iteratee a m ()
logMessage m = do
liftIO $ putStrLn m
-- Convenience functions for DRY construction of headers.
hdrConnectionClose :: Header
hdrConnectionClose = headerConnection "close"
hdrAcceptRangesBytes :: Header
hdrAcceptRangesBytes = (CI.mk "accept-ranges", "bytes")
hdrContentRange :: Integer -> Integer -> Integer -> Header
hdrContentRange l h s = (CI.mk "content-range", B8.pack $ printf "%d-%d/%d" l h s)
hdrContentLength :: Integral a => a -> Header
hdrContentLength l = headerContentLength $ encodeUtf8 $ T.pack $ show l
-- Name of the range header.
rangeHeader :: CI ByteString
rangeHeader = CI.mk "range"