packages feed

hans-2.1.0.0: example/WebServer.hs

module WebServer where

import Control.Concurrent (forkIO,threadDelay)
import Data.Time.Calendar (Day(..))
import Data.Time.Clock (UTCTime(..),addUTCTime)
import Data.Time.Clock.POSIX (POSIXTime,getPOSIXTime,posixSecondsToUTCTime)
import Data.Time.Format (formatTime)
import Hans.Layer.Tcp.Socket
    (Socket,readLine,sendSocket,acceptSocket,listenPort,closeSocket
    ,SocketError(..))
import Hans.Message.Tcp (TcpPort)
import Hans.Setup (NetworkStack(nsTcp))
import System.Exit (exitFailure)
import System.Locale (defaultTimeLocale)
import qualified Control.Exception as X
import qualified Data.ByteString as S

webserver :: NetworkStack -> TcpPort -> IO ()
webserver ns port = body `X.catch` \se -> print (se :: X.SomeException)
  where
  body = do
    start <- getPOSIXTime
    sock  <- initServer ns port
    serverLoop start sock

accept :: Socket -> IO Socket
accept sock = loop
  where
  loop = X.catch (acceptSocket sock) $ \se -> do
    case se of
      AcceptError err -> putStrLn ("Accept error: " ++ err)
      _               -> putStrLn ("Socket error: " ++ show se)
    loop

serverLoop :: POSIXTime -> Socket -> IO ()
serverLoop start sock = loop
  where
  loop = do
    client <- accept sock
    _      <- forkIO (handleClient start client)
    loop

initServer :: NetworkStack -> TcpPort -> IO Socket
initServer ns port = listenPort (nsTcp ns) port `X.catch` h
  where
  h ListenError{} = do
    putStrLn ("Unable to listen on port: " ++ show port)
    exitFailure
  h se = do
    print se
    exitFailure

handleClient :: POSIXTime -> Socket -> IO ()
handleClient start client = body `X.catch` \se -> print (se :: X.SomeException)
  where
  body = do
    mb <- processRequest client
    case mb of
      Nothing        -> closeSocket client
      Just (url,req) -> do
        sendSocket client =<< makeResponse start url req
        threadDelay 1000000
        closeSocket client

processRequest :: Socket -> IO (Maybe (String,[S.ByteString]))
processRequest sock = do
  ls <- readRequest sock
  case ls of
    []   -> return Nothing
    l:_  -> return (Just (parseUrl l, ls))

crlf :: S.ByteString
crlf  = S.pack [0x0d, 0x0a]

readRequest :: Socket -> IO [S.ByteString]
readRequest sock = loop
  where
  loop = do
    line <- readLine sock
    if line == crlf
       then return []
       else do
         rest <- loop
         return (line:rest)

parseUrl :: S.ByteString -> String
parseUrl  = head . drop 1 . words . toString

fromString :: String -> S.ByteString
fromString  = S.pack . map (toEnum . fromEnum)

toString :: S.ByteString -> String
toString  = map (toEnum . fromEnum) . S.unpack

status200 :: S.ByteString
status200  = fromString "HTTP/1.1 200 OK\r\n"

contentLength :: Int -> S.ByteString
contentLength len = fromString ("Content-Length: " ++ show len)

contentType :: String -> S.ByteString
contentType ty = fromString ("Content-Type: " ++ ty)

response404 :: S.ByteString
response404  = fromString $ concat
  [ "HTTP/1.1 404 Not Found\r\n"
  , "Content-Length: 0\r\n"
  , "\r\n"
  ]

connectionClose :: S.ByteString
connectionClose  = fromString "Connection: close"

makeResponse :: POSIXTime -> String -> [S.ByteString] -> IO S.ByteString
makeResponse start url req
  | url == "/favicon.ico" = return response404
  | otherwise             = do
    uptime <- timePassed start
    let date = posixSecondsToUTCTime start
        body = fromString (concat
               [ "<html><head><title>HaLVM</title></head><body>"
               , "<h1>Welcome to the HaLVM!</h1><br />\r\n\r\n"
               , "Started on: "
               , formatDate date
               , ", and up for "
               , uptime
               , "\r\n<h2>HTTP Request:</h2>\r\n<pre>"
               ]) `S.append` S.concat req
                  `S.append` fromString "</pre></body></html>"
    return $! S.concat
          [ status200
          , contentLength (S.length body), crlf
          , contentType "text/html", crlf
          , connectionClose, crlf
          , crlf
          , body
          ]

formatDate :: UTCTime -> String
formatDate  = formatTime defaultTimeLocale "%c"

zeroUTCTime :: UTCTime
zeroUTCTime  = UTCTime (ModifiedJulianDay 0) 0

timePassed :: POSIXTime -> IO String
timePassed start = do
  now <- getPOSIXTime
  let date@(UTCTime day _) = addUTCTime (now - start) zeroUTCTime
  return $ concat
    [ show (toModifiedJulianDay day)
    , " days, "
    , formatTime defaultTimeLocale "%k hours, %M minutes, %S seconds." date
    ]