on master help sign in
Fixed a bunch of middlewares
commitfdcc6aa86d524b1e89425f98fdd76bfedda8e10e authorMichael Snoyman <michael@snoyman.com> dateThu 22 May 2014, 15:36 (12 years ago) parent1e1b8c2
9 files changed+124−75
wai-extra/Network/Wai/Middleware/Gzip.hs+71−24 wai-extra/Network/Wai/Middleware/HttpAuth.hs+4−4 wai-extra/Network/Wai/Middleware/Jsonp.hs+13−18 wai-extra/Network/Wai/Middleware/MethodOverridePost.hs+13−8 wai-extra/Network/Wai/Middleware/Rewrite.hs+2−2 wai-extra/Network/Wai/Middleware/Vhost.hs+3−3 wai-extra/Network/Wai/Test.hs+10−7 wai-extra/Network/Wai/UrlMap.hs+7−9 wai-extra/wai-extra.cabal+1
wai-extra/Network/Wai/Middleware/Gzip.hs+71−24
@@ -37,6 +37,14 @@ import Blaze.ByteString.Builder (fromByteString)
3737import Control.Exception (try, SomeException)
3838import qualified Data.Set as Set
3939import 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)
4048
4149data GzipSettings = GzipSettings
4250 { gzipFiles :: GzipFiles
@@ -69,20 +77,19 @@ defaultCheckMime bs =
6977--
7078-- * Only compress if the response is above a certain size.
7179gzip :: GzipSettings -> Middleware
72−gzip set app env = do
73− res <- app env
80+gzip set app env sendResponse = app env $ \res ->
7481 case res of
75− ResponseFile{} | gzipFiles set == GzipIgnore -> return res
82+ ResponseFile{} | gzipFiles set == GzipIgnore -> sendResponse res
7683 _ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res)
7784 then
7885 case (res, gzipFiles set) of
7986 (ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
8087 case lookup "content-type" hs of
8188 Just m
82− | gzipCheckMime set m -> liftIO $ compressFile s hs file cache
83− _ -> return res
84− _ -> return $ compressE set res
85− 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
8693 where
8794 enc = fromMaybe [] $ (splitCommas . S8.unpack)
8895 `fmap` lookup "Accept-Encoding" (requestHeaders env)
@@ -90,22 +97,38 @@ gzip set app env = do
9097 isMSIE6 = "MSIE 6" `S.isInfixOf` ua
9198 isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res
9299
93−compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response
94−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
95102 e <- doesFileExist tmpfile
96103 if e
97104 then onSucc
98105 else do
99106 createDirectoryIfMissing True cache
100− x <-
101− try $ runResourceT $ CB.sourceFile file
102− C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile
103− 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!
104128 where
105− onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing
129+ onSucc = sendResponse $ responseFile s (fixHeaders hs) tmpfile Nothing
106130
107− onErr :: SomeException -> IO Response
108− 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
109132
110133 tmpfile = cache ++ '/' : map safe file
111134 safe c
@@ -118,18 +141,42 @@ compressFile s hs file cache = do
118141
119142compressE :: GzipSettings
120143 -> Response
121− -> Response
122−compressE set res =
144+ -> (Response -> IO a)
145+ -> IO a
146+compressE set res sendResponse =
123147 case lookup "content-type" hs of
124148 Just m | gzipCheckMime set m ->
125149 let hs' = fixHeaders hs
126− in ResponseSource s hs' $ \f -> wb $ \b -> f $
127− b C.$= builderToByteStringFlush
128− C.$= CZ.compressFlush 1 (CZ.WindowBits 31)
129− C.$= CL.map (fmap fromByteString)
130− _ -> 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
131178 where
132− (s, hs, wb) = responseToSource res
179+ (s, hs, wb) = responseToStream res
133180
134181-- Remove Content-Length header, since we will certainly have a
135182-- 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−18
@@ -36,7 +36,7 @@ import Network.HTTP.Types (Status)
3636-- having a content type of \"text\/javascript\" and calling the specified
3737-- callback function.
3838jsonp :: Middleware
39−jsonp app env = do
39+jsonp app env sendResponse = do
4040 let accept = fromMaybe B8.empty $ lookup "Accept" $ requestHeaders env
4141 let callback :: Maybe B8.ByteString
4242 callback =
@@ -51,15 +51,15 @@ jsonp app env = do
5151 "application/json"
5252 $ requestHeaders env
5353 }
54− res <- app env'
55− return $ case callback of
56− Nothing -> res
57− Just c -> go c res
54+ app env' $ \res ->
55+ case callback of
56+ Nothing -> sendResponse res
57+ Just c -> go c res
5858 where
5959 go c r@(ResponseBuilder s hs b) =
60− case checkJSON hs of
60+ sendResponse $ case checkJSON hs of
6161 Nothing -> r
62− Just hs' -> ResponseBuilder s hs' $
62+ Just hs' -> responseBuilder s hs' $
6363 copyByteString c
6464 `mappend` fromChar '('
6565 `mappend` b
@@ -67,9 +67,9 @@ jsonp app env = do
6767 go c r =
6868 case checkJSON hs of
6969 Just hs' -> addCallback c s hs' wb
70− Nothing -> r
70+ Nothing -> sendResponse r
7171 where
72− (s, hs, wb) = responseToSource r
72+ (s, hs, wb) = responseToStream r
7373
7474 checkJSON hs =
7575 case lookup "Content-Type" hs of
@@ -79,16 +79,11 @@ jsonp app env = do
7979 _ -> Nothing
8080 fixHeaders = changeVal "Content-Type" "text/javascript"
8181
82− addCallback :: ByteString
83− -> Status
84− -> [(CI ByteString, ByteString)]
85− -> (forall b. WithSource IO (C.Flush Builder) b)
86− -> Response
8782 addCallback cb s hs wb =
88− ResponseSource s hs $ \f -> wb $ \b -> f $
89− C.yield (C.Chunk $ copyByteString cb `mappend` fromChar '(')
90− `mappend` b
91− `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 ')'
9287
9388changeVal :: Eq a
9489 => a
wai-extra/Network/Wai/Middleware/MethodOverridePost.hs+13−8
@@ -10,7 +10,9 @@ module Network.Wai.Middleware.MethodOverridePost
1010
1111import Network.Wai
1212import Network.HTTP.Types (parseQuery)
13−import Data.Monoid (mconcat)
13+import Data.Monoid (mconcat, mempty)
14+import Data.IORef
15+import Data.ByteString.Lazy (toChunks)
1416
1517-- | Allows overriding of the HTTP request method via the _method post string parameter.
1618--
@@ -24,13 +26,16 @@ import Data.Monoid (mconcat)
2426-- * This middleware only applies when the initial request method is POST.
2527--
2628methodOverridePost :: Middleware
27−methodOverridePost app req = case (requestMethod req, lookup "Content-Type" (requestHeaders req)) of
28− ("POST", Just "application/x-www-form-urlencoded") -> setPost req >>= app
29− _ -> 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
3033
3134setPost :: Request -> IO Request
3235setPost req = do
33− body <- lazyConsume (requestBody req)
34− case parseQuery (mconcat body) of
35− (("_method", Just newmethod):_) -> return $ req {requestBody = sourceList body, requestMethod = newmethod}
36− _ -> 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/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/Test.hs+10−7
@@ -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)
@@ -47,9 +48,7 @@ import qualified Data.Text.Encoding as TE
4748import Data.IORef
4849import Data.Monoid (mempty, mappend)
4950
50−type Session = ReaderT
51− (Request -> (Response -> IO SResponse) -> IO SResponse)
52− (StateT ClientState IO)
51+type Session = ReaderT Application (StateT ClientState IO)
5352
5453data ClientState = ClientState
5554 { _clientCookies :: Map ByteString ByteString
@@ -104,17 +103,21 @@ srequest (SRequest req bod) = do
104103 [] -> ([], S.empty)
105104 x:y -> (y, x)
106105 }
107− liftIO $ app req' runResponse
106+ liftIO $ do
107+ ref <- newIORef $ error "runResponse gave no result"
108+ ResponseReceived <- app req' (runResponse ref)
109+ readIORef ref
108110 -- FIXME cookie processing
109111 --return sres
110112
111−runResponse :: Response -> IO SResponse
112−runResponse res = do
113+runResponse :: IORef SResponse -> Response -> IO ResponseReceived
114+runResponse ref res = do
113115 refBuilder <- newIORef mempty
114116 let add y = atomicModifyIORef refBuilder $ \x -> (x `mappend` y, ())
115117 withBody $ \body -> body add (return ())
116118 builder <- readIORef refBuilder
117− return $ SResponse s h $ toLazyByteString builder
119+ writeIORef ref $ SResponse s h $ toLazyByteString builder
120+ return ResponseReceived
118121 where
119122 (s, h, withBody) = responseToStream res
120123
wai-extra/Network/Wai/UrlMap.hs+7−9
@@ -50,13 +50,13 @@ instance Alternative UrlMap' where
5050 empty = UrlMap' empty
5151 (UrlMap' xs) <|> (UrlMap' ys) = UrlMap' (xs <|> ys)
5252
53−type UrlMap = UrlMap' Application'
53+type UrlMap = UrlMap' Application
5454
5555-- | Mount an application under a given path. The ToApplication typeclass gives
5656-- you the option to pass either an 'Network.Wai.Application' or an 'UrlMap'
5757-- as the second argument.
5858mount' :: ToApplication a => Path -> a -> UrlMap
59−mount' prefix thing = UrlMap' [(prefix, Application' $ toApplication thing)]
59+mount' prefix thing = UrlMap' [(prefix, toApplication thing)]
6060
6161-- | A convenience function like mount', but for mounting things under a single
6262-- path segment.
@@ -78,15 +78,15 @@ try xs tuples = foldl go Nothing tuples
7878 go _ (prefix, y) = stripPrefix prefix xs >>= \xs' -> return (xs', y)
7979
8080class ToApplication a where
81− toApplication :: a -> Application'
81+ toApplication :: a -> Application
8282
8383instance ToApplication Application where
84− toApplication = Application'
84+ toApplication = id
8585
8686instance ToApplication UrlMap where
87− toApplication urlMap req sendResponse = Application' $
87+ toApplication urlMap req sendResponse =
8888 case try (pathInfo req) (unUrlMap urlMap) of
89− Just (newPath, Application' app) ->
89+ Just (newPath, app) ->
9090 app (req { pathInfo = newPath
9191 , rawPathInfo = makeRaw newPath
9292 }) sendResponse
@@ -101,6 +101,4 @@ instance ToApplication UrlMap where
101101 makeRaw = ("/" `B.append`) . T.encodeUtf8 . T.intercalate "/"
102102
103103mapUrls :: UrlMap -> Application
104−mapUrls um =
105− case toApplication um of
106− Application' a -> a
104+mapUrls = toApplication
wai-extra/wai-extra.cabal+1
@@ -44,6 +44,7 @@ Library
4444 , word8
4545 , lifted-base >= 0.1.2
4646 , deepseq
47+ , streaming-commons
4748
4849 if os(windows)
4950 cpp-options: -DWINDOWS