wai-extra 2.1.1.3 → 3.0.0
raw patch · 19 files changed
+788/−326 lines, 19 filesdep +data-default-classdep +deepseqdep +streaming-commonsdep −blaze-builder-conduitdep −conduitdep −conduit-extradep ~http-typesdep ~waiPVP ok
version bump matches the API change (PVP)
Dependencies added: data-default-class, deepseq, streaming-commons
Dependencies removed: blaze-builder-conduit, conduit, conduit-extra, wai-test, zlib-bindings, zlib-conduit
Dependency ranges changed: http-types, wai
API changes (from Hackage documentation)
- Network.Wai.Parse: conduitRequestBody :: BackEnd y -> RequestBodyType -> Conduit ByteString IO (Either Param (File y))
+ Network.Wai.EventSource: CloseEvent :: ServerEvent
+ Network.Wai.EventSource: CommentEvent :: Builder -> ServerEvent
+ Network.Wai.EventSource: RetryEvent :: Int -> ServerEvent
+ Network.Wai.EventSource: ServerEvent :: Maybe Builder -> Maybe Builder -> [Builder] -> ServerEvent
+ Network.Wai.EventSource: data ServerEvent
+ Network.Wai.EventSource: eventComment :: ServerEvent -> Builder
+ Network.Wai.EventSource: eventData :: ServerEvent -> [Builder]
+ Network.Wai.EventSource: eventId :: ServerEvent -> Maybe Builder
+ Network.Wai.EventSource: eventName :: ServerEvent -> Maybe Builder
+ Network.Wai.EventSource: eventRetry :: ServerEvent -> Int
+ Network.Wai.EventSource: eventSourceAppChan :: Chan ServerEvent -> Application
+ Network.Wai.EventSource: eventSourceAppIO :: IO ServerEvent -> Application
+ Network.Wai.EventSource.EventStream: CloseEvent :: ServerEvent
+ Network.Wai.EventSource.EventStream: CommentEvent :: Builder -> ServerEvent
+ Network.Wai.EventSource.EventStream: RetryEvent :: Int -> ServerEvent
+ Network.Wai.EventSource.EventStream: ServerEvent :: Maybe Builder -> Maybe Builder -> [Builder] -> ServerEvent
+ Network.Wai.EventSource.EventStream: data ServerEvent
+ Network.Wai.EventSource.EventStream: eventComment :: ServerEvent -> Builder
+ Network.Wai.EventSource.EventStream: eventData :: ServerEvent -> [Builder]
+ Network.Wai.EventSource.EventStream: eventId :: ServerEvent -> Maybe Builder
+ Network.Wai.EventSource.EventStream: eventName :: ServerEvent -> Maybe Builder
+ Network.Wai.EventSource.EventStream: eventRetry :: ServerEvent -> Int
+ Network.Wai.EventSource.EventStream: eventToBuilder :: ServerEvent -> Maybe Builder
+ Network.Wai.Test: SRequest :: Request -> ByteString -> SRequest
+ Network.Wai.Test: SResponse :: Status -> ResponseHeaders -> ByteString -> SResponse
+ Network.Wai.Test: WaiTestFailure :: String -> WaiTestFailure
+ Network.Wai.Test: assertBody :: ByteString -> SResponse -> Session ()
+ Network.Wai.Test: assertBodyContains :: ByteString -> SResponse -> Session ()
+ Network.Wai.Test: assertContentType :: ByteString -> SResponse -> Session ()
+ Network.Wai.Test: assertHeader :: CI ByteString -> ByteString -> SResponse -> Session ()
+ Network.Wai.Test: assertNoHeader :: CI ByteString -> SResponse -> Session ()
+ Network.Wai.Test: assertStatus :: Int -> SResponse -> Session ()
+ Network.Wai.Test: data SRequest
+ Network.Wai.Test: data SResponse
+ Network.Wai.Test: data WaiTestFailure
+ Network.Wai.Test: defaultRequest :: Request
+ Network.Wai.Test: instance Eq SResponse
+ Network.Wai.Test: instance Eq WaiTestFailure
+ Network.Wai.Test: instance Exception WaiTestFailure
+ Network.Wai.Test: instance Show SResponse
+ Network.Wai.Test: instance Show WaiTestFailure
+ Network.Wai.Test: instance Typeable WaiTestFailure
+ Network.Wai.Test: request :: Request -> Session SResponse
+ Network.Wai.Test: runSession :: Session a -> Application -> IO a
+ Network.Wai.Test: setPath :: Request -> ByteString -> Request
+ Network.Wai.Test: setRawPathInfo :: Request -> ByteString -> Request
+ Network.Wai.Test: simpleBody :: SResponse -> ByteString
+ Network.Wai.Test: simpleHeaders :: SResponse -> ResponseHeaders
+ Network.Wai.Test: simpleRequest :: SRequest -> Request
+ Network.Wai.Test: simpleRequestBody :: SRequest -> ByteString
+ Network.Wai.Test: simpleStatus :: SResponse -> Status
+ Network.Wai.Test: srequest :: SRequest -> Session SResponse
+ Network.Wai.Test: type Session = ReaderT Application (StateT ClientState IO)
- Network.Wai.Handler.CGI: requestBodyFunc :: (Int -> IO (Maybe ByteString)) -> Int -> Source IO ByteString
+ Network.Wai.Handler.CGI: requestBodyFunc :: (Int -> IO (Maybe ByteString)) -> Int -> IO (IO ByteString)
- Network.Wai.Handler.CGI: runGeneric :: [(String, String)] -> (Int -> Source IO ByteString) -> (ByteString -> IO ()) -> Maybe ByteString -> Application -> IO ()
+ Network.Wai.Handler.CGI: runGeneric :: [(String, String)] -> (Int -> IO (IO ByteString)) -> (ByteString -> IO ()) -> Maybe ByteString -> Application -> IO ()
- Network.Wai.Parse: lbsBackEnd :: Monad m => ignored1 -> ignored2 -> Sink ByteString m ByteString
+ Network.Wai.Parse: lbsBackEnd :: Monad m => ignored1 -> ignored2 -> m ByteString -> m ByteString
- Network.Wai.Parse: sinkRequestBody :: BackEnd y -> RequestBodyType -> Sink ByteString IO ([Param], [File y])
+ Network.Wai.Parse: sinkRequestBody :: BackEnd y -> RequestBodyType -> IO ByteString -> IO ([Param], [File y])
- Network.Wai.Parse: tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> Sink ByteString IO FilePath
+ Network.Wai.Parse: tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> IO ByteString -> IO FilePath
- Network.Wai.Parse: tempFileBackEndOpts :: IO FilePath -> String -> InternalState -> ignored1 -> ignored2 -> Sink ByteString IO FilePath
+ Network.Wai.Parse: tempFileBackEndOpts :: IO FilePath -> String -> InternalState -> ignored1 -> ignored2 -> IO ByteString -> IO FilePath
- Network.Wai.Parse: type BackEnd a = ByteString -> FileInfo () -> Sink ByteString IO a
+ Network.Wai.Parse: type BackEnd a = ByteString -> FileInfo () -> IO ByteString -> IO a
Files
- Network/Wai/EventSource.hs +38/−0
- Network/Wai/EventSource/EventStream.hs +75/−0
- Network/Wai/Handler/CGI.hs +54/−53
- Network/Wai/Middleware/Autohead.hs +5/−10
- Network/Wai/Middleware/CleanPath.hs +3/−3
- Network/Wai/Middleware/Gzip.hs +72/−30
- Network/Wai/Middleware/HttpAuth.hs +4/−4
- Network/Wai/Middleware/Jsonp.hs +13/−19
- Network/Wai/Middleware/MethodOverridePost.hs +13/−10
- Network/Wai/Middleware/RequestLogger.hs +39/−40
- Network/Wai/Middleware/Rewrite.hs +2/−2
- Network/Wai/Middleware/Vhost.hs +3/−3
- Network/Wai/Parse.hs +162/−105
- Network/Wai/Test.hs +214/−0
- Network/Wai/UrlMap.hs +6/−5
- test/Network/Wai/TestSpec.hs +36/−0
- test/Spec.hs +1/−0
- test/WaiExtraTest.hs +29/−31
- wai-extra.cabal +19/−11
+ Network/Wai/EventSource.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+ A WAI adapter to the HTML5 Server-Sent Events API.+-}+module Network.Wai.EventSource (+ ServerEvent(..),+ eventSourceAppChan,+ eventSourceAppIO+ ) where++import Blaze.ByteString.Builder (Builder)+import Data.Function (fix)+import Control.Concurrent.Chan (Chan, dupChan, readChan)+import Control.Monad.IO.Class (liftIO)+import Network.HTTP.Types (status200)+import Network.Wai (Application, Response, responseStream)++import Network.Wai.EventSource.EventStream++-- | Make a new WAI EventSource application reading events from+-- the given channel.+eventSourceAppChan :: Chan ServerEvent -> Application+eventSourceAppChan chan req sendResponse = do+ chan' <- liftIO $ dupChan chan+ eventSourceAppIO (readChan chan') req sendResponse++-- | Make a new WAI EventSource application reading events from+-- the given IO action.+eventSourceAppIO :: IO ServerEvent -> Application+eventSourceAppIO src _ sendResponse =+ sendResponse $ responseStream+ status200+ [("Content-Type", "text/event-stream")]+ $ \sendChunk flush -> fix $ \loop -> do+ se <- src+ case eventToBuilder se of+ Nothing -> return ()+ Just b -> sendChunk b >> flush >> loop
+ Network/Wai/EventSource/EventStream.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}+{- code adapted by Mathias Billman originaly from Chris Smith https://github.com/cdsmith/gloss-web -}++{-|+ Internal module, usually you don't need to use it.+-}+module Network.Wai.EventSource.EventStream (+ ServerEvent(..),+ eventToBuilder+ ) where++import Blaze.ByteString.Builder+import Blaze.ByteString.Builder.Char8+import Data.Monoid++{-|+ Type representing a communication over an event stream. This can be an+ actual event, a comment, a modification to the retry timer, or a special+ "close" event indicating the server should close the connection.+-}+data ServerEvent+ = ServerEvent {+ eventName :: Maybe Builder,+ eventId :: Maybe Builder,+ eventData :: [Builder]+ }+ | CommentEvent {+ eventComment :: Builder+ }+ | RetryEvent {+ eventRetry :: Int+ }+ | CloseEvent+++{-|+ Newline as a Builder.+-}+nl :: Builder+nl = fromChar '\n'+++{-|+ Field names as Builder+-}+nameField, idField, dataField, retryField, commentField :: Builder+nameField = fromString "event:"+idField = fromString "id:"+dataField = fromString "data:"+retryField = fromString "retry:"+commentField = fromChar ':'+++{-|+ Wraps the text as a labeled field of an event stream.+-}+field :: Builder -> Builder -> Builder+field l b = l `mappend` b `mappend` nl+++{-|+ Converts a 'ServerEvent' to its wire representation as specified by the+ @text/event-stream@ content type.+-}+eventToBuilder :: ServerEvent -> Maybe Builder+eventToBuilder (CommentEvent txt) = Just $ field commentField txt+eventToBuilder (RetryEvent n) = Just $ field retryField (fromShow n)+eventToBuilder (CloseEvent) = Nothing+eventToBuilder (ServerEvent n i d)= Just $+ (name n $ evid i $ mconcat (map (field dataField) d)) `mappend` nl+ where+ name Nothing = id+ name (Just n') = mappend (field nameField n')+ evid Nothing = id+ evid (Just i') = mappend (field idField i')
Network/Wai/Handler/CGI.hs view
@@ -13,8 +13,8 @@ import Network.Wai import Network.Wai.Internal import Network.Socket (getAddrInfo, addrAddress)+import Data.IORef import Data.Maybe (fromMaybe)-import Control.Exception (mask) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import Control.Arrow ((***))@@ -22,17 +22,17 @@ import qualified System.IO import qualified Data.String as String import Data.Monoid (mconcat, mempty)-import Blaze.ByteString.Builder (fromByteString, toLazyByteString)+import Blaze.ByteString.Builder (fromByteString, toLazyByteString, flush) import Blaze.ByteString.Builder.Char8 (fromChar, fromString)-import Data.Conduit.Blaze (builderToByteStringFlush)-import Control.Monad.IO.Class (liftIO) import Data.ByteString.Lazy.Internal (defaultChunkSize) import System.IO (Handle) import Network.HTTP.Types (Status (..)) import qualified Network.HTTP.Types as H import qualified Data.CaseInsensitive as CI import Data.Monoid (mappend)-import Data.Conduit+import qualified Data.Streaming.Blaze as Blaze+import Data.Function (fix)+import Control.Monad (unless, void) #if WINDOWS import System.Environment (getEnvironment)@@ -76,7 +76,7 @@ -- stick with 'run' or 'runSendfile'. runGeneric :: [(String, String)] -- ^ all variables- -> (Int -> Source IO B.ByteString) -- ^ responseBody of input+ -> (Int -> IO (IO B.ByteString)) -- ^ responseBody of input -> (B.ByteString -> IO ()) -- ^ destination for output -> Maybe B.ByteString -- ^ does the server support the X-Sendfile header? -> Application@@ -98,40 +98,48 @@ "https" -> True _ -> False addrs <- getAddrInfo Nothing (Just remoteHost') Nothing+ requestBody' <- inputH contentLength let addr = case addrs of a:_ -> addrAddress a [] -> error $ "Invalid REMOTE_ADDR or REMOTE_HOST: " ++ remoteHost'- mask $ \restore -> do- let reqHeaders = map (cleanupVarName *** B.pack) vars- env = Request- { requestMethod = rmethod- , rawPathInfo = B.pack pinfo- , pathInfo = H.decodePathSegments $ B.pack pinfo- , rawQueryString = B.pack qstring- , queryString = H.parseQuery $ B.pack qstring- , requestHeaders = reqHeaders- , isSecure = isSecure'- , remoteHost = addr- , httpVersion = H.http11 -- FIXME- , requestBody = inputH contentLength- , vault = mempty- , requestBodyLength = KnownLength $ fromIntegral contentLength- , requestHeaderHost = lookup "host" reqHeaders- , requestHeaderRange = lookup "range" reqHeaders- }- -- FIXME worry about exception?- res <- restore $ app env+ reqHeaders = map (cleanupVarName *** B.pack) vars+ env = Request+ { requestMethod = rmethod+ , rawPathInfo = B.pack pinfo+ , pathInfo = H.decodePathSegments $ B.pack pinfo+ , rawQueryString = B.pack qstring+ , queryString = H.parseQuery $ B.pack qstring+ , requestHeaders = reqHeaders+ , isSecure = isSecure'+ , remoteHost = addr+ , httpVersion = H.http11 -- FIXME+ , requestBody = requestBody'+ , vault = mempty+ , requestBodyLength = KnownLength $ fromIntegral contentLength+ , requestHeaderHost = lookup "host" reqHeaders+ , requestHeaderRange = lookup "range" reqHeaders+ }+ void $ app env $ \res -> case (xsendfile, res) of- (Just sf, ResponseFile s hs fp Nothing) ->- restore $ mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp+ (Just sf, ResponseFile s hs fp Nothing) -> do+ mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp+ return ResponseReceived _ -> do- let (s, hs, wb) = responseToSource res- wb $ \b ->- let src = do- yield (Chunk $ headers s hs `mappend` fromChar '\n')- b- in src $$ builderSink+ let (s, hs, wb) = responseToStream res+ (blazeRecv, blazeFinish) <- Blaze.newBlazeRecv Blaze.defaultStrategy+ wb $ \b -> do+ let sendBuilder builder = do+ popper <- blazeRecv builder+ fix $ \loop -> do+ bs <- popper+ unless (B.null bs) $ do+ outputH bs+ loop+ sendBuilder $ headers s hs `mappend` fromChar '\n'+ b sendBuilder (sendBuilder flush)+ blazeFinish >>= maybe (return ()) outputH+ return ResponseReceived where headers s hs = mconcat (map header $ status s : map header' (fixHeaders hs)) status (Status i m) = (fromByteString "Status", mconcat@@ -153,13 +161,6 @@ , fromByteString sf , fromByteString " not supported" ]- bsSink = await >>= maybe (return ()) push- push (Chunk bs) = do- liftIO $ outputH bs- bsSink- -- FIXME actually flush?- push Flush = bsSink- builderSink = builderToByteStringFlush =$ bsSink fixHeaders h = case lookup "content-type" h of Nothing -> ("Content-Type", "text/html; charset=utf-8") : h@@ -178,19 +179,19 @@ helper' (x:rest) = toLower x : helper' rest helper' [] = [] -requestBodyHandle :: Handle -> Int -> Source IO B.ByteString+requestBodyHandle :: Handle -> Int -> IO (IO B.ByteString) requestBodyHandle h = requestBodyFunc $ \i -> do bs <- B.hGet h i return $ if B.null bs then Nothing else Just bs -requestBodyFunc :: (Int -> IO (Maybe B.ByteString)) -> Int -> Source IO B.ByteString-requestBodyFunc get =- loop- where- loop 0 = return ()- loop count = do- mbs <- liftIO $ get $ min count defaultChunkSize- let count' = count - maybe 0 B.length mbs- case mbs of- Nothing -> return ()- Just bs -> yield bs >> loop count'+requestBodyFunc :: (Int -> IO (Maybe B.ByteString)) -> Int -> IO (IO B.ByteString)+requestBodyFunc get count0 = do+ ref <- newIORef count0+ return $ do+ count <- readIORef ref+ if count <= 0+ then return B.empty+ else do+ mbs <- get $ min count defaultChunkSize+ writeIORef ref $ count - maybe 0 B.length mbs+ return $ fromMaybe B.empty mbs
Network/Wai/Middleware/Autohead.hs view
@@ -4,17 +4,12 @@ module Network.Wai.Middleware.Autohead (autohead) where import Network.Wai-import Network.Wai.Internal import Data.Monoid (mempty) autohead :: Middleware-autohead app req- | requestMethod req == "HEAD" = do- res <- app req { requestMethod = "GET" }- let go (ResponseFile s hs _ _) = ResponseBuilder s hs mempty- go (ResponseBuilder s hs _) = ResponseBuilder s hs mempty- go (ResponseSource s hs _) = ResponseBuilder s hs mempty- go (ResponseRaw raw r) = ResponseRaw raw (go r)- return (go res)- | otherwise = app req+autohead app req sendResponse+ | requestMethod req == "HEAD" = app req { requestMethod = "GET" } $ \res -> do+ let (s, hs, _) = responseToStream res+ sendResponse $ responseBuilder s hs mempty+ | otherwise = app req sendResponse
Network/Wai/Middleware/CleanPath.hs view
@@ -14,10 +14,10 @@ -> B.ByteString -> ([Text] -> Application) -> Application-cleanPath splitter prefix app env =+cleanPath splitter prefix app env sendResponse = case splitter $ pathInfo env of- Right pieces -> app pieces env- Left p -> return+ Right pieces -> app pieces env sendResponse+ Left p -> sendResponse $ responseLBS status301 [("Location", mconcat [prefix, p, suffix])] $ L.empty
Network/Wai/Middleware/Gzip.hs view
@@ -28,20 +28,23 @@ import Data.Maybe (fromMaybe, isJust) import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString as S-import Data.Default+import Data.Default.Class import Network.HTTP.Types (Status, Header) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Resource (runResourceT) import System.Directory (doesFileExist, createDirectoryIfMissing)-import qualified Data.Conduit as C-import qualified Data.Conduit.Zlib as CZ-import qualified Data.Conduit.Binary as CB-import qualified Data.Conduit.List as CL-import Data.Conduit.Blaze (builderToByteStringFlush) import Blaze.ByteString.Builder (fromByteString) import Control.Exception (try, SomeException) import qualified Data.Set as Set import Network.Wai.Internal+import qualified Data.Streaming.Blaze as B+import qualified Data.Streaming.Zlib as Z+import qualified Blaze.ByteString.Builder as Blaze+import Control.Monad (unless)+import Data.Function (fix)+import Control.Exception (throwIO)+import qualified System.IO as IO+import Data.ByteString.Lazy.Internal (defaultChunkSize) data GzipSettings = GzipSettings { gzipFiles :: GzipFiles@@ -74,20 +77,19 @@ -- -- * Only compress if the response is above a certain size. gzip :: GzipSettings -> Middleware-gzip set app env = do- res <- app env+gzip set app env sendResponse = app env $ \res -> case res of- ResponseFile{} | gzipFiles set == GzipIgnore -> return res+ ResponseFile{} | gzipFiles set == GzipIgnore -> sendResponse res _ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res) then case (res, gzipFiles set) of (ResponseFile s hs file Nothing, GzipCacheFolder cache) -> case lookup "content-type" hs of Just m- | gzipCheckMime set m -> liftIO $ compressFile s hs file cache- _ -> return res- _ -> return $ compressE set res- else return res+ | gzipCheckMime set m -> compressFile s hs file cache sendResponse+ _ -> sendResponse res+ _ -> compressE set res sendResponse+ else sendResponse res where enc = fromMaybe [] $ (splitCommas . S8.unpack) `fmap` lookup "Accept-Encoding" (requestHeaders env)@@ -95,22 +97,38 @@ isMSIE6 = "MSIE 6" `S.isInfixOf` ua isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res -compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response-compressFile s hs file cache = do+compressFile :: Status -> [Header] -> FilePath -> FilePath -> (Response -> IO a) -> IO a+compressFile s hs file cache sendResponse = do e <- doesFileExist tmpfile if e then onSucc else do createDirectoryIfMissing True cache- x <-- try $ runResourceT $ CB.sourceFile file- C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile- either onErr (const onSucc) x+ x <- try $+ IO.withBinaryFile file IO.ReadMode $ \inH ->+ IO.withBinaryFile tmpfile IO.WriteMode $ \outH -> do+ deflate <- Z.initDeflate 7 $ Z.WindowBits 31+ -- FIXME this code should write to a temporary file, then+ -- rename to the final file+ let goPopper popper = fix $ \loop -> do+ res <- popper+ case res of+ Z.PRDone -> return ()+ Z.PRNext bs -> do+ S.hPut outH bs+ loop+ Z.PRError e -> throwIO e+ fix $ \loop -> do+ bs <- S.hGetSome inH defaultChunkSize+ unless (S.null bs) $ do+ Z.feedDeflate deflate bs >>= goPopper+ loop+ goPopper $ Z.finishDeflate deflate+ either onErr (const onSucc) (x :: Either SomeException ()) -- FIXME bad! don't catch all exceptions like that! where- onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing+ onSucc = sendResponse $ responseFile s (fixHeaders hs) tmpfile Nothing - onErr :: SomeException -> IO Response- onErr = const $ return $ ResponseFile s hs file Nothing -- FIXME log the error message+ onErr _ = sendResponse $ responseFile s hs file Nothing -- FIXME log the error message tmpfile = cache ++ '/' : map safe file safe c@@ -123,18 +141,42 @@ compressE :: GzipSettings -> Response- -> Response-compressE set res =+ -> (Response -> IO a)+ -> IO a+compressE set res sendResponse = case lookup "content-type" hs of Just m | gzipCheckMime set m -> let hs' = fixHeaders hs- in ResponseSource s hs' $ \f -> wb $ \b -> f $- b C.$= builderToByteStringFlush- C.$= CZ.compressFlush 1 (CZ.WindowBits 31)- C.$= CL.map (fmap fromByteString)- _ -> res+ in wb $ \body -> sendResponse $ responseStream s hs' $ \sendChunk flush -> do+ (blazeRecv, blazeFinish) <- B.newBlazeRecv B.defaultStrategy+ deflate <- Z.initDeflate 1 (Z.WindowBits 31)+ let sendBuilder builder = do+ popper <- blazeRecv builder+ fix $ \loop -> do+ bs <- popper+ unless (S.null bs) $ do+ sendBS bs+ loop+ sendBS bs = Z.feedDeflate deflate bs >>= deflatePopper+ flushBuilder = do+ sendBuilder Blaze.flush+ deflatePopper $ Z.flushDeflate deflate+ flush+ deflatePopper popper = fix $ \loop -> do+ res <- popper+ case res of+ Z.PRDone -> return ()+ Z.PRNext bs' -> do+ sendChunk $ fromByteString bs'+ loop+ Z.PRError e -> throwIO e++ body sendBuilder flushBuilder+ sendBuilder Blaze.flush+ deflatePopper $ Z.finishDeflate deflate+ _ -> sendResponse res where- (s, hs, wb) = responseToSource res+ (s, hs, wb) = responseToStream res -- Remove Content-Length header, since we will certainly have a -- different length after gzip compression.
Network/Wai/Middleware/HttpAuth.hs view
@@ -32,12 +32,12 @@ basicAuth :: CheckCreds -> AuthSettings -> Middleware-basicAuth checkCreds AuthSettings {..} app req = do+basicAuth checkCreds AuthSettings {..} app req sendResponse = do isProtected <- authIsProtected req allowed <- if isProtected then check else return True if allowed- then app req- else authOnNoAuth authRealm req+ then app req sendResponse+ else authOnNoAuth authRealm req sendResponse where check = case lookup "Authorization" $ requestHeaders req of@@ -83,7 +83,7 @@ instance IsString AuthSettings where fromString s = AuthSettings { authRealm = fromString s- , authOnNoAuth = \realm _req -> return $ responseLBS+ , authOnNoAuth = \realm _req f -> f $ responseLBS status401 [ ("Content-Type", "text/plain") , ("WWW-Authenticate", S.concat
Network/Wai/Middleware/Jsonp.hs view
@@ -25,7 +25,6 @@ import Control.Monad (join) import Data.Maybe (fromMaybe) import qualified Data.ByteString as S-import qualified Data.Conduit as C import Data.CaseInsensitive (CI) import Network.HTTP.Types (Status) @@ -37,7 +36,7 @@ -- having a content type of \"text\/javascript\" and calling the specified -- callback function. jsonp :: Middleware-jsonp app env = do+jsonp app env sendResponse = do let accept = fromMaybe B8.empty $ lookup "Accept" $ requestHeaders env let callback :: Maybe B8.ByteString callback =@@ -52,15 +51,15 @@ "application/json" $ requestHeaders env }- res <- app env'- return $ case callback of- Nothing -> res- Just c -> go c res+ app env' $ \res ->+ case callback of+ Nothing -> sendResponse res+ Just c -> go c res where go c r@(ResponseBuilder s hs b) =- case checkJSON hs of+ sendResponse $ case checkJSON hs of Nothing -> r- Just hs' -> ResponseBuilder s hs' $+ Just hs' -> responseBuilder s hs' $ copyByteString c `mappend` fromChar '(' `mappend` b@@ -68,9 +67,9 @@ go c r = case checkJSON hs of Just hs' -> addCallback c s hs' wb- Nothing -> r+ Nothing -> sendResponse r where- (s, hs, wb) = responseToSource r+ (s, hs, wb) = responseToStream r checkJSON hs = case lookup "Content-Type" hs of@@ -80,16 +79,11 @@ _ -> Nothing fixHeaders = changeVal "Content-Type" "text/javascript" - addCallback :: ByteString- -> Status- -> [(CI ByteString, ByteString)]- -> (forall b. WithSource IO (C.Flush Builder) b)- -> Response addCallback cb s hs wb =- ResponseSource s hs $ \f -> wb $ \b -> f $- C.yield (C.Chunk $ copyByteString cb `mappend` fromChar '(')- `mappend` b- `mappend` C.yield (C.Chunk $ fromChar ')')+ wb $ \body -> sendResponse $ responseStream s hs $ \sendChunk flush -> do+ sendChunk $ copyByteString cb `mappend` fromChar '('+ body sendChunk flush+ sendChunk $ fromChar ')' changeVal :: Eq a => a
Network/Wai/Middleware/MethodOverridePost.hs view
@@ -10,9 +10,9 @@ import Network.Wai import Network.HTTP.Types (parseQuery)-import Data.Monoid (mconcat)-import Data.Conduit.Lazy (lazyConsume)-import Data.Conduit.List (sourceList)+import Data.Monoid (mconcat, mempty)+import Data.IORef+import Data.ByteString.Lazy (toChunks) -- | Allows overriding of the HTTP request method via the _method post string parameter. --@@ -26,13 +26,16 @@ -- * This middleware only applies when the initial request method is POST. -- methodOverridePost :: Middleware-methodOverridePost app req = case (requestMethod req, lookup "Content-Type" (requestHeaders req)) of- ("POST", Just "application/x-www-form-urlencoded") -> setPost req >>= app- _ -> app req+methodOverridePost app req send =+ case (requestMethod req, lookup "Content-Type" (requestHeaders req)) of+ ("POST", Just "application/x-www-form-urlencoded") -> setPost req >>= flip app send+ _ -> app req send setPost :: Request -> IO Request setPost req = do- body <- lazyConsume (requestBody req)- case parseQuery (mconcat body) of- (("_method", Just newmethod):_) -> return $ req {requestBody = sourceList body, requestMethod = newmethod}- _ -> return $ req {requestBody = sourceList body}+ body <- (mconcat . toChunks) `fmap` lazyRequestBody req+ ref <- newIORef body+ let rb = atomicModifyIORef ref $ \bs -> (mempty, bs)+ case parseQuery body of+ (("_method", Just newmethod):_) -> return $ req {requestBody = rb, requestMethod = newmethod}+ _ -> return $ req {requestBody = rb}
Network/Wai/Middleware/RequestLogger.hs view
@@ -29,20 +29,16 @@ import Network.HTTP.Types as H import Data.Maybe (fromMaybe) import Data.Monoid (mconcat)-import Data.Time (getCurrentTime, diffUTCTime) import Network.Wai.Parse (sinkRequestBody, lbsBackEnd, fileName, Param, File, getRequestBodyType) import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Char8 as S8 -import qualified Data.Conduit as C-import qualified Data.Conduit.List as CL- import System.Console.ANSI import Data.IORef.Lifted import System.IO.Unsafe -import Data.Default (Default (def))+import Data.Default.Class (Default (def)) import Network.Wai.Logger import Network.Wai.Middleware.RequestLogger.Internal @@ -97,23 +93,21 @@ return $ customMiddleware callback getdate formatter apacheMiddleware :: ApacheLoggerActions -> Middleware-apacheMiddleware ala app req = do- res <- app req+apacheMiddleware ala app req sendResponse = app req $ \res -> do let msize = lookup "content-length" (responseHeaders res) >>= readInt' readInt' bs = case S8.readInteger bs of Just (i, "") -> Just i _ -> Nothing apacheLogger ala req (responseStatus res) msize- return res+ sendResponse res customMiddleware :: Callback -> IO ZonedDate -> OutputFormatter -> Middleware-customMiddleware cb getdate formatter app req = do- res <- app req+customMiddleware cb getdate formatter app req sendResponse = app req $ \res -> do date <- liftIO getdate -- We use Nothing for the response size since we generally don't know it liftIO $ cb $ formatter date req (responseStatus res) Nothing- return res+ sendResponse res -- | Production request logger middleware. -- Implemented on top of "logCallback", but prints to 'stdout'@@ -156,11 +150,13 @@ -- -- Example ouput: ----- > GET search :: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8+-- > GET search+-- > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -- > -- > Status: 200 OK. search -- >--- > GET static/css/normalize.css :: text/css,*/*;q=0.1+-- > GET static/css/normalize.css+-- > Accept: text/css,*/*;q=0.1 -- > GET [("LXwioiBG","")] -- > -- > Status: 304 Not Modified. static/css/normalize.css@@ -185,13 +181,18 @@ detailedMiddleware' :: Callback -> IO (BS.ByteString -> [BS.ByteString]) -> Middleware-detailedMiddleware' cb getAddColor app req = do+detailedMiddleware' cb getAddColor app req sendResponse = do let mlen = lookup "content-length" (requestHeaders req) >>= readInt (req', body) <- case mlen of -- log the request body if it is small Just len | len <= 2048 -> do- body <- requestBody req C.$$ CL.consume+ let loop front = do+ bs <- requestBody req+ if S8.null bs+ then return $ front []+ else loop $ front . (bs:)+ body <- loop id -- logging the body here consumes it, so fill it back up -- obviously not efficient, but this is the development logger --@@ -203,14 +204,10 @@ -- implementation ensures that each chunk is only returned -- once. ichunks <- newIORef body- let rbody = do- chunks <- readIORef ichunks+ let rbody = atomicModifyIORef ichunks $ \chunks -> case chunks of- [] -> return ()- x:xs -> do- writeIORef ichunks xs- C.yield x- rbody+ [] -> ([], S8.empty)+ x:y -> (y, x) let req' = req { requestBody = rbody } return (req', body) _ -> return (req, [])@@ -236,24 +233,20 @@ , "\n" ] - t0 <- getCurrentTime- rsp <- app req'- t1 <- getCurrentTime+ app req' $ \rsp -> do - -- log the status of the response- -- this is color coordinated with the request logging- -- also includes the request path to connect it to the request- liftIO $ cb $ mconcat $ map toLogStr $- addColor "Status: " ++ statusBS rsp ++- [ " "- , msgBS rsp- , ". "- , pack $ show $ diffUTCTime t1 t0- , ". "- , rawPathInfo req -- if you need help matching the 2 logging statements- , "\n"- ]- return rsp+ -- log the status of the response+ -- this is color coordinated with the request logging+ -- also includes the request path to connect it to the request+ liftIO $ cb $ mconcat $ map toLogStr $+ addColor "Status: " ++ statusBS rsp +++ [ " "+ , msgBS rsp+ , ". "+ , rawPathInfo req -- if you need help matching the 2 logging statements+ , "\n"+ ]+ sendResponse rsp where paramsToBS prefix params = if null params then ""@@ -262,7 +255,13 @@ allPostParams body = case getRequestBodyType req of Nothing -> return ([], [])- Just rbt -> CL.sourceList body C.$$ sinkRequestBody lbsBackEnd rbt+ Just rbt -> do+ ichunks <- newIORef body+ let rbody = atomicModifyIORef ichunks $ \chunks ->+ case chunks of+ [] -> ([], S8.empty)+ x:y -> (y, x)+ sinkRequestBody lbsBackEnd rbt rbody emptyGetParam :: (BS.ByteString, Maybe BS.ByteString) -> (BS.ByteString, BS.ByteString) emptyGetParam (k, Just v) = (k,v)
Network/Wai/Middleware/Rewrite.hs view
@@ -13,10 +13,10 @@ -- | rewrite based on your own conversion rules rewrite :: ([Text] -> H.RequestHeaders -> IO [Text]) -> Middleware-rewrite convert app req = do+rewrite convert app req sendResponse = do newPathInfo <- liftIO $ convert (pathInfo req) (requestHeaders req) let rawPInfo = TE.encodeUtf8 $ T.intercalate "/" newPathInfo- app req { pathInfo = newPathInfo, rawPathInfo = rawPInfo }+ app req { pathInfo = newPathInfo, rawPathInfo = rawPInfo } sendResponse -- | rewrite based on your own conversion rules -- Example convert function:
Network/Wai/Middleware/Vhost.hs view
@@ -20,10 +20,10 @@ redirectIf home (maybe True (BS.isPrefixOf "www") . lookup "host" . requestHeaders) redirectIf :: Text -> (Request -> Bool) -> Application -> Application-redirectIf home cond app req =+redirectIf home cond app req sendResponse = if cond req- then return $ redirectTo $ TE.encodeUtf8 home- else app req+ then sendResponse $ redirectTo $ TE.encodeUtf8 home+ else app req sendResponse redirectTo :: BS.ByteString -> Response redirectTo location = responseLBS H.status301
Network/Wai/Parse.hs view
@@ -12,7 +12,6 @@ , RequestBodyType (..) , getRequestBodyType , sinkRequestBody- , conduitRequestBody , BackEnd , lbsBackEnd , tempFileBackEnd@@ -38,22 +37,14 @@ import Data.Word (Word8) import Data.Maybe (fromMaybe) import Data.List (sortBy)-import Data.Function (on)+import Data.Function (on, fix) import System.Directory (removeFile, getTemporaryDirectory) import System.IO (hClose, openBinaryTempFile) import Network.Wai-import Data.Conduit-import Data.Conduit.Internal ()-import qualified Data.Conduit.List as CL-import qualified Data.Conduit.Binary as CB-import Control.Monad.IO.Class (liftIO) import qualified Network.HTTP.Types as H-import Data.Either (partitionEithers) import Control.Monad (when, unless)-import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Resource (allocate, release, register, InternalState, runInternalState)-import Data.Conduit.Internal (Pipe (NeedInput, HaveOutput), (>+>), withUpstream, injectLeftovers, ConduitM (..))-import Data.Void (Void)+import Data.IORef breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString) breakDiscard w s =@@ -85,15 +76,22 @@ _ -> 1.0 -- | Store uploaded files in memory-lbsBackEnd :: Monad m => ignored1 -> ignored2 -> Sink S.ByteString m L.ByteString-lbsBackEnd _ _ = fmap L.fromChunks CL.consume+lbsBackEnd :: Monad m => ignored1 -> ignored2 -> m S.ByteString -> m L.ByteString+lbsBackEnd _ _ popper =+ loop id+ where+ loop front = do+ bs <- popper+ if S.null bs+ then return $ L.fromChunks $ front []+ else loop $ front . (bs:) -- | Save uploaded files on disk as temporary files -- -- Note: starting with version 2.0, removal of temp files is registered with -- the provided @InternalState@. It is the responsibility of the caller to -- ensure that this @InternalState@ gets cleaned up.-tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> Sink S.ByteString IO FilePath+tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> IO S.ByteString -> IO FilePath tempFileBackEnd = tempFileBackEndOpts getTemporaryDirectory "webenc.buf" -- | Same as 'tempFileSink', but use configurable temp folders and patterns.@@ -102,14 +100,19 @@ -> InternalState -> ignored1 -> ignored2- -> Sink S.ByteString IO FilePath-tempFileBackEndOpts getTmpDir pattern internalState _ _ = do+ -> IO S.ByteString+ -> IO FilePath+tempFileBackEndOpts getTmpDir pattern internalState _ _ popper = do (key, (fp, h)) <- flip runInternalState internalState $ allocate (do tempDir <- getTmpDir openBinaryTempFile tempDir pattern) (\(_, h) -> hClose h) _ <- runInternalState (register $ removeFile fp) internalState- CB.sinkHandle h- lift $ release key+ fix $ \loop -> do+ bs <- popper+ unless (S.null bs) $ do+ S.hPut h bs+ loop+ release key return fp -- | Information on an uploaded file.@@ -126,11 +129,12 @@ -- | Post parameter name and associated file information. type File y = (S.ByteString, FileInfo y) --- | A file uploading backend. Takes the parameter name, file name, and content--- type, and returns a `Sink` for storing the contents.+-- | A file uploading backend. Takes the parameter name, file name, and a+-- stream of data. type BackEnd a = S.ByteString -- ^ parameter name -> FileInfo ()- -> Sink S.ByteString IO a+ -> IO S.ByteString+ -> IO a data RequestBodyType = UrlEncoded | Multipart S.ByteString @@ -173,60 +177,99 @@ parseRequestBody s r = case getRequestBodyType r of Nothing -> return ([], [])- Just rbt -> fmap partitionEithers $ requestBody r $$ conduitRequestBody s rbt =$ CL.consume+ Just rbt -> sinkRequestBody s rbt (requestBody r) sinkRequestBody :: BackEnd y -> RequestBodyType- -> Sink S.ByteString IO ([Param], [File y])-sinkRequestBody s r = fmap partitionEithers $ conduitRequestBody s r =$ CL.consume+ -> IO S.ByteString+ -> IO ([Param], [File y])+sinkRequestBody s r body = do+ ref <- newIORef (id, id)+ let add x = atomicModifyIORef ref $ \(y, z) ->+ case x of+ Left y' -> ((y . (y':), z), ())+ Right z' -> ((y, z . (z':)), ())+ conduitRequestBody s r body add+ (x, y) <- readIORef ref+ return (x [], y []) conduitRequestBody :: BackEnd y -> RequestBodyType- -> Conduit S.ByteString IO (Either Param (File y))-conduitRequestBody _ UrlEncoded = do+ -> IO S.ByteString+ -> (Either Param (File y) -> IO ())+ -> IO ()+conduitRequestBody _ UrlEncoded rbody add = do -- NOTE: in general, url-encoded data will be in a single chunk. -- Therefore, I'm optimizing for the usual case by sticking with -- strict byte strings here.- bs <- CL.consume- mapM_ yield $ map Left $ H.parseSimpleQuery $ S.concat bs-conduitRequestBody backend (Multipart bound) =- parsePieces backend $ S8.pack "--" `S.append` bound+ let loop front = do+ bs <- rbody+ if S.null bs+ then return $ S.concat $ front []+ else loop $ front . (bs:)+ bs <- loop id+ mapM_ (add . Left) $ H.parseSimpleQuery bs+conduitRequestBody backend (Multipart bound) rbody add =+ parsePieces backend (S8.pack "--" `S.append` bound) rbody add -takeLine :: Monad m => Consumer S.ByteString m (Maybe S.ByteString)-takeLine =+takeLine :: Source -> IO (Maybe S.ByteString)+takeLine src = go id where- go front = await >>= maybe (close front) (push front)+ go front = do+ bs <- readSource src+ if S.null bs+ then close front+ else push front bs - close front = leftover (front S.empty) >> return Nothing+ close front = leftover src (front S.empty) >> return Nothing push front bs = do let (x, y) = S.break (== 10) $ front bs -- LF in if S.null y then go $ S.append x else do- when (S.length y > 1) $ leftover $ S.drop 1 y+ when (S.length y > 1) $ leftover src $ S.drop 1 y return $ Just $ killCR x -takeLines :: Consumer S.ByteString IO [S.ByteString]-takeLines = do- res <- takeLine+takeLines :: Source -> IO [S.ByteString]+takeLines src = do+ res <- takeLine src case res of Nothing -> return [] Just l | S.null l -> return [] | otherwise -> do- ls <- takeLines+ ls <- takeLines src return $ l : ls +data Source = Source (IO S.ByteString) (IORef S.ByteString)++mkSource :: IO S.ByteString -> IO Source+mkSource f = do+ ref <- newIORef S.empty+ return $ Source f ref++readSource :: Source -> IO S.ByteString+readSource (Source f ref) = do+ bs <- atomicModifyIORef ref $ \bs -> (S.empty, bs)+ if S.null bs+ then f+ else return bs++leftover :: Source -> S.ByteString -> IO ()+leftover (Source _ ref) bs = writeIORef ref bs+ parsePieces :: BackEnd y -> S.ByteString- -> ConduitM S.ByteString (Either Param (File y)) IO ()-parsePieces sink bound =- loop+ -> IO S.ByteString+ -> (Either Param (File y) -> IO ())+ -> IO ()+parsePieces sink bound rbody add =+ mkSource rbody >>= loop where- loop = do- _boundLine <- takeLine- res' <- takeLines+ loop src = do+ _boundLine <- takeLine src+ res' <- takeLines src unless (null res') $ do let ls' = map parsePair res' let x = do@@ -239,23 +282,23 @@ Just (mct, name, Just filename) -> do let ct = fromMaybe "application/octet-stream" mct fi0 = FileInfo filename ct ()- (wasFound, y) <- sinkTillBound' bound name fi0 sink- yield $ Right (name, fi0 { fileContent = y })- when wasFound loop+ (wasFound, y) <- sinkTillBound' bound name fi0 sink src+ add $ Right (name, fi0 { fileContent = y })+ when wasFound (loop src) Just (_ct, name, Nothing) -> do let seed = id let iter front bs = return $ front . (:) bs- (wasFound, front) <- sinkTillBound bound iter seed+ (wasFound, front) <- sinkTillBound bound iter seed src let bs = S.concat $ front [] let x' = (name, bs)- yield $ Left x'- when wasFound loop+ add $ Left x'+ when wasFound (loop src) _ -> do -- ignore this part let seed = () iter () _ = return ()- (wasFound, ()) <- sinkTillBound bound iter seed- when wasFound loop+ (wasFound, ()) <- sinkTillBound bound iter seed src+ when wasFound (loop src) where contDisp = S8.pack "Content-Disposition" contType = S8.pack "Content-Type"@@ -295,64 +338,78 @@ -> S.ByteString -> FileInfo () -> BackEnd y- -> ConduitM S.ByteString o IO (Bool, y)-sinkTillBound' bound name fi sink =- ConduitM $ anyOutput $- conduitTillBound bound >+> withUpstream (fix $ sink name fi)- where- fix :: Sink S8.ByteString IO y -> Pipe Void S8.ByteString Void Bool IO y- fix p = ignoreTerm >+> injectLeftovers (unConduitM p)- ignoreTerm = await' >>= maybe (return ()) (\x -> yield' x >> ignoreTerm)- await' = NeedInput (return . Just) (const $ return Nothing)- yield' = HaveOutput (return ()) (return ())-- anyOutput p = p >+> dropInput- dropInput = NeedInput (const dropInput) return+ -> Source+ -> IO (Bool, y)+sinkTillBound' bound name fi sink src = do+ (next, final) <- wrapTillBound bound src+ y <- sink name fi next+ b <- final+ return (b, y) -conduitTillBound :: Monad m- => S.ByteString -- bound- -> Pipe S.ByteString S.ByteString S.ByteString () m Bool-conduitTillBound bound =- unConduitM $- go id+data WTB = WTBWorking (S.ByteString -> S.ByteString)+ | WTBDone Bool+wrapTillBound :: S.ByteString -- ^ bound+ -> Source+ -> IO (IO S.ByteString, IO Bool) -- ^ Bool indicates if the bound was found+wrapTillBound bound src = do+ ref <- newIORef $ WTBWorking id+ return (go ref, final ref) where- go front = await >>= maybe (close front) (push front)- close front = do- let bs = front S.empty- unless (S.null bs) $ yield bs- return False- push front bs' = do- let bs = front bs'- case findBound bound bs of- FoundBound before after -> do- let before' = killCRLF before- yield before'- leftover after- return True- NoBound -> do- -- don't emit newlines, in case it's part of a bound- let (toEmit, front') =- if not (S8.null bs) && S8.last bs `elem` "\r\n"- then let (x, y) = S.splitAt (S.length bs - 2) bs- in (x, S.append y)- else (bs, id)- yield toEmit- go front'- PartialBound -> go $ S.append bs+ final ref = do+ x <- readIORef ref+ case x of+ WTBWorking _ -> error "wrapTillBound did not finish"+ WTBDone y -> return y + go ref = do+ state <- readIORef ref+ case state of+ WTBDone _ -> return S.empty+ WTBWorking front -> do+ bs <- readSource src+ if S.null bs+ then do+ writeIORef ref $ WTBDone False+ return $ front bs+ else push $ front bs+ where+ push bs =+ case findBound bound bs of+ FoundBound before after -> do+ let before' = killCRLF before+ leftover src after+ writeIORef ref $ WTBDone True+ return before'+ NoBound -> do+ -- don't emit newlines, in case it's part of a bound+ let (toEmit, front') =+ if not (S8.null bs) && S8.last bs `elem` "\r\n"+ then let (x, y) = S.splitAt (S.length bs - 2) bs+ in (x, S.append y)+ else (bs, id)+ writeIORef ref $ WTBWorking front'+ if S.null toEmit+ then go ref+ else return toEmit+ PartialBound -> do+ writeIORef ref $ WTBWorking $ S.append bs+ go ref+ sinkTillBound :: S.ByteString -> (x -> S.ByteString -> IO x) -> x- -> Consumer S.ByteString IO (Bool, x)-sinkTillBound bound iter seed0 =- ConduitM $- (conduitTillBound bound >+> (withUpstream $ ij $ CL.foldM iter' seed0))- where- iter' a b = liftIO $ iter a b- ij (ConduitM p) = ignoreTerm >+> injectLeftovers p- ignoreTerm = await' >>= maybe (return ()) (\x -> yield' x >> ignoreTerm)- await' = NeedInput (return . Just) (const $ return Nothing)- yield' = HaveOutput (return ()) (return ())+ -> Source+ -> IO (Bool, x)+sinkTillBound bound iter seed0 src = do+ (next, final) <- wrapTillBound bound src+ let loop seed = do+ bs <- next+ if S.null bs+ then return seed+ else iter seed bs >>= loop+ seed <- loop seed0+ b <- final+ return (b, seed) parseAttrs :: S.ByteString -> [(S.ByteString, S.ByteString)] parseAttrs = map go . S.split 59 -- semicolon
+ Network/Wai/Test.hs view
@@ -0,0 +1,214 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+module Network.Wai.Test+ ( -- * Session+ Session+ , runSession+ -- * Requests+ , request+ , srequest+ , SRequest (..)+ , SResponse (..)+ , defaultRequest+ , setPath+ , setRawPathInfo+ -- * Assertions+ , assertStatus+ , assertContentType+ , assertBody+ , assertBodyContains+ , assertHeader+ , assertNoHeader+ , WaiTestFailure (..)+ ) where++import Network.Wai+import Network.Wai.Internal (ResponseReceived (ResponseReceived))+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.State (StateT, evalStateT)+import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)+import Control.Monad (unless)+import Control.DeepSeq (deepseq)+import Control.Exception (throwIO, Exception)+import Data.Typeable (Typeable)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as S8+import Blaze.ByteString.Builder (toLazyByteString)+import qualified Blaze.ByteString.Builder as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as L8+import qualified Network.HTTP.Types as H+import Data.CaseInsensitive (CI)+import qualified Data.ByteString as S+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.IORef+import Data.Monoid (mempty, mappend)++type Session = ReaderT Application (StateT ClientState IO)++data ClientState = ClientState+ { _clientCookies :: Map ByteString ByteString+ }++initState :: ClientState+initState = ClientState Map.empty++runSession :: Session a -> Application -> IO a+runSession session app = evalStateT (runReaderT session app) initState++data SRequest = SRequest+ { simpleRequest :: Request+ , simpleRequestBody :: L.ByteString+ }+data SResponse = SResponse+ { simpleStatus :: H.Status+ , simpleHeaders :: H.ResponseHeaders+ , simpleBody :: L.ByteString+ }+ deriving (Show, Eq)+request :: Request -> Session SResponse+request = srequest . flip SRequest L.empty++-- | Set whole path (request path + query string).+setPath :: Request -> S8.ByteString -> Request+setPath req path = req {+ pathInfo = segments+ , rawPathInfo = B.toByteString (H.encodePathSegments segments)+ , queryString = query+ , rawQueryString = (H.renderQuery True query)+ }+ where+ (segments, query) = H.decodePath path++setRawPathInfo :: Request -> S8.ByteString -> Request+setRawPathInfo r rawPinfo =+ let pInfo = dropFrontSlash $ T.split (== '/') $ TE.decodeUtf8 rawPinfo+ in r { rawPathInfo = rawPinfo, pathInfo = pInfo }+ where+ dropFrontSlash ("":"":[]) = [] -- homepage, a single slash+ dropFrontSlash ("":path) = path+ dropFrontSlash path = path++srequest :: SRequest -> Session SResponse+srequest (SRequest req bod) = do+ app <- ask+ refChunks <- liftIO $ newIORef $ L.toChunks bod+ let req' = req+ { requestBody = atomicModifyIORef refChunks $ \bss ->+ case bss of+ [] -> ([], S.empty)+ x:y -> (y, x)+ }+ liftIO $ do+ ref <- newIORef $ error "runResponse gave no result"+ ResponseReceived <- app req' (runResponse ref)+ readIORef ref+ -- FIXME cookie processing+ --return sres++runResponse :: IORef SResponse -> Response -> IO ResponseReceived+runResponse ref res = do+ refBuilder <- newIORef mempty+ let add y = atomicModifyIORef refBuilder $ \x -> (x `mappend` y, ())+ withBody $ \body -> body add (return ())+ builder <- readIORef refBuilder+ let lbs = toLazyByteString builder+ len = L.length lbs+ -- Force evaluation of the body to have exceptions thrown at the right+ -- time.+ seq len $ writeIORef ref $ SResponse s h $ toLazyByteString builder+ return ResponseReceived+ where+ (s, h, withBody) = responseToStream res++assertBool :: String -> Bool -> Session ()+assertBool s b = unless b $ assertFailure s++assertString :: String -> Session ()+assertString s = unless (null s) $ assertFailure s++assertFailure :: String -> Session ()+assertFailure msg = msg `deepseq` liftIO (throwIO (WaiTestFailure msg))++data WaiTestFailure = WaiTestFailure String+ deriving (Show, Eq, Typeable)+instance Exception WaiTestFailure++assertContentType :: ByteString -> SResponse -> Session ()+assertContentType ct SResponse{simpleHeaders = h} =+ case lookup "content-type" h of+ Nothing -> assertString $ concat+ [ "Expected content type "+ , show ct+ , ", but no content type provided"+ ]+ Just ct' -> assertBool (concat+ [ "Expected content type "+ , show ct+ , ", but received "+ , show ct'+ ]) (go ct == go ct')+ where+ go = S8.takeWhile (/= ';')++assertStatus :: Int -> SResponse -> Session ()+assertStatus i SResponse{simpleStatus = s} = assertBool (concat+ [ "Expected status code "+ , show i+ , ", but received "+ , show sc+ ]) $ i == sc+ where+ sc = H.statusCode s++assertBody :: L.ByteString -> SResponse -> Session ()+assertBody lbs SResponse{simpleBody = lbs'} = assertBool (concat+ [ "Expected response body "+ , show $ L8.unpack lbs+ , ", but received "+ , show $ L8.unpack lbs'+ ]) $ lbs == lbs'++assertBodyContains :: L.ByteString -> SResponse -> Session ()+assertBodyContains lbs SResponse{simpleBody = lbs'} = assertBool (concat+ [ "Expected response body to contain "+ , show $ L8.unpack lbs+ , ", but received "+ , show $ L8.unpack lbs'+ ]) $ strict lbs `S.isInfixOf` strict lbs'+ where+ strict = S.concat . L.toChunks++assertHeader :: CI ByteString -> ByteString -> SResponse -> Session ()+assertHeader header value SResponse{simpleHeaders = h} =+ case lookup header h of+ Nothing -> assertString $ concat+ [ "Expected header "+ , show header+ , " to be "+ , show value+ , ", but it was not present"+ ]+ Just value' -> assertBool (concat+ [ "Expected header "+ , show header+ , " to be "+ , show value+ , ", but received "+ , show value'+ ]) (value == value')++assertNoHeader :: CI ByteString -> SResponse -> Session ()+assertNoHeader header SResponse{simpleHeaders = h} =+ case lookup header h of+ Nothing -> return ()+ Just s -> assertString $ concat+ [ "Unexpected header "+ , show header+ , " containing "+ , show s+ ]
Network/Wai/UrlMap.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}+{-# LANGUAGE ExistentialQuantification #-} {- | This module gives you a way to mount applications under sub-URIs. For example: @@ -83,14 +84,14 @@ toApplication = id instance ToApplication UrlMap where- toApplication urlMap = \req ->+ toApplication urlMap req sendResponse = case try (pathInfo req) (unUrlMap urlMap) of Just (newPath, app) ->- app $ req { pathInfo = newPath- , rawPathInfo = makeRaw newPath- }+ app (req { pathInfo = newPath+ , rawPathInfo = makeRaw newPath+ }) sendResponse Nothing ->- return $ responseLBS+ sendResponse $ responseLBS status404 [("content-type", "text/plain")] "Not found\n"
+ test/Network/Wai/TestSpec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.Wai.TestSpec (main, spec) where++import Test.Hspec++import Network.Wai+import Network.Wai.Test++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "setPath" $ do++ let req = setPath defaultRequest "/foo/bar/baz?foo=23&bar=42&baz"++ it "sets pathInfo" $ do+ pathInfo req `shouldBe` ["foo", "bar", "baz"]++ it "utf8 path" $+ pathInfo (setPath defaultRequest "/foo/%D7%A9%D7%9C%D7%95%D7%9D/bar") `shouldBe`+ ["foo", "שלום", "bar"]++ it "sets rawPathInfo" $ do+ rawPathInfo req `shouldBe` "/foo/bar/baz"++ it "sets queryString" $ do+ queryString req `shouldBe` [("foo", Just "23"), ("bar", Just "42"), ("baz", Nothing)]++ it "sets rawQueryString" $ do+ rawQueryString req `shouldBe` "?foo=23&bar=42&baz"++ context "when path has no query string" $ do+ it "sets rawQueryString to empty string" $ do+ rawQueryString (setPath defaultRequest "/foo/bar/baz") `shouldBe` ""
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
test/WaiExtraTest.hs view
@@ -3,7 +3,7 @@ import Test.Hspec import Test.HUnit hiding (Test)-import Data.Monoid (mappend, mempty, (<>))+import Data.Monoid (mappend, mempty) import Network.Wai import Network.Wai.Test@@ -30,9 +30,6 @@ import Network.Wai.Middleware.RequestLogger import Codec.Compression.GZip (decompress) -import qualified Data.Conduit as C-import qualified Data.Conduit.List as CL-import Data.Conduit.Binary (sourceHandle) import Control.Monad.IO.Class (liftIO) import Data.Maybe (fromMaybe) import Network.HTTP.Types (parseSimpleQuery, status200)@@ -128,7 +125,13 @@ parseRequestBody' sink (SRequest req bod) = case getRequestBodyType req of Nothing -> return ([], [])- Just rbt -> CL.sourceList (L.toChunks bod) C.$$ sinkRequestBody sink rbt+ Just rbt -> do+ ref <- I.newIORef $ L.toChunks bod+ let rb = I.atomicModifyIORef ref $ \chunks ->+ case chunks of+ [] -> ([], S.empty)+ x:y -> (y, x)+ sinkRequestBody sink rbt rb caseParseRequestBody :: Assertion caseParseRequestBody =@@ -295,7 +298,7 @@ -} jsonpApp :: Application-jsonpApp = jsonp $ const $ return $ responseLBS+jsonpApp = jsonp $ \_ f -> f $ responseLBS status200 [("Content-Type", "application/json")] "{\"foo\":\"bar\"}"@@ -324,7 +327,7 @@ assertBody "{\"foo\":\"bar\"}" sres3 gzipApp :: Application-gzipApp = gzip def $ const $ return $ responseLBS status200+gzipApp = gzip def $ \_ f -> f $ responseLBS status200 [("Content-Type", "text/plain")] "test" @@ -332,7 +335,7 @@ -- that the compression is skipped based on the presence of -- the Content-Encoding header. gzipPrecompressedApp :: Application-gzipPrecompressedApp = gzip def $ const $ return $ responseLBS status200+gzipPrecompressedApp = gzip def $ \_ f -> f $ responseLBS status200 [("Content-Type", "text/plain"), ("Content-Encoding", "gzip")] "test" @@ -380,8 +383,8 @@ assertBody "test" sres1 -- the body is not actually compressed vhostApp1, vhostApp2, vhostApp :: Application-vhostApp1 = const $ return $ responseLBS status200 [] "app1"-vhostApp2 = const $ return $ responseLBS status200 [] "app2"+vhostApp1 _ f = f $ responseLBS status200 [] "app1"+vhostApp2 _ f = f $ responseLBS status200 [] "app2" vhostApp = vhost [ ((== Just "foo.com") . lookup "host" . requestHeaders, vhostApp1) ]@@ -400,7 +403,7 @@ assertBody "app2" sres2 autoheadApp :: Application-autoheadApp = autohead $ const $ return $ responseLBS status200+autoheadApp = autohead $ \_ f -> f $ responseLBS status200 [("Foo", "Bar")] "body" caseAutohead :: Assertion@@ -418,7 +421,7 @@ assertBody "" sres2 moApp :: Application-moApp = methodOverride $ \req -> return $ responseLBS status200+moApp = methodOverride $ \req f -> f $ responseLBS status200 [("Method", requestMethod req)] "" caseMethodOverride :: Assertion@@ -442,7 +445,7 @@ assertHeader "Method" "PUT" sres3 mopApp :: Application-mopApp = methodOverridePost $ \req -> return $ responseLBS status200 [("Method", requestMethod req)] ""+mopApp = methodOverridePost $ \req f -> f $ responseLBS status200 [("Method", requestMethod req)] "" caseMethodOverridePost :: Assertion caseMethodOverridePost = flip runSession mopApp $ do@@ -468,7 +471,7 @@ assertHeader "Method" "POST" sres4 aoApp :: Application-aoApp = acceptOverride $ \req -> return $ responseLBS status200+aoApp = acceptOverride $ \req f -> f $ responseLBS status200 [("Accept", fromMaybe "" $ lookup "Accept" $ requestHeaders req)] "" caseAcceptOverride :: Assertion@@ -520,7 +523,7 @@ case getRequestBodyType request' of Nothing -> return ([], []) Just rbt -> withFile "test/requests/dalvik-request" ReadMode $ \h ->- sourceHandle h C.$$ sinkRequestBody lbsBackEnd rbt+ sinkRequestBody lbsBackEnd rbt $ S.hGetSome h 2048 lookup "scannedTime" params @?= Just "1.298590056748E9" lookup "geoLong" params @?= Just "0" lookup "geoLat" params @?= Just "0"@@ -545,26 +548,21 @@ where params = [("foo", "bar"), ("baz", "bin")] -- FIXME change back once we include post parameter output in logging postOutput = T.pack $ "POST \nAccept: \nPOST " ++ (show params)- -- the time cannot be known, so match around it- postOutput = ("POST / :: \nStatus: 200 OK. 0", "s. /\n")- getOutput params' = ("GET /location :: \nGET " <> T.pack (show params') <> "\nStatus: 200 OK. 0", "s. /location\n")+ postOutput = T.pack $ "POST / :: \nStatus: 200 OK. /\n"+ getOutput params' = T.pack $ "GET /location :: \nGET " ++ show params' ++ "\nStatus: 200 OK. /location\n" - debugApp (beginning, ending) req = do- iactual <- liftIO $ I.newIORef mempty- middleware <- liftIO $ mkRequestLogger def+ debugApp output' req send = do+ iactual <- I.newIORef mempty+ middleware <- mkRequestLogger def { destination = Callback $ \strs -> I.modifyIORef iactual $ (`mappend` strs) , outputFormat = Detailed False }- res <- middleware (\_req -> return $ responseLBS status200 [ ] "") req- actual <- logToBs <$> liftIO (I.readIORef iactual)- liftIO $ do- actual `shouldSatisfy` S.isPrefixOf begin- actual `shouldSatisfy` S.isSuffixOf end-+ res <- middleware (\_req f -> f $ responseLBS status200 [ ] "") req send+ actual <- I.readIORef iactual+ assertEqual "debug" output $ logToBs actual return res where- begin = TE.encodeUtf8 $ T.toStrict beginning- end = TE.encodeUtf8 $ T.toStrict ending+ output = TE.encodeUtf8 $ T.toStrict output' logToBs = fromLogStr @@ -583,8 +581,8 @@ where trivialApp :: S.ByteString -> Application- trivialApp name req =- return $+ trivialApp name req f =+ f $ responseLBS status200 [ ("content-type", "text/plain")
wai-extra.cabal view
@@ -1,5 +1,5 @@ Name: wai-extra-Version: 2.1.1.3+Version: 3.0.0 Synopsis: Provides some basic WAI handlers and middleware. Description: The goal here is to provide common features without many dependencies. License: MIT@@ -22,7 +22,7 @@ Library Build-Depends: base >= 4 && < 5 , bytestring >= 0.9.1.4- , wai >= 2.1 && < 2.2+ , wai >= 3.0 && < 3.1 , old-locale >= 1.0.0.2 && < 1.1 , time >= 1.1.4 , network >= 2.2.1.5@@ -32,13 +32,9 @@ , http-types >= 0.7 , text >= 0.7 , case-insensitive >= 0.2- , data-default+ , data-default-class , fast-logger >= 2.1 && < 2.2 , wai-logger >= 2.0 && < 2.2- , conduit >= 1.0 && < 1.2- , conduit-extra >= 0.1 && < 1.2- , zlib-conduit >= 0.5 && < 1.2- , blaze-builder-conduit >= 0.5 && < 1.2 , ansi-terminal , resourcet >= 0.4.6 && < 1.2 , void >= 0.5@@ -47,6 +43,8 @@ , base64-bytestring , word8 , lifted-base >= 0.1.2+ , deepseq+ , streaming-commons if os(windows) cpp-options: -DWINDOWS@@ -68,6 +66,9 @@ Network.Wai.Middleware.HttpAuth Network.Wai.Parse Network.Wai.UrlMap+ Network.Wai.Test+ Network.Wai.EventSource+ Network.Wai.EventSource.EventStream other-modules: Network.Wai.Middleware.RequestLogger.Internal ghc-options: -Wall @@ -79,7 +80,6 @@ build-depends: base >= 4 && < 5 , wai-extra- , wai-test >= 1.3 , hspec >= 1.3 , HUnit @@ -90,14 +90,22 @@ , text , bytestring , directory- , zlib-bindings , blaze-builder >= 0.2.1.4 && < 0.4 , data-default- , conduit , fast-logger >= 2.1 , resourcet- , conduit-extra ghc-options: -Wall++test-suite spec+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules: Network.Wai.TestSpec+ build-depends: base >= 4 && < 5+ , wai-extra+ , wai+ , hspec >= 1.3+ ghc-options: -Wall -Werror source-repository head type: git