packages feed

hums-0.6.0: src/Handlers.hs

{-
    hums - The Haskell UPnP Server
    Copyright (C) 2009, 2012 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           Blaze.ByteString.Builder (fromByteString)
import           Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as L
import           Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import           Data.Conduit (Flush(..), ($$), mapOutput)
import qualified Data.Conduit.List as CL
import           Data.Conduit.Binary (sourceHandleRange)
import           Data.IORef (IORef, readIORef)
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.Text.Encoding (encodeUtf8, decodeUtf8)
import           Network.HTTP.Types (Status, Header, partialContent206, forbidden403, notImplemented501, ok200, notFound404)
import           Network.HTTP.Types.Header (hConnection, hContentLength, hContentType)
import           Network.Wai (Application, Request, Response, responseSourceBracket, requestBody, requestHeaders, responseLBS)
import           Filesystem.Path (FilePath, (</>))
import           Filesystem.Path.CurrentOS (encodeString, fromText)
import qualified Filesystem as FS
import           Prelude hiding (FilePath)
import qualified System.IO as IO
import           Text.Printf (printf)

import           Soap
import           Configuration
import           Service
import           Action
import           MimeType
import           Object
import           HttpExtra

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 :).

-}

serveStaticFile :: Request -> ByteString -> FilePath -> IO Response
serveStaticFile req mimeType fp = do
  logMessage $ printf "Serving file '%s'..." $ sfp
  -- 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 <- FS.getSize fp
  response <- serveFile fsz ranges
  return $ response
  where
    sfp :: String
    sfp = encodeString fp

    serveFile fsz [(l,h)] = do
      let l' = maybe 0 id l
      let h' = maybe (fsz-1) id h
      let n = (h' - l' + 1)
      let hdrs = [ hdrContentLength n
                 , (hContentType, mimeType)
                 , hdrContentRange l' h' fsz
                 , hdrAcceptRangesBytes
                 , hdrConnectionClose ]
      let src hnd = mapOutput (Chunk . fromByteString) $ sourceHandleRange hnd (Just l') (Just n)
      responseSourceBracket
         (IO.openFile sfp IO.ReadMode)
         (IO.hClose)
         (\hnd -> return (partialContent206, hdrs, src hnd))

    serveFile _ _ = do
      -- This requires multipart/byteranges, but we don't support that as of yet.
      sendError notImplemented501

-- Handler for the root description.
rootDescriptionHandler :: State -> IO Response
rootDescriptionHandler (c,mc,ai,s,_) = do
  logMessage "Got request for root description."
  let xml = generateDescriptionXml c mc s
  return $ responseLBS ok200 [ hdrConnectionClose
                             , hdrContentLength (L.length xml)
                             , xmlContentType
                             ] xml

-- Handle static files.
staticHandler :: Request -> FilePath -> [Text] -> IO Response
staticHandler req root path = do
  logMessage $ "Got request for static content: " ++ (show path)
  if dotDot `elem` path then     -- Reject relative URLs.
    sendError forbidden403
    else
    serveStaticFile req mimeType fp
   where
     fp = foldl (</>) root (map fromText path)
     mimeType = guessMimeType fp
     dotDot = ".."

-- Handle requests for content.
contentHandler :: Request -> State -> Text -> IO Response
contentHandler req (c,mc,ai,s,objects_) oid = do
  objects <- readIORef objects_     -- Current snapshot of object tree.
  logMessage $ printf "Got request for CONTENT for objectId=%s" (T.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 notFound404

-- Handle requests for device CONTROL urls.
serviceControlHandler :: State -> DeviceType -> Application
serviceControlHandler (c,mc,ai,s,objects_) deviceType req = do
  objects <- 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 $ requestBody req $$ CL.consume
  logMessage $ "Request: " ++ (show requestXml)
  action <- 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 notFound404
  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 notFound404


-- Last resort handler.
fallbackHandler :: IO Response
fallbackHandler = return $ responseLBS notFound404 [] ""

-- Send an empty error response.
sendError :: Status -> IO Response
sendError s = return $ responseLBS s [ hdrConnectionClose
                                     , hdrContentLength (0 :: Integer)
                                     ] ""

-- Send generated XML.
sendXml :: L.ByteString -> IO Response
sendXml xml = return $ responseLBS ok200 [ hdrConnectionClose
                                         , hdrContentLength (L.length xml)
                                         , xmlContentType
                                         ] xml

logMessage :: String -> IO ()
logMessage m = putStrLn m

-- Convenience functions for DRY construction of headers.
hdrConnectionClose :: Header
hdrConnectionClose = (hConnection, "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 :: (Show a, Integral a) => a -> Header
hdrContentLength l = (hContentLength, encodeUtf8 $ T.pack $ show l)

-- Name of the range header.
rangeHeader :: CI ByteString
rangeHeader = CI.mk "range"

-- XML content type
xmlContentType :: Header
xmlContentType = (hContentType, "text/xml")