@@ -0,0 +1,233 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+---------------------------------------------------------
6+-- Module : Network.Wai.Handler.Warp
7+-- Copyright : Michael Snoyman
10+-- Maintainer : Michael Snoyman <michael@snoyman.com>
12+-- Portability : portable
14+-- A fast, light-weight HTTP server handler for WAI.
16+---------------------------------------------------------
17+module Network.Wai.Handler.Warp
24+import qualified System.IO
26+import Data.ByteString (ByteString)
27+import qualified Data.ByteString.Char8 as B
29+ ( listenOn, accept, sClose, PortID(PortNumber), Socket
31+import Control.Exception (bracket, finally, Exception, throwIO)
32+import System.IO (Handle, hClose, hFlush)
33+import System.IO.Error (isEOFError, ioeGetHandle)
34+import Control.Concurrent (forkIO)
35+import Control.Monad (unless)
36+import Data.Maybe (fromMaybe)
38+import Data.Typeable (Typeable)
40+import Control.Arrow (first)
42+import Data.Enumerator (($$), (>>==))
43+import qualified Data.Enumerator as E
44+import Data.Enumerator.IO (iterHandle)
45+import Blaze.ByteString.Builder.Enumerator (builderToByteString)
46+import Blaze.ByteString.Builder.HTTP
47+ (chunkedTransferEncoding, chunkedTransferTerminator)
48+import Blaze.ByteString.Builder (fromByteString, Builder)
49+import Blaze.ByteString.Builder.Char8 (fromChar, fromString)
50+import Data.Monoid (mconcat)
52+import Control.Monad.IO.Class (liftIO)
53+import Data.ByteString.Lazy.Internal (defaultChunkSize)
55+run :: Port -> Application -> IO ()
56+run port = withSocketsDo .
58+ (listenOn $ PortNumber $ fromIntegral port)
60+ serveConnections port
63+serveConnections :: Port -> Application -> Socket -> IO ()
64+serveConnections port app socket = do
65+ (conn, remoteHost', _) <- accept socket -- FIXME use sockets directly instead of Handlers?
66+ _ <- forkIO $ serveConnection port app conn remoteHost'
67+ serveConnections port app socket
69+serveConnection :: Port -> Application -> Handle -> String -> IO ()
70+serveConnection port app conn remoteHost' = do
76+ where serveConnection' = do
77+ env <- parseRequest port conn remoteHost'
79+ sendResponse (httpVersion env) conn res
80+ -- FIXME drain request body
84+ catchEOFError :: IOError -> IO ()
85+ catchEOFError e | isEOFError e = case ioeGetHandle e of
86+ Just h -> unless (h == conn) (ioError e)
88+ | otherwise = ioError e
90+parseRequest :: Port -> Handle -> String -> IO Request
91+parseRequest port conn remoteHost' = do
92+ headers' <- takeUntilBlank conn id
93+ parseRequest' port headers' conn remoteHost'
95+takeUntilBlank :: Handle
96+ -> ([ByteString] -> [ByteString])
98+takeUntilBlank h front = do
99+ l <- stripCR `fmap` B.hGetLine h
101+ then return $ front []
102+ else takeUntilBlank h $ front . (:) l
104+stripCR :: ByteString -> ByteString
107+ | B.last bs == '\r' = B.init bs
110+data InvalidRequest =
111+ NotEnoughLines [String]
112+ | BadFirstLine String
114+ deriving (Show, Typeable)
115+instance Exception InvalidRequest
117+-- | Parse a set of header lines and body into a 'Request'.
118+parseRequest' :: Port
123+parseRequest' port lines' handle remoteHost' = do
124+ (firstLine, otherLines) <-
126+ x:xs -> return (x, xs)
127+ [] -> throwIO $ NotEnoughLines $ map B.unpack lines'
128+ (method, rpath', gets, httpversion) <- parseFirst firstLine
129+ let rpath = '/' : case B.unpack rpath' of
131+ _ -> B.unpack rpath'
132+ let heads = map (first mkCIByteString . parseHeaderNoAttr) otherLines
133+ let host = fromMaybe "" $ lookup "host" heads
134+ let len = fromMaybe 0 $ do
135+ bs <- lookup "Content-Length" heads
136+ let str = B.unpack bs
140+ let (serverName', _) = B.break (== ':') host
142+ { requestMethod = method
143+ , httpVersion = httpversion
144+ , pathInfo = B.pack rpath
145+ , queryString = gets
146+ , serverName = serverName'
148+ , requestHeaders = heads
150+ , requestBody = requestBodyHandle handle len
151+ , errorHandler = System.IO.hPutStr System.IO.stderr
152+ , remoteHost = B.pack remoteHost'
155+parseFirst :: ByteString
156+ -> IO (ByteString, ByteString, ByteString, HttpVersion)
158+ let pieces = B.words s
159+ (method, query, http') <-
161+ [x, y, z] -> return (x, y, z)
162+ _ -> throwIO $ BadFirstLine $ B.unpack s
163+ let (hfirst, hsecond) = B.splitAt 5 http'
164+ unless (hfirst == B.pack "HTTP/") $ throwIO NonHttp
165+ let (rpath, qstring) = B.break (== '?') query
166+ return (method, rpath, qstring, hsecond)
168+headers :: HttpVersion -> Status -> ResponseHeaders -> Builder
169+headers httpversion status responseHeaders = mconcat
170+ [ fromByteString "HTTP/"
171+ , fromByteString httpversion
173+ , fromString $ show $ statusCode status
175+ , fromByteString $ statusMessage status
176+ , fromByteString "\r\n"
177+ , mconcat $ map go responseHeaders
178+ , fromByteString "Transfer-Encoding: chunked\r\n\r\n" -- FIXME perhaps put in non-chunked for HTTP 1.0 clients?
182+ [ fromByteString $ ciOriginal x
183+ , fromByteString ": "
185+ , fromByteString "\r\n"
188+sendResponse :: HttpVersion -> Handle -> Response -> IO ()
189+sendResponse hv handle res = do
190+ responseEnumerator res $ \s hs ->
192+ $$ E.enumList 1 [headers hv s hs]
193+ $$ E.joinI $ builderToByteString
194+ $$ iterHandle handle
196+ chunk :: E.Enumeratee Builder Builder IO ()
197+ chunk = E.checkDone $ E.continue . step
198+ step k E.EOF = k (E.Chunks [chunkedTransferTerminator]) >>== return
199+ step k (E.Chunks []) = E.continue $ step k
200+ step k (E.Chunks builders) =
201+ k (E.Chunks [chunked]) >>== chunk
203+ chunked = chunkedTransferEncoding $ mconcat builders
205+parseHeaderNoAttr :: ByteString -> (ByteString, ByteString)
206+parseHeaderNoAttr s =
207+ let (k, rest) = B.span (/= ':') s
208+ rest' = if not (B.null rest) &&
209+ B.head rest == ':' &&
210+ not (B.null $ B.tail rest) &&
211+ B.head (B.tail rest) == ' '
216+requestBodyHandle :: Handle -> Int -> E.Enumerator B.ByteString IO a
217+requestBodyHandle h =
220+ go i = Just `fmap` B.hGet h (min i defaultChunkSize)
222+requestBodyFunc :: (Int -> IO (Maybe B.ByteString))
224+ -> E.Enumerator B.ByteString IO a
225+requestBodyFunc _ 0 step = E.returnI step
226+requestBodyFunc h len (E.Continue k) = do
227+ mbs <- liftIO $ h len
229+ Nothing -> E.continue k
231+ let newLen = len - B.length bs
232+ k (E.Chunks [bs]) >>== requestBodyFunc h newLen
233+requestBodyFunc _ _ step = E.returnI step