module Network.HTTP.Lucu.Resource.Internal
( Rsrc
, Resource(..)
, spawnRsrc
, getConfig
, getRemoteAddr
#if defined(HAVE_SSL)
, getRemoteCertificate
#endif
, getRequest
, getResourcePath
, getChunk
, setStatus
, setHeader
, deleteHeader
, putBuilder
, driftTo
)
where
import Blaze.ByteString.Builder (Builder)
import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad hiding (mapM_)
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Reader (ReaderT, runReaderT, ask)
import Control.Monad.Unicode
import Data.Ascii (Ascii, CIAscii, AsciiBuilder)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Collections
import Data.Convertible.Base
import Data.Convertible.Instances.Text ()
import Data.List (intersperse, nub)
import Data.Maybe
import Data.Monoid
import Data.Monoid.Unicode
import Network.HTTP.Lucu.Abortion
import Network.HTTP.Lucu.Abortion.Internal
import Network.HTTP.Lucu.Config
import Network.HTTP.Lucu.DefaultPage
import qualified Network.HTTP.Lucu.Headers as H
import Network.HTTP.Lucu.Interaction
import Network.HTTP.Lucu.Postprocess
import Network.HTTP.Lucu.Request
import Network.HTTP.Lucu.Response
import Network.HTTP.Lucu.Response.StatusCode
import Network.HTTP.Lucu.Utils
import Network.Socket
#if defined(HAVE_SSL)
import OpenSSL.X509
#endif
import Prelude hiding (catch, concat, filter, mapM_, tail)
import Prelude.Unicode
import System.IO
newtype Rsrc a
= Rsrc {
unRsrc ∷ ReaderT NormalInteraction IO a
}
deriving (Applicative, Functor, Monad, MonadFix, MonadIO)
runRsrc ∷ Rsrc a → NormalInteraction → IO a
runRsrc = runReaderT ∘ unRsrc
data Resource = Resource {
resGet ∷ !(Maybe (Rsrc ()))
, resHead ∷ !(Maybe (Rsrc ()))
, resPost ∷ !(Maybe (Rsrc ()))
, resPut ∷ !(Maybe (Rsrc ()))
, resDelete ∷ !(Maybe (Rsrc ()))
}
instance Monoid Resource where
mempty
= Resource {
resGet = Nothing
, resHead = Nothing
, resPost = Nothing
, resPut = Nothing
, resDelete = Nothing
}
mappend a b
= Resource {
resGet = resGet a <|> resGet b
, resHead = resHead a <|> resHead b
, resPost = resPost a <|> resPost b
, resPut = resPut a <|> resPut b
, resDelete = resDelete a <|> resDelete b
}
instance Unfoldable Resource (Method, Rsrc ()) where
insert (GET , a) r = r { resGet = Just a }
insert (HEAD , a) r = r { resHead = Just a }
insert (POST , a) r = r { resPost = Just a }
insert (PUT , a) r = r { resPut = Just a }
insert (DELETE, a) r = r { resDelete = Just a }
insert _ r = r
empty = (∅)
instance Foldable Resource (Method, Rsrc ()) where
foldMap f (Resource {..})
= maybe (∅) (f ∘ ((,) GET )) resGet ⊕
maybe (∅) (f ∘ ((,) HEAD )) resHead ⊕
maybe (∅) (f ∘ ((,) POST )) resPost ⊕
maybe (∅) (f ∘ ((,) PUT )) resPut ⊕
maybe (∅) (f ∘ ((,) DELETE)) resDelete
instance Collection Resource (Method, Rsrc ()) where
filter = (fromList ∘) ∘ (∘ fromFoldable) ∘ filter
spawnRsrc ∷ Resource → NormalInteraction → IO ThreadId
spawnRsrc (Resource {..}) ni@(NI {..})
= forkIO $ run `catch` processException
where
run ∷ IO ()
run = flip runRsrc ni $
do req ← getRequest
fromMaybe notAllowed $ rsrc req
driftTo Done
rsrc ∷ Request → Maybe (Rsrc ())
rsrc req
= case reqMethod req of
GET → resGet
HEAD → case resHead of
Just r → Just r
Nothing → resGet
POST → resPost
PUT → resPut
DELETE → resDelete
_ → error $ "Unknown request method: " ⧺ show (reqMethod req)
notAllowed ∷ Rsrc ()
notAllowed = do setStatus MethodNotAllowed
setHeader "Allow"
$ cs
$ mconcat
$ intersperse (cs (", " ∷ Ascii) ∷ AsciiBuilder)
$ map cs allowedMethods
allowedMethods ∷ [Ascii]
allowedMethods = nub $ concat [ methods resGet ["GET", "HEAD"]
, methods resHead ["HEAD"]
, methods resPost ["POST"]
, methods resPut ["PUT"]
, methods resDelete ["DELETE"]
]
methods ∷ Maybe a → [Ascii] → [Ascii]
methods m xs
| isJust m = xs
| otherwise = []
toAbortion ∷ SomeException → Abortion
toAbortion e
= case fromException e of
Just abortion → abortion
Nothing → mkAbortion' InternalServerError $ cs $ show e
processException ∷ SomeException → IO ()
processException exc
= do let abo = toAbortion exc
state ← atomically $ readTVar niState
res ← atomically $ readTVar niResponse
if state ≤ DecidingHeader then
flip runRsrc ni $
do setStatus $ aboStatus abo
mapM_ (uncurry setHeader) (aboHeaders abo)
setHeader "Content-Type" defaultPageContentType
deleteHeader "Content-Encoding"
putBuilder $ abortPage niConfig (Just niRequest) res abo
else
when (cnfDumpTooLateAbortionToStderr niConfig)
$ dumpAbortion abo
runRsrc (driftTo Done) ni
dumpAbortion ∷ Abortion → IO ()
dumpAbortion abo
= hPutStr stderr
$ concat [ "Lucu: an exception occured after "
, "sending the response header to the client:\n"
, " ", show abo, "\n"
]
getInteraction ∷ Rsrc NormalInteraction
getInteraction = Rsrc ask
getConfig ∷ Rsrc Config
getConfig = niConfig <$> getInteraction
getRemoteAddr ∷ Rsrc SockAddr
getRemoteAddr = niRemoteAddr <$> getInteraction
#if defined(HAVE_SSL)
getRemoteCertificate ∷ Rsrc (Maybe X509)
getRemoteCertificate = niRemoteCert <$> getInteraction
#endif
getRequest ∷ Rsrc Request
getRequest = niRequest <$> getInteraction
getResourcePath ∷ Rsrc Path
getResourcePath = niResourcePath <$> getInteraction
getChunk ∷ Int → Rsrc ByteString
getChunk = (driftTo ReceivingBody *>) ∘ getChunk'
getChunk' ∷ Int → Rsrc ByteString
getChunk' n
| n < 0 = fail ("getChunk: n must not be negative: " ⧺ show n)
| n ≡ 0 = return (∅)
| otherwise = do req ← getRequest
if reqHasBody req then
askForInput =≪ getInteraction
else
driftTo DecidingHeader *> return (∅)
where
askForInput ∷ NormalInteraction → Rsrc ByteString
askForInput (NI {..})
= do
liftIO $ atomically
$ putTMVar niReceiveBodyReq (ReceiveBody n)
chunk ← liftIO
$ atomically
$ takeTMVar niReceivedBody
when (BS.null chunk)
$ driftTo DecidingHeader
return chunk
setStatus ∷ StatusCode sc ⇒ sc → Rsrc ()
setStatus sc
= do ni ← getInteraction
liftIO $ atomically
$ do state ← readTVar $ niState ni
when (state > DecidingHeader)
$ fail "Too late to declare the response status."
res ← readTVar $ niResponse ni
writeTVar (niResponse ni) $ setStatusCode sc res
setHeader ∷ CIAscii → Ascii → Rsrc ()
setHeader name value = liftIO ∘ atomically ∘ go =≪ getInteraction
where
go ∷ NormalInteraction → STM ()
go (NI {..})
= do state ← readTVar niState
when (state > DecidingHeader) $
fail "Too late to declare a response header field."
res ← readTVar niResponse
writeTVar niResponse $ H.setHeader name value res
when (name ≡ "Content-Type") $
writeTVar niResponseHasCType True
deleteHeader ∷ CIAscii → Rsrc ()
deleteHeader name = liftIO ∘ atomically ∘ go =≪ getInteraction
where
go ∷ NormalInteraction → STM ()
go (NI {..})
= do state ← readTVar niState
when (state > DecidingHeader) $
fail "Too late to delete a response header field."
res ← readTVar niResponse
writeTVar niResponse $ H.deleteHeader name res
when (name ≡ "Content-Type") $
writeTVar niResponseHasCType False
putBuilder ∷ Builder → Rsrc ()
putBuilder b = liftIO ∘ atomically ∘ go =≪ getInteraction
where
go ∷ NormalInteraction → STM ()
go ni@(NI {..})
= do driftTo' ni SendingBody
hasCType ← readTVar niResponseHasCType
unless hasCType
$ throwSTM
$ mkAbortion' InternalServerError
"putBuilder: Content-Type has not been set."
putTMVar niBodyToSend b
driftTo ∷ InteractionState → Rsrc ()
driftTo = (getInteraction ≫=) ∘ ((liftIO ∘ atomically) ∘) ∘ flip driftTo'
driftTo' ∷ NormalInteraction → InteractionState → STM ()
driftTo' ni@(NI {..}) newState
= do oldState ← readTVar niState
driftFrom oldState
where
driftFrom ∷ InteractionState → STM ()
driftFrom oldState
| newState < oldState = throwStateError oldState newState
| newState ≡ oldState = return ()
| otherwise
= do let a = [oldState .. newState]
b = tail a
c = zip a b
mapM_ (uncurry driftFromTo) c
writeTVar niState newState
throwStateError ∷ Monad m ⇒ InteractionState → InteractionState → m a
throwStateError Done SendingBody
= fail "It makes no sense to output something after finishing outputs."
throwStateError old new
= fail ("State error: " ⧺ show old ⧺ " ==> " ⧺ show new)
driftFromTo ∷ InteractionState → InteractionState → STM ()
driftFromTo ReceivingBody _
= putTMVar niReceiveBodyReq WasteAll
driftFromTo DecidingHeader _
= postprocess ni
driftFromTo _ _
= return ()