hums-0.3.2: 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
) where
import Soap
import Configuration
import Network.HTTP.Base
import Network.HTTP.Headers
import Network.StreamSocket()
import Service
import Text.Printf
import Action
import System.IO (withFile, hFileSize, IOMode(..))
import MimeType
import Object
import HttpExtra
import System.FilePath
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as L
import Data.List (isInfixOf)
import Data.IORef
import HttpMonad
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (MonadIO)
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 :).
-}
hCanonicalizeRanges :: Integer -> [(Maybe Integer, Maybe Integer)] -> [(Integer,Integer)]
hCanonicalizeRanges fsz ranges =
map f ranges
where
f (lo,hi) = do
let lo' = case lo of
Just x -> x
Nothing -> 0
let hi' = case hi of
Just x -> x
Nothing -> fsz - 1
(lo', hi')
fileSize :: FilePath -> IO Integer
fileSize fp =
withFile fp ReadMode $ \h -> hFileSize h
serveStaticFile :: String -> FilePath -> HttpT IO ()
serveStaticFile mimeType fp = do
logMessage $ printf "Serving file '%s'..." fp
-- Do we have a range header?
hs <- fmap getHeaders getRequest
let ranges =
case lookupHeader HdrRange hs of
Just value -> parseRangeHeader value
Nothing -> [] -- Whole file
-- Set up the common headers.
addHeader (Header HdrContentType mimeType)
addHeader (Header (HdrCustom "Accept-Ranges") "bytes")
addHeader (Header HdrConnection "close")
-- Serve the ranges.
fsz <- lift $ fileSize fp
let ranges' = hCanonicalizeRanges fsz ranges
serveFile fsz ranges'
where
serveFile fsz [] = do
-- Set the headers for full transfer.
setResponseCode OK
setContentLength $ Just fsz
-- Send the file contents.
writeFileToBody fp 0 fsz
serveFile fsz [(rLow,rHigh)] = do
-- Set the headers for partial content.
setResponseCode PartialContent
setContentLength $ Just (rHigh-rLow+1)
addHeader (Header HdrContentRange $ printf "%d-%d/%d" rLow rHigh fsz)
-- Send the file contents.
writeFileToBody fp rLow $ fromInteger $ rHigh - rLow + 1
serveFile _ _ =
-- This requires multipart/byteranges, but we don't support that as of yet.
sendError NotImplemented
-- Regular expressions for avoiding relative URLs. These
-- are overly conservative, but what the heck...
dotDotSlash :: String
dotDotSlash = "../"
slashDotDot :: String
slashDotDot = "/.."
-- Handler for the root description.
rootDescriptionHandler :: State -> HttpT IO ()
rootDescriptionHandler (c,mc,ai,s,_) = do
logMessage "Got request for root description."
sendXml $ generateDescriptionXml c mc s
logMessage "Sent root description."
-- Handle static files.
staticHandler :: String -> String -> HttpT IO ()
staticHandler root p = do
logMessage $ "Got request for static content: " ++ p
if dotDotSlash `isInfixOf` p || -- Reject relative URLs.
slashDotDot `isInfixOf` p then
sendError InternalServerError
else
serveStaticFile mimeType fp
where
fp = root </> p
mimeType = guessMimeType fp
-- Handle requests for content.
contentHandler :: State -> String -> HttpT IO ()
contentHandler (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" oid
-- Serve the file which the object maps to.
case findByObjectId oid objects of
Just o ->
serveStaticFile mt fp
where
od = getObjectData o
fp = objectFileName od
mt = objectMimeType od
Nothing ->
sendError NotFound
-- Handle requests for device CONTROL urls.
serviceControlHandler :: State -> DeviceType -> String -> HttpT IO ()
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 rqBody getRequest
logMessage $ printf "Request: %s" requestXml
action <- lift $ parseControlSoapXml requestXml
logMessage $ printf "Action: %s" $ 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 ()
Nothing ->
sendError NotImplemented
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 NotImplemented
-- Last resort handler.
fallbackHandler :: HttpT IO ()
fallbackHandler = do
r <- getRequest
logMessage $ printf "Fallback handler got request: %s" $ show r
sendError InternalServerError
-- Send an empty error response.
sendError :: Monad m => HttpResponseCode -> HttpT m ()
sendError c = do
setResponseCode c
setContentLength $ Just 0
addHeader (Header HdrConnection "close")
-- Send generated XML.
sendXml :: (MonadIO m, Functor m) => ByteString -> HttpT m ()
sendXml xml = do
setResponseCode OK
setContentLength $ Just $ toInteger $ L.length xml
addHeader (Header HdrConnection "close")
addHeader (Header HdrContentType "text/xml")
-- logMessage $ "Sending XML:"
-- logDataLBS xml
writeToBody xml