/src/Main.hs

http://github.com/biilmann/eventsource-broker · Haskell · 232 lines · 177 code · 47 blank · 8 comment · 16 complexity · bf6d97d4c718e077df24c12ff244b4fe MD5 · raw file

  1. {-# LANGUAGE OverloadedStrings #-}
  2. module Main where
  3. import Control.Applicative ((<|>))
  4. import Control.Monad.Trans (liftIO)
  5. import Control.Concurrent (forkIO, threadDelay)
  6. import Control.Concurrent.Chan (Chan, readChan, dupChan)
  7. import Control.Exception (bracket)
  8. import Snap.Types
  9. import Snap.Util.FileServe (serveFile, serveDirectory)
  10. import Snap.Http.Server( quickHttpServe)
  11. import Data.ByteString(ByteString)
  12. import qualified Data.ByteString.Char8 as BS
  13. import Data.UString (UString, u)
  14. import qualified Data.UString as US
  15. import Data.Digest.Pure.SHA (sha1, bytestringDigest)
  16. import Data.Time.Clock.POSIX (POSIXTime)
  17. import Blaze.ByteString.Builder(fromByteString)
  18. import qualified System.UUID.V4 as UUID
  19. import AMQPEvents(AMQPEvent(..), Channel, openEventChannel, publishEvent)
  20. import EventStream(ServerEvent(..), eventSourceStream, eventSourceResponse)
  21. import DB
  22. import qualified Models.Connection as Conn
  23. import qualified Models.User as User
  24. import System.Posix.Env(getEnvDefault)
  25. import Data.Time.Clock.POSIX (getPOSIXTime)
  26. import Text.StringTemplate
  27. -- |Setup a channel listening to an AMQP exchange and start Snap
  28. main :: IO ()
  29. main = do
  30. uuid <- fmap (u . show) UUID.uuid
  31. origin <- getEnvDefault "ORIGIN" "http://127.0.0.1"
  32. templates <- directoryGroup "templates" :: IO (STGroup ByteString)
  33. let queue = US.append "eventsource." uuid
  34. let Just js = fmap (render . (setAttribute "origin" origin)) (getStringTemplate "eshq.js" templates)
  35. (publisher, listener) <- openEventChannel (show queue)
  36. bracket openDB (\db -> Conn.remove db uuid >> closeDB db) $ \db -> do
  37. forkIO $ connectionSweeper db uuid
  38. quickHttpServe $
  39. ifTop (serveFile "static/index.html") <|>
  40. path "iframe" (serveFile "static/iframe.html") <|>
  41. path "es.js" (serveJS js) <|>
  42. dir "static" (serveDirectory "static") <|>
  43. method POST (route [
  44. ("event", postEvent db publisher queue),
  45. ("socket", createSocket db uuid),
  46. ("socket/:socket", postEventFromSocket db publisher queue)
  47. ]) <|>
  48. method GET (route [
  49. ("broker", brokerInfo db uuid),
  50. ("eventsource", eventSource db uuid listener)
  51. ])
  52. -- |Clean up disconnected connections for this broker at regular intervals
  53. connectionSweeper :: DB -> UString -> IO ()
  54. connectionSweeper db uuid = do
  55. threadDelay 15000000
  56. Conn.sweep db uuid
  57. connectionSweeper db uuid
  58. brokerInfo :: DB -> UString -> Snap ()
  59. brokerInfo db uuid = do
  60. result <- liftIO $ Conn.count db uuid
  61. case result of
  62. Right count ->
  63. sendJSON $ BS.pack $ "{\"brokerId\": " ++ (show uuid) ++ ", \"connections\": " ++ (show count) ++ "}"
  64. Left e -> do
  65. modifyResponse $ setResponseCode 500
  66. writeBS $ BS.pack $ "Database Connection Problem: " ++ (show e)
  67. -- |Create a new socket and return the ID
  68. createSocket :: DB -> UString -> Snap ()
  69. createSocket db uuid = do
  70. withAuth db $ \user -> do
  71. withParam "channel" $ \channel -> do
  72. socketId <- liftIO $ fmap show UUID.uuid
  73. presenceId <- getParam "presence_id"
  74. result <- liftIO $ Conn.store db Conn.Connection {
  75. Conn.socketId = u socketId
  76. , Conn.brokerId = uuid
  77. , Conn.userId = User.apiKey user
  78. , Conn.channel = channel
  79. , Conn.presenceId = fmap ufrombs presenceId
  80. , Conn.disconnectAt = Just 10
  81. }
  82. case result of
  83. Left failure -> do
  84. logError (BS.pack $ show failure)
  85. showError 500 "Database Connection Error"
  86. Right _ ->
  87. sendJSON $ BS.pack ("{\"socket\": \"" ++ socketId ++ "\"}")
  88. postEvent :: DB -> Channel -> UString -> Snap ()
  89. postEvent db chan queue =
  90. withAuth db $ \user ->
  91. withParam "channel" $ \channel ->
  92. withParam "data" $ \dataParam -> do
  93. liftIO $ publishEvent chan (show queue) $
  94. AMQPEvent (utobs channel) (utobs $ User.apiKey user) (utobs dataParam) Nothing Nothing
  95. writeBS "Ok"
  96. -- |Post a new event from a socket.
  97. postEventFromSocket :: DB -> Channel -> UString -> Snap ()
  98. postEventFromSocket db chan queue =
  99. withConnection db $ \conn ->
  100. withParam "data" $ \dataParam -> do
  101. liftIO $ publishEvent chan (show queue) $
  102. AMQPEvent (utobs $ Conn.channel conn) (utobs $ Conn.userId conn) (utobs dataParam) Nothing Nothing
  103. writeBS "Ok"
  104. -- |Stream events from a channel of AMQPEvents to EventSource
  105. eventSource :: DB -> UString -> Chan AMQPEvent -> Snap ()
  106. eventSource db uuid chan = do
  107. chan' <- liftIO $ dupChan chan
  108. withConnection db $ \conn -> do
  109. liftIO $ before conn
  110. transport <- getTransport
  111. transport (filterEvents conn chan') (after conn)
  112. where
  113. before conn = Conn.store db conn { Conn.brokerId = uuid } >> return ()
  114. after conn = Conn.mark db (conn { Conn.disconnectAt = Just 10 } ) >> return ()
  115. serveJS :: ByteString -> Snap ()
  116. serveJS js = do
  117. modifyResponse $ setContentType "text/javascript; charset=UTF-8"
  118. writeBS js
  119. withParam :: UString -> (UString -> Snap ()) -> Snap ()
  120. withParam param fn = do
  121. param' <- getParam (utobs param)
  122. case param' of
  123. Just value -> fn (ufrombs value)
  124. Nothing -> showError 400 $ BS.concat ["Missing param: ", utobs param]
  125. withConnection :: DB -> (Conn.Connection -> Snap ()) -> Snap ()
  126. withConnection db fn = do
  127. withParam "socket" $ \sid -> do
  128. withDBResult (Conn.get db sid) (showError 404 "Socket Not Found") fn
  129. withAuth :: DB -> (User.User -> Snap ()) -> Snap ()
  130. withAuth db handler = do
  131. key <- getParam "key"
  132. token <- getParam "token"
  133. timestamp <- getParam "timestamp"
  134. case (key, token, timestamp) of
  135. (Just key', Just token', Just timestamp') -> do
  136. currentTime <- liftIO getPOSIXTime
  137. withDBResult (User.get db (ufrombs key')) (showError 404 "User not found") $ \user ->
  138. if validTime timestamp' currentTime && User.authenticate user token' timestamp'
  139. then handler user
  140. else showError 401 "Access Denied"
  141. withDBResult :: IO (Either Failure (Maybe a)) -> Snap () -> (a -> Snap ()) -> Snap ()
  142. withDBResult f notFound found= do
  143. result <- liftIO f
  144. case result of
  145. Right (Just model) -> found model
  146. Right Nothing -> notFound
  147. Left failure -> do
  148. logError (BS.pack $ show failure)
  149. showError 500 "Database Connection Error"
  150. validTime :: ByteString -> POSIXTime -> Bool
  151. validTime timestamp currentTime =
  152. let t1 = read $ BS.unpack timestamp
  153. t2 = floor currentTime in
  154. abs (t1 - t2) < 5 * 60
  155. showError :: Int -> ByteString -> Snap ()
  156. showError code msg = do
  157. modifyResponse $ setResponseCode code
  158. writeBS msg
  159. r <- getResponse
  160. finishWith r
  161. sendJSON :: ByteString -> Snap ()
  162. sendJSON json = do
  163. modifyResponse $ setContentType "application/json"
  164. writeBS json
  165. -- |Returns the transport method to use for this request
  166. getTransport :: Snap (IO ServerEvent -> IO () -> Snap ())
  167. getTransport = withRequest $ \request ->
  168. case getHeader "X-Requested-With" request of
  169. Just "XMLHttpRequest" -> return eventSourceResponse
  170. _ -> return eventSourceStream
  171. -- |Filter AMQPEvents by channelId
  172. filterEvents :: Conn.Connection -> Chan AMQPEvent -> IO ServerEvent
  173. filterEvents conn chan = do
  174. event <- readChan chan
  175. if amqpUser event == userId && amqpChannel event == channel
  176. then return $ ServerEvent (toBS $ amqpName event) (toBS $ amqpId event) [fromByteString $ amqpData event]
  177. else filterEvents conn chan
  178. where
  179. toBS = fmap fromByteString
  180. userId = utobs $ Conn.userId conn
  181. channel = utobs $ Conn.channel conn
  182. ufrombs :: ByteString -> UString
  183. ufrombs = US.fromByteString_
  184. utobs :: UString -> ByteString
  185. utobs = US.toByteString