{-# LANGUAGE ScopedTypeVariables #-} -- | Library for creating Websocket servers. Some parts cribbed from -- Jeff Foster's blog post at -- module Network.Websocket( Config(..), ConfigRestriction(..), WS(..), startServer, send ) where import Char (chr) import Control.Concurrent import Control.Exception hiding (catch) import Control.Monad import Data.Char (isSpace) import Data.Maybe import qualified Network as N import qualified Network.Socket as NS import Network.Web.HTTP import Network.URI import Network.Web.Server import System.IO instance Show Request where show req = let method = show $ reqMethod req uri = show $ reqURI req fields = show $ reqFields req in method ++ " " ++ uri ++ "\n" ++ fields data ConfigRestriction = Any | Only [String] restrictionValid x Any = True restrictionValid x (Only xs) = elem x xs instance Eq Status -- | Server configuration structure data Config = Config { -- | The port to bind to configPort :: Int, -- | The origin URL used in the handshake configOrigins :: ConfigRestriction, -- |The location URL used in the handshake. This must match -- the Websocket url that the browsers connect to. configDomains :: ConfigRestriction, -- | The onopen callback, called when a socket is opened configOnOpen :: WS -> IO (), -- | The onmessage callback, called when a message is received configOnMessage :: WS -> String -> IO (), -- | The onclose callback, called when the connection is closed. configOnClose :: WS -> IO () } -- | Connection state structure data WS = WS { -- | The server's configuration wsConfig :: Config, -- | The handle of the connected socket wsHandle :: Handle } readFrame :: Handle -> IO String readFrame h = readUntil h "" where readUntil h str = do new <- hGetChar h if new == chr 0 then readUntil h "" else if new == chr 255 then return str else readUntil h (str ++ [new]) sendFrame :: Handle -> String -> IO () sendFrame h s = do hPutChar h (chr 0) hPutStr h s hPutChar h (chr 255) hFlush h -- | Send a message to the connected browser. send ws = sendFrame (wsHandle ws) parseRequest req = do upgrade <- lookupField (FkOther "Upgrade") req origin <- lookupField (FkOther "Origin") req host <- lookupField FkHost req hostURI <- parseURI ("ws://" ++ host ++ "/") hostAuth <- uriAuthority hostURI let domain = uriRegName hostAuth return (upgrade, origin, domain) doWebSocket socket f = do (h :: Handle, _, _) <- N.accept socket forkIO $ bracket (do maybeReq <- receive h return (h, maybeReq)) (\(h,_) -> hClose h) (\(h, maybeReq) -> case maybeReq of Nothing -> putStrLn "Got bad request" Just req -> f h req) sendHandshake h origin location = hPutStr h handshake >> hFlush h where handshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n\ \Upgrade: WebSocket\r\n\ \Connection: Upgrade\r\n\ \WebSocket-Origin: " ++ origin ++ "\r\n\ \WebSocket-Location: "++ show location ++ "\r\n\ \WebSocket-Protocol: sample\r\n\r\n" accept config socket = forever $ doWebSocket socket $ \h req -> do let (upgrade, origin, hostDomain) = case parseRequest req of Nothing -> throw (userError "Invalid request") Just a -> a location = (reqURI req) { uriScheme = "ws:" } ws = WS { wsConfig = config, wsHandle = h } return $ assert (upgrade == "WebSocket") () return $ assert (restrictionValid origin (configOrigins config)) () return $ assert (restrictionValid hostDomain (configDomains config)) () sendHandshake h origin location onOpen ws (forever $ do msg <- readFrame h onMessage ws msg) `catch` (\e -> onClose ws) where onOpen = configOnOpen config onMessage = configOnMessage config onClose = configOnClose config -- | Start a websocket server startServer config = do let port = N.PortNumber $ fromIntegral (configPort config) bracket (N.listenOn port) NS.sClose (accept config) return ()