on master help sign in
splitting modules.
commitdbcd95eba0c8f8a1e1251966f91618e72405dba9 authorKazu Yamamoto <kazu@iij.ad.jp> dateTue 14 Aug 2012, 15:34 (14 years ago) parent00f103e
10 files changed+923−860
warp/Network/Wai/Handler/Warp.hs+49−818 warp/Network/Wai/Handler/Warp/Conduit.hs+148 warp/ReadInt.hs → warp/Network/Wai/Handler/Warp/ReadInt.hs+6−3 warp/Network/Wai/Handler/Warp/Request.hs+211 warp/Network/Wai/Handler/Warp/Response.hs+188 warp/Network/Wai/Handler/Warp/Run.hs+145 warp/Network/Wai/Handler/Warp/Settings.hs+52 warp/Timeout.hs → warp/Network/Wai/Handler/Warp/Timeout.hs+23−13 warp/Network/Wai/Handler/Warp/Types.hs+69 warp/warp.cabal+32−26
warp/Network/Wai/Handler/Warp.hs+49−818
@@ -1,11 +1,5 @@
1−{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE OverloadedStrings #-}
41{-# LANGUAGE CPP #-}
5−{-# LANGUAGE BangPatterns #-}
6−{-# LANGUAGE MagicHash #-}
7−{-# LANGUAGE ScopedTypeVariables #-}
8−{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
2+
93---------------------------------------------------------
104--
115-- Module : Network.Wai.Handler.Warp
@@ -21,816 +15,53 @@
2115---------------------------------------------------------
2216
2317-- | A fast, light-weight HTTP server handler for WAI.
24−module Network.Wai.Handler.Warp
25− ( -- * Run a Warp server
26− run
27− , runSettings
28− , runSettingsSocket
29− -- * Settings
30− , Settings
31− , defaultSettings
32− , settingsPort
33− , settingsHost
34− , settingsOnException
35− , settingsOnOpen
36− , settingsOnClose
37− , settingsTimeout
38− , settingsIntercept
39− , settingsManager
40− -- ** Data types
41− , HostPreference (..)
42− -- * Connection
43− , Connection (..)
44− , runSettingsConnection
45− -- * Datatypes
46− , Port
47− , InvalidRequest (..)
48− -- * Internal
49− , Manager
50− , withManager
51− , parseRequest
52− , sendResponse
53− , registerKillThread
54− , pause
55− , resume
56− , T.cancel
57− , T.register
58− , T.initialize
59− , socketConnection
18+module Network.Wai.Handler.Warp (
19+ -- * Run a Warp server
20+ run
21+ , runSettings
22+ , runSettingsSocket
23+ -- * Settings
24+ , Settings
25+ , defaultSettings
26+ , settingsPort
27+ , settingsHost
28+ , settingsOnException
29+ , settingsOnOpen
30+ , settingsOnClose
31+ , settingsTimeout
32+ , settingsIntercept
33+ , settingsManager
34+ -- ** Data types
35+ , HostPreference (..)
36+ -- * Connection
37+ , Connection (..)
38+ , runSettingsConnection
39+ -- * Datatypes
40+ , Port
41+ , InvalidRequest (..)
42+ -- * Internal
43+ , Manager
44+ , withManager
45+ , parseRequest
46+ , sendResponse
47+ , registerKillThread
48+ , pause
49+ , resume
50+ , cancel
51+ , register
52+ , initialize
53+ , socketConnection
6054#if TEST
61− , takeHeaders
62− , parseFirst
63− , readInt
64−#endif
65− ) where
66−
67−import Prelude hiding (lines, catch)
68−import Network.Wai
69−
70−import Data.ByteString (ByteString)
71−import qualified Data.ByteString as S
72−import qualified Data.ByteString.Unsafe as SU
73−import qualified Data.ByteString.Char8 as B
74−import qualified Data.ByteString.Lazy as L
75−import Network (sClose, Socket)
76−import Network.Socket (accept, SockAddr)
77−import qualified Network.Socket.ByteString as Sock
78−import Control.Exception
79− ( mask, catch, handle, onException, bracket
80− , Exception, SomeException, IOException, AsyncException (ThreadKilled)
81− , fromException, toException
82− , try
83−#if __GLASGOW_HASKELL__ >= 702
84− , allowInterrupt
85−#else
86− , unblock
87−#endif
88−#if WINDOWS
89− , finally
90−#endif
91− )
92−import Control.Concurrent (forkIO, threadDelay)
93−import Data.Maybe (fromMaybe, isJust)
94−import Data.Char (toLower, isHexDigit)
95−import Data.Word (Word)
96−
97−import Data.Typeable (Typeable)
98−
99−import Data.Conduit
100−import Data.Conduit.Internal (ResumableSource (..))
101−import qualified Data.Conduit.List as CL
102−import qualified Data.Conduit.Binary as CB
103−import Data.Conduit.Blaze (builderToByteString)
104−import Control.Exception.Lifted (throwIO)
105−import Blaze.ByteString.Builder.HTTP
106− (chunkedTransferEncoding, chunkedTransferTerminator)
107−import Blaze.ByteString.Builder
108− (copyByteString, Builder, toLazyByteString, toByteStringIO, flush)
109−import Blaze.ByteString.Builder.Char8 (fromChar, fromShow)
110−import Data.Monoid (mappend, mempty)
111−import Network.Sendfile
112−
113−import qualified System.PosixCompat.Files as P
114−
115−import Control.Monad.IO.Class (MonadIO, liftIO)
116−import Control.Monad.Trans.Class (lift)
117−import qualified Timeout as T
118−import Timeout (Manager, registerKillThread, pause, resume)
119−import Data.Word (Word8)
120−import Data.List (foldl')
121−import Control.Monad (forever, when, void)
122−import qualified Network.HTTP.Types as H
123−import qualified Data.CaseInsensitive as CI
124−import System.IO (hPrint, stderr)
125−import ReadInt (readInt64)
126−import qualified Data.IORef as I
127−import Data.Conduit.Network (bindPort, HostPreference (HostIPv4))
128−
129−#if WINDOWS
130−import qualified Control.Concurrent.MVar as MV
131−import Network.Socket (withSocketsDo)
132−#endif
133−
134−import Data.Version (showVersion)
135−import qualified Paths_warp
136−
137−warpVersion :: String
138−warpVersion = showVersion Paths_warp.version
139−
140−#if __GLASGOW_HASKELL__ < 702
141−allowInterrupt :: IO ()
142−allowInterrupt = unblock $ return ()
143−#endif
144−
145−-- |
146−--
147−-- In order to provide slowloris protection, Warp provides timeout handlers. We
148−-- follow these rules:
149−--
150−-- * A timeout is created when a connection is opened.
151−--
152−-- * When all request headers are read, the timeout is tickled.
153−--
154−-- * Every time at least 2048 bytes of the request body are read, the timeout
155−-- is tickled.
156−--
157−-- * The timeout is paused while executing user code. This will apply to both
158−-- the application itself, and a ResponseSource response. The timeout is
159−-- resumed as soon as we return from user code.
160−--
161−-- * Every time data is successfully sent to the client, the timeout is tickled.
162−data Connection = Connection
163− { connSendMany :: [B.ByteString] -> IO ()
164− , connSendAll :: B.ByteString -> IO ()
165− , connSendFile :: FilePath -> Integer -> Integer -> IO () -> [ByteString] -> IO () -- ^ offset, length
166− , connClose :: IO ()
167− , connRecv :: IO B.ByteString
168− }
169−
170−socketConnection :: Socket -> Connection
171−socketConnection s = Connection
172− { connSendMany = Sock.sendMany s
173− , connSendAll = Sock.sendAll s
174− , connSendFile = \fp off len act hdr -> sendfileWithHeader s fp (PartOfFile off len) act hdr
175− , connClose = sClose s
176− , connRecv = Sock.recv s bytesPerRead
177− }
178−
179−-- | Run an 'Application' on the given port. This calls 'runSettings' with
180−-- 'defaultSettings'.
181−run :: Port -> Application -> IO ()
182−run p = runSettings defaultSettings { settingsPort = p }
183−
184−-- | Run a Warp server with the given settings.
185−runSettings :: Settings -> Application -> IO ()
186−#if WINDOWS
187−runSettings set app = withSocketsDo $ do
188− var <- MV.newMVar Nothing
189− let clean = MV.modifyMVar_ var $ \s -> maybe (return ()) sClose s >> return Nothing
190− _ <- forkIO $ bracket
191− (bindPort (settingsPort set) (settingsHost set))
192− (const clean)
193− (\s -> do
194− MV.modifyMVar_ var (\_ -> return $ Just s)
195− runSettingsSocket set s app)
196− forever (threadDelay maxBound) `finally` clean
197−#else
198−runSettings set =
199− bracket
200− (bindPort (settingsPort set) (settingsHost set))
201− sClose .
202− flip (runSettingsSocket set)
55+ , takeHeaders
56+ , parseFirst
57+ , readInt
20358#endif
204−
205−type Port = Int
206−
207−-- | Same as 'runSettings', but uses a user-supplied socket instead of opening
208−-- one. This allows the user to provide, for example, Unix named socket, which
209−-- can be used when reverse HTTP proxying into your application.
210−--
211−-- Note that the 'settingsPort' will still be passed to 'Application's via the
212−-- 'serverPort' record.
213−runSettingsSocket :: Settings -> Socket -> Application -> IO ()
214−runSettingsSocket set socket app =
215− runSettingsConnection set getter app
216− where
217− getter = do
218− (conn, sa) <- accept socket
219− return (socketConnection conn, sa)
220−
221−runSettingsConnection :: Settings -> IO (Connection, SockAddr) -> Application -> IO ()
222−runSettingsConnection set getConn app = do
223− tm <- maybe (T.initialize $ settingsTimeout set * 1000000) return
224− $ settingsManager set
225− mask $ \restore -> forever $ do
226− allowInterrupt
227− (conn, addr) <- getConnLoop
228− void $ forkIO $ do
229− th <- T.registerKillThread tm
230− handle onE $ (do onOpen
231− restore $ serveConnection set th port app conn addr
232− connClose conn >> T.cancel th >> onClose
233− ) `onException` (T.cancel th >> connClose conn >> onClose)
234− where
235− -- FIXME: only IOEception is caught. What about other exceptions?
236− getConnLoop = getConn `catch` \(e :: IOException) -> do
237− onE (toException e)
238− -- "resource exhausted (Too many open files)" may happen by accept().
239− -- Wait a second hoping that resource will be available.
240− threadDelay 1000000
241− getConnLoop
242− onE = settingsOnException set
243− port = settingsPort set
244− onOpen = settingsOnOpen set
245− onClose = settingsOnClose set
246−
247−-- | Contains a @Source@ and a byte count that is still to be read in.
248−newtype IsolatedBSSource = IsolatedBSSource (I.IORef (Int, ResumableSource (ResourceT IO) ByteString))
249−
250−-- | Given an @IsolatedBSSource@ provide a @Source@ that only allows up to the
251−-- specified number of bytes to be passed downstream. All leftovers should be
252−-- retained within the @Source@. If there are not enough bytes available,
253−-- throws a @ConnectionClosedByPeer@ exception.
254−ibsIsolate :: IsolatedBSSource -> Source (ResourceT IO) ByteString
255−ibsIsolate ibs@(IsolatedBSSource ref) = do
256− (count, src) <- liftIO $ I.readIORef ref
257− if count == 0
258− -- No more bytes wanted downstream, so we're done.
259− then return ()
260− else do
261− -- Get the next chunk (if available) and the updated source
262− (src', mbs) <- lift $ src $$++ CL.head
263−
264− -- If no chunk available, then there aren't enough bytes in the
265− -- stream. Throw a ConnectionClosedByPeer
266− bs <- maybe (liftIO $ throwIO ConnectionClosedByPeer) return mbs
267−
268− let -- How many of the bytes in this chunk to send downstream
269− toSend = min count (S.length bs)
270− -- How many bytes will still remain to be sent downstream
271− count' = count - toSend
272− case () of
273− ()
274− -- The expected count is greater than the size of the
275− -- chunk we just read. Send the entire chunk
276− -- downstream, and then loop on this function for the
277− -- next chunk.
278− | count' > 0 -> do
279− liftIO $ I.writeIORef ref (count', src')
280− yield bs
281− ibsIsolate ibs
282−
283− -- The expected count is the total size of the chunk we
284− -- just read. Send this chunk downstream, and then
285− -- terminate the stream.
286− | count == S.length bs -> do
287− liftIO $ I.writeIORef ref (count', src')
288− yield bs
289−
290− -- Some of the bytes in this chunk should not be sent
291− -- downstream. Split up the chunk into the sent and
292− -- not-sent parts, add the not-sent parts onto the new
293− -- source, and send the rest of the chunk downstream.
294− | otherwise -> do
295− let (x, y) = S.splitAt toSend bs
296− liftIO $ I.writeIORef ref (count', fmapResume (yield y >>) src')
297− yield x
298−
299−-- | Extract the underlying @Source@ from an @IsolatedBSSource@, which will not
300−-- perform any more isolation.
301−ibsDone :: IsolatedBSSource -> IO (ResumableSource (ResourceT IO) ByteString)
302−ibsDone (IsolatedBSSource ref) = fmap snd $ I.readIORef ref
303−
304−serveConnection :: Settings
305− -> T.Handle
306− -> Port -> Application -> Connection -> SockAddr-> IO ()
307−serveConnection settings th port app conn remoteHost' =
308− runResourceT serveConnection'
309− where
310− serveConnection' :: ResourceT IO ()
311− serveConnection' = do
312− let fromClient = connSource conn th
313− serveConnection'' fromClient
314−
315− serveConnection'' fromClient = do
316− (env, getSource) <- parseRequest conn port remoteHost' fromClient
317− case settingsIntercept settings env of
318− Nothing -> do
319− -- Let the application run for as long as it wants
320− liftIO $ T.pause th
321− res <- app env
322−
323− liftIO $ T.resume th
324− keepAlive <- sendResponse th env conn res
325−
326− -- flush the rest of the request body
327− requestBody env $$ CL.sinkNull
328− ResumableSource fromClient' _ <- liftIO getSource
329−
330− when keepAlive $ serveConnection'' fromClient'
331− Just intercept -> do
332− liftIO $ T.pause th
333− ResumableSource fromClient' _ <- liftIO getSource
334− intercept fromClient' conn
335−
336−parseRequest :: Connection -> Port -> SockAddr
337− -> Source (ResourceT IO) S.ByteString
338− -> ResourceT IO (Request, IO (ResumableSource (ResourceT IO) ByteString))
339−parseRequest conn port remoteHost' src1 = do
340− (src2, headers') <- src1 $$+ takeHeaders
341− parseRequest' conn port headers' remoteHost' src2
342−
343−-- FIXME come up with good values here
344−bytesPerRead, maxTotalHeaderLength :: Int
345−bytesPerRead = 4096
346−maxTotalHeaderLength = 50 * 1024
347−
348−data InvalidRequest =
349− NotEnoughLines [String]
350− | BadFirstLine String
351− | NonHttp
352− | IncompleteHeaders
353− | ConnectionClosedByPeer
354− | OverLargeHeader
355− deriving (Show, Typeable, Eq)
356−instance Exception InvalidRequest
357−
358−handleExpect :: Connection
359− -> H.HttpVersion
360− -> ([H.Header] -> [H.Header])
361− -> [H.Header]
362− -> IO [H.Header]
363−handleExpect _ _ front [] = return $ front []
364−handleExpect conn hv front (("expect", "100-continue"):rest) = do
365− connSendAll conn $
366− if hv == H.http11
367− then "HTTP/1.1 100 Continue\r\n\r\n"
368− else "HTTP/1.0 100 Continue\r\n\r\n"
369− return $ front rest
370−handleExpect conn hv front (x:xs) = handleExpect conn hv (front . (x:)) xs
371−
372−-- | Parse a set of header lines and body into a 'Request'.
373−parseRequest' :: Connection
374− -> Port
375− -> [ByteString]
376− -> SockAddr
377− -> ResumableSource (ResourceT IO) S.ByteString -- FIXME was buffered
378− -> ResourceT IO (Request, IO (ResumableSource (ResourceT IO) ByteString))
379−parseRequest' _ _ [] _ _ = throwIO $ NotEnoughLines []
380−parseRequest' conn port (firstLine:otherLines) remoteHost' src = do
381− (method, rpath', gets, httpversion) <- parseFirst firstLine
382− let (host',rpath)
383− | S.null rpath' = ("", "/")
384− | "http://" `S.isPrefixOf` rpath' = S.breakByte 47 $ S.drop 7 rpath'
385− | otherwise = ("", rpath')
386− heads <- liftIO
387− $ handleExpect conn httpversion id
388− (map parseHeaderNoAttr otherLines)
389− let host = fromMaybe host' $ lookup "host" heads
390− let len0 =
391− case lookup "content-length" heads of
392− Nothing -> 0
393− Just bs -> readInt bs
394− let serverName' = takeUntil 58 host -- ':'
395− let chunked = maybe False ((== "chunked") . B.map toLower)
396− $ lookup "transfer-encoding" heads
397− (rbody, getSource) <- liftIO $
398− if chunked
399− then do
400− ref <- I.newIORef (src, NeedLen)
401− return (chunkedSource ref, fmap fst $ I.readIORef ref)
402− else do
403− ibs <- fmap IsolatedBSSource $ I.newIORef (len0, src)
404− return (ibsIsolate ibs, ibsDone ibs)
405−
406− return (Request
407− { requestMethod = method
408− , httpVersion = httpversion
409− , pathInfo = H.decodePathSegments rpath
410− , rawPathInfo = rpath
411− , rawQueryString = gets
412− , queryString = H.parseQuery gets
413− , serverName = serverName'
414− , serverPort = port
415− , requestHeaders = heads
416− , isSecure = False
417− , remoteHost = remoteHost'
418− , requestBody = rbody
419− , vault = mempty
420− }, getSource)
421−
422−data ChunkState = NeedLen
423− | NeedLenNewline
424− | HaveLen Word
425−
426−chunkedSource :: MonadIO m
427− => I.IORef (ResumableSource m ByteString, ChunkState)
428− -> Source m ByteString
429−chunkedSource ipair = do
430− (src, mlen) <- liftIO $ I.readIORef ipair
431− go src mlen
432− where
433− go' src front = do
434− (src', (len, bs)) <- lift $ src $$++ front getLen
435− let src''
436− | S.null bs = src'
437− | otherwise = fmapResume (yield bs >>) src'
438− go src'' $ HaveLen len
439−
440− go src NeedLen = go' src id
441− go src NeedLenNewline = go' src (CB.take 2 >>)
442− go src (HaveLen 0) = liftIO $ I.writeIORef ipair (src, HaveLen 0)
443− go src (HaveLen len) = do
444− (src', mbs) <- lift $ src $$++ CL.head
445− case mbs of
446− Nothing -> liftIO $ I.writeIORef ipair (src', HaveLen 0)
447− Just bs ->
448− case S.length bs `compare` fromIntegral len of
449− EQ -> yield' src' NeedLenNewline bs
450− LT -> do
451− let mlen = HaveLen $ len - fromIntegral (S.length bs)
452− yield' src' mlen bs
453− GT -> do
454− let (x, y) = S.splitAt (fromIntegral len) bs
455− let src'' = fmapResume (yield y >>) src'
456− yield' src'' NeedLenNewline x
457−
458− yield' src mlen bs = do
459− liftIO $ I.writeIORef ipair (src, mlen)
460− yield bs
461− go src mlen
462−
463− getLen :: Monad m => Sink ByteString m (Word, ByteString)
464− getLen = do
465− mbs <- CL.head
466− case mbs of
467− Nothing -> return (0, S.empty)
468− Just bs -> do
469− (x, y) <-
470− case S.breakByte 10 bs of
471− (x, y)
472− | S.null y -> do
473− mbs2 <- CL.head
474− case mbs2 of
475− Nothing -> return (x, y)
476− Just bs2 -> return $ S.breakByte 10 $ bs `S.append` bs2
477− | otherwise -> return (x, y)
478− let w =
479− S.foldl' (\i c -> i * 16 + fromIntegral (hexToWord c)) 0
480− $ B.takeWhile isHexDigit x
481− return (w, S.drop 1 y)
482−
483− hexToWord w
484− | w < 58 = w - 48
485− | w < 71 = w - 55
486− | otherwise = w - 87
487−
488−takeUntil :: Word8 -> ByteString -> ByteString
489−takeUntil c bs =
490− case S.elemIndex c bs of
491− Just !idx -> SU.unsafeTake idx bs
492− Nothing -> bs
493−{-# INLINE takeUntil #-}
494−
495−parseFirst :: ByteString
496− -> ResourceT IO (ByteString, ByteString, ByteString, H.HttpVersion)
497−parseFirst s =
498− case filter (not . S.null) $ S.splitWith (\c -> c == 32 || c == 9) s of -- ' '
499− (method:query:http'') -> do
500− let http' = S.concat http''
501− (hfirst, hsecond) = B.splitAt 5 http'
502− if hfirst == "HTTP/"
503− then let (rpath, qstring) = S.breakByte 63 query -- '?'
504− hv =
505− case hsecond of
506− "1.1" -> H.http11
507− _ -> H.http10
508− in return (method, rpath, qstring, hv)
509− else throwIO NonHttp
510− _ -> throwIO $ BadFirstLine $ B.unpack s
511−{-# INLINE parseFirst #-} -- FIXME is this inline necessary? the function is only called from one place and not exported
512−
513−httpBuilder, spaceBuilder, newlineBuilder, transferEncodingBuilder
514− , colonSpaceBuilder :: Builder
515−httpBuilder = copyByteString "HTTP/"
516−spaceBuilder = fromChar ' '
517−newlineBuilder = copyByteString "\r\n"
518−transferEncodingBuilder = copyByteString "Transfer-Encoding: chunked\r\n\r\n"
519−colonSpaceBuilder = copyByteString ": "
520−
521−headers :: H.HttpVersion -> H.Status -> H.ResponseHeaders -> Bool -> Builder
522−headers !httpversion !status !responseHeaders !isChunked' = {-# SCC "headers" #-}
523− let !start = httpBuilder
524− `mappend` copyByteString
525− (case httpversion of
526− H.HttpVersion 1 1 -> "1.1"
527− _ -> "1.0")
528− `mappend` spaceBuilder
529− `mappend` fromShow (H.statusCode status)
530− `mappend` spaceBuilder
531− `mappend` copyByteString (H.statusMessage status)
532− `mappend` newlineBuilder
533− !start' = foldl' responseHeaderToBuilder start (serverHeader responseHeaders)
534− !end = if isChunked'
535− then transferEncodingBuilder
536− else newlineBuilder
537− in start' `mappend` end
538−
539−responseHeaderToBuilder :: Builder -> H.Header -> Builder
540−responseHeaderToBuilder b (x, y) = b
541− `mappend` copyByteString (CI.original x)
542− `mappend` colonSpaceBuilder
543− `mappend` copyByteString y
544− `mappend` newlineBuilder
545−
546−checkPersist :: Request -> Bool
547−checkPersist req
548− | ver == H.http11 = checkPersist11 conn
549− | otherwise = checkPersist10 conn
550− where
551− ver = httpVersion req
552− conn = lookup "connection" $ requestHeaders req
553− checkPersist11 (Just x)
554− | CI.foldCase x == "close" = False
555− checkPersist11 _ = True
556− checkPersist10 (Just x)
557− | CI.foldCase x == "keep-alive" = True
558− checkPersist10 _ = False
559−
560−isChunked :: H.HttpVersion -> Bool
561−isChunked = (==) H.http11
562−
563−hasBody :: H.Status -> Request -> Bool
564−hasBody s req = s /= H.Status 204 "" && s /= H.status304 &&
565− H.statusCode s >= 200 && requestMethod req /= "HEAD"
566−
567−sendResponse :: T.Handle
568− -> Request -> Connection -> Response -> ResourceT IO Bool
569−sendResponse th req conn r = sendResponse' r
570− where
571− version = httpVersion req
572− isPersist = checkPersist req
573− isChunked' = isChunked version
574− needsChunked hs = isChunked' && not (hasLength hs)
575− isKeepAlive hs = isPersist && (isChunked' || hasLength hs)
576− hasLength hs = isJust $ lookup "content-length" hs
577− sendHeader = connSendMany conn . L.toChunks . toLazyByteString
578−
579− sendResponse' :: Response -> ResourceT IO Bool
580− sendResponse' (ResponseFile s hs fp mpart) = do
581− eres <-
582− case (readInt `fmap` lookup "content-length" hs, mpart) of
583− (Just cl, _) -> return $ Right (hs, cl)
584− (Nothing, Nothing) -> liftIO $ try $ do
585− cl <- P.fileSize `fmap` P.getFileStatus fp
586− return $ addClToHeaders cl
587− (Nothing, Just part) -> do
588− let cl = filePartByteCount part
589− return $ Right $ addClToHeaders cl
590− case eres of
591− Left (_ :: SomeException) -> sendResponse' $ responseLBS
592− H.status404
593− [("Content-Type", "text/plain")]
594− "File not found"
595− Right (lengthyHeaders, cl) -> liftIO $ do
596− let headers' = L.toChunks . toLazyByteString $ headers version s lengthyHeaders False
597− T.tickle th
598− if hasBody s req then do
599− case mpart of
600− Nothing -> connSendFile conn fp 0 cl (T.tickle th) headers'
601− Just part -> connSendFile conn fp (filePartOffset part) (filePartByteCount part) (T.tickle th) headers'
602− T.tickle th
603− return isPersist
604− else
605− return isPersist
606− where
607− addClToHeaders cl = (("Content-Length", B.pack $ show cl):hs, fromIntegral cl)
608−
609− sendResponse' (ResponseBuilder s hs b)
610− | hasBody s req = liftIO $ do
611− toByteStringIO (\bs -> do
612− connSendAll conn bs
613− T.tickle th) body
614− return (isKeepAlive hs)
615− | otherwise = liftIO $ do
616− sendHeader $ headers' False
617− T.tickle th
618− return isPersist
619− where
620− headers' = headers version s hs
621− needsChunked' = needsChunked hs
622− body = if needsChunked'
623− then headers' needsChunked'
624− `mappend` chunkedTransferEncoding b
625− `mappend` chunkedTransferTerminator
626− else headers' False `mappend` b
627−
628− sendResponse' (ResponseSource s hs bodyFlush)
629− | hasBody s req = do
630− let src = CL.sourceList [headers' needsChunked'] `mappend`
631− (if needsChunked' then body $= chunk else body)
632− src $$ builderToByteString =$ connSink conn th
633− return $ isKeepAlive hs
634− | otherwise = liftIO $ do
635− sendHeader $ headers' False
636− T.tickle th
637− return isPersist
638− where
639− body = mapOutput (\x -> case x of
640− Flush -> flush
641− Chunk builder -> builder) bodyFlush
642− headers' = headers version s hs
643− -- FIXME perhaps alloca a buffer per thread and reuse that in all
644− -- functions below. Should lessen greatly the GC burden (I hope)
645− needsChunked' = needsChunked hs
646− chunk :: Conduit Builder (ResourceT IO) Builder
647− chunk = await >>= maybe (yield chunkedTransferTerminator) (\x -> yield (chunkedTransferEncoding x) >> chunk)
648−
649−parseHeaderNoAttr :: ByteString -> H.Header
650−parseHeaderNoAttr s =
651− let (k, rest) = S.breakByte 58 s -- ':'
652− rest' = S.dropWhile (\c -> c == 32 || c == 9) $ S.drop 1 rest
653− in (CI.mk k, rest')
654−
655−connSource :: Connection -> T.Handle -> Source (ResourceT IO) ByteString
656−connSource Connection { connRecv = recv } th =
657− src
658− where
659− src = do
660− bs <- liftIO recv
661− if S.null bs
662− then return ()
663− else do
664− when (S.length bs >= 2048) $ liftIO $ T.tickle th
665− yield bs
666− src
667−
668−-- | Use 'connSendAll' to send this data while respecting timeout rules.
669−connSink :: Connection -> T.Handle -> Sink B.ByteString (ResourceT IO) ()
670−connSink Connection { connSendAll = send } th =
671− sink
672− where
673− sink = await >>= maybe close push
674− close = liftIO (T.resume th)
675− push x = do
676− liftIO $ T.resume th
677− liftIO $ send x
678− liftIO $ T.pause th
679− sink
680− -- We pause timeouts before passing control back to user code. This ensures
681− -- that a timeout will only ever be executed when Warp is in control. We
682− -- also make sure to resume the timeout after the completion of user code
683− -- so that we can kill idle connections.
684−
685−------ The functions below are not warp-specific and could be split out into a
686−--separate package.
687−
688−-- | Various Warp server settings. This is purposely kept as an abstract data
689−-- type so that new settings can be added without breaking backwards
690−-- compatibility. In order to create a 'Settings' value, use 'defaultSettings'
691−-- and record syntax to modify individual records. For example:
692−--
693−-- > defaultSettings { settingsTimeout = 20 }
694−data Settings = Settings
695− { settingsPort :: Int -- ^ Port to listen on. Default value: 3000
696− , settingsHost :: HostPreference -- ^ Default value: HostIPv4
697− , settingsOnException :: SomeException -> IO () -- ^ What to do with exceptions thrown by either the application or server. Default: ignore server-generated exceptions (see 'InvalidRequest') and print application-generated applications to stderr.
698− , settingsOnOpen :: IO () -- ^ What to do when a connection is open. Default: do nothing.
699− , settingsOnClose :: IO () -- ^ What to do when a connection is close. Default: do nothing.
700− , settingsTimeout :: Int -- ^ Timeout value in seconds. Default value: 30
701− , settingsIntercept :: Request -> Maybe (Source (ResourceT IO) S.ByteString -> Connection -> ResourceT IO ())
702− , settingsManager :: Maybe Manager -- ^ Use an existing timeout manager instead of spawning a new one. If used, 'settingsTimeout' is ignored. Default is 'Nothing'
703− }
704−
705−-- | The default settings for the Warp server. See the individual settings for
706−-- the default value.
707−defaultSettings :: Settings
708−defaultSettings = Settings
709− { settingsPort = 3000
710− , settingsHost = HostIPv4
711− , settingsOnException = \e ->
712− case fromException e of
713− Just x -> go x
714− Nothing ->
715− when (go' $ fromException e) $
716− hPrint stderr e
717− , settingsOnOpen = return ()
718− , settingsOnClose = return ()
719− , settingsTimeout = 30
720− , settingsIntercept = const Nothing
721− , settingsManager = Nothing
722− }
723− where
724− go :: InvalidRequest -> IO ()
725− go _ = return ()
726− go' (Just ThreadKilled) = False
727− go' _ = True
728−
729−type BSEndo = ByteString -> ByteString
730−type BSEndoList = [ByteString] -> [ByteString]
731−
732−data THStatus = THStatus
733− {-# UNPACK #-} !Int -- running total byte count
734− BSEndoList -- previously parsed lines
735− BSEndo -- bytestrings to be prepended
736−
737−takeHeaders :: Sink ByteString (ResourceT IO) [ByteString]
738−takeHeaders =
739− await >>= maybe (throwIO ConnectionClosedByPeer) (push (THStatus 0 id id))
740− where
741− close :: Sink ByteString (ResourceT IO) a
742− close = throwIO IncompleteHeaders
743−
744− push (THStatus len lines prepend) bs
745− -- Too many bytes
746− | len > maxTotalHeaderLength = throwIO OverLargeHeader
747− | otherwise =
748− case mnl of
749− -- No newline find in this chunk. Add it to the prepend,
750− -- update the length, and continue processing.
751− Nothing ->
752− let len' = len + bsLen
753− prepend' = prepend . S.append bs
754− status = THStatus len' lines prepend'
755− in await >>= maybe close (push status)
756− -- Found a newline, but next line continues as a multiline header
757− Just (end, True) ->
758− let rest = S.drop (end + 1) bs
759− prepend' = prepend . S.append (SU.unsafeTake (checkCR bs end) bs)
760− len' = len + end
761− status = THStatus len' lines prepend'
762− in push status rest
763− -- Found a newline at position end.
764− Just (end, False) ->
765− let start = end + 1 -- start of next chunk
766− line
767− -- There were some bytes before the newline, get them
768− | end > 0 = prepend $ SU.unsafeTake (checkCR bs end) bs
769− -- No bytes before the newline
770− | otherwise = prepend S.empty
771− in if S.null line
772− -- no more headers
773− then
774− let lines' = lines []
775− -- leftover
776− rest = if start < bsLen
777− then Just (SU.unsafeDrop start bs)
778− else Nothing
779− in maybe (return ()) leftover rest >> return lines'
780− -- more headers
781− else
782− let len' = len + start
783− lines' = lines . (line:)
784− status = THStatus len' lines' id
785− in if start < bsLen
786− -- more bytes in this chunk, push again
787− then let bs' = SU.unsafeDrop start bs
788− in push status bs'
789− -- no more bytes in this chunk, ask for more
790− else await >>= maybe close (push status)
791− where
792− bsLen = S.length bs
793− mnl = do
794− nl <- S.elemIndex 10 bs
795− -- check if there are two more bytes in the bs
796− -- if so, see if the second of those is a horizontal space
797− if bsLen > nl + 1
798− then
799− let c = S.index bs (nl + 1)
800− in Just (nl, c == 32 || c == 9)
801− else Just (nl, False)
802−{-# INLINE takeHeaders #-}
803−
804−checkCR :: ByteString -> Int -> Int
805−checkCR bs pos =
806− let !p = pos - 1
807− in if '\r' == B.index bs p
808− then p
809− else pos
810−{-# INLINE checkCR #-}
811−
812−readInt :: Integral a => ByteString -> a
813−readInt bs = fromIntegral $ readInt64 bs
814−{-# INLINE readInt #-}
815−
816−
817−-- | Call the inner function with a timeout manager.
818−withManager :: Int -- ^ timeout in microseconds
819− -> (Manager -> IO a)
820− -> IO a
821−withManager timeout f = do
822− -- FIXME when stopManager is available, use it
823− man <- T.initialize timeout
824− f man
825−
826−serverHeader :: H.RequestHeaders -> H.RequestHeaders
827−serverHeader hdrs = case lookup key hdrs of
828− Nothing -> server : hdrs
829− Just _ -> hdrs
830− where
831− key = "Server"
832− ver = B.pack $ "Warp/" ++ warpVersion
833− server = (key, ver)
834−
835−fmapResume :: (Source m o1 -> Source m o2) -> ResumableSource m o1 -> ResumableSource m o2
836−fmapResume f (ResumableSource src m) = ResumableSource (f src) m
59+ ) where
60+
61+import Network.Wai.Handler.Warp.Request
62+import Network.Wai.Handler.Warp.Response
63+import Network.Wai.Handler.Warp.Run
64+import Network.Wai.Handler.Warp.Settings
65+import Network.Wai.Handler.Warp.Types
66+import Network.Wai.Handler.Warp.Timeout
67+import Data.Conduit.Network (HostPreference(..))
warp/Network/Wai/Handler/Warp/Conduit.hsnew file+148
@@ -0,0 +1,148 @@
1+module Network.Wai.Handler.Warp.Conduit where
2+
3+import Control.Exception
4+import Control.Monad.IO.Class (MonadIO, liftIO)
5+import Control.Monad.Trans.Class (lift)
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as S
8+import qualified Data.ByteString.Char8 as B
9+import Data.Char (isHexDigit)
10+import Data.Conduit
11+import qualified Data.Conduit.Binary as CB
12+import Data.Conduit.Internal (ResumableSource (..))
13+import qualified Data.Conduit.List as CL
14+import qualified Data.IORef as I
15+import Data.Word (Word)
16+import Network.Wai.Handler.Warp.Types
17+
18+----------------------------------------------------------------
19+
20+-- | Contains a @Source@ and a byte count that is still to be read in.
21+newtype IsolatedBSSource = IsolatedBSSource (I.IORef (Int, ResumableSource (ResourceT IO) ByteString))
22+
23+-- | Given an @IsolatedBSSource@ provide a @Source@ that only allows up to the
24+-- specified number of bytes to be passed downstream. All leftovers should be
25+-- retained within the @Source@. If there are not enough bytes available,
26+-- throws a @ConnectionClosedByPeer@ exception.
27+ibsIsolate :: IsolatedBSSource -> Source (ResourceT IO) ByteString
28+ibsIsolate ibs@(IsolatedBSSource ref) = do
29+ (count, src) <- liftIO $ I.readIORef ref
30+ if count == 0
31+ -- No more bytes wanted downstream, so we're done.
32+ then return ()
33+ else do
34+ -- Get the next chunk (if available) and the updated source
35+ (src', mbs) <- lift $ src $$++ CL.head
36+
37+ -- If no chunk available, then there aren't enough bytes in the
38+ -- stream. Throw a ConnectionClosedByPeer
39+ bs <- maybe (liftIO $ throwIO ConnectionClosedByPeer) return mbs
40+
41+ let -- How many of the bytes in this chunk to send downstream
42+ toSend = min count (S.length bs)
43+ -- How many bytes will still remain to be sent downstream
44+ count' = count - toSend
45+ case () of
46+ ()
47+ -- The expected count is greater than the size of the
48+ -- chunk we just read. Send the entire chunk
49+ -- downstream, and then loop on this function for the
50+ -- next chunk.
51+ | count' > 0 -> do
52+ liftIO $ I.writeIORef ref (count', src')
53+ yield bs
54+ ibsIsolate ibs
55+
56+ -- The expected count is the total size of the chunk we
57+ -- just read. Send this chunk downstream, and then
58+ -- terminate the stream.
59+ | count == S.length bs -> do
60+ liftIO $ I.writeIORef ref (count', src')
61+ yield bs
62+
63+ -- Some of the bytes in this chunk should not be sent
64+ -- downstream. Split up the chunk into the sent and
65+ -- not-sent parts, add the not-sent parts onto the new
66+ -- source, and send the rest of the chunk downstream.
67+ | otherwise -> do
68+ let (x, y) = S.splitAt toSend bs
69+ liftIO $ I.writeIORef ref (count', fmapResume (yield y >>) src')
70+ yield x
71+
72+-- | Extract the underlying @Source@ from an @IsolatedBSSource@, which will not
73+-- perform any more isolation.
74+ibsDone :: IsolatedBSSource -> IO (ResumableSource (ResourceT IO) ByteString)
75+ibsDone (IsolatedBSSource ref) = fmap snd $ I.readIORef ref
76+
77+----------------------------------------------------------------
78+
79+data ChunkState = NeedLen
80+ | NeedLenNewline
81+ | HaveLen Word
82+
83+chunkedSource :: MonadIO m
84+ => I.IORef (ResumableSource m ByteString, ChunkState)
85+ -> Source m ByteString
86+chunkedSource ipair = do
87+ (src, mlen) <- liftIO $ I.readIORef ipair
88+ go src mlen
89+ where
90+ go' src front = do
91+ (src', (len, bs)) <- lift $ src $$++ front getLen
92+ let src''
93+ | S.null bs = src'
94+ | otherwise = fmapResume (yield bs >>) src'
95+ go src'' $ HaveLen len
96+
97+ go src NeedLen = go' src id
98+ go src NeedLenNewline = go' src (CB.take 2 >>)
99+ go src (HaveLen 0) = liftIO $ I.writeIORef ipair (src, HaveLen 0)
100+ go src (HaveLen len) = do
101+ (src', mbs) <- lift $ src $$++ CL.head
102+ case mbs of
103+ Nothing -> liftIO $ I.writeIORef ipair (src', HaveLen 0)
104+ Just bs ->
105+ case S.length bs `compare` fromIntegral len of
106+ EQ -> yield' src' NeedLenNewline bs
107+ LT -> do
108+ let mlen = HaveLen $ len - fromIntegral (S.length bs)
109+ yield' src' mlen bs
110+ GT -> do
111+ let (x, y) = S.splitAt (fromIntegral len) bs
112+ let src'' = fmapResume (yield y >>) src'
113+ yield' src'' NeedLenNewline x
114+
115+ yield' src mlen bs = do
116+ liftIO $ I.writeIORef ipair (src, mlen)
117+ yield bs
118+ go src mlen
119+
120+ getLen :: Monad m => Sink ByteString m (Word, ByteString)
121+ getLen = do
122+ mbs <- CL.head
123+ case mbs of
124+ Nothing -> return (0, S.empty)
125+ Just bs -> do
126+ (x, y) <-
127+ case S.breakByte 10 bs of
128+ (x, y)
129+ | S.null y -> do
130+ mbs2 <- CL.head
131+ case mbs2 of
132+ Nothing -> return (x, y)
133+ Just bs2 -> return $ S.breakByte 10 $ bs `S.append` bs2
134+ | otherwise -> return (x, y)
135+ let w =
136+ S.foldl' (\i c -> i * 16 + fromIntegral (hexToWord c)) 0
137+ $ B.takeWhile isHexDigit x
138+ return (w, S.drop 1 y)
139+
140+ hexToWord w
141+ | w < 58 = w - 48
142+ | w < 71 = w - 55
143+ | otherwise = w - 87
144+
145+----------------------------------------------------------------
146+
147+fmapResume :: (Source m o1 -> Source m o2) -> ResumableSource m o1 -> ResumableSource m o2
148+fmapResume f (ResumableSource src m) = ResumableSource (f src) m
warp/ReadInt.hs → warp/Network/Wai/Handler/Warp/ReadInt.hsrenamed+6−3
@@ -3,18 +3,21 @@
33-- Copyright : Erik de Castro Lopo <erikd@mega-nerd.com>
44-- License : BSD3
55
6−module ReadInt (readInt64) where
6+module Network.Wai.Handler.Warp.ReadInt (readInt) where
77
88-- This function lives in its own file because the MagicHash pragma interacts
99-- poorly with the CPP pragma.
1010
1111import Data.ByteString (ByteString)
12+import qualified Data.ByteString.Char8 as B
13+import qualified Data.Char as C
1214import Data.Int (Int64)
1315import GHC.Prim
1416import GHC.Types
1517
16−import qualified Data.ByteString.Char8 as B
17−import qualified Data.Char as C
18+{-# INLINE readInt #-}
19+readInt :: Integral a => ByteString -> a
20+readInt bs = fromIntegral $ readInt64 bs
1821
1922-- This function is used to parse the Content-Length field of HTTP headers and
2023-- is a performance hot spot. It should only be replaced with something
warp/Network/Wai/Handler/Warp/Request.hsnew file+211
@@ -0,0 +1,211 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Network.Wai.Handler.Warp.Request where
5+
6+import Control.Exception.Lifted (throwIO)
7+import Control.Monad.IO.Class (MonadIO, liftIO)
8+import Data.ByteString (ByteString)
9+import qualified Data.ByteString as S
10+import qualified Data.ByteString.Char8 as B
11+import qualified Data.ByteString.Unsafe as SU
12+import qualified Data.CaseInsensitive as CI
13+import Data.Char (toLower)
14+import Data.Conduit
15+import qualified Data.IORef as I
16+import Data.Maybe (fromMaybe)
17+import Data.Monoid (mempty)
18+import Data.Word (Word8)
19+import qualified Network.HTTP.Types as H
20+import Network.Socket (SockAddr)
21+import Network.Wai
22+import Network.Wai.Handler.Warp.Conduit
23+import Network.Wai.Handler.Warp.ReadInt
24+import Network.Wai.Handler.Warp.Types
25+import Prelude hiding (lines)
26+
27+parseRequest :: Connection -> Port -> SockAddr
28+ -> Source (ResourceT IO) S.ByteString
29+ -> ResourceT IO (Request, IO (ResumableSource (ResourceT IO) ByteString))
30+parseRequest conn port remoteHost' src1 = do
31+ (src2, headers') <- src1 $$+ takeHeaders
32+ parseRequest' conn port headers' remoteHost' src2
33+
34+handleExpect :: Connection
35+ -> H.HttpVersion
36+ -> ([H.Header] -> [H.Header])
37+ -> [H.Header]
38+ -> IO [H.Header]
39+handleExpect _ _ front [] = return $ front []
40+handleExpect conn hv front (("expect", "100-continue"):rest) = do
41+ connSendAll conn $
42+ if hv == H.http11
43+ then "HTTP/1.1 100 Continue\r\n\r\n"
44+ else "HTTP/1.0 100 Continue\r\n\r\n"
45+ return $ front rest
46+handleExpect conn hv front (x:xs) = handleExpect conn hv (front . (x:)) xs
47+
48+-- | Parse a set of header lines and body into a 'Request'.
49+parseRequest' :: Connection
50+ -> Port
51+ -> [ByteString]
52+ -> SockAddr
53+ -> ResumableSource (ResourceT IO) S.ByteString -- FIXME was buffered
54+ -> ResourceT IO (Request, IO (ResumableSource (ResourceT IO) ByteString))
55+parseRequest' _ _ [] _ _ = throwIO $ NotEnoughLines []
56+parseRequest' conn port (firstLine:otherLines) remoteHost' src = do
57+ (method, rpath', gets, httpversion) <- parseFirst firstLine
58+ let (host',rpath)
59+ | S.null rpath' = ("", "/")
60+ | "http://" `S.isPrefixOf` rpath' = S.breakByte 47 $ S.drop 7 rpath'
61+ | otherwise = ("", rpath')
62+ heads <- liftIO
63+ $ handleExpect conn httpversion id
64+ (map parseHeaderNoAttr otherLines)
65+ let host = fromMaybe host' $ lookup "host" heads
66+ let len0 =
67+ case lookup "content-length" heads of
68+ Nothing -> 0
69+ Just bs -> readInt bs
70+ let serverName' = takeUntil 58 host -- ':'
71+ let chunked = maybe False ((== "chunked") . B.map toLower)
72+ $ lookup "transfer-encoding" heads
73+ (rbody, getSource) <- liftIO $
74+ if chunked
75+ then do
76+ ref <- I.newIORef (src, NeedLen)
77+ return (chunkedSource ref, fmap fst $ I.readIORef ref)
78+ else do
79+ ibs <- fmap IsolatedBSSource $ I.newIORef (len0, src)
80+ return (ibsIsolate ibs, ibsDone ibs)
81+
82+ return (Request
83+ { requestMethod = method
84+ , httpVersion = httpversion
85+ , pathInfo = H.decodePathSegments rpath
86+ , rawPathInfo = rpath
87+ , rawQueryString = gets
88+ , queryString = H.parseQuery gets
89+ , serverName = serverName'
90+ , serverPort = port
91+ , requestHeaders = heads
92+ , isSecure = False
93+ , remoteHost = remoteHost'
94+ , requestBody = rbody
95+ , vault = mempty
96+ }, getSource)
97+
98+
99+takeUntil :: Word8 -> ByteString -> ByteString
100+takeUntil c bs =
101+ case S.elemIndex c bs of
102+ Just !idx -> SU.unsafeTake idx bs
103+ Nothing -> bs
104+{-# INLINE takeUntil #-}
105+
106+parseFirst :: ByteString
107+ -> ResourceT IO (ByteString, ByteString, ByteString, H.HttpVersion)
108+parseFirst s =
109+ case filter (not . S.null) $ S.splitWith (\c -> c == 32 || c == 9) s of -- ' '
110+ (method:query:http'') -> do
111+ let http' = S.concat http''
112+ (hfirst, hsecond) = B.splitAt 5 http'
113+ if hfirst == "HTTP/"
114+ then let (rpath, qstring) = S.breakByte 63 query -- '?'
115+ hv =
116+ case hsecond of
117+ "1.1" -> H.http11
118+ _ -> H.http10
119+ in return (method, rpath, qstring, hv)
120+ else throwIO NonHttp
121+ _ -> throwIO $ BadFirstLine $ B.unpack s
122+{-# INLINE parseFirst #-} -- FIXME is this inline necessary? the function is only called from one place and not exported
123+
124+parseHeaderNoAttr :: ByteString -> H.Header
125+parseHeaderNoAttr s =
126+ let (k, rest) = S.breakByte 58 s -- ':'
127+ rest' = S.dropWhile (\c -> c == 32 || c == 9) $ S.drop 1 rest
128+ in (CI.mk k, rest')
129+
130+type BSEndo = ByteString -> ByteString
131+type BSEndoList = [ByteString] -> [ByteString]
132+
133+data THStatus = THStatus
134+ {-# UNPACK #-} !Int -- running total byte count
135+ BSEndoList -- previously parsed lines
136+ BSEndo -- bytestrings to be prepended
137+
138+takeHeaders :: Sink ByteString (ResourceT IO) [ByteString]
139+takeHeaders =
140+ await >>= maybe (throwIO ConnectionClosedByPeer) (push (THStatus 0 id id))
141+ where
142+ close :: Sink ByteString (ResourceT IO) a
143+ close = throwIO IncompleteHeaders
144+
145+ push (THStatus len lines prepend) bs
146+ -- Too many bytes
147+ | len > maxTotalHeaderLength = throwIO OverLargeHeader
148+ | otherwise =
149+ case mnl of
150+ -- No newline find in this chunk. Add it to the prepend,
151+ -- update the length, and continue processing.
152+ Nothing ->
153+ let len' = len + bsLen
154+ prepend' = prepend . S.append bs
155+ status = THStatus len' lines prepend'
156+ in await >>= maybe close (push status)
157+ -- Found a newline, but next line continues as a multiline header
158+ Just (end, True) ->
159+ let rest = S.drop (end + 1) bs
160+ prepend' = prepend . S.append (SU.unsafeTake (checkCR bs end) bs)
161+ len' = len + end
162+ status = THStatus len' lines prepend'
163+ in push status rest
164+ -- Found a newline at position end.
165+ Just (end, False) ->
166+ let start = end + 1 -- start of next chunk
167+ line
168+ -- There were some bytes before the newline, get them
169+ | end > 0 = prepend $ SU.unsafeTake (checkCR bs end) bs
170+ -- No bytes before the newline
171+ | otherwise = prepend S.empty
172+ in if S.null line
173+ -- no more headers
174+ then
175+ let lines' = lines []
176+ -- leftover
177+ rest = if start < bsLen
178+ then Just (SU.unsafeDrop start bs)
179+ else Nothing
180+ in maybe (return ()) leftover rest >> return lines'
181+ -- more headers
182+ else
183+ let len' = len + start
184+ lines' = lines . (line:)
185+ status = THStatus len' lines' id
186+ in if start < bsLen
187+ -- more bytes in this chunk, push again
188+ then let bs' = SU.unsafeDrop start bs
189+ in push status bs'
190+ -- no more bytes in this chunk, ask for more
191+ else await >>= maybe close (push status)
192+ where
193+ bsLen = S.length bs
194+ mnl = do
195+ nl <- S.elemIndex 10 bs
196+ -- check if there are two more bytes in the bs
197+ -- if so, see if the second of those is a horizontal space
198+ if bsLen > nl + 1
199+ then
200+ let c = S.index bs (nl + 1)
201+ in Just (nl, c == 32 || c == 9)
202+ else Just (nl, False)
203+{-# INLINE takeHeaders #-}
204+
205+checkCR :: ByteString -> Int -> Int
206+checkCR bs pos =
207+ let !p = pos - 1
208+ in if '\r' == B.index bs p
209+ then p
210+ else pos
211+{-# INLINE checkCR #-}
warp/Network/Wai/Handler/Warp/Response.hsnew file+188
@@ -0,0 +1,188 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+{-# LANGUAGE BangPatterns #-}
4+
5+module Network.Wai.Handler.Warp.Response where
6+
7+import Data.List (foldl')
8+import qualified Data.Conduit.List as CL
9+import Blaze.ByteString.Builder (copyByteString, Builder, toLazyByteString, toByteStringIO, flush)
10+import Blaze.ByteString.Builder.Char8 (fromChar, fromShow)
11+import Blaze.ByteString.Builder.HTTP (chunkedTransferEncoding, chunkedTransferTerminator)
12+import Control.Exception
13+import Control.Monad.IO.Class (MonadIO, liftIO)
14+import qualified Data.ByteString.Char8 as B
15+import qualified Data.ByteString.Lazy as L
16+import qualified Data.CaseInsensitive as CI
17+import Data.Conduit
18+import Data.Conduit.Blaze (builderToByteString)
19+import Data.Maybe (isJust)
20+import Data.Monoid (mappend)
21+import qualified Network.HTTP.Types as H
22+import Network.Wai
23+import Network.Wai.Handler.Warp.ReadInt
24+import qualified Network.Wai.Handler.Warp.Timeout as T
25+import Network.Wai.Handler.Warp.Types
26+import qualified System.PosixCompat.Files as P
27+
28+sendResponse :: T.Handle
29+ -> Request -> Connection -> Response -> ResourceT IO Bool
30+sendResponse th req conn r = sendResponse' r
31+ where
32+ version = httpVersion req
33+ isPersist = checkPersist req
34+ isChunked' = isChunked version
35+ needsChunked hs = isChunked' && not (hasLength hs)
36+ isKeepAlive hs = isPersist && (isChunked' || hasLength hs)
37+ hasLength hs = isJust $ lookup "content-length" hs
38+ sendHeader = connSendMany conn . L.toChunks . toLazyByteString
39+
40+ sendResponse' :: Response -> ResourceT IO Bool
41+ sendResponse' (ResponseFile s hs fp mpart) = do
42+ eres <-
43+ case (readInt `fmap` lookup "content-length" hs, mpart) of
44+ (Just cl, _) -> return $ Right (hs, cl)
45+ (Nothing, Nothing) -> liftIO $ try $ do
46+ cl <- P.fileSize `fmap` P.getFileStatus fp
47+ return $ addClToHeaders cl
48+ (Nothing, Just part) -> do
49+ let cl = filePartByteCount part
50+ return $ Right $ addClToHeaders cl
51+ case eres of
52+ Left (_ :: SomeException) -> sendResponse' $ responseLBS
53+ H.status404
54+ [("Content-Type", "text/plain")]
55+ "File not found"
56+ Right (lengthyHeaders, cl) -> liftIO $ do
57+ let headers' = L.toChunks . toLazyByteString $ headers version s lengthyHeaders False
58+ T.tickle th
59+ if hasBody s req then do
60+ case mpart of
61+ Nothing -> connSendFile conn fp 0 cl (T.tickle th) headers'
62+ Just part -> connSendFile conn fp (filePartOffset part) (filePartByteCount part) (T.tickle th) headers'
63+ T.tickle th
64+ return isPersist
65+ else
66+ return isPersist
67+ where
68+ addClToHeaders cl = (("Content-Length", B.pack $ show cl):hs, fromIntegral cl)
69+
70+ sendResponse' (ResponseBuilder s hs b)
71+ | hasBody s req = liftIO $ do
72+ toByteStringIO (\bs -> do
73+ connSendAll conn bs
74+ T.tickle th) body
75+ return (isKeepAlive hs)
76+ | otherwise = liftIO $ do
77+ sendHeader $ headers' False
78+ T.tickle th
79+ return isPersist
80+ where
81+ headers' = headers version s hs
82+ needsChunked' = needsChunked hs
83+ body = if needsChunked'
84+ then headers' needsChunked'
85+ `mappend` chunkedTransferEncoding b
86+ `mappend` chunkedTransferTerminator
87+ else headers' False `mappend` b
88+
89+ sendResponse' (ResponseSource s hs bodyFlush)
90+ | hasBody s req = do
91+ let src = CL.sourceList [headers' needsChunked'] `mappend`
92+ (if needsChunked' then body $= chunk else body)
93+ src $$ builderToByteString =$ connSink conn th
94+ return $ isKeepAlive hs
95+ | otherwise = liftIO $ do
96+ sendHeader $ headers' False
97+ T.tickle th
98+ return isPersist
99+ where
100+ body = mapOutput (\x -> case x of
101+ Flush -> flush
102+ Chunk builder -> builder) bodyFlush
103+ headers' = headers version s hs
104+ -- FIXME perhaps alloca a buffer per thread and reuse that in all
105+ -- functions below. Should lessen greatly the GC burden (I hope)
106+ needsChunked' = needsChunked hs
107+ chunk :: Conduit Builder (ResourceT IO) Builder
108+ chunk = await >>= maybe (yield chunkedTransferTerminator) (\x -> yield (chunkedTransferEncoding x) >> chunk)
109+
110+checkPersist :: Request -> Bool
111+checkPersist req
112+ | ver == H.http11 = checkPersist11 conn
113+ | otherwise = checkPersist10 conn
114+ where
115+ ver = httpVersion req
116+ conn = lookup "connection" $ requestHeaders req
117+ checkPersist11 (Just x)
118+ | CI.foldCase x == "close" = False
119+ checkPersist11 _ = True
120+ checkPersist10 (Just x)
121+ | CI.foldCase x == "keep-alive" = True
122+ checkPersist10 _ = False
123+
124+isChunked :: H.HttpVersion -> Bool
125+isChunked = (==) H.http11
126+
127+hasBody :: H.Status -> Request -> Bool
128+hasBody s req = s /= H.Status 204 "" && s /= H.status304 &&
129+ H.statusCode s >= 200 && requestMethod req /= "HEAD"
130+
131+httpBuilder, spaceBuilder, newlineBuilder, transferEncodingBuilder
132+ , colonSpaceBuilder :: Builder
133+httpBuilder = copyByteString "HTTP/"
134+spaceBuilder = fromChar ' '
135+newlineBuilder = copyByteString "\r\n"
136+transferEncodingBuilder = copyByteString "Transfer-Encoding: chunked\r\n\r\n"
137+colonSpaceBuilder = copyByteString ": "
138+
139+headers :: H.HttpVersion -> H.Status -> H.ResponseHeaders -> Bool -> Builder
140+headers !httpversion !status !responseHeaders !isChunked' = {-# SCC "headers" #-}
141+ let !start = httpBuilder
142+ `mappend` copyByteString
143+ (case httpversion of
144+ H.HttpVersion 1 1 -> "1.1"
145+ _ -> "1.0")
146+ `mappend` spaceBuilder
147+ `mappend` fromShow (H.statusCode status)
148+ `mappend` spaceBuilder
149+ `mappend` copyByteString (H.statusMessage status)
150+ `mappend` newlineBuilder
151+ !start' = foldl' responseHeaderToBuilder start (serverHeader responseHeaders)
152+ !end = if isChunked'
153+ then transferEncodingBuilder
154+ else newlineBuilder
155+ in start' `mappend` end
156+
157+responseHeaderToBuilder :: Builder -> H.Header -> Builder
158+responseHeaderToBuilder b (x, y) = b
159+ `mappend` copyByteString (CI.original x)
160+ `mappend` colonSpaceBuilder
161+ `mappend` copyByteString y
162+ `mappend` newlineBuilder
163+
164+-- | Use 'connSendAll' to send this data while respecting timeout rules.
165+connSink :: Connection -> T.Handle -> Sink B.ByteString (ResourceT IO) ()
166+connSink Connection { connSendAll = send } th =
167+ sink
168+ where
169+ sink = await >>= maybe close push
170+ close = liftIO (T.resume th)
171+ push x = do
172+ liftIO $ T.resume th
173+ liftIO $ send x
174+ liftIO $ T.pause th
175+ sink
176+ -- We pause timeouts before passing control back to user code. This ensures
177+ -- that a timeout will only ever be executed when Warp is in control. We
178+ -- also make sure to resume the timeout after the completion of user code
179+ -- so that we can kill idle connections.
180+
181+serverHeader :: H.RequestHeaders -> H.RequestHeaders
182+serverHeader hdrs = case lookup key hdrs of
183+ Nothing -> server : hdrs
184+ Just _ -> hdrs
185+ where
186+ key = "Server"
187+ ver = B.pack $ "Warp/" ++ warpVersion
188+ server = (key, ver)
warp/Network/Wai/Handler/Warp/Run.hsnew file+145
@@ -0,0 +1,145 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+module Network.Wai.Handler.Warp.Run where
5+
6+import Control.Concurrent (forkIO, threadDelay)
7+import Control.Exception
8+import Control.Monad (forever, when, void)
9+import Control.Monad.IO.Class (MonadIO, liftIO)
10+import Data.ByteString (ByteString)
11+import qualified Data.ByteString as S
12+import Data.Conduit
13+import Data.Conduit.Internal (ResumableSource (..))
14+import qualified Data.Conduit.List as CL
15+import Data.Conduit.Network (bindPort)
16+import Network (sClose, Socket)
17+import Network.Socket (accept, SockAddr)
18+import Network.Wai
19+import Network.Wai.Handler.Warp.Request
20+import Network.Wai.Handler.Warp.Response
21+import Network.Wai.Handler.Warp.Settings
22+import qualified Network.Wai.Handler.Warp.Timeout as T
23+import Network.Wai.Handler.Warp.Types
24+import Prelude hiding (catch)
25+
26+#if WINDOWS
27+import qualified Control.Concurrent.MVar as MV
28+import Network.Socket (withSocketsDo)
29+#endif
30+
31+#if __GLASGOW_HASKELL__ < 702
32+allowInterrupt :: IO ()
33+allowInterrupt = unblock $ return ()
34+#endif
35+
36+-- | Run an 'Application' on the given port. This calls 'runSettings' with
37+-- 'defaultSettings'.
38+run :: Port -> Application -> IO ()
39+run p = runSettings defaultSettings { settingsPort = p }
40+
41+-- | Run a Warp server with the given settings.
42+runSettings :: Settings -> Application -> IO ()
43+#if WINDOWS
44+runSettings set app = withSocketsDo $ do
45+ var <- MV.newMVar Nothing
46+ let clean = MV.modifyMVar_ var $ \s -> maybe (return ()) sClose s >> return Nothing
47+ _ <- forkIO $ bracket
48+ (bindPort (settingsPort set) (settingsHost set))
49+ (const clean)
50+ (\s -> do
51+ MV.modifyMVar_ var (\_ -> return $ Just s)
52+ runSettingsSocket set s app)
53+ forever (threadDelay maxBound) `finally` clean
54+#else
55+runSettings set =
56+ bracket
57+ (bindPort (settingsPort set) (settingsHost set))
58+ sClose .
59+ flip (runSettingsSocket set)
60+#endif
61+
62+-- | Same as 'runSettings', but uses a user-supplied socket instead of opening
63+-- one. This allows the user to provide, for example, Unix named socket, which
64+-- can be used when reverse HTTP proxying into your application.
65+--
66+-- Note that the 'settingsPort' will still be passed to 'Application's via the
67+-- 'serverPort' record.
68+runSettingsSocket :: Settings -> Socket -> Application -> IO ()
69+runSettingsSocket set socket app =
70+ runSettingsConnection set getter app
71+ where
72+ getter = do
73+ (conn, sa) <- accept socket
74+ return (socketConnection conn, sa)
75+
76+runSettingsConnection :: Settings -> IO (Connection, SockAddr) -> Application -> IO ()
77+runSettingsConnection set getConn app = do
78+ tm <- maybe (T.initialize $ settingsTimeout set * 1000000) return
79+ $ settingsManager set
80+ mask $ \restore -> forever $ do
81+ allowInterrupt
82+ (conn, addr) <- getConnLoop
83+ void $ forkIO $ do
84+ th <- T.registerKillThread tm
85+ handle onE $ (do onOpen
86+ restore $ serveConnection set th port app conn addr
87+ connClose conn >> T.cancel th >> onClose
88+ ) `onException` (T.cancel th >> connClose conn >> onClose)
89+ where
90+ -- FIXME: only IOEception is caught. What about other exceptions?
91+ getConnLoop = getConn `catch` \(e :: IOException) -> do
92+ onE (toException e)
93+ -- "resource exhausted (Too many open files)" may happen by accept().
94+ -- Wait a second hoping that resource will be available.
95+ threadDelay 1000000
96+ getConnLoop
97+ onE = settingsOnException set
98+ port = settingsPort set
99+ onOpen = settingsOnOpen set
100+ onClose = settingsOnClose set
101+
102+serveConnection :: Settings
103+ -> T.Handle
104+ -> Port -> Application -> Connection -> SockAddr-> IO ()
105+serveConnection settings th port app conn remoteHost' =
106+ runResourceT serveConnection'
107+ where
108+ serveConnection' :: ResourceT IO ()
109+ serveConnection' = do
110+ let fromClient = connSource conn th
111+ serveConnection'' fromClient
112+
113+ serveConnection'' fromClient = do
114+ (env, getSource) <- parseRequest conn port remoteHost' fromClient
115+ case settingsIntercept settings env of
116+ Nothing -> do
117+ -- Let the application run for as long as it wants
118+ liftIO $ T.pause th
119+ res <- app env
120+
121+ liftIO $ T.resume th
122+ keepAlive <- sendResponse th env conn res
123+
124+ -- flush the rest of the request body
125+ requestBody env $$ CL.sinkNull
126+ ResumableSource fromClient' _ <- liftIO getSource
127+
128+ when keepAlive $ serveConnection'' fromClient'
129+ Just intercept -> do
130+ liftIO $ T.pause th
131+ ResumableSource fromClient' _ <- liftIO getSource
132+ intercept fromClient' conn
133+
134+connSource :: Connection -> T.Handle -> Source (ResourceT IO) ByteString
135+connSource Connection { connRecv = recv } th =
136+ src
137+ where
138+ src = do
139+ bs <- liftIO recv
140+ if S.null bs
141+ then return ()
142+ else do
143+ when (S.length bs >= 2048) $ liftIO $ T.tickle th
144+ yield bs
145+ src
warp/Network/Wai/Handler/Warp/Settings.hsnew file+52
@@ -0,0 +1,52 @@
1+module Network.Wai.Handler.Warp.Settings where
2+
3+import Control.Exception
4+import Control.Monad
5+import qualified Data.ByteString as S
6+import Data.Conduit
7+import Data.Conduit.Network (HostPreference (HostIPv4))
8+import Network.Wai
9+import Network.Wai.Handler.Warp.Timeout
10+import Network.Wai.Handler.Warp.Types
11+import System.IO (hPrint, stderr)
12+
13+-- | Various Warp server settings. This is purposely kept as an abstract data
14+-- type so that new settings can be added without breaking backwards
15+-- compatibility. In order to create a 'Settings' value, use 'defaultSettings'
16+-- and record syntax to modify individual records. For example:
17+--
18+-- > defaultSettings { settingsTimeout = 20 }
19+data Settings = Settings
20+ { settingsPort :: Int -- ^ Port to listen on. Default value: 3000
21+ , settingsHost :: HostPreference -- ^ Default value: HostIPv4
22+ , settingsOnException :: SomeException -> IO () -- ^ What to do with exceptions thrown by either the application or server. Default: ignore server-generated exceptions (see 'InvalidRequest') and print application-generated applications to stderr.
23+ , settingsOnOpen :: IO () -- ^ What to do when a connection is open. Default: do nothing.
24+ , settingsOnClose :: IO () -- ^ What to do when a connection is close. Default: do nothing.
25+ , settingsTimeout :: Int -- ^ Timeout value in seconds. Default value: 30
26+ , settingsIntercept :: Request -> Maybe (Source (ResourceT IO) S.ByteString -> Connection -> ResourceT IO ())
27+ , settingsManager :: Maybe Manager -- ^ Use an existing timeout manager instead of spawning a new one. If used, 'settingsTimeout' is ignored. Default is 'Nothing'
28+ }
29+
30+-- | The default settings for the Warp server. See the individual settings for
31+-- the default value.
32+defaultSettings :: Settings
33+defaultSettings = Settings
34+ { settingsPort = 3000
35+ , settingsHost = HostIPv4
36+ , settingsOnException = \e ->
37+ case fromException e of
38+ Just x -> go x
39+ Nothing ->
40+ when (go' $ fromException e) $
41+ hPrint stderr e
42+ , settingsOnOpen = return ()
43+ , settingsOnClose = return ()
44+ , settingsTimeout = 30
45+ , settingsIntercept = const Nothing
46+ , settingsManager = Nothing
47+ }
48+ where
49+ go :: InvalidRequest -> IO ()
50+ go _ = return ()
51+ go' (Just ThreadKilled) = False
52+ go' _ = True
warp/Timeout.hs → warp/Network/Wai/Handler/Warp/Timeout.hsrenamed+23−13
@@ -1,19 +1,20 @@
1−module Timeout
2− ( Manager
3− , Handle
4− , initialize
5− , register
6− , registerKillThread
7− , tickle
8− , pause
9− , resume
10− , cancel
11− ) where
1+module Network.Wai.Handler.Warp.Timeout (
2+ Manager
3+ , Handle
4+ , initialize
5+ , register
6+ , registerKillThread
7+ , tickle
8+ , pause
9+ , resume
10+ , cancel
11+ , withManager
12+ ) where
1213
13−import qualified Data.IORef as I
1414import Control.Concurrent (forkIO, threadDelay, myThreadId, killThread)
15−import Control.Monad (forever)
1615import qualified Control.Exception as E
16+import Control.Monad (forever)
17+import qualified Data.IORef as I
1718
1819-- FIXME implement stopManager
1920
@@ -64,3 +65,12 @@ tickle (Handle _ iactive) = I.writeIORef iactive Active
6465pause (Handle _ iactive) = I.writeIORef iactive Paused
6566resume = tickle
6667cancel (Handle _ iactive) = I.writeIORef iactive Canceled
68+
69+-- | Call the inner function with a timeout manager.
70+withManager :: Int -- ^ timeout in microseconds
71+ -> (Manager -> IO a)
72+ -> IO a
73+withManager timeout f = do
74+ -- FIXME when stopManager is available, use it
75+ man <- initialize timeout
76+ f man
warp/Network/Wai/Handler/Warp/Types.hsnew file+69
@@ -0,0 +1,69 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
2+
3+module Network.Wai.Handler.Warp.Types where
4+
5+import Control.Exception
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Char8 as B
8+import Data.Typeable (Typeable)
9+import Data.Version (showVersion)
10+import Network (sClose, Socket)
11+import Network.Sendfile
12+import qualified Network.Socket.ByteString as Sock
13+import qualified Paths_warp
14+
15+warpVersion :: String
16+warpVersion = showVersion Paths_warp.version
17+
18+type Port = Int
19+
20+-- FIXME come up with good values here
21+bytesPerRead, maxTotalHeaderLength :: Int
22+bytesPerRead = 4096
23+maxTotalHeaderLength = 50 * 1024
24+
25+data InvalidRequest =
26+ NotEnoughLines [String]
27+ | BadFirstLine String
28+ | NonHttp
29+ | IncompleteHeaders
30+ | ConnectionClosedByPeer
31+ | OverLargeHeader
32+ deriving (Eq, Show, Typeable)
33+
34+instance Exception InvalidRequest
35+
36+-- |
37+--
38+-- In order to provide slowloris protection, Warp provides timeout handlers. We
39+-- follow these rules:
40+--
41+-- * A timeout is created when a connection is opened.
42+--
43+-- * When all request headers are read, the timeout is tickled.
44+--
45+-- * Every time at least 2048 bytes of the request body are read, the timeout
46+-- is tickled.
47+--
48+-- * The timeout is paused while executing user code. This will apply to both
49+-- the application itself, and a ResponseSource response. The timeout is
50+-- resumed as soon as we return from user code.
51+--
52+-- * Every time data is successfully sent to the client, the timeout is tickled.
53+data Connection = Connection
54+ { connSendMany :: [B.ByteString] -> IO ()
55+ , connSendAll :: B.ByteString -> IO ()
56+ , connSendFile :: FilePath -> Integer -> Integer -> IO () -> [ByteString] -> IO () -- ^ offset, length
57+ , connClose :: IO ()
58+ , connRecv :: IO B.ByteString
59+ }
60+
61+socketConnection :: Socket -> Connection
62+socketConnection s = Connection
63+ { connSendMany = Sock.sendMany s
64+ , connSendAll = Sock.sendAll s
65+ , connSendFile = \fp off len act hdr -> sendfileWithHeader s fp (PartOfFile off len) act hdr
66+ , connClose = sClose s
67+ , connRecv = Sock.recv s bytesPerRead
68+ }
69+
warp/warp.cabal+32−26
@@ -11,9 +11,9 @@ Build-Type: Simple
1111Cabal-Version: >=1.8
1212Stability: Stable
1313Description: The premier WAI handler. For more information, see <http://steve.vinoski.net/blog/2011/05/01/warp-a-haskell-web-server/>.
14−extra-source-files: test/main.hs
14+Extra-Source-Files: test/main.hs
1515
16−flag network-bytestring
16+Flag network-bytestring
1717 Default: False
1818
1919Library
@@ -32,35 +32,41 @@ Library
3232 , unix-compat >= 0.2
3333 , ghc-prim
3434 if flag(network-bytestring)
35− build-depends: network >= 2.2.1.5 && < 2.2.3
35+ Build-Depends: network >= 2.2.1.5 && < 2.2.3
3636 , network-bytestring >= 0.1.3 && < 0.1.4
3737 else
38− build-depends: network >= 2.3 && < 2.4
38+ Build-Depends: network >= 2.3 && < 2.4
3939 Exposed-modules: Network.Wai.Handler.Warp
40− Other-modules: Timeout
41− ReadInt
40+ Other-modules: Network.Wai.Handler.Warp.Conduit
41+ Network.Wai.Handler.Warp.ReadInt
42+ Network.Wai.Handler.Warp.Request
43+ Network.Wai.Handler.Warp.Response
44+ Network.Wai.Handler.Warp.Run
45+ Network.Wai.Handler.Warp.Settings
46+ Network.Wai.Handler.Warp.Timeout
47+ Network.Wai.Handler.Warp.Types
4248 Paths_warp
43− ghc-options: -Wall
49+ Ghc-Options: -Wall
4450 if os(windows)
45− Cpp-options: -DWINDOWS
51+ Cpp-options: -DWINDOWS
4652
47−test-suite test
48− main-is: main.hs
49− hs-source-dirs: test
50− type: exitcode-stdio-1.0
53+Test-Suite test
54+ Main-Is: main.hs
55+ Hs-Source-Dirs: test
56+ Type: exitcode-stdio-1.0
5157
52− ghc-options: -Wall
53− build-depends: base >= 4 && < 5
54− , HUnit
55− , hspec
56− , bytestring
57− , warp
58− , conduit
59− , network
60− , http-types
61− , transformers
62− , wai
58+ Ghc-Options: -Wall
59+ Build-Depends: base >= 4 && < 5
60+ , HUnit
61+ , hspec
62+ , bytestring
63+ , warp
64+ , conduit
65+ , network
66+ , http-types
67+ , transformers
68+ , wai
6369
64−source-repository head
65− type: git
66− location: git://github.com/yesodweb/wai.git
70+Source-Repository head
71+ Type: git
72+ Location: git://github.com/yesodweb/wai.git