{-# LANGUAGE LambdaCase, RecordWildCards #-}
module Network.N2O.Web.Http ( runServer ) where
import Control.Concurrent
import Control.Exception (catch, finally, SomeException(..), bracket, try)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
import Network.Socket hiding (recv, send)
import Network.Socket.ByteString
import Network.N2O.Types
import Network.N2O.Protocols.Types
import Network.N2O.Core
import Network.N2O.Web.WebSockets
import Prelude hiding (takeWhile)
import Data.Attoparsec.ByteString hiding (try)
import Data.CaseInsensitive
import qualified Network.WebSockets as WS
data HttpConf = HttpConf
data Resp = Resp
{ respCode :: Int
, respHead :: [Header]
, respBody :: BS.ByteString
} deriving (Show)
mkResp = Resp { respCode = 200, respHead = [], respBody = BS.empty }
runServer :: String -> Int -> Context N2OProto a -> IO ()
runServer host port cx =
withSocketsDo $ do
addr <- resolve host (show port)
bracket (open addr) close (acceptConnections HttpConf cx)
where
resolve host port = do
let hints = defaultHints {addrSocketType = Stream, addrFlags = [AI_PASSIVE]}
addr:_ <- getAddrInfo (Just hints) (Just host) (Just port)
return addr
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
setSocketOption sock ReuseAddr 1
bind sock (addrAddress addr)
listen sock 10
return sock
acceptConnections :: HttpConf -> Context N2OProto a -> Socket -> IO ()
acceptConnections conf cx sock = do
(handle, host_addr) <- accept sock
forkIO (catch
(talk conf cx handle host_addr `finally` close handle)
(\e@(SomeException _) -> print e))
acceptConnections conf cx sock
talk :: HttpConf -> Context N2OProto a -> Socket -> SockAddr -> IO ()
talk conf cx sock addr = do
bs <- recv sock 4096
let either = parseReq bs
case either of
Left resp -> sendResp conf sock resp
Right req ->
if needUpgrade req
then do
pending <- mkPending WS.defaultConnectionOptions sock req
wsApp cx {cxReq = req} pending
else fileResp (preparePath $ C.unpack $ reqPath req) (sendResp conf sock)
where
preparePath ('/':path) = path
preparePath path = path
status = \case
200 -> "OK"
400 -> "Bad Request"
404 -> "Not Found"
500 -> "Internal Server Eror"
_ -> ""
calcLen resp@Resp{..} = (C.pack "Content-Length", C.pack $ show $ BS.length respBody) : respHead
sendResp :: HttpConf -> Socket -> Resp -> IO ()
sendResp conf sock resp@Resp {..} = do
let headers = fmap (\(k, v) -> k <> C.pack ": " <> v <> C.pack "\r\n") (calcLen resp)
cmd = C.pack "HTTP/1.1 " <> C.pack (show respCode) <> C.pack " " <> C.pack (status respCode) <> C.pack "\r\n"
x = cmd : headers
y = x ++ [C.pack "\r\n", respBody]
send sock (mconcat y)
return ()
fileResp :: FilePath -> (Resp -> IO ()) -> IO ()
fileResp path respond = do
res <- try (BS.readFile path)
let (status, content) = case res of
Left e@(SomeException _) -> (404, C.pack "File Not Found")
Right content -> (200, content)
respond $ mkResp {respCode = status, respBody = content}
parseReq :: BS.ByteString -> Either Resp Req
parseReq bs = case parseOnly reqParser bs of
Left _ -> Left $ mkResp {respCode = 400}
Right req -> Right req
needUpgrade :: Req -> Bool
needUpgrade req =
case getHeader (C.pack "upgrade") (reqHead req) of
Nothing -> False
Just h -> mk (snd h) == mk (C.pack "websocket")
isKeepAlive :: Req -> Bool
isKeepAlive req =
case getHeader (C.pack "connection") (reqHead req) of
Nothing -> False
Just h -> mk (snd h) == mk (C.pack "keep-alive")
getHeader :: BS.ByteString -> [Header] -> Maybe Header
getHeader _ [] = Nothing
getHeader k (h:hs)
| mk k == mk (fst h) = Just h
| otherwise = getHeader k hs
crlf = (||) <$> (==10) <*> (==13)
isSpace = (== 32)
reqParser :: Parser Req
reqParser = do
cmd <- takeWhile1 $ not.isSpace
skipWhile isSpace
path <- takeWhile1 $ not.isSpace
skipWhile isSpace
ver <- takeWhile1 $ not.crlf
skipWhile crlf
headers <- many' headerParser
skipWhile crlf
takeByteString
endOfInput
return $ mkReq {reqMeth = cmd, reqPath = path, reqVers = ver, reqHead = headers }
headerParser :: Parser (BS.ByteString, BS.ByteString)
headerParser = do
name <- takeWhile1 (\b -> b /= 58 && b /= 10 && b /= 13)
skip (== 58)
skipWhile isSpace
val <- takeWhile1 $ not.crlf
skipWhile crlf
return (name, val)