on master help sign in
Merge branch 'no-conduit'

Conflicts: wai-extra/wai-extra.cabal wai-handler-launch/wai-handler-launch.cabal wai-test/wai-test.cabal wai-websockets/wai-websockets.cabal wai/wai.cabal warp-tls/warp-tls.cabal warp/Network/Wai/Handler/Warp/Response.hs warp/warp.cabal

commitefe040c9166a2efacba28aa93fe638f7257c37e1 authorMichael Snoyman <michael@snoyman.com> dateMon 9 Jun 2014, 10:24 (12 years ago) parent46b5809c2eff63
63 files changed+1434−1069
sources.txt+1 wai-app-static/Network/Wai/Application/Static.hs+13−13 wai-app-static/wai-app-static.cabal+9−8 wai-conduit/LICENSE+20 wai-conduit/Network/Wai/Conduit.hs+67 wai-conduit/Setup.hs+2 wai-conduit/wai-conduit.cabal+24 wai-eventsource/src/Network/Wai/EventSource.hs−67 wai-eventsource/wai-eventsource.cabal+4−21 wai-extra/Network/Wai/EventSource.hs+38 wai-eventsource/src/Network/Wai/EventSource/EventStream.hs → wai-extra/Network/Wai/EventSource/EventStream.hs wai-extra/Network/Wai/Handler/CGI.hs+54−53 wai-extra/Network/Wai/Middleware/Autohead.hs+5−10 wai-extra/Network/Wai/Middleware/CleanPath.hs+3−3 wai-extra/Network/Wai/Middleware/Gzip.hs+72−30 wai-extra/Network/Wai/Middleware/HttpAuth.hs+4−4 wai-extra/Network/Wai/Middleware/Jsonp.hs+13−19 wai-extra/Network/Wai/Middleware/MethodOverridePost.hs+13−10 wai-extra/Network/Wai/Middleware/RequestLogger.hs+36−34 wai-extra/Network/Wai/Middleware/Rewrite.hs+2−2 wai-extra/Network/Wai/Middleware/Vhost.hs+3−3 wai-extra/Network/Wai/Parse.hs+164−107 wai-test/Network/Wai/Test.hs → wai-extra/Network/Wai/Test.hs+26−13 wai-extra/Network/Wai/UrlMap.hs+6−5 wai-eventsource/src/Main.hs → wai-extra/app/Main.hs wai-eventsource/static/index.html → wai-extra/app/index.html wai-test/test/Network/Wai/TestSpec.hs → wai-extra/test/Network/Wai/TestSpec.hs wai-test/test/Spec.hs → wai-extra/test/Spec.hs wai-extra/test/WaiExtraTest.hs+25−22 wai-extra/wai-extra.cabal+19−11 wai-frontend-monadcgi/Network/Wai/Frontend/MonadCGI.hs+3−5 wai-frontend-monadcgi/wai-frontend-monadcgi.cabal+2−4 wai-handler-fastcgi/wai-handler-fastcgi.cabal+3−3 wai-handler-launch/Network/Wai/Handler/Launch.hs+100−50 wai-handler-launch/test.hs+1−1 wai-handler-launch/wai-handler-launch.cabal+4−7 wai-handler-webkit/wai-handler-webkit.cabal+3−3 wai-test/README wai-test/wai-test.cabal+4−35 wai-websockets/Network/Wai/Handler/WebSockets.hs+9−27 wai-websockets/wai-websockets.cabal+2−4 wai/Network/Wai.hs+63−53 wai/Network/Wai/Internal.hs+23−10 wai/test/Network/WaiSpec.hs+73 wai/test/Spec.hs+1 wai/wai.cabal+19−5 warp-tls/Network/Wai/Handler/WarpTLS.hs+48−39 warp-tls/pong.hs+7−7 warp-tls/pong.txt+1−1 warp-tls/warp-tls.cabal+3−8 warp/Network/Wai/Handler/Warp.hs+1−14 warp/Network/Wai/Handler/Warp/Conduit.hs+98−84 warp/Network/Wai/Handler/Warp/Request.hs+65−50 warp/Network/Wai/Handler/Warp/Response.hs+46−68 warp/Network/Wai/Handler/Warp/Run.hs+50−58 warp/Network/Wai/Handler/Warp/Settings.hs+1−5 warp/Network/Wai/Handler/Warp/Types.hs+30 warp/test/ConduitSpec.hs+26−12 warp/test/ExceptionSpec.hs+8−8 warp/test/RequestSpec.hs+45−16 warp/test/ResponseSpec.hs+2−2 warp/test/RunSpec.hs+68−43 warp/warp.cabal+2−12
sources.txt+1
@@ -9,4 +9,5 @@
99./wai-frontend-monadcgi
1010./wai-websockets
1111./wai-eventsource
12+./wai-conduit
1213./mime-types
wai-app-static/Network/Wai/Application/Static.hs+13−13
@@ -193,35 +193,35 @@ staticApp :: StaticSettings -> W.Application
193193staticApp set req = staticAppPieces set (W.pathInfo req) req
194194
195195staticAppPieces :: StaticSettings -> [Text] -> W.Application
196−staticAppPieces _ _ req
197− | W.requestMethod req /= "GET" = return $ W.responseLBS
196+staticAppPieces _ _ req sendResponse
197+ | W.requestMethod req /= "GET" = sendResponse $ W.responseLBS
198198 H.status405
199199 [("Content-Type", "text/plain")]
200200 "Only GET is supported"
201−staticAppPieces _ [".hidden", "folder.png"] _ = return $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/folder.png")]
202−staticAppPieces _ [".hidden", "haskell.png"] _ = return $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/haskell.png")]
203−staticAppPieces ss rawPieces req = liftIO $ do
201+staticAppPieces _ [".hidden", "folder.png"] _ sendResponse = sendResponse $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/folder.png")]
202+staticAppPieces _ [".hidden", "haskell.png"] _ sendResponse = sendResponse $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/haskell.png")]
203+staticAppPieces ss rawPieces req sendResponse = liftIO $ do
204204 case toPieces rawPieces of
205205 Just pieces -> checkPieces ss pieces req >>= response
206− Nothing -> return $ W.responseLBS H.status403
206+ Nothing -> sendResponse $ W.responseLBS H.status403
207207 [ ("Content-Type", "text/plain")
208208 ] "Forbidden"
209209 where
210− response :: StaticResponse -> IO W.Response
210+ response :: StaticResponse -> IO W.ResponseReceived
211211 response (FileResponse file ch) = do
212212 mimetype <- ssGetMimeType ss file
213213 let filesize = fileGetSize file
214214 let headers = ("Content-Type", mimetype)
215215 : ("Content-Length", S8.pack $ show filesize)
216216 : ch
217− return $ fileToResponse file H.status200 headers
217+ sendResponse $ fileToResponse file H.status200 headers
218218
219219 response NotModified =
220− return $ W.responseLBS H.status304 [] ""
220+ sendResponse $ W.responseLBS H.status304 [] ""
221221
222222 response (SendContent mt lbs) = do
223223 -- TODO: set caching headers
224− return $ W.responseLBS H.status200
224+ sendResponse $ W.responseLBS H.status200
225225 [ ("Content-Type", mt)
226226 -- TODO: set Content-Length
227227 ] lbs
@@ -232,13 +232,13 @@ staticAppPieces ss rawPieces req = liftIO $ do
232232 Just hash -> replace "etag" (Just hash) (W.queryString req)
233233 Nothing -> remove "etag" (W.queryString req)
234234
235− return $ W.responseLBS H.status301
235+ sendResponse $ W.responseLBS H.status301
236236 [ ("Content-Type", "text/plain")
237237 , ("Location", S8.append loc $ H.renderQuery True qString)
238238 ] "Redirect"
239239
240− response NotFound = return $ W.responseLBS H.status404
240+ response NotFound = sendResponse $ W.responseLBS H.status404
241241 [ ("Content-Type", "text/plain")
242242 ] "File not found"
243243
244− response (WaiResponse r) = return r
244+ response (WaiResponse r) = sendResponse r
wai-app-static/wai-app-static.cabal+9−8
@@ -1,5 +1,5 @@
11name: wai-app-static
2−version: 2.0.1
2+version: 3.0.0
33license: MIT
44license-file: LICENSE
55author: Michael Snoyman <michael@snoyman.com>
@@ -24,7 +24,7 @@ Flag print
2424
2525library
2626 build-depends: base >= 4 && < 5
27− , wai >= 2.0 && < 2.2
27+ , wai >= 3.0 && < 3.1
2828 , bytestring >= 0.9.1.4
2929 , http-types >= 0.7
3030 , transformers >= 0.2.2
@@ -39,7 +39,6 @@ library
3939 , base64-bytestring >= 0.1
4040 , byteable
4141 , cryptohash >= 0.11
42− , cryptohash-conduit >= 0.1.0
4342 , system-filepath >= 0.4
4443 , system-fileio >= 0.3
4544 , http-date
@@ -50,9 +49,11 @@ library
5049 , template-haskell >= 2.7
5150 , zlib >= 0.5
5251 , filepath
53− , wai-extra >= 2.0 && < 2.2
52+ , wai-extra >= 3.0 && < 3.1
5453 , optparse-applicative >= 0.7
55− , warp >= 2.0 && < 2.2
54+ , warp >= 3.0 && < 3.1
55+ -- Used exclusively for hashFile function
56+ , cryptohash-conduit
5657
5758 exposed-modules: Network.Wai.Application.Static
5859 WaiAppStatic.Storage.Filesystem
@@ -73,7 +74,7 @@ Executable warp
7374 Main-is: warp-static.hs
7475 hs-source-dirs: app
7576 Build-depends: base >= 4 && < 5
76− , wai-app-static >= 2.0 && < 2.1
77+ , wai-app-static
7778 , directory >= 1.0
7879 , containers >= 0.2
7980 , bytestring >= 0.9.1.4
@@ -91,8 +92,8 @@ test-suite runtests
9192 , time
9293 , old-locale
9394 , http-date
94− , wai-app-static >= 1.3
95− , wai-test
95+ , wai-app-static
96+ , wai-extra
9697 , wai
9798 , http-types
9899 , network
wai-conduit/LICENSEnew file+20
@@ -0,0 +1,20 @@
1+Copyright (c) 2014 Michael Snoyman
2+
3+Permission is hereby granted, free of charge, to any person obtaining
4+a copy of this software and associated documentation files (the
5+"Software"), to deal in the Software without restriction, including
6+without limitation the rights to use, copy, modify, merge, publish,
7+distribute, sublicense, and/or sell copies of the Software, and to
8+permit persons to whom the Software is furnished to do so, subject to
9+the following conditions:
10+
11+The above copyright notice and this permission notice shall be included
12+in all copies or substantial portions of the Software.
13+
14+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
wai-conduit/Network/Wai/Conduit.hsnew file+67
@@ -0,0 +1,67 @@
1+-- | A light-weight wrapper around @Network.Wai@ to provide easy conduit support.
2+module Network.Wai.Conduit
3+ ( -- * Request body
4+ sourceRequestBody
5+ -- * Response body
6+ , responseSource
7+ , responseRawSource
8+ -- * Re-export
9+ , module Network.Wai
10+ ) where
11+
12+import Network.Wai
13+import Data.Conduit
14+import Control.Monad.IO.Class (MonadIO, liftIO)
15+import Data.ByteString (ByteString)
16+import qualified Data.ByteString as S
17+import Control.Monad (unless)
18+import Network.HTTP.Types
19+import Blaze.ByteString.Builder (Builder)
20+import Data.IORef
21+import qualified Data.Conduit.List as CL
22+
23+-- | Stream the request body.
24+--
25+-- Since 3.0.0
26+sourceRequestBody :: MonadIO m => Request -> Source m ByteString
27+sourceRequestBody req =
28+ loop
29+ where
30+ go = liftIO (requestBody req)
31+
32+ loop = do
33+ bs <- go
34+ unless (S.null bs) $ do
35+ yield bs
36+ loop
37+
38+-- | Create an HTTP response out of a @Source@.
39+--
40+-- Since 3.0.0
41+responseSource :: Status -> ResponseHeaders -> Source IO (Flush Builder) -> Response
42+responseSource s hs src = responseStream s hs $ \send flush ->
43+ src $$ CL.mapM_ (\mbuilder ->
44+ case mbuilder of
45+ Chunk b -> send b
46+ Flush -> flush)
47+
48+-- | Create a raw response using a @Source@ and @Sink@ to represent the input
49+-- and output, respectively.
50+--
51+-- Since 3.0.0
52+responseRawSource :: (MonadIO m, MonadIO n)
53+ => (Source m ByteString -> Sink ByteString n () -> IO ())
54+ -> Response
55+ -> Response
56+responseRawSource app backup =
57+ responseRaw app' backup
58+ where
59+ app' recv send =
60+ app src sink
61+ where
62+ src = do
63+ bs <- liftIO recv
64+ unless (S.null bs) $ do
65+ yield bs
66+ src
67+ sink = CL.mapM_ $ liftIO . send
wai-conduit/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
wai-conduit/wai-conduit.cabalnew file+24
@@ -0,0 +1,24 @@
1+name: wai-conduit
2+version: 3.0.0
3+synopsis: conduit wrappers for WAI
4+description: Since version 3.0.0, WAI has no built-in streaming data abstraction.
5+ This library provides similar functionality to what existed in WAI 2.x.
6+homepage: https://github.com/yesodweb/wai
7+license: MIT
8+license-file: LICENSE
9+author: Michael Snoyman
10+maintainer: michael@snoyman.com
11+category: Web, Conduit
12+build-type: Simple
13+cabal-version: >=1.10
14+
15+library
16+ exposed-modules: Network.Wai.Conduit
17+ build-depends: base >= 4 && < 5
18+ , wai >= 3.0 && < 3.1
19+ , conduit
20+ , transformers
21+ , bytestring
22+ , http-types
23+ , blaze-builder
24+ default-language: Haskell2010
wai-eventsource/src/Network/Wai/EventSource.hsdeleted−67
@@ -1,67 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−{-|
3− A WAI adapter to the HTML5 Server-Sent Events API.
4−-}
5−module Network.Wai.EventSource (
6− ServerEvent(..),
7− eventSourceAppChan,
8− eventSourceAppSource,
9− eventSourceAppIO,
10− sourceToSource
11− ) where
12−
13−import Blaze.ByteString.Builder (Builder)
14−import Control.Concurrent.Chan (Chan, dupChan, readChan)
15−import Control.Monad.IO.Class (liftIO)
16−import Data.Conduit
17−import qualified Data.Conduit.List as CL
18−import Network.HTTP.Types (status200)
19−import Network.Wai (Application, Response, responseSource)
20−
21−import Network.Wai.EventSource.EventStream
22−
23−-- | Make a new WAI EventSource application reading events from
24−-- the given channel.
25−eventSourceAppChan :: Chan ServerEvent -> Application
26−eventSourceAppChan chan _ = do
27− chan' <- liftIO $ dupChan chan
28− return $ response chanToSource chan'
29−
30−-- | Make a new WAI EventSource application reading events from
31−-- the given source.
32−eventSourceAppSource :: Source IO ServerEvent -> Application
33−eventSourceAppSource src _ = return $ response sourceToSource src
34−
35−-- | Make a new WAI EventSource application reading events from
36−-- the given IO action.
37−eventSourceAppIO :: IO ServerEvent -> Application
38−eventSourceAppIO act _ = return $ response ioToSource act
39−
40−response :: (a -> Source IO (Flush Builder)) -> a -> Response
41−response f a = responseSource status200 [("Content-Type", "text/event-stream")] $ f a
42−
43−chanToSource :: Chan ServerEvent -> Source IO (Flush Builder)
44−chanToSource = ioToSource . readChan
45−
46−ioToSource :: IO ServerEvent -> Source IO (Flush Builder)
47−ioToSource act =
48− loop
49− where
50− loop = do
51− x <- liftIO act
52− case eventToBuilder x of
53− Nothing -> return ()
54− Just y -> do
55− yield $ Chunk y
56− yield Flush
57− loop
58−
59−-- | Convert a ServerEvent source into a Builder source of serialized
60−-- events.
61−sourceToSource :: Monad m => Source m ServerEvent -> Source m (Flush Builder)
62−sourceToSource src = src $= CL.concatMap eventToFlushBuilder
63− where
64− eventToFlushBuilder event =
65− case eventToBuilder event of
66− Nothing -> []
67− Just x -> [Chunk x, Flush]
wai-eventsource/wai-eventsource.cabal+4−21
@@ -1,7 +1,7 @@
11Name: wai-eventsource
2−Version: 2.0.0.2
3−Synopsis: WAI support for server-sent events
4−Description: WAI support for server-sent events
2+Version: 3.0.0
3+Synopsis: WAI support for server-sent events (deprecated)
4+Description: Since WAI 3.0, this code has been merged into wai-extra.
55License: MIT
66Author: Chris Smith, Mathias Biilmann Christensen
77Maintainer: greg@gregweber.info
@@ -13,21 +13,4 @@ homepage: http://www.yesodweb.com/book/web-application-interface
1313license-file: LICENSE
1414
1515Library
16− Build-depends:
17− base >= 4.3 && < 5
18− , bytestring >= 0.9.1.4
19− , blaze-builder >= 0.3 && < 0.4
20− , conduit >= 0.5 && < 1.2
21− , http-types >= 0.7
22− , wai >= 2.0 && < 2.2
23− , transformers
24−
25− ghc-options: -Wall
26− hs-source-dirs: src
27− exposed-modules:
28− Network.Wai.EventSource
29− Network.Wai.EventSource.EventStream
30−
31−source-repository head
32− type: git
33− location: git://github.com/yesodweb/wai.git
16+ build-depends: wai >= 3.0
wai-extra/Network/Wai/EventSource.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-|
3+ A WAI adapter to the HTML5 Server-Sent Events API.
4+-}
5+module Network.Wai.EventSource (
6+ ServerEvent(..),
7+ eventSourceAppChan,
8+ eventSourceAppIO
9+ ) where
10+
11+import Blaze.ByteString.Builder (Builder)
12+import Data.Function (fix)
13+import Control.Concurrent.Chan (Chan, dupChan, readChan)
14+import Control.Monad.IO.Class (liftIO)
15+import Network.HTTP.Types (status200)
16+import Network.Wai (Application, Response, responseStream)
17+
18+import Network.Wai.EventSource.EventStream
19+
20+-- | Make a new WAI EventSource application reading events from
21+-- the given channel.
22+eventSourceAppChan :: Chan ServerEvent -> Application
23+eventSourceAppChan chan req sendResponse = do
24+ chan' <- liftIO $ dupChan chan
25+ eventSourceAppIO (readChan chan') req sendResponse
26+
27+-- | Make a new WAI EventSource application reading events from
28+-- the given IO action.
29+eventSourceAppIO :: IO ServerEvent -> Application
30+eventSourceAppIO src _ sendResponse =
31+ sendResponse $ responseStream
32+ status200
33+ [("Content-Type", "text/event-stream")]
34+ $ \sendChunk flush -> fix $ \loop -> do
35+ se <- src
36+ case eventToBuilder se of
37+ Nothing -> return ()
38+ Just b -> sendChunk b >> flush >> loop
wai-eventsource/src/Network/Wai/EventSource/EventStream.hs → wai-extra/Network/Wai/EventSource/EventStream.hsrenamed

The content is the same.

wai-extra/Network/Wai/Handler/CGI.hs+54−53
@@ -13,8 +13,8 @@ module Network.Wai.Handler.CGI
1313import Network.Wai
1414import Network.Wai.Internal
1515import Network.Socket (getAddrInfo, addrAddress)
16+import Data.IORef
1617import Data.Maybe (fromMaybe)
17−import Control.Exception (mask)
1818import qualified Data.ByteString.Char8 as B
1919import qualified Data.ByteString.Lazy as L
2020import Control.Arrow ((***))
@@ -22,17 +22,17 @@ import Data.Char (toLower)
2222import qualified System.IO
2323import qualified Data.String as String
2424import Data.Monoid (mconcat, mempty)
25−import Blaze.ByteString.Builder (fromByteString, toLazyByteString)
25+import Blaze.ByteString.Builder (fromByteString, toLazyByteString, flush)
2626import Blaze.ByteString.Builder.Char8 (fromChar, fromString)
27−import Data.Conduit.Blaze (builderToByteStringFlush)
28−import Control.Monad.IO.Class (liftIO)
2927import Data.ByteString.Lazy.Internal (defaultChunkSize)
3028import System.IO (Handle)
3129import Network.HTTP.Types (Status (..))
3230import qualified Network.HTTP.Types as H
3331import qualified Data.CaseInsensitive as CI
3432import Data.Monoid (mappend)
35−import Data.Conduit
33+import qualified Data.Streaming.Blaze as Blaze
34+import Data.Function (fix)
35+import Control.Monad (unless, void)
3636
3737#if WINDOWS
3838import System.Environment (getEnvironment)
@@ -76,7 +76,7 @@ runSendfile sf app = do
7676-- stick with 'run' or 'runSendfile'.
7777runGeneric
7878 :: [(String, String)] -- ^ all variables
79− -> (Int -> Source IO B.ByteString) -- ^ responseBody of input
79+ -> (Int -> IO (IO B.ByteString)) -- ^ responseBody of input
8080 -> (B.ByteString -> IO ()) -- ^ destination for output
8181 -> Maybe B.ByteString -- ^ does the server support the X-Sendfile header?
8282 -> Application
@@ -98,40 +98,48 @@ runGeneric vars inputH outputH xsendfile app = do
9898 "https" -> True
9999 _ -> False
100100 addrs <- getAddrInfo Nothing (Just remoteHost') Nothing
101+ requestBody' <- inputH contentLength
101102 let addr =
102103 case addrs of
103104 a:_ -> addrAddress a
104105 [] -> error $ "Invalid REMOTE_ADDR or REMOTE_HOST: " ++ remoteHost'
105− mask $ \restore -> do
106− let reqHeaders = map (cleanupVarName *** B.pack) vars
107− env = Request
108− { requestMethod = rmethod
109− , rawPathInfo = B.pack pinfo
110− , pathInfo = H.decodePathSegments $ B.pack pinfo
111− , rawQueryString = B.pack qstring
112− , queryString = H.parseQuery $ B.pack qstring
113− , requestHeaders = reqHeaders
114− , isSecure = isSecure'
115− , remoteHost = addr
116− , httpVersion = H.http11 -- FIXME
117− , requestBody = inputH contentLength
118− , vault = mempty
119− , requestBodyLength = KnownLength $ fromIntegral contentLength
120− , requestHeaderHost = lookup "host" reqHeaders
121− , requestHeaderRange = lookup "range" reqHeaders
122− }
123− -- FIXME worry about exception?
124− res <- restore $ app env
106+ reqHeaders = map (cleanupVarName *** B.pack) vars
107+ env = Request
108+ { requestMethod = rmethod
109+ , rawPathInfo = B.pack pinfo
110+ , pathInfo = H.decodePathSegments $ B.pack pinfo
111+ , rawQueryString = B.pack qstring
112+ , queryString = H.parseQuery $ B.pack qstring
113+ , requestHeaders = reqHeaders
114+ , isSecure = isSecure'
115+ , remoteHost = addr
116+ , httpVersion = H.http11 -- FIXME
117+ , requestBody = requestBody'
118+ , vault = mempty
119+ , requestBodyLength = KnownLength $ fromIntegral contentLength
120+ , requestHeaderHost = lookup "host" reqHeaders
121+ , requestHeaderRange = lookup "range" reqHeaders
122+ }
123+ void $ app env $ \res ->
125124 case (xsendfile, res) of
126− (Just sf, ResponseFile s hs fp Nothing) ->
127− restore $ mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp
125+ (Just sf, ResponseFile s hs fp Nothing) -> do
126+ mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp
127+ return ResponseReceived
128128 _ -> do
129− let (s, hs, wb) = responseToSource res
130− wb $ \b ->
131− let src = do
132− yield (Chunk $ headers s hs `mappend` fromChar '\n')
133− b
134− in src $$ builderSink
129+ let (s, hs, wb) = responseToStream res
130+ (blazeRecv, blazeFinish) <- Blaze.newBlazeRecv Blaze.defaultStrategy
131+ wb $ \b -> do
132+ let sendBuilder builder = do
133+ popper <- blazeRecv builder
134+ fix $ \loop -> do
135+ bs <- popper
136+ unless (B.null bs) $ do
137+ outputH bs
138+ loop
139+ sendBuilder $ headers s hs `mappend` fromChar '\n'
140+ b sendBuilder (sendBuilder flush)
141+ blazeFinish >>= maybe (return ()) outputH
142+ return ResponseReceived
135143 where
136144 headers s hs = mconcat (map header $ status s : map header' (fixHeaders hs))
137145 status (Status i m) = (fromByteString "Status", mconcat
@@ -153,13 +161,6 @@ runGeneric vars inputH outputH xsendfile app = do
153161 , fromByteString sf
154162 , fromByteString " not supported"
155163 ]
156− bsSink = await >>= maybe (return ()) push
157− push (Chunk bs) = do
158− liftIO $ outputH bs
159− bsSink
160− -- FIXME actually flush?
161− push Flush = bsSink
162− builderSink = builderToByteStringFlush =$ bsSink
163164 fixHeaders h =
164165 case lookup "content-type" h of
165166 Nothing -> ("Content-Type", "text/html; charset=utf-8") : h
@@ -178,19 +179,19 @@ cleanupVarName s =
178179 helper' (x:rest) = toLower x : helper' rest
179180 helper' [] = []
180181
181−requestBodyHandle :: Handle -> Int -> Source IO B.ByteString
182+requestBodyHandle :: Handle -> Int -> IO (IO B.ByteString)
182183requestBodyHandle h = requestBodyFunc $ \i -> do
183184 bs <- B.hGet h i
184185 return $ if B.null bs then Nothing else Just bs
185186
186−requestBodyFunc :: (Int -> IO (Maybe B.ByteString)) -> Int -> Source IO B.ByteString
187−requestBodyFunc get =
188− loop
189− where
190− loop 0 = return ()
191− loop count = do
192− mbs <- liftIO $ get $ min count defaultChunkSize
193− let count' = count - maybe 0 B.length mbs
194− case mbs of
195− Nothing -> return ()
196− Just bs -> yield bs >> loop count'
187+requestBodyFunc :: (Int -> IO (Maybe B.ByteString)) -> Int -> IO (IO B.ByteString)
188+requestBodyFunc get count0 = do
189+ ref <- newIORef count0
190+ return $ do
191+ count <- readIORef ref
192+ if count <= 0
193+ then return B.empty
194+ else do
195+ mbs <- get $ min count defaultChunkSize
196+ writeIORef ref $ count - maybe 0 B.length mbs
197+ return $ fromMaybe B.empty mbs
wai-extra/Network/Wai/Middleware/Autohead.hs+5−10
@@ -4,17 +4,12 @@
44module Network.Wai.Middleware.Autohead (autohead) where
55
66import Network.Wai
7−import Network.Wai.Internal
87import Data.Monoid (mempty)
98
109autohead :: Middleware
11−autohead app req
12− | requestMethod req == "HEAD" = do
13− res <- app req { requestMethod = "GET" }
14− let go (ResponseFile s hs _ _) = ResponseBuilder s hs mempty
15− go (ResponseBuilder s hs _) = ResponseBuilder s hs mempty
16− go (ResponseSource s hs _) = ResponseBuilder s hs mempty
17− go (ResponseRaw raw r) = ResponseRaw raw (go r)
18− return (go res)
19− | otherwise = app req
10+autohead app req sendResponse
11+ | requestMethod req == "HEAD" = app req { requestMethod = "GET" } $ \res -> do
12+ let (s, hs, _) = responseToStream res
13+ sendResponse $ responseBuilder s hs mempty
14+ | otherwise = app req sendResponse
2015
wai-extra/Network/Wai/Middleware/CleanPath.hs+3−3
@@ -14,10 +14,10 @@ cleanPath :: ([Text] -> Either B.ByteString [Text])
1414 -> B.ByteString
1515 -> ([Text] -> Application)
1616 -> Application
17−cleanPath splitter prefix app env =
17+cleanPath splitter prefix app env sendResponse =
1818 case splitter $ pathInfo env of
19− Right pieces -> app pieces env
20− Left p -> return
19+ Right pieces -> app pieces env sendResponse
20+ Left p -> sendResponse
2121 $ responseLBS status301
2222 [("Location", mconcat [prefix, p, suffix])]
2323 $ L.empty
wai-extra/Network/Wai/Middleware/Gzip.hs+72−30
@@ -28,20 +28,23 @@ import Network.Wai
2828import Data.Maybe (fromMaybe, isJust)
2929import qualified Data.ByteString.Char8 as S8
3030import qualified Data.ByteString as S
31−import Data.Default
31+import Data.Default.Class
3232import Network.HTTP.Types (Status, Header)
3333import Control.Monad.IO.Class (liftIO)
3434import Control.Monad.Trans.Resource (runResourceT)
3535import System.Directory (doesFileExist, createDirectoryIfMissing)
36−import qualified Data.Conduit as C
37−import qualified Data.Conduit.Zlib as CZ
38−import qualified Data.Conduit.Binary as CB
39−import qualified Data.Conduit.List as CL
40−import Data.Conduit.Blaze (builderToByteStringFlush)
4136import Blaze.ByteString.Builder (fromByteString)
4237import Control.Exception (try, SomeException)
4338import qualified Data.Set as Set
4439import Network.Wai.Internal
40+import qualified Data.Streaming.Blaze as B
41+import qualified Data.Streaming.Zlib as Z
42+import qualified Blaze.ByteString.Builder as Blaze
43+import Control.Monad (unless)
44+import Data.Function (fix)
45+import Control.Exception (throwIO)
46+import qualified System.IO as IO
47+import Data.ByteString.Lazy.Internal (defaultChunkSize)
4548
4649data GzipSettings = GzipSettings
4750 { gzipFiles :: GzipFiles
@@ -74,20 +77,19 @@ defaultCheckMime bs =
7477--
7578-- * Only compress if the response is above a certain size.
7679gzip :: GzipSettings -> Middleware
77−gzip set app env = do
78− res <- app env
80+gzip set app env sendResponse = app env $ \res ->
7981 case res of
80− ResponseFile{} | gzipFiles set == GzipIgnore -> return res
82+ ResponseFile{} | gzipFiles set == GzipIgnore -> sendResponse res
8183 _ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res)
8284 then
8385 case (res, gzipFiles set) of
8486 (ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
8587 case lookup "content-type" hs of
8688 Just m
87− | gzipCheckMime set m -> liftIO $ compressFile s hs file cache
88− _ -> return res
89− _ -> return $ compressE set res
90− else return res
89+ | gzipCheckMime set m -> compressFile s hs file cache sendResponse
90+ _ -> sendResponse res
91+ _ -> compressE set res sendResponse
92+ else sendResponse res
9193 where
9294 enc = fromMaybe [] $ (splitCommas . S8.unpack)
9395 `fmap` lookup "Accept-Encoding" (requestHeaders env)
@@ -95,22 +97,38 @@ gzip set app env = do
9597 isMSIE6 = "MSIE 6" `S.isInfixOf` ua
9698 isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res
9799
98−compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response
99−compressFile s hs file cache = do
100+compressFile :: Status -> [Header] -> FilePath -> FilePath -> (Response -> IO a) -> IO a
101+compressFile s hs file cache sendResponse = do
100102 e <- doesFileExist tmpfile
101103 if e
102104 then onSucc
103105 else do
104106 createDirectoryIfMissing True cache
105− x <-
106− try $ runResourceT $ CB.sourceFile file
107− C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile
108− either onErr (const onSucc) x
107+ x <- try $
108+ IO.withBinaryFile file IO.ReadMode $ \inH ->
109+ IO.withBinaryFile tmpfile IO.WriteMode $ \outH -> do
110+ deflate <- Z.initDeflate 7 $ Z.WindowBits 31
111+ -- FIXME this code should write to a temporary file, then
112+ -- rename to the final file
113+ let goPopper popper = fix $ \loop -> do
114+ res <- popper
115+ case res of
116+ Z.PRDone -> return ()
117+ Z.PRNext bs -> do
118+ S.hPut outH bs
119+ loop
120+ Z.PRError e -> throwIO e
121+ fix $ \loop -> do
122+ bs <- S.hGetSome inH defaultChunkSize
123+ unless (S.null bs) $ do
124+ Z.feedDeflate deflate bs >>= goPopper
125+ loop
126+ goPopper $ Z.finishDeflate deflate
127+ either onErr (const onSucc) (x :: Either SomeException ()) -- FIXME bad! don't catch all exceptions like that!
109128 where
110− onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing
129+ onSucc = sendResponse $ responseFile s (fixHeaders hs) tmpfile Nothing
111130
112− onErr :: SomeException -> IO Response
113− onErr = const $ return $ ResponseFile s hs file Nothing -- FIXME log the error message
131+ onErr _ = sendResponse $ responseFile s hs file Nothing -- FIXME log the error message
114132
115133 tmpfile = cache ++ '/' : map safe file
116134 safe c
@@ -123,18 +141,42 @@ compressFile s hs file cache = do
123141
124142compressE :: GzipSettings
125143 -> Response
126− -> Response
127−compressE set res =
144+ -> (Response -> IO a)
145+ -> IO a
146+compressE set res sendResponse =
128147 case lookup "content-type" hs of
129148 Just m | gzipCheckMime set m ->
130149 let hs' = fixHeaders hs
131− in ResponseSource s hs' $ \f -> wb $ \b -> f $
132− b C.$= builderToByteStringFlush
133− C.$= CZ.compressFlush 1 (CZ.WindowBits 31)
134− C.$= CL.map (fmap fromByteString)
135− _ -> res
150+ in wb $ \body -> sendResponse $ responseStream s hs' $ \sendChunk flush -> do
151+ (blazeRecv, blazeFinish) <- B.newBlazeRecv B.defaultStrategy
152+ deflate <- Z.initDeflate 1 (Z.WindowBits 31)
153+ let sendBuilder builder = do
154+ popper <- blazeRecv builder
155+ fix $ \loop -> do
156+ bs <- popper
157+ unless (S.null bs) $ do
158+ sendBS bs
159+ loop
160+ sendBS bs = Z.feedDeflate deflate bs >>= deflatePopper
161+ flushBuilder = do
162+ sendBuilder Blaze.flush
163+ deflatePopper $ Z.flushDeflate deflate
164+ flush
165+ deflatePopper popper = fix $ \loop -> do
166+ res <- popper
167+ case res of
168+ Z.PRDone -> return ()
169+ Z.PRNext bs' -> do
170+ sendChunk $ fromByteString bs'
171+ loop
172+ Z.PRError e -> throwIO e
173+
174+ body sendBuilder flushBuilder
175+ sendBuilder Blaze.flush
176+ deflatePopper $ Z.finishDeflate deflate
177+ _ -> sendResponse res
136178 where
137− (s, hs, wb) = responseToSource res
179+ (s, hs, wb) = responseToStream res
138180
139181-- Remove Content-Length header, since we will certainly have a
140182-- different length after gzip compression.
wai-extra/Network/Wai/Middleware/HttpAuth.hs+4−4
@@ -32,12 +32,12 @@ type CheckCreds = ByteString
3232basicAuth :: CheckCreds
3333 -> AuthSettings
3434 -> Middleware
35−basicAuth checkCreds AuthSettings {..} app req = do
35+basicAuth checkCreds AuthSettings {..} app req sendResponse = do
3636 isProtected <- authIsProtected req
3737 allowed <- if isProtected then check else return True
3838 if allowed
39− then app req
40− else authOnNoAuth authRealm req
39+ then app req sendResponse
40+ else authOnNoAuth authRealm req sendResponse
4141 where
4242 check =
4343 case lookup "Authorization" $ requestHeaders req of
@@ -83,7 +83,7 @@ data AuthSettings = AuthSettings
8383instance IsString AuthSettings where
8484 fromString s = AuthSettings
8585 { authRealm = fromString s
86− , authOnNoAuth = \realm _req -> return $ responseLBS
86+ , authOnNoAuth = \realm _req f -> f $ responseLBS
8787 status401
8888 [ ("Content-Type", "text/plain")
8989 , ("WWW-Authenticate", S.concat
wai-extra/Network/Wai/Middleware/Jsonp.hs+13−19
@@ -25,7 +25,6 @@ import Data.Monoid (mappend)
2525import Control.Monad (join)
2626import Data.Maybe (fromMaybe)
2727import qualified Data.ByteString as S
28−import qualified Data.Conduit as C
2928import Data.CaseInsensitive (CI)
3029import Network.HTTP.Types (Status)
3130
@@ -37,7 +36,7 @@ import Network.HTTP.Types (Status)
3736-- having a content type of \"text\/javascript\" and calling the specified
3837-- callback function.
3938jsonp :: Middleware
40−jsonp app env = do
39+jsonp app env sendResponse = do
4140 let accept = fromMaybe B8.empty $ lookup "Accept" $ requestHeaders env
4241 let callback :: Maybe B8.ByteString
4342 callback =
@@ -52,15 +51,15 @@ jsonp app env = do
5251 "application/json"
5352 $ requestHeaders env
5453 }
55− res <- app env'
56− return $ case callback of
57− Nothing -> res
58− Just c -> go c res
54+ app env' $ \res ->
55+ case callback of
56+ Nothing -> sendResponse res
57+ Just c -> go c res
5958 where
6059 go c r@(ResponseBuilder s hs b) =
61− case checkJSON hs of
60+ sendResponse $ case checkJSON hs of
6261 Nothing -> r
63− Just hs' -> ResponseBuilder s hs' $
62+ Just hs' -> responseBuilder s hs' $
6463 copyByteString c
6564 `mappend` fromChar '('
6665 `mappend` b
@@ -68,9 +67,9 @@ jsonp app env = do
6867 go c r =
6968 case checkJSON hs of
7069 Just hs' -> addCallback c s hs' wb
71− Nothing -> r
70+ Nothing -> sendResponse r
7271 where
73− (s, hs, wb) = responseToSource r
72+ (s, hs, wb) = responseToStream r
7473
7574 checkJSON hs =
7675 case lookup "Content-Type" hs of
@@ -80,16 +79,11 @@ jsonp app env = do
8079 _ -> Nothing
8180 fixHeaders = changeVal "Content-Type" "text/javascript"
8281
83− addCallback :: ByteString
84− -> Status
85− -> [(CI ByteString, ByteString)]
86− -> (forall b. WithSource IO (C.Flush Builder) b)
87− -> Response
8882 addCallback cb s hs wb =
89− ResponseSource s hs $ \f -> wb $ \b -> f $
90− C.yield (C.Chunk $ copyByteString cb `mappend` fromChar '(')
91− `mappend` b
92− `mappend` C.yield (C.Chunk $ fromChar ')')
83+ wb $ \body -> sendResponse $ responseStream s hs $ \sendChunk flush -> do
84+ sendChunk $ copyByteString cb `mappend` fromChar '('
85+ body sendChunk flush
86+ sendChunk $ fromChar ')'
9387
9488changeVal :: Eq a
9589 => a
wai-extra/Network/Wai/Middleware/MethodOverridePost.hs+13−10
@@ -10,9 +10,9 @@ module Network.Wai.Middleware.MethodOverridePost
1010
1111import Network.Wai
1212import Network.HTTP.Types (parseQuery)
13−import Data.Monoid (mconcat)
14−import Data.Conduit.Lazy (lazyConsume)
15−import Data.Conduit.List (sourceList)
13+import Data.Monoid (mconcat, mempty)
14+import Data.IORef
15+import Data.ByteString.Lazy (toChunks)
1616
1717-- | Allows overriding of the HTTP request method via the _method post string parameter.
1818--
@@ -26,13 +26,16 @@ import Data.Conduit.List (sourceList)
2626-- * This middleware only applies when the initial request method is POST.
2727--
2828methodOverridePost :: Middleware
29−methodOverridePost app req = case (requestMethod req, lookup "Content-Type" (requestHeaders req)) of
30− ("POST", Just "application/x-www-form-urlencoded") -> setPost req >>= app
31− _ -> app req
29+methodOverridePost app req send =
30+ case (requestMethod req, lookup "Content-Type" (requestHeaders req)) of
31+ ("POST", Just "application/x-www-form-urlencoded") -> setPost req >>= flip app send
32+ _ -> app req send
3233
3334setPost :: Request -> IO Request
3435setPost req = do
35− body <- lazyConsume (requestBody req)
36− case parseQuery (mconcat body) of
37− (("_method", Just newmethod):_) -> return $ req {requestBody = sourceList body, requestMethod = newmethod}
38− _ -> return $ req {requestBody = sourceList body}
36+ body <- (mconcat . toChunks) `fmap` lazyRequestBody req
37+ ref <- newIORef body
38+ let rb = atomicModifyIORef ref $ \bs -> (mempty, bs)
39+ case parseQuery body of
40+ (("_method", Just newmethod):_) -> return $ req {requestBody = rb, requestMethod = newmethod}
41+ _ -> return $ req {requestBody = rb}
wai-extra/Network/Wai/Middleware/RequestLogger.hs+36−34
@@ -34,14 +34,11 @@ import Network.Wai.Parse (sinkRequestBody, lbsBackEnd, fileName, Param, File, ge
3434import qualified Data.ByteString.Lazy as LBS
3535import qualified Data.ByteString.Char8 as S8
3636
37−import qualified Data.Conduit as C
38−import qualified Data.Conduit.List as CL
39−
4037import System.Console.ANSI
4138import Data.IORef.Lifted
4239import System.IO.Unsafe
4340
44−import Data.Default (Default (def))
41+import Data.Default.Class (Default (def))
4542import Network.Wai.Logger
4643import Network.Wai.Middleware.RequestLogger.Internal
4744
@@ -96,23 +93,21 @@ mkRequestLogger RequestLoggerSettings{..} = do
9693 return $ customMiddleware callback getdate formatter
9794
9895apacheMiddleware :: ApacheLoggerActions -> Middleware
99−apacheMiddleware ala app req = do
100− res <- app req
96+apacheMiddleware ala app req sendResponse = app req $ \res -> do
10197 let msize = lookup "content-length" (responseHeaders res) >>= readInt'
10298 readInt' bs =
10399 case S8.readInteger bs of
104100 Just (i, "") -> Just i
105101 _ -> Nothing
106102 apacheLogger ala req (responseStatus res) msize
107− return res
103+ sendResponse res
108104
109105customMiddleware :: Callback -> IO ZonedDate -> OutputFormatter -> Middleware
110−customMiddleware cb getdate formatter app req = do
111− res <- app req
106+customMiddleware cb getdate formatter app req sendResponse = app req $ \res -> do
112107 date <- liftIO getdate
113108 -- We use Nothing for the response size since we generally don't know it
114109 liftIO $ cb $ formatter date req (responseStatus res) Nothing
115− return res
110+ sendResponse res
116111
117112-- | Production request logger middleware.
118113-- Implemented on top of "logCallback", but prints to 'stdout'
@@ -186,13 +181,18 @@ ansiColor color bs = [
186181detailedMiddleware' :: Callback
187182 -> IO (BS.ByteString -> [BS.ByteString])
188183 -> Middleware
189−detailedMiddleware' cb getAddColor app req = do
184+detailedMiddleware' cb getAddColor app req sendResponse = do
190185 let mlen = lookup "content-length" (requestHeaders req) >>= readInt
191186 (req', body) <-
192187 case mlen of
193188 -- log the request body if it is small
194189 Just len | len <= 2048 -> do
195− body <- requestBody req C.$$ CL.consume
190+ let loop front = do
191+ bs <- requestBody req
192+ if S8.null bs
193+ then return $ front []
194+ else loop $ front . (bs:)
195+ body <- loop id
196196 -- logging the body here consumes it, so fill it back up
197197 -- obviously not efficient, but this is the development logger
198198 --
@@ -204,14 +204,10 @@ detailedMiddleware' cb getAddColor app req = do
204204 -- implementation ensures that each chunk is only returned
205205 -- once.
206206 ichunks <- newIORef body
207− let rbody = do
208− chunks <- readIORef ichunks
207+ let rbody = atomicModifyIORef ichunks $ \chunks ->
209208 case chunks of
210− [] -> return ()
211− x:xs -> do
212− writeIORef ichunks xs
213− C.yield x
214− rbody
209+ [] -> ([], S8.empty)
210+ x:y -> (y, x)
215211 let req' = req { requestBody = rbody }
216212 return (req', body)
217213 _ -> return (req, [])
@@ -237,20 +233,20 @@ detailedMiddleware' cb getAddColor app req = do
237233 , "\n"
238234 ]
239235
240− rsp <- app req'
241−
242− -- log the status of the response
243− -- this is color coordinated with the request logging
244− -- also includes the request path to connect it to the request
245− liftIO $ cb $ mconcat $ map toLogStr $
246− addColor "Status: " ++ statusBS rsp ++
247− [ " "
248− , msgBS rsp
249− , ". "
250− , rawPathInfo req -- if you need help matching the 2 logging statements
251− , "\n"
252− ]
253− return rsp
236+ app req' $ \rsp -> do
237+
238+ -- log the status of the response
239+ -- this is color coordinated with the request logging
240+ -- also includes the request path to connect it to the request
241+ liftIO $ cb $ mconcat $ map toLogStr $
242+ addColor "Status: " ++ statusBS rsp ++
243+ [ " "
244+ , msgBS rsp
245+ , ". "
246+ , rawPathInfo req -- if you need help matching the 2 logging statements
247+ , "\n"
248+ ]
249+ sendResponse rsp
254250 where
255251 paramsToBS prefix params =
256252 if null params then ""
@@ -259,7 +255,13 @@ detailedMiddleware' cb getAddColor app req = do
259255 allPostParams body =
260256 case getRequestBodyType req of
261257 Nothing -> return ([], [])
262− Just rbt -> CL.sourceList body C.$$ sinkRequestBody lbsBackEnd rbt
258+ Just rbt -> do
259+ ichunks <- newIORef body
260+ let rbody = atomicModifyIORef ichunks $ \chunks ->
261+ case chunks of
262+ [] -> ([], S8.empty)
263+ x:y -> (y, x)
264+ sinkRequestBody lbsBackEnd rbt rbody
263265
264266 emptyGetParam :: (BS.ByteString, Maybe BS.ByteString) -> (BS.ByteString, BS.ByteString)
265267 emptyGetParam (k, Just v) = (k,v)
wai-extra/Network/Wai/Middleware/Rewrite.hs+2−2
@@ -13,10 +13,10 @@ import Network.HTTP.Types as H
1313
1414-- | rewrite based on your own conversion rules
1515rewrite :: ([Text] -> H.RequestHeaders -> IO [Text]) -> Middleware
16−rewrite convert app req = do
16+rewrite convert app req sendResponse = do
1717 newPathInfo <- liftIO $ convert (pathInfo req) (requestHeaders req)
1818 let rawPInfo = TE.encodeUtf8 $ T.intercalate "/" newPathInfo
19− app req { pathInfo = newPathInfo, rawPathInfo = rawPInfo }
19+ app req { pathInfo = newPathInfo, rawPathInfo = rawPInfo } sendResponse
2020
2121-- | rewrite based on your own conversion rules
2222-- Example convert function:
wai-extra/Network/Wai/Middleware/Vhost.hs+3−3
@@ -20,10 +20,10 @@ redirectWWW home =
2020 redirectIf home (maybe True (BS.isPrefixOf "www") . lookup "host" . requestHeaders)
2121
2222redirectIf :: Text -> (Request -> Bool) -> Application -> Application
23−redirectIf home cond app req =
23+redirectIf home cond app req sendResponse =
2424 if cond req
25− then return $ redirectTo $ TE.encodeUtf8 home
26− else app req
25+ then sendResponse $ redirectTo $ TE.encodeUtf8 home
26+ else app req sendResponse
2727
2828redirectTo :: BS.ByteString -> Response
2929redirectTo location = responseLBS H.status301
wai-extra/Network/Wai/Parse.hs+164−107
@@ -12,7 +12,6 @@ module Network.Wai.Parse
1212 , RequestBodyType (..)
1313 , getRequestBodyType
1414 , sinkRequestBody
15− , conduitRequestBody
1615 , BackEnd
1716 , lbsBackEnd
1817 , tempFileBackEnd
@@ -38,22 +37,14 @@ import qualified Data.ByteString.Char8 as S8
3837import Data.Word (Word8)
3938import Data.Maybe (fromMaybe)
4039import Data.List (sortBy)
41−import Data.Function (on)
40+import Data.Function (on, fix)
4241import System.Directory (removeFile, getTemporaryDirectory)
4342import System.IO (hClose, openBinaryTempFile)
4443import Network.Wai
45−import Data.Conduit
46−import Data.Conduit.Internal ()
47−import qualified Data.Conduit.List as CL
48−import qualified Data.Conduit.Binary as CB
49−import Control.Monad.IO.Class (liftIO)
5044import qualified Network.HTTP.Types as H
51−import Data.Either (partitionEithers)
5245import Control.Monad (when, unless)
53−import Control.Monad.Trans.Class (lift)
5446import Control.Monad.Trans.Resource (allocate, release, register, InternalState, runInternalState)
55−import Data.Conduit.Internal (Pipe (NeedInput, HaveOutput), (>+>), withUpstream, injectLeftovers, ConduitM (..))
56−import Data.Void (Void)
47+import Data.IORef
5748
5849breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString)
5950breakDiscard w s =
@@ -85,15 +76,22 @@ parseHttpAccept = map fst
8576 _ -> 1.0
8677
8778-- | Store uploaded files in memory
88−lbsBackEnd :: Monad m => ignored1 -> ignored2 -> Sink S.ByteString m L.ByteString
89−lbsBackEnd _ _ = fmap L.fromChunks CL.consume
79+lbsBackEnd :: Monad m => ignored1 -> ignored2 -> m S.ByteString -> m L.ByteString
80+lbsBackEnd _ _ popper =
81+ loop id
82+ where
83+ loop front = do
84+ bs <- popper
85+ if S.null bs
86+ then return $ L.fromChunks $ front []
87+ else loop $ front . (bs:)
9088
9189-- | Save uploaded files on disk as temporary files
9290--
9391-- Note: starting with version 2.0, removal of temp files is registered with
9492-- the provided @InternalState@. It is the responsibility of the caller to
9593-- ensure that this @InternalState@ gets cleaned up.
96−tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> Sink S.ByteString IO FilePath
94+tempFileBackEnd :: InternalState -> ignored1 -> ignored2 -> IO S.ByteString -> IO FilePath
9795tempFileBackEnd = tempFileBackEndOpts getTemporaryDirectory "webenc.buf"
9896
9997-- | Same as 'tempFileSink', but use configurable temp folders and patterns.
@@ -102,14 +100,19 @@ tempFileBackEndOpts :: IO FilePath -- ^ get temporary directory
102100 -> InternalState
103101 -> ignored1
104102 -> ignored2
105− -> Sink S.ByteString IO FilePath
106−tempFileBackEndOpts getTmpDir pattern internalState _ _ = do
103+ -> IO S.ByteString
104+ -> IO FilePath
105+tempFileBackEndOpts getTmpDir pattern internalState _ _ popper = do
107106 (key, (fp, h)) <- flip runInternalState internalState $ allocate (do
108107 tempDir <- getTmpDir
109108 openBinaryTempFile tempDir pattern) (\(_, h) -> hClose h)
110109 _ <- runInternalState (register $ removeFile fp) internalState
111− CB.sinkHandle h
112− lift $ release key
110+ fix $ \loop -> do
111+ bs <- popper
112+ unless (S.null bs) $ do
113+ S.hPut h bs
114+ loop
115+ release key
113116 return fp
114117
115118-- | Information on an uploaded file.
@@ -126,11 +129,12 @@ type Param = (S.ByteString, S.ByteString)
126129-- | Post parameter name and associated file information.
127130type File y = (S.ByteString, FileInfo y)
128131
129−-- | A file uploading backend. Takes the parameter name, file name, and content
130−-- type, and returns a `Sink` for storing the contents.
132+-- | A file uploading backend. Takes the parameter name, file name, and a
133+-- stream of data.
131134type BackEnd a = S.ByteString -- ^ parameter name
132135 -> FileInfo ()
133− -> Sink S.ByteString IO a
136+ -> IO S.ByteString
137+ -> IO a
134138
135139data RequestBodyType = UrlEncoded | Multipart S.ByteString
136140
@@ -173,60 +177,99 @@ parseRequestBody :: BackEnd y
173177parseRequestBody s r =
174178 case getRequestBodyType r of
175179 Nothing -> return ([], [])
176− Just rbt -> fmap partitionEithers $ requestBody r $$ conduitRequestBody s rbt =$ CL.consume
180+ Just rbt -> sinkRequestBody s rbt (requestBody r)
177181
178182sinkRequestBody :: BackEnd y
179183 -> RequestBodyType
180− -> Sink S.ByteString IO ([Param], [File y])
181−sinkRequestBody s r = fmap partitionEithers $ conduitRequestBody s r =$ CL.consume
184+ -> IO S.ByteString
185+ -> IO ([Param], [File y])
186+sinkRequestBody s r body = do
187+ ref <- newIORef (id, id)
188+ let add x = atomicModifyIORef ref $ \(y, z) ->
189+ case x of
190+ Left y' -> ((y . (y':), z), ())
191+ Right z' -> ((y, z . (z':)), ())
192+ conduitRequestBody s r body add
193+ (x, y) <- readIORef ref
194+ return (x [], y [])
182195
183196conduitRequestBody :: BackEnd y
184197 -> RequestBodyType
185− -> Conduit S.ByteString IO (Either Param (File y))
186−conduitRequestBody _ UrlEncoded = do
198+ -> IO S.ByteString
199+ -> (Either Param (File y) -> IO ())
200+ -> IO ()
201+conduitRequestBody _ UrlEncoded rbody add = do
187202 -- NOTE: in general, url-encoded data will be in a single chunk.
188203 -- Therefore, I'm optimizing for the usual case by sticking with
189204 -- strict byte strings here.
190− bs <- CL.consume
191− mapM_ yield $ map Left $ H.parseSimpleQuery $ S.concat bs
192−conduitRequestBody backend (Multipart bound) =
193− parsePieces backend $ S8.pack "--" `S.append` bound
194−
195−takeLine :: Monad m => Consumer S.ByteString m (Maybe S.ByteString)
196−takeLine =
205+ let loop front = do
206+ bs <- rbody
207+ if S.null bs
208+ then return $ S.concat $ front []
209+ else loop $ front . (bs:)
210+ bs <- loop id
211+ mapM_ (add . Left) $ H.parseSimpleQuery bs
212+conduitRequestBody backend (Multipart bound) rbody add =
213+ parsePieces backend (S8.pack "--" `S.append` bound) rbody add
214+
215+takeLine :: Source -> IO (Maybe S.ByteString)
216+takeLine src =
197217 go id
198218 where
199− go front = await >>= maybe (close front) (push front)
219+ go front = do
220+ bs <- readSource src
221+ if S.null bs
222+ then close front
223+ else push front bs
200224
201− close front = leftover (front S.empty) >> return Nothing
225+ close front = leftover src (front S.empty) >> return Nothing
202226 push front bs = do
203227 let (x, y) = S.break (== 10) $ front bs -- LF
204228 in if S.null y
205229 then go $ S.append x
206230 else do
207− when (S.length y > 1) $ leftover $ S.drop 1 y
231+ when (S.length y > 1) $ leftover src $ S.drop 1 y
208232 return $ Just $ killCR x
209233
210−takeLines :: Consumer S.ByteString IO [S.ByteString]
211−takeLines = do
212− res <- takeLine
234+takeLines :: Source -> IO [S.ByteString]
235+takeLines src = do
236+ res <- takeLine src
213237 case res of
214238 Nothing -> return []
215239 Just l
216240 | S.null l -> return []
217241 | otherwise -> do
218− ls <- takeLines
242+ ls <- takeLines src
219243 return $ l : ls
220244
245+data Source = Source (IO S.ByteString) (IORef S.ByteString)
246+
247+mkSource :: IO S.ByteString -> IO Source
248+mkSource f = do
249+ ref <- newIORef S.empty
250+ return $ Source f ref
251+
252+readSource :: Source -> IO S.ByteString
253+readSource (Source f ref) = do
254+ bs <- atomicModifyIORef ref $ \bs -> (S.empty, bs)
255+ if S.null bs
256+ then f
257+ else return bs
258+
259+leftover :: Source -> S.ByteString -> IO ()
260+leftover (Source _ ref) bs = writeIORef ref bs
261+
221262parsePieces :: BackEnd y
222263 -> S.ByteString
223− -> ConduitM S.ByteString (Either Param (File y)) IO ()
224−parsePieces sink bound =
225− loop
264+ -> IO S.ByteString
265+ -> (Either Param (File y) -> IO ())
266+ -> IO ()
267+parsePieces sink bound rbody add =
268+ mkSource rbody >>= loop
226269 where
227− loop = do
228− _boundLine <- takeLine
229− res' <- takeLines
270+ loop src = do
271+ _boundLine <- takeLine src
272+ res' <- takeLines src
230273 unless (null res') $ do
231274 let ls' = map parsePair res'
232275 let x = do
@@ -239,23 +282,23 @@ parsePieces sink bound =
239282 Just (mct, name, Just filename) -> do
240283 let ct = fromMaybe "application/octet-stream" mct
241284 fi0 = FileInfo filename ct ()
242− (wasFound, y) <- sinkTillBound' bound name fi0 sink
243− yield $ Right (name, fi0 { fileContent = y })
244− when wasFound loop
285+ (wasFound, y) <- sinkTillBound' bound name fi0 sink src
286+ add $ Right (name, fi0 { fileContent = y })
287+ when wasFound (loop src)
245288 Just (_ct, name, Nothing) -> do
246289 let seed = id
247290 let iter front bs = return $ front . (:) bs
248− (wasFound, front) <- sinkTillBound bound iter seed
291+ (wasFound, front) <- sinkTillBound bound iter seed src
249292 let bs = S.concat $ front []
250293 let x' = (name, bs)
251− yield $ Left x'
252− when wasFound loop
294+ add $ Left x'
295+ when wasFound (loop src)
253296 _ -> do
254297 -- ignore this part
255298 let seed = ()
256299 iter () _ = return ()
257− (wasFound, ()) <- sinkTillBound bound iter seed
258− when wasFound loop
300+ (wasFound, ()) <- sinkTillBound bound iter seed src
301+ when wasFound (loop src)
259302 where
260303 contDisp = S8.pack "Content-Disposition"
261304 contType = S8.pack "Content-Type"
@@ -295,64 +338,78 @@ sinkTillBound' :: S.ByteString
295338 -> S.ByteString
296339 -> FileInfo ()
297340 -> BackEnd y
298− -> ConduitM S.ByteString o IO (Bool, y)
299−sinkTillBound' bound name fi sink =
300− ConduitM $ anyOutput $
301− conduitTillBound bound >+> withUpstream (fix $ sink name fi)
302− where
303− fix :: Sink S8.ByteString IO y -> Pipe Void S8.ByteString Void Bool IO y
304− fix p = ignoreTerm >+> injectLeftovers (unConduitM p)
305− ignoreTerm = await' >>= maybe (return ()) (\x -> yield' x >> ignoreTerm)
306− await' = NeedInput (return . Just) (const $ return Nothing)
307− yield' = HaveOutput (return ()) (return ())
308−
309− anyOutput p = p >+> dropInput
310− dropInput = NeedInput (const dropInput) return
311−
312−conduitTillBound :: Monad m
313− => S.ByteString -- bound
314− -> Pipe S.ByteString S.ByteString S.ByteString () m Bool
315−conduitTillBound bound =
316− unConduitM $
317− go id
341+ -> Source
342+ -> IO (Bool, y)
343+sinkTillBound' bound name fi sink src = do
344+ (next, final) <- wrapTillBound bound src
345+ y <- sink name fi next
346+ b <- final
347+ return (b, y)
348+
349+data WTB = WTBWorking (S.ByteString -> S.ByteString)
350+ | WTBDone Bool
351+wrapTillBound :: S.ByteString -- ^ bound
352+ -> Source
353+ -> IO (IO S.ByteString, IO Bool) -- ^ Bool indicates if the bound was found
354+wrapTillBound bound src = do
355+ ref <- newIORef $ WTBWorking id
356+ return (go ref, final ref)
318357 where
319− go front = await >>= maybe (close front) (push front)
320− close front = do
321− let bs = front S.empty
322− unless (S.null bs) $ yield bs
323− return False
324− push front bs' = do
325− let bs = front bs'
326− case findBound bound bs of
327− FoundBound before after -> do
328− let before' = killCRLF before
329− yield before'
330− leftover after
331− return True
332− NoBound -> do
333− -- don't emit newlines, in case it's part of a bound
334− let (toEmit, front') =
335− if not (S8.null bs) && S8.last bs `elem` "\r\n"
336− then let (x, y) = S.splitAt (S.length bs - 2) bs
337− in (x, S.append y)
338− else (bs, id)
339− yield toEmit
340− go front'
341− PartialBound -> go $ S.append bs
358+ final ref = do
359+ x <- readIORef ref
360+ case x of
361+ WTBWorking _ -> error "wrapTillBound did not finish"
362+ WTBDone y -> return y
363+
364+ go ref = do
365+ state <- readIORef ref
366+ case state of
367+ WTBDone _ -> return S.empty
368+ WTBWorking front -> do
369+ bs <- readSource src
370+ if S.null bs
371+ then do
372+ writeIORef ref $ WTBDone False
373+ return $ front bs
374+ else push $ front bs
375+ where
376+ push bs =
377+ case findBound bound bs of
378+ FoundBound before after -> do
379+ let before' = killCRLF before
380+ leftover src after
381+ writeIORef ref $ WTBDone True
382+ return before'
383+ NoBound -> do
384+ -- don't emit newlines, in case it's part of a bound
385+ let (toEmit, front') =
386+ if not (S8.null bs) && S8.last bs `elem` "\r\n"
387+ then let (x, y) = S.splitAt (S.length bs - 2) bs
388+ in (x, S.append y)
389+ else (bs, id)
390+ writeIORef ref $ WTBWorking front'
391+ if S.null toEmit
392+ then go ref
393+ else return toEmit
394+ PartialBound -> do
395+ writeIORef ref $ WTBWorking $ S.append bs
396+ go ref
342397
343398sinkTillBound :: S.ByteString
344399 -> (x -> S.ByteString -> IO x)
345400 -> x
346− -> Consumer S.ByteString IO (Bool, x)
347−sinkTillBound bound iter seed0 =
348− ConduitM $
349− (conduitTillBound bound >+> (withUpstream $ ij $ CL.foldM iter' seed0))
350− where
351− iter' a b = liftIO $ iter a b
352− ij (ConduitM p) = ignoreTerm >+> injectLeftovers p
353− ignoreTerm = await' >>= maybe (return ()) (\x -> yield' x >> ignoreTerm)
354− await' = NeedInput (return . Just) (const $ return Nothing)
355− yield' = HaveOutput (return ()) (return ())
401+ -> Source
402+ -> IO (Bool, x)
403+sinkTillBound bound iter seed0 src = do
404+ (next, final) <- wrapTillBound bound src
405+ let loop seed = do
406+ bs <- next
407+ if S.null bs
408+ then return seed
409+ else iter seed bs >>= loop
410+ seed <- loop seed0
411+ b <- final
412+ return (b, seed)
356413
357414parseAttrs :: S.ByteString -> [(S.ByteString, S.ByteString)]
358415parseAttrs = map go . S.split 59 -- semicolon
wai-test/Network/Wai/Test.hs → wai-extra/Network/Wai/Test.hsrenamed+26−13
@@ -24,6 +24,7 @@ module Network.Wai.Test
2424 ) where
2525
2626import Network.Wai
27+import Network.Wai.Internal (ResponseReceived (ResponseReceived))
2728import Control.Monad.IO.Class (liftIO)
2829import Control.Monad.Trans.State (StateT, evalStateT)
2930import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
@@ -35,8 +36,7 @@ import Data.Map (Map)
3536import qualified Data.Map as Map
3637import Data.ByteString (ByteString)
3738import qualified Data.ByteString.Char8 as S8
38−import Data.Conduit.Blaze (builderToByteString)
39−import Blaze.ByteString.Builder (flush)
39+import Blaze.ByteString.Builder (toLazyByteString)
4040import qualified Blaze.ByteString.Builder as B
4141import qualified Data.ByteString.Lazy as L
4242import qualified Data.ByteString.Lazy.Char8 as L8
@@ -45,8 +45,8 @@ import Data.CaseInsensitive (CI)
4545import qualified Data.ByteString as S
4646import qualified Data.Text as T
4747import qualified Data.Text.Encoding as TE
48−import qualified Data.Conduit as C
49−import qualified Data.Conduit.List as CL
48+import Data.IORef
49+import Data.Monoid (mempty, mappend)
5050
5151type Session = ReaderT Application (StateT ClientState IO)
5252
@@ -96,21 +96,34 @@ setRawPathInfo r rawPinfo =
9696srequest :: SRequest -> Session SResponse
9797srequest (SRequest req bod) = do
9898 app <- ask
99+ refChunks <- liftIO $ newIORef $ L.toChunks bod
99100 let req' = req
100− { requestBody = CL.sourceList $ L.toChunks bod
101+ { requestBody = atomicModifyIORef refChunks $ \bss ->
102+ case bss of
103+ [] -> ([], S.empty)
104+ x:y -> (y, x)
101105 }
102− liftIO $ app req' >>= runResponse
106+ liftIO $ do
107+ ref <- newIORef $ error "runResponse gave no result"
108+ ResponseReceived <- app req' (runResponse ref)
109+ readIORef ref
103110 -- FIXME cookie processing
104111 --return sres
105112
106−runResponse :: Response -> IO SResponse
107−runResponse res = do
108− bss <- withBody $ \body -> body C.$= CL.map toBuilder C.$= builderToByteString C.$$ CL.consume
109− return $ SResponse s h $ L.fromChunks bss
113+runResponse :: IORef SResponse -> Response -> IO ResponseReceived
114+runResponse ref res = do
115+ refBuilder <- newIORef mempty
116+ let add y = atomicModifyIORef refBuilder $ \x -> (x `mappend` y, ())
117+ withBody $ \body -> body add (return ())
118+ builder <- readIORef refBuilder
119+ let lbs = toLazyByteString builder
120+ len = L.length lbs
121+ -- Force evaluation of the body to have exceptions thrown at the right
122+ -- time.
123+ seq len $ writeIORef ref $ SResponse s h $ toLazyByteString builder
124+ return ResponseReceived
110125 where
111− (s, h, withBody) = responseToSource res
112− toBuilder (C.Chunk builder) = builder
113− toBuilder C.Flush = flush
126+ (s, h, withBody) = responseToStream res
114127
115128assertBool :: String -> Bool -> Session ()
116129assertBool s b = unless b $ assertFailure s
wai-extra/Network/Wai/UrlMap.hs+6−5
@@ -1,4 +1,5 @@
11{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
2+{-# LANGUAGE ExistentialQuantification #-}
23{- | This module gives you a way to mount applications under sub-URIs.
34For example:
45
@@ -83,14 +84,14 @@ instance ToApplication Application where
8384 toApplication = id
8485
8586instance ToApplication UrlMap where
86− toApplication urlMap = \req ->
87+ toApplication urlMap req sendResponse =
8788 case try (pathInfo req) (unUrlMap urlMap) of
8889 Just (newPath, app) ->
89− app $ req { pathInfo = newPath
90− , rawPathInfo = makeRaw newPath
91− }
90+ app (req { pathInfo = newPath
91+ , rawPathInfo = makeRaw newPath
92+ }) sendResponse
9293 Nothing ->
93− return $ responseLBS
94+ sendResponse $ responseLBS
9495 status404
9596 [("content-type", "text/plain")]
9697 "Not found\n"
wai-eventsource/src/Main.hs → wai-extra/app/Main.hsrenamed

The content is the same.

wai-eventsource/static/index.html → wai-extra/app/index.htmlrenamed

The content is the same.

wai-test/test/Network/Wai/TestSpec.hs → wai-extra/test/Network/Wai/TestSpec.hsrenamed

The content is the same.

wai-test/test/Spec.hs → wai-extra/test/Spec.hsrenamed

The content is the same.

wai-extra/test/WaiExtraTest.hs+25−22
@@ -30,9 +30,6 @@ import Network.Wai.Middleware.AcceptOverride
3030import Network.Wai.Middleware.RequestLogger
3131import Codec.Compression.GZip (decompress)
3232
33−import qualified Data.Conduit as C
34−import qualified Data.Conduit.List as CL
35−import Data.Conduit.Binary (sourceHandle)
3633import Control.Monad.IO.Class (liftIO)
3734import Data.Maybe (fromMaybe)
3835import Network.HTTP.Types (parseSimpleQuery, status200)
@@ -128,7 +125,13 @@ parseRequestBody' :: BackEnd file
128125parseRequestBody' sink (SRequest req bod) =
129126 case getRequestBodyType req of
130127 Nothing -> return ([], [])
131− Just rbt -> CL.sourceList (L.toChunks bod) C.$$ sinkRequestBody sink rbt
128+ Just rbt -> do
129+ ref <- I.newIORef $ L.toChunks bod
130+ let rb = I.atomicModifyIORef ref $ \chunks ->
131+ case chunks of
132+ [] -> ([], S.empty)
133+ x:y -> (y, x)
134+ sinkRequestBody sink rbt rb
132135
133136caseParseRequestBody :: Assertion
134137caseParseRequestBody =
@@ -295,7 +298,7 @@ caseTakeLine = do
295298-}
296299
297300jsonpApp :: Application
298−jsonpApp = jsonp $ const $ return $ responseLBS
301+jsonpApp = jsonp $ \_ f -> f $ responseLBS
299302 status200
300303 [("Content-Type", "application/json")]
301304 "{\"foo\":\"bar\"}"
@@ -324,7 +327,7 @@ caseJsonp = flip runSession jsonpApp $ do
324327 assertBody "{\"foo\":\"bar\"}" sres3
325328
326329gzipApp :: Application
327−gzipApp = gzip def $ const $ return $ responseLBS status200
330+gzipApp = gzip def $ \_ f -> f $ responseLBS status200
328331 [("Content-Type", "text/plain")]
329332 "test"
330333
@@ -332,7 +335,7 @@ gzipApp = gzip def $ const $ return $ responseLBS status200
332335-- that the compression is skipped based on the presence of
333336-- the Content-Encoding header.
334337gzipPrecompressedApp :: Application
335−gzipPrecompressedApp = gzip def $ const $ return $ responseLBS status200
338+gzipPrecompressedApp = gzip def $ \_ f -> f $ responseLBS status200
336339 [("Content-Type", "text/plain"), ("Content-Encoding", "gzip")]
337340 "test"
338341
@@ -380,8 +383,8 @@ caseGzipBypassPre = flip runSession gzipPrecompressedApp $ do
380383 assertBody "test" sres1 -- the body is not actually compressed
381384
382385vhostApp1, vhostApp2, vhostApp :: Application
383−vhostApp1 = const $ return $ responseLBS status200 [] "app1"
384−vhostApp2 = const $ return $ responseLBS status200 [] "app2"
386+vhostApp1 _ f = f $ responseLBS status200 [] "app1"
387+vhostApp2 _ f = f $ responseLBS status200 [] "app2"
385388vhostApp = vhost
386389 [ ((== Just "foo.com") . lookup "host" . requestHeaders, vhostApp1)
387390 ]
@@ -400,7 +403,7 @@ caseVhost = flip runSession vhostApp $ do
400403 assertBody "app2" sres2
401404
402405autoheadApp :: Application
403−autoheadApp = autohead $ const $ return $ responseLBS status200
406+autoheadApp = autohead $ \_ f -> f $ responseLBS status200
404407 [("Foo", "Bar")] "body"
405408
406409caseAutohead :: Assertion
@@ -418,7 +421,7 @@ caseAutohead = flip runSession autoheadApp $ do
418421 assertBody "" sres2
419422
420423moApp :: Application
421−moApp = methodOverride $ \req -> return $ responseLBS status200
424+moApp = methodOverride $ \req f -> f $ responseLBS status200
422425 [("Method", requestMethod req)] ""
423426
424427caseMethodOverride :: Assertion
@@ -442,7 +445,7 @@ caseMethodOverride = flip runSession moApp $ do
442445 assertHeader "Method" "PUT" sres3
443446
444447mopApp :: Application
445−mopApp = methodOverridePost $ \req -> return $ responseLBS status200 [("Method", requestMethod req)] ""
448+mopApp = methodOverridePost $ \req f -> f $ responseLBS status200 [("Method", requestMethod req)] ""
446449
447450caseMethodOverridePost :: Assertion
448451caseMethodOverridePost = flip runSession mopApp $ do
@@ -468,7 +471,7 @@ caseMethodOverridePost = flip runSession mopApp $ do
468471 assertHeader "Method" "POST" sres4
469472
470473aoApp :: Application
471−aoApp = acceptOverride $ \req -> return $ responseLBS status200
474+aoApp = acceptOverride $ \req f -> f $ responseLBS status200
472475 [("Accept", fromMaybe "" $ lookup "Accept" $ requestHeaders req)] ""
473476
474477caseAcceptOverride :: Assertion
@@ -520,7 +523,7 @@ dalvikHelper includeLength = do
520523 case getRequestBodyType request' of
521524 Nothing -> return ([], [])
522525 Just rbt -> withFile "test/requests/dalvik-request" ReadMode $ \h ->
523− sourceHandle h C.$$ sinkRequestBody lbsBackEnd rbt
526+ sinkRequestBody lbsBackEnd rbt $ S.hGetSome h 2048
524527 lookup "scannedTime" params @?= Just "1.298590056748E9"
525528 lookup "geoLong" params @?= Just "0"
526529 lookup "geoLat" params @?= Just "0"
@@ -548,15 +551,15 @@ caseDebugRequestBody = do
548551 postOutput = T.pack $ "POST / :: \nStatus: 200 OK. /\n"
549552 getOutput params' = T.pack $ "GET /location :: \nGET " ++ show params' ++ "\nStatus: 200 OK. /location\n"
550553
551− debugApp output' req = do
552− iactual <- liftIO $ I.newIORef mempty
553− middleware <- liftIO $ mkRequestLogger def
554+ debugApp output' req send = do
555+ iactual <- I.newIORef mempty
556+ middleware <- mkRequestLogger def
554557 { destination = Callback $ \strs -> I.modifyIORef iactual $ (`mappend` strs)
555558 , outputFormat = Detailed False
556559 }
557− res <- middleware (\_req -> return $ responseLBS status200 [ ] "") req
558− actual <- liftIO $ I.readIORef iactual
559− liftIO $ assertEqual "debug" output $ logToBs actual
560+ res <- middleware (\_req f -> f $ responseLBS status200 [ ] "") req send
561+ actual <- I.readIORef iactual
562+ assertEqual "debug" output $ logToBs actual
560563 return res
561564 where
562565 output = TE.encodeUtf8 $ T.toStrict output'
@@ -578,8 +581,8 @@ urlMapTestApp = mapUrls $
578581
579582 where
580583 trivialApp :: S.ByteString -> Application
581− trivialApp name req =
582− return $
584+ trivialApp name req f =
585+ f $
583586 responseLBS
584587 status200
585588 [ ("content-type", "text/plain")
wai-extra/wai-extra.cabal+19−11
@@ -1,5 +1,5 @@
11Name: wai-extra
2−Version: 2.1.1.2
2+Version: 3.0.0
33Synopsis: Provides some basic WAI handlers and middleware.
44Description: The goal here is to provide common features without many dependencies.
55License: MIT
@@ -22,7 +22,7 @@ extra-source-files:
2222Library
2323 Build-Depends: base >= 4 && < 5
2424 , bytestring >= 0.9.1.4
25− , wai >= 2.1 && < 2.2
25+ , wai >= 3.0 && < 3.1
2626 , old-locale >= 1.0.0.2 && < 1.1
2727 , time >= 1.1.4
2828 , network >= 2.2.1.5
@@ -32,13 +32,9 @@ Library
3232 , http-types >= 0.7
3333 , text >= 0.7
3434 , case-insensitive >= 0.2
35− , data-default
35+ , data-default-class
3636 , fast-logger >= 2.1 && < 2.2
3737 , wai-logger >= 2.0 && < 2.2
38− , conduit >= 1.0 && < 1.2
39− , conduit-extra >= 0.1 && < 1.2
40− , zlib-conduit >= 0.5 && < 1.2
41− , blaze-builder-conduit >= 0.5 && < 1.2
4238 , ansi-terminal
4339 , resourcet >= 0.4.6 && < 1.2
4440 , void >= 0.5
@@ -47,6 +43,8 @@ Library
4743 , base64-bytestring
4844 , word8
4945 , lifted-base >= 0.1.2
46+ , deepseq
47+ , streaming-commons
5048
5149 if os(windows)
5250 cpp-options: -DWINDOWS
@@ -68,6 +66,9 @@ Library
6866 Network.Wai.Middleware.HttpAuth
6967 Network.Wai.Parse
7068 Network.Wai.UrlMap
69+ Network.Wai.Test
70+ Network.Wai.EventSource
71+ Network.Wai.EventSource.EventStream
7172 other-modules: Network.Wai.Middleware.RequestLogger.Internal
7273 ghc-options: -Wall
7374
@@ -79,7 +80,6 @@ test-suite tests
7980
8081 build-depends: base >= 4 && < 5
8182 , wai-extra
82− , wai-test >= 1.3
8383 , hspec >= 1.3
8484 , HUnit
8585
@@ -90,15 +90,23 @@ test-suite tests
9090 , text
9191 , bytestring
9292 , directory
93− , zlib-bindings
9493 , blaze-builder >= 0.2.1.4 && < 0.4
9594 , data-default
96− , conduit
9795 , fast-logger >= 2.1
9896 , resourcet
99− , conduit-extra
10097 ghc-options: -Wall
10198
99+test-suite spec
100+ type: exitcode-stdio-1.0
101+ hs-source-dirs: test
102+ main-is: Spec.hs
103+ other-modules: Network.Wai.TestSpec
104+ build-depends: base >= 4 && < 5
105+ , wai-extra
106+ , wai
107+ , hspec >= 1.3
108+ ghc-options: -Wall -Werror
109+
102110source-repository head
103111 type: git
104112 location: git://github.com/yesodweb/wai.git
wai-frontend-monadcgi/Network/Wai/Frontend/MonadCGI.hs+3−5
@@ -10,8 +10,6 @@ import Network.HTTP.Types (Status (..))
1010import Control.Monad.IO.Class (liftIO)
1111import Data.CaseInsensitive (original)
1212
13−import Data.Conduit.Lazy (lazyConsume)
14−
1513import qualified Data.Map as Map
1614import qualified Data.ByteString.Lazy as BS
1715import qualified Data.ByteString.Char8 as S8
@@ -32,8 +30,8 @@ cgiToAppGeneric :: Monad m
3230 => (m (Headers, CGIResult) -> IO (Headers, CGIResult))
3331 -> CGIT m CGIResult
3432 -> Application
35−cgiToAppGeneric toIO cgi env = do
36− input <- fmap BS.fromChunks $ lazyConsume $ requestBody env
33+cgiToAppGeneric toIO cgi env sendResponse = do
34+ input <- lazyRequestBody env
3735 let vars = map (first fixVarName . go) (requestHeaders env)
3836 ++ getCgiVars env
3937 (inputs, body') = decodeInput vars input
@@ -51,7 +49,7 @@ cgiToAppGeneric toIO cgi env = do
5149 let status' = case lookup (fromString "Status") headers' of
5250 Nothing -> 200
5351 Just s -> safeRead 200 $ S8.unpack s
54− return $ responseLBS (Status status' S8.empty) headers' output
52+ sendResponse $ responseLBS (Status status' S8.empty) headers' output
5553 where
5654 go (x, y) = (S8.unpack $ original x, S8.unpack y)
5755
wai-frontend-monadcgi/wai-frontend-monadcgi.cabal+2−4
@@ -1,5 +1,5 @@
11name: wai-frontend-monadcgi
2−version: 2.0.0.2
2+version: 3.0.0
33license: MIT
44license-file: LICENSE
55author: Michael Snoyman <michael@snoyman.com>
@@ -16,12 +16,10 @@ library
1616 , bytestring
1717 , containers >= 0.2
1818 , cgi
19− , conduit >= 0.5 && < 1.2
20− , conduit-extra
2119 , http-types
2220 , transformers
2321 , case-insensitive
24− , wai >= 2.0 && < 2.2
22+ , wai >= 3.0 && < 3.1
2523 exposed-modules: Network.Wai.Frontend.MonadCGI
2624 ghc-options: -Wall
2725
wai-handler-fastcgi/wai-handler-fastcgi.cabal+3−3
@@ -1,5 +1,5 @@
11name: wai-handler-fastcgi
2−version: 2.0.0.1
2+version: 3.0.0
33license: MIT
44license-file: LICENSE
55author: Michael Snoyman <michael@snoyman.com>
@@ -15,8 +15,8 @@ description: Calls out to the libfcgi C library.
1515
1616library
1717 build-depends: base >= 4 && < 5
18− , wai >= 2.0 && < 2.2
19− , wai-extra >= 2.0 && < 2.2
18+ , wai >= 3.0 && < 3.1
19+ , wai-extra >= 3.0 && < 3.1
2020 , bytestring >= 0.9.1.4
2121 exposed-modules: Network.Wai.Handler.FastCGI
2222 ghc-options: -Wall
wai-handler-launch/Network/Wai/Handler/Launch.hs+100−50
@@ -12,84 +12,134 @@ import Network.Wai.Internal
1212import Network.HTTP.Types
1313import qualified Network.Wai.Handler.Warp as Warp
1414import Data.IORef
15+import Data.Monoid (mappend)
1516import Control.Concurrent (forkIO, threadDelay)
1617import Control.Monad.IO.Class (liftIO)
18+import Control.Monad (unless)
19+import Control.Exception (throwIO)
20+import Data.Function (fix)
1721import qualified Data.ByteString as S
18−import Blaze.ByteString.Builder (fromByteString)
22+import Blaze.ByteString.Builder (fromByteString, Builder, flush)
23+import qualified Blaze.ByteString.Builder as Blaze
1924#if WINDOWS
2025import Foreign
2126import Foreign.C.String
2227#else
23−import System.Cmd (rawSystem)
28+import System.Process (rawSystem)
2429#endif
25−import Data.Conduit.Zlib (decompressFlush, WindowBits (WindowBits))
26−import Data.Conduit.Blaze (builderToByteStringFlush)
27−import Data.Conduit
28−import qualified Data.Conduit.List as CL
30+import Data.Streaming.Blaze (newBlazeRecv, defaultStrategy)
31+import qualified Data.Streaming.Zlib as Z
2932
3033ping :: IORef Bool -> Middleware
31−ping var app req
34+ping var app req sendResponse
3235 | pathInfo req == ["_ping"] = do
3336 liftIO $ writeIORef var True
34− return $ responseLBS status200 [] ""
35− | otherwise = do
36− res <- app req
37+ sendResponse $ responseLBS status200 [] ""
38+ | otherwise = app req $ \res -> do
3739 let isHtml hs =
3840 case lookup "content-type" hs of
3941 Just ct -> "text/html" `S.isPrefixOf` ct
4042 Nothing -> False
41− case res of
42− ResponseFile _ hs _ _
43− | not $ isHtml hs -> return res
44− ResponseBuilder _ hs _
45− | not $ isHtml hs -> return res
46− ResponseSource _ hs _
47− | not $ isHtml hs -> return res
48− _ -> do
49− let (s, hs, withBody) = responseToSource res
50− let (isEnc, headers') = fixHeaders id hs
51− let headers'' = filter (\(x, _) -> x /= "content-length") headers'
52− let fixEnc src =
53− if isEnc then
54− src $= decompressFlush (WindowBits 31)
55− else src
56− return $ ResponseSource s headers'' $ \f -> withBody $ \body -> f
57− $ fixEnc (body $= builderToByteStringFlush)
58− $= insideHead
59− $= CL.map (fmap fromByteString)
43+ if isHtml $ responseHeaders res
44+ then do
45+ let (s, hs, withBody) = responseToStream res
46+ (isEnc, headers') = fixHeaders id hs
47+ headers'' = filter (\(x, _) -> x /= "content-length") headers'
48+ withBody $ \body ->
49+ sendResponse $ responseStream s headers'' $ \sendChunk flush ->
50+ addInsideHead sendChunk flush $ \sendChunk' flush' ->
51+ if isEnc
52+ then decode sendChunk' flush' body
53+ else body sendChunk' flush'
54+ else sendResponse res
55+
56+decode :: (Builder -> IO ()) -> IO ()
57+ -> StreamingBody
58+ -> IO ()
59+decode sendInner flushInner streamingBody = do
60+ (blazeRecv, blazeFinish) <- newBlazeRecv defaultStrategy
61+ inflate <- Z.initInflate $ Z.WindowBits 31
62+ let send builder = blazeRecv builder >>= goBuilderPopper
63+ goBuilderPopper popper = fix $ \loop -> do
64+ bs <- popper
65+ unless (S.null bs) $ do
66+ Z.feedInflate inflate bs >>= goZlibPopper
67+ loop
68+ goZlibPopper popper = fix $ \loop -> do
69+ res <- popper
70+ case res of
71+ Z.PRDone -> return ()
72+ Z.PRNext bs -> do
73+ sendInner $ fromByteString bs
74+ loop
75+ Z.PRError e -> throwIO e
76+ streamingBody send (send flush)
77+ mbs <- blazeFinish
78+ case mbs of
79+ Nothing -> return ()
80+ Just bs -> Z.feedInflate inflate bs >>= goZlibPopper
81+ Z.finishInflate inflate >>= sendInner . fromByteString
6082
6183toInsert :: S.ByteString
6284toInsert = "<script>setInterval(function(){var x;if(window.XMLHttpRequest){x=new XMLHttpRequest();}else{x=new ActiveXObject(\"Microsoft.XMLHTTP\");}x.open(\"GET\",\"/_ping\",false);x.send();},60000)</script>"
6385
64−insideHead :: Conduit (Flush S.ByteString) IO (Flush S.ByteString)
65−insideHead =
66− loop' (S.empty, whole)
86+addInsideHead :: (Builder -> IO ())
87+ -> IO ()
88+ -> StreamingBody
89+ -> IO ()
90+addInsideHead sendInner flushInner streamingBody = do
91+ (blazeRecv, blazeFinish) <- newBlazeRecv defaultStrategy
92+ ref <- newIORef $ Just (S.empty, whole)
93+ streamingBody (inner blazeRecv ref) (flush blazeRecv ref)
94+ state <- readIORef ref
95+ mbs <- blazeFinish
96+ held <- case mbs of
97+ Nothing -> return state
98+ Just bs -> push state bs
99+ case state of
100+ Nothing -> return ()
101+ Just (held, _) -> sendInner $ fromByteString held `mappend` fromByteString toInsert
67102 where
68− loop' state = await >>= maybe (close state) (push' state)
69103 whole = "<head>"
70− push' state (Chunk x) = push state x
71− push' state Flush = yield Flush >> loop' state
72104
73− push (held, atFront) x
105+ flush blazeRecv ref = inner blazeRecv ref Blaze.flush
106+
107+ inner blazeRecv ref builder = do
108+ state0 <- readIORef ref
109+ popper <- blazeRecv builder
110+ let loop state = do
111+ bs <- popper
112+ if S.null bs
113+ then writeIORef ref state
114+ else push state bs >>= loop
115+ loop state0
116+
117+ push Nothing x = sendInner (fromByteString x) >> return Nothing
118+ push (Just (held, atFront)) x
74119 | atFront `S.isPrefixOf` x = do
75120 let y = S.drop (S.length atFront) x
76− mapM_ (yield . Chunk) [held, atFront, toInsert, y]
77− CL.map id
121+ sendInner $ fromByteString held
122+ `mappend` fromByteString atFront
123+ `mappend` fromByteString toInsert
124+ `mappend` fromByteString y
125+ return Nothing
78126 | whole `S.isInfixOf` x = do
79127 let (before, rest) = S.breakSubstring whole x
80128 let after = S.drop (S.length whole) rest
81− mapM_ (yield . Chunk) [held, before, whole, toInsert, after]
82− CL.map id
129+ sendInner $ fromByteString held
130+ `mappend` fromByteString before
131+ `mappend` fromByteString whole
132+ `mappend` fromByteString toInsert
133+ `mappend` fromByteString after
134+ return Nothing
83135 | x `S.isPrefixOf` atFront = do
84136 let held' = held `S.append` x
85137 atFront' = S.drop (S.length x) atFront
86− loop' (held', atFront')
138+ return $ Just (held', atFront')
87139 | otherwise = do
88140 let (held', atFront', x') = getOverlap whole x
89− mapM_ (yield . Chunk) [held, x']
90− loop' (held', atFront')
91−
92− close (held, _) = mapM_ yield [Chunk held, Chunk toInsert]
141+ sendInner $ fromByteString held `mappend` fromByteString x'
142+ return $ Just (held', atFront')
93143
94144getOverlap :: S.ByteString -> S.ByteString -> (S.ByteString, S.ByteString, S.ByteString)
95145getOverlap whole x =
@@ -138,11 +188,11 @@ runUrl = runUrlPort 4587
138188runUrlPort :: Int -> String -> Application -> IO ()
139189runUrlPort port url app = do
140190 x <- newIORef True
141− _ <- forkIO $ Warp.runSettings Warp.defaultSettings
142− { Warp.settingsPort = port
143− , Warp.settingsOnException = (\_ _ -> return ())
144− , Warp.settingsHost = "*4"
145− } $ ping x app
191+ _ <- forkIO $ Warp.runSettings
192+ ( Warp.setPort port
193+ $ Warp.setOnException (\_ _ -> return ())
194+ $ Warp.setHost "*4" Warp.defaultSettings)
195+ $ ping x app
146196 launch port url
147197 loop x
148198
wai-handler-launch/test.hs+1−1
@@ -4,4 +4,4 @@ import Network.HTTP.Types
44import Network.Wai.Handler.Launch
55import Network.Wai.Middleware.Gzip
66
7−main = runUrl "FIXME" $ gzip True $ \_ -> return $ responseLBS status200 [("Content-Type", "text/html; charset=utf8")] "<html><head></head><body>HELLO THERE"
7+main = runUrl "FIXME" $ gzip def $ \_ f -> f $ responseLBS status200 [("Content-Type", "text/html; charset=utf8")] "<html><head></head><body>HELLO THERE"
wai-handler-launch/wai-handler-launch.cabal+4−7
@@ -1,5 +1,5 @@
11Name: wai-handler-launch
2−Version: 2.0.1.3
2+Version: 3.0.0
33Synopsis: Launch a web app in the default browser.
44Description: This handles cross-platform launching and inserts Javascript code to ping the server. When the server no longer receives pings, it shuts down.
55License: MIT
@@ -13,16 +13,13 @@ Cabal-version: >=1.6
1313Library
1414 Exposed-modules: Network.Wai.Handler.Launch
1515 build-depends: base >= 4 && < 5
16− , wai >= 2.0 && < 2.2
17− , warp >= 2.0 && < 2.2
16+ , wai >= 3.0 && < 3.1
17+ , warp >= 3.0 && < 3.1
1818 , http-types >= 0.7
1919 , transformers >= 0.2.2
2020 , bytestring >= 0.9.1.4
2121 , blaze-builder >= 0.2.1.4 && < 0.4
22− , conduit >= 0.5 && < 1.2
23− , conduit-extra >= 0.0 && < 1.2
24− , blaze-builder-conduit >= 0.5 && < 1.2
25− , zlib-conduit >= 0.5 && < 1.2
22+ , streaming-commons
2623
2724 if os(windows)
2825 c-sources: windows.c
wai-handler-webkit/wai-handler-webkit.cabal+3−3
@@ -1,5 +1,5 @@
11name: wai-handler-webkit
2−version: 2.0.0.2
2+version: 3.0.0
33license: MIT
44license-file: LICENSE
55author: Michael Snoyman <michael@snoyman.com>
@@ -16,8 +16,8 @@ homepage: https://github.com/yesodweb/wai/tree/master/wai-handler-webkit
1616
1717library
1818 build-depends: base >= 4 && < 5
19− , wai >= 2.0 && < 2.2
20− , warp >= 2.0 && < 2.2
19+ , wai >= 3.0 && < 3.1
20+ , warp >= 3.0 && < 3.1
2121 ghc-options: -Wall
2222 exposed-modules: Network.Wai.Handler.Webkit
2323 c-sources: webkit.cpp
wai-test/READMEdeleted

No changes to the content.

wai-test/wai-test.cabal+4−35
@@ -1,47 +1,16 @@
11name: wai-test
2−version: 2.0.1.3
2+version: 3.0.0
33license: MIT
44license-file: LICENSE
55author: Michael Snoyman <michael@snoyman.com>
66maintainer: Michael Snoyman <michael@snoyman.com>
7−synopsis: Unit test framework (built on HUnit) for WAI applications.
7+synopsis: Unit test framework (built on HUnit) for WAI applications. (deprecated)
8+description: Since WAI 3.0, this code has been merged into wai-extra.
89category: Testing, Web, Yesod
910stability: Stable
1011cabal-version: >= 1.8
1112build-type: Simple
1213homepage: http://www.yesodweb.com/book/web-application-interface
13−description: Unit test framework (built on HUnit) for WAI applications.
1414
1515library
16− build-depends: base >= 4 && < 5
17− , wai >= 2.0 && < 2.2
18− , bytestring >= 0.9.1.4
19− , text >= 0.7
20− , blaze-builder >= 0.2.1.4 && < 0.4
21− , transformers >= 0.2.2
22− , containers >= 0.2
23− , conduit >= 0.5 && < 1.2
24− , conduit-extra
25− , blaze-builder-conduit >= 0.5 && < 1.2
26− , cookie >= 0.2 && < 0.5
27− , http-types >= 0.7
28− , case-insensitive >= 0.2
29− , network
30− , deepseq
31− exposed-modules: Network.Wai.Test
32− ghc-options: -Wall
33−
34−test-suite spec
35− type: exitcode-stdio-1.0
36− hs-source-dirs: test
37− main-is: Spec.hs
38− other-modules: Network.Wai.TestSpec
39− build-depends: base >= 4 && < 5
40− , wai-test
41− , wai
42− , hspec >= 1.3
43− ghc-options: -Wall -Werror
44−
45−source-repository head
46− type: git
47− location: git://github.com/yesodweb/wai.git
16+ build-depends: wai >= 3.0
wai-websockets/Network/Wai/Handler/WebSockets.hs+9−27
@@ -10,13 +10,10 @@ module Network.Wai.Handler.WebSockets
1010import Data.ByteString (ByteString)
1111import qualified Data.ByteString.Char8 as BC
1212import qualified Data.CaseInsensitive as CI
13−import Data.Conduit
14−import Data.IORef (newIORef, readIORef, writeIORef)
1513import Network.HTTP.Types (status500)
1614import qualified Network.Wai as Wai
1715import qualified Network.WebSockets as WS
1816import qualified Network.WebSockets.Connection as WS
19−import System.IO.Streams (InputStream, OutputStream)
2017import qualified System.IO.Streams as Streams
2118
2219--------------------------------------------------------------------------------
@@ -29,10 +26,10 @@ websocketsOr :: WS.ConnectionOptions
2926 -> WS.ServerApp
3027 -> Wai.Application
3128 -> Wai.Application
32−websocketsOr opts app backup req =
29+websocketsOr opts app backup req sendResponse =
3330 case websocketsApp opts app req of
34− Nothing -> backup req
35− Just res -> return res
31+ Nothing -> backup req sendResponse
32+ Just res -> sendResponse res
3633
3734--------------------------------------------------------------------------------
3835websocketsApp :: WS.ConnectionOptions
@@ -61,13 +58,15 @@ getRequestHead req = WS.RequestHead
6158runWebSockets :: WS.ConnectionOptions
6259 -> WS.RequestHead
6360 -> (WS.PendingConnection -> IO a)
64− -> Source IO ByteString
65− -> Sink ByteString IO ()
61+ -> IO ByteString
62+ -> (ByteString -> IO ())
6663 -> IO a
6764runWebSockets opts req app src sink = do
6865
69− is <- srcToInput src
70− os <- sinkToOutput sink >>= Streams.builderStream
66+ is <- Streams.makeInputStream $ do
67+ bs <- src
68+ return $ if BC.null bs then Nothing else Just bs
69+ os <- Streams.makeOutputStream (maybe (return ()) sink) >>= Streams.builderStream
7170
7271 let pc = WS.PendingConnection
7372 { WS.pendingOptions = opts
@@ -78,20 +77,3 @@ runWebSockets opts req app src sink = do
7877 }
7978
8079 app pc
81−
82−srcToInput :: Source IO ByteString -> IO (InputStream ByteString)
83−srcToInput src0 = do
84− (rsrc0, ()) <- src0 $$+ return ()
85− ref <- newIORef rsrc0
86− Streams.makeInputStream $ do
87− rsrc <- readIORef ref
88− (rsrc', mbs) <- rsrc $$++ await
89− writeIORef ref rsrc'
90− return mbs
91−
92−sinkToOutput :: Sink ByteString IO () -> IO (OutputStream ByteString)
93−sinkToOutput sink =
94− Streams.makeOutputStream output
95− where
96− output Nothing = return ()
97− output (Just bs) = yield bs $$ sink
wai-websockets/wai-websockets.cabal+2−4
@@ -1,5 +1,5 @@
11Name: wai-websockets
2−Version: 2.1.0.2
2+Version: 3.0.0
33Synopsis: Provide a bridge betweeen WAI and the websockets package.
44License: MIT
55License-file: LICENSE
@@ -19,8 +19,7 @@ flag example
1919Library
2020 Build-Depends: base >= 3 && < 5
2121 , bytestring >= 0.9.1.4
22− , conduit >= 0.5 && < 1.2
23− , wai >= 2.1 && < 2.2
22+ , wai >= 3.0 && < 3.1
2423 , blaze-builder >= 0.2.1.4 && < 0.4
2524 , case-insensitive >= 0.2
2625 , network >= 2.2.1.5
@@ -35,7 +34,6 @@ Executable wai-websockets-example
3534 if flag(example)
3635 buildable: True
3736 Build-Depends: base >= 3 && < 5
38− , conduit
3937 , wai-websockets
4038 , websockets
4139 , warp
wai/Network/Wai.hs+63−53
@@ -39,6 +39,7 @@ module Network.Wai
3939 -- * Types
4040 Application
4141 , Middleware
42+ , ResponseReceived
4243 -- * Request
4344 , Request
4445 , defaultRequest
@@ -61,36 +62,35 @@ module Network.Wai
6162 , lazyRequestBody
6263 -- * Response
6364 , Response
65+ , StreamingBody
6466 , FilePart (..)
65− , WithSource
6667 -- ** Response composers
6768 , responseFile
6869 , responseBuilder
6970 , responseLBS
70− , responseSource
71− , responseSourceBracket
71+ , responseStream
7272 , responseRaw
7373 -- * Response accessors
7474 , responseStatus
7575 , responseHeaders
76− , responseToSource
76+ , responseToStream
7777 ) where
7878
7979import Blaze.ByteString.Builder (Builder, fromLazyByteString)
8080import Blaze.ByteString.Builder (fromByteString)
81−import Control.Exception (bracket, bracketOnError)
81+import Control.Monad (unless)
8282import qualified Data.ByteString as B
8383import qualified Data.ByteString.Lazy as L
84+import qualified Data.ByteString.Lazy.Internal as LI
85+import Data.ByteString.Lazy.Internal (defaultChunkSize)
8486import Data.ByteString.Lazy.Char8 ()
85−import qualified Data.Conduit as C
86−import qualified Data.Conduit.Binary as CB
87−import Data.Conduit.Lazy (lazyConsume)
88−import qualified Data.Conduit.List as CL
87+import Data.Function (fix)
8988import Data.Monoid (mempty)
9089import qualified Network.HTTP.Types as H
9190import Network.Socket (SockAddr (SockAddrInet))
9291import Network.Wai.Internal
9392import qualified System.IO as IO
93+import System.IO.Unsafe (unsafeInterleaveIO)
9494
9595----------------------------------------------------------------
9696
@@ -130,29 +130,11 @@ responseLBS :: H.Status -> H.ResponseHeaders -> L.ByteString -> Response
130130responseLBS s h = ResponseBuilder s h . fromLazyByteString
131131
132132-- | Creating 'Response' from 'C.Source'.
133−responseSource :: H.Status -> H.ResponseHeaders -> C.Source IO (C.Flush Builder) -> Response
134−responseSource st hs src = ResponseSource st hs ($ src)
135−
136−-- | Creating 'Response' with allocated resource safely released.
137−--
138−-- * The first argument is an action to allocate resource.
139−--
140−-- * The second argument is a function to release the resource.
141−--
142−-- * The third argument is a function to create
143−-- ('H.Status','H.ResponseHeaders','C.Source' 'IO' ('C.Flush' 'Builder'))
144−-- from the resource.
145−responseSourceBracket :: IO a
146− -> (a -> IO ())
147− -> (a -> IO (H.Status
148− ,H.ResponseHeaders
149− ,C.Source IO (C.Flush Builder)))
150− -> IO Response
151−responseSourceBracket setup teardown action =
152− bracketOnError setup teardown $ \resource -> do
153− (st,hdr,src) <- action resource
154− return $ ResponseSource st hdr $ \f ->
155− bracket (return resource) teardown (\_ -> f src)
133+responseStream :: H.Status
134+ -> H.ResponseHeaders
135+ -> StreamingBody
136+ -> Response
137+responseStream = ResponseStream
156138
157139-- | Create a response for a raw application. This is useful for \"upgrade\"
158140-- situations such as WebSockets, where an application requests for the server
@@ -165,10 +147,10 @@ responseSourceBracket setup teardown action =
165147-- @responseRaw@, behavior is undefined.
166148--
167149-- Since 2.1.0
168−responseRaw :: (C.Source IO B.ByteString -> C.Sink B.ByteString IO () -> IO ())
150+responseRaw :: (IO B.ByteString -> (B.ByteString -> IO ()) -> IO ())
169151 -> Response
170152 -> Response
171−responseRaw rawApp fallback = ResponseRaw ($ rawApp) fallback
153+responseRaw = ResponseRaw
172154
173155----------------------------------------------------------------
174156
@@ -176,36 +158,55 @@ responseRaw rawApp fallback = ResponseRaw ($ rawApp) fallback
176158responseStatus :: Response -> H.Status
177159responseStatus (ResponseFile s _ _ _) = s
178160responseStatus (ResponseBuilder s _ _ ) = s
179−responseStatus (ResponseSource s _ _ ) = s
161+responseStatus (ResponseStream s _ _ ) = s
180162responseStatus (ResponseRaw _ res ) = responseStatus res
181163
182164-- | Accessing 'H.ResponseHeaders' in 'Response'.
183165responseHeaders :: Response -> H.ResponseHeaders
184166responseHeaders (ResponseFile _ hs _ _) = hs
185167responseHeaders (ResponseBuilder _ hs _ ) = hs
186−responseHeaders (ResponseSource _ hs _ ) = hs
168+responseHeaders (ResponseStream _ hs _ ) = hs
187169responseHeaders (ResponseRaw _ res) = responseHeaders res
188170
189171-- | Converting the body information in 'Response' to 'Source'.
190−responseToSource :: Response
191− -> (H.Status, H.ResponseHeaders, WithSource IO (C.Flush Builder) b)
192−responseToSource (ResponseSource s h b) = (s, h, b)
193−responseToSource (ResponseFile s h fp (Just part)) =
194− (s, h, \f -> IO.withFile fp IO.ReadMode $ \handle -> f $ sourceFilePart handle part C.$= CL.map (C.Chunk . fromByteString))
195−responseToSource (ResponseFile s h fp Nothing) =
196− (s, h, \f -> IO.withFile fp IO.ReadMode $ \handle -> f $ CB.sourceHandle handle C.$= CL.map (C.Chunk . fromByteString))
197−responseToSource (ResponseBuilder s h b) =
198− (s, h, ($ CL.sourceList [C.Chunk b]))
199−responseToSource (ResponseRaw _ res) = responseToSource res
200−
201−sourceFilePart :: IO.Handle -> FilePart -> C.Source IO B.ByteString
202−sourceFilePart handle (FilePart offset count _) =
203− CB.sourceHandleRange handle (Just offset) (Just count)
172+responseToStream :: Response
173+ -> ( H.Status
174+ , H.ResponseHeaders
175+ , (StreamingBody -> IO a) -> IO a
176+ )
177+responseToStream (ResponseStream s h b) = (s, h, ($ b))
178+responseToStream (ResponseFile s h fp (Just part)) =
179+ ( s
180+ , h
181+ , \withBody -> IO.withBinaryFile fp IO.ReadMode $ \handle -> withBody $ \sendChunk _flush -> do
182+ IO.hSeek handle IO.AbsoluteSeek $ filePartOffset part
183+ let loop remaining | remaining <= 0 = return ()
184+ loop remaining = do
185+ bs <- B.hGetSome handle defaultChunkSize
186+ unless (B.null bs) $ do
187+ let x = B.take remaining bs
188+ sendChunk $ fromByteString x
189+ loop $ remaining - B.length x
190+ loop $ fromIntegral $ filePartByteCount part
191+ )
192+responseToStream (ResponseFile s h fp Nothing) =
193+ ( s
194+ , h
195+ , \withBody -> IO.withBinaryFile fp IO.ReadMode $ \handle ->
196+ withBody $ \sendChunk _flush -> fix $ \loop -> do
197+ bs <- B.hGetSome handle defaultChunkSize
198+ unless (B.null bs) $ do
199+ sendChunk $ fromByteString bs
200+ loop
201+ )
202+responseToStream (ResponseBuilder s h b) =
203+ (s, h, \withBody -> withBody $ \sendChunk _flush -> sendChunk b)
204+responseToStream (ResponseRaw _ res) = responseToStream res
204205
205206----------------------------------------------------------------
206207
207208-- | The WAI application.
208−type Application = Request -> IO Response
209+type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
209210
210211-- | Middleware is a component that sits between the server and application. It
211212-- can do such tasks as GZIP encoding or response caching. What follows is the
@@ -237,7 +238,7 @@ defaultRequest = Request
237238 , remoteHost = SockAddrInet 0 0
238239 , pathInfo = []
239240 , queryString = []
240− , requestBody = return ()
241+ , requestBody = return B.empty
241242 , vault = mempty
242243 , requestBodyLength = KnownLength 0
243244 , requestHeaderHost = Nothing
@@ -249,4 +250,13 @@ defaultRequest = Request
249250--
250251-- Since 1.4.1
251252lazyRequestBody :: Request -> IO L.ByteString
252−lazyRequestBody = fmap L.fromChunks . lazyConsume . requestBody
253+lazyRequestBody req =
254+ loop
255+ where
256+ loop = unsafeInterleaveIO $ do
257+ bs <- requestBody req
258+ if B.null bs
259+ then return LI.Empty
260+ else do
261+ bss <- loop
262+ return $ LI.Chunk bs bss
wai/Network/Wai/Internal.hs+23−10
@@ -8,7 +8,6 @@ module Network.Wai.Internal where
88
99import Blaze.ByteString.Builder (Builder)
1010import qualified Data.ByteString as B
11−import qualified Data.Conduit as C
1211import Data.Text (Text)
1312import Data.Typeable (Typeable)
1413import Data.Vault.Lazy (Vault)
@@ -51,8 +50,9 @@ data Request = Request {
5150 , pathInfo :: [Text]
5251 -- | Parsed query string information
5352 , queryString :: H.Query
54− -- | A request body provided as 'Source'.
55− , requestBody :: C.Source IO B.ByteString
53+ -- | Get the next chunk of the body. Returns an empty bytestring when the
54+ -- body is fully consumed.
55+ , requestBody :: IO B.ByteString
5656 -- | A location for arbitrary data to be shared by applications and middleware.
5757 , vault :: Vault
5858 -- | The size of the request body. In the case of a chunked request body,
@@ -84,15 +84,17 @@ data Request = Request {
8484data Response
8585 = ResponseFile H.Status H.ResponseHeaders FilePath (Maybe FilePart)
8686 | ResponseBuilder H.Status H.ResponseHeaders Builder
87− | ResponseSource H.Status H.ResponseHeaders (forall b. WithSource IO (C.Flush Builder) b)
88− | ResponseRaw (forall b. WithRawApp b) Response
87+ | ResponseStream H.Status H.ResponseHeaders StreamingBody
88+ | ResponseRaw (IO B.ByteString -> (B.ByteString -> IO ()) -> IO ()) Response
8989 deriving Typeable
9090
91−type RawApp = C.Source IO B.ByteString -> C.Sink B.ByteString IO () -> IO ()
92−type WithRawApp b = (RawApp -> IO b) -> IO b
93−
94−-- | Auxiliary type for 'ResponseSource'.
95−type WithSource m a b = (C.Source m a -> m b) -> m b
91+-- | Represents a streaming HTTP response body. It's a function of two
92+-- parameters; the first parameter provides a means of sending another chunk of
93+-- data, and the second parameter provides a means of flushing the data to the
94+-- client.
95+--
96+-- Since 3.0.0
97+type StreamingBody = (Builder -> IO ()) -> IO () -> IO ()
9698
9799-- | The size of the request body. In the case of chunked bodies, the size will
98100-- not be known.
@@ -108,3 +110,14 @@ data FilePart = FilePart
108110 , filePartByteCount :: Integer
109111 , filePartFileSize :: Integer
110112 } deriving Show
113+
114+-- | A special datatype to indicate that the WAI handler has received the
115+-- response. This is to avoid the need for Rank2Types in the definition of
116+-- Application.
117+--
118+-- It is /highly/ advised that only WAI handlers import and use the data
119+-- constructor for this data type.
120+--
121+-- Since 3.0.0
122+data ResponseReceived = ResponseReceived
123+ deriving Typeable
wai/test/Network/WaiSpec.hsnew file+73
@@ -0,0 +1,73 @@
1+module Network.WaiSpec (spec) where
2+
3+import Test.Hspec
4+import Test.Hspec.QuickCheck (prop)
5+import Network.Wai
6+import Network.Wai.Internal (Request (Request))
7+import Data.IORef
8+import Data.Monoid
9+import qualified Data.ByteString as S
10+import qualified Data.ByteString.Lazy as L
11+import Blaze.ByteString.Builder (toByteString, Builder, fromWord8)
12+import Control.Monad (forM_)
13+
14+spec :: Spec
15+spec = do
16+ describe "responseToStream" $ do
17+ let getBody res = do
18+ let (_, _, f) = responseToStream res
19+ f $ \streamingBody -> do
20+ builderRef <- newIORef mempty
21+ let add :: Builder -> IO ()
22+ add b = atomicModifyIORef builderRef $ \builder ->
23+ (builder `mappend` b, ())
24+ flush :: IO ()
25+ flush = return ()
26+ streamingBody add flush
27+ fmap toByteString $ readIORef builderRef
28+ prop "responseLBS" $ \bytes -> do
29+ body <- getBody $ responseLBS undefined undefined $ L.pack bytes
30+ body `shouldBe` S.pack bytes
31+ prop "responseBuilder" $ \bytes -> do
32+ body <- getBody $ responseBuilder undefined undefined
33+ $ mconcat $ map fromWord8 bytes
34+ body `shouldBe` S.pack bytes
35+ prop "responseStream" $ \chunks -> do
36+ body <- getBody $ responseStream undefined undefined $ \sendChunk _ ->
37+ forM_ chunks $ \chunk -> sendChunk $ mconcat $ map fromWord8 chunk
38+ body `shouldBe` S.concat (map S.pack chunks)
39+ it "responseFile total" $ do
40+ let fp = "wai.cabal"
41+ body <- getBody $ responseFile undefined undefined fp Nothing
42+ expected <- S.readFile fp
43+ body `shouldBe` expected
44+ prop "responseFile partial" $ \offset' count' -> do
45+ let fp = "wai.cabal"
46+ totalBS <- S.readFile fp
47+ let total = S.length totalBS
48+ offset = abs offset' `mod` total
49+ count = abs count' `mod` (total - offset)
50+ body <- getBody $ responseFile undefined undefined fp $ Just FilePart
51+ { filePartOffset = fromIntegral offset
52+ , filePartByteCount = fromIntegral count
53+ , filePartFileSize = fromIntegral total
54+ }
55+ let expected = S.take count $ S.drop offset totalBS
56+ body `shouldBe` expected
57+ describe "lazyRequestBody" $ do
58+ prop "works" $ \chunks -> do
59+ ref <- newIORef $ map S.pack $ filter (not . null) chunks
60+ let req = Request
61+ { requestBody = atomicModifyIORef ref $ \bss ->
62+ case bss of
63+ [] -> ([], S.empty)
64+ x:y -> (y, x)
65+ }
66+ body <- lazyRequestBody req
67+ body `shouldBe` L.fromChunks (map S.pack chunks)
68+ it "is lazy" $ do
69+ let req = Request
70+ { requestBody = error "requestBody"
71+ }
72+ _ <- lazyRequestBody req
73+ return ()
wai/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
wai/wai.cabal+19−5
@@ -1,5 +1,5 @@
11Name: wai
2−Version: 2.1.0.3
2+Version: 3.0.0
33Synopsis: Web Application Interface.
44Description: Provides a common protocol for communication between web applications and web servers.
55License: MIT
@@ -9,7 +9,7 @@ Maintainer: michael@snoyman.com
99Homepage: https://github.com/yesodweb/wai
1010Category: Web
1111Build-Type: Simple
12−Cabal-Version: >=1.6
12+Cabal-Version: >=1.8
1313Stability: Stable
1414
1515Source-repository head
@@ -20,13 +20,27 @@ Library
2020 Build-Depends: base >= 4 && < 5
2121 , bytestring >= 0.9.1.4
2222 , blaze-builder >= 0.2.1.4 && < 0.4
23− , conduit >= 1.0.8 && < 1.2
24− , conduit-extra >= 1.0
2523 , network >= 2.2.1.5
2624 , http-types >= 0.7
2725 , text >= 0.7
28− , transformers >= 0.2.2
2926 , vault >= 0.3 && < 0.4
3027 Exposed-modules: Network.Wai
3128 Network.Wai.Internal
3229 ghc-options: -Wall
30+
31+test-suite test
32+ hs-source-dirs: test
33+ main-is: Spec.hs
34+ type: exitcode-stdio-1.0
35+ ghc-options: -threaded
36+ cpp-options: -DTEST
37+ build-depends: base
38+ , wai
39+ , hspec
40+ , blaze-builder
41+ , bytestring
42+ other-modules: Network.WaiSpec
43+
44+source-repository head
45+ type: git
46+ location: git://github.com/yesodweb/wai.git
warp-tls/Network/Wai/Handler/WarpTLS.hs+48−39
@@ -1,4 +1,3 @@
1−{-# LANGUAGE CPP #-}
21{-# LANGUAGE OverloadedStrings #-}
32{-# LANGUAGE RankNTypes #-}
43{-# LANGUAGE RecordWildCards #-}
@@ -27,32 +26,24 @@ module Network.Wai.Handler.WarpTLS (
2726
2827import qualified Network.TLS as TLS
2928import Network.Wai.Handler.Warp
30−import Network.Wai
31−import Network.Socket
29+import Network.Wai (Application)
30+import Network.Socket (Socket, sClose, withSocketsDo)
3231import qualified Data.ByteString.Lazy as L
33−import Data.Conduit.Binary (sourceFileRange)
34−import Control.Monad.Trans.Resource (runResourceT)
35−import qualified Data.Conduit as C
36−import qualified Data.Conduit.List as CL
3732import Control.Exception (bracket, finally, handle)
3833import qualified Network.TLS.Extra as TLSExtra
3934import qualified Data.ByteString as B
40−#if MIN_VERSION_conduit(1,1,0)
41−import Data.Streaming.Network (bindPortTCP, acceptSafe)
42−#else
43−import Data.Conduit.Network (bindPort, acceptSafe)
44−#define bindPortTCP bindPort
45−#endif
35+import Data.Streaming.Network (bindPortTCP, acceptSafe, safeRecv)
4636import Control.Applicative ((<$>))
47−import Data.Conduit.Network (sourceSocket, sinkSocket)
48−import Data.Maybe (fromMaybe)
4937import qualified Data.IORef as I
5038import Control.Exception (Exception, throwIO)
5139import Data.Typeable (Typeable)
52−import Data.Default.Class
53−import qualified Data.Conduit.Binary as CB
40+import Data.Default.Class (def)
5441import qualified Crypto.Random.AESCtr
55−import Network.Wai.Handler.Warp.Buffer
42+import Network.Wai.Handler.Warp.Buffer (allocateBuffer, bufferSize, freeBuffer)
43+import Network.Socket.ByteString (sendAll)
44+import Control.Monad (unless)
45+import Data.ByteString.Lazy.Internal (defaultChunkSize)
46+import qualified System.IO as IO
5647
5748data TLSSettings = TLSSettings
5849 { certFile :: FilePath
@@ -127,24 +118,35 @@ runTLSSocket TLSSettings {..} set sock app = do
127118 where
128119 getter params = do
129120 (s, sa) <- acceptSafe sock
130− let mkConn = do
131− (fromClient, firstBS) <- sourceSocket s C.$$+ CL.peek
132− let toClient = sinkSocket s
133− ifromClient <- I.newIORef fromClient
134− let getNext sink = do
135− fromClient' <- I.readIORef ifromClient
136− (fromClient'', bs) <- fromClient' C.$$++ sink
137− I.writeIORef ifromClient fromClient''
138− return bs
139− if maybe False ((== 0x16) . fst) (firstBS >>= B.uncons)
121+ let mkConn :: IO (Connection, Bool)
122+ mkConn = do
123+ firstBS <- safeRecv s 4096
124+ cachedRef <- I.newIORef firstBS
125+ let getNext size = do
126+ cached <- I.readIORef cachedRef
127+ loop cached
128+ where
129+ loop bs | B.length bs >= size = do
130+ let (x, y) = B.splitAt size bs
131+ I.writeIORef cachedRef y
132+ return x
133+ loop bs1 = do
134+ bs2 <- safeRecv s 4096
135+ if B.null bs2
136+ then do
137+ -- FIXME does this deserve an exception being thrown?
138+ I.writeIORef cachedRef B.empty
139+ return bs1
140+ else loop $ B.append bs1 bs2
141+ if not (B.null firstBS) && B.head firstBS == 0x16
140142 then do
141143 gen <- Crypto.Random.AESCtr.makeSystem
142144 ctx <- TLS.contextNew
143145 TLS.Backend
144146 { TLS.backendFlush = return ()
145147 , TLS.backendClose = sClose s
146− , TLS.backendSend = \bs -> C.yield bs C.$$ toClient
147− , TLS.backendRecv = getNext . fmap (B.concat . L.toChunks) . CB.take
148+ , TLS.backendSend = sendAll s
149+ , TLS.backendRecv = getNext
148150 }
149151 params
150152 gen
@@ -157,7 +159,16 @@ runTLSSocket TLSSettings {..} set sock app = do
157159 , connSendAll = TLS.sendData ctx . L.fromChunks . return
158160 , connSendFile = \fp offset len _th headers -> do
159161 TLS.sendData ctx $ L.fromChunks headers
160− runResourceT $ sourceFileRange fp (Just offset) (Just len) C.$$ CL.mapM_ (TLS.sendData ctx . L.fromChunks . return)
162+ IO.withBinaryFile fp IO.ReadMode $ \h -> do
163+ IO.hSeek h IO.AbsoluteSeek offset
164+ let loop remaining | remaining <= 0 = return ()
165+ loop remaining = do
166+ bs <- B.hGetSome h defaultChunkSize
167+ unless (B.null bs) $ do
168+ let x = B.take remaining bs
169+ TLS.sendData ctx $ L.fromChunks [x]
170+ loop $ remaining - B.length x
171+ loop $ fromIntegral len
161172 , connClose =
162173 freeBuffer readBuf `finally`
163174 freeBuffer writeBuf `finally`
@@ -166,12 +177,12 @@ runTLSSocket TLSSettings {..} set sock app = do
166177 , connRecv =
167178 let onEOF TLS.Error_EOF = return B.empty
168179 onEOF e = throwIO e
169− go = handle onEOF $ do
180+ go = do
170181 x <- TLS.recvData ctx
171182 if B.null x
172183 then go
173184 else return x
174− in go
185+ in handle onEOF go
175186 , connSendFileOverride = NotOverride
176187 , connReadBuffer = readBuf
177188 , connWriteBuffer = writeBuf
@@ -183,13 +194,11 @@ runTLSSocket TLSSettings {..} set sock app = do
183194 AllowInsecure -> do
184195 conn' <- socketConnection s
185196 return (conn'
186− { connRecv = getNext $ fmap (fromMaybe B.empty) C.await
197+ { connRecv = getNext 4096
187198 }, False)
188199 DenyInsecure lbs -> do
189− let src = do
190− C.yield "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"
191− mapM_ C.yield $ L.toChunks lbs
192− src C.$$ sinkSocket s
200+ sendAll s "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"
201+ mapM_ (sendAll s) $ L.toChunks lbs
193202 sClose s
194203 throwIO InsecureConnectionDenied
195204 return (mkConn, sa)
@@ -202,7 +211,7 @@ instance Exception WarpTLSException
202211runTLS :: TLSSettings -> Settings -> Application -> IO ()
203212runTLS tset set app = withSocketsDo $
204213 bracket
205− (bindPortTCP (settingsPort set) (settingsHost set))
214+ (bindPortTCP (getPort set) (getHost set))
206215 sClose
207216 (\sock -> runTLSSocket tset set sock app)
208217
warp-tls/pong.hs+7−7
@@ -1,6 +1,6 @@
11{-# LANGUAGE OverloadedStrings #-}
22import Network.Wai
3−import Network.Wai.Handler.Warp (defaultSettings, settingsPort)
3+import Network.Wai.Handler.Warp (defaultSettings, setPort)
44import Network.Wai.Handler.WarpTLS
55import Network.HTTP.Types (status200)
66import Blaze.ByteString.Builder (copyByteString)
@@ -15,9 +15,9 @@ main = do
1515 --manager <- newManager def
1616 runTLS (tlsSettings "certificate.pem" "key.pem")
1717 { onInsecure = AllowInsecure
18− } defaultSettings { settingsPort = 3009 } app
18+ } (setPort 3009 defaultSettings) app
1919
20−app req = return $
20+app req f = f $
2121 case rawPathInfo req of
2222 "/builder/withlen" -> builderWithLen
2323 "/builder/nolen" -> builderNoLen
@@ -41,18 +41,18 @@ builderNoLen = responseBuilder
4141 ]
4242 $ copyByteString "PONG"
4343
44−sourceWithLen = responseSource
44+sourceWithLen = responseStream
4545 status200
4646 [ ("Content-Type", "text/plain")
4747 , ("Content-Length", "4")
4848 ]
49− $ CL.sourceList [C.Chunk $ copyByteString "PONG"]
49+ $ \sendChunk _flush -> sendChunk $ copyByteString "PONG"
5050
51−sourceNoLen = responseSource
51+sourceNoLen = responseStream
5252 status200
5353 [ ("Content-Type", "text/plain")
5454 ]
55− $ CL.sourceList [C.Chunk $ copyByteString "PONG"]
55+ $ \sendChunk _flush -> sendChunk $ copyByteString "PONG"
5656
5757fileWithLen = responseFile
5858 status200
warp-tls/pong.txt+1−1
@@ -1 +1 @@
1−PONG
1+PONG
warp-tls/warp-tls.cabal+3−8
@@ -1,5 +1,5 @@
11Name: warp-tls
2−Version: 2.0.5
2+Version: 3.0.0
33Synopsis: HTTP over SSL/TLS support for Warp via the TLS package
44License: MIT
55License-file: LICENSE
@@ -15,18 +15,13 @@ Description: HTTP over SSL/TLS support for Warp via the TLS package.
1515Library
1616 Build-Depends: base >= 4 && < 5
1717 , bytestring >= 0.9
18− , wai >= 2.0 && < 2.2
19− , warp >= 2.1.5 && < 2.2
20− , transformers >= 0.2
21− , conduit >= 0.5 && < 1.2
22− , conduit-extra
23− , network-conduit >= 0.6 && < 1.2
18+ , wai >= 3.0 && < 3.1
19+ , warp >= 3.0 && < 3.1
2420 , data-default-class >= 0.0.1
2521 , tls >= 1.2.1
2622 , network >= 2.2.1
2723 , cprng-aes >= 0.5.0
2824 , streaming-commons
29− , resourcet
3025 Exposed-modules: Network.Wai.Handler.WarpTLS
3126 ghc-options: -Wall
3227
warp/Network/Wai/Handler/Warp.hs+1−14
@@ -35,7 +35,6 @@ module Network.Wai.Handler.Warp (
3535 , setOnOpen
3636 , setOnClose
3737 , setTimeout
38− , setIntercept
3938 , setManager
4039 , setFdCacheDuration
4140 , setBeforeMainLoop
@@ -52,7 +51,6 @@ module Network.Wai.Handler.Warp (
5251 , settingsOnOpen
5352 , settingsOnClose
5453 , settingsTimeout
55− , settingsIntercept
5654 , settingsManager
5755 , settingsFdCacheDuration
5856 , settingsBeforeMainLoop
@@ -87,7 +85,6 @@ module Network.Wai.Handler.Warp (
8785 , sendResponse
8886 ) where
8987
90−import Data.Conduit.Network (HostPreference(..))
9188import Network.Wai.Handler.Warp.Date
9289import Network.Wai.Handler.Warp.FdCache
9390import Network.Wai.Handler.Warp.Header
@@ -100,8 +97,8 @@ import Network.Wai.Handler.Warp.Types
10097import Control.Exception (SomeException)
10198import Network.Wai (Request, Response)
10299import Network.Socket (SockAddr)
103−import Data.Conduit (Source)
104100import Data.ByteString (ByteString)
101+import Data.Streaming.Network (HostPreference)
105102
106103-- | Port to listen on. Default value: 3000
107104--
@@ -151,16 +148,6 @@ setOnClose x y = y { settingsOnClose = x }
151148setTimeout :: Int -> Settings -> Settings
152149setTimeout x y = y { settingsTimeout = x }
153150
154−-- | Register some intercept handler to deal with specific requests. Prime use
155−-- case: websockets.
156−--
157−-- Default: always return @Nothing@.
158−--
159−-- Since 2.1.0
160−setIntercept :: (Request -> IO (Maybe (Source IO ByteString -> Connection -> IO ())))
161− -> Settings -> Settings
162−setIntercept x y = y { settingsIntercept = x }
163−
164151-- | Use an existing timeout manager instead of spawning a new one. If used,
165152-- 'settingsTimeout' is ignored.
166153--
warp/Network/Wai/Handler/Warp/Conduit.hs+98−84
@@ -2,17 +2,12 @@ module Network.Wai.Handler.Warp.Conduit where
22
33import Control.Applicative
44import Control.Exception
5−import Control.Monad (unless)
6−import Control.Monad.IO.Class (MonadIO, liftIO)
7−import Control.Monad.Trans.Class (lift)
5+import Control.Monad (when, unless)
86import Data.ByteString (ByteString)
97import Data.ByteString.Lazy.Char8 (pack)
108import qualified Data.ByteString as S
9+import qualified Data.ByteString.Char8 as S8
1110import qualified Data.ByteString.Lazy as L
12−import Data.Conduit
13−import qualified Data.Conduit.Binary as CB
14−import Data.Conduit.Internal (ResumableSource (..))
15−import qualified Data.Conduit.List as CL
1611import qualified Data.IORef as I
1712import Data.Word (Word, Word8)
1813import Network.Wai.Handler.Warp.Types
@@ -20,22 +15,29 @@ import Network.Wai.Handler.Warp.Types
2015----------------------------------------------------------------
2116
2217-- | Contains a @Source@ and a byte count that is still to be read in.
23−newtype IsolatedBSSource = IsolatedBSSource (I.IORef (Int, ResumableSource IO ByteString))
18+data ISource = ISource !Source !(I.IORef Int)
19+
20+mkISource :: Source -> Int -> IO ISource
21+mkISource src cnt = do
22+ ref <- I.newIORef cnt
23+ return $! ISource src ref
2424
2525-- | Given an @IsolatedBSSource@ provide a @Source@ that only allows up to the
2626-- specified number of bytes to be passed downstream. All leftovers should be
2727-- retained within the @Source@. If there are not enough bytes available,
2828-- throws a @ConnectionClosedByPeer@ exception.
29−ibsIsolate :: IsolatedBSSource -> Source IO ByteString
30−ibsIsolate ibs@(IsolatedBSSource ref) = do
31− (count, src) <- liftIO $ I.readIORef ref
32− unless (count == 0) $ do
33− -- Get the next chunk (if available) and the updated source
34− (src', mbs) <- lift $ src $$++ CL.head
29+readISource :: ISource -> IO ByteString
30+readISource (ISource src ref) = do
31+ count <- I.readIORef ref
32+ if count == 0
33+ then return S.empty
34+ else do
35+
36+ bs <- readSource src
3537
3638 -- If no chunk available, then there aren't enough bytes in the
3739 -- stream. Throw a ConnectionClosedByPeer
38− bs <- maybe (liftIO $ throwIO ConnectionClosedByPeer) return mbs
40+ when (S.null bs) $ throwIO ConnectionClosedByPeer
3941
4042 let -- How many of the bytes in this chunk to send downstream
4143 toSend = min count (S.length bs)
@@ -48,16 +50,8 @@ ibsIsolate ibs@(IsolatedBSSource ref) = do
4850 -- downstream, and then loop on this function for the
4951 -- next chunk.
5052 | count' > 0 -> do
51− liftIO $ I.writeIORef ref (count', src')
52− yield bs
53− ibsIsolate ibs
54−
55− -- The expected count is the total size of the chunk we
56− -- just read. Send this chunk downstream, and then
57− -- terminate the stream.
58− | count == S.length bs -> do
59− liftIO $ I.writeIORef ref (count', src')
60− yield bs
53+ I.writeIORef ref count'
54+ return bs
6155
6256 -- Some of the bytes in this chunk should not be sent
6357 -- downstream. Split up the chunk into the sent and
@@ -65,84 +59,109 @@ ibsIsolate ibs@(IsolatedBSSource ref) = do
6559 -- source, and send the rest of the chunk downstream.
6660 | otherwise -> do
6761 let (x, y) = S.splitAt toSend bs
68− liftIO $ I.writeIORef ref (count', fmapResume (yield y >>) src')
69− yield x
62+ leftoverSource src y
63+ assert (count' == 0) $ I.writeIORef ref count'
64+ return x
7065
7166-- | Extract the underlying @Source@ from an @IsolatedBSSource@, which will not
7267-- perform any more isolation.
73−ibsDone :: IsolatedBSSource -> IO (ResumableSource IO ByteString)
74−ibsDone (IsolatedBSSource ref) = snd <$> I.readIORef ref
68+isourceDone :: ISource -> Source
69+isourceDone (ISource src _) = src
7570
7671----------------------------------------------------------------
7772
73+data CSource = CSource !Source !(I.IORef ChunkState)
74+
7875data ChunkState = NeedLen
7976 | NeedLenNewline
8077 | HaveLen Word
78+ | DoneChunking
79+ deriving Show
8180
8281bsCRLF :: L.ByteString
8382bsCRLF = pack "\r\n"
8483
85−chunkedSource :: MonadIO m
86− => I.IORef (ResumableSource m ByteString, ChunkState)
87− -> Source m ByteString
88−chunkedSource ipair = do
89− (src, mlen) <- liftIO $ I.readIORef ipair
90− go src mlen
84+mkCSource :: Source -> IO CSource
85+mkCSource src = do
86+ ref <- I.newIORef NeedLen
87+ return $! CSource src ref
88+
89+readCSource :: CSource -> IO ByteString
90+readCSource (CSource src ref) = do
91+ mlen <- I.readIORef ref
92+ go mlen
9193 where
92− go' src front = do
93− (src', (len, bs)) <- lift $ src $$++ front getLen
94− let src''
95− | S.null bs = src'
96− | otherwise = fmapResume (yield bs >>) src'
97− go src'' $ HaveLen len
98−
99− go src NeedLen = go' src id
100− go src NeedLenNewline = go' src (CB.take 2 >>)
101− go src (HaveLen 0) = do
94+ withLen 0 bs = do
95+ leftoverSource src bs
96+ dropCRLF
97+ yield' S.empty DoneChunking
98+ withLen len bs
99+ | S.null bs = do
100+ -- FIXME should this throw an exception if len > 0?
101+ I.writeIORef ref DoneChunking
102+ return S.empty
103+ | otherwise =
104+ case S.length bs `compare` fromIntegral len of
105+ EQ -> yield' bs NeedLenNewline
106+ LT -> yield' bs $ HaveLen $ len - fromIntegral (S.length bs)
107+ GT -> do
108+ let (x, y) = S.splitAt (fromIntegral len) bs
109+ leftoverSource src y
110+ yield' x NeedLenNewline
111+
112+ yield' bs mlen = do
113+ I.writeIORef ref mlen
114+ return bs
115+
116+ dropCRLF = do
117+ bs <- readSource src
118+ case S.uncons bs of
119+ Nothing -> return ()
120+ Just (13, bs') -> dropLF bs'
121+ Just (10, bs') -> leftoverSource src bs'
122+ Just _ -> leftoverSource src bs
123+
124+ dropLF bs =
125+ case S.uncons bs of
126+ Nothing -> do
127+ bs2 <- readSource' src
128+ unless (S.null bs2) $ dropLF bs2
129+ Just (10, bs') -> leftoverSource src bs'
130+ Just _ -> leftoverSource src bs
131+
132+ go NeedLen = getLen
133+ go NeedLenNewline = dropCRLF >> getLen
134+ go (HaveLen 0) = do
102135 -- Drop the final CRLF
103− (src', ()) <- lift $ src $$++ do
104− crlf <- CB.take 2
105− unless (crlf == bsCRLF) $ leftover $ S.concat $ L.toChunks crlf
106− liftIO $ I.writeIORef ipair (src', HaveLen 0)
107− go src (HaveLen len) = do
108− (src', mbs) <- lift $ src $$++ CL.head
109− case mbs of
110− Nothing -> liftIO $ I.writeIORef ipair (src', HaveLen 0)
111− Just bs ->
112− case S.length bs `compare` fromIntegral len of
113− EQ -> yield' src' NeedLenNewline bs
114− LT -> do
115− let mlen = HaveLen $ len - fromIntegral (S.length bs)
116− yield' src' mlen bs
117− GT -> do
118− let (x, y) = S.splitAt (fromIntegral len) bs
119− let src'' = fmapResume (yield y >>) src'
120− yield' src'' NeedLenNewline x
121−
122− yield' src mlen bs = do
123− liftIO $ I.writeIORef ipair (src, mlen)
124− yield bs
125− go src mlen
126−
127− getLen :: Monad m => Sink ByteString m (Word, ByteString)
136+ dropCRLF
137+ I.writeIORef ref DoneChunking
138+ return S.empty
139+ go (HaveLen len) = do
140+ bs <- readSource src
141+ withLen len bs
142+ go DoneChunking = return S.empty
143+
144+ -- Get the length from the source, and then pass off control to withLen
128145 getLen = do
129− mbs <- CL.head
130− case mbs of
131− Nothing -> return (0, S.empty)
132− Just bs -> do
146+ bs <- readSource src
147+ if S.null bs
148+ then do
149+ I.writeIORef ref $ assert False $ HaveLen 0
150+ return S.empty
151+ else do
133152 (x, y) <-
134153 case S.breakByte 10 bs of
135154 (x, y)
136155 | S.null y -> do
137− mbs2 <- CL.head
138− case mbs2 of
139− Nothing -> return (x, y)
140− Just bs2 -> return $ S.breakByte 10 $ bs `S.append` bs2
156+ bs2 <- readSource' src
157+ if S.null bs2
158+ then return (x, y)
159+ else return $ S.breakByte 10 $ bs `S.append` bs2
141160 | otherwise -> return (x, y)
142161 let w =
143162 S.foldl' (\i c -> i * 16 + fromIntegral (hexToWord c)) 0
144163 $ S.takeWhile isHexDigit x
145− return (w, S.drop 1 y)
164+ withLen w $ S.drop 1 y
146165
147166 hexToWord w
148167 | w < 58 = w - 48
@@ -153,8 +172,3 @@ isHexDigit :: Word8 -> Bool
153172isHexDigit w = w >= 48 && w <= 57
154173 || w >= 65 && w <= 70
155174 || w >= 97 && w <= 102
156−
157−----------------------------------------------------------------
158−
159−fmapResume :: (Source m o1 -> Source m o2) -> ResumableSource m o1 -> ResumableSource m o2
160−fmapResume f (ResumableSource src m) = ResumableSource (f src) m
warp/Network/Wai/Handler/Warp/Request.hs+65−50
@@ -10,14 +10,12 @@ module Network.Wai.Handler.Warp.Request (
1010
1111import Control.Applicative
1212import qualified Control.Concurrent as Conc (yield)
13−import Control.Exception.Lifted (throwIO)
14−import Control.Monad.IO.Class (liftIO)
13+import Control.Exception (throwIO)
1514import Data.Array ((!))
1615import Data.ByteString (ByteString)
1716import qualified Data.ByteString as S
1817import qualified Data.ByteString.Unsafe as SU
1918import qualified Data.CaseInsensitive as CI
20−import Data.Conduit
2119import qualified Data.IORef as I
2220import Data.Monoid (mempty)
2321import qualified Network.HTTP.Types as H
@@ -32,6 +30,7 @@ import qualified Network.Wai.Handler.Warp.Timeout as Timeout
3230import Network.Wai.Handler.Warp.Types
3331import Network.Wai.Internal
3432import Prelude hiding (lines)
33+import Control.Monad (when)
3534
3635----------------------------------------------------------------
3736
@@ -47,25 +46,22 @@ recvRequest :: Settings
4746 -> Connection
4847 -> InternalInfo
4948 -> SockAddr -- ^ Peer's address.
50− -> Source IO ByteString -- ^ Where HTTP request comes from.
49+ -> Source -- ^ Where HTTP request comes from.
5150 -> IO (Request
52− ,IndexedHeader
53− ,IO (ResumableSource IO ByteString)
54− ,Maybe ByteString) -- ^
51+ ,IndexedHeader) -- ^
5552 -- 'Request' passed to 'Application',
5653 -- 'IndexedHeader' of HTTP request for internal use,
57− -- leftover source (i.e. body and other HTTP reqeusts in HTTP pipelining),
58− -- leftovers from request header parsing (used for raw responses)
5954
60−recvRequest settings conn ii addr src0 = do
61− (src, (leftover', hdrlines)) <- src0 $$+ headerLines
55+recvRequest settings conn ii addr src = do
56+ hdrlines <- headerLines src
6257 (method, unparsedPath, path, query, httpversion, hdr) <- parseHeaderLines hdrlines
6358 let idxhdr = indexRequestHeader hdr
6459 expect = idxhdr ! idxExpect
6560 cl = idxhdr ! idxContentLength
6661 te = idxhdr ! idxTransferEncoding
67− liftIO $ handleExpect conn httpversion expect
68− (rbody, bodyLength, getSource) <- bodyAndSource src cl te
62+ handleExpect conn httpversion expect
63+ (rbody, bodyLength) <- bodyAndSource src cl te
64+ rbody' <- timeoutBody th rbody
6965 let req = Request {
7066 requestMethod = method
7167 , httpVersion = httpversion
@@ -76,21 +72,24 @@ recvRequest settings conn ii addr src0 = do
7672 , requestHeaders = hdr
7773 , isSecure = False
7874 , remoteHost = addr
79− , requestBody = timeoutBody th rbody
75+ , requestBody = rbody'
8076 , vault = mempty
8177 , requestBodyLength = bodyLength
8278 , requestHeaderHost = idxhdr ! idxHost
8379 , requestHeaderRange = idxhdr ! idxRange
8480 }
85− return (req, idxhdr, getSource, leftover')
81+ return (req, idxhdr)
8682 where
8783 th = threadHandle ii
8884
8985----------------------------------------------------------------
9086
91−headerLines :: Sink ByteString IO (Maybe ByteString, [ByteString])
92−headerLines =
93− await >>= maybe (throwIO (NotEnoughLines [])) (push (THStatus 0 id id))
87+headerLines :: Source -> IO [ByteString]
88+headerLines src = do
89+ bs <- readSource src
90+ if S.null bs
91+ then throwIO $ NotEnoughLines []
92+ else push src (THStatus 0 id id) bs
9493
9594----------------------------------------------------------------
9695
@@ -109,19 +108,19 @@ handleExpect _ _ _ = return ()
109108
110109----------------------------------------------------------------
111110
112−bodyAndSource :: ResumableSource IO ByteString
113− -> Maybe HeaderValue
114− -> Maybe HeaderValue
115− -> IO (Source IO ByteString
111+bodyAndSource :: Source
112+ -> Maybe HeaderValue -- ^ content length
113+ -> Maybe HeaderValue -- ^ transfer-encoding
114+ -> IO (IO ByteString
116115 ,RequestBodyLength
117− ,IO (ResumableSource IO ByteString))
116+ )
118117bodyAndSource src cl te
119118 | chunked = do
120− ref <- I.newIORef (src, NeedLen)
121− return (chunkedSource ref, ChunkedBody, fst <$> I.readIORef ref)
119+ csrc <- mkCSource src
120+ return (readCSource csrc, ChunkedBody)
122121 | otherwise = do
123− ibs <- IsolatedBSSource <$> I.newIORef (len, src)
124− return (ibsIsolate ibs, bodyLen, ibsDone ibs)
122+ isrc <- mkISource src len
123+ return (readISource isrc, bodyLen)
125124 where
126125 len = toLength cl
127126 bodyLen = KnownLength $ fromIntegral len
@@ -137,17 +136,28 @@ isChunked _ = False
137136
138137----------------------------------------------------------------
139138
140−timeoutBody :: Timeout.Handle -> Source IO ByteString -> Source IO ByteString
139+timeoutBody :: Timeout.Handle -> IO ByteString -> IO (IO ByteString)
141140timeoutBody timeoutHandle rbody = do
142− -- Timeout handling was paused after receiving the full request
143− -- headers. Now we need to resume it to avoid a slowloris
144− -- attack during request body sending.
145− liftIO $ Timeout.resume timeoutHandle
146− -- As soon as we finish receiving the request body, whether
147− -- because the application is not interested in more bytes, or
148− -- because there is no more data available, pause the timeout
149− -- handler again.
150− addCleanup (const $ liftIO $ Timeout.pause timeoutHandle) rbody
141+ isFirstRef <- I.newIORef True
142+
143+ return $ do
144+ isFirst <- I.readIORef isFirstRef
145+
146+ when isFirst $
147+ -- Timeout handling was paused after receiving the full request
148+ -- headers. Now we need to resume it to avoid a slowloris
149+ -- attack during request body sending.
150+ Timeout.resume timeoutHandle
151+
152+ bs <- rbody
153+
154+ -- As soon as we finish receiving the request body, whether
155+ -- because the application is not interested in more bytes, or
156+ -- because there is no more data available, pause the timeout
157+ -- handler again.
158+ when (S.null bs) (Timeout.pause timeoutHandle)
159+
160+ return bs
151161
152162----------------------------------------------------------------
153163
@@ -161,11 +171,13 @@ data THStatus = THStatus
161171
162172----------------------------------------------------------------
163173
174+{- FIXME
164175close :: Sink ByteString IO a
165176close = throwIO IncompleteHeaders
177+-}
166178
167−push :: THStatus -> ByteString -> Sink ByteString IO (Maybe ByteString, [ByteString])
168−push (THStatus len lines prepend) bs'
179+push :: Source -> THStatus -> ByteString -> IO [ByteString]
180+push src (THStatus len lines prepend) bs'
169181 -- Too many bytes
170182 | len > maxTotalHeaderLength = throwIO OverLargeHeader
171183 | otherwise = push' mnl
@@ -187,15 +199,19 @@ push (THStatus len lines prepend) bs'
187199 Just (nl, False)
188200
189201 {-# INLINE push' #-}
202+ push' :: Maybe (Int, Bool) -> IO [ByteString]
190203 -- No newline find in this chunk. Add it to the prepend,
191204 -- update the length, and continue processing.
192− push' Nothing = await >>= maybe close (push status)
205+ push' Nothing = do
206+ bs <- readSource' src
207+ when (S.null bs) $ throwIO IncompleteHeaders
208+ push src status bs
193209 where
194210 len' = len + bsLen
195211 prepend' = S.append bs
196212 status = THStatus len' lines prepend'
197213 -- Found a newline, but next line continues as a multiline header
198− push' (Just (end, True)) = push status rest
214+ push' (Just (end, True)) = push src status rest
199215 where
200216 rest = S.drop (end + 1) bs
201217 prepend' = S.append (SU.unsafeTake (checkCR bs end) bs)
@@ -204,12 +220,9 @@ push (THStatus len lines prepend) bs'
204220 -- Found a newline at position end.
205221 push' (Just (end, False))
206222 -- leftover
207− | S.null line = let lines' = lines []
208− rest = if start < bsLen then
209− Just (SU.unsafeDrop start bs)
210− else
211− Nothing
212− in maybe (return ()) leftover rest >> return (rest, lines')
223+ | S.null line = do
224+ when (start < bsLen) $ leftoverSource src (SU.unsafeDrop start bs)
225+ return (lines [])
213226 -- more headers
214227 | otherwise = let len' = len + start
215228 lines' = lines . (line:)
@@ -217,10 +230,12 @@ push (THStatus len lines prepend) bs'
217230 in if start < bsLen then
218231 -- more bytes in this chunk, push again
219232 let bs'' = SU.unsafeDrop start bs
220− in push status bs''
221− else
233+ in push src status bs''
234+ else do
222235 -- no more bytes in this chunk, ask for more
223− await >>= maybe close (push status)
236+ bs <- readSource' src
237+ when (S.null bs) $ throwIO IncompleteHeaders
238+ push src status bs
224239 where
225240 start = end + 1 -- start of next chunk
226241 line = SU.unsafeTake (checkCR bs end) bs
warp/Network/Wai/Handler/Warp/Response.hs+46−68
@@ -15,16 +15,13 @@ import Blaze.ByteString.Builder (fromByteString, Builder, flush)
1515import Blaze.ByteString.Builder.HTTP (chunkedTransferEncoding, chunkedTransferTerminator)
1616import Control.Applicative
1717import Control.Exception
18−import Control.Monad.IO.Class (liftIO)
1918import Data.Array ((!))
2019import Data.ByteString (ByteString)
20+import Data.Streaming.Blaze (newBlazeRecv, reuseBufferStrategy, allocBuffer)
2121import qualified Data.ByteString as S
22−import Control.Monad (unless)
22+import Control.Monad (unless, when)
2323import qualified Data.ByteString.Char8 as B (pack)
2424import qualified Data.CaseInsensitive as CI
25−import Data.Conduit
26−import qualified Data.Conduit.List as CL
27−import Data.Conduit.Blaze (unsafeBuilderToByteString)
2825import Data.Function (on)
2926import Data.List (deleteBy)
3027import Data.Maybe (isJust, listToMaybe)
@@ -158,20 +155,19 @@ checkPartRange fileSize = checkPart
158155
159156sendResponse :: Connection
160157 -> InternalInfo
161− -> (forall a. IO a -> IO a) -- ^ Restore masking state.
162158 -> Request -- ^ HTTP request.
163159 -> IndexedHeader -- ^ Indexed header of HTTP request.
164− -> Maybe ByteString -- ^ leftovers from header parsing
160+ -> IO ByteString -- ^ source from client, for raw response
165161 -> Response -- ^ HTTP response including status code and response header.
166162 -> IO Bool -- ^ Returing True if the connection is persistent.
167−sendResponse conn ii restore req reqidxhdr leftover' response = do
163+sendResponse conn ii req reqidxhdr src response = do
168164 hs <- addServerAndDate hs0
169165 if hasBody s req then do
170− sendRsp conn ver s hs restore rsp
166+ sendRsp conn ver s hs rsp
171167 T.tickle th
172168 return ret
173169 else do
174− sendResponseNoBody conn ver s hs restore response
170+ sendResponseNoBody conn ver s hs
175171 T.tickle th
176172 return isPersist
177173 where
@@ -188,20 +184,20 @@ sendResponse conn ii restore req reqidxhdr leftover' response = do
188184 rsp = case response of
189185 ResponseFile _ _ path mPart -> RspFile path mPart mRange (T.tickle th)
190186 ResponseBuilder _ _ b -> RspBuilder b needsChunked
191− ResponseSource _ _ fb -> RspSource fb needsChunked th
192− ResponseRaw raw _ -> RspRaw raw leftover' (T.tickle th)
187+ ResponseStream _ _ fb -> RspStream fb needsChunked th
188+ ResponseRaw raw _ -> RspRaw raw src (T.tickle th)
193189 ret = case response of
194190 ResponseFile {} -> isPersist
195191 ResponseBuilder {} -> isKeepAlive
196− ResponseSource {} -> isKeepAlive
192+ ResponseStream {} -> isKeepAlive
197193 ResponseRaw {} -> False
198194
199195----------------------------------------------------------------
200196
201197data Rsp = RspFile FilePath (Maybe FilePart) (Maybe HeaderValue) (IO ())
202198 | RspBuilder Builder Bool
203− | RspSource (forall b. WithSource IO (Flush Builder) b) Bool T.Handle
204− | RspRaw (forall b. WithRawApp b) (Maybe ByteString) (IO ())
199+ | RspStream StreamingBody Bool T.Handle
200+ | RspRaw (IO ByteString -> (ByteString -> IO ()) -> IO ()) (IO ByteString) (IO ())
205201
206202----------------------------------------------------------------
207203
@@ -209,17 +205,16 @@ sendRsp :: Connection
209205 -> H.HttpVersion
210206 -> H.Status
211207 -> H.ResponseHeaders
212− -> (forall a. IO a -> IO a) -- ^ restore
213208 -> Rsp
214209 -> IO ()
215−sendRsp conn ver s0 hs0 restore (RspFile path mPart mRange hook) = restore $ do
210+sendRsp conn ver s0 hs0 (RspFile path mPart mRange hook) = do
216211 ex <- fileRange s0 hs path mPart mRange
217212 case ex of
218213 Left _ex ->
219214#ifdef WARP_DEBUG
220215 print _ex >>
221216#endif
222− sendRsp conn ver s2 hs2 id (RspBuilder body True)
217+ sendRsp conn ver s2 hs2 (RspBuilder body True)
223218 Right (s, hs1, beg, len) -> do
224219 lheader <- composeHeader ver s hs1
225220 connSendFile conn path beg len hook [lheader]
@@ -231,7 +226,7 @@ sendRsp conn ver s0 hs0 restore (RspFile path mPart mRange hook) = restore $ do
231226
232227----------------------------------------------------------------
233228
234−sendRsp conn ver s hs restore (RspBuilder body needsChunked) = restore $ do
229+sendRsp conn ver s hs (RspBuilder body needsChunked) = do
235230 header <- composeHeaderBuilder ver s hs needsChunked
236231 let hdrBdy
237232 | needsChunked = header <> chunkedTransferEncoding body
@@ -243,37 +238,37 @@ sendRsp conn ver s hs restore (RspBuilder body needsChunked) = restore $ do
243238
244239----------------------------------------------------------------
245240
246−sendRsp conn ver s hs restore (RspSource withBodyFlush needsChunked th) =
247− withBodyFlush $ \bodyFlush -> restore $ do
241+sendRsp conn ver s hs (RspStream withBodyFlush needsChunked th) = do
248242 header <- composeHeaderBuilder ver s hs needsChunked
249− buffer <- toBlazeBuffer (connWriteBuffer conn) (connBufferSize conn)
250− let src = yield header >> cbody bodyFlush
251− src $$ unsafeBuilderToByteString (return buffer) =$ connSink conn th
252− where
253− cbody bodyFlush = if needsChunked then body $= chunk else body
254− where
255− body = mapOutput (\x -> case x of
256− Flush -> flush
257− Chunk builder -> builder)
258− bodyFlush
259− chunk :: Conduit Builder IO Builder
260− chunk = await >>= maybe (yield chunkedTransferTerminator) (\x -> yield (chunkedTransferEncoding x) >> chunk)
243+ (recv, finish) <- newBlazeRecv $ reuseBufferStrategy
244+ $ toBlazeBuffer (connWriteBuffer conn) (connBufferSize conn)
245+ let addChunk builder = do
246+ popper <- recv builder
247+ let loop = do
248+ bs <- popper
249+ unless (S.null bs) $ do
250+ connSink conn th bs
251+ loop
252+ loop
253+ addChunk'
254+ | needsChunked = addChunk . chunkedTransferEncoding
255+ | otherwise = addChunk
256+ addChunk header
257+ withBodyFlush addChunk' (addChunk' flush)
258+ when needsChunked $ addChunk chunkedTransferTerminator
259+ mbs <- finish
260+ maybe (return ()) (connSink conn th) mbs
261261
262262----------------------------------------------------------------
263263
264−sendRsp conn _ _ _ restore (RspRaw withApp mbs tickle) =
265− withApp $ \app -> restore $ app src sink
264+sendRsp conn _ _ _ (RspRaw withApp src tickle) =
265+ withApp recv send
266266 where
267− sink = CL.mapM_ (\bs -> connSendAll conn bs >> tickle)
268− src = do
269− maybe (return ()) yield mbs
270− loop
271− where
272− loop = do
273− bs <- liftIO $ connRecv conn
274− unless (S.null bs) $ do
275− liftIO tickle
276− yield bs >> loop
267+ recv = do
268+ bs <- src
269+ unless (S.null bs) tickle
270+ return bs
271+ send bs = connSendAll conn bs >> tickle
277272
278273----------------------------------------------------------------
279274
@@ -281,35 +276,18 @@ sendResponseNoBody :: Connection
281276 -> H.HttpVersion
282277 -> H.Status
283278 -> H.ResponseHeaders
284− -> (forall a. IO a -> IO a) -- ^ restore
285− -> Response
286279 -> IO ()
287−sendResponseNoBody conn ver s hs restore (ResponseSource _ _ withBodyFlush) =
288− withBodyFlush $ \_bodyFlush ->
289− restore $ composeHeader ver s hs >>= connSendAll conn
290−sendResponseNoBody conn ver s hs restore (ResponseRaw withRaw _) =
291− withRaw $ \_raw ->
292− restore $ composeHeader ver s hs >>= connSendAll conn
293−sendResponseNoBody conn ver s hs restore ResponseBuilder{} =
294− restore $ composeHeader ver s hs >>= connSendAll conn
295−sendResponseNoBody conn ver s hs restore ResponseFile{} =
296− restore $ composeHeader ver s hs >>= connSendAll conn
280+sendResponseNoBody conn ver s hs = composeHeader ver s hs >>= connSendAll conn
297281
298282----------------------------------------------------------------
299283----------------------------------------------------------------
300284
301285-- | Use 'connSendAll' to send this data while respecting timeout rules.
302−connSink :: Connection -> T.Handle -> Sink ByteString IO ()
303−connSink Connection { connSendAll = send } th = sink
304− where
305− sink = await >>= maybe close push
306− close = liftIO (T.resume th)
307− push x = do
308− liftIO $ do
309− T.resume th
310− send x
311− T.pause th
312− sink
286+connSink :: Connection -> T.Handle -> ByteString -> IO ()
287+connSink Connection { connSendAll = send } th bs = do
288+ T.resume th
289+ send bs
290+ T.pause th
313291 -- We pause timeouts before passing control back to user code. This ensures
314292 -- that a timeout will only ever be executed when Warp is in control. We
315293 -- also make sure to resume the timeout after the completion of user code
warp/Network/Wai/Handler/Warp/Run.hs+50−58
@@ -10,22 +10,14 @@ import Control.Concurrent (threadDelay, forkIOWithUnmask)
1010import qualified Control.Concurrent as Conc (yield)
1111import Control.Exception as E
1212import Control.Monad (forever, when, unless, void)
13−import Control.Monad.IO.Class (liftIO)
1413import Data.ByteString (ByteString)
1514import qualified Data.ByteString as S
16−import Data.Conduit
17−import Data.Conduit.Internal (ResumableSource (..))
18−import qualified Data.Conduit.List as CL
19−#if MIN_VERSION_conduit(1,1,0)
2015import Data.Streaming.Network (bindPortTCP)
21−#else
22−import Data.Conduit.Network (bindPort)
23−#define bindPortTCP bindPort
24−#endif
2516import Network (sClose, Socket)
2617import Network.Socket (accept, withSocketsDo, SockAddr)
2718import qualified Network.Socket.ByteString as Sock
2819import Network.Wai
20+import Network.Wai.Internal (ResponseReceived (ResponseReceived))
2921import qualified Network.Wai.Handler.Warp.Date as D
3022import qualified Network.Wai.Handler.Warp.FdCache as F
3123import Network.Wai.Handler.Warp.Buffer
@@ -228,7 +220,8 @@ serveConnection :: Connection
228220 -> IO ()
229221serveConnection conn ii addr isSecure' settings app = do
230222 istatus <- newIORef False
231− recvSendLoop istatus (connSource conn th istatus) `E.catch` \e -> do
223+ src <- mkSource (connSource conn th istatus)
224+ recvSendLoop istatus src `E.catch` \e -> do
232225 sendErrorResponse istatus e
233226 throwIO (e :: SomeException)
234227
@@ -237,66 +230,65 @@ serveConnection conn ii addr isSecure' settings app = do
237230
238231 sendErrorResponse istatus e = do
239232 status <- readIORef istatus
240− when status $ void $ mask $ \restore ->
241− sendResponse conn ii restore dummyreq defaultIndexRequestHeader Nothing (errorResponse e)
233+ when status $ void $
234+ sendResponse conn ii dummyreq defaultIndexRequestHeader (return S.empty) (errorResponse e)
242235
243236 dummyreq = defaultRequest { remoteHost = addr }
244237
245238 errorResponse e = settingsOnExceptionResponse settings e
246239
247240 recvSendLoop istatus fromClient = do
248− (req', idxhdr, getSource, leftover') <- recvRequest settings conn ii addr fromClient
241+ (req', idxhdr) <- recvRequest settings conn ii addr fromClient
249242 let req = req' { isSecure = isSecure' }
250− intercept' <- settingsIntercept settings req
251− case intercept' of
252− Nothing -> do
253− -- Let the application run for as long as it wants
254− T.pause th
243+ -- Let the application run for as long as it wants
244+ T.pause th
255245
256− -- In the event that some scarce resource was acquired during
257− -- creating the request, we need to make sure that we don't get
258− -- an async exception before calling the ResponseSource.
259− keepAlive <- mask $ \restore -> do
260− res <- restore $ app req
261− T.resume th
262− -- FIXME consider forcing evaluation of the res here to
263− -- send more meaningful error messages to the user.
264− -- However, it may affect performance.
265− writeIORef istatus False
266− sendResponse conn ii restore req idxhdr leftover' res
246+ -- In the event that some scarce resource was acquired during
247+ -- creating the request, we need to make sure that we don't get
248+ -- an async exception before calling the ResponseSource.
249+ keepAliveRef <- newIORef $ error "keepAliveRef not filled"
250+ _ <- app req $ \res -> do
251+ T.resume th
252+ -- FIXME consider forcing evaluation of the res here to
253+ -- send more meaningful error messages to the user.
254+ -- However, it may affect performance.
255+ writeIORef istatus False
256+ keepAlive <- sendResponse conn ii req idxhdr (readSource fromClient) res
257+ writeIORef keepAliveRef keepAlive
258+ return ResponseReceived
259+ keepAlive <- readIORef keepAliveRef
267260
268− -- We just send a Response and it takes a time to
269− -- receive a Request again. If we immediately call recv,
270− -- it is likely to fail and the IO manager works.
271− -- It is very costy. So, we yield to another Haskell
272− -- thread hoping that the next Request will arraive
273− -- when this Haskell thread will be re-scheduled.
274− -- This improves performance at least when
275− -- the number of cores is small.
276− Conc.yield
261+ -- We just send a Response and it takes a time to
262+ -- receive a Request again. If we immediately call recv,
263+ -- it is likely to fail and the IO manager works.
264+ -- It is very costy. So, we yield to another Haskell
265+ -- thread hoping that the next Request will arraive
266+ -- when this Haskell thread will be re-scheduled.
267+ -- This improves performance at least when
268+ -- the number of cores is small.
269+ Conc.yield
277270
278− when keepAlive $ do
279− -- flush the rest of the request body
280− requestBody req $$ CL.sinkNull
281− ResumableSource fromClient' _ <- getSource
282− T.resume th
283− recvSendLoop istatus fromClient'
284− Just intercept -> do
285− T.pause th
286− ResumableSource fromClient' _ <- getSource
287− intercept fromClient' conn
271+ when keepAlive $ do
272+ -- flush the rest of the request body
273+ flushBody $ requestBody req
274+ T.resume th
275+ recvSendLoop istatus fromClient
288276
289−connSource :: Connection -> T.Handle -> IORef Bool -> Source IO ByteString
290−connSource Connection { connRecv = recv } th istatus = src
277+flushBody :: IO ByteString -> IO ()
278+flushBody src =
279+ loop
291280 where
292− src = do
293− bs <- liftIO recv
294− unless (S.null bs) $ do
295− liftIO $ do
296− writeIORef istatus True
297− when (S.length bs >= 2048) $ T.tickle th
298− yield bs
299− src
281+ loop = do
282+ bs <- src
283+ unless (S.null bs) loop
284+
285+connSource :: Connection -> T.Handle -> IORef Bool -> IO ByteString
286+connSource Connection { connRecv = recv } th istatus = do
287+ bs <- recv
288+ unless (S.null bs) $ do
289+ writeIORef istatus True
290+ when (S.length bs >= 2048) $ T.tickle th
291+ return bs
300292
301293-- Copied from: https://github.com/mzero/plush/blob/master/src/Plush/Server/Warp.hs
302294setSocketCloseOnExec :: Socket -> IO ()
warp/Network/Wai/Handler/Warp/Settings.hs+1−5
@@ -8,8 +8,7 @@ import Control.Monad (when)
88import qualified Data.ByteString as S
99import qualified Data.Text as T
1010import qualified Data.Text.IO as TIO
11−import Data.Conduit
12−import Data.Conduit.Network (HostPreference)
11+import Data.Streaming.Network (HostPreference)
1312import GHC.IO.Exception (IOErrorType(..))
1413import qualified Network.HTTP.Types as H
1514import Network.Socket (SockAddr)
@@ -40,7 +39,6 @@ data Settings = Settings
4039 , settingsOnOpen :: SockAddr -> IO Bool -- ^ What to do when a connection is open. When 'False' is returned, the connection is closed immediately. Otherwise, the connection is going on. Default: always returns 'True'.
4140 , settingsOnClose :: SockAddr -> IO () -- ^ What to do when a connection is close. Default: do nothing.
4241 , settingsTimeout :: Int -- ^ Timeout value in seconds. Default value: 30
43− , settingsIntercept :: Request -> IO (Maybe (Source IO S.ByteString -> Connection -> IO ()))
4442 , settingsManager :: Maybe Manager -- ^ Use an existing timeout manager instead of spawning a new one. If used, 'settingsTimeout' is ignored. Default is 'Nothing'
4543 , settingsFdCacheDuration :: Int -- ^ Cache duratoin time of file descriptors in seconds. 0 means that the cache mechanism is not used. Default value: 10
4644 , settingsBeforeMainLoop :: IO ()
@@ -72,7 +70,6 @@ defaultSettings = Settings
7270 , settingsOnOpen = const $ return True
7371 , settingsOnClose = const $ return ()
7472 , settingsTimeout = 30
75− , settingsIntercept = const (return Nothing)
7673 , settingsManager = Nothing
7774 , settingsFdCacheDuration = 10
7875 , settingsBeforeMainLoop = return ()
@@ -112,7 +109,6 @@ exceptionResponseForDebug e = responseLBS H.internalServerError500 [(H.hContentT
112109{-# DEPRECATED settingsOnOpen "Use setOnOpen instead" #-}
113110{-# DEPRECATED settingsOnClose "Use setOnClose instead" #-}
114111{-# DEPRECATED settingsTimeout "Use setTimeout instead" #-}
115−{-# DEPRECATED settingsIntercept "Use setIntercept instead" #-}
116112{-# DEPRECATED settingsManager "Use setManager instead" #-}
117113{-# DEPRECATED settingsFdCacheDuration "Use setFdCacheDuration instead" #-}
118114{-# DEPRECATED settingsBeforeMainLoop "Use setBeforeMainLoop instead" #-}
warp/Network/Wai/Handler/Warp/Types.hs+30
@@ -13,6 +13,8 @@ import qualified Network.Wai.Handler.Warp.Date as D
1313import qualified Network.Wai.Handler.Warp.FdCache as F
1414import qualified Network.Wai.Handler.Warp.Timeout as T
1515import Network.Wai.Handler.Warp.Buffer (Buffer,BufSize)
16+import qualified Data.ByteString as S
17+import Data.IORef (IORef, readIORef, writeIORef, newIORef)
1618
1719----------------------------------------------------------------
1820
@@ -88,3 +90,31 @@ data InternalInfo = InternalInfo {
8890 , fdCacher :: Maybe F.MutableFdCache
8991 , dateCacher :: D.DateCache
9092 }
93+
94+----------------------------------------------------------------
95+
96+data Source = Source !(IORef ByteString) !(IO ByteString)
97+
98+mkSource :: IO ByteString -> IO Source
99+mkSource func = do
100+ ref <- newIORef S.empty
101+ return $! Source ref func
102+
103+readSource :: Source -> IO ByteString
104+readSource (Source ref func) = do
105+ bs <- readIORef ref
106+ if S.null bs
107+ then func
108+ else do
109+ writeIORef ref S.empty
110+ return bs
111+
112+-- | Read from a Source, ignoring any leftovers.
113+readSource' :: Source -> IO ByteString
114+readSource' (Source _ func) = func
115+
116+leftoverSource :: Source -> ByteString -> IO ()
117+leftoverSource (Source ref _) bs = writeIORef ref bs
118+
119+readLeftoverSource :: Source -> IO ByteString
120+readLeftoverSource (Source ref _) = readIORef ref
warp/test/ConduitSpec.hs+26−12
@@ -2,10 +2,10 @@
22module ConduitSpec (main, spec) where
33
44import Network.Wai.Handler.Warp.Conduit
5−import Data.Conduit
6−import qualified Data.Conduit.List as CL
5+import Network.Wai.Handler.Warp.Types
6+import Control.Monad (replicateM)
77import Test.Hspec
8−import qualified Data.IORef as I
8+import Data.IORef as I
99import qualified Data.ByteString as S
1010
1111main :: IO ()
@@ -14,19 +14,33 @@ main = hspec spec
1414spec :: Spec
1515spec = describe "conduit" $ do
1616 it "IsolatedBSSource" $ do
17− (rsrc, ()) <- mapM_ yield (map S.singleton [1..50]) $$+ return ()
18− ibs <- fmap IsolatedBSSource $ I.newIORef (40, rsrc)
19− x <- ibsIsolate ibs $$ CL.take 20
17+ ref <- newIORef $ map S.singleton [1..50]
18+ src <- mkSource $ do
19+ x <- readIORef ref
20+ case x of
21+ [] -> return S.empty
22+ y:z -> do
23+ writeIORef ref z
24+ return y
25+ isrc <- mkISource src 40
26+ x <- replicateM 20 $ readISource isrc
2027 S.concat x `shouldBe` S.pack [1..20]
2128
22− y <- ibsIsolate ibs $$ CL.consume
29+ y <- replicateM 40 $ readISource isrc
2330 S.concat y `shouldBe` S.pack [21..40]
2431
25− rsrc' <- ibsDone ibs
26− z <- rsrc' $$+- CL.consume
32+ z <- replicateM 40 $ readSource src
2733 S.concat z `shouldBe` S.pack [41..50]
2834 it "chunkedSource" $ do
29− (rsrc, ()) <- yield "5\r\n12345\r\n3\r\n678\r\n0\r\nBLAH" $$+ return ()
30− ref <- I.newIORef (rsrc, NeedLen)
31− x <- chunkedSource ref $$ CL.consume
35+ ref <- newIORef $ "5\r\n12345\r\n3\r\n678\r\n0\r\n\r\nBLAH"
36+ src <- mkSource $ do
37+ x <- readIORef ref
38+ writeIORef ref S.empty
39+ return x
40+ csrc <- mkCSource src
41+
42+ x <- replicateM 15 $ readCSource csrc
3243 S.concat x `shouldBe` "12345678"
44+
45+ y <- replicateM 15 $ readSource src
46+ S.concat y `shouldBe` "BLAH"
warp/test/ExceptionSpec.hs+8−8
@@ -30,20 +30,20 @@ withTestServer inner = bracket
3030 $ \_ -> inner port
3131
3232testApp :: Application
33−testApp (Network.Wai.Internal.Request {pathInfo = [x]})
33+testApp (Network.Wai.Internal.Request {pathInfo = [x]}) f
3434 | x == "statusError" =
35− return $ responseLBS undefined [] "foo"
35+ f $ responseLBS undefined [] "foo"
3636 | x == "headersError" =
37− return $ responseLBS ok200 undefined "foo"
37+ f $ responseLBS ok200 undefined "foo"
3838 | x == "headerError" =
39− return $ responseLBS ok200 [undefined] "foo"
39+ f $ responseLBS ok200 [undefined] "foo"
4040 | x == "bodyError" =
41− return $ responseLBS ok200 [] undefined
41+ f $ responseLBS ok200 [] undefined
4242 | x == "ioException" = do
4343 void $ fail "ioException"
44− return $ responseLBS ok200 [] "foo"
45−testApp _ =
46− return $ responseLBS ok200 [] "foo"
44+ f $ responseLBS ok200 [] "foo"
45+testApp _ f =
46+ f $ responseLBS ok200 [] "foo"
4747
4848spec :: Spec
4949spec = describe "responds even if there is an exception" $ do
warp/test/RequestSpec.hs+45−16
@@ -4,17 +4,16 @@
44
55module RequestSpec (main, spec) where
66
7−import Data.Conduit
8−import qualified Data.Conduit.Binary as CB
9−import Data.Conduit.List (sourceList)
107import Network.Wai.Handler.Warp.Request
118import Network.Wai.Handler.Warp.RequestHeader (parseByteRanges)
129import Network.Wai.Handler.Warp.Types
1310import Test.Hspec
1411import Test.Hspec.QuickCheck
12+import qualified Data.ByteString as S
1513import qualified Data.ByteString.Char8 as S8
1614import qualified Data.ByteString.Lazy as L
1715import qualified Network.HTTP.Types.Header as HH
16+import Data.IORef
1817
1918main :: IO ()
2019main = hspec spec
@@ -23,9 +22,9 @@ spec :: Spec
2322spec = do
2423 describe "headerLines" $ do
2524 it "takes until blank" $
26− blankSafe >>= (`shouldBe` (Nothing, ["foo", "bar", "baz"]))
25+ blankSafe >>= (`shouldBe` ("", ["foo", "bar", "baz"]))
2726 it "ignored leading whitespace in bodies" $
28− whiteSafe >>= (`shouldBe` (Just " hi there", ["foo", "bar", "baz"]))
27+ whiteSafe >>= (`shouldBe` (" hi there", ["foo", "bar", "baz"]))
2928 it "throws OverLargeHeader when too many" $
3029 tooMany `shouldThrow` overLargeHeader
3130 it "throws OverLargeHeader when too large" $
@@ -35,8 +34,9 @@ spec = do
3534 [ "GET / HTTP/1.1\r\nConnection: Close\r"
3635 , "\n\r\n"
3736 ]
38− (mleftover, actual) <- mapM_ yield chunks $$ headerLines
39− mleftover `shouldBe` Nothing
37+ (actual, src) <- headerLinesList' chunks
38+ leftover <- readLeftoverSource src
39+ leftover `shouldBe` S.empty
4040 actual `shouldBe` ["GET / HTTP/1.1", "Connection: Close"]
4141 prop "random chunking" $ \breaks extraS -> do
4242 let bsFull = "GET / HTTP/1.1\r\nConnection: Close\r\n\r\n" `S8.append` extra
@@ -47,11 +47,9 @@ spec = do
4747 bs1 : loop xs bs2
4848 where
4949 (bs1, bs2) = S8.splitAt ((x `mod` 10) + 1) bs
50− (leftover, actual) <- mapM_ yield chunks $$ do
51− (_, actual) <- headerLines
52− x' <- CB.take (length extraS)
53− let x = S8.concat $ L.toChunks x'
54− return (x, actual)
50+ (actual, src) <- headerLinesList' chunks
51+ leftover <- consumeLen (length extraS) src
52+
5553 actual `shouldBe` ["GET / HTTP/1.1", "Connection: Close"]
5654 leftover `shouldBe` extra
5755 describe "parseByteRanges" $ do
@@ -63,10 +61,41 @@ spec = do
6361 test "foobytes=9500-" Nothing
6462 test "bytes=0-0,-1" $ Just [HH.ByteRangeFromTo 0 0, HH.ByteRangeSuffix 1]
6563 where
66− blankSafe = (sourceList ["f", "oo\n", "bar\nbaz\n\r\n"]) $$ headerLines
67− whiteSafe = (sourceList ["foo\r\nbar\r\nbaz\r\n\r\n hi there"]) $$ headerLines
68− tooMany = (sourceList $ repeat "f\n") $$ headerLines
69− tooLarge = (sourceList $ repeat "f") $$ headerLines
64+ blankSafe = headerLinesList ["f", "oo\n", "bar\nbaz\n\r\n"]
65+ whiteSafe = headerLinesList ["foo\r\nbar\r\nbaz\r\n\r\n hi there"]
66+ tooMany = headerLinesList $ repeat "f\n"
67+ tooLarge = headerLinesList $ repeat "f"
68+
69+headerLinesList orig = do
70+ (res, src) <- headerLinesList' orig
71+ leftover <- readLeftoverSource src
72+ return (leftover, res)
73+
74+headerLinesList' orig = do
75+ ref <- newIORef orig
76+ let src = do
77+ x <- readIORef ref
78+ case x of
79+ [] -> return S.empty
80+ y:z -> do
81+ writeIORef ref z
82+ return y
83+ src' <- mkSource src
84+ res <- headerLines src'
85+ return (res, src')
86+
87+consumeLen len0 src =
88+ loop id len0
89+ where
90+ loop front len
91+ | len <= 0 = return $ S.concat $ front []
92+ | otherwise = do
93+ bs <- readSource src
94+ if S.null bs
95+ then loop front 0
96+ else do
97+ let (x, y) = S.splitAt len bs
98+ loop (front . (x:)) (len - S.length x)
7099
71100overLargeHeader :: Selector InvalidRequest
72101overLargeHeader e = e == OverLargeHeader
warp/test/ResponseSpec.hs+2−2
@@ -36,7 +36,7 @@ testRange range out crange = it title $ withApp defaultSettings app $ \port -> d
3636 lookup "Content-Range" hs `shouldBe` Just ("bytes " ++ crange)
3737 lookup "Content-Length" hs `shouldBe` Just (show $ length $ last bss)
3838 where
39− app _ = return $ responseFile status200 [] "attic/hex" Nothing
39+ app _ = ($ responseFile status200 [] "attic/hex" Nothing)
4040 title = show (range, out, crange)
4141 toHeader s =
4242 case break (== ':') s of
@@ -60,7 +60,7 @@ testPartial size offset count out = it title $ withApp defaultSettings app $ \po
6060 lookup "Content-Length" hs `shouldBe` Just (show $ length $ last bss)
6161 lookup "Content-Range" hs `shouldBe` Just range
6262 where
63− app _ = return $ responseFile status200 [] "attic/hex" $ Just $ FilePart offset count size
63+ app _ = ($ responseFile status200 [] "attic/hex" $ Just $ FilePart offset count size)
6464 title = show (offset, count, out)
6565 toHeader s =
6666 case break (== ':') s of
warp/test/RunSpec.hs+68−43
@@ -5,15 +5,14 @@
55module RunSpec (main, spec, withApp) where
66
77import Control.Concurrent (forkIO, killThread, threadDelay)
8−import Control.Monad (forM_, replicateM_)
8+import Control.Monad (forM_, replicateM_, unless)
99import System.Timeout (timeout)
1010import Control.Monad.IO.Class (MonadIO, liftIO)
1111import Data.ByteString (ByteString, hPutStr, hGetSome)
12+import qualified Control.Exception as E
1213import qualified Data.ByteString as S
1314import qualified Data.ByteString.Char8 as S8
1415import qualified Data.ByteString.Lazy as L
15−import Data.Conduit (($$), (=$))
16−import qualified Data.Conduit.List
1716import qualified Data.IORef as I
1817import Network (connectTo, PortID (PortNumber))
1918import Network.HTTP.Types
@@ -27,8 +26,6 @@ import Control.Exception.Lifted (bracket, try, IOException, onException)
2726import Data.Streaming.Network (bindPortTCP, getSocketTCP, safeRecv)
2827import Network.Socket (sClose)
2928import qualified Network.HTTP as HTTP
30−import Data.Conduit (Flush (Chunk), ($=))
31−import qualified Data.Conduit.List as CL
3229import Blaze.ByteString.Builder (fromByteString)
3330import Network.Socket.ByteString (sendAll)
3431
@@ -48,8 +45,8 @@ err :: (MonadIO m, Show a) => Counter -> a -> m ()
4845err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg
4946
5047readBody :: CounterApplication
51−readBody icount req = do
52− body <- requestBody req $$ Data.Conduit.List.consume
48+readBody icount req f = do
49+ body <- consumeBody $ requestBody req
5350 case () of
5451 ()
5552 | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
@@ -59,21 +56,21 @@ readBody icount req = do
5956 | not $ requestMethod req `elem` ["GET", "POST"]
6057 -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
6158 | otherwise -> incr icount
62− return $ responseLBS status200 [] "Read the body"
59+ f $ responseLBS status200 [] "Read the body"
6360
6461ignoreBody :: CounterApplication
65−ignoreBody icount req = do
62+ignoreBody icount req f = do
6663 if (requestMethod req `elem` ["GET", "POST"])
6764 then incr icount
6865 else err icount ("Invalid request method" :: String, requestMethod req)
69− return $ responseLBS status200 [] "Ignored the body"
66+ f $ responseLBS status200 [] "Ignored the body"
7067
7168doubleConnect :: CounterApplication
72−doubleConnect icount req = do
73− _ <- requestBody req $$ Data.Conduit.List.consume
74− _ <- requestBody req $$ Data.Conduit.List.consume
69+doubleConnect icount req f = do
70+ _ <- consumeBody $ requestBody req
71+ _ <- consumeBody $ requestBody req
7572 incr icount
76− return $ responseLBS status200 [] "double connect"
73+ f $ responseLBS status200 [] "double connect"
7774
7875nextPort :: I.IORef Int
7976nextPort = unsafePerformIO $ I.newIORef 5000
@@ -117,7 +114,7 @@ runTest expected app chunks = do
117114 Right i -> i `shouldBe` expected
118115
119116dummyApp :: Application
120−dummyApp _ = return $ responseLBS status200 [] "foo"
117+dummyApp _ f = f $ responseLBS status200 [] "foo"
121118
122119runTerminateTest :: InvalidRequest
123120 -> ByteString
@@ -197,9 +194,9 @@ spec = do
197194 describe "special input" $ do
198195 it "multiline headers" $ do
199196 iheaders <- I.newIORef []
200− let app req = do
197+ let app req f = do
201198 liftIO $ I.writeIORef iheaders $ requestHeaders req
202− return $ responseLBS status200 [] ""
199+ f $ responseLBS status200 [] ""
203200 withApp defaultSettings app $ \port -> do
204201 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
205202 let input = S.concat
@@ -215,9 +212,9 @@ spec = do
215212 ]
216213 it "no space between colon and value" $ do
217214 iheaders <- I.newIORef []
218− let app req = do
215+ let app req f = do
219216 liftIO $ I.writeIORef iheaders $ requestHeaders req
220− return $ responseLBS status200 [] ""
217+ f $ responseLBS status200 [] ""
221218 withApp defaultSettings app $ \port -> do
222219 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
223220 let input = S.concat
@@ -235,10 +232,10 @@ spec = do
235232 describe "chunked bodies" $ do
236233 it "works" $ do
237234 ifront <- I.newIORef id
238− let app req = do
239− bss <- requestBody req $$ Data.Conduit.List.consume
235+ let app req f = do
236+ bss <- consumeBody $ requestBody req
240237 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
241− return $ responseLBS status200 [] ""
238+ f $ responseLBS status200 [] ""
242239 withApp defaultSettings app $ \port -> do
243240 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
244241 let input = S.concat
@@ -258,10 +255,10 @@ spec = do
258255 ]
259256 it "lots of chunks" $ do
260257 ifront <- I.newIORef id
261− let app req = do
262− bss <- requestBody req $$ Data.Conduit.List.consume
263− liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
264− return $ responseLBS status200 [] ""
258+ let app req f = do
259+ bss <- consumeBody $ requestBody req
260+ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
261+ f $ responseLBS status200 [] ""
265262 withApp defaultSettings app $ \port -> do
266263 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
267264 let input = concat $ replicate 2 $
@@ -270,15 +267,15 @@ spec = do
270267 ["0\r\n\r\n"]
271268 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) input
272269 hClose handle
273− threadDelay 1000
270+ threadDelay 100000 -- FIXME why does this delay need to be so high?
274271 front <- I.readIORef ifront
275272 front [] `shouldBe` replicate 2 (S.concat $ replicate 50 "12345")
276273 it "in chunks" $ do
277274 ifront <- I.newIORef id
278− let app req = do
279− bss <- requestBody req $$ Data.Conduit.List.consume
275+ let app req f = do
276+ bss <- consumeBody $ requestBody req
280277 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
281− return $ responseLBS status200 [] ""
278+ f $ responseLBS status200 [] ""
282279 withApp defaultSettings app $ \port -> do
283280 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
284281 let input = S.concat
@@ -297,13 +294,16 @@ spec = do
297294 ]
298295 it "timeout in request body" $ do
299296 ifront <- I.newIORef id
300− let app req = do
301− bss <- (requestBody req $$ Data.Conduit.List.consume) `onException`
297+ let app req f = do
298+ bss <- (consumeBody $ requestBody req) `onException`
302299 liftIO (I.atomicModifyIORef ifront (\front -> (front . ("consume interrupted":), ())))
303− liftIO $ threadDelay 4000000 `onException`
304− I.atomicModifyIORef ifront (\front -> (front . ("threadDelay interrupted":), ()))
300+ liftIO $ threadDelay 4000000 `E.catch` \e -> do
301+ I.atomicModifyIORef ifront (\front ->
302+ ( front . ((S8.pack $ "threadDelay interrupted: " ++ show e):)
303+ , ()))
304+ E.throwIO (e :: E.SomeException)
305305 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
306− return $ responseLBS status200 [] ""
306+ f $ responseLBS status200 [] ""
307307 withApp (setTimeout 1 defaultSettings) app $ \port -> do
308308 let bs1 = S.replicate 2048 88
309309 bs2 = "This is short"
@@ -323,12 +323,15 @@ spec = do
323323 S.concat (front []) `shouldBe` bs
324324 describe "raw body" $ do
325325 it "works" $ do
326− let app _req = do
326+ let app _req f = do
327327 let backup = responseLBS status200 [] "Not raw"
328− return $ flip responseRaw backup $ \src sink ->
329− src
330− $$ Data.Conduit.List.map doubleBS
331− =$ sink
328+ f $ flip responseRaw backup $ \src sink -> do
329+ let loop = do
330+ bs <- src
331+ unless (S.null bs) $ do
332+ sink $ doubleBS bs
333+ loop
334+ loop
332335 doubleBS = S.concatMap $ \w -> S.pack [w, w]
333336 withApp defaultSettings app $ \port -> do
334337 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
@@ -340,7 +343,7 @@ spec = do
340343 timeout 100000 (S.hGet handle 10) >>= (`shouldBe` Just "6677889900")
341344
342345 it "only one date and server header" $ do
343− let app _ = return $ responseLBS status200
346+ let app _ f = f $ responseLBS status200
344347 [ ("server", "server")
345348 , ("date", "date")
346349 ] ""
@@ -352,8 +355,13 @@ spec = do
352355 `shouldBe` ["date"]
353356
354357 it "streaming echo #249" $ do
355− let app req = return $ responseSource status200 []
356− $ requestBody req $= CL.map (Chunk . fromByteString)
358+ let app req f = f $ responseStream status200 [] $ \write _ -> do
359+ let loop = do
360+ bs <- requestBody req
361+ unless (S.null bs) $ do
362+ write $ fromByteString bs
363+ loop
364+ loop
357365 withApp defaultSettings app $ \port -> do
358366 (socket, _addr) <- getSocketTCP "127.0.0.1" port
359367 sendAll socket "POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
@@ -361,3 +369,20 @@ spec = do
361369 sendAll socket "5\r\nhello\r\n0\r\n\r\n"
362370 bs <- safeRecv socket 4096
363371 S.takeWhile (/= 13) bs `shouldBe` "HTTP/1.1 200 OK"
372+
373+ it "streaming response with length" $ do
374+ let app _ f = f $ responseStream status200 [("content-length", "20")] $ \write _ -> do
375+ replicateM_ 4 $ write $ fromByteString "Hello"
376+ withApp defaultSettings app $ \port -> do
377+ Right res <- HTTP.simpleHTTP (HTTP.getRequest $ "http://127.0.0.1:" ++ show port)
378+ HTTP.rspBody res `shouldBe` "HelloHelloHelloHello"
379+
380+consumeBody :: IO ByteString -> IO [ByteString]
381+consumeBody body =
382+ loop id
383+ where
384+ loop front = do
385+ bs <- body
386+ if S.null bs
387+ then return $ front []
388+ else loop $ front . (bs:)
warp/warp.cabal+2−12
@@ -1,5 +1,5 @@
11Name: warp
2−Version: 2.1.5.2
2+Version: 3.0.0
33Synopsis: A fast, light-weight web server for WAI applications.
44License: MIT
55License-file: LICENSE
@@ -39,20 +39,14 @@ Library
3939 Build-Depends: base >= 3 && < 5
4040 , array
4141 , blaze-builder >= 0.3.3 && < 0.4
42− , blaze-builder-conduit >= 0.5 && < 1.2
4342 , bytestring >= 0.9.1.4
4443 , case-insensitive >= 0.2
45− , conduit >= 0.5 && < 1.2
46− , conduit-extra
4744 , ghc-prim
4845 , http-types >= 0.7
49− , lifted-base >= 0.1
50− , network-conduit >= 0.5 && < 1.2
5146 , simple-sendfile >= 0.2.7 && < 0.3
52− , transformers >= 0.2.2
5347 , unix-compat >= 0.2
5448 , void
55− , wai >= 2.1 && < 2.2
49+ , wai >= 3.0 && < 3.1
5650 , text
5751 , streaming-commons
5852 if flag(network-bytestring)
@@ -126,15 +120,12 @@ Test-Suite spec
126120 Build-Depends: base >= 4 && < 5
127121 , array
128122 , blaze-builder >= 0.3.3 && < 0.4
129− , blaze-builder-conduit >= 0.5
130123 , bytestring >= 0.9.1.4
131124 , case-insensitive >= 0.2
132− , conduit >= 0.5
133125 , ghc-prim
134126 , HTTP
135127 , http-types >= 0.8.4
136128 , lifted-base >= 0.1
137− , network-conduit
138129 , simple-sendfile >= 0.2.4 && < 0.3
139130 , transformers >= 0.2.2
140131 , unix-compat >= 0.2
@@ -147,7 +138,6 @@ Test-Suite spec
147138 , time
148139 , old-locale
149140 , text
150− , conduit-extra
151141 , streaming-commons >= 0.1.1
152142 , async
153143