module Network.Monad.HTTP (
send,
receive,
respond,
) where
import Network.URI
( URI(URI, uriAuthority)
, URIAuth(uriUserInfo, uriRegName, uriPort)
, parseURIReference
)
import qualified Network.Monad.HTTP.Header as Header
import qualified Network.Monad.Reader as StreamMonad
import qualified Network.Monad.Body as Body
import Network.Stream (ConnError(ErrorParse,ErrorClosed), )
import Network.HTTP.Base
(Request(..), RequestData, RequestMethod(..),
Response(..), ResponseData, ResponseCode, )
import Network.Monad.Reader (readLine, readBlock, writeBlock, )
import Control.Monad.Trans.Class (lift, )
import qualified Control.Monad.Exception.Asynchronous as Async
import qualified Control.Monad.Exception.Synchronous as Sync
import qualified Network.Monad.Exception as Exc
import qualified Data.Map as Map
import Data.String.HT (trim, )
import Data.Maybe.HT (toMaybe, )
import Data.Char (isDigit, intToDigit, digitToInt, toLower, )
import Data.Monoid (Monoid, mappend, mempty, )
import Data.Semigroup (Semigroup, (<>), )
import Control.Monad (liftM, liftM2, mplus, )
import Numeric (readHex, )
type SynchronousExceptional body m a =
Sync.ExceptionalT ConnError (StreamMonad.T body m) a
type AsynchronousExceptional body m a =
Async.ExceptionalT ConnError (StreamMonad.T body m) a
requestMethodDict :: Map.Map String RequestMethod
requestMethodDict =
Map.fromList $
("HEAD", HEAD) :
("PUT", PUT) :
("GET", GET) :
("POST", POST) :
("DELETE", DELETE) :
("OPTIONS", OPTIONS) :
("TRACE", TRACE) :
[]
parseRequestHead :: [String] -> Sync.Exceptional ConnError RequestData
parseRequestHead [] = Sync.throw ErrorClosed
parseRequestHead (com:hdrs) =
requestCommand com >>= \(_version,rqm,uri) ->
return (rqm, uri, Header.parseManyStraight hdrs)
where
requestCommand line =
case words line of
(rqm:uri:version) ->
liftM2
(\r u -> (version,r,u))
(Sync.fromMaybe
(ErrorParse $ "Unknown HTTP method: " ++ rqm)
(Map.lookup rqm requestMethodDict))
(Sync.fromMaybe
(ErrorParse $ "Malformed URI: " ++ uri)
(parseURIReference uri))
_ -> Sync.throw $
if null line
then ErrorClosed
else ErrorParse $ "Request command line parse failure: " ++ line
parseResponseHead :: [String] -> Sync.Exceptional ConnError ResponseData
parseResponseHead [] = Sync.throw ErrorClosed
parseResponseHead (sts:hdrs) =
responseStatus sts >>= \(_version,code,reason) ->
return (code, reason, Header.parseManyStraight hdrs)
where
responseStatus line =
case words line of
(version:code:reason) ->
do digits <- mapM getDigit code
case digits of
[a,b,c] ->
return (version, (a,b,c), concatMap (++" ") reason)
_ -> Sync.throw $ ErrorParse $ "Response Code must consist of three digits: " ++ show code
_ -> Sync.throw $
if null line
then ErrorClosed
else ErrorParse $ "Response status line parse failure: " ++ line
getDigit d =
if isDigit d
then return $ digitToInt d
else Sync.throw $ ErrorParse $ "Non-digit "++d:" in Response Code"
data Behaviour = Continue
| Retry
| Done
| ExpectEntity
| DieHorribly String
matchResponse :: RequestMethod -> ResponseCode -> Behaviour
matchResponse rqst rsp =
let ans = if rqst == HEAD then Done else ExpectEntity
in case rsp of
(1,0,0) -> Continue
(1,0,1) -> Done
(1,_,_) -> Continue
(2,0,4) -> Done
(2,0,5) -> Done
(2,_,_) -> ans
(3,0,4) -> Done
(3,0,5) -> Done
(3,_,_) -> ans
(4,1,7) -> Retry
(4,_,_) -> ans
(5,_,_) -> ans
(a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")
send :: (Monad m, Body.C body) => Request body -> SynchronousExceptional body m (Async.Exceptional ConnError (Bool, Response body))
send rq =
liftM
(fmap (\rsp -> (findConnClose (rqHeaders rq ++ rspHeaders rsp), rsp))) $
sendMain $
fixHostHeader rq
sendMain :: (Monad m, Body.C body) => Request body -> SynchronousExceptional body m (Async.Exceptional ConnError (Response body))
sendMain rqst =
do
writeBlock (Body.fromString $ show rqst)
writeBlock (rqBody rqst)
withResponseHead $ switchResponse True False rqst
getResponseHead :: (Monad m, Body.C body) => SynchronousExceptional body m (Async.Exceptional ConnError ResponseData)
getResponseHead =
Sync.ExceptionalT $
liftM (Async.sequence . fmap (parseResponseHead . map Body.toString)) $
Async.runExceptionalT readTillEmpty1
withResponseHead :: (Monad m, Body.C body) => (ResponseData -> SynchronousExceptional body m (Async.Exceptional ConnError (Response body))) -> SynchronousExceptional body m (Async.Exceptional ConnError (Response body))
withResponseHead =
Exc.switchM getResponseHead (\(cd,rn,hdrs) -> return $ Response cd rn hdrs mempty)
switchResponse :: (Monad m, Body.C body) =>
Bool
-> Bool
-> Request body
-> ResponseData
-> SynchronousExceptional body m (Async.Exceptional ConnError (Response body))
switchResponse allow_retry bdy_sent rqst (cd,rn,hdrs) =
case matchResponse (rqMethod rqst) cd of
Continue ->
if not bdy_sent
then
writeBlock (rqBody rqst) >>
(withResponseHead $ switchResponse allow_retry True rqst)
else
withResponseHead $
switchResponse allow_retry bdy_sent rqst
Retry ->
writeBlock (Body.fromString (show rqst) `mappend` rqBody rqst) >>
(withResponseHead $
switchResponse False bdy_sent rqst)
Done ->
return $ Async.pure $ Response cd rn hdrs mempty
DieHorribly str ->
Sync.throwT $ ErrorParse ("Invalid response: " ++ str)
ExpectEntity ->
let tc = Header.lookup Header.HdrTransferEncoding hdrs
cl = Header.lookup Header.HdrContentLength hdrs
in lift $ Async.runExceptionalT $
assembleHeaderBody (Response cd rn) hdrs $
case tc of
Nothing ->
case cl of
Just x -> linearTransferStrLen x
Nothing -> hopefulTransfer
Just x ->
case map toLower (trim x) of
"chunked" -> chunkedTransfer False
_ -> uglyDeathTransfer
fixHostHeader :: Request body -> Request body
fixHostHeader rq =
let uri = rqURI rq
host_ = uriToAuthorityString uri
in Header.insertIfMissing Header.HdrHost host_ rq
findConnClose :: [Header.T] -> Bool
findConnClose hdrs =
case Header.lookup Header.HdrConnection hdrs of
Nothing -> False
Just x -> map toLower (trim x) == "close"
uriToAuthorityString :: URI -> String
uriToAuthorityString URI{uriAuthority=Nothing} = ""
uriToAuthorityString URI{uriAuthority=Just ua} = uriUserInfo ua ++
uriRegName ua ++
uriPort ua
receive :: (Monad m, Body.C body) => SynchronousExceptional body m (Async.Exceptional ConnError (Request body))
receive =
Exc.switchM getRequestHead
(\(rm,uri,hdrs) -> return $ Request uri rm hdrs mempty)
(lift . Async.runExceptionalT . processRequest)
getRequestHead :: (Monad m, Body.C body) => SynchronousExceptional body m (Async.Exceptional ConnError RequestData)
getRequestHead =
Sync.ExceptionalT $
liftM (Async.sequence . fmap (parseRequestHead . map Body.toString)) $
Async.runExceptionalT readTillEmpty1
processRequest :: (Monad m, Body.C body) => RequestData -> AsynchronousExceptional body m (Request body)
processRequest (rm,uri,hdrs) =
let tc = Header.lookup Header.HdrTransferEncoding hdrs
cl = Header.lookup Header.HdrContentLength hdrs
in assembleHeaderBody (Request uri rm) hdrs $
case tc of
Nothing ->
case cl of
Just x -> linearTransferStrLen x
Nothing -> mempty
Just x ->
case map toLower (trim x) of
"chunked" -> chunkedTransfer False
_ -> uglyDeathTransfer
assembleHeaderBody :: (Monad m) => ([Header.T] -> body -> a) -> [Header.T] -> AsynchronousExceptional body m ([Header.T], body) -> AsynchronousExceptional body m a
assembleHeaderBody make hdrs =
Exc.map (\(_ftrs,bdy) -> make hdrs bdy)
respond :: (Monad m, Body.C body) => Response body -> SynchronousExceptional body m ()
respond rsp =
do writeBlock (Body.fromString $ show rsp)
writeBlock (rspBody rsp)
linearTransferStrLen :: (Monad m, Monoid body) => String -> AsynchronousExceptional body m ([Header.T],body)
linearTransferStrLen ns =
case reads ns of
[(n,"")] -> linearTransfer n
_ -> Async.throwMonoidT $ ErrorParse $ "Content-Length header contains not a number: " ++ show ns
linearTransfer :: Monad m => Int -> AsynchronousExceptional body m ([Header.T],body)
linearTransfer n =
Exc.map ((,) []) $ readBlock n
hopefulTransfer :: (Monad m, Body.C body) => AsynchronousExceptional body m ([Header.T],body)
hopefulTransfer =
let go =
readLineSwitch $ \line ->
if Body.isEmpty line
then mempty
else Exc.map (mappend line) go
in Exc.map ((,) []) go
data ChunkedResponse body =
ChunkedResponse [Header.T] [Int] body
deriving Show
instance Semigroup body => Semigroup (ChunkedResponse body) where
ChunkedResponse hx lx sx <> ChunkedResponse hy ly sy =
ChunkedResponse (hx <> hy) (lx <> ly) (sx <> sy)
instance Monoid body => Monoid (ChunkedResponse body) where
mempty = ChunkedResponse mempty mempty mempty
mappend (ChunkedResponse hx lx sx) (ChunkedResponse hy ly sy) =
ChunkedResponse (mappend hx hy) (mappend lx ly) (mappend sx sy)
forceCR :: ChunkedResponse body -> ChunkedResponse body
forceCR ~(ChunkedResponse h l s) = (ChunkedResponse h l s)
chunkedTransfer :: (Monad m, Body.C body) => Bool -> AsynchronousExceptional body m ([Header.T],body)
chunkedTransfer attachLength =
Exc.map (\(ChunkedResponse ftrs sizes info) ->
((if attachLength
then (Header.Header Header.HdrContentLength (show $ sum sizes) :)
else id) ftrs,
info)) $
chunkedTransferLoop
chunkedTransferLoop :: (Monad m, Body.C body) => AsynchronousExceptional body m (ChunkedResponse body)
chunkedTransferLoop =
readLineSwitch $ \line ->
case readHex $ Body.toString line of
[(size,_)] ->
if size == 0
then
Exc.map (\strs -> ChunkedResponse (Header.parseManyStraight $ map Body.toString strs) [0] mempty)
readTillEmpty2
else
Exc.map (\block -> ChunkedResponse [] [0] block) (readBlock size)
`mappend`
Async.ExceptionalT
((liftM
(\newLineE ->
mplus
(Async.exception newLineE)
(toMaybe
(not $ Body.isLineTerm $ Async.result newLineE)
(ErrorParse $ "no CR+LF after chunk"))) $
Async.runExceptionalT (readBlock 2))
`Async.continueM`
Async.runExceptionalT (Exc.map forceCR chunkedTransferLoop))
_ ->
Async.throwMonoidT
(ErrorParse $ "Chunk-Length is not a number: " ++ show (Body.toString line))
uglyDeathTransfer :: (Monad m, Monoid body) => AsynchronousExceptional body m ([Header.T],body)
uglyDeathTransfer =
Async.throwMonoidT $
ErrorParse "Unknown Transfer-Encoding"
readTillEmpty1 :: (Monad m, Body.C body) => AsynchronousExceptional body m [body]
readTillEmpty1 =
readLineSwitch $ \s ->
if Body.isLineTerm s
then readTillEmpty1
else Exc.map (s:) readTillEmpty2
readTillEmpty2 :: (Monad m, Body.C body) => AsynchronousExceptional body m [body]
readTillEmpty2 =
readLineSwitch $ \s ->
if Body.isLineTerm s || Body.isEmpty s
then mempty
else Exc.map (s:) readTillEmpty2
readLineSwitch :: (Monad m, Monoid a) => (body -> AsynchronousExceptional body m a) -> AsynchronousExceptional body m a
readLineSwitch next =
Async.bindT readLine next