module Snap.Internal.Test.RequestBuilder
( RequestBuilder
, MultipartParams
, MultipartParam(..)
, FileData (..)
, RequestType (..)
, addHeader
, buildRequest
, delete
, dumpResponse
, evalHandler
, evalHandlerM
, get
, postMultipart
, postRaw
, postUrlEncoded
, put
, responseToString
, runHandler
, runHandlerM
, setContentType
, setHeader
, setHttpVersion
, setQueryString
, setQueryStringRaw
, setRequestPath
, setRequestType
, setSecure
) where
import Blaze.ByteString.Builder
import Blaze.ByteString.Builder.Char8
import Control.Monad.State hiding (get, put)
import qualified Control.Monad.State as State
import Data.Bits
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString as S8
import Data.CaseInsensitive (CI)
import Data.IORef
import qualified Data.Map as Map
import Data.Monoid
import Data.Word
import qualified Data.Vector as V
import System.PosixCompat.Time
import System.Random
import Snap.Internal.Http.Types hiding (addHeader,
setContentType,
setHeader)
import qualified Snap.Internal.Http.Types as H
import Snap.Internal.Parsing
import Snap.Internal.Types (evalSnap)
import Snap.Iteratee hiding (map)
import Snap.Core hiding ( addHeader
, setContentType
, setHeader )
import qualified Snap.Types.Headers as H
newtype RequestBuilder m a = RequestBuilder (StateT Request m a)
deriving (Monad, MonadIO, MonadState Request, MonadTrans)
mkDefaultRequest :: IO Request
mkDefaultRequest = do
bodyRef <- newIORef $ SomeEnumerator enumEOF
return $ Request "localhost"
8080
"127.0.0.1"
60000
"127.0.0.1"
8080
"localhost"
False
H.empty
bodyRef
Nothing
GET
(1,1)
[]
""
"/"
"/"
""
Map.empty
Map.empty
Map.empty
buildRequest :: MonadIO m => RequestBuilder m () -> m Request
buildRequest mm = do
let (RequestBuilder m) = (mm >> fixup)
rq0 <- liftIO mkDefaultRequest
execStateT m rq0
where
fixup = do
fixupURI
fixupMethod
fixupCL
fixupParams
fixupMethod = do
rq <- rGet
if (rqMethod rq == GET || rqMethod rq == DELETE ||
rqMethod rq == HEAD)
then do
let rq' = deleteHeader "Content-Type" rq
liftIO $ writeIORef (rqBody rq') (SomeEnumerator enumEOF)
rPut $ rq' { rqContentLength = Nothing }
else return $! ()
fixupCL = do
rq <- rGet
maybe (rPut $ deleteHeader "Content-Length" rq)
(\cl -> rPut $ H.setHeader "Content-Length"
(S.pack (show cl)) rq)
(rqContentLength rq)
fixupParams = do
rq <- rGet
let query = rqQueryString rq
let queryParams = parseUrlEncoded query
let mbCT = getHeader "Content-Type" rq
postParams <- if mbCT == Just "application/x-www-form-urlencoded"
then do
(SomeEnumerator e) <- liftIO $ readIORef $ rqBody rq
s <- liftM S.concat (liftIO $ run_ $ e $$ consume)
return $ parseUrlEncoded s
else return Map.empty
rPut $ rq { rqParams = Map.unionWith (++) queryParams postParams
, rqQueryParams = queryParams }
type MultipartParams = [(ByteString, MultipartParam)]
data MultipartParam =
FormData [ByteString]
| Files [FileData]
deriving (Show)
data FileData = FileData {
fdFileName :: ByteString
, fdContentType :: ByteString
, fdContents :: ByteString
}
deriving (Show)
data RequestType
= GetRequest
| RequestWithRawBody Method ByteString
| MultipartPostRequest MultipartParams
| UrlEncodedPostRequest Params
| DeleteRequest
deriving (Show)
setRequestType :: MonadIO m => RequestType -> RequestBuilder m ()
setRequestType GetRequest = do
rq <- rGet
liftIO $ writeIORef (rqBody rq) $ SomeEnumerator enumEOF
rPut $ rq { rqMethod = GET
, rqContentLength = Nothing
}
setRequestType DeleteRequest = do
rq <- rGet
liftIO $ writeIORef (rqBody rq) $ SomeEnumerator enumEOF
rPut $ rq { rqMethod = DELETE
, rqContentLength = Nothing
}
setRequestType (RequestWithRawBody m b) = do
rq <- rGet
liftIO $ writeIORef (rqBody rq) $ SomeEnumerator $ enumBS b
rPut $ rq { rqMethod = m
, rqContentLength = Just $ S.length b
}
setRequestType (MultipartPostRequest fp) = encodeMultipart fp
setRequestType (UrlEncodedPostRequest fp) = do
rq <- liftM (H.setHeader "Content-Type"
"application/x-www-form-urlencoded") rGet
let b = printUrlEncoded fp
liftIO $ writeIORef (rqBody rq) $ SomeEnumerator $ enumBS b
rPut $ rq { rqMethod = POST
, rqContentLength = Just $ S.length b
}
makeBoundary :: MonadIO m => m ByteString
makeBoundary = do
xs <- liftIO $ replicateM 16 randomWord8
let x = S.pack $ map (toEnum . fromEnum) xs
return $ S.concat [ "snap-boundary-", encode x ]
where
randomWord8 :: IO Word8
randomWord8 = liftM (\c -> toEnum $ c .&. 0xff) randomIO
table = V.fromList [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
, 'a', 'b', 'c', 'd', 'e', 'f' ]
encode = toByteString . S8.foldl' f mempty
#if MIN_VERSION_base(4,5,0)
shR = unsafeShiftR
#else
shR = shiftR
#endif
f m c = let low = c .&. 0xf
hi = (c .&. 0xf0) `shR` 4
k = \i -> fromWord8 $! toEnum $! fromEnum $!
V.unsafeIndex table (fromEnum i)
in m `mappend` k hi `mappend` k low
multipartHeader :: ByteString -> ByteString -> Builder
multipartHeader boundary name =
mconcat [ fromByteString boundary
, fromByteString "\r\ncontent-disposition: form-data"
, fromByteString "; name=\""
, fromByteString name
, fromByteString "\"\r\n" ]
encodeFormData :: ByteString -> ByteString -> [ByteString] -> IO Builder
encodeFormData boundary name vals =
case vals of
[] -> return mempty
[v] -> return $ mconcat [ hdr
, cr
, fromByteString v
, fromByteString "\r\n--" ]
_ -> multi
where
hdr = multipartHeader boundary name
cr = fromByteString "\r\n"
oneVal b v = mconcat [ fromByteString b
, cr
, cr
, fromByteString v
, fromByteString "\r\n--" ]
multi = do
b <- makeBoundary
return $ mconcat [ hdr
, multipartMixed b
, cr
, fromByteString "--"
, mconcat (map (oneVal b) vals)
, fromByteString b
, fromByteString "--\r\n--" ]
multipartMixed :: ByteString -> Builder
multipartMixed b = mconcat [ fromByteString "Content-Type: multipart/mixed"
, fromByteString "; boundary="
, fromByteString b
, fromByteString "\r\n" ]
encodeFiles :: ByteString -> ByteString -> [FileData] -> IO Builder
encodeFiles boundary name files =
case files of
[] -> return mempty
_ -> do
b <- makeBoundary
return $ mconcat [ hdr
, multipartMixed b
, cr
, fromByteString "--"
, mconcat (map (oneVal b) files)
, fromByteString b
, fromByteString "--\r\n--"
]
where
contentDisposition fn = mconcat [
fromByteString "Content-Disposition: attachment"
, fromByteString "; filename=\""
, fromByteString fn
, fromByteString "\"\r\n"
]
contentType ct = mconcat [
fromByteString "Content-Type: "
, fromByteString ct
, cr
]
oneVal b (FileData fileName ct contents) =
mconcat [ fromByteString b
, cr
, contentType ct
, contentDisposition fileName
, fromByteString "Content-Transfer-Encoding: binary\r\n"
, cr
, fromByteString contents
, fromByteString "\r\n--"
]
hdr = multipartHeader boundary name
cr = fromByteString "\r\n"
encodeMultipart :: MonadIO m => MultipartParams -> RequestBuilder m ()
encodeMultipart kvps = do
boundary <- liftIO $ makeBoundary
builders <- liftIO $ mapM (handleOne boundary) kvps
let b = toByteString $
mconcat (fromByteString "--" : builders)
`mappend` finalBoundary boundary
rq0 <- rGet
liftIO $ writeIORef (rqBody rq0) $ SomeEnumerator $ enumBS b
let rq = H.setHeader "Content-Type"
(S.append "multipart/form-data; boundary=" boundary)
rq0
rPut $ rq { rqMethod = POST
, rqContentLength = Just $ S.length b
}
where
finalBoundary b = mconcat [fromByteString b, fromByteString "--\r\n"]
handleOne boundary (name, mp) =
case mp of
(FormData vals) -> encodeFormData boundary name vals
(Files fs) -> encodeFiles boundary name fs
fixupURI :: Monad m => RequestBuilder m ()
fixupURI = do
rq <- rGet
let u = S.concat [ rqContextPath rq
, rqPathInfo rq
, let q = rqQueryString rq
in if S.null q
then ""
else S.append "?" q
]
rPut $ rq { rqURI = u }
setQueryStringRaw :: Monad m => ByteString -> RequestBuilder m ()
setQueryStringRaw r = do
rq <- rGet
rPut $ rq { rqQueryString = r }
fixupURI
setQueryString :: Monad m => Params -> RequestBuilder m ()
setQueryString p = setQueryStringRaw $ printUrlEncoded p
setHeader :: (Monad m) => CI ByteString -> ByteString -> RequestBuilder m ()
setHeader k v = rModify (H.setHeader k v)
addHeader :: (Monad m) => CI ByteString -> ByteString -> RequestBuilder m ()
addHeader k v = rModify (H.addHeader k v)
setContentType :: Monad m => ByteString -> RequestBuilder m ()
setContentType c = rModify (H.setHeader "Content-Type" c)
setSecure :: Monad m => Bool -> RequestBuilder m ()
setSecure b = rModify $ \rq -> rq { rqIsSecure = b }
setHttpVersion :: Monad m => (Int,Int) -> RequestBuilder m ()
setHttpVersion v = rModify $ \rq -> rq { rqVersion = v }
setRequestPath :: Monad m => ByteString -> RequestBuilder m ()
setRequestPath p0 = do
rModify $ \rq -> rq { rqContextPath = "/"
, rqPathInfo = p }
fixupURI
where
p = if S.isPrefixOf "/" p0 then S.drop 1 p0 else p0
get :: MonadIO m =>
ByteString
-> Params
-> RequestBuilder m ()
get uri params = do
setRequestType GetRequest
setQueryString params
setRequestPath uri
delete :: MonadIO m =>
ByteString
-> Params
-> RequestBuilder m ()
delete uri params = do
setRequestType DeleteRequest
setQueryString params
setRequestPath uri
postUrlEncoded :: MonadIO m =>
ByteString
-> Params
-> RequestBuilder m ()
postUrlEncoded uri params = do
setRequestType $ UrlEncodedPostRequest params
setRequestPath uri
postMultipart :: MonadIO m =>
ByteString
-> MultipartParams
-> RequestBuilder m ()
postMultipart uri params = do
setRequestType $ MultipartPostRequest params
setRequestPath uri
put :: MonadIO m =>
ByteString
-> ByteString
-> ByteString
-> RequestBuilder m ()
put uri contentType putData = do
setRequestType $ RequestWithRawBody PUT putData
setHeader "Content-Type" contentType
setRequestPath uri
postRaw :: MonadIO m =>
ByteString
-> ByteString
-> ByteString
-> RequestBuilder m ()
postRaw uri contentType postData = do
setRequestType $ RequestWithRawBody POST postData
setHeader "Content-Type" contentType
setRequestPath uri
runHandler :: MonadIO m =>
RequestBuilder m ()
-> Snap a
-> m Response
runHandler = runHandlerM rs
where
rs rq s = do
(_,rsp) <- liftIO $ run_ $ runSnap s
(const $ return $! ())
(const $ return $! ())
rq
return rsp
runHandlerM :: (MonadIO m, MonadSnap n) =>
(forall a . Request -> n a -> m Response)
-> RequestBuilder m ()
-> n b
-> m Response
runHandlerM rSnap rBuilder snap = do
rq <- buildRequest rBuilder
rsp <- rSnap rq snap
t1 <- liftIO (epochTime >>= formatHttpTime)
return $ H.setHeader "Date" t1
$ H.setHeader "Server" "Snap/test"
$ if rspContentLength rsp == Nothing &&
rspHttpVersion rsp < (1,1)
then H.setHeader "Connection" "close" rsp
else rsp
evalHandler :: MonadIO m =>
RequestBuilder m ()
-> Snap a
-> m a
evalHandler = evalHandlerM rs
where
rs rq s = liftIO $ run_
$ evalSnap s (const $ return $! ())
(const $ return $! ())
rq
evalHandlerM :: (MonadIO m, MonadSnap n) =>
(forall a . Request -> n a -> m a)
-> RequestBuilder m ()
-> n b
-> m b
evalHandlerM rSnap rBuilder snap = do
rq <- buildRequest rBuilder
rSnap rq snap
dumpResponse :: Response -> IO ()
dumpResponse resp = responseToString resp >>= S.putStrLn
responseToString :: Response -> IO ByteString
responseToString resp = do
b <- run_ (rspBodyToEnum (rspBody resp) $$
liftM mconcat consume)
return $ toByteString $ fromShow resp `mappend` b
rGet :: Monad m => RequestBuilder m Request
rGet = RequestBuilder State.get
rPut :: Monad m => Request -> RequestBuilder m ()
rPut s = RequestBuilder $ State.put s
rModify :: Monad m => (Request -> Request) -> RequestBuilder m ()
rModify f = RequestBuilder $ modify f