module Network.Wai.Handler.Warp
(
run
, runSettings
, runSettingsSocket
, Settings
, defaultSettings
, settingsPort
, settingsHost
, settingsOnException
, settingsOnOpen
, settingsOnClose
, settingsTimeout
, settingsIntercept
, settingsManager
, HostPreference (..)
, Connection (..)
, runSettingsConnection
, Port
, InvalidRequest (..)
, Manager
, withManager
, parseRequest
, sendResponse
, registerKillThread
, pause
, resume
, T.cancel
, T.register
, T.initialize
#if TEST
, takeHeaders
, readInt
#endif
) where
import Prelude hiding (lines)
import Network.Wai
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Unsafe as SU
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as L
import Network (sClose, Socket)
import Network.Socket (accept, SockAddr)
import qualified Network.Socket.ByteString as Sock
import Control.Exception
( mask, handle, onException, bracket
, Exception, SomeException
, fromException, AsyncException (ThreadKilled)
, try
#if __GLASGOW_HASKELL__ >= 702
, allowInterrupt
#else
, unblock
#endif
#if WINDOWS
, finally
#endif
)
import Control.Concurrent (forkIO)
import Data.Maybe (fromMaybe, isJust)
import Data.Char (toLower, isHexDigit)
import Data.Word (Word)
import Data.Typeable (Typeable)
import Data.Conduit (ResourceT, runResourceT)
import qualified Data.Conduit as C
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Binary as CB
import Data.Conduit.Blaze (builderToByteString)
import Control.Exception.Lifted (throwIO)
import Blaze.ByteString.Builder.HTTP
(chunkedTransferEncoding, chunkedTransferTerminator)
import Blaze.ByteString.Builder
(copyByteString, Builder, toLazyByteString, toByteStringIO, flush)
import Blaze.ByteString.Builder.Char8 (fromChar, fromShow)
import Data.Monoid (mappend, mempty)
import Network.Sendfile
import qualified System.PosixCompat.Files as P
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (lift)
import qualified Timeout as T
import Timeout (Manager, registerKillThread, pause, resume)
import Data.Word (Word8)
import Data.List (foldl')
import Control.Monad (forever, when, void)
import qualified Network.HTTP.Types as H
import qualified Data.CaseInsensitive as CI
import System.IO (hPrint, stderr)
import ReadInt (readInt64)
import qualified Data.IORef as I
import Data.Conduit.Network (bindPort, HostPreference (HostIPv4))
#if WINDOWS
import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.MVar as MV
import Network.Socket (withSocketsDo)
#endif
import Data.Version (showVersion)
import qualified Paths_warp
warpVersion :: String
warpVersion = showVersion Paths_warp.version
#if __GLASGOW_HASKELL__ < 702
allowInterrupt :: IO ()
allowInterrupt = unblock $ return ()
#endif
data Connection = Connection
{ connSendMany :: [B.ByteString] -> IO ()
, connSendAll :: B.ByteString -> IO ()
, connSendFile :: FilePath -> Integer -> Integer -> IO () -> IO ()
, connClose :: IO ()
, connRecv :: IO B.ByteString
}
socketConnection :: Socket -> Connection
socketConnection s = Connection
{ connSendMany = Sock.sendMany s
, connSendAll = Sock.sendAll s
, connSendFile = \fp off len act -> sendfile s fp (PartOfFile off len) act
, connClose = sClose s
, connRecv = Sock.recv s bytesPerRead
}
run :: Port -> Application -> IO ()
run p = runSettings defaultSettings { settingsPort = p }
runSettings :: Settings -> Application -> IO ()
#if WINDOWS
runSettings set app = withSocketsDo $ do
var <- MV.newMVar Nothing
let clean = MV.modifyMVar_ var $ \s -> maybe (return ()) sClose s >> return Nothing
_ <- forkIO $ bracket
(bindPort (settingsPort set) (settingsHost set))
(const clean)
(\s -> do
MV.modifyMVar_ var (\_ -> return $ Just s)
runSettingsSocket set s app)
forever (threadDelay maxBound) `finally` clean
#else
runSettings set =
bracket
(bindPort (settingsPort set) (settingsHost set))
sClose .
flip (runSettingsSocket set)
#endif
type Port = Int
runSettingsSocket :: Settings -> Socket -> Application -> IO ()
runSettingsSocket set socket app =
runSettingsConnection set getter app
where
getter = do
(conn, sa) <- accept socket
return (socketConnection conn, sa)
runSettingsConnection :: Settings -> IO (Connection, SockAddr) -> Application -> IO ()
runSettingsConnection set getConn app = do
let onE = settingsOnException set
port = settingsPort set
onOpen = settingsOnOpen set
onClose = settingsOnClose set
tm <- maybe (T.initialize $ settingsTimeout set * 1000000) return
$ settingsManager set
mask $ \restore -> forever $ do
allowInterrupt
(conn, addr) <- getConn
void $ forkIO $ do
th <- T.registerKillThread tm
handle onE $ (do onOpen
restore $ serveConnection set th port app conn addr
connClose conn >> T.cancel th >> onClose
) `onException` (T.cancel th >> connClose conn >> onClose)
newtype IsolatedBSSource = IsolatedBSSource (I.IORef (Int, C.Source (ResourceT IO) ByteString))
ibsIsolate :: IsolatedBSSource -> C.Source (ResourceT IO) ByteString
ibsIsolate ibs@(IsolatedBSSource ref) =
C.PipeM pull (return ())
where
pull = do
(count, src) <- liftIO $ I.readIORef ref
if count == 0
then return $ C.Done Nothing ()
else do
(src', mbs) <- src C.$$+ CL.head
bs <- maybe (liftIO $ throwIO ConnectionClosedByPeer) return mbs
let
toSend = min count (S.length bs)
count' = count toSend
case () of
()
| count' > 0 -> do
liftIO $ I.writeIORef ref (count', src')
return $ C.HaveOutput (ibsIsolate ibs) (return ()) bs
| count == S.length bs -> do
liftIO $ I.writeIORef ref (count', src')
return $ C.HaveOutput (C.Done Nothing ()) (return ()) bs
| otherwise -> do
let (x, y) = S.splitAt toSend bs
liftIO $ I.writeIORef ref (count', C.HaveOutput src' (return ()) y)
return $ C.HaveOutput (C.Done Nothing ()) (return ()) x
ibsDone :: IsolatedBSSource -> IO (C.Source (ResourceT IO) ByteString)
ibsDone (IsolatedBSSource ref) = fmap snd $ I.readIORef ref
serveConnection :: Settings
-> T.Handle
-> Port -> Application -> Connection -> SockAddr-> IO ()
serveConnection settings th port app conn remoteHost' =
runResourceT serveConnection'
where
serveConnection' :: ResourceT IO ()
serveConnection' = do
let fromClient = connSource conn th
serveConnection'' fromClient
serveConnection'' fromClient = do
(env, getSource) <- parseRequest conn port remoteHost' fromClient
case settingsIntercept settings env of
Nothing -> do
liftIO $ T.pause th
res <- app env
requestBody env C.$$ CL.sinkNull
fromClient' <- liftIO getSource
liftIO $ T.resume th
keepAlive <- sendResponse th env conn res
when keepAlive $ serveConnection'' fromClient'
Just intercept -> do
liftIO $ T.pause th
fromClient' <- liftIO getSource
intercept fromClient' conn
parseRequest :: Connection -> Port -> SockAddr
-> C.Source (ResourceT IO) S.ByteString
-> ResourceT IO (Request, IO (C.Source (ResourceT IO) ByteString))
parseRequest conn port remoteHost' src1 = do
(src2, headers') <- src1 C.$$+ takeHeaders
parseRequest' conn port headers' remoteHost' src2
bytesPerRead, maxTotalHeaderLength :: Int
bytesPerRead = 4096
maxTotalHeaderLength = 50 * 1024
data InvalidRequest =
NotEnoughLines [String]
| BadFirstLine String
| NonHttp
| IncompleteHeaders
| ConnectionClosedByPeer
| OverLargeHeader
deriving (Show, Typeable, Eq)
instance Exception InvalidRequest
handleExpect :: Connection
-> H.HttpVersion
-> ([H.Header] -> [H.Header])
-> [H.Header]
-> IO [H.Header]
handleExpect _ _ front [] = return $ front []
handleExpect conn hv front (("expect", "100-continue"):rest) = do
connSendAll conn $
if hv == H.http11
then "HTTP/1.1 100 Continue\r\n\r\n"
else "HTTP/1.0 100 Continue\r\n\r\n"
return $ front rest
handleExpect conn hv front (x:xs) = handleExpect conn hv (front . (x:)) xs
parseRequest' :: Connection
-> Port
-> [ByteString]
-> SockAddr
-> C.Source (ResourceT IO) S.ByteString
-> ResourceT IO (Request, IO (C.Source (ResourceT IO) ByteString))
parseRequest' _ _ [] _ _ = throwIO $ NotEnoughLines []
parseRequest' conn port (firstLine:otherLines) remoteHost' src = do
(method, rpath', gets, httpversion) <- parseFirst firstLine
let (host',rpath)
| S.null rpath' = ("", "/")
| "http://" `S.isPrefixOf` rpath' = S.breakByte 47 $ S.drop 7 rpath'
| otherwise = ("", rpath')
heads <- liftIO
$ handleExpect conn httpversion id
(map parseHeaderNoAttr otherLines)
let host = fromMaybe host' $ lookup "host" heads
let len0 =
case lookup "content-length" heads of
Nothing -> 0
Just bs -> readInt bs
let serverName' = takeUntil 58 host
let chunked = maybe False ((== "chunked") . B.map toLower)
$ lookup "transfer-encoding" heads
(rbody, getSource) <- liftIO $
if chunked
then do
ref <- I.newIORef (src, NeedLen)
return (chunkedSource ref, fmap fst $ I.readIORef ref)
else do
ibs <- fmap IsolatedBSSource $ I.newIORef (len0, src)
return (ibsIsolate ibs, ibsDone ibs)
return (Request
{ requestMethod = method
, httpVersion = httpversion
, pathInfo = H.decodePathSegments rpath
, rawPathInfo = rpath
, rawQueryString = gets
, queryString = H.parseQuery gets
, serverName = serverName'
, serverPort = port
, requestHeaders = heads
, isSecure = False
, remoteHost = remoteHost'
, requestBody = rbody
, vault = mempty
}, getSource)
data ChunkState = NeedLen
| NeedLenNewline
| HaveLen Word
chunkedSource :: MonadIO m
=> I.IORef (C.Source m ByteString, ChunkState)
-> C.Source m ByteString
chunkedSource ipair = do
(src, mlen) <- liftIO $ I.readIORef ipair
go src mlen
where
go' src front = do
(src', (len, bs)) <- lift $ src C.$$+ front getLen
let src''
| S.null bs = src'
| otherwise = C.yield bs >> src'
go src'' $ HaveLen len
go src NeedLen = go' src id
go src NeedLenNewline = go' src (CB.take 2 >>)
go src (HaveLen 0) = liftIO $ I.writeIORef ipair (src, HaveLen 0)
go src (HaveLen len) = do
(src', mbs) <- lift $ src C.$$+ CL.head
case mbs of
Nothing -> liftIO $ I.writeIORef ipair (src', HaveLen 0)
Just bs ->
case S.length bs `compare` fromIntegral len of
EQ -> yield' src' NeedLenNewline bs
LT -> do
let mlen = HaveLen $ len fromIntegral (S.length bs)
yield' src' mlen bs
GT -> do
let (x, y) = S.splitAt (fromIntegral len) bs
let src'' = C.yield y >> src'
yield' src'' NeedLenNewline x
yield' src mlen bs = do
liftIO $ I.writeIORef ipair (src, mlen)
C.yield bs
go src mlen
getLen :: Monad m => C.Sink ByteString m (Word, ByteString)
getLen = do
mbs <- CL.head
case mbs of
Nothing -> return (0, S.empty)
Just bs -> do
(x, y) <-
case S.breakByte 10 bs of
(x, y)
| S.null y -> do
mbs2 <- CL.head
case mbs2 of
Nothing -> return (x, y)
Just bs2 -> return $ S.breakByte 10 $ bs `S.append` bs2
| otherwise -> return (x, y)
let w =
S.foldl' (\i c -> i * 16 + fromIntegral (hexToWord c)) 0
$ B.takeWhile isHexDigit x
return (w, S.drop 1 y)
hexToWord w
| w < 58 = w 48
| w < 71 = w 55
| otherwise = w 87
takeUntil :: Word8 -> ByteString -> ByteString
takeUntil c bs =
case S.elemIndex c bs of
Just !idx -> SU.unsafeTake idx bs
Nothing -> bs
parseFirst :: ByteString
-> ResourceT IO (ByteString, ByteString, ByteString, H.HttpVersion)
parseFirst s =
case S.split 32 s of
[method, query, http'] -> do
let (hfirst, hsecond) = B.splitAt 5 http'
if hfirst == "HTTP/"
then let (rpath, qstring) = S.breakByte 63 query
hv =
case hsecond of
"1.1" -> H.http11
_ -> H.http10
in return (method, rpath, qstring, hv)
else throwIO NonHttp
_ -> throwIO $ BadFirstLine $ B.unpack s
httpBuilder, spaceBuilder, newlineBuilder, transferEncodingBuilder
, colonSpaceBuilder :: Builder
httpBuilder = copyByteString "HTTP/"
spaceBuilder = fromChar ' '
newlineBuilder = copyByteString "\r\n"
transferEncodingBuilder = copyByteString "Transfer-Encoding: chunked\r\n\r\n"
colonSpaceBuilder = copyByteString ": "
headers :: H.HttpVersion -> H.Status -> H.ResponseHeaders -> Bool -> Builder
headers !httpversion !status !responseHeaders !isChunked' =
let !start = httpBuilder
`mappend` copyByteString
(case httpversion of
H.HttpVersion 1 1 -> "1.1"
_ -> "1.0")
`mappend` spaceBuilder
`mappend` fromShow (H.statusCode status)
`mappend` spaceBuilder
`mappend` copyByteString (H.statusMessage status)
`mappend` newlineBuilder
!start' = foldl' responseHeaderToBuilder start (serverHeader responseHeaders)
!end = if isChunked'
then transferEncodingBuilder
else newlineBuilder
in start' `mappend` end
responseHeaderToBuilder :: Builder -> H.Header -> Builder
responseHeaderToBuilder b (x, y) = b
`mappend` copyByteString (CI.original x)
`mappend` colonSpaceBuilder
`mappend` copyByteString y
`mappend` newlineBuilder
checkPersist :: Request -> Bool
checkPersist req
| ver == H.http11 = checkPersist11 conn
| otherwise = checkPersist10 conn
where
ver = httpVersion req
conn = lookup "connection" $ requestHeaders req
checkPersist11 (Just x)
| CI.foldCase x == "close" = False
checkPersist11 _ = True
checkPersist10 (Just x)
| CI.foldCase x == "keep-alive" = True
checkPersist10 _ = False
isChunked :: H.HttpVersion -> Bool
isChunked = (==) H.http11
hasBody :: H.Status -> Request -> Bool
hasBody s req = s /= H.Status 204 "" && s /= H.status304 &&
H.statusCode s >= 200 && requestMethod req /= "HEAD"
sendResponse :: T.Handle
-> Request -> Connection -> Response -> ResourceT IO Bool
sendResponse th req conn r = sendResponse' r
where
version = httpVersion req
isPersist = checkPersist req
isChunked' = isChunked version
needsChunked hs = isChunked' && not (hasLength hs)
isKeepAlive hs = isPersist && (isChunked' || hasLength hs)
hasLength hs = isJust $ lookup "content-length" hs
sendHeader = connSendMany conn . L.toChunks . toLazyByteString
sendResponse' :: Response -> ResourceT IO Bool
sendResponse' (ResponseFile s hs fp mpart) = do
eres <-
case (readInt `fmap` lookup "content-length" hs, mpart) of
(Just cl, _) -> return $ Right (hs, cl)
(Nothing, Nothing) -> liftIO $ try $ do
cl <- P.fileSize `fmap` P.getFileStatus fp
return $ addClToHeaders cl
(Nothing, Just part) -> do
let cl = filePartByteCount part
return $ Right $ addClToHeaders cl
case eres of
Left (_ :: SomeException) -> sendResponse' $ responseLBS
H.status404
[("Content-Type", "text/plain")]
"File not found"
Right (lengthyHeaders, cl) -> liftIO $ do
let headers' = headers version s lengthyHeaders
sendHeader $ headers' False
T.tickle th
if hasBody s req then do
case mpart of
Nothing -> connSendFile conn fp 0 cl (T.tickle th)
Just part -> connSendFile conn fp (filePartOffset part) (filePartByteCount part) (T.tickle th)
T.tickle th
return isPersist
else
return isPersist
where
addClToHeaders cl = (("Content-Length", B.pack $ show cl):hs, fromIntegral cl)
sendResponse' (ResponseBuilder s hs b)
| hasBody s req = liftIO $ do
toByteStringIO (\bs -> do
connSendAll conn bs
T.tickle th) body
return (isKeepAlive hs)
| otherwise = liftIO $ do
sendHeader $ headers' False
T.tickle th
return isPersist
where
headers' = headers version s hs
needsChunked' = needsChunked hs
body = if needsChunked'
then headers' needsChunked'
`mappend` chunkedTransferEncoding b
`mappend` chunkedTransferTerminator
else headers' False `mappend` b
sendResponse' (ResponseSource s hs bodyFlush)
| hasBody s req = do
let src = CL.sourceList [headers' needsChunked'] `mappend`
(if needsChunked' then body C.$= chunk else body)
src C.$$ builderToByteString C.=$ connSink conn th
return $ isKeepAlive hs
| otherwise = liftIO $ do
sendHeader $ headers' False
T.tickle th
return isPersist
where
body = fmap2 (\x -> case x of
C.Flush -> flush
C.Chunk builder -> builder) bodyFlush
headers' = headers version s hs
needsChunked' = needsChunked hs
chunk :: C.Conduit Builder (ResourceT IO) Builder
chunk = C.NeedInput push close
push x = C.HaveOutput chunk (return ()) (chunkedTransferEncoding x)
close = C.HaveOutput (C.Done Nothing ()) (return ()) chunkedTransferTerminator
fmap2 :: Functor m => (o1 -> o2) -> C.Pipe i o1 m r -> C.Pipe i o2 m r
fmap2 f (C.HaveOutput p c o) = C.HaveOutput (fmap2 f p) c (f o)
fmap2 f (C.NeedInput p c) = C.NeedInput (fmap2 f . p) (fmap2 f c)
fmap2 f (C.PipeM mp c) = C.PipeM (fmap (fmap2 f) mp) c
fmap2 _ (C.Done i x) = C.Done i x
parseHeaderNoAttr :: ByteString -> H.Header
parseHeaderNoAttr s =
let (k, rest) = S.breakByte 58 s
restLen = S.length rest
rest' = if restLen > 1 && SU.unsafeTake 2 rest == ": "
then SU.unsafeDrop 2 rest
else rest
in (CI.mk k, rest')
connSource :: Connection -> T.Handle -> C.Source (ResourceT IO) ByteString
connSource Connection { connRecv = recv } th =
src
where
src = C.PipeM (do
bs <- liftIO recv
if S.null bs
then return $ C.Done Nothing ()
else do
when (S.length bs >= 2048) $ liftIO $ T.tickle th
return (C.HaveOutput src (return ()) bs))
(return ())
connSink :: Connection -> T.Handle -> C.Sink B.ByteString (ResourceT IO) ()
connSink Connection { connSendAll = send } th =
sink
where
sink = C.NeedInput push close
close = liftIO (T.resume th)
push x = C.PipeM (liftIO $ do
T.resume th
send x
T.pause th
return sink) (liftIO $ T.resume th)
data Settings = Settings
{ settingsPort :: Int
, settingsHost :: HostPreference
, settingsOnException :: SomeException -> IO ()
, settingsOnOpen :: IO ()
, settingsOnClose :: IO ()
, settingsTimeout :: Int
, settingsIntercept :: Request -> Maybe (C.Source (ResourceT IO) S.ByteString -> Connection -> ResourceT IO ())
, settingsManager :: Maybe Manager
}
defaultSettings :: Settings
defaultSettings = Settings
{ settingsPort = 3000
, settingsHost = HostIPv4
, settingsOnException = \e ->
case fromException e of
Just x -> go x
Nothing ->
when (go' $ fromException e) $
hPrint stderr e
, settingsOnOpen = return ()
, settingsOnClose = return ()
, settingsTimeout = 30
, settingsIntercept = const Nothing
, settingsManager = Nothing
}
where
go :: InvalidRequest -> IO ()
go _ = return ()
go' (Just ThreadKilled) = False
go' _ = True
type BSEndo = ByteString -> ByteString
type BSEndoList = [ByteString] -> [ByteString]
data THStatus = THStatus
!Int
BSEndoList
BSEndo
takeHeaders :: C.Sink ByteString (ResourceT IO) [ByteString]
takeHeaders =
C.NeedInput (push (THStatus 0 id id)) close
where
close = throwIO IncompleteHeaders
push (THStatus len lines prepend) bs
| len > maxTotalHeaderLength = throwIO OverLargeHeader
| otherwise =
case mnl of
Nothing ->
let len' = len + bsLen
prepend' = prepend . S.append bs
status = THStatus len' lines prepend'
in C.NeedInput (push status) close
Just end ->
let start = end + 1
line
| end > 0 = prepend $ SU.unsafeTake (checkCR bs end) bs
| otherwise = prepend S.empty
in if S.null line
then
let lines' = lines []
rest = if start < bsLen
then Just (SU.unsafeDrop start bs)
else Nothing
in C.Done rest lines'
else
let len' = len + start
lines' = lines . (line:)
status = THStatus len' lines' id
in if start < bsLen
then let bs' = SU.unsafeDrop start bs
in push status bs'
else C.NeedInput (push status) close
where
bsLen = S.length bs
mnl = S.elemIndex 10 bs
checkCR :: ByteString -> Int -> Int
checkCR bs pos =
let !p = pos 1
in if '\r' == B.index bs p
then p
else pos
readInt :: Integral a => ByteString -> a
readInt bs = fromIntegral $ readInt64 bs
withManager :: Int
-> (Manager -> IO a)
-> IO a
withManager timeout f = do
man <- T.initialize timeout
f man
serverHeader :: H.RequestHeaders -> H.RequestHeaders
serverHeader hdrs = case lookup key hdrs of
Nothing -> server : hdrs
Just _ -> hdrs
where
key = "Server"
ver = B.pack $ "Warp/" ++ warpVersion
server = (key, ver)