websockets 0.6.0.4 → 0.13.0.0
raw patch · 47 files changed
Files
- CHANGELOG +214/−0
- benchmarks/mask.hs +72/−0
- cbits/cbits.c +72/−0
- example/client.hs +40/−0
- example/server.lhs +151/−0
- src/Network/WebSockets.hs +80/−200
- src/Network/WebSockets/Client.hs +190/−0
- src/Network/WebSockets/Connection.hs +457/−0
- src/Network/WebSockets/Connection/Options.hs +132/−0
- src/Network/WebSockets/Connection/PingPong.hs +62/−0
- src/Network/WebSockets/Extensions.hs +24/−0
- src/Network/WebSockets/Extensions/Description.hs +65/−0
- src/Network/WebSockets/Extensions/PermessageDeflate.hs +287/−0
- src/Network/WebSockets/Extensions/StrictUnicode.hs +40/−0
- src/Network/WebSockets/Handshake.hs +0/−37
- src/Network/WebSockets/Handshake/Http.hs +0/−129
- src/Network/WebSockets/Http.hs +267/−0
- src/Network/WebSockets/Hybi13.hs +258/−0
- src/Network/WebSockets/Hybi13/Demultiplex.hs +157/−0
- src/Network/WebSockets/Hybi13/Mask.hs +81/−0
- src/Network/WebSockets/Internal.hs +0/−8
- src/Network/WebSockets/Monad.hs +0/−257
- src/Network/WebSockets/Protocol.hs +71/−61
- src/Network/WebSockets/Protocol/Hybi00.hs +0/−25
- src/Network/WebSockets/Protocol/Hybi00/Internal.hs +0/−102
- src/Network/WebSockets/Protocol/Hybi10.hs +0/−24
- src/Network/WebSockets/Protocol/Hybi10/Demultiplex.hs +0/−77
- src/Network/WebSockets/Protocol/Hybi10/Internal.hs +0/−156
- src/Network/WebSockets/Protocol/Hybi10/Mask.hs +0/−24
- src/Network/WebSockets/Protocol/Unsafe.hs +0/−35
- src/Network/WebSockets/Server.hs +174/−0
- src/Network/WebSockets/Socket.hs +0/−78
- src/Network/WebSockets/Stream.hs +218/−0
- src/Network/WebSockets/Types.hs +128/−31
- src/Network/WebSockets/Util/PubSub.hs +0/−79
- tests/autobahn/server.hs +91/−0
- tests/haskell/Network/WebSockets/Extensions/PermessageDeflate/Tests.hs +46/−0
- tests/haskell/Network/WebSockets/Extensions/Tests.hs +44/−0
- tests/haskell/Network/WebSockets/Handshake/Tests.hs +185/−0
- tests/haskell/Network/WebSockets/Http/Tests.hs +88/−0
- tests/haskell/Network/WebSockets/Hybi13/Demultiplex/Tests.hs +72/−0
- tests/haskell/Network/WebSockets/Mask/Tests.hs +57/−0
- tests/haskell/Network/WebSockets/Server/Tests.hs +185/−0
- tests/haskell/Network/WebSockets/Tests.hs +223/−0
- tests/haskell/Network/WebSockets/Tests/Util.hs +38/−0
- tests/haskell/TestSuite.hs +18/−6
- websockets.cabal +191/−68
+ CHANGELOG view
@@ -0,0 +1,214 @@+# CHANGELOG++- 0.13.0.0 (2023-12-30)+ * **BREAKING**: Remove `serverRequirePong` option in favor of the new+ implementation.+ * **BREAKING**: Client: Rejecting request raises+ `RequestRejected RequestHead ResponseHead`+ * Timeout initial socket connection after 30s.+ * If the socket is closed unexpectedly, raise `ConnectionClosed`.+ * Added a way to manually send a Pong message.+ * `runServer` now cleans up threads correctly.+ * Remove redundant bytestring-builder dependency.+ * Introduce `Network.WebSockets.Connection.PingPong` to+ handle ping pong for any Connection, be it Client or Server.+ * Bump `text `dependency upper bound to 2.2+ * Bump `random `dependency lower bound to 1.0.1++- 0.12.7.3 (2021-10-26)+ * Bump `attoparsec` dependency upper bound to 0.15++- 0.12.7.2 (2020-12-07)+ * Bump `QuickCheck` dependency upper bound to 2.15+ * Bump `base64-bytestring` dependency upper bound to 1.3+ * Bump `bytestring` dependency upper bound to 0.12+ * Bump `random` dependency upper bound to 1.3++- 0.12.7.1 (2020-05-03)+ * Bump `base64-bytestring` dependency upper bound to 1.2++- 0.12.7.0 (2019-12-31)+ * Bump `base` lower bound to 4.8, this drops support for GHC 7.6 and 7.8+ * Add a new `runServerWithOptions` that can be extended in a more+ future-compatible way+ * Add a connection killer setting in `runServerWithOptions`+ * Fix an unsafe read issue in `decodeResponseHead`++- 0.12.6.1 (2019-10-29)+ * Bump `network` dependency to 3.1++- 0.12.6.0 (2019-10-28)+ * Expose a lower-level API to construct client connections (by Philipp+ Balzarek)+ * Close underlying stream only on synchronous exceptions, not asynchronous+ exceptions (by kamoii)+ * Add a `withPingThread` and lower-level `pingThread` to replace+ `forkPingThread`+ * Bump `QuickCheck` dependency to 2.13++- 0.12.5.3 (2019-01-31)+ * Bump `network` dependency to 3.0++- 0.12.5.2 (2018-09-25)+ * Bump `containers` dependency to 0.6+ * Bump `network` dependency to 2.8+ * Bump `QuickCheck` dependency to 2.12+ * Bump `binary` dependency to 0.10++- 0.12.5.1 (2018-06-12)+ * Fix build with GHC 7.6 and 7.8++- 0.12.5.0 (2018-06-01)+ * Add `newClientConnection` (by Renzo Carbonara)++- 0.12.4.1 (2018-05-11)+ * Bump `network` dependency to 2.7++- 0.12.4.0 (2018-03-13)+ * Remove `blaze-builder` dependency+ * Bump `streaming-commons` dependency to 0.2+ * Bump `QuickCheck` dependency to 2.11+ * Fix compatibility with old GHC versions+ * Re-export more functions from `Network.WebSockets`+ - `sendDataMessages`+ - `sendBinaryDatas`+ - `sendCloseCode`+ * Don't crash when sending the empty list of messages+ * Add `SemiGroup` instance for `SizeLimit`++- 0.12.3.1 (2018-01-10)+ * Bump CHANGELOG with IPv6 warning+ * Run all autobahn tests during CI++- 0.12.3.0 (2018-01-02)+ * Fix error thrown from runClient functions+ * Bump `QuickCheck` dependency to 2.10+ * Bump `entropy` dependency to 0.4+ * Bump `binary` dependency to 0.10++- 0.12.2.0 (2017-07-28)+ * Don't use LambdaCase, we want to support older GHC versions++- 0.12.1.0 (2017-07-22)+ * Fix Monoid import on older base versions+ * Increase lower bound on `binary` to 0.8.1 (by Jonathan Daugherty)++- 0.12.0.0+ * Add limit options for frame and message size to prevent against (D)DoS+ attacks+ * Fix space leak in encodeMessages (by Roman Borschel)+ * Stricter frame/encoding decoding for ping/close frames (by Lars Petersen)++- 0.11.2.0+ * Fix 0-width reason phrase parsing+ * Change receive buffer from 1024 to 8192 bytes (by Ondrej Palkovsky)+ * Implement fast masking in C (by Ondrej Palkovsky and myself)+ * Some haddock improvements+ * Bump `HUnit` dependency to 1.6++- 0.11.1.0+ * Fix compilation issue with GHC-7.8++- 0.11.0.0+ * Support for IPv6 in the built-in server, client and tests (by agentm).+ This can cause issues on backends that do not enable IPv6. For more+ information and a workaround, see this issue:+ <https://github.com/jaspervdj/websockets/issues/140#issuecomment-296732964>.+ * Faster masking (by Dmitry Ivanov)+ * Support for `permessage-deflate` extension (by Marcin Tolysz)+ * Strict unicode checking and proper extension mechanism++- 0.10.0.0+ * Fix client specifying empty path+ * Allow sending collections of messages (by David Turner)+ * Allow sending extra headers when accepting request (by James Deery)++- 0.9.8.2+ * Bump `HUnit` dependency to 1.5++- 0.9.8.1+ * Restore state of the package to version `0.9.7.0`++- 0.9.8.0+ * This release contained a feature which broke backwards-compatibility.+ Hence, it was marked as broken a new release containing the changes will+ be uploaded as `0.10.0.0`.++- 0.9.7.0+ * Fix issue trying to kill builtin server+ * Bump `QuickCheck` dependency to 2.9++- 0.9.6.2+ * Bump `binary` dependency for GHC 8.0 compatibility++- 0.9.6.1+ * Fix issue with fragmentation test++- 0.9.6.0+ * Optionally include example server in the cabal file+ * Send correct port from client+ * Set `TCP_NO_DELAY` in builtin server+ * Bump `HUnit` dependency+ * Drop dependency on `mtl`+ * Fix `QuickCheck` dependency lower bound++- 0.9.5.0+ * Bugfixes wrt closing sockets and streams++- 0.9.4.0+ * Add `makePendingConnectionFromStream` function+ * Bump `attoparsec` dependency++- 0.9.3.1+ * Bump `QuickCheck` dependency++- 0.9.3.0+ * Use a shared closed state for connection input/output stream+ * Make sure `runServer` doesn't leak any sockets+ * Bump `blaze-builder` dependency++- 0.9.2.2+ * Bump `random` dependency++- 0.9.2.1+ * Fix exception handling issues++- 0.9.2.0+ * Make sending and receiving messages thread-safe by default+ * Export `forkPingThread`+ * Fix Windows `withSocketsDo` issue++- 0.9.1.0+ * Don't use Network.ByteString.Lazy.sendAll on Windows++- 0.9.0.1+ * Allow compilation with older bytestring versions+ * Bump text dependency++- 0.9.0.0+ * Bump various dependencies+ * Remove io-streams dependency+ * New close mechanism+ * More flexible API interface++- 0.8.2.6+ * Bump QuickCheck dependency++- 0.8.2.5+ * Bump attoparsec dependency++- 0.8.2.4+ * Bump entropy dependency++- 0.8.2.3+ * Bump mtl dependency++- 0.8.2.2+ * Bump network dependency++- 0.8.2.1+ * Add benchmark with many open connections+ * Update example to use gender-neutral language++- 0.8.2.0+ * Fix possible leaking of client sockets when connection times out
+ benchmarks/mask.hs view
@@ -0,0 +1,72 @@+{-# language BangPatterns #-}+{-# language OverloadedStrings #-}++import Criterion+import Criterion.Main+import qualified Data.Binary.Get as Get++import Network.WebSockets.Hybi13.Mask++import Data.Bits (shiftR, xor)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL++setupEnv = do+ let kilo = BL.replicate 1024 37+ mega = BL.replicate (1024 * 1024) 37+ megaU = BL.fromChunks [B.drop 1 (B.replicate (1024 * 1024) 37)]+ megaS = BL.fromChunks [B.replicate (1024 * 1024) 37]+ return (kilo, mega, megaU, megaS)++maskPayload' :: Maybe B.ByteString -> BL.ByteString -> BL.ByteString+maskPayload' Nothing = id+maskPayload' (Just mask) = snd . BL.mapAccumL f (cycle $ B.unpack mask)+ where+ f [] !c = ([], c)+ f (m:ms) !c = (ms, m `xor` c)++main = defaultMain [+ env setupEnv $ \ ~(kilo, mega, megaU, megaS) -> bgroup "main"+ [ bgroup "kilobyte payload"+ [ bgroup "zero_mask"+ [ bench "current" $ nf (maskPayload (mkMask $ "\x00\x00\x00\x00")) kilo+ , bench "old" $ nf (maskPayload' (Just "\x00\x00\x00\x00")) kilo+ ]+ , bgroup "full_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xFF\xFF\xFF\xFF")) kilo+ , bench "current-unaligned" $ nf (maskPayload (mkMask "\xFF\xFF\xFF\xFF")) (BL.drop 1 kilo)+ , bench "old" $ nf (maskPayload' (Just "\xFF\xFF\xFF\xFF")) kilo+ ]+ , bgroup "one_byte_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xCC\xCC\xCC\xCC")) kilo+ , bench "old" $ nf (maskPayload' (Just "\xCC\xCC\xCC\xCC")) kilo+ ]+ , bgroup "other_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xB0\xA2\xB0\xA2")) kilo+ , bench "old" $ nf (maskPayload' (Just "\xB0\xA2\xB0\xA2")) kilo+ ]+ ]+ , bgroup "megabyte payload"+ [ bgroup "zero_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\x00\x00\x00\x00")) mega+ , bench "old" $ nf (maskPayload' (Just "\x00\x00\x00\x00")) mega+ ]+ , bgroup "full_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xFF\xFF\xFF\xFF")) mega+ , bench "current-unaligned" $ nf (maskPayload (mkMask "\xFF\xFF\xFF\xFF")) megaU+ , bench "current-aligned" $ nf (maskPayload (mkMask "\xFF\xFF\xFF\xFF")) megaS+ , bench "old" $ nf (maskPayload' (Just "\xFF\xFF\xFF\xFF")) mega+ ]+ , bgroup "one_byte_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xCC\xCC\xCC\xCC")) mega+ , bench "old" $ nf (maskPayload' (Just "\xCC\xCC\xCC\xCC")) mega+ ]+ , bgroup "other_mask"+ [ bench "current" $ nf (maskPayload (mkMask "\xB0\xA2\xB0\xA2")) mega+ , bench "old" $ nf (maskPayload' (Just "\xB0\xA2\xB0\xA2")) mega+ ]+ ]+ ]+ ]+ where+ mkMask b = Just $ Get.runGet parseMask b
+ cbits/cbits.c view
@@ -0,0 +1,72 @@+#include <stdint.h>+#include <string.h>+#include <limits.h>+#include <assert.h>++/* Taken from:+ *+ * <http://stackoverflow.com/questions/776508/best-practices-for-circular-shift-rotate-operations-in-c>+ */+static inline uint32_t rotr32(uint32_t n, unsigned int c) {+ const unsigned int mask = (CHAR_BIT*sizeof(n)-1);+ c &= mask; /* avoid undef behaviour with NDEBUG. 0 overhead for most types / compilers */+ return (n>>c) | (n<<( (-c)&mask ));+}++/* - `mask` is the 4-byte mask to apply to the source. It is stored in the+ * hosts' native byte ordering.+ * - `mask_offset` is the initial offset in the mask. It is specified in bytes+ * and should be between 0 and 3 (inclusive). This is necessary for when we+ * are dealing with multiple chunks.+ * - `src` is the source pointer.+ * - `len` is the size of the source (and destination) in bytes.+ * - `dst` is the destination.+ */+void _hs_mask_chunk(+ uint32_t mask, int mask_offset,+ uint8_t *src, size_t len,+ uint8_t *dst) {+ const uint8_t *src_end = src + len;++ /* We have two fast paths: one for `x86_64` and one for `i386`+ * architectures. In these fast paths, we mask 8 (or 4) bytes at a time.+ *+ * Note that we use unaligned loads and stores (allowed on these+ * architectures). This makes the code much easier to write, since we don't+ * need to guarantee that `src` and `dst` have the same alignment.+ *+ * It only causes a minor slowdown, around 5% on my machine (TM).+ */+#if defined(__x86_64__)+ uint64_t mask64;+ /* Set up 64 byte mask. */+ mask64 = (uint64_t)(rotr32(mask, 8 * mask_offset));+ mask64 |= (mask64 << 32);+ /* Take the fast road. */+ while (src < src_end - 7) {+ *(uint64_t *)dst = *(uint64_t*)src ^ mask64;+ src += 8;+ dst += 8;+ }+#elif defined(__i386__)+ /* Set up 32 byte mask. */+ uint32_t mask32;+ mask32 = (uint32_t)(rotr32(mask, 8 * mask_offset));++ /* Take the fast road. */+ while (src < src_end - 3) {+ *(uint32_t *)dst = *(uint32_t*)src ^ mask32;+ src += 4;+ dst += 4;+ }+#endif++ /* This is the slow path which also handles the un-aligned suffix. */+ uint8_t *mask_ptr = (uint8_t *) &mask;+ while (src != src_end) {+ *dst = *src ^ *(mask_ptr + mask_offset);+ src++;+ dst++;+ mask_offset = (mask_offset + 1) & 0x3;+ }+}
+ example/client.hs view
@@ -0,0 +1,40 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Main+ ( main+ ) where+++--------------------------------------------------------------------------------+import Control.Concurrent (forkIO)+import Control.Monad (forever, unless)+import Control.Monad.Trans (liftIO)+import Network.Socket (withSocketsDo)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Network.WebSockets as WS+++--------------------------------------------------------------------------------+app :: WS.ClientApp ()+app conn = do+ putStrLn "Connected!"++ -- Fork a thread that writes WS data to stdout+ _ <- forkIO $ forever $ do+ msg <- WS.receiveData conn+ liftIO $ T.putStrLn msg++ -- Read from stdin and write to WS+ let loop = do+ line <- T.getLine+ unless (T.null line) $ WS.sendTextData conn line >> loop++ loop+ WS.sendClose conn ("Bye!" :: Text)+++--------------------------------------------------------------------------------+main :: IO ()+main = withSocketsDo $ WS.runClient "echo.websocket.org" 80 "/" app
+ example/server.lhs view
@@ -0,0 +1,151 @@+websockets example+==================++This is the Haskell implementation of the example for the WebSockets library. We+implement a simple multi-user chat program. A live demo of the example is+available [here](/example/client.html). In order to understand this example,+keep the [reference](/reference/) nearby to check out the functions we use.++> {-# LANGUAGE OverloadedStrings #-}+> module Main where+> import Data.Char (isPunctuation, isSpace)+> import Data.Monoid (mappend)+> import Data.Text (Text)+> import Control.Exception (finally)+> import Control.Monad (forM_, forever)+> import Control.Concurrent (MVar, newMVar, modifyMVar_, modifyMVar, readMVar)+> import qualified Data.Text as T+> import qualified Data.Text.IO as T++> import qualified Network.WebSockets as WS++We represent a client by their username and a `WS.Connection`. We will see how we+obtain this `WS.Connection` later on.++> type Client = (Text, WS.Connection)++The state kept on the server is simply a list of connected clients. We've added+an alias and some utility functions, so it will be easier to extend this state+later on.++> type ServerState = [Client]++Create a new, initial state:++> newServerState :: ServerState+> newServerState = []++Get the number of active clients:++> numClients :: ServerState -> Int+> numClients = length++Check if a user already exists (based on username):++> clientExists :: Client -> ServerState -> Bool+> clientExists client = any ((== fst client) . fst)++Add a client (this does not check if the client already exists, you should do+this yourself using `clientExists`):++> addClient :: Client -> ServerState -> ServerState+> addClient client clients = client : clients++Remove a client:++> removeClient :: Client -> ServerState -> ServerState+> removeClient client = filter ((/= fst client) . fst)++Send a message to all clients, and log it on stdout:++> broadcast :: Text -> ServerState -> IO ()+> broadcast message clients = do+> T.putStrLn message+> forM_ clients $ \(_, conn) -> WS.sendTextData conn message++The main function first creates a new state for the server, then spawns the+actual server. For this purpose, we use the simple server provided by+`WS.runServer`.++> main :: IO ()+> main = do+> state <- newMVar newServerState+> WS.runServer "127.0.0.1" 9160 $ application state++Our main application has the type:++> application :: MVar ServerState -> WS.ServerApp++Note that `WS.ServerApp` is nothing but a type synonym for+`WS.PendingConnection -> IO ()`.++Our application starts by accepting the connection. In a more realistic+application, you probably want to check the path and headers provided by the+pending request.++We also fork a pinging thread in the background. This will ensure the connection+stays alive on some browsers.++> application state pending = do+> conn <- WS.acceptRequest pending+> WS.withPingThread conn 30 (return ()) $ do++When a client is succesfully connected, we read the first message. This should+be in the format of "Hi! I am Jasper", where Jasper is the requested username.++> msg <- WS.receiveData conn+> clients <- readMVar state+> case msg of++Check that the first message has the right format:++> _ | not (prefix `T.isPrefixOf` msg) ->+> WS.sendTextData conn ("Wrong announcement" :: Text)++Check the validity of the username:++> | any ($ fst client)+> [T.null, T.any isPunctuation, T.any isSpace] ->+> WS.sendTextData conn ("Name cannot " <>+> "contain punctuation or whitespace, and " <>+> "cannot be empty" :: Text)++Check that the given username is not already taken:++> | clientExists client clients ->+> WS.sendTextData conn ("User already exists" :: Text)++All is right! We're going to allow the client, but for safety reasons we *first*+setup a `disconnect` function that will be run when the connection is closed.++> | otherwise -> flip finally disconnect $ do++We send a "Welcome!", according to our own little protocol. We add the client to+the list and broadcast the fact that he has joined. Then, we give control to the+'talk' function.++> modifyMVar_ state $ \s -> do+> let s' = addClient client s+> WS.sendTextData conn $+> "Welcome! Users: " <>+> T.intercalate ", " (map fst s)+> broadcast (fst client <> " joined") s'+> return s'+> talk client state+> where+> prefix = "Hi! I am "+> client = (T.drop (T.length prefix) msg, conn)+> disconnect = do+> -- Remove client and return new state+> s <- modifyMVar state $ \s ->+> let s' = removeClient client s in return (s', s')+> broadcast (fst client <> " disconnected") s++The talk function continues to read messages from a single client until he+disconnects. All messages are broadcasted to the other clients.++> talk :: Client -> MVar ServerState -> IO ()+> talk (user, conn) state = forever $ do+> msg <- WS.receiveData conn+> readMVar state >>= broadcast+> (user `mappend` ": " `mappend` msg)
src/Network/WebSockets.hs view
@@ -1,220 +1,100 @@--- | How do you use this library? Here's how:------ Get an enumerator/iteratee pair from your favorite web server (or use a--- library which provides integration). Alternatively, use 'I.runServer' to--- set up a simple standalone server.------ An application typically has the form of @I.Request -> I.WebSockets p ()@.--- The first thing to do is accept or reject the request, usually based upon--- the path in the 'I.Request'. An example:------ > {-# LANGUAGE OverloadedStrings #-}--- > import Network.WebSockets--- >--- > app :: Protocol p => Request -> WebSockets p ()--- > app rq = case requestPath rq of--- > "/forbidden" -> rejectRequest rq "Forbidden!"--- > _ -> do--- > acceptRequest rq--- > ... actual application ...------ You can now start using the socket for sending and receiving data. But what's--- with the @p@ in @WebSockets p ()@?------ Well, the answer is that this library aims to support many versions of the--- WebSockets protocol. Unfortunately, not all versions of the protocol have the--- same capabilities: for example, older versions are not able to send binary--- data.------ The library user (you!) choose which capabilities you need. Then, the browser--- and library will negotiate at runtime which version will be actually used.------ As an example, here are two applications which need different capabilities:------ > import Network.WebSockets--- > import qualified Data.ByteString as B--- > import qualified Data.Text as T--- > --- > app1 :: TextProtocol p => WebSockets p ()--- > app1 = sendTextData (T.pack "Hello world!")--- > --- > app2 :: BinaryProtocol p => WebSockets p ()--- > app2 = sendBinaryData (B.pack [0 .. 100])------ When you /tie the knot/, you will need to decide what protocol to use, to--- prevent ambiguousness. A good rule of thumb is to select the lowest protocol--- possible, since higher versions are generally backwards compatible in terms--- of features. . For example, the following application uses only--- /features from Hybi00/, and is therefore /compatible with Hybi10/ and later--- protocols.--- --- > app :: Request -> WebSockets Hybi00 ()--- > app _ = app1--- > --- > main :: IO ()--- > main = runServer "0.0.0.0" 8000 app--- --- In some cases, you want to escape from the 'I.WebSockets' monad and send data--- to the websocket from different threads. To this end, the 'I.getSink' method--- is provided. The next example spawns a thread which continuously spams the--- client in another thread:------ > import Control.Concurrent (forkIO)--- > import Control.Monad (forever)--- > import Control.Monad.Trans (liftIO)--- > import Network.WebSockets--- > import qualified Data.Text as T--- > --- > spam :: TextProtocol p => WebSockets p ()--- > spam = do--- > sink <- getSink--- > _ <- liftIO $ forkIO $ forever $--- > sendSink sink $ textData (T.pack "SPAM SPAM SPAM!")--- > sendTextData (T.pack "Hello world!")------ For safety reasons, you can only read from the socket in the 'I.WebSockets'--- monad.------ For a full example, see:------ <http://jaspervdj.be/websockets/example.html>+-------------------------------------------------------------------------------- {-# LANGUAGE ScopedTypeVariables #-} module Network.WebSockets- ( - -- * WebSocket type- I.WebSocketsOptions (..)- , I.defaultWebSocketsOptions- , I.WebSockets- , I.runWebSockets- , I.runWebSocketsWith- , I.runWebSocketsHandshake- , I.runWebSocketsWithHandshake-- -- * Protocol versions- , I.Protocol- , I.TextProtocol- , I.BinaryProtocol- , I.Hybi00- , I.Hybi10+ ( -- * Incoming connections and handshaking+ PendingConnection+ , pendingRequest+ , acceptRequest+ , AcceptRequest(..)+ , defaultAcceptRequest+ , acceptRequestWith+ , rejectRequest+ , RejectRequest(..)+ , defaultRejectRequest+ , rejectRequestWith - -- * A simple standalone server- , I.runServer- , I.runWithSocket+ -- * Main connection type+ , Connection - -- * Types- , I.Headers- , I.RequestHttpPart (..)- , I.Request (..)- , I.Response (..)- , I.Message (..)- , I.ControlMessage (..)- , I.DataMessage (..)- , I.WebSocketsData (..)+ -- * Options for connections+ , ConnectionOptions (..)+ , defaultConnectionOptions - -- * Handshake- , acceptRequest- , rejectRequest+ -- ** Compression options+ , CompressionOptions (..)+ , PermessageDeflate (..)+ , defaultPermessageDeflate - -- * Various- , I.getVersion+ -- ** Protection limits+ , SizeLimit (..) - -- * Receiving- , I.receive+ -- * Sending and receiving messages+ , receive , receiveDataMessage , receiveData-- -- * Sending- , I.send+ , send+ , sendDataMessage+ , sendDataMessages , sendTextData+ , sendTextDatas , sendBinaryData+ , sendBinaryDatas+ , sendClose+ , sendCloseCode+ , sendPing - -- * Asynchronous sending- , I.Sink- , I.sendSink- , I.getSink- , I.close- , I.ping- , I.pong- , I.textData- , I.binaryData- , I.spawnPingThread+ -- * HTTP Types+ , Headers+ , Request (..)+ , RequestHead (..)+ , getRequestSubprotocols+ , Response (..)+ , ResponseHead (..) - -- * Error Handling- , I.throwWsError- , I.catchWsError- , I.HandshakeError(..)- , I.ConnectionError(..)- ) where+ -- * WebSocket message types+ , Message (..)+ , ControlMessage (..)+ , DataMessage (..)+ , WebSocketsData (..) -import Control.Monad.Trans (liftIO)+ -- * Exceptions+ , HandshakeException (..)+ , ConnectionException (..) -import qualified Network.WebSockets.Handshake as I-import qualified Network.WebSockets.Handshake.Http as I-import qualified Network.WebSockets.Monad as I-import qualified Network.WebSockets.Protocol as I-import qualified Network.WebSockets.Protocol.Hybi00 as I-import qualified Network.WebSockets.Protocol.Hybi10 as I-import qualified Network.WebSockets.Protocol.Unsafe as Unsafe-import qualified Network.WebSockets.Socket as I-import qualified Network.WebSockets.Types as I+ -- * Running a standalone server+ , ServerApp+ , runServer+ , runServerWith+ , ServerOptions (..)+ , defaultServerOptions+ , runServerWithOptions --- This doesn't work this way any more. As the Protocol first has to be--- determined by the request, we can't provide this as a WebSockets action. See--- the various flavours of runWebSockets.+ -- * Utilities for writing your own server+ , makeListenSocket+ , makePendingConnection+ , makePendingConnectionFromStream --- | Receive an application message. Automatically respond to control messages.-receiveDataMessage :: I.Protocol p => I.WebSockets p (I.DataMessage p)-receiveDataMessage = do- m <- I.receive- case m of- (I.DataMessage am) -> return am- (I.ControlMessage cm) -> case cm of- I.Close _ -> I.throwWsError I.ConnectionClosed- I.Pong _ -> do- options <- I.getOptions- liftIO $ I.onPong options- receiveDataMessage- I.Ping pl -> do- -- Note that we are using an /unsafe/ pong here. If the - -- underlying protocol cannot encode this pong, our thread will- -- crash. We assume, however that the protocol /is/ able to- -- encode the pong, since it was able to encode a ping.- I.send $ Unsafe.pong pl- receiveDataMessage+ -- * Running a client+ , ClientApp+ , runClient+ , runClientWith+ , runClientWithSocket+ , runClientWithStream+ , newClientConnection --- | Receive a message, treating it as data transparently-receiveData :: (I.Protocol p, I.WebSocketsData a) => I.WebSockets p a-receiveData = do- dm <- receiveDataMessage- case dm of- I.Text x -> return (I.fromLazyByteString x)- I.Binary x -> return (I.fromLazyByteString x)+ -- * Utilities+ , PingPongOptions(..)+ , defaultPingPongOptions+ , withPingPong+ , withPingThread+ , forkPingThread+ ) where --- | Send a 'I.Response' to the socket immediately.-sendResponse :: I.Protocol p => I.Response -> I.WebSockets p ()-sendResponse = I.sendBuilder . I.encodeResponse --- | Send a text message-sendTextData :: (I.TextProtocol p, I.WebSocketsData a) => a -> I.WebSockets p ()-sendTextData = I.send . I.textData---- | Send some binary data-sendBinaryData :: (I.BinaryProtocol p, I.WebSocketsData a)- => a -> I.WebSockets p ()-sendBinaryData = I.send . I.binaryData---- | Reject a request, sending a 400 (Bad Request) to the client and throwing a--- RequestRejected (HandshakeError)-rejectRequest :: I.Protocol p- => I.Request -> String -> I.WebSockets p a-rejectRequest req reason = failHandshakeWith $ I.RequestRejected req reason--failHandshakeWith :: forall p a. I.Protocol p- => I.HandshakeError -> I.WebSockets p a-failHandshakeWith err = do- sendResponse $ I.responseError (undefined :: p) err- I.throwWsError err---- | Accept a request. After this, you can start sending and receiving data.-acceptRequest :: I.Protocol p => I.Request -> I.WebSockets p ()-acceptRequest = sendResponse . I.requestResponse+--------------------------------------------------------------------------------+import Network.WebSockets.Client+import Network.WebSockets.Connection+import Network.WebSockets.Connection.PingPong+import Network.WebSockets.Http+import Network.WebSockets.Server+import Network.WebSockets.Types
+ src/Network/WebSockets/Client.hs view
@@ -0,0 +1,190 @@+--------------------------------------------------------------------------------+-- | This part of the library provides you with utilities to create WebSockets+-- clients (in addition to servers).+module Network.WebSockets.Client+ ( ClientApp+ , runClient+ , runClientWith+ , runClientWithSocket+ , runClientWithStream+ , newClientConnection+ -- * Low level functionality+ , createRequest+ , Protocol(..)+ , defaultProtocol+ , checkServerResponse+ , streamToClientConnection+ ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString.Builder as Builder+import Control.Exception (bracket, finally, throwIO)+import Control.Concurrent.MVar (newEmptyMVar)+import Control.Monad (void)+import Data.IORef (newIORef)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Network.Socket as S+import System.Timeout (timeout)+++--------------------------------------------------------------------------------+import Network.WebSockets.Connection+import Network.WebSockets.Http+import Network.WebSockets.Protocol+import Network.WebSockets.Stream (Stream)+import qualified Network.WebSockets.Stream as Stream+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+-- | A client application interacting with a single server. Once this 'IO'+-- action finished, the underlying socket is closed automatically.+type ClientApp a = Connection -> IO a+++--------------------------------------------------------------------------------+-- TODO: Maybe this should all be strings+runClient :: String -- ^ Host+ -> Int -- ^ Port+ -> String -- ^ Path+ -> ClientApp a -- ^ Client application+ -> IO a+runClient host port path ws =+ runClientWith host port path defaultConnectionOptions [] ws+++--------------------------------------------------------------------------------+runClientWith :: String -- ^ Host+ -> Int -- ^ Port+ -> String -- ^ Path+ -> ConnectionOptions -- ^ Options+ -> Headers -- ^ Custom headers to send+ -> ClientApp a -- ^ Client application+ -> IO a+runClientWith host port path0 opts customHeaders app = do+ -- Create and connect socket+ let hints = S.defaultHints+ {S.addrSocketType = S.Stream}++ -- Correct host and path.+ fullHost = if port == 80 then host else (host ++ ":" ++ show port)+ path = if null path0 then "/" else path0+ addr:_ <- S.getAddrInfo (Just hints) (Just host) (Just $ show port)+ sock <- S.socket (S.addrFamily addr) S.Stream S.defaultProtocol+ S.setSocketOption sock S.NoDelay 1++ -- Connect WebSocket and run client+ res <- bracket+ (timeout (connectionTimeout opts * 1000 * 1000) $ S.connect sock (S.addrAddress addr))+ (const $ S.close sock) $ \maybeConnected -> case maybeConnected of+ Nothing -> throwIO $ ConnectionTimeout+ Just () -> runClientWithSocket sock fullHost path opts customHeaders app+++ -- Clean up+ return res+++--------------------------------------------------------------------------------++runClientWithStream+ :: Stream+ -- ^ Stream+ -> String+ -- ^ Host+ -> String+ -- ^ Path+ -> ConnectionOptions+ -- ^ Connection options+ -> Headers+ -- ^ Custom headers to send+ -> ClientApp a+ -- ^ Client application+ -> IO a+runClientWithStream stream host path opts customHeaders app = do+ newClientConnection stream host path opts customHeaders >>= app++-- | Build a new 'Connection' from the client's point of view.+--+-- /WARNING/: Be sure to call 'Stream.close' on the given 'Stream' after you are+-- done using the 'Connection' in order to properly close the communication+-- channel. 'runClientWithStream' handles this for you, prefer to use it when+-- possible.+newClientConnection+ :: Stream+ -- ^ Stream that will be used by the new 'Connection'.+ -> String+ -- ^ Host+ -> String+ -- ^ Path+ -> ConnectionOptions+ -- ^ Connection options+ -> Headers+ -- ^ Custom headers to send+ -> IO Connection+newClientConnection stream host path opts customHeaders = do+ -- Create the request and send it+ request <- createRequest protocol bHost bPath False customHeaders+ Stream.write stream (Builder.toLazyByteString $ encodeRequestHead request)+ checkServerResponse stream request+ streamToClientConnection stream opts+ where+ protocol = defaultProtocol -- TODO+ bHost = T.encodeUtf8 $ T.pack host+ bPath = T.encodeUtf8 $ T.pack path++-- | Check the response from the server.+-- Throws 'OtherHandshakeException' on failure+checkServerResponse :: Stream -> RequestHead -> IO ()+checkServerResponse stream request = do+ mbResponse <- Stream.parse stream decodeResponseHead+ response <- case mbResponse of+ Just response -> return response+ Nothing -> throwIO $ OtherHandshakeException $+ "Network.WebSockets.Client.newClientConnection: no handshake " +++ "response from server"+ void $ either throwIO return $ finishResponse protocol request response+ where+ protocol = defaultProtocol -- TODO+++-- | Build a 'Connection' from a pre-established stream with already finished+-- handshake.+--+-- /NB/: this will not perform any handshaking.+streamToClientConnection :: Stream -> ConnectionOptions -> IO Connection+streamToClientConnection stream opts = do+ parse <- decodeMessages protocol+ (connectionFramePayloadSizeLimit opts)+ (connectionMessageDataSizeLimit opts) stream+ write <- encodeMessages protocol ClientConnection stream+ sentRef <- newIORef False+ heartbeat <- newEmptyMVar+ return $ Connection+ { connectionOptions = opts+ , connectionType = ClientConnection+ , connectionProtocol = protocol+ , connectionParse = parse+ , connectionWrite = write+ , connectionHeartbeat = heartbeat+ , connectionSentClose = sentRef+ }+ where+ protocol = defaultProtocol+++--------------------------------------------------------------------------------+runClientWithSocket :: S.Socket -- ^ Socket+ -> String -- ^ Host+ -> String -- ^ Path+ -> ConnectionOptions -- ^ Options+ -> Headers -- ^ Custom headers to send+ -> ClientApp a -- ^ Client application+ -> IO a+runClientWithSocket sock host path opts customHeaders app = bracket+ (Stream.makeSocketStream sock)+ Stream.close+ (\stream ->+ runClientWithStream stream host path opts customHeaders app)
+ src/Network/WebSockets/Connection.hs view
@@ -0,0 +1,457 @@+--------------------------------------------------------------------------------+-- | This module exposes connection internals+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Connection+ ( PendingConnection (..)+ , acceptRequest+ , AcceptRequest(..)+ , defaultAcceptRequest+ , acceptRequestWith+ , rejectRequest+ , RejectRequest(..)+ , defaultRejectRequest+ , rejectRequestWith++ , Connection (..)++ , ConnectionOptions (..)+ , defaultConnectionOptions++ , receive+ , receiveDataMessage+ , receiveData+ , send+ , sendDataMessage+ , sendDataMessages+ , sendTextData+ , sendTextDatas+ , sendBinaryData+ , sendBinaryDatas+ , sendClose+ , sendCloseCode+ , sendPing+ , sendPong++ , withPingThread+ , forkPingThread+ , pingThread++ , CompressionOptions (..)+ , PermessageDeflate (..)+ , defaultPermessageDeflate++ , SizeLimit (..)+ ) where+++--------------------------------------------------------------------------------+import Control.Applicative ((<$>))+import Control.Concurrent (forkIO,+ threadDelay)+import qualified Control.Concurrent.Async as Async+import Control.Concurrent.MVar (MVar, newEmptyMVar, tryPutMVar)+import Control.Exception (AsyncException,+ fromException,+ handle,+ throwIO)+import Control.Monad (foldM, unless,+ when)+import qualified Data.ByteString as B+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Char8 as B8+import Data.IORef (IORef,+ newIORef,+ readIORef,+ writeIORef)+import Data.List (find)+import Data.Maybe (catMaybes)+import qualified Data.Text as T+import Data.Word (Word16)+import Prelude+++--------------------------------------------------------------------------------+import Network.WebSockets.Connection.Options+import Network.WebSockets.Extensions as Extensions+import Network.WebSockets.Extensions.PermessageDeflate+import Network.WebSockets.Extensions.StrictUnicode+import Network.WebSockets.Http+import Network.WebSockets.Protocol+import Network.WebSockets.Stream (Stream)+import qualified Network.WebSockets.Stream as Stream+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+-- | A new client connected to the server. We haven't accepted the connection+-- yet, though.+data PendingConnection = PendingConnection+ { pendingOptions :: !ConnectionOptions+ -- ^ Options, passed as-is to the 'Connection'+ , pendingRequest :: !RequestHead+ -- ^ Useful for e.g. inspecting the request path.+ , pendingOnAccept :: !(Connection -> IO ())+ -- ^ One-shot callback fired when a connection is accepted, i.e., *after*+ -- the accepting response is sent to the client.+ , pendingStream :: !Stream+ -- ^ Input/output stream+ }+++--------------------------------------------------------------------------------+-- | This datatype allows you to set options for 'acceptRequestWith'. It is+-- strongly recommended to use 'defaultAcceptRequest' and then modify the+-- various fields, that way new fields introduced in the library do not break+-- your code.+data AcceptRequest = AcceptRequest+ { acceptSubprotocol :: !(Maybe B.ByteString)+ -- ^ The subprotocol to speak with the client. If 'pendingSubprotcols' is+ -- non-empty, 'acceptSubprotocol' must be one of the subprotocols from the+ -- list.+ , acceptHeaders :: !Headers+ -- ^ Extra headers to send with the response.+ }+++--------------------------------------------------------------------------------+defaultAcceptRequest :: AcceptRequest+defaultAcceptRequest = AcceptRequest Nothing []+++--------------------------------------------------------------------------------+-- | Utility+sendResponse :: PendingConnection -> Response -> IO ()+sendResponse pc rsp = Stream.write (pendingStream pc)+ (Builder.toLazyByteString (encodeResponse rsp))+++--------------------------------------------------------------------------------+-- | Accept a pending connection, turning it into a 'Connection'.+acceptRequest :: PendingConnection -> IO Connection+acceptRequest pc = acceptRequestWith pc defaultAcceptRequest+++--------------------------------------------------------------------------------+-- | This function is like 'acceptRequest' but allows you to set custom options+-- using the 'AcceptRequest' datatype.+acceptRequestWith :: PendingConnection -> AcceptRequest -> IO Connection+acceptRequestWith pc ar = case find (flip compatible request) protocols of+ Nothing -> do+ sendResponse pc $ response400 versionHeader ""+ throwIO NotSupported+ Just protocol -> do++ -- Get requested list of exceptions from client.+ rqExts <- either throwIO return $+ getRequestSecWebSocketExtensions request++ -- Set up permessage-deflate extension if configured.+ pmdExt <- case connectionCompressionOptions (pendingOptions pc) of+ NoCompression -> return Nothing+ PermessageDeflateCompression pmd0 ->+ case negotiateDeflate (connectionMessageDataSizeLimit options) (Just pmd0) rqExts of+ Left err -> do+ rejectRequestWith pc defaultRejectRequest {rejectMessage = B8.pack err}+ throwIO NotSupported+ Right pmd1 -> return (Just pmd1)++ -- Set up strict utf8 extension if configured.+ let unicodeExt =+ if connectionStrictUnicode (pendingOptions pc)+ then Just strictUnicode else Nothing++ -- Final extension list.+ let exts = catMaybes [pmdExt, unicodeExt]++ let subproto = maybe [] (\p -> [("Sec-WebSocket-Protocol", p)]) $ acceptSubprotocol ar+ headers = subproto ++ acceptHeaders ar ++ concatMap extHeaders exts+ response = finishRequest protocol request headers++ either throwIO (sendResponse pc) response++ parseRaw <- decodeMessages+ protocol+ (connectionFramePayloadSizeLimit options)+ (connectionMessageDataSizeLimit options)+ (pendingStream pc)+ writeRaw <- encodeMessages protocol ServerConnection (pendingStream pc)++ write <- foldM (\x ext -> extWrite ext x) writeRaw exts+ parse <- foldM (\x ext -> extParse ext x) parseRaw exts++ sentRef <- newIORef False+ heartbeat <- newEmptyMVar+ let connection = Connection+ { connectionOptions = options+ , connectionType = ServerConnection+ , connectionProtocol = protocol+ , connectionParse = parse+ , connectionWrite = write+ , connectionHeartbeat = heartbeat+ , connectionSentClose = sentRef+ }++ pendingOnAccept pc connection+ return connection+ where+ options = pendingOptions pc+ request = pendingRequest pc+ versionHeader = [("Sec-WebSocket-Version",+ B.intercalate ", " $ concatMap headerVersions protocols)]+++--------------------------------------------------------------------------------+-- | Parameters that allow you to tweak how a request is rejected. Please use+-- 'defaultRejectRequest' and modify fields using record syntax so your code+-- will not break when new fields are added.+data RejectRequest = RejectRequest+ { -- | The status code, 400 by default.+ rejectCode :: !Int+ , -- | The message, "Bad Request" by default+ rejectMessage :: !B.ByteString+ , -- | Extra headers to be sent with the response.+ rejectHeaders :: Headers+ , -- | Reponse body of the rejection.+ rejectBody :: !B.ByteString+ }+++--------------------------------------------------------------------------------+defaultRejectRequest :: RejectRequest+defaultRejectRequest = RejectRequest+ { rejectCode = 400+ , rejectMessage = "Bad Request"+ , rejectHeaders = []+ , rejectBody = ""+ }+++--------------------------------------------------------------------------------+rejectRequestWith+ :: PendingConnection -- ^ Connection to reject+ -> RejectRequest -- ^ Params on how to reject the request+ -> IO ()+rejectRequestWith pc reject = sendResponse pc $ Response+ ResponseHead+ { responseCode = rejectCode reject+ , responseMessage = rejectMessage reject+ , responseHeaders = rejectHeaders reject+ }+ (rejectBody reject)+++--------------------------------------------------------------------------------+-- | Requires calling 'pendingStream' and 'Stream.close'.+rejectRequest+ :: PendingConnection -- ^ Connection to reject+ -> B.ByteString -- ^ Rejection response body+ -> IO ()+rejectRequest pc body = rejectRequestWith pc+ defaultRejectRequest {rejectBody = body}+++--------------------------------------------------------------------------------+data Connection = Connection+ { connectionOptions :: !ConnectionOptions+ , connectionType :: !ConnectionType+ , connectionProtocol :: !Protocol+ , connectionHeartbeat :: !(MVar ())+ -- ^ This MVar is filled whenever a pong is received. This is used by+ -- 'withPingPong' to timeout the connection if a pong is not received.+ , connectionParse :: !(IO (Maybe Message))+ , connectionWrite :: !([Message] -> IO ())+ , connectionSentClose :: !(IORef Bool)+ -- ^ According to the RFC, both the client and the server MUST send+ -- a close control message to each other. Either party can initiate+ -- the first close message but then the other party must respond. Finally,+ -- the server is in charge of closing the TCP connection. This IORef tracks+ -- if we have sent a close message and are waiting for the peer to respond.+ }+++--------------------------------------------------------------------------------+receive :: Connection -> IO Message+receive conn = do+ mbMsg <- connectionParse conn+ case mbMsg of+ Nothing -> throwIO ConnectionClosed+ Just msg -> return msg+++--------------------------------------------------------------------------------+-- | Receive an application message. Automatically respond to control messages.+--+-- When the peer sends a close control message, an exception of type 'CloseRequest'+-- is thrown. The peer can send a close control message either to initiate a+-- close or in response to a close message we have sent to the peer. In either+-- case the 'CloseRequest' exception will be thrown. The RFC specifies that+-- the server is responsible for closing the TCP connection, which should happen+-- after receiving the 'CloseRequest' exception from this function.+--+-- This will throw 'ConnectionClosed' if the TCP connection dies unexpectedly.+receiveDataMessage :: Connection -> IO DataMessage+receiveDataMessage conn = do+ msg <- receive conn+ case msg of+ DataMessage _ _ _ am -> return am+ ControlMessage cm -> case cm of+ Close i closeMsg -> do+ hasSentClose <- readIORef $ connectionSentClose conn+ unless hasSentClose $ send conn msg+ throwIO $ CloseRequest i closeMsg+ Pong _ -> do+ _ <- tryPutMVar (connectionHeartbeat conn) ()+ connectionOnPong (connectionOptions conn)+ receiveDataMessage conn+ Ping pl -> do+ send conn (ControlMessage (Pong pl))+ receiveDataMessage conn+++--------------------------------------------------------------------------------+-- | Receive a message, converting it to whatever format is needed.+receiveData :: WebSocketsData a => Connection -> IO a+receiveData conn = fromDataMessage <$> receiveDataMessage conn+++--------------------------------------------------------------------------------+send :: Connection -> Message -> IO ()+send conn = sendAll conn . return++--------------------------------------------------------------------------------+sendAll :: Connection -> [Message] -> IO ()+sendAll _ [] = return ()+sendAll conn msgs = do+ when (any isCloseMessage msgs) $+ writeIORef (connectionSentClose conn) True+ connectionWrite conn msgs+ where+ isCloseMessage (ControlMessage (Close _ _)) = True+ isCloseMessage _ = False++--------------------------------------------------------------------------------+-- | Send a 'DataMessage'. This allows you send both human-readable text and+-- binary data. This is a slightly more low-level interface than 'sendTextData'+-- or 'sendBinaryData'.+sendDataMessage :: Connection -> DataMessage -> IO ()+sendDataMessage conn = sendDataMessages conn . return++--------------------------------------------------------------------------------+-- | Send a collection of 'DataMessage's. This is more efficient than calling+-- 'sendDataMessage' many times.+sendDataMessages :: Connection -> [DataMessage] -> IO ()+sendDataMessages conn = sendAll conn . map (DataMessage False False False)++--------------------------------------------------------------------------------+-- | Send a textual message. The message will be encoded as UTF-8. This should+-- be the default choice for human-readable text-based protocols such as JSON.+sendTextData :: WebSocketsData a => Connection -> a -> IO ()+sendTextData conn = sendTextDatas conn . return++--------------------------------------------------------------------------------+-- | Send a number of textual messages. This is more efficient than calling+-- 'sendTextData' many times.+sendTextDatas :: WebSocketsData a => Connection -> [a] -> IO ()+sendTextDatas conn =+ sendDataMessages conn .+ map (\x -> Text (toLazyByteString x) Nothing)++--------------------------------------------------------------------------------+-- | Send a binary message. This is useful for sending binary blobs, e.g.+-- images, data encoded with MessagePack, images...+sendBinaryData :: WebSocketsData a => Connection -> a -> IO ()+sendBinaryData conn = sendBinaryDatas conn . return++--------------------------------------------------------------------------------+-- | Send a number of binary messages. This is more efficient than calling+-- 'sendBinaryData' many times.+sendBinaryDatas :: WebSocketsData a => Connection -> [a] -> IO ()+sendBinaryDatas conn = sendDataMessages conn . map (Binary . toLazyByteString)++--------------------------------------------------------------------------------+-- | Send a friendly close message. Note that after sending this message,+-- you should still continue calling 'receiveDataMessage' to process any+-- in-flight messages. The peer will eventually respond with a close control+-- message of its own which will cause 'receiveDataMessage' to throw the+-- 'CloseRequest' exception. This exception is when you can finally consider+-- the connection closed.+sendClose :: WebSocketsData a => Connection -> a -> IO ()+sendClose conn = sendCloseCode conn 1000+++--------------------------------------------------------------------------------+-- | Send a friendly close message and close code. Similar to 'sendClose',+-- you should continue calling 'receiveDataMessage' until you receive a+-- 'CloseRequest' exception.+--+-- See <http://tools.ietf.org/html/rfc6455#section-7.4> for a list of close+-- codes.+sendCloseCode :: WebSocketsData a => Connection -> Word16 -> a -> IO ()+sendCloseCode conn code =+ send conn . ControlMessage . Close code . toLazyByteString+++--------------------------------------------------------------------------------+-- | Send a ping+sendPing :: WebSocketsData a => Connection -> a -> IO ()+sendPing conn = send conn . ControlMessage . Ping . toLazyByteString++--------------------------------------------------------------------------------+-- | Send a pong+sendPong :: WebSocketsData a => Connection -> a -> IO ()+sendPong conn = send conn . ControlMessage . Pong . toLazyByteString++--------------------------------------------------------------------------------+-- | Forks a ping thread, sending a ping message every @n@ seconds over the+-- connection. The thread is killed when the inner IO action is finished.+--+-- This is useful to keep idle connections open through proxies and whatnot.+-- Many (but not all) proxies have a 60 second default timeout, so based on that+-- sending a ping every 30 seconds is a good idea.+--+-- Note that usually you want to use 'Network.WebSockets.Connection.PingPong.withPingPong'+-- to timeout the connection if a pong is not received.+withPingThread+ :: Connection+ -> Int -- ^ Second interval in which pings should be sent.+ -> IO () -- ^ Repeat this after sending a ping.+ -> IO a -- ^ Application to wrap with a ping thread.+ -> IO a -- ^ Executes application and kills ping thread when done.+withPingThread conn n action app =+ Async.withAsync (pingThread conn n action) (\_ -> app)++--------------------------------------------------------------------------------+-- | DEPRECATED: Use 'withPingThread' instead.+--+-- Forks a ping thread, sending a ping message every @n@ seconds over the+-- connection. The thread dies silently if the connection crashes or is closed.+--+-- This is useful to keep idle connections open through proxies and whatnot.+-- Many (but not all) proxies have a 60 second default timeout, so based on that+-- sending a ping every 30 seconds is a good idea.+forkPingThread :: Connection -> Int -> IO ()+forkPingThread conn n = do+ _ <- forkIO $ pingThread conn n (return ())+ return ()+{-# DEPRECATED forkPingThread "Use 'withPingThread' instead" #-}+++--------------------------------------------------------------------------------+-- | Use this if you want to run the ping thread yourself.+--+-- See also 'withPingThread'.+pingThread :: Connection -> Int -> IO () -> IO ()+pingThread conn n action+ | n <= 0 = return ()+ | otherwise = ignore `handle` go 1+ where+ go :: Int -> IO ()+ go i = do+ threadDelay (n * 1000 * 1000)+ sendPing conn (T.pack $ show i)+ action+ go (i + 1)++ ignore e = case fromException e of+ Just async -> throwIO (async :: AsyncException)+ Nothing -> return ()
+ src/Network/WebSockets/Connection/Options.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE CPP #-}+--------------------------------------------------------------------------------+module Network.WebSockets.Connection.Options+ ( ConnectionOptions (..)+ , defaultConnectionOptions++ , CompressionOptions (..)+ , PermessageDeflate (..)+ , defaultPermessageDeflate++ , SizeLimit (..)+ , atMostSizeLimit+ ) where+++--------------------------------------------------------------------------------+import Data.Int (Int64)+import Data.Monoid (Monoid (..))+import Prelude+++--------------------------------------------------------------------------------+-- | Set options for a 'Connection'. Please do not use this constructor+-- directly, but rather use 'defaultConnectionOptions' and then set the fields+-- you want, e.g.:+--+-- > myOptions = defaultConnectionOptions {connectionStrictUnicode = True}+--+-- This way your code does not break if the library introduces new fields.+data ConnectionOptions = ConnectionOptions+ { connectionOnPong :: !(IO ())+ -- ^ Whenever a 'pong' is received, this IO action is executed. It can be+ -- used to tickle connections or fire missiles.+ , connectionTimeout :: !Int+ -- ^ Timeout for connection establishment in seconds. Only used in the client.+ , connectionCompressionOptions :: !CompressionOptions+ -- ^ Enable 'PermessageDeflate'.+ , connectionStrictUnicode :: !Bool+ -- ^ Enable strict unicode on the connection. This means that if a client+ -- (or server) sends invalid UTF-8, we will throw a 'UnicodeException'+ -- rather than replacing it by the unicode replacement character U+FFFD.+ , connectionFramePayloadSizeLimit :: !SizeLimit+ -- ^ The maximum size for incoming frame payload size in bytes. If a+ -- frame exceeds this limit, a 'ParseException' is thrown.+ , connectionMessageDataSizeLimit :: !SizeLimit+ -- ^ 'connectionFrameSizeLimit' is often not enough since a malicious+ -- client can send many small frames to create a huge message. This limit+ -- allows you to protect from that. If a message exceeds this limit, a+ -- 'ParseException' is thrown.+ --+ -- Note that, if compression is enabled, we check the size of the+ -- compressed messages, as well as the size of the uncompressed messages+ -- as we are deflating them to ensure we don't use too much memory in any+ -- case.+ }+++--------------------------------------------------------------------------------+-- | The default connection options:+--+-- * Nothing happens when a pong is received.+-- * Compression is disabled.+-- * Lenient unicode decoding.+-- * 30 second timeout for connection establishment.+defaultConnectionOptions :: ConnectionOptions+defaultConnectionOptions = ConnectionOptions+ { connectionOnPong = return ()+ , connectionTimeout = 30+ , connectionCompressionOptions = NoCompression+ , connectionStrictUnicode = False+ , connectionFramePayloadSizeLimit = mempty+ , connectionMessageDataSizeLimit = mempty+ }+++--------------------------------------------------------------------------------+data CompressionOptions+ = NoCompression+ | PermessageDeflateCompression PermessageDeflate+ deriving (Eq, Show)+++--------------------------------------------------------------------------------+-- | Four extension parameters are defined for "permessage-deflate" to+-- help endpoints manage per-connection resource usage.+--+-- - "server_no_context_takeover"+-- - "client_no_context_takeover"+-- - "server_max_window_bits"+-- - "client_max_window_bits"+data PermessageDeflate = PermessageDeflate+ { serverNoContextTakeover :: Bool+ , clientNoContextTakeover :: Bool+ , serverMaxWindowBits :: Int+ , clientMaxWindowBits :: Int+ , pdCompressionLevel :: Int+ } deriving (Eq, Show)+++--------------------------------------------------------------------------------+defaultPermessageDeflate :: PermessageDeflate+defaultPermessageDeflate = PermessageDeflate False False 15 15 8+++--------------------------------------------------------------------------------+-- | A size limit, in bytes. The 'Monoid' instance takes the minimum limit.+data SizeLimit+ = NoSizeLimit+ | SizeLimit !Int64+ deriving (Eq, Show)+++--------------------------------------------------------------------------------+instance Monoid SizeLimit where+ mempty = NoSizeLimit++#if !MIN_VERSION_base(4,11,0)+ mappend NoSizeLimit y = y+ mappend x NoSizeLimit = x+ mappend (SizeLimit x) (SizeLimit y) = SizeLimit (min x y)+#else+instance Semigroup SizeLimit where+ (<>) NoSizeLimit y = y+ (<>) x NoSizeLimit = x+ (<>) (SizeLimit x) (SizeLimit y) = SizeLimit (min x y)+#endif++--------------------------------------------------------------------------------+atMostSizeLimit :: Int64 -> SizeLimit -> Bool+atMostSizeLimit _ NoSizeLimit = True+atMostSizeLimit s (SizeLimit l) = s <= l+{-# INLINE atMostSizeLimit #-}
+ src/Network/WebSockets/Connection/PingPong.hs view
@@ -0,0 +1,62 @@+module Network.WebSockets.Connection.PingPong+ ( withPingPong+ , PingPongOptions(..)+ , PongTimeout(..)+ , defaultPingPongOptions+ ) where ++import Control.Concurrent.Async as Async+import Control.Exception+import Control.Monad (void)+import Network.WebSockets.Connection (Connection, connectionHeartbeat, pingThread)+import Control.Concurrent.MVar (takeMVar)+import System.Timeout (timeout)+++-- | Exception type used to kill connections if there+-- is a pong timeout.+data PongTimeout = PongTimeout deriving Show++instance Exception PongTimeout+++-- | Options for ping-pong+-- +-- Make sure that the ping interval is less than the pong timeout,+-- for example N/2.+data PingPongOptions = PingPongOptions {+ pingInterval :: Int, -- ^ Interval in seconds+ pongTimeout :: Int, -- ^ Timeout in seconds+ pingAction :: IO () -- ^ Action to perform after sending a ping+}++-- | Default options for ping-pong+-- +-- Ping every 15 seconds, timeout after 30 seconds+defaultPingPongOptions :: PingPongOptions+defaultPingPongOptions = PingPongOptions {+ pingInterval = 15,+ pongTimeout = 30,+ pingAction = return ()+}++-- | Run an application with ping-pong enabled. Raises PongTimeout if a pong is not received.+-- +-- Can used with Client and Server connections.+withPingPong :: PingPongOptions -> Connection -> (Connection -> IO ()) -> IO ()+withPingPong options connection app = void $ + withAsync (app connection) $ \appAsync -> do+ withAsync (pingThread connection (pingInterval options) (pingAction options)) $ \pingAsync -> do+ withAsync (heartbeat >> throwIO PongTimeout) $ \heartbeatAsync -> do+ waitAnyCancel [appAsync, pingAsync, heartbeatAsync]+ where+ heartbeat = whileJust $ timeout (pongTimeout options * 1000 * 1000) + $ takeMVar (connectionHeartbeat connection)++ -- Loop until action returns Nothing+ whileJust :: IO (Maybe a) -> IO ()+ whileJust action = do+ result <- action+ case result of+ Nothing -> return ()+ Just _ -> whileJust action
+ src/Network/WebSockets/Extensions.hs view
@@ -0,0 +1,24 @@+module Network.WebSockets.Extensions+ ( ExtensionDescription (..)+ , ExtensionDescriptions+ , parseExtensionDescriptions++ , NegotiateExtension+ , Extension (..)+ ) where++import Network.WebSockets.Extensions.Description+import Network.WebSockets.Http+import Network.WebSockets.Types++type NegotiateExtension = ExtensionDescriptions -> Either String Extension++-- | An extension is currently allowed to set extra headers and transform the+-- parse/write functions of 'Connection'.+--+-- This type is very likely to change as other extensions are introduced.+data Extension = Extension+ { extHeaders :: Headers+ , extParse :: IO (Maybe Message) -> IO (IO (Maybe Message))+ , extWrite :: ([Message] -> IO ()) -> IO ([Message] -> IO ())+ }
+ src/Network/WebSockets/Extensions/Description.hs view
@@ -0,0 +1,65 @@+-- | Code for parsing extensions headers.+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+module Network.WebSockets.Extensions.Description+ ( ExtensionParam+ , ExtensionDescription (..)+ , ExtensionDescriptions++ , parseExtensionDescriptions+ , encodeExtensionDescriptions+ ) where++import Control.Applicative ((*>), (<*))+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.ByteString.Char8 as AC8+import qualified Data.ByteString as B+import Data.Monoid (mconcat, mappend)+import Prelude++type ExtensionParam = (B.ByteString, Maybe B.ByteString)++data ExtensionDescription = ExtensionDescription+ { extName :: !B.ByteString+ , extParams :: ![ExtensionParam]+ } deriving (Eq, Show)++parseExtensionDescription :: A.Parser ExtensionDescription+parseExtensionDescription = do+ extName <- parseIdentifier+ extParams <- A.many' (token ';' *> parseParam)+ return ExtensionDescription {..}+ where+ parseIdentifier = AC8.takeWhile isIdentifierChar <* AC8.skipSpace++ token c = AC8.char8 c <* AC8.skipSpace++ isIdentifierChar c =+ (c >= 'a' && c <= 'z') ||+ (c >= 'A' && c <= 'Z') ||+ (c >= '0' && c <= '9') ||+ c == '-' || c == '_'++ parseParam :: A.Parser ExtensionParam+ parseParam = do+ name <- parseIdentifier+ val <- A.option Nothing $ fmap Just $ token '=' *> parseIdentifier+ return (name, val)++encodeExtensionDescription :: ExtensionDescription -> B.ByteString+encodeExtensionDescription ExtensionDescription {..} =+ mconcat (extName : map encodeParam extParams)+ where+ encodeParam (key, Nothing) = ";" `mappend` key+ encodeParam (key, Just val) = ";" `mappend` key `mappend` "=" `mappend` val++type ExtensionDescriptions = [ExtensionDescription]++parseExtensionDescriptions :: B.ByteString -> Either String ExtensionDescriptions+parseExtensionDescriptions = A.parseOnly $+ AC8.skipSpace *>+ A.sepBy parseExtensionDescription (AC8.char8 ',' <* AC8.skipSpace) <*+ A.endOfInput++encodeExtensionDescriptions :: ExtensionDescriptions -> B.ByteString+encodeExtensionDescriptions = B.intercalate "," . map encodeExtensionDescription
+ src/Network/WebSockets/Extensions/PermessageDeflate.hs view
@@ -0,0 +1,287 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}+module Network.WebSockets.Extensions.PermessageDeflate+ ( defaultPermessageDeflate+ , PermessageDeflate(..)+ , negotiateDeflate++ -- * Considered internal+ , makeMessageInflater+ , makeMessageDeflater+ ) where+++--------------------------------------------------------------------------------+import Control.Applicative ((<$>))+import Control.Exception (throwIO)+import Control.Monad (foldM, unless)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BL8+import qualified Data.ByteString.Lazy.Internal as BL+import Data.Int (Int64)+import Data.Monoid+import qualified Data.Streaming.Zlib as Zlib+import Network.WebSockets.Connection.Options+import Network.WebSockets.Extensions+import Network.WebSockets.Extensions.Description+import Network.WebSockets.Http+import Network.WebSockets.Types+import Prelude+import Text.Read (readMaybe)+++--------------------------------------------------------------------------------+-- | Convert the parameters to an 'ExtensionDescription' that we can put in a+-- 'Sec-WebSocket-Extensions' header.+toExtensionDescription :: PermessageDeflate -> ExtensionDescription+toExtensionDescription PermessageDeflate {..} = ExtensionDescription+ { extName = "permessage-deflate"+ , extParams =+ [("server_no_context_takeover", Nothing) | serverNoContextTakeover] +++ [("client_no_context_takeover", Nothing) | clientNoContextTakeover] +++ [("server_max_window_bits", param serverMaxWindowBits) | serverMaxWindowBits /= 15] +++ [("client_max_window_bits", param clientMaxWindowBits) | clientMaxWindowBits /= 15]+ }+ where+ param = Just . B8.pack . show+++--------------------------------------------------------------------------------+toHeaders :: PermessageDeflate -> Headers+toHeaders pmd =+ [ ( "Sec-WebSocket-Extensions"+ , encodeExtensionDescriptions [toExtensionDescription pmd]+ )+ ]+++--------------------------------------------------------------------------------+negotiateDeflate+ :: SizeLimit -> Maybe PermessageDeflate -> NegotiateExtension+negotiateDeflate messageLimit pmd0 exts0 = do+ (headers, pmd1) <- negotiateDeflateOpts exts0 pmd0+ return Extension+ { extHeaders = headers+ , extParse = \parseRaw -> do+ inflate <- makeMessageInflater messageLimit pmd1+ return $ do+ msg <- parseRaw+ case msg of+ Nothing -> return Nothing+ Just m -> fmap Just (inflate m)++ , extWrite = \writeRaw -> do+ deflate <- makeMessageDeflater pmd1+ return $ \msgs ->+ mapM deflate msgs >>= writeRaw+ }+ where+ negotiateDeflateOpts+ :: ExtensionDescriptions+ -> Maybe PermessageDeflate+ -> Either String (Headers, Maybe PermessageDeflate)++ negotiateDeflateOpts (ext : _) (Just x)+ | extName ext == "x-webkit-deflate-frame" = Right+ ([("Sec-WebSocket-Extensions", "x-webkit-deflate-frame")], Just x)++ negotiateDeflateOpts (ext : _) (Just x)+ | extName ext == "permessage-deflate" = do+ x' <- foldM setParam x (extParams ext)+ Right (toHeaders x', Just x')++ negotiateDeflateOpts (_ : exts) (Just x) =+ negotiateDeflateOpts exts (Just x)++ negotiateDeflateOpts _ _ = Right ([], Nothing)+++--------------------------------------------------------------------------------+setParam+ :: PermessageDeflate -> ExtensionParam -> Either String PermessageDeflate+setParam pmd ("server_no_context_takeover", _) =+ Right pmd {serverNoContextTakeover = True}++setParam pmd ("client_no_context_takeover", _) =+ Right pmd {clientNoContextTakeover = True}++setParam pmd ("server_max_window_bits", Nothing) =+ Right pmd {serverMaxWindowBits = 15}++setParam pmd ("server_max_window_bits", Just param) = do+ w <- parseWindow param+ Right pmd {serverMaxWindowBits = w}++setParam pmd ("client_max_window_bits", Nothing) = do+ Right pmd {clientMaxWindowBits = 15}++setParam pmd ("client_max_window_bits", Just param) = do+ w <- parseWindow param+ Right pmd {clientMaxWindowBits = w}++setParam pmd (_, _) = Right pmd+++--------------------------------------------------------------------------------+parseWindow :: B.ByteString -> Either String Int+parseWindow bs8 = case readMaybe (B8.unpack bs8) of+ Just w+ | w >= 8 && w <= 15 -> Right w+ | otherwise -> Left $ "Window out of bounds: " ++ show w+ Nothing -> Left $ "Can't parse window: " ++ show bs8+++--------------------------------------------------------------------------------+-- | If the window_bits parameter is set to 8, we must set it to 9 instead.+--+-- Related issues:+-- - https://github.com/haskell/zlib/issues/11+-- - https://github.com/madler/zlib/issues/94+--+-- Quote from zlib manual:+--+-- For the current implementation of deflate(), a windowBits value of 8 (a+-- window size of 256 bytes) is not supported. As a result, a request for 8 will+-- result in 9 (a 512-byte window). In that case, providing 8 to inflateInit2()+-- will result in an error when the zlib header with 9 is checked against the+-- initialization of inflate(). The remedy is to not use 8 with deflateInit2()+-- with this initialization, or at least in that case use 9 with inflateInit2().+fixWindowBits :: Int -> Int+fixWindowBits n+ | n < 9 = 9+ | n > 15 = 15+ | otherwise = n+++--------------------------------------------------------------------------------+appTailL :: BL.ByteString+appTailL = BL.pack [0x00,0x00,0xff,0xff]+++--------------------------------------------------------------------------------+maybeStrip :: BL.ByteString -> BL.ByteString+maybeStrip x | appTailL `BL.isSuffixOf` x = BL.take (BL.length x - 4) x+maybeStrip x = x+++--------------------------------------------------------------------------------+rejectExtensions :: Message -> IO Message+rejectExtensions (DataMessage rsv1 rsv2 rsv3 _) | rsv1 || rsv2 || rsv3 =+ throwIO $ CloseRequest 1002 "Protocol Error"+rejectExtensions x = return x+++--------------------------------------------------------------------------------+makeMessageDeflater+ :: Maybe PermessageDeflate -> IO (Message -> IO Message)+makeMessageDeflater Nothing = return rejectExtensions+makeMessageDeflater (Just pmd)+ | serverNoContextTakeover pmd = do+ return $ \msg -> do+ ptr <- initDeflate pmd+ deflateMessageWith (deflateBody ptr) msg+ | otherwise = do+ ptr <- initDeflate pmd+ return $ \msg ->+ deflateMessageWith (deflateBody ptr) msg+ where+ ----------------------------------------------------------------------------+ initDeflate :: PermessageDeflate -> IO Zlib.Deflate+ initDeflate PermessageDeflate {..} =+ Zlib.initDeflate+ pdCompressionLevel+ (Zlib.WindowBits (- (fixWindowBits serverMaxWindowBits)))+++ ----------------------------------------------------------------------------+ deflateMessageWith+ :: (BL.ByteString -> IO BL.ByteString)+ -> Message -> IO Message+ deflateMessageWith deflater (DataMessage False False False (Text x _)) = do+ x' <- deflater x+ return (DataMessage True False False (Text x' Nothing))+ deflateMessageWith deflater (DataMessage False False False (Binary x)) = do+ x' <- deflater x+ return (DataMessage True False False (Binary x'))+ deflateMessageWith _ x = return x+++ ----------------------------------------------------------------------------+ deflateBody :: Zlib.Deflate -> BL.ByteString -> IO BL.ByteString+ deflateBody ptr = fmap maybeStrip . go . BL.toChunks+ where+ go [] =+ dePopper (Zlib.flushDeflate ptr)+ go (c : cs) = do+ chunk <- Zlib.feedDeflate ptr c >>= dePopper+ (chunk <>) <$> go cs+++--------------------------------------------------------------------------------+dePopper :: Zlib.Popper -> IO BL.ByteString+dePopper p = p >>= \res -> case res of+ Zlib.PRDone -> return BL.empty+ Zlib.PRNext c -> BL.chunk c <$> dePopper p+ Zlib.PRError x -> throwIO $ CloseRequest 1002 (BL8.pack (show x))+++--------------------------------------------------------------------------------+makeMessageInflater+ :: SizeLimit -> Maybe PermessageDeflate+ -> IO (Message -> IO Message)+makeMessageInflater _ Nothing = return rejectExtensions+makeMessageInflater messageLimit (Just pmd)+ | clientNoContextTakeover pmd =+ return $ \msg -> do+ ptr <- initInflate pmd+ inflateMessageWith (inflateBody ptr) msg+ | otherwise = do+ ptr <- initInflate pmd+ return $ \msg ->+ inflateMessageWith (inflateBody ptr) msg+ where+ --------------------------------------------------------------------------------+ initInflate :: PermessageDeflate -> IO Zlib.Inflate+ initInflate PermessageDeflate {..} =+ Zlib.initInflate+ (Zlib.WindowBits (- (fixWindowBits clientMaxWindowBits)))+++ ----------------------------------------------------------------------------+ inflateMessageWith+ :: (BL.ByteString -> IO BL.ByteString)+ -> Message -> IO Message+ inflateMessageWith inflater (DataMessage True a b (Text x _)) = do+ x' <- inflater x+ return (DataMessage False a b (Text x' Nothing))+ inflateMessageWith inflater (DataMessage True a b (Binary x)) = do+ x' <- inflater x+ return (DataMessage False a b (Binary x'))+ inflateMessageWith _ x = return x+++ ----------------------------------------------------------------------------+ inflateBody :: Zlib.Inflate -> BL.ByteString -> IO BL.ByteString+ inflateBody ptr =+ go 0 . BL.toChunks . (<> appTailL)+ where+ go :: Int64 -> [B.ByteString] -> IO BL.ByteString+ go size0 [] = do+ chunk <- Zlib.flushInflate ptr+ checkSize (fromIntegral (B.length chunk) + size0)+ return (BL.fromStrict chunk)+ go size0 (c : cs) = do+ chunk <- Zlib.feedInflate ptr c >>= dePopper+ let size1 = size0 + BL.length chunk+ checkSize size1+ (chunk <>) <$> go size1 cs+++ ----------------------------------------------------------------------------+ checkSize :: Int64 -> IO ()+ checkSize size = unless (atMostSizeLimit size messageLimit) $ throwIO $+ ParseException $ "Message of size " ++ show size ++ " exceeded limit"
+ src/Network/WebSockets/Extensions/StrictUnicode.hs view
@@ -0,0 +1,40 @@+--------------------------------------------------------------------------------+module Network.WebSockets.Extensions.StrictUnicode+ ( strictUnicode+ ) where+++--------------------------------------------------------------------------------+import Control.Exception (throwIO)+import qualified Data.ByteString.Lazy as BL+import Network.WebSockets.Extensions+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+strictUnicode :: Extension+strictUnicode = Extension+ { extHeaders = []+ , extParse = \parseRaw -> return (parseRaw >>= strictParse)+ , extWrite = return+ }+++--------------------------------------------------------------------------------+strictParse :: Maybe Message -> IO (Maybe Message)+strictParse Nothing = return Nothing+strictParse (Just (DataMessage rsv1 rsv2 rsv3 (Text bl _))) =+ case decodeUtf8Strict bl of+ Left err -> throwIO err+ Right txt ->+ return (Just (DataMessage rsv1 rsv2 rsv3 (Text bl (Just txt))))+strictParse (Just msg@(ControlMessage (Close _ bl))) =+ -- If there is a body, the first two bytes of the body MUST be a 2-byte+ -- unsigned integer (in network byte order) representing a status code with+ -- value /code/ defined in Section 7.4. Following the 2-byte integer, the+ -- body MAY contain UTF-8-encoded data with value /reason/, the+ -- interpretation of which is not defined by this specification.+ case decodeUtf8Strict (BL.drop 2 bl) of+ Left err -> throwIO err+ Right _ -> return (Just msg)+strictParse (Just msg) = return (Just msg)
− src/Network/WebSockets/Handshake.hs
@@ -1,37 +0,0 @@--- | Implementation of the WebSocket handshake-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}-module Network.WebSockets.Handshake- ( HandshakeError (..)- , handshake- , responseError- ) where--import Data.List (find)--import qualified Data.ByteString as B-import qualified Data.Enumerator as E--import Network.WebSockets.Handshake.Http-import Network.WebSockets.Protocol---- | Receives and checks the client handshake. If no suitable protocol is found--- (or the client sends garbage), a 'HandshakeError' will be thrown.-handshake :: (Monad m, Protocol p)- => RequestHttpPart- -> E.Iteratee B.ByteString m (Request, p)-handshake rhp = case find (flip supported rhp) implementations of- Nothing -> E.throwError NotSupported- Just p -> do- rq <- finishRequest p rhp- return (rq, p)---- | Respond to errors encountered during handshake. First argument may be--- bottom.-responseError :: forall p. Protocol p => p -> HandshakeError -> Response-responseError _ err = response400 $ case err of- -- TODO: fix- NotSupported -> versionHeader -- Version negotiation- _ -> []- where- versionHeader = [("Sec-WebSocket-Version",- B.intercalate ", " $ concatMap headerVersions (implementations :: [p]))]
− src/Network/WebSockets/Handshake/Http.hs
@@ -1,129 +0,0 @@--- | Module dealing with HTTP: request data types, encoding and decoding...-{-# LANGUAGE DeriveDataTypeable, OverloadedStrings #-}-module Network.WebSockets.Handshake.Http- ( Headers- , RequestHttpPart (..)- , Request (..)- , Response (..)- , HandshakeError (..)- , getSecWebSocketVersion- , decodeRequest- , encodeResponse- , response101- , response400- ) where--import Data.Dynamic (Typeable)-import Data.Monoid (mappend, mconcat)-import Control.Applicative (pure, (<$>), (<*>), (*>), (<*))-import Control.Exception (Exception)-import Control.Monad.Error (Error (..))--import Data.ByteString.Char8 ()-import Data.ByteString.Internal (c2w)-import qualified Data.Attoparsec as A-import qualified Blaze.ByteString.Builder as Builder-import qualified Blaze.ByteString.Builder.Char.Utf8 as Builder-import qualified Data.ByteString as B-import qualified Data.CaseInsensitive as CI---- | Request headers-type Headers = [(CI.CI B.ByteString, B.ByteString)]---- | (Internally used) HTTP headers and requested path.-data RequestHttpPart = RequestHttpPart- { requestHttpPath :: !B.ByteString- , requestHttpHeaders :: Headers- , requestHttpSecure :: Bool- } deriving (Eq, Show)---- | Full request type-data Request = Request- { requestPath :: !B.ByteString- , requestHeaders :: Headers- , requestResponse :: Response- }- deriving (Show)---- | Response to a 'Request'-data Response = Response- { responseCode :: !Int- , responseMessage :: !B.ByteString- , responseHeaders :: Headers- , responseBody :: B.ByteString- } deriving (Show)---- | Error in case of failed handshake. Will be thrown as an iteratee--- exception. ('Error' condition).------ TODO: This should probably be in the Handshake module, and is solely here to--- prevent a cyclic dependency.-data HandshakeError- -- | We don't have a match for the protocol requested by the client.- -- todo: version parameter- = NotSupported- -- | The request was somehow invalid (missing headers or wrong security- -- token)- | MalformedRequest RequestHttpPart String- -- | The request was well-formed, but the library user rejected it.- -- (e.g. "unknown path")- | RequestRejected Request String- -- | for example "EOF came too early" (which is actually a parse error)- -- or for your own errors. (like "unknown path"?)- | OtherHandshakeError String- deriving (Show, Typeable)--instance Error HandshakeError where- strMsg = OtherHandshakeError--instance Exception HandshakeError---- | Get the @Sec-WebSocket-Version@ header-getSecWebSocketVersion :: RequestHttpPart -> Maybe B.ByteString-getSecWebSocketVersion p = lookup "Sec-WebSocket-Version" (requestHttpHeaders p)---- | Parse an initial request-decodeRequest :: Bool -> A.Parser RequestHttpPart-decodeRequest isSecure = RequestHttpPart- <$> requestLine- <*> A.manyTill header newline- <*> pure isSecure- where- space = A.word8 (c2w ' ')- newline = A.string "\r\n"-- requestLine = A.string "GET" *> space *> A.takeWhile1 (/= c2w ' ')- <* space- <* A.string "HTTP/1.1" <* newline-- header = (,)- <$> (CI.mk <$> A.takeWhile1 (/= c2w ':'))- <* A.string ": "- <*> A.takeWhile1 (/= c2w '\r')- <* newline---- | Encode an HTTP upgrade response-encodeResponse :: Response -> Builder.Builder-encodeResponse (Response code msg headers body) =- Builder.copyByteString "HTTP/1.1 " `mappend`- Builder.fromString (show code) `mappend`- Builder.fromChar ' ' `mappend`- Builder.fromByteString msg `mappend`- Builder.fromByteString "\r\n" `mappend`- mconcat (map header headers) `mappend`- Builder.copyByteString "\r\n" `mappend`- Builder.copyByteString body -- (body is empty except for version -00)- where- header (k, v) = mconcat $ map Builder.copyByteString- [CI.original k, ": ", v, "\r\n"]---- | An upgrade response-response101 :: Headers -> B.ByteString -> Response-response101 headers body = Response 101 "WebSocket Protocol Handshake"- (("Upgrade", "websocket") : ("Connection", "Upgrade") : headers)- body---- | Bad request----response400 :: Headers -> Response-response400 headers = Response 400 "Bad Request" headers ""
+ src/Network/WebSockets/Http.hs view
@@ -0,0 +1,267 @@+--------------------------------------------------------------------------------+-- | Module dealing with HTTP: request data types, encoding and decoding...+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Http+ ( Headers+ , RequestHead (..)+ , Request (..)+ , ResponseHead (..)+ , Response (..)+ , HandshakeException (..)++ , encodeRequestHead+ , encodeRequest+ , decodeRequestHead++ , encodeResponseHead+ , encodeResponse+ , decodeResponseHead+ , decodeResponse++ , response101+ , response400++ , getRequestHeader+ , getResponseHeader+ , getRequestSecWebSocketVersion+ , getRequestSubprotocols+ , getRequestSecWebSocketExtensions+ ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Builder.Extra as Builder+import Control.Applicative (pure, (*>), (<$>),+ (<*), (<*>))+import Control.Exception (Exception)+import qualified Data.Attoparsec.ByteString as A+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.ByteString.Char8 ()+import qualified Data.ByteString.Char8 as BC+import Data.ByteString.Internal (c2w)+import qualified Data.CaseInsensitive as CI+import Data.Dynamic (Typeable)+import Data.Monoid (mappend, mconcat)+import qualified Network.WebSockets.Extensions.Description as Extensions+++--------------------------------------------------------------------------------+-- | Request headers+type Headers = [(CI.CI ByteString, ByteString)]+++--------------------------------------------------------------------------------+-- | An HTTP request. The request body is not yet read.+data RequestHead = RequestHead+ { requestPath :: !B.ByteString+ , requestHeaders :: Headers+ , requestSecure :: Bool+ } deriving (Show)+++--------------------------------------------------------------------------------+-- | A request with a body+data Request = Request RequestHead B.ByteString+ deriving (Show)+++--------------------------------------------------------------------------------+-- | HTTP response, without body.+data ResponseHead = ResponseHead+ { responseCode :: !Int+ , responseMessage :: !B.ByteString+ , responseHeaders :: Headers+ } deriving (Show)+++--------------------------------------------------------------------------------+-- | A response including a body+data Response = Response ResponseHead B.ByteString+ deriving (Show)+++--------------------------------------------------------------------------------+-- | Error in case of failed handshake. Will be thrown as an 'Exception'.+--+-- TODO: This should probably be in the Handshake module, and is solely here to+-- prevent a cyclic dependency.+data HandshakeException+ -- | We don't have a match for the protocol requested by the client.+ -- todo: version parameter+ = NotSupported+ -- | The request was somehow invalid (missing headers or wrong security+ -- token)+ | MalformedRequest RequestHead String+ -- | The servers response was somehow invalid (missing headers or wrong+ -- security token)+ | MalformedResponse ResponseHead String+ -- | The request was well-formed, but the library user rejected it.+ -- (e.g. "unknown path")+ | RequestRejected RequestHead ResponseHead+ -- | The connection timed out+ | ConnectionTimeout+ -- | for example "EOF came too early" (which is actually a parse error)+ -- or for your own errors. (like "unknown path"?)+ | OtherHandshakeException String+ deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Exception HandshakeException+++--------------------------------------------------------------------------------+encodeRequestHead :: RequestHead -> Builder.Builder+encodeRequestHead (RequestHead path headers _) =+ Builder.byteStringCopy "GET " `mappend`+ Builder.byteStringCopy path `mappend`+ Builder.byteStringCopy " HTTP/1.1" `mappend`+ Builder.byteString "\r\n" `mappend`+ mconcat (map header headers) `mappend`+ Builder.byteStringCopy "\r\n"+ where+ header (k, v) = mconcat $ map Builder.byteStringCopy+ [CI.original k, ": ", v, "\r\n"]+++--------------------------------------------------------------------------------+encodeRequest :: Request -> Builder.Builder+encodeRequest (Request head' body) =+ encodeRequestHead head' `mappend` Builder.byteStringCopy body+++--------------------------------------------------------------------------------+-- | Parse an initial request+decodeRequestHead :: Bool -> A.Parser RequestHead+decodeRequestHead isSecure = RequestHead+ <$> requestLine+ <*> A.manyTill decodeHeaderLine newline+ <*> pure isSecure+ where+ space = A.word8 (c2w ' ')+ newline = A.string "\r\n"++ requestLine = A.string "GET" *> space *> A.takeWhile1 (/= c2w ' ')+ <* space+ <* A.string "HTTP/1.1" <* newline+++--------------------------------------------------------------------------------+-- | Encode an HTTP upgrade response+encodeResponseHead :: ResponseHead -> Builder.Builder+encodeResponseHead (ResponseHead code msg headers) =+ Builder.byteStringCopy "HTTP/1.1 " `mappend`+ Builder.stringUtf8 (show code) `mappend`+ Builder.charUtf8 ' ' `mappend`+ Builder.byteString msg `mappend`+ Builder.byteString "\r\n" `mappend`+ mconcat (map header headers) `mappend`+ Builder.byteStringCopy "\r\n"+ where+ header (k, v) = mconcat $ map Builder.byteStringCopy+ [CI.original k, ": ", v, "\r\n"]+++--------------------------------------------------------------------------------+encodeResponse :: Response -> Builder.Builder+encodeResponse (Response head' body) =+ encodeResponseHead head' `mappend` Builder.byteStringCopy body+++--------------------------------------------------------------------------------+-- | An upgrade response+response101 :: Headers -> B.ByteString -> Response+response101 headers = Response+ (ResponseHead 101 "WebSocket Protocol Handshake"+ (("Upgrade", "websocket") : ("Connection", "Upgrade") : headers))+++--------------------------------------------------------------------------------+-- | Bad request+response400 :: Headers -> B.ByteString -> Response+response400 headers = Response (ResponseHead 400 "Bad Request" headers)+++--------------------------------------------------------------------------------+-- | HTTP response parser+decodeResponseHead :: A.Parser ResponseHead+decodeResponseHead = ResponseHead+ <$> fmap (read . BC.unpack) code+ <*> message+ <*> A.manyTill decodeHeaderLine newline+ where+ space = A.word8 (c2w ' ')+ newline = A.string "\r\n"++ code = A.string "HTTP/1.1" *> space *> A.takeWhile1 digit <* space+ digit = \x -> x >= c2w '0' && x <= c2w '9'+ message = A.takeWhile (/= c2w '\r') <* newline+++--------------------------------------------------------------------------------+decodeResponse :: A.Parser Response+decodeResponse = Response <$> decodeResponseHead <*> A.takeByteString+++--------------------------------------------------------------------------------+getRequestHeader :: RequestHead+ -> CI.CI ByteString+ -> Either HandshakeException ByteString+getRequestHeader rq key = case lookup key (requestHeaders rq) of+ Just t -> Right t+ Nothing -> Left $ MalformedRequest rq $+ "Header missing: " ++ BC.unpack (CI.original key)+++--------------------------------------------------------------------------------+getResponseHeader :: ResponseHead+ -> CI.CI ByteString+ -> Either HandshakeException ByteString+getResponseHeader rsp key = case lookup key (responseHeaders rsp) of+ Just t -> Right t+ Nothing -> Left $ MalformedResponse rsp $+ "Header missing: " ++ BC.unpack (CI.original key)+++--------------------------------------------------------------------------------+-- | Get the @Sec-WebSocket-Version@ header+getRequestSecWebSocketVersion :: RequestHead -> Maybe B.ByteString+getRequestSecWebSocketVersion p =+ lookup "Sec-WebSocket-Version" (requestHeaders p)+++--------------------------------------------------------------------------------+-- | List of subprotocols specified by the client, in order of preference.+-- If the client did not specify a list of subprotocols, this will be the+-- empty list.+getRequestSubprotocols :: RequestHead -> [B.ByteString]+getRequestSubprotocols rh = maybe [] parse mproto+ where+ mproto = lookup "Sec-WebSocket-Protocol" $ requestHeaders rh+ parse = filter (not . B.null) . BC.splitWith (\o -> o == ',' || o == ' ')+++--------------------------------------------------------------------------------+-- | Get the @Sec-WebSocket-Extensions@ header+getRequestSecWebSocketExtensions+ :: RequestHead -> Either HandshakeException Extensions.ExtensionDescriptions+getRequestSecWebSocketExtensions rq =+ case lookup "Sec-WebSocket-Extensions" (requestHeaders rq) of+ Nothing -> Right []+ Just ext -> case Extensions.parseExtensionDescriptions ext of+ Right x -> Right x+ Left err -> Left $ MalformedRequest rq $+ "Malformed Sec-WebSockets-Extensions: " ++ err+++--------------------------------------------------------------------------------+decodeHeaderLine :: A.Parser (CI.CI ByteString, ByteString)+decodeHeaderLine = (,)+ <$> (CI.mk <$> A.takeWhile1 (/= c2w ':'))+ <* A.word8 (c2w ':')+ <* A.option (c2w ' ') (A.word8 (c2w ' '))+ <*> A.takeWhile (/= c2w '\r')+ <* A.string "\r\n"
+ src/Network/WebSockets/Hybi13.hs view
@@ -0,0 +1,258 @@+--------------------------------------------------------------------------------+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Hybi13+ ( headerVersions+ , finishRequest+ , finishResponse+ , encodeMessage+ , encodeMessages+ , decodeMessages+ , createRequest++ -- Internal (used for testing)+ , encodeFrame+ , parseFrame+ ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString.Builder as B+import Control.Applicative (pure, (<$>))+import Control.Arrow (first)+import Control.Exception (throwIO)+import Control.Monad (forM, liftM, unless,+ when)+import Data.Binary.Get (Get, getInt64be,+ getLazyByteString,+ getWord16be, getWord8)+import Data.Binary.Put (putWord16be, runPut)+import Data.Bits ((.&.), (.|.))+import Data.ByteString (ByteString)+import qualified Data.ByteString.Base64 as B64+import Data.ByteString.Char8 ()+import qualified Data.ByteString.Lazy as BL+import Data.Digest.Pure.SHA (bytestringDigest, sha1)+import Data.IORef+import Data.Monoid (mappend, mconcat,+ mempty)+import Data.Tuple (swap)+import System.Entropy as R+import System.Random (RandomGen, newStdGen)+++--------------------------------------------------------------------------------+import Network.WebSockets.Connection.Options+import Network.WebSockets.Http+import Network.WebSockets.Hybi13.Demultiplex+import Network.WebSockets.Hybi13.Mask+import Network.WebSockets.Stream (Stream)+import qualified Network.WebSockets.Stream as Stream+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+headerVersions :: [ByteString]+headerVersions = ["13"]+++--------------------------------------------------------------------------------+finishRequest :: RequestHead+ -> Headers+ -> Either HandshakeException Response+finishRequest reqHttp headers = do+ !key <- getRequestHeader reqHttp "Sec-WebSocket-Key"+ let !hash = hashKey key+ !encoded = B64.encode hash+ return $ response101 (("Sec-WebSocket-Accept", encoded):headers) ""+++--------------------------------------------------------------------------------+finishResponse :: RequestHead+ -> ResponseHead+ -> Either HandshakeException Response+finishResponse request response = do+ -- Response message should be one of+ --+ -- - WebSocket Protocol Handshake+ -- - Switching Protocols+ --+ -- But we don't check it for now+ when (responseCode response == 400) $ Left $+ RequestRejected request response + when (responseCode response /= 101) $ Left $+ MalformedResponse response "Wrong response status or message."++ key <- getRequestHeader request "Sec-WebSocket-Key"+ responseHash <- getResponseHeader response "Sec-WebSocket-Accept"+ let challengeHash = B64.encode $ hashKey key+ when (responseHash /= challengeHash) $ Left $+ MalformedResponse response "Challenge and response hashes do not match."++ return $ Response response ""+++--------------------------------------------------------------------------------+encodeMessage :: RandomGen g => ConnectionType -> g -> Message -> (g, B.Builder)+encodeMessage conType gen msg = (gen', builder)+ where+ mkFrame = Frame True False False False+ (mask, gen') = case conType of+ ServerConnection -> (Nothing, gen)+ ClientConnection -> first Just (randomMask gen)+ builder = encodeFrame mask $ case msg of+ (ControlMessage (Close code pl)) -> mkFrame CloseFrame $+ runPut (putWord16be code) `mappend` pl+ (ControlMessage (Ping pl)) -> mkFrame PingFrame pl+ (ControlMessage (Pong pl)) -> mkFrame PongFrame pl+ (DataMessage rsv1 rsv2 rsv3 (Text pl _)) -> Frame True rsv1 rsv2 rsv3 TextFrame pl+ (DataMessage rsv1 rsv2 rsv3 (Binary pl)) -> Frame True rsv1 rsv2 rsv3 BinaryFrame pl+++--------------------------------------------------------------------------------+encodeMessages+ :: ConnectionType+ -> Stream+ -> IO ([Message] -> IO ())+encodeMessages conType stream = do+ genRef <- newIORef =<< newStdGen+ return $ \msgs -> do+ builders <- forM msgs $ \msg ->+ atomicModifyIORef' genRef $ \s -> encodeMessage conType s msg+ Stream.write stream (B.toLazyByteString $ mconcat builders)+++--------------------------------------------------------------------------------+encodeFrame :: Maybe Mask -> Frame -> B.Builder+encodeFrame mask f = B.word8 byte0 `mappend`+ B.word8 byte1 `mappend` len `mappend` maskbytes `mappend`+ B.lazyByteString (maskPayload mask payload)+ where++ byte0 = fin .|. rsv1 .|. rsv2 .|. rsv3 .|. opcode+ fin = if frameFin f then 0x80 else 0x00+ rsv1 = if frameRsv1 f then 0x40 else 0x00+ rsv2 = if frameRsv2 f then 0x20 else 0x00+ rsv3 = if frameRsv3 f then 0x10 else 0x00+ payload = case frameType f of+ ContinuationFrame -> framePayload f+ TextFrame -> framePayload f+ BinaryFrame -> framePayload f+ CloseFrame -> BL.take 125 $ framePayload f+ PingFrame -> BL.take 125 $ framePayload f+ PongFrame -> BL.take 125 $ framePayload f+ opcode = case frameType f of+ ContinuationFrame -> 0x00+ TextFrame -> 0x01+ BinaryFrame -> 0x02+ CloseFrame -> 0x08+ PingFrame -> 0x09+ PongFrame -> 0x0a+ (maskflag, maskbytes) = case mask of+ Nothing -> (0x00, mempty)+ Just m -> (0x80, encodeMask m)++ byte1 = maskflag .|. lenflag+ len' = BL.length payload+ (lenflag, len)+ | len' < 126 = (fromIntegral len', mempty)+ | len' < 0x10000 = (126, B.word16BE (fromIntegral len'))+ | otherwise = (127, B.word64BE (fromIntegral len'))+++--------------------------------------------------------------------------------+decodeMessages+ :: SizeLimit+ -> SizeLimit+ -> Stream+ -> IO (IO (Maybe Message))+decodeMessages frameLimit messageLimit stream = do+ dmRef <- newIORef emptyDemultiplexState+ return $ go dmRef+ where+ go dmRef = do+ mbFrame <- Stream.parseBin stream (parseFrame frameLimit)+ case mbFrame of+ Nothing -> return Nothing+ Just frame -> do+ demultiplexResult <- atomicModifyIORef' dmRef $+ \s -> swap $ demultiplex messageLimit s frame+ case demultiplexResult of+ DemultiplexError err -> throwIO err+ DemultiplexContinue -> go dmRef+ DemultiplexSuccess msg -> return (Just msg)+++--------------------------------------------------------------------------------+-- | Parse a frame+parseFrame :: SizeLimit -> Get Frame+parseFrame frameSizeLimit = do+ byte0 <- getWord8+ let fin = byte0 .&. 0x80 == 0x80+ rsv1 = byte0 .&. 0x40 == 0x40+ rsv2 = byte0 .&. 0x20 == 0x20+ rsv3 = byte0 .&. 0x10 == 0x10+ opcode = byte0 .&. 0x0f++ byte1 <- getWord8+ let mask = byte1 .&. 0x80 == 0x80+ lenflag = byte1 .&. 0x7f++ len <- case lenflag of+ 126 -> fromIntegral <$> getWord16be+ 127 -> getInt64be+ _ -> return (fromIntegral lenflag)++ -- Check size against limit.+ unless (atMostSizeLimit len frameSizeLimit) $+ fail $ "Frame of size " ++ show len ++ " exceeded limit"++ ft <- case opcode of+ 0x00 -> return ContinuationFrame+ 0x01 -> return TextFrame+ 0x02 -> return BinaryFrame+ 0x08 -> enforceControlFrameRestrictions len fin >> return CloseFrame+ 0x09 -> enforceControlFrameRestrictions len fin >> return PingFrame+ 0x0a -> enforceControlFrameRestrictions len fin >> return PongFrame+ _ -> fail $ "Unknown opcode: " ++ show opcode++ masker <- maskPayload <$> if mask then Just <$> parseMask else pure Nothing++ chunks <- getLazyByteString len++ return $ Frame fin rsv1 rsv2 rsv3 ft (masker chunks)++ where+ enforceControlFrameRestrictions len fin+ | not fin = fail "Control Frames must not be fragmented!"+ | len > 125 = fail "Control Frames must not carry payload > 125 bytes!"+ | otherwise = pure ()++--------------------------------------------------------------------------------+hashKey :: ByteString -> ByteString+hashKey key = unlazy $ bytestringDigest $ sha1 $ lazy $ key `mappend` guid+ where+ guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"+ lazy = BL.fromChunks . return+ unlazy = mconcat . BL.toChunks+++--------------------------------------------------------------------------------+createRequest :: ByteString+ -> ByteString+ -> Bool+ -> Headers+ -> IO RequestHead+createRequest hostname path secure customHeaders = do+ key <- B64.encode `liftM` getEntropy 16+ return $ RequestHead path (headers key ++ customHeaders) secure+ where+ headers key =+ [ ("Host" , hostname )+ , ("Connection" , "Upgrade" )+ , ("Upgrade" , "websocket" )+ , ("Sec-WebSocket-Key" , key )+ , ("Sec-WebSocket-Version" , versionNumber)+ ]++ versionNumber = head headerVersions
+ src/Network/WebSockets/Hybi13/Demultiplex.hs view
@@ -0,0 +1,157 @@+--------------------------------------------------------------------------------+-- | Demultiplexing of frames into messages+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Hybi13.Demultiplex+ ( FrameType (..)+ , Frame (..)+ , DemultiplexState+ , emptyDemultiplexState+ , DemultiplexResult (..)+ , demultiplex+ ) where+++--------------------------------------------------------------------------------+import Data.ByteString.Builder (Builder)+import qualified Data.ByteString.Builder as B+import Control.Exception (Exception)+import Data.Binary.Get (getWord16be, runGet)+import qualified Data.ByteString.Lazy as BL+import Data.Int (Int64)+import Data.Monoid (mappend)+import Data.Typeable (Typeable)+import Network.WebSockets.Connection.Options+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+-- | A low-level representation of a WebSocket packet+data Frame = Frame+ { frameFin :: !Bool+ , frameRsv1 :: !Bool+ , frameRsv2 :: !Bool+ , frameRsv3 :: !Bool+ , frameType :: !FrameType+ , framePayload :: !BL.ByteString+ } deriving (Eq, Show)+++--------------------------------------------------------------------------------+-- | The type of a frame. Not all types are allowed for all protocols.+data FrameType+ = ContinuationFrame+ | TextFrame+ | BinaryFrame+ | CloseFrame+ | PingFrame+ | PongFrame+ deriving (Eq, Show)+++--------------------------------------------------------------------------------+-- | Thrown if the client sends invalid multiplexed data+data DemultiplexException = DemultiplexException+ deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Exception DemultiplexException+++--------------------------------------------------------------------------------+-- | Internal state used by the demultiplexer+data DemultiplexState+ = EmptyDemultiplexState+ | DemultiplexState !Int64 !Builder !(Builder -> Message)+++--------------------------------------------------------------------------------+emptyDemultiplexState :: DemultiplexState+emptyDemultiplexState = EmptyDemultiplexState+++--------------------------------------------------------------------------------+-- | Result of demultiplexing+data DemultiplexResult+ = DemultiplexSuccess Message+ | DemultiplexError ConnectionException+ | DemultiplexContinue+++--------------------------------------------------------------------------------+demultiplex :: SizeLimit+ -> DemultiplexState+ -> Frame+ -> (DemultiplexResult, DemultiplexState)++demultiplex _ state (Frame True False False False PingFrame pl)+ | BL.length pl > 125 =+ (DemultiplexError $ CloseRequest 1002 "Protocol Error", emptyDemultiplexState)+ | otherwise =+ (DemultiplexSuccess $ ControlMessage (Ping pl), state)++demultiplex _ state (Frame True False False False PongFrame pl) =+ (DemultiplexSuccess (ControlMessage (Pong pl)), state)++demultiplex _ _ (Frame True False False False CloseFrame pl) =+ (DemultiplexSuccess (ControlMessage (uncurry Close parsedClose)), emptyDemultiplexState)+ where+ -- The Close frame MAY contain a body (the "Application data" portion of the+ -- frame) that indicates a reason for closing, such as an endpoint shutting+ -- down, an endpoint having received a frame too large, or an endpoint+ -- having received a frame that does not conform to the format expected by+ -- the endpoint. If there is a body, the first two bytes of the body MUST+ -- be a 2-byte unsigned integer (in network byte order) representing a+ -- status code with value /code/ defined in Section 7.4.+ parsedClose+ | BL.length pl >= 2 = case runGet getWord16be pl of+ a | a < 1000 || a `elem` [1004,1005,1006+ ,1014,1015,1016+ ,1100,2000,2999+ ,5000,65535] -> (1002, BL.empty)+ a -> (a, BL.drop 2 pl)+ | BL.length pl == 1 = (1002, BL.empty)+ | otherwise = (1000, BL.empty)++demultiplex sizeLimit EmptyDemultiplexState (Frame fin rsv1 rsv2 rsv3 tp pl) = case tp of+ _ | not (atMostSizeLimit size sizeLimit) ->+ ( DemultiplexError $ ParseException $+ "Message of size " ++ show size ++ " exceeded limit"+ , emptyDemultiplexState+ )++ TextFrame+ | fin ->+ (DemultiplexSuccess (text pl), emptyDemultiplexState)+ | otherwise ->+ (DemultiplexContinue, DemultiplexState size plb (text . B.toLazyByteString))+++ BinaryFrame+ | fin -> (DemultiplexSuccess (binary pl), emptyDemultiplexState)+ | otherwise -> (DemultiplexContinue, DemultiplexState size plb (binary . B.toLazyByteString))++ _ -> (DemultiplexError $ CloseRequest 1002 "Protocol Error", emptyDemultiplexState)++ where+ size = BL.length pl+ plb = B.lazyByteString pl+ text x = DataMessage rsv1 rsv2 rsv3 (Text x Nothing)+ binary x = DataMessage rsv1 rsv2 rsv3 (Binary x)++demultiplex sizeLimit (DemultiplexState size0 b f) (Frame fin False False False ContinuationFrame pl)+ | not (atMostSizeLimit size1 sizeLimit) =+ ( DemultiplexError $ ParseException $+ "Message of size " ++ show size1 ++ " exceeded limit"+ , emptyDemultiplexState+ )+ | fin = (DemultiplexSuccess (f b'), emptyDemultiplexState)+ | otherwise = (DemultiplexContinue, DemultiplexState size1 b' f)+ where+ size1 = size0 + BL.length pl+ b' = b `mappend` plb+ plb = B.lazyByteString pl++demultiplex _ _ _ =+ (DemultiplexError (CloseRequest 1002 "Protocol Error"), emptyDemultiplexState)
+ src/Network/WebSockets/Hybi13/Mask.hs view
@@ -0,0 +1,81 @@+--------------------------------------------------------------------------------+-- | Masking of fragmes using a simple XOR algorithm+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Network.WebSockets.Hybi13.Mask+ ( Mask+ , parseMask+ , encodeMask+ , randomMask++ , maskPayload+ ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Builder.Extra as Builder+import Data.Binary.Get (Get, getWord32host)+import qualified Data.ByteString.Internal as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Internal as BL+import Data.Word (Word32, Word8)+import Foreign.C.Types (CChar (..), CInt (..),+ CSize (..))+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Ptr (Ptr, plusPtr)+import System.Random (RandomGen, random)+++--------------------------------------------------------------------------------+foreign import ccall unsafe "_hs_mask_chunk" c_mask_chunk+ :: Word32 -> CInt -> Ptr CChar -> CSize -> Ptr Word8 -> IO ()+++--------------------------------------------------------------------------------+-- | A mask is sequence of 4 bytes. We store this in a 'Word32' in the host's+-- native byte ordering.+newtype Mask = Mask {unMask :: Word32}+++--------------------------------------------------------------------------------+-- | Parse a mask.+parseMask :: Get Mask+parseMask = fmap Mask getWord32host+++--------------------------------------------------------------------------------+-- | Encode a mask+encodeMask :: Mask -> Builder.Builder+encodeMask = Builder.word32Host . unMask+++--------------------------------------------------------------------------------+-- | Create a random mask+randomMask :: forall g. RandomGen g => g -> (Mask, g)+randomMask gen = (Mask int, gen')+ where+ (!int, !gen') = random gen :: (Word32, g)+++--------------------------------------------------------------------------------+-- | Mask a lazy bytestring. Uses 'c_mask_chunk' under the hood.+maskPayload :: Maybe Mask -> BL.ByteString -> BL.ByteString+maskPayload Nothing = id+maskPayload (Just (Mask 0)) = id+maskPayload (Just (Mask mask)) = go 0+ where+ go _ BL.Empty = BL.Empty+ go !maskOffset (BL.Chunk (B.PS payload off len) rest) =+ BL.Chunk maskedChunk (go ((maskOffset + len) `rem` 4) rest)+ where+ maskedChunk =+ B.unsafeCreate len $ \dst ->+ withForeignPtr payload $ \src ->+ c_mask_chunk mask+ (fromIntegral maskOffset)+ (src `plusPtr` off)+ (fromIntegral len)+ dst
− src/Network/WebSockets/Internal.hs
@@ -1,8 +0,0 @@--- | This module exports some extra functions. However, note that these--- utilities are primarily meant for internal use, and can change between minor--- releases.-module Network.WebSockets.Internal- ( S.iterSocket- ) where--import qualified Network.WebSockets.Socket as S
− src/Network/WebSockets/Monad.hs
@@ -1,257 +0,0 @@--- | Provides a simple, clean monad to write websocket servers in-{-# LANGUAGE BangPatterns, GeneralizedNewtypeDeriving, OverloadedStrings,- NoMonomorphismRestriction, Rank2Types, ScopedTypeVariables #-}-module Network.WebSockets.Monad- ( WebSocketsOptions (..)- , defaultWebSocketsOptions- , WebSockets (..)- , runWebSockets- , runWebSocketsWith- , runWebSocketsHandshake- , runWebSocketsWithHandshake- , runWebSocketsWith'- , receive- , sendBuilder- , send- , Sink- , sendSink- , getSink- , getOptions- , getProtocol- , getVersion- , throwWsError- , catchWsError- , spawnPingThread- ) where--import Control.Applicative (Applicative, (<$>))-import Control.Concurrent (forkIO, threadDelay)-import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar)-import Control.Exception (Exception (..), SomeException, throw)-import Control.Monad (forever)-import Control.Monad.Reader (ReaderT, ask, runReaderT)-import Control.Monad.Trans (MonadIO, lift, liftIO)-import Data.Foldable (forM_)--import Blaze.ByteString.Builder (Builder)-import Data.ByteString (ByteString)-import Data.Enumerator (Enumerator, Iteratee, ($$), (>>==), (=$))-import qualified Blaze.ByteString.Builder as BB-import qualified Data.Attoparsec as A-import qualified Data.ByteString.Lazy as BL-import qualified Data.Attoparsec.Enumerator as AE-import qualified Data.Enumerator as E-import qualified Data.Enumerator.List as EL--import Network.WebSockets.Handshake-import Network.WebSockets.Handshake.Http-import Network.WebSockets.Protocol-import Network.WebSockets.Types---- | Options for the WebSocket program-data WebSocketsOptions = WebSocketsOptions- { onPong :: IO ()- }---- | Default options-defaultWebSocketsOptions :: WebSocketsOptions-defaultWebSocketsOptions = WebSocketsOptions- { onPong = return ()- }---- | Environment in which the 'WebSockets' monad actually runs-data WebSocketsEnv p = WebSocketsEnv- { envOptions :: WebSocketsOptions- , envSendBuilder :: Builder -> IO ()- , envSink :: Sink p- , envProtocol :: p- }---- | Used for asynchronous sending.-newtype Sink p = Sink- { unSink :: MVar (E.Iteratee (Message p) IO ())- }---- | The monad in which you can write WebSocket-capable applications-newtype WebSockets p a = WebSockets- { unWebSockets :: ReaderT (WebSocketsEnv p) (Iteratee (Message p) IO) a- } deriving (Applicative, Functor, Monad, MonadIO)---- | Receives the initial client handshake, then behaves like 'runWebSockets'.-runWebSocketsHandshake :: Protocol p- => Bool- -> (Request -> WebSockets p a)- -> Iteratee ByteString IO ()- -> Iteratee ByteString IO a-runWebSocketsHandshake = runWebSocketsWithHandshake defaultWebSocketsOptions---- | Receives the initial client handshake, then behaves like--- 'runWebSocketsWith'.-runWebSocketsWithHandshake :: Protocol p- => WebSocketsOptions- -> Bool- -> (Request -> WebSockets p a)- -> Iteratee ByteString IO ()- -> Iteratee ByteString IO a-runWebSocketsWithHandshake opts isSecure goWs outIter = do- httpReq <- receiveIteratee $ decodeRequest isSecure- runWebSocketsWith opts httpReq goWs outIter---- | Run a 'WebSockets' application on an 'Enumerator'/'Iteratee' pair, given--- that you (read: your web server) has already received the HTTP part of the--- initial request. If not, you might want to use 'runWebSocketsWithHandshake'--- instead.------ If the handshake failed, throws a 'HandshakeError'. Otherwise, executes the--- supplied continuation. You should still send a response to the client--- yourself.-runWebSockets :: Protocol p- => RequestHttpPart- -> (Request -> WebSockets p a)- -> Iteratee ByteString IO ()- -> Iteratee ByteString IO a-runWebSockets = runWebSocketsWith defaultWebSocketsOptions---- | Version of 'runWebSockets' which allows you to specify custom options-runWebSocketsWith :: forall p a. Protocol p- => WebSocketsOptions- -> RequestHttpPart- -> (Request -> WebSockets p a)- -> Iteratee ByteString IO ()- -> Iteratee ByteString IO a-runWebSocketsWith opts httpReq goWs outIter = E.catchError ok $ \e -> do- -- If handshake went bad, send response- forM_ (fromException e) $ \he ->- let builder = encodeResponse $ responseError (undefined :: p) he- in liftIO $ makeBuilderSender outIter builder- -- Re-throw error- E.throwError e- where- -- Perform handshake, call runWebSocketsWith'- ok = do- (rq, p) <- handshake httpReq- runWebSocketsWith' opts p (goWs rq) outIter--runWebSocketsWith' :: Protocol p- => WebSocketsOptions- -> p- -> WebSockets p a- -> Iteratee ByteString IO ()- -> Iteratee ByteString IO a-runWebSocketsWith' opts proto ws outIter = do- -- Create sink with a random source- let sinkIter = encodeMessages proto =$ builderToByteString =$ outIter- sink <- Sink <$> liftIO (newMVar sinkIter)-- let sender = makeBuilderSender outIter- env = WebSocketsEnv opts sender sink proto- iter = runReaderT (unWebSockets ws) env-- decodeMessages proto =$ iter--makeBuilderSender :: MonadIO m => Iteratee ByteString m b -> Builder -> m ()-makeBuilderSender outIter x = do- ok <- E.run $ singleton x $$ builderToByteString $$ outIter- case ok of- Left err -> throw err- Right _ -> return ()---- | @spawnPingThread n@ spawns a thread which sends a ping every @n@ seconds--- (if the protocol supports it). To be called after having sent the response.-spawnPingThread :: BinaryProtocol p => Int -> WebSockets p ()-spawnPingThread i = do- sink <- getSink- _ <- liftIO $ forkIO $ forever $ do- -- An ugly hack here. We first sleep before sending the first- -- ping, so the ping (hopefully) doesn't interfere with the- -- intitial request/response.- threadDelay (i * 1000 * 1000) -- seconds- sendSink sink $ ping ("Hi" :: ByteString)- return ()---- | Receive arbitrary data.-receiveIteratee :: A.Parser a -> Iteratee ByteString IO a-receiveIteratee parser = do- eof <- E.isEOF- if eof- then E.throwError ConnectionClosed- else wrappingParseError . AE.iterParser $ parser---- | Execute an iteratee, wrapping attoparsec-enumeratee's ParseError into the--- ParseError constructor (which is a ConnectionError).-wrappingParseError :: (Monad m) => Iteratee a m b -> Iteratee a m b-wrappingParseError = flip E.catchError $ \e -> E.throwError $- maybe e (toException . ParseError) $ fromException e---- | Receive a message-receive :: Protocol p => WebSockets p (Message p)-receive = liftIteratee $ do- mmsg <- EL.head- case mmsg of- Nothing -> E.throwError ConnectionClosed- Just msg -> return msg---- | Send an arbitrary 'Builder'-sendBuilder :: Builder -> WebSockets p ()-sendBuilder builder = WebSockets $ do- sb <- envSendBuilder <$> ask- liftIO $ sb builder---- | Low-level sending with an arbitrary 'T.Message'-send :: Protocol p => Message p -> WebSockets p ()-send msg = getSink >>= \sink -> liftIO $ sendSink sink msg---- | Send a message to a sink. Might generate an exception if the underlying--- connection is closed.-sendSink :: Sink p -> Message p -> IO ()-sendSink sink msg = modifyMVar_ (unSink sink) $ \iter -> do- step <- E.runIteratee $ singleton msg $$ iter- case step of- E.Error err -> throw err- _ -> return $! E.returnI step---- | In case the user of the library wants to do asynchronous sending to the--- socket, he can extract a 'Sink' and pass this value around, for example,--- to other threads.-getSink :: Protocol p => WebSockets p (Sink p)-getSink = WebSockets $ envSink <$> ask--singleton :: Monad m => a -> Enumerator a m b-singleton c = E.checkContinue0 $ \_ f -> f (E.Chunks [c]) >>== E.returnI---- TODO: Figure out why Blaze.ByteString.Enumerator.builderToByteString doesn't--- work, then inform Simon or send a patch.-builderToByteString :: Monad m => E.Enumeratee Builder ByteString m a-builderToByteString = EL.concatMap $ BL.toChunks . BB.toLazyByteString---- | Get the current configuration-getOptions :: WebSockets p WebSocketsOptions-getOptions = WebSockets $ ask >>= return . envOptions---- | Get the underlying protocol-getProtocol :: WebSockets p p-getProtocol = WebSockets $ envProtocol <$> ask---- | Find out the 'WebSockets' version used at runtime-getVersion :: Protocol p => WebSockets p String-getVersion = version <$> getProtocol---- | Throw an iteratee error in the WebSockets monad-throwWsError :: (Exception e) => e -> WebSockets p a-throwWsError = liftIteratee . E.throwError---- | Catch an iteratee error in the WebSockets monad-catchWsError :: WebSockets p a- -> (SomeException -> WebSockets p a)- -> WebSockets p a-catchWsError act c = WebSockets $ do- env <- ask- let it = peelWebSockets env $ act- cit = peelWebSockets env . c- lift $ it `E.catchError` cit- where- peelWebSockets env = flip runReaderT env . unWebSockets---- | Lift an Iteratee computation to WebSockets-liftIteratee :: Iteratee (Message p) IO a -> WebSockets p a-liftIteratee = WebSockets . lift
src/Network/WebSockets/Protocol.hs view
@@ -1,81 +1,91 @@+-------------------------------------------------------------------------------- -- | Wrapper for supporting multiple protocol versions {-# LANGUAGE ExistentialQuantification #-} module Network.WebSockets.Protocol ( Protocol (..)- , TextProtocol- , BinaryProtocol- , close- , ping- , pong- , textData- , binaryData+ , defaultProtocol+ , protocols+ , compatible+ , headerVersions+ , finishRequest+ , finishResponse+ , encodeMessages+ , decodeMessages+ , createRequest ) where -import Blaze.ByteString.Builder (Builder)-import qualified Data.ByteString as B-import qualified Data.Enumerator as E -import Network.WebSockets.Types-import Network.WebSockets.Handshake.Http-import qualified Network.WebSockets.Protocol.Unsafe as Unsafe+--------------------------------------------------------------------------------+import Data.ByteString (ByteString)+import qualified Data.ByteString as B -class Protocol p where- -- | Unique identifier for us.- version :: p -> String - -- | Version accepted in the "Sec-WebSocket-Version " header. This is- -- usually not the same, or derivable from "version", e.g. for hybi10, it's- -- "7", "8" or "17".- headerVersions :: p -> [B.ByteString]+--------------------------------------------------------------------------------+import Network.WebSockets.Connection.Options+import Network.WebSockets.Http+import qualified Network.WebSockets.Hybi13 as Hybi13+import Network.WebSockets.Stream (Stream)+import Network.WebSockets.Types - -- | Determine if the protocol is compatible with a requested version. A- -- default implementation exists which uses the @headerVersions@ of the- -- protocol.- supported :: p -> RequestHttpPart -> Bool- supported p h = case getSecWebSocketVersion h of- Just v -> v `elem` headerVersions p- _ -> False - -- | Encodes messages to binary 'Builder's. Takes a random source so it is- -- able to do masking of frames (needed in some cases).- encodeMessages :: Monad m- => p- -> E.Enumeratee (Message p) Builder m a+--------------------------------------------------------------------------------+data Protocol+ = Hybi13+ deriving (Show) - -- | Decodes messages from binary 'B.ByteString's.- decodeMessages :: Monad m => p -> E.Enumeratee B.ByteString (Message p) m a - -- | Parse and validate the rest of the request. For hybi10, this is just- -- validation, but hybi00 also needs to fetch a "security token"- --- -- In case of failure, this function may throw a 'HandshakeError'.- -- be amended with the RequestHttpPart for the user)- finishRequest :: Monad m- => p -> RequestHttpPart- -> E.Iteratee B.ByteString m Request+--------------------------------------------------------------------------------+defaultProtocol :: Protocol+defaultProtocol = Hybi13 - -- | Implementations of the specification- implementations :: [p] -class Protocol p => TextProtocol p-class TextProtocol p => BinaryProtocol p+--------------------------------------------------------------------------------+protocols :: [Protocol]+protocols = [Hybi13] --- | Construct a close message-close :: (TextProtocol p, WebSocketsData a) => a -> Message p-close = Unsafe.close --- | Construct a ping message-ping :: (BinaryProtocol p, WebSocketsData a) => a -> Message p-ping = Unsafe.ping+--------------------------------------------------------------------------------+headerVersions :: Protocol -> [ByteString]+headerVersions Hybi13 = Hybi13.headerVersions --- | Construct a pong message-pong :: (BinaryProtocol p, WebSocketsData a) => a -> Message p-pong = Unsafe.pong --- | Construct a text message-textData :: (TextProtocol p, WebSocketsData a) => a -> Message p-textData = Unsafe.textData+--------------------------------------------------------------------------------+compatible :: Protocol -> RequestHead -> Bool+compatible protocol req = case getRequestSecWebSocketVersion req of+ Just v -> v `elem` headerVersions protocol+ _ -> True -- Whatever? --- | Construct a binary message-binaryData :: (BinaryProtocol p, WebSocketsData a) => a -> Message p-binaryData = Unsafe.binaryData++--------------------------------------------------------------------------------+finishRequest+ :: Protocol -> RequestHead -> Headers -> Either HandshakeException Response+finishRequest Hybi13 = Hybi13.finishRequest+++--------------------------------------------------------------------------------+finishResponse+ :: Protocol -> RequestHead -> ResponseHead+ -> Either HandshakeException Response+finishResponse Hybi13 = Hybi13.finishResponse+++--------------------------------------------------------------------------------+encodeMessages+ :: Protocol -> ConnectionType -> Stream+ -> IO ([Message] -> IO ())+encodeMessages Hybi13 = Hybi13.encodeMessages+++--------------------------------------------------------------------------------+decodeMessages+ :: Protocol -> SizeLimit -> SizeLimit -> Stream+ -> IO (IO (Maybe Message))+decodeMessages Hybi13 frameLimit messageLimit =+ Hybi13.decodeMessages frameLimit messageLimit+++--------------------------------------------------------------------------------+createRequest+ :: Protocol -> B.ByteString -> B.ByteString -> Bool -> Headers+ -> IO RequestHead+createRequest Hybi13 = Hybi13.createRequest
− src/Network/WebSockets/Protocol/Hybi00.hs
@@ -1,25 +0,0 @@-{-# LANGUAGE ExistentialQuantification, OverloadedStrings #-}-module Network.WebSockets.Protocol.Hybi00- ( Hybi00_ (..)- , Hybi00- ) where--import Data.Enumerator ((=$))-import Data.Enumerator.List as EL-import Network.WebSockets.Protocol-import Network.WebSockets.Protocol.Hybi00.Internal-import Network.WebSockets.Protocol.Hybi10.Internal-import Network.WebSockets.Protocol.Unsafe--data Hybi00 = forall p. Protocol p => Hybi00 p--instance Protocol Hybi00 where- version (Hybi00 p) = version p- headerVersions (Hybi00 p) = headerVersions p- supported (Hybi00 p) h = supported p h- encodeMessages (Hybi00 p) = (EL.map castMessage =$) . encodeMessages p- decodeMessages (Hybi00 p) = (decodeMessages p =$) . EL.map castMessage- finishRequest (Hybi00 p) = finishRequest p- implementations = [Hybi00 Hybi10_, Hybi00 Hybi00_]--instance TextProtocol Hybi00
− src/Network/WebSockets/Protocol/Hybi00/Internal.hs
@@ -1,102 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}-module Network.WebSockets.Protocol.Hybi00.Internal- ( Hybi00_ (..)- ) where--import Control.Applicative ((<|>))-import Data.Char (isDigit)--import Data.Binary (encode)-import Data.ByteString.Lazy.Char8 ()-import Data.Digest.Pure.MD5 (md5)-import Data.Int (Int32)-import qualified Blaze.ByteString.Builder as BB-import qualified Data.Attoparsec as A-import qualified Data.Attoparsec.Enumerator as A-import qualified Data.ByteString as B-import qualified Data.Enumerator as E-import qualified Data.Enumerator.List as EL-import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL-import qualified Data.CaseInsensitive as CI--import Network.WebSockets.Handshake.Http-import Network.WebSockets.Protocol-import Network.WebSockets.Types--data Hybi00_ = Hybi00_--instance Protocol Hybi00_ where- version Hybi00_ = "hybi00"- headerVersions Hybi00_ = [] -- The client will elide it- supported Hybi00_ h = getSecWebSocketVersion h == Nothing- encodeMessages Hybi00_ = EL.map encodeMessage- decodeMessages Hybi00_ = E.sequence (A.iterParser parseMessage)- finishRequest Hybi00_ = handshakeHybi00- implementations = [Hybi00_]--instance TextProtocol Hybi00_--encodeMessage :: Message p -> BB.Builder-encodeMessage (DataMessage (Text pl)) =- BB.fromLazyByteString $ "\0" `BL.append` pl `BL.append` "\255"-encodeMessage (ControlMessage (Close _)) =- BB.fromLazyByteString "\255\0"-encodeMessage msg = error $- "Network.WebSockets.Protocol.Hybi00.encodeFrame: unsupported message: " ++- show msg--parseMessage :: A.Parser (Message p)-parseMessage = parseText <|> parseClose- where- parseText = do- _ <- A.word8 0x00- utf8string <- A.manyTill A.anyWord8 (A.try $ A.word8 0xff)- return $ DataMessage $ Text $ BL.pack utf8string-- parseClose = do- _ <- A.word8 0xff- _ <- A.word8 0x00- return $ ControlMessage $ Close ""--divBySpaces :: String -> Maybe Int32-divBySpaces str- | spaces == 0 = Nothing- | otherwise = Just . fromIntegral $ number `div` spaces- where- number = read $ filter isDigit str :: Integer- spaces = fromIntegral . length $ filter (== ' ') str--handshakeHybi00 :: Monad m- => RequestHttpPart- -> E.Iteratee B.ByteString m Request-handshakeHybi00 reqHttp@(RequestHttpPart path h isSecure) = do- keyPart3 <- A.iterParser $ A.take 8- keyPart1 <- numberFromToken =<< getHeader "Sec-WebSocket-Key1"- keyPart2 <- numberFromToken =<< getHeader "Sec-WebSocket-Key2"-- let key = B.concat . BL.toChunks . encode . md5 $ BL.concat- [keyPart1, keyPart2, BL.fromChunks [keyPart3]]-- host <- getHeader "Host"- -- todo: origin right? (also applies to hybi10)- origin <- getHeader "Origin"- let schema = if isSecure then "wss://" else "ws://"- let response = response101- [ ("Sec-WebSocket-Location", B.concat [schema, host, path])- , ("Sec-WebSocket-Origin", origin)- ]- key-- return $ Request path h response- where- getHeader k = case lookup k h of- Just t -> return t- Nothing -> E.throwError $ MalformedRequest reqHttp $- "Header missing: " ++ BC.unpack (CI.original k)-- numberFromToken token = case divBySpaces (BC.unpack token) of- Just n -> return $ encode n- Nothing -> E.throwError $ MalformedRequest reqHttp- "Security token does not contain enough spaces"
− src/Network/WebSockets/Protocol/Hybi10.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-module Network.WebSockets.Protocol.Hybi10- ( Hybi10- ) where--import Data.Enumerator ((=$))-import Data.Enumerator.List as EL-import Network.WebSockets.Protocol-import Network.WebSockets.Protocol.Hybi10.Internal-import Network.WebSockets.Protocol.Unsafe--data Hybi10 = forall p. Protocol p => Hybi10 p--instance Protocol Hybi10 where- version (Hybi10 p) = version p- headerVersions (Hybi10 p) = headerVersions p- supported (Hybi10 p) h = supported p h- encodeMessages (Hybi10 p) = (EL.map castMessage =$) . encodeMessages p- decodeMessages (Hybi10 p) = (decodeMessages p =$) . EL.map castMessage- finishRequest (Hybi10 p) = finishRequest p- implementations = [Hybi10 Hybi10_]--instance TextProtocol Hybi10-instance BinaryProtocol Hybi10
− src/Network/WebSockets/Protocol/Hybi10/Demultiplex.hs
@@ -1,77 +0,0 @@--- | Demultiplexing of frames into messages-module Network.WebSockets.Protocol.Hybi10.Demultiplex- ( FrameType (..)- , Frame (..)- , DemultiplexState- , emptyDemultiplexState- , demultiplex- ) where--import Blaze.ByteString.Builder (Builder)-import Data.Monoid (mappend)-import qualified Blaze.ByteString.Builder as B-import qualified Data.ByteString.Lazy as BL--import Network.WebSockets.Types---- | A low-level representation of a WebSocket packet-data Frame = Frame- { frameFin :: !Bool- , frameRsv1 :: !Bool- , frameRsv2 :: !Bool- , frameRsv3 :: !Bool- , frameType :: !FrameType- , framePayload :: !BL.ByteString- } deriving (Eq, Show)---- | The type of a frame. Not all types are allowed for all protocols.-data FrameType- = ContinuationFrame- | TextFrame- | BinaryFrame- | CloseFrame- | PingFrame- | PongFrame- deriving (Eq, Show)---- | Internal state used by the demultiplexer-newtype DemultiplexState = DemultiplexState- { unDemultiplexState :: Maybe (FrameType, Builder)- }--emptyDemultiplexState :: DemultiplexState-emptyDemultiplexState = DemultiplexState Nothing--demultiplex :: DemultiplexState- -> Frame- -> (Maybe (Message p), DemultiplexState)-demultiplex state (Frame fin _ _ _ tp pl) = case tp of- -- Return control messages immediately, they have no influence on the state- CloseFrame -> (Just (ControlMessage (Close pl)), state)- PingFrame -> (Just (ControlMessage (Ping pl)), state)- PongFrame -> (Just (ControlMessage (Pong pl)), state)- -- If we're dealing with a continuation...- ContinuationFrame -> case unDemultiplexState state of- -- We received a continuation but we don't have any state. Let's ignore- -- this fragment...- Nothing -> (Nothing, DemultiplexState Nothing)- -- Append the payload to the state- -- TODO: protect against overflows- Just (amt, b)- | not fin -> (Nothing, DemultiplexState (Just (amt, b')))- | otherwise -> case amt of- TextFrame -> (Just (DataMessage (Text m)), e)- BinaryFrame -> (Just (DataMessage (Binary m)), e)- _ -> error "Demultiplex.demultiplex: Internal error"- where- b' = b `mappend` plb- m = B.toLazyByteString b'- TextFrame- | fin -> (Just (DataMessage (Text pl)), e)- | otherwise -> (Nothing, DemultiplexState (Just (TextFrame, plb)))- BinaryFrame- | fin -> (Just (DataMessage (Binary pl)), e)- | otherwise -> (Nothing, DemultiplexState (Just (BinaryFrame, plb)))- where- e = emptyDemultiplexState- plb = B.fromLazyByteString pl
− src/Network/WebSockets/Protocol/Hybi10/Internal.hs
@@ -1,156 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Network.WebSockets.Protocol.Hybi10.Internal- ( Hybi10_ (..)- , encodeFrameHybi10- ) where--import Control.Applicative (pure, (<$>))-import Data.Bits ((.&.), (.|.))-import Data.Maybe (maybeToList)-import Data.Monoid (mempty, mappend, mconcat)--import Data.Attoparsec (anyWord8)-import Data.Binary.Get (runGet, getWord16be, getWord64be)-import Data.ByteString (ByteString)-import Data.ByteString.Char8 ()-import Data.Digest.Pure.SHA (bytestringDigest, sha1)-import Data.Int (Int64)-import Data.Enumerator ((=$))-import qualified Blaze.ByteString.Builder as B-import qualified Data.Attoparsec as A-import qualified Data.Attoparsec.Enumerator as A-import qualified Data.ByteString.Base64 as B64-import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL-import qualified Data.CaseInsensitive as CI-import qualified Data.Enumerator as E-import qualified Data.Enumerator.List as EL--import Network.WebSockets.Handshake.Http-import Network.WebSockets.Protocol-import Network.WebSockets.Protocol.Hybi10.Demultiplex-import Network.WebSockets.Protocol.Hybi10.Mask-import Network.WebSockets.Types--data Hybi10_ = Hybi10_--instance Protocol Hybi10_ where- version Hybi10_ = "hybi10"- headerVersions Hybi10_ = ["13", "8", "7"]- encodeMessages Hybi10_ = EL.map encodeMessageHybi10- decodeMessages Hybi10_ = decodeMessagesHybi10- finishRequest Hybi10_ = handshakeHybi10- implementations = [Hybi10_]--instance TextProtocol Hybi10_-instance BinaryProtocol Hybi10_--encodeMessageHybi10 :: Message p -> B.Builder-encodeMessageHybi10 msg = builder- where- mkFrame = Frame True False False False- builder = encodeFrameHybi10 $ case msg of- (ControlMessage (Close pl)) -> mkFrame CloseFrame pl- (ControlMessage (Ping pl)) -> mkFrame PingFrame pl- (ControlMessage (Pong pl)) -> mkFrame PongFrame pl- (DataMessage (Text pl)) -> mkFrame TextFrame pl- (DataMessage (Binary pl)) -> mkFrame BinaryFrame pl---- | Encode a frame-encodeFrameHybi10 :: Frame -> B.Builder-encodeFrameHybi10 f = B.fromWord8 byte0 `mappend`- B.fromWord8 byte1 `mappend` len `mappend`- B.fromLazyByteString (framePayload f)- where- byte0 = fin .|. rsv1 .|. rsv2 .|. rsv3 .|. opcode- fin = if frameFin f then 0x80 else 0x00- rsv1 = if frameRsv1 f then 0x40 else 0x00- rsv2 = if frameRsv2 f then 0x20 else 0x00- rsv3 = if frameRsv3 f then 0x10 else 0x00- opcode = case frameType f of- ContinuationFrame -> 0x00- TextFrame -> 0x01- BinaryFrame -> 0x02- CloseFrame -> 0x08- PingFrame -> 0x09- PongFrame -> 0x0a-- byte1 = lenflag- len' = BL.length (framePayload f)- (lenflag, len)- | len' < 126 = (fromIntegral len', mempty)- | len' < 0x10000 = (126, B.fromWord16be (fromIntegral len'))- | otherwise = (127, B.fromWord64be (fromIntegral len'))--decodeMessagesHybi10 :: Monad m => E.Enumeratee ByteString (Message p) m a-decodeMessagesHybi10 =- (E.sequence (A.iterParser parseFrame) =$) . demultiplexEnum--demultiplexEnum :: Monad m => E.Enumeratee Frame (Message p) m a-demultiplexEnum = EL.concatMapAccum step emptyDemultiplexState- where- step s f = let (m, s') = demultiplex s f in (s', maybeToList m)---- | Parse a frame-parseFrame :: A.Parser Frame-parseFrame = do- byte0 <- anyWord8- let fin = byte0 .&. 0x80 == 0x80- rsv1 = byte0 .&. 0x40 == 0x40- rsv2 = byte0 .&. 0x20 == 0x20- rsv3 = byte0 .&. 0x10 == 0x10- opcode = byte0 .&. 0x0f-- let ft = case opcode of- 0x00 -> ContinuationFrame- 0x01 -> TextFrame- 0x02 -> BinaryFrame- 0x08 -> CloseFrame- 0x09 -> PingFrame- 0x0a -> PongFrame- _ -> error "Unknown opcode"-- byte1 <- anyWord8- let mask = byte1 .&. 0x80 == 0x80- lenflag = fromIntegral (byte1 .&. 0x7f)-- len <- case lenflag of- 126 -> fromIntegral . runGet' getWord16be <$> A.take 2- 127 -> fromIntegral . runGet' getWord64be <$> A.take 8- _ -> return lenflag-- masker <- maskPayload <$> if mask then Just <$> A.take 4 else pure Nothing-- chunks <- take64 len-- return $ Frame fin rsv1 rsv2 rsv3 ft (masker $ BL.fromChunks chunks)- where- runGet' g = runGet g . BL.fromChunks . return-- take64 :: Int64 -> A.Parser [ByteString]- take64 n- | n <= 0 = return []- | otherwise = do- let n' = min intMax n- chunk <- A.take (fromIntegral n')- (chunk :) <$> take64 (n - n')- where- intMax :: Int64- intMax = fromIntegral (maxBound :: Int)--handshakeHybi10 :: Monad m- => RequestHttpPart- -> E.Iteratee ByteString m Request-handshakeHybi10 reqHttp@(RequestHttpPart path h _) = do- key <- getHeader "Sec-WebSocket-Key"- let hash = unlazy $ bytestringDigest $ sha1 $ lazy $ key `mappend` guid- let encoded = B64.encode hash- return $ Request path h $ response101 [("Sec-WebSocket-Accept", encoded)] ""- where- guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"- lazy = BL.fromChunks . return- unlazy = mconcat . BL.toChunks- getHeader k = case lookup k h of- Just t -> return t- Nothing -> E.throwError $ MalformedRequest reqHttp $ - "Header missing: " ++ BC.unpack (CI.original k)
− src/Network/WebSockets/Protocol/Hybi10/Mask.hs
@@ -1,24 +0,0 @@--- | Masking of fragmes using a simple XOR algorithm-{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}-module Network.WebSockets.Protocol.Hybi10.Mask- ( Mask- , maskPayload- ) where--import Data.Bits (xor)--import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL---- | ByteString should be exactly 4 bytes long-type Mask = Maybe B.ByteString---- | Apply mask-maskPayload :: Mask -> BL.ByteString -> BL.ByteString-maskPayload Nothing = id-maskPayload (Just mask) = snd . BL.mapAccumL f 0- where- len = B.length mask- f !i !c = let i' = (i + 1) `mod` len- m = mask `B.index` i- in (i', m `xor` c)
− src/Network/WebSockets/Protocol/Unsafe.hs
@@ -1,35 +0,0 @@-module Network.WebSockets.Protocol.Unsafe- ( castMessage- , close- , ping- , pong- , textData- , binaryData- ) where--import Network.WebSockets.Types--castMessage :: Message p1 -> Message p2-castMessage (ControlMessage m) = ControlMessage $ case m of- Close b -> Close b- Ping b -> Ping b- Pong b -> Pong b-castMessage (DataMessage m) = DataMessage $ case m of- Text b -> Text b- Binary b -> Binary b-{-# INLINE castMessage #-}--close :: WebSocketsData a => a -> Message p-close = ControlMessage . Close . toLazyByteString--ping :: WebSocketsData a => a -> Message p-ping = ControlMessage . Ping . toLazyByteString--pong :: WebSocketsData a => a -> Message p-pong = ControlMessage . Pong . toLazyByteString--textData :: WebSocketsData a => a -> Message p-textData = DataMessage . Text . toLazyByteString--binaryData :: WebSocketsData a => a -> Message p-binaryData = DataMessage . Binary . toLazyByteString
+ src/Network/WebSockets/Server.hs view
@@ -0,0 +1,174 @@+--------------------------------------------------------------------------------+-- | This provides a simple stand-alone server for 'WebSockets' applications.+-- Note that in production you want to use a real webserver such as snap or+-- warp.+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Server+ ( ServerApp+ , runServer+ , ServerOptions (..)+ , defaultServerOptions+ , runServerWithOptions+ , runServerWith+ , makeListenSocket+ , makePendingConnection+ , makePendingConnectionFromStream++ , PongTimeout+ ) where+++--------------------------------------------------------------------------------+import qualified Control.Concurrent.Async as Async+import Control.Exception (bracket,+ bracketOnError, finally, mask_,+ throwIO)+import Network.Socket (Socket)+import qualified Network.Socket as S+++--------------------------------------------------------------------------------+import Network.WebSockets.Connection+import Network.WebSockets.Connection.PingPong (PongTimeout(..))+import Network.WebSockets.Http+import qualified Network.WebSockets.Stream as Stream+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+-- | WebSockets application that can be ran by a server. Once this 'IO' action+-- finishes, the underlying socket is closed automatically.+type ServerApp = PendingConnection -> IO ()+++--------------------------------------------------------------------------------+-- | Provides a simple server. This function blocks forever. Note that this+-- is merely provided for quick-and-dirty or internal applications, but for real+-- applications, you should use a real server.+--+-- For example:+--+-- * Performance is reasonable under load, but:+-- * No protection against DoS attacks is provided.+-- * No logging is performed.+-- * ...+--+-- Glue for using this package with real servers is provided by:+--+-- * <https://hackage.haskell.org/package/wai-websockets>+--+-- * <https://hackage.haskell.org/package/websockets-snap>+runServer :: String -- ^ Address to bind+ -> Int -- ^ Port to listen on+ -> ServerApp -- ^ Application+ -> IO () -- ^ Never returns+runServer host port app = runServerWith host port defaultConnectionOptions app+++--------------------------------------------------------------------------------+-- | A version of 'runServer' which allows you to customize some options.+runServerWith :: String -> Int -> ConnectionOptions -> ServerApp -> IO ()+runServerWith host port opts = runServerWithOptions defaultServerOptions+ { serverHost = host+ , serverPort = port+ , serverConnectionOptions = opts+ }+{-# DEPRECATED runServerWith "Use 'runServerWithOptions' instead" #-}+++--------------------------------------------------------------------------------+data ServerOptions = ServerOptions+ { serverHost :: String+ , serverPort :: Int+ , serverConnectionOptions :: ConnectionOptions+ }+++--------------------------------------------------------------------------------+defaultServerOptions :: ServerOptions+defaultServerOptions = ServerOptions+ { serverHost = "127.0.0.1"+ , serverPort = 8080+ , serverConnectionOptions = defaultConnectionOptions+ }+++--------------------------------------------------------------------------------+-- | Customizable version of 'runServer'. Never returns until killed.+--+-- Please use the 'defaultServerOptions' combined with record updates to set the+-- fields you want. This way your code is unlikely to break on future changes.+runServerWithOptions :: ServerOptions -> ServerApp -> IO a+runServerWithOptions opts app = S.withSocketsDo $+ bracket+ (makeListenSocket (serverHost opts) (serverPort opts))+ S.close+ (\sock ->+ let+ mainThread = do+ (conn, _) <- S.accept sock+ Async.withAsyncWithUnmask+ (\unmask -> unmask (runApp conn (serverConnectionOptions opts) app) `finally` S.close conn)+ (\_ -> mainThread)+ in mask_ mainThread+ )+++--------------------------------------------------------------------------------+-- | Create a standardized socket on which you can listen for incomming+-- connections. Should only be used for a quick and dirty solution! Should be+-- preceded by the call 'Network.Socket.withSocketsDo'.+makeListenSocket :: String -> Int -> IO Socket+makeListenSocket host port = do+ addr:_ <- S.getAddrInfo (Just hints) (Just host) (Just (show port))+ bracketOnError+ (S.socket (S.addrFamily addr) S.Stream S.defaultProtocol)+ S.close+ (\sock -> do+ _ <- S.setSocketOption sock S.ReuseAddr 1+ _ <- S.setSocketOption sock S.NoDelay 1+ S.bind sock (S.addrAddress addr)+ S.listen sock 5+ return sock+ )+ where+ hints = S.defaultHints { S.addrSocketType = S.Stream }+++--------------------------------------------------------------------------------+runApp :: Socket+ -> ConnectionOptions+ -> ServerApp+ -> IO ()+runApp socket opts app =+ bracket+ (makePendingConnection socket opts)+ (Stream.close . pendingStream)+ app+++--------------------------------------------------------------------------------+-- | Turns a socket, connected to some client, into a 'PendingConnection'. The+-- 'PendingConnection' should be closed using 'pendingStream' and 'Stream.close' later.+makePendingConnection+ :: Socket -> ConnectionOptions -> IO PendingConnection+makePendingConnection socket opts = do+ stream <- Stream.makeSocketStream socket+ makePendingConnectionFromStream stream opts+++-- | More general version of 'makePendingConnection' for 'Stream.Stream'+-- instead of a 'Socket'.+makePendingConnectionFromStream+ :: Stream.Stream -> ConnectionOptions -> IO PendingConnection+makePendingConnectionFromStream stream opts = do+ -- TODO: we probably want to send a 40x if the request is bad?+ mbRequest <- Stream.parse stream (decodeRequestHead False)+ case mbRequest of+ Nothing -> throwIO ConnectionClosed+ Just request -> return PendingConnection+ { pendingOptions = opts+ , pendingRequest = request+ , pendingOnAccept = \_ -> return ()+ , pendingStream = stream+ }
− src/Network/WebSockets/Socket.hs
@@ -1,78 +0,0 @@--- | Simple module which provides the low-level socket handling in case you want--- to write a stand-alone 'WebSockets' application.-{-# LANGUAGE OverloadedStrings #-}-module Network.WebSockets.Socket- ( runServer- , runWithSocket- , iterSocket- ) where--import Prelude hiding (catch)--import Control.Concurrent (forkIO)-import Control.Exception (SomeException, catch)-import Control.Monad (forever)-import Control.Monad.Trans (liftIO)--import Data.ByteString (ByteString)-import Data.Enumerator (Iteratee, ($$))-import Network.Socket (Socket)-import qualified Data.Enumerator as E-import qualified Network.Socket as S-import qualified Network.Socket.ByteString as SB-import qualified Network.Socket.Enumerator as SE--import Network.WebSockets.Handshake.Http-import Network.WebSockets.Monad-import Network.WebSockets.Protocol-import Network.WebSockets.Types---- | Provides a simple server. This function blocks forever. Note that this--- is merely provided for quick-and-dirty standalone applications, for real--- applications, you should use a real server.-runServer :: Protocol p- => String -- ^ Address to bind to- -> Int -- ^ Port to listen on- -> (Request -> WebSockets p ()) -- ^ Application to serve- -> IO () -- ^ Never returns-runServer host port ws = S.withSocketsDo $ do- sock <- S.socket S.AF_INET S.Stream S.defaultProtocol- _ <- S.setSocketOption sock S.ReuseAddr 1- host' <- S.inet_addr host- S.bindSocket sock (S.SockAddrInet (fromIntegral port) host')- S.listen sock 5- flip catch (closeSock sock) $ forever $ do- (conn, _) <- S.accept sock- -- Voodoo fix: set this to True as soon as we notice the connection was- -- closed. Will prevent iterSocket' from even trying to send anything.- -- Without it, we got many "Couldn't decode text frame as UTF8" errors- -- in the browser (although the payload is definitely UTF8).- -- killRef <- newIORef False- _ <- forkIO $ runWithSocket conn ws >> return ()- return ()- where- closeSock :: Socket -> SomeException -> IO ()- closeSock sock _ = S.sClose sock---- | This function wraps 'runWebSockets' in order to provide a simple API for--- stand-alone servers.-runWithSocket :: Protocol p- => Socket -> (Request -> WebSockets p a) -> IO a-runWithSocket s ws = do- r <- E.run $ SE.enumSocket 4096 s $$ runWebSocketsWithHandshake- defaultWebSocketsOptions False ws (iterSocket s)- S.sClose s- either (error . show) return r---- | Create an iterator which writes to a socket. Throws a 'ConnectionClosed'--- exception if the user attempts to write to a closed socket.-iterSocket :: Socket -> Iteratee ByteString IO ()-iterSocket s = E.continue go- where- go (E.Chunks []) = E.continue go- go (E.Chunks cs) = do- b <- liftIO $ S.sIsWritable s- if b- then E.tryIO (SB.sendMany s cs) >> E.continue go- else E.throwError ConnectionClosed- go E.EOF = E.continue go
+ src/Network/WebSockets/Stream.hs view
@@ -0,0 +1,218 @@+--------------------------------------------------------------------------------+-- | Lightweight abstraction over an input/output stream.+{-# LANGUAGE CPP #-}+module Network.WebSockets.Stream+ ( Stream+ , makeStream+ , makeSocketStream+ , makeEchoStream+ , parse+ , parseBin+ , write+ , close+ ) where++import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar,+ putMVar, takeMVar, withMVar)+import Control.Exception (SomeException, SomeAsyncException, throwIO, catch, try, fromException)+import Control.Monad (forM_)+import qualified Data.Attoparsec.ByteString as Atto+import qualified Data.Binary.Get as BIN+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.IORef (IORef, atomicModifyIORef',+ newIORef, readIORef,+ writeIORef)+import qualified Network.Socket as S+import qualified Network.Socket.ByteString as SB (recv)++#if !defined(mingw32_HOST_OS)+import qualified Network.Socket.ByteString.Lazy as SBL (sendAll)+#else+import qualified Network.Socket.ByteString as SB (sendAll)+#endif+import System.IO.Error (isResourceVanishedError)++import Network.WebSockets.Types+++--------------------------------------------------------------------------------+-- | State of the stream+data StreamState+ = Closed !B.ByteString -- Remainder+ | Open !B.ByteString -- Buffer+++--------------------------------------------------------------------------------+-- | Lightweight abstraction over an input/output stream.+data Stream = Stream+ { streamIn :: IO (Maybe B.ByteString)+ , streamOut :: (Maybe BL.ByteString -> IO ())+ , streamState :: !(IORef StreamState)+ }+++--------------------------------------------------------------------------------+-- | Create a stream from a "receive" and "send" action. The following+-- properties apply:+--+-- - Regardless of the provided "receive" and "send" functions, reading and+-- writing from the stream will be thread-safe, i.e. this function will create+-- a receive and write lock to be used internally.+--+-- - Reading from or writing to a closed 'Stream' will always throw an+-- exception, even if the underlying "receive" and "send" functions do not+-- (we do the bookkeeping).+--+-- - Streams should always be closed.+makeStream+ :: IO (Maybe B.ByteString) -- ^ Reading+ -> (Maybe BL.ByteString -> IO ()) -- ^ Writing+ -> IO Stream -- ^ Resulting stream+makeStream receive send = do+ ref <- newIORef (Open B.empty)+ receiveLock <- newMVar ()+ sendLock <- newMVar ()+ return $ Stream (receive' ref receiveLock) (send' ref sendLock) ref+ where+ closeRef :: IORef StreamState -> IO ()+ closeRef ref = atomicModifyIORef' ref $ \state -> case state of+ Open buf -> (Closed buf, ())+ Closed buf -> (Closed buf, ())++ -- Throw a 'ConnectionClosed' is the connection is not 'Open'.+ assertOpen :: IORef StreamState -> IO ()+ assertOpen ref = do+ state <- readIORef ref+ case state of+ Closed _ -> throwIO ConnectionClosed+ Open _ -> return ()++ receive' :: IORef StreamState -> MVar () -> IO (Maybe B.ByteString)+ receive' ref lock = withMVar lock $ \() -> do+ assertOpen ref+ mbBs <- onSyncException receive (closeRef ref)+ case mbBs of+ Nothing -> closeRef ref >> return Nothing+ Just bs -> return (Just bs)++ send' :: IORef StreamState -> MVar () -> (Maybe BL.ByteString -> IO ())+ send' ref lock mbBs = withMVar lock $ \() -> do+ case mbBs of+ Nothing -> closeRef ref+ Just _ -> assertOpen ref+ onSyncException (send mbBs) (closeRef ref)++ onSyncException :: IO a -> IO b -> IO a+ onSyncException io what =+ catch io $ \e -> do+ case fromException (e :: SomeException) :: Maybe SomeAsyncException of+ Just _ -> pure ()+ Nothing -> what *> pure ()+ throwIO e+++--------------------------------------------------------------------------------+makeSocketStream :: S.Socket -> IO Stream+makeSocketStream socket = makeStream receive send+ where+ receive = do+ bs <- try $ SB.recv socket 8192+ case bs of+ -- If the resource vanished, the socket was closed+ Left e | isResourceVanishedError e -> return Nothing+ | otherwise -> throwIO e+ Right bs' | B.null bs' -> return Nothing+ | otherwise -> return $ Just bs'++ send Nothing = return ()+ send (Just bs) = do+#if !defined(mingw32_HOST_OS)+ SBL.sendAll socket bs+#else+ forM_ (BL.toChunks bs) (SB.sendAll socket)+#endif+++--------------------------------------------------------------------------------+makeEchoStream :: IO Stream+makeEchoStream = do+ mvar <- newEmptyMVar+ makeStream (takeMVar mvar) $ \mbBs -> case mbBs of+ Nothing -> putMVar mvar Nothing+ Just bs -> forM_ (BL.toChunks bs) $ \c -> putMVar mvar (Just c)+++--------------------------------------------------------------------------------+parseBin :: Stream -> BIN.Get a -> IO (Maybe a)+parseBin stream parser = do+ state <- readIORef (streamState stream)+ case state of+ Closed remainder+ | B.null remainder -> return Nothing+ | otherwise -> go (BIN.runGetIncremental parser `BIN.pushChunk` remainder) True+ Open buffer+ | B.null buffer -> do+ mbBs <- streamIn stream+ case mbBs of+ Nothing -> do+ writeIORef (streamState stream) (Closed B.empty)+ return Nothing+ Just bs -> go (BIN.runGetIncremental parser `BIN.pushChunk` bs) False+ | otherwise -> go (BIN.runGetIncremental parser `BIN.pushChunk` buffer) False+ where+ -- Buffer is empty when entering this function.+ go (BIN.Done remainder _ x) closed = do+ writeIORef (streamState stream) $+ if closed then Closed remainder else Open remainder+ return (Just x)+ go (BIN.Partial f) closed+ | closed = go (f Nothing) True+ | otherwise = do+ mbBs <- streamIn stream+ case mbBs of+ Nothing -> go (f Nothing) True+ Just bs -> go (f (Just bs)) False+ go (BIN.Fail _ _ err) _ = throwIO (ParseException err)+++parse :: Stream -> Atto.Parser a -> IO (Maybe a)+parse stream parser = do+ state <- readIORef (streamState stream)+ case state of+ Closed remainder+ | B.null remainder -> return Nothing+ | otherwise -> go (Atto.parse parser remainder) True+ Open buffer+ | B.null buffer -> do+ mbBs <- streamIn stream+ case mbBs of+ Nothing -> do+ writeIORef (streamState stream) (Closed B.empty)+ return Nothing+ Just bs -> go (Atto.parse parser bs) False+ | otherwise -> go (Atto.parse parser buffer) False+ where+ -- Buffer is empty when entering this function.+ go (Atto.Done remainder x) closed = do+ writeIORef (streamState stream) $+ if closed then Closed remainder else Open remainder+ return (Just x)+ go (Atto.Partial f) closed+ | closed = go (f B.empty) True+ | otherwise = do+ mbBs <- streamIn stream+ case mbBs of+ Nothing -> go (f B.empty) True+ Just bs -> go (f bs) False+ go (Atto.Fail _ _ err) _ = throwIO (ParseException err)+++--------------------------------------------------------------------------------+write :: Stream -> BL.ByteString -> IO ()+write stream = streamOut stream . Just+++--------------------------------------------------------------------------------+close :: Stream -> IO ()+close stream = streamOut stream Nothing
src/Network/WebSockets/Types.hs view
@@ -1,61 +1,82 @@--{-# LANGUAGE DeriveDataTypeable #-}-+-------------------------------------------------------------------------------- -- | Primary types+{-# LANGUAGE DeriveDataTypeable #-} module Network.WebSockets.Types ( Message (..) , ControlMessage (..) , DataMessage (..) , WebSocketsData (..) - , ConnectionError (..)+ , HandshakeException (..)+ , ConnectionException (..)++ , ConnectionType (..)++ , decodeUtf8Lenient+ , decodeUtf8Strict ) where -import Control.Exception (Exception(..))-import Data.Typeable (Typeable) -import qualified Data.Attoparsec.Enumerator as AE-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Encoding as TL+--------------------------------------------------------------------------------+import Control.Exception (Exception (..))+import Control.Exception (throw, try)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Text.Encoding.Error as TL+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TL+import Data.Typeable (Typeable)+import Data.Word (Word16)+import System.IO.Unsafe (unsafePerformIO) --- | The connection couldn't be established or broke down unexpectedly. thrown--- as an iteratee exception.-data ConnectionError- -- | The client sent malformed data.- = ParseError AE.ParseError- -- | the client closed the connection while- -- we were trying to receive some data.- --- -- todo: Also want this for sending.- | ConnectionClosed - deriving (Show, Typeable) -instance Exception ConnectionError+--------------------------------------------------------------------------------+import Network.WebSockets.Http ++-------------------------------------------------------------------------------- -- | The kind of message a server application typically deals with-data Message p- = ControlMessage (ControlMessage p)- | DataMessage (DataMessage p)+data Message+ = ControlMessage ControlMessage+ -- | Reserved bits, actual message+ | DataMessage Bool Bool Bool DataMessage deriving (Eq, Show) ++-------------------------------------------------------------------------------- -- | Different control messages-data ControlMessage p- = Close BL.ByteString+data ControlMessage+ = Close Word16 BL.ByteString | Ping BL.ByteString | Pong BL.ByteString deriving (Eq, Show) ++-------------------------------------------------------------------------------- -- | For an end-user of this library, dealing with 'Frame's would be a bit -- low-level. This is why define another type on top of it, which represents -- data for the application layer.-data DataMessage p- = Text BL.ByteString+--+-- There are currently two kinds of data messages supported by the WebSockets+-- protocol:+--+-- * Textual UTF-8 encoded data. This corresponds roughly to sending a String+-- in JavaScript.+--+-- * Binary data. This corresponds roughly to send an ArrayBuffer in+-- JavaScript.+data DataMessage+ -- | A textual message. The second field /might/ contain the decoded UTF-8+ -- text for caching reasons. This field is computed lazily so if it's not+ -- accessed, it should have no performance impact.+ = Text BL.ByteString (Maybe TL.Text)+ -- | A binary message. | Binary BL.ByteString deriving (Eq, Show) ++-------------------------------------------------------------------------------- -- | In order to have an even more high-level API, we define a typeclass for -- values the user can receive from and send to the socket. A few warnings -- apply:@@ -70,21 +91,97 @@ -- * Messages can be very large. If this is the case, it might be inefficient to -- use the strict 'B.ByteString' and 'T.Text' instances. class WebSocketsData a where+ fromDataMessage :: DataMessage -> a+ fromLazyByteString :: BL.ByteString -> a toLazyByteString :: a -> BL.ByteString ++-------------------------------------------------------------------------------- instance WebSocketsData BL.ByteString where+ fromDataMessage (Text bl _) = bl+ fromDataMessage (Binary bl) = bl+ fromLazyByteString = id toLazyByteString = id ++-------------------------------------------------------------------------------- instance WebSocketsData B.ByteString where+ fromDataMessage (Text bl _) = fromLazyByteString bl+ fromDataMessage (Binary bl) = fromLazyByteString bl+ fromLazyByteString = B.concat . BL.toChunks toLazyByteString = BL.fromChunks . return ++-------------------------------------------------------------------------------- instance WebSocketsData TL.Text where+ fromDataMessage (Text _ (Just tl)) = tl+ fromDataMessage (Text bl Nothing) = fromLazyByteString bl+ fromDataMessage (Binary bl) = fromLazyByteString bl++ fromLazyByteString = TL.decodeUtf8 toLazyByteString = TL.encodeUtf8 ++-------------------------------------------------------------------------------- instance WebSocketsData T.Text where+ fromDataMessage (Text _ (Just tl)) = T.concat (TL.toChunks tl)+ fromDataMessage (Text bl Nothing) = fromLazyByteString bl+ fromDataMessage (Binary bl) = fromLazyByteString bl+ fromLazyByteString = T.concat . TL.toChunks . fromLazyByteString toLazyByteString = toLazyByteString . TL.fromChunks . return+++--------------------------------------------------------------------------------+-- | Various exceptions that can occur while receiving or transmitting messages+data ConnectionException+ -- | The peer has requested that the connection be closed, and included+ -- a close code and a reason for closing. When receiving this exception,+ -- no more messages can be sent. Also, the server is responsible for+ -- closing the TCP connection once this exception is received.+ --+ -- See <http://tools.ietf.org/html/rfc6455#section-7.4> for a list of close+ -- codes.+ = CloseRequest Word16 BL.ByteString++ -- | The peer unexpectedly closed the connection while we were trying to+ -- receive some data. This is a violation of the websocket RFC since the+ -- TCP connection should only be closed after sending and receiving close+ -- control messages.+ | ConnectionClosed++ -- | The client sent garbage, i.e. we could not parse the WebSockets stream.+ | ParseException String++ -- | The client sent invalid UTF-8. Note that this exception will only be+ -- thrown if strict decoding is set in the connection options.+ | UnicodeException String+ deriving (Eq, Show, Typeable)+++--------------------------------------------------------------------------------+instance Exception ConnectionException+++--------------------------------------------------------------------------------+data ConnectionType = ServerConnection | ClientConnection+ deriving (Eq, Ord, Show)+++--------------------------------------------------------------------------------+-- | Replace an invalid input byte with the Unicode replacement character+-- U+FFFD.+decodeUtf8Lenient :: BL.ByteString -> TL.Text+decodeUtf8Lenient = TL.decodeUtf8With TL.lenientDecode+++--------------------------------------------------------------------------------+-- | Throw an error if there is an invalid input byte.+decodeUtf8Strict :: BL.ByteString -> Either ConnectionException TL.Text+decodeUtf8Strict bl = unsafePerformIO $ try $+ let txt = TL.decodeUtf8With (\err _ -> throw (UnicodeException err)) bl in+ TL.length txt `seq` return txt
− src/Network/WebSockets/Util/PubSub.hs
@@ -1,79 +0,0 @@--- | This is a simple utility module to implement a publish-subscribe pattern.--- Note that this only allows communication in a single direction: pusing data--- from the server to connected clients (browsers).------ Usage:------ * Create a new 'PubSub' handle using 'newPubSub'------ * Subscribe your clients using the 'subscribe' call------ * Push new updates from the server using the 'publish' call----{-# LANGUAGE Rank2Types, ScopedTypeVariables #-}-module Network.WebSockets.Util.PubSub- ( PubSub- , newPubSub- , publish- , subscribe- ) where--import Control.Applicative ((<$>))-import Control.Exception (IOException, handle)-import Control.Monad (foldM, forever)-import Control.Monad.Trans (liftIO)-import Data.IntMap (IntMap)-import Data.List (foldl')-import qualified Control.Concurrent.MVar as MV--import qualified Data.IntMap as IM--import Network.WebSockets--data PubSub_ p = PubSub_- { pubSubNextId :: Int- , pubSubSinks :: IntMap (Sink p)- }--addClient :: Sink p -> PubSub_ p -> (PubSub_ p, Int)-addClient sink (PubSub_ nid sinks) =- (PubSub_ (nid + 1) (IM.insert nid sink sinks), nid)--removeClient :: Int -> PubSub_ p -> PubSub_ p-removeClient ref ps = ps {pubSubSinks = IM.delete ref (pubSubSinks ps)}---- | A handle which keeps track of subscribed clients-newtype PubSub p = PubSub (MV.MVar (PubSub_ p))---- | Create a new 'PubSub' handle, with no clients initally connected-newPubSub :: IO (PubSub p)-newPubSub = PubSub <$> MV.newMVar PubSub_- { pubSubNextId = 0- , pubSubSinks = IM.empty- }---- | Broadcast a message to all connected clients-publish :: PubSub p -> Message p -> IO ()-publish (PubSub mvar) msg = MV.modifyMVar_ mvar $ \pubSub -> do- -- Take care to detect and remove broken clients- broken <- foldM publish' [] (IM.toList $ pubSubSinks pubSub)- return $ foldl' (\p b -> removeClient b p) pubSub broken- where- -- Publish the message to a single client, add it to the broken list if an- -- IOException occurs- publish' broken (i, s) =- handle (\(_ :: IOException) -> return (i : broken)) $ do- sendSink s msg- return broken---- | Blocks forever-subscribe :: Protocol p => PubSub p -> WebSockets p ()-subscribe (PubSub mvar) = do- sink <- getSink- ref <- liftIO $ MV.modifyMVar mvar $ return . addClient sink- catchWsError loop $ const $ liftIO $- MV.modifyMVar_ mvar $ return . removeClient ref- where- loop = forever $ do- _ <- receiveDataMessage- return ()
+ tests/autobahn/server.hs view
@@ -0,0 +1,91 @@+--------------------------------------------------------------------------------+-- | The server part of the tests+{-# LANGUAGE OverloadedStrings #-}+module Main+ ( main+ ) where++{-++## once+virtualenv pyt+source pyt/bin/activate+### pip install --upgrade setuptools ### possibly+pip install autobahntestsuite++## each time+source pyt/bin/activate+mkdir -p test && cd test+wstest -m fuzzingclient+websockets-autobahn+-}+++--------------------------------------------------------------------------------+import Control.Exception (catch)+import Data.ByteString.Lazy.Char8 ()+import Data.String (fromString)+import Data.Version (showVersion)+++--------------------------------------------------------------------------------+import qualified Network.WebSockets as WS+import qualified Paths_websockets+++--------------------------------------------------------------------------------+echoDataMessage :: WS.Connection -> IO ()+echoDataMessage conn = go 0+ where+ go :: Int -> IO ()+ go x = do+ msg <- WS.receiveDataMessage conn+ WS.sendDataMessage conn msg+ go (x + 1)+++--------------------------------------------------------------------------------+infoHeaders :: WS.Headers+infoHeaders =+ [ ( "Server"+ , fromString $ "websockets/" ++ showVersion Paths_websockets.version+ )+ ]+++--------------------------------------------------------------------------------+-- | Application+application :: WS.ServerApp+application pc = do+ conn <- WS.acceptRequestWith pc WS.defaultAcceptRequest+ { WS.acceptHeaders = infoHeaders+ }+ echoDataMessage conn `catch` handleClose++ where+ handleClose (WS.CloseRequest i "") =+ putStrLn $ "Clean close (" ++ show i ++ ")"+ handleClose (WS.CloseRequest i msg) =+ putStrLn $ "Clean close (" ++ show i ++ "): " ++ show msg+ handleClose WS.ConnectionClosed =+ putStrLn "Unexpected connection closed exception"+ handleClose (WS.ParseException e) =+ putStrLn $ "Recevied parse exception: " ++ show e+ handleClose (WS.UnicodeException e) =+ putStrLn $ "Recevied unicode exception: " ++ show e+++--------------------------------------------------------------------------------+-- | Accepts clients, spawns a single handler for each one.+main :: IO ()+main = WS.runServerWithOptions options application+ where+ options = WS.defaultServerOptions+ { WS.serverHost = "0.0.0.0"+ , WS.serverPort = 9001+ , WS.serverConnectionOptions = WS.defaultConnectionOptions+ { WS.connectionCompressionOptions =+ WS.PermessageDeflateCompression WS.defaultPermessageDeflate+ , WS.connectionStrictUnicode = True+ }+ }
+ tests/haskell/Network/WebSockets/Extensions/PermessageDeflate/Tests.hs view
@@ -0,0 +1,46 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Extensions.PermessageDeflate.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import Control.Exception (try)+import qualified Data.ByteString.Lazy as BL+import Network.WebSockets.Extensions.PermessageDeflate+import Network.WebSockets.Types+import Network.WebSockets.Connection.Options+import Test.Framework (Test,+ testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion,+ (@?=))+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Extensions.PermessageDeflate.Tests"+ [ testCase "OK 1" $ do+ inflater <- makeMessageInflater+ (SizeLimit 100) (Just defaultPermessageDeflate)+ message <- inflater $ DataMessage True False False (Binary deflated100)+ message @?=+ DataMessage False False False (Binary inflated100)+ , testCase "Exceed 1" $ do+ inflater <- makeMessageInflater+ (SizeLimit 99) (Just defaultPermessageDeflate)+ assertParseException $+ inflater $ DataMessage True False False (Binary deflated100)+ ]+ where+ assertParseException :: IO a -> Assertion+ assertParseException io = do+ errOrX <- try io+ case errOrX of+ Left (ParseException _) -> return ()+ _ -> fail "Excepted ParseException"++ -- This inflates to 100 bytes.+ deflated100 = "b`\160=\NUL\NUL"+ inflated100 = BL.replicate 100 0
+ tests/haskell/Network/WebSockets/Extensions/Tests.hs view
@@ -0,0 +1,44 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Extensions.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import Network.WebSockets.Extensions+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit ((@?=))+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Extensions.Tests"+ [ testCase "parseExtensionDescriptions 01" $ do+ parseExtensionDescriptions "permessage-deflate" @?= Right+ [ ExtensionDescription "permessage-deflate" [] ]++ , testCase "parseExtensionDescriptions 02" $ do+ parseExtensionDescriptions "permessage-deflate; client_max_window_bits; server_max_window_bits=10" @?= Right+ [ ExtensionDescription "permessage-deflate"+ [ ("client_max_window_bits", Nothing)+ , ("server_max_window_bits", Just "10")+ ]+ ]++ , testCase "parseExtensionDescriptions 03" $ do+ parseExtensionDescriptions "permessage-deflate; client_max_window_bits=15; server_max_window_bits=10, permessage-deflate; client_max_window_bits,permessage-deflate; client_max_window_bits=15; client_max_window_bits=10" @?= Right+ [ ExtensionDescription "permessage-deflate"+ [ ("client_max_window_bits", Just "15")+ , ("server_max_window_bits", Just "10")+ ]+ , ExtensionDescription "permessage-deflate"+ [ ("client_max_window_bits", Nothing)+ ]+ , ExtensionDescription "permessage-deflate"+ [ ("client_max_window_bits", Just "15")+ , ("client_max_window_bits", Just "10")+ ]+ ]+ ]
+ tests/haskell/Network/WebSockets/Handshake/Tests.hs view
@@ -0,0 +1,185 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Handshake.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import Control.Concurrent (forkIO)+import Control.Exception (handle)+import Data.ByteString.Char8 ()+import Data.IORef (newIORef, readIORef,+ writeIORef)+import Data.Maybe (fromJust)+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion, assert, (@?=))+++--------------------------------------------------------------------------------+import Network.WebSockets+import Network.WebSockets.Connection+import Network.WebSockets.Http+import qualified Network.WebSockets.Stream as Stream+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Handshake.Test"+ [ testCase "handshake Hybi13" testHandshakeHybi13+ , testCase "handshake Hybi13 with subprotocols" testHandshakeHybi13WithProto+ , testCase "handshake Hybi13 with headers" testHandshakeHybi13WithHeaders+ , testCase "handshake Hybi13 with subprotocols and headers" testHandshakeHybi13WithProtoAndHeaders+ , testCase "handshake reject" testHandshakeReject+ , testCase "handshake reject with custom code" testHandshakeRejectWithCode+ , testCase "handshake Hybi9000" testHandshakeHybi9000+ ]+++--------------------------------------------------------------------------------+testHandshake :: RequestHead -> (PendingConnection -> IO a) -> IO ResponseHead+testHandshake rq app = do+ echo <- Stream.makeEchoStream+ _ <- forkIO $ do+ _ <- app (PendingConnection defaultConnectionOptions rq nullify echo)+ return ()+ mbRh <- Stream.parse echo decodeResponseHead+ Stream.close echo+ case mbRh of+ Nothing -> fail "testHandshake: No response"+ Just rh -> return rh+ where+ nullify _ = return ()+++--------------------------------------------------------------------------------+(!) :: Eq a => [(a, b)] -> a -> b+assoc ! key = fromJust (lookup key assoc)+++--------------------------------------------------------------------------------+rq13 :: RequestHead+rq13 = RequestHead "/mychat"+ [ ("Host", "server.example.com")+ , ("Upgrade", "websocket")+ , ("Connection", "Upgrade")+ , ("Sec-WebSocket-Key", "x3JJHMbDL1EzLkh9GBhXDw==")+ , ("Sec-WebSocket-Protocol", "chat, superchat")+ , ("Sec-WebSocket-Version", "13")+ , ("Origin", "http://example.com")+ ]+ False+++--------------------------------------------------------------------------------+testHandshakeHybi13 :: Assertion+testHandshakeHybi13 = do+ onAcceptFired <- newIORef False+ ResponseHead code message headers <- testHandshake rq13 $ \pc ->+ acceptRequest pc {pendingOnAccept = \_ -> writeIORef onAcceptFired True}++ readIORef onAcceptFired >>= assert+ code @?= 101+ message @?= "WebSocket Protocol Handshake"+ headers ! "Sec-WebSocket-Accept" @?= "HSmrc0sMlYUkAGmm5OPpG2HaGWk="+ headers ! "Connection" @?= "Upgrade"+ lookup "Sec-WebSocket-Protocol" headers @?= Nothing++--------------------------------------------------------------------------------+testHandshakeHybi13WithProto :: Assertion+testHandshakeHybi13WithProto = do+ onAcceptFired <- newIORef False+ ResponseHead code message headers <- testHandshake rq13 $ \pc -> do+ getRequestSubprotocols (pendingRequest pc) @?= ["chat", "superchat"]+ acceptRequestWith pc {pendingOnAccept = \_ -> writeIORef onAcceptFired True}+ (AcceptRequest (Just "superchat") [])++ readIORef onAcceptFired >>= assert+ code @?= 101+ message @?= "WebSocket Protocol Handshake"+ headers ! "Sec-WebSocket-Accept" @?= "HSmrc0sMlYUkAGmm5OPpG2HaGWk="+ headers ! "Connection" @?= "Upgrade"+ headers ! "Sec-WebSocket-Protocol" @?= "superchat"++--------------------------------------------------------------------------------+testHandshakeHybi13WithHeaders :: Assertion+testHandshakeHybi13WithHeaders = do+ onAcceptFired <- newIORef False+ ResponseHead code message headers <- testHandshake rq13 $ \pc -> do+ getRequestSubprotocols (pendingRequest pc) @?= ["chat", "superchat"]+ acceptRequestWith pc {pendingOnAccept = \_ -> writeIORef onAcceptFired True}+ (AcceptRequest Nothing [("Set-Cookie","sid=foo")])++ readIORef onAcceptFired >>= assert+ code @?= 101+ message @?= "WebSocket Protocol Handshake"+ headers ! "Sec-WebSocket-Accept" @?= "HSmrc0sMlYUkAGmm5OPpG2HaGWk="+ headers ! "Connection" @?= "Upgrade"+ headers ! "Set-Cookie" @?= "sid=foo"+ lookup "Sec-WebSocket-Protocol" headers @?= Nothing++--------------------------------------------------------------------------------+testHandshakeHybi13WithProtoAndHeaders :: Assertion+testHandshakeHybi13WithProtoAndHeaders = do+ onAcceptFired <- newIORef False+ ResponseHead code message headers <- testHandshake rq13 $ \pc -> do+ getRequestSubprotocols (pendingRequest pc) @?= ["chat", "superchat"]+ acceptRequestWith pc {pendingOnAccept = \_ -> writeIORef onAcceptFired True}+ (AcceptRequest (Just "superchat") [("Set-Cookie","sid=foo")])++ readIORef onAcceptFired >>= assert+ code @?= 101+ message @?= "WebSocket Protocol Handshake"+ headers ! "Sec-WebSocket-Accept" @?= "HSmrc0sMlYUkAGmm5OPpG2HaGWk="+ headers ! "Connection" @?= "Upgrade"+ headers ! "Sec-WebSocket-Protocol" @?= "superchat"+ headers ! "Set-Cookie" @?= "sid=foo"+++--------------------------------------------------------------------------------+testHandshakeReject :: Assertion+testHandshakeReject = do+ ResponseHead code _ _ <- testHandshake rq13 $ \pc ->+ rejectRequest pc "YOU SHALL NOT PASS"++ code @?= 400+++--------------------------------------------------------------------------------+testHandshakeRejectWithCode :: Assertion+testHandshakeRejectWithCode = do+ ResponseHead code _ _ <- testHandshake rq13 $ \pc ->+ rejectRequestWith pc defaultRejectRequest+ { rejectBody = "YOU SHALL NOT PASS"+ , rejectCode = 401+ }++ code @?= 401+++--------------------------------------------------------------------------------+-- I don't believe this one is supported yet+rq9000 :: RequestHead+rq9000 = RequestHead "/chat"+ [ ("Host", "server.example.com")+ , ("Upgrade", "websocket")+ , ("Connection", "Upgrade")+ , ("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")+ , ("Sec-WebSocket-Origin", "http://example.com")+ , ("Sec-WebSocket-Protocol", "chat, superchat")+ , ("Sec-WebSocket-Version", "9000")+ ]+ False+++--------------------------------------------------------------------------------+testHandshakeHybi9000 :: Assertion+testHandshakeHybi9000 = do+ ResponseHead code _ headers <- testHandshake rq9000 $ \pc ->+ flip handle (acceptRequest pc) $ \e -> case e of+ NotSupported -> return undefined+ _ -> error $ "Unexpected Exception: " ++ show e++ code @?= 400+ headers ! "Sec-WebSocket-Version" @?= "13"
+ tests/haskell/Network/WebSockets/Http/Tests.hs view
@@ -0,0 +1,88 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Http.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import qualified Data.Attoparsec.ByteString as A+import qualified Data.ByteString.Char8 as BC+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion, assert)+++--------------------------------------------------------------------------------+import Network.WebSockets.Http+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Http.Tests"+ [ testCase "jwebsockets response" jWebSocketsResponse+ , testCase "chromium response" chromiumResponse+ , testCase "matchbook response" matchbookResponse+ ]+++--------------------------------------------------------------------------------+-- | This is a specific response sent by jwebsockets which caused trouble+jWebSocketsResponse :: Assertion+jWebSocketsResponse = assert $ case A.parseOnly decodeResponseHead input of+ Left err -> error err+ Right _ -> True+ where+ input = BC.intercalate "\r\n"+ [ "HTTP/1.1 101 Switching Protocols"+ , "Upgrade: websocket"+ , "Connection: Upgrade"+ , "Sec-WebSocket-Accept: Ha0QR1T9CoYx/nqwHsVnW8KVTSo="+ , "Sec-WebSocket-Origin: "+ , "Sec-WebSocket-Location: ws://127.0.0.1"+ , "Set-Cookie: JWSSESSIONID=2e0690e2e328f327056a5676b6a890e3; HttpOnly"+ , ""+ , ""+ ]+++--------------------------------------------------------------------------------+-- | This is a specific response sent by chromium which caused trouble+chromiumResponse :: Assertion+chromiumResponse = assert $ case A.parseOnly decodeResponseHead input of+ Left err -> error err+ Right _ -> True+ where+ input = BC.intercalate "\r\n"+ [ "HTTP/1.1 500 Internal Error"+ , "Content-Type:text/html"+ , "Content-Length:23"+ , ""+ , "No such target id: 20_1"+ ]++--------------------------------------------------------------------------------+-- | This is a specific response sent by Matchbook.com which caused trouble++matchbookResponse :: Assertion+matchbookResponse = assert $ case A.parseOnly decodeResponseHead input of+ Left err -> error err+ Right _ -> True+ where+ input = BC.intercalate "\r\n"+ [ "HTTP/1.1 101 "+ , "Date: Mon, 22 May 2017 19:39:08 GMT"+ , "Connection: upgrade"+ , "Set-Cookie: __cfduid=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdea; expires=Tue, 22-May-18 19:39:08 GMT; path=/; domain=.matchbook.com; HttpOnly"+ , "X-Content-Type-Options: nosniff"+ , "X-XSS-Protection: 1; mode=block"+ , "X-Frame-Options: DENY"+ , "Upgrade: websocket"+ , "Sec-WebSocket-Accept: dEadB33fDeadbEEfD3aDbE3Fdea="+ , "X-MB-HA: edge-socket"+ , "X-MB-HAP: haproxy01aws"+ , "Server: cloudflare-nginx"+ , "CF-RAY: 3632deadbeef5b33-HEL"+ , ""+ , ""+ ]
+ tests/haskell/Network/WebSockets/Hybi13/Demultiplex/Tests.hs view
@@ -0,0 +1,72 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Hybi13.Demultiplex.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import Control.Applicative ((<$>))+import qualified Data.ByteString.Lazy as BL+import Network.WebSockets+import Network.WebSockets.Hybi13.Demultiplex+import Prelude+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion, (@=?))+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Hybi13.Demultiplex.Tests"+ [ testMessageDataSizeLimit+ ]+++--------------------------------------------------------------------------------+testMessageDataSizeLimit :: Test+testMessageDataSizeLimit = testGroup "testMessageDataSizeLimit Hybi13"+ [ testCase "OK 1" $+ Right [DataMessage False False False (Binary (mkZeroes 100))] @=?+ testDemultiplex (SizeLimit 100) (fragmented 5 20)+ , testCase "Exceeds 1" $+ assertLeft $+ testDemultiplex (SizeLimit 99) (fragmented 5 20)+ , testCase "Exceeds 2" $+ assertLeft $+ testDemultiplex (SizeLimit 100) (fragmented 6 20)+ , testCase "Exceeds 3" $+ assertLeft $+ testDemultiplex (SizeLimit 100) (fragmented 101 1)+ , testCase "Exceeds 4" $+ assertLeft $+ testDemultiplex (SizeLimit 100) (fragmented 1 101)+ ]+ where+ fragmented :: Int -> Int -> [Frame]+ fragmented n size =+ let payload = mkZeroes size in+ [Frame False False False False BinaryFrame payload] +++ replicate (n - 2) (Frame False False False False ContinuationFrame payload) +++ [Frame True False False False ContinuationFrame payload]++ mkZeroes :: Int -> BL.ByteString+ mkZeroes size = BL.replicate (fromIntegral size) 0++ assertLeft :: Either a b -> Assertion+ assertLeft (Left _) = return ()+ assertLeft (Right _) = fail "Expecting test to fail"+++--------------------------------------------------------------------------------+testDemultiplex+ :: SizeLimit+ -> [Frame]+ -> Either ConnectionException [Message]+testDemultiplex messageLimit = go emptyDemultiplexState+ where+ go _state0 [] = return []+ go state0 (frame : frames) = case demultiplex messageLimit state0 frame of+ (DemultiplexContinue, state1) -> go state1 frames+ (DemultiplexError err, _) -> Left err+ (DemultiplexSuccess m, state1) -> (m :) <$> go state1 frames
+ tests/haskell/Network/WebSockets/Mask/Tests.hs view
@@ -0,0 +1,57 @@+--------------------------------------------------------------------------------+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.WebSockets.Mask.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import qualified Data.Binary.Get as Get+import Data.Bits (xor)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Network.WebSockets.Hybi13.Mask+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.QuickCheck (Arbitrary (..), (===))+import qualified Test.QuickCheck as QC+++--------------------------------------------------------------------------------+import Network.WebSockets.Tests.Util++tests :: Test+tests = testGroup "Network.WebSockets.Masks.Tests"+ [ testProperty "correct fast masking" testMasking ]++maskPayload' :: Maybe B.ByteString -> BL.ByteString -> BL.ByteString+maskPayload' Nothing = id+maskPayload' (Just mask) = snd . BL.mapAccumL f (cycle $ B.unpack mask)+ where+ f [] !c = ([], c)+ f (m:ms) !c = (ms, m `xor` c)++newtype AMask = AMask B.ByteString deriving (Show)+instance Arbitrary AMask where+ arbitrary = do+ c1 <- arbitrary+ c2 <- arbitrary+ c3 <- arbitrary+ c4 <- arbitrary+ return (AMask (B.pack [c1,c2,c3,c4]))++newtype APkt = APkt BL.ByteString deriving (Show)+instance Arbitrary APkt where+ arbitrary = do+ b1 <- arbitraryByteString+ b2 <- arbitraryByteString+ return $ APkt (b1 `BL.append` b2) -- Just for sure to test correctly different alignments+ shrink (APkt bs) =+ map APkt [ BL.append a b | (a, b) <- zip (BL.inits bs) (tail $ BL.tails bs) ]++testMasking :: QC.Property+testMasking =+ QC.forAllShrink QC.arbitrary QC.shrink $ \(AMask mask, APkt pkt) ->+ let wmask = Get.runGet parseMask (BL.fromStrict mask)+ in maskPayload' (Just mask) pkt === maskPayload (Just wmask) pkt
+ tests/haskell/Network/WebSockets/Server/Tests.hs view
@@ -0,0 +1,185 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Network.WebSockets.Server.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import Control.Applicative ((<$>), (<|>))+import Control.Concurrent (forkIO, killThread,+ threadDelay)+import Control.Concurrent.Async (Async, async, cancel)+import Control.Exception (SomeException, catch, handle)+import Control.Monad (forever, replicateM, unless)+import Data.IORef (IORef, newIORef, readIORef,+ writeIORef)++--------------------------------------------------------------------------------+import qualified Data.ByteString.Lazy as BL+import Data.Text (Text)+import System.Environment (getEnvironment)+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion, assert, (@=?))+import Test.QuickCheck (Arbitrary, arbitrary)+import Test.QuickCheck.Gen (Gen (..))+import Test.QuickCheck.Random (newQCGen)+++--------------------------------------------------------------------------------+import Network.WebSockets+import Network.WebSockets.Tests.Util+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Server.Tests"+ [ testCase "simple server/client" testSimpleServerClient+ , testCase "bulk server/client" testBulkServerClient+ , testCase "onPong" testOnPong+ , testCase "ipv6 server" testIpv6Server+ , testCase "reject request" testRejectRequest + ]+++--------------------------------------------------------------------------------+testSimpleServerClient :: Assertion+testSimpleServerClient = testServerClient "127.0.0.1" $ \conn -> mapM_ (sendTextData conn)+++--------------------------------------------------------------------------------+-- | This is a bit ugly but it seems CI services don't support ipv6 in 2018.+skipIpv6Incompatible :: Assertion -> Assertion+skipIpv6Incompatible assertion = do+ env <- getEnvironment+ case lookup "TRAVIS" env <|> lookup "CIRCLECI" env of+ Just "true" -> return ()+ _ -> assertion++--------------------------------------------------------------------------------+testIpv6Server :: Assertion+testIpv6Server = skipIpv6Incompatible $+ testServerClient "::1" $ \conn -> mapM_ (sendTextData conn)++--------------------------------------------------------------------------------+testBulkServerClient :: Assertion+testBulkServerClient = testServerClient "127.0.0.1" sendTextDatas++--------------------------------------------------------------------------------+testServerClient :: String -> (Connection -> [BL.ByteString] -> IO ()) -> Assertion+testServerClient host sendMessages = withEchoServer host 42940 "Bye" $ do+ texts <- map unArbitraryUtf8 <$> sample+ texts' <- runClient host 42940 "/chat" $ client texts+ texts @=? texts'+ where+ client :: [BL.ByteString] -> ClientApp [BL.ByteString]+ client texts conn = do+ sendMessages conn texts+ texts' <- replicateM (length texts) (receiveData conn)+ sendClose conn ("Bye" :: BL.ByteString)+ expectCloseException conn "Bye"+ return texts'++--------------------------------------------------------------------------------+testRejectRequest :: Assertion+testRejectRequest = withRejectingServer+ where+ client :: ClientApp ()+ client _ = error "Client should not be able to connect"++ server :: ServerApp+ server pendingConnection = rejectRequest pendingConnection "Bye"++ withRejectingServer :: IO ()+ withRejectingServer = do+ serverThread <- async $ runServer "127.0.0.1" 42940 server+ waitSome+ () <- runClient "127.0.0.1" 42940 "/chat" client `catch` handler+ waitSome+ cancel serverThread+ return ()++ handler :: HandshakeException -> IO ()+ handler (RequestRejected _ response) = do+ responseCode response @=? 400+ handler exc = error $ "Unexpected exception " ++ show exc++--------------------------------------------------------------------------------+testOnPong :: Assertion+testOnPong = withEchoServer "127.0.0.1" 42941 "Bye" $ do+ gotPong <- newIORef False+ let opts = defaultConnectionOptions+ { connectionOnPong = writeIORef gotPong True+ }++ rcv <- runClientWith "127.0.0.1" 42941 "/" opts [] client+ assert rcv+ assert =<< readIORef gotPong+ where+ client :: ClientApp Bool+ client conn = do+ sendPing conn ("What's a fish without an eye?" :: Text)+ sendTextData conn ("A fsh!" :: Text)+ msg <- receiveData conn+ sendCloseCode conn 1000 ("Bye" :: BL.ByteString)+ expectCloseException conn "Bye"+ return $ "A fsh!" == (msg :: Text)+++--------------------------------------------------------------------------------+sample :: Arbitrary a => IO [a]+sample = do+ gen <- newQCGen+ return $ (unGen arbitrary) gen 512+++--------------------------------------------------------------------------------+waitSome :: IO ()+waitSome = threadDelay $ 200 * 1000++--------------------------------------------------------------------------------+withEchoServer :: String -> Int -> BL.ByteString -> IO a -> IO a+withEchoServer host port expectedClose action = do+ cRef <- newIORef False+ serverThread <- async $ runServer host port (\c -> server c `catch` handleClose cRef)+ waitSome+ result <- action+ waitSome+ cancel serverThread+ closeCalled <- readIORef cRef+ unless closeCalled $ error "Expecting the CloseRequest exception"+ return result+ where+ server :: ServerApp+ server pc = do+ conn <- acceptRequest pc+ forever $ do+ msg <- receiveDataMessage conn+ sendDataMessage conn msg++ handleClose :: IORef Bool -> ConnectionException -> IO ()+ handleClose cRef (CloseRequest i msg) = do+ i @=? 1000+ msg @=? expectedClose+ writeIORef cRef True+ handleClose _ ConnectionClosed =+ error "Unexpected connection closed exception"+ handleClose _ (ParseException _) =+ error "Unexpected parse exception"+ handleClose _ (UnicodeException _) =+ error "Unexpected unicode exception"+++--------------------------------------------------------------------------------+expectCloseException :: Connection -> BL.ByteString -> IO ()+expectCloseException conn msg = act `catch` handler+ where+ act = receiveDataMessage conn >> error "Expecting CloseRequest exception"+ handler (CloseRequest i msg') = do+ i @=? 1000+ msg' @=? msg+ handler ConnectionClosed = error "Unexpected connection closed"+ handler (ParseException _) = error "Unexpected parse exception"+ handler (UnicodeException _) = error "Unexpected unicode exception"
+ tests/haskell/Network/WebSockets/Tests.hs view
@@ -0,0 +1,223 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Network.WebSockets.Tests+ ( tests+ ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString.Builder as Builder+import Control.Applicative ((<$>))+import Control.Concurrent (forkIO)+import Control.Exception (try)+import Control.Monad (replicateM)+import Data.Binary.Get (runGetOrFail)+import qualified Data.ByteString.Lazy as BL+import Data.List (intersperse)+import Data.Maybe (catMaybes)+import Data.Monoid (mempty, mconcat)+import Network.WebSockets+import qualified Network.WebSockets.Hybi13 as Hybi13+import Network.WebSockets.Hybi13.Demultiplex+import Network.WebSockets.Protocol+import qualified Network.WebSockets.Stream as Stream+import Network.WebSockets.Tests.Util+import Network.WebSockets.Types+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.HUnit ((@=?))+import Test.QuickCheck (Arbitrary (..), Gen,+ Property)+import qualified Test.QuickCheck as QC+import qualified Test.QuickCheck.Monadic as QC+import Prelude+++--------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Network.WebSockets.Test"+ [ testProperty "simple encode/decode Hybi13" (testSimpleEncodeDecode Hybi13)+ , testProperty "fragmented Hybi13" testFragmentedHybi13+ , testRfc_6455_5_5_1+ , testRfc_6455_5_5_2+ , testFramePayloadSizeLimit+ ]++--------------------------------------------------------------------------------+testSimpleEncodeDecode :: Protocol -> Property+testSimpleEncodeDecode protocol = QC.monadicIO $+ QC.forAllM QC.arbitrary $ \msgs -> QC.run $ do+ echo <- Stream.makeEchoStream+ parse <- decodeMessages protocol mempty mempty echo+ write <- encodeMessages protocol ClientConnection echo+ _ <- forkIO $ write msgs+ msgs' <- catMaybes <$> replicateM (length msgs) parse+ Stream.close echo+ msgs @=? msgs'+++--------------------------------------------------------------------------------+testFragmentedHybi13 :: Property+testFragmentedHybi13 = QC.monadicIO $+ QC.forAllM QC.arbitrary $ \fragmented -> QC.run $ do+ echo <- Stream.makeEchoStream+ parse <- Hybi13.decodeMessages mempty mempty echo+ -- is' <- Streams.filter isDataMessage =<< Hybi13.decodeMessages is++ -- Simple hacky encoding of all frames+ _ <- forkIO $ do+ mapM_ (Stream.write echo)+ [ Builder.toLazyByteString (Hybi13.encodeFrame Nothing f)+ | FragmentedMessage _ frames <- fragmented+ , f <- frames+ ]+ Stream.close echo++ -- Check if we got all data+ msgs <- filter isDataMessage <$> parseAll parse+ [msg | FragmentedMessage msg _ <- fragmented] @=? msgs+ where+ isDataMessage (ControlMessage _) = False+ isDataMessage (DataMessage _ _ _ _) = True++ parseAll parse = do+ mbMsg <- try parse+ case mbMsg of+ Left ConnectionClosed -> return []+ Left _ -> return []+ Right (Just msg) -> (msg :) <$> parseAll parse+ Right Nothing -> return []++--------------------------------------------------------------------------------+testRfc_6455_5_5_1 :: Test+testRfc_6455_5_5_1 =+ testCase "RFC 6455, 5.5: Frame encoder shall truncate control frame payload to 125 bytes" $ do+ 260 @=? BL.length (encodedFrame ContinuationFrame)+ 260 @=? BL.length (encodedFrame TextFrame)+ 260 @=? BL.length (encodedFrame BinaryFrame)+ 127 @=? BL.length (encodedFrame CloseFrame)+ 127 @=? BL.length (encodedFrame PingFrame)+ 127 @=? BL.length (encodedFrame PongFrame)+ where+ payload256 = BL.replicate 256 0+ encodedFrame ft+ = Builder.toLazyByteString+ $ Hybi13.encodeFrame Nothing (Frame True False False False ft payload256)++--------------------------------------------------------------------------------+testRfc_6455_5_5_2 :: Test+testRfc_6455_5_5_2 =+ testCase "RFC 6455, 5.5: Frame decoder shall fail if control frame payload length > 125 bytes" $+ Left (BL.drop 4 ping126, 4, errMsg) @=? runGetOrFail (Hybi13.parseFrame mempty) ping126+ where+ errMsg = "Control Frames must not carry payload > 125 bytes!"+ ping126 = mconcat+ [ "\137\254\NUL~\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI\190\252\219\SI\190\252\219\SI\190\252\219"+ , "\SI\190\252\219\SI"+ ]++testFramePayloadSizeLimit :: Test+testFramePayloadSizeLimit = testGroup "FramePayloadSizeLimit Hybi13"+ [ testCase "OK 1" $ case parse (frame 99) of+ Right _ -> return ()+ Left _ -> fail "Expecting successful parse."+ , testCase "OK 2" $ case parse (frame 100) of+ Right _ -> return ()+ Left _ -> fail "Expecting successful parse."+ , testCase "Exceed" $ case parse (frame 101) of+ Right _ -> fail "Expecting parse to fail."+ Left _ -> return ()+ ]+ where+ parse = runGetOrFail (Hybi13.parseFrame (SizeLimit 100))+ frame n = Builder.toLazyByteString $ Hybi13.encodeFrame Nothing $+ Frame True False False False BinaryFrame (BL.replicate n 20)+++--------------------------------------------------------------------------------+instance Arbitrary Message where+ arbitrary = QC.oneof+ [ do+ payload <- BL.take 125 . BL.pack <$> arbitrary+ return $ ControlMessage (Ping payload)+ , do+ payload <- BL.take 125 . BL.pack <$> arbitrary+ return $ ControlMessage (Pong payload)+ , do+ payload <- BL.pack <$> arbitrary+ return $ DataMessage False False False (Text payload Nothing)+ , do+ payload <- BL.pack <$> arbitrary+ return $ DataMessage False False False (Binary payload)+ ]+++--------------------------------------------------------------------------------+data FragmentedMessage = FragmentedMessage Message [Frame]+ deriving (Show)+++--------------------------------------------------------------------------------+instance Arbitrary FragmentedMessage where+ arbitrary = do+ -- Pick a frametype and a corresponding random payload+ ft <- QC.elements [TextFrame, BinaryFrame]+ payload <- case ft of+ TextFrame -> arbitraryUtf8+ _ -> arbitraryByteString++ fragments <- arbitraryFragmentation payload+ let fs = makeFrames $ zip (ft : repeat ContinuationFrame) fragments+ msg = case ft of+ TextFrame -> DataMessage False False False (Text payload Nothing)+ BinaryFrame -> DataMessage False False False (Binary payload)+ _ -> error "Arbitrary FragmentedMessage crashed"++ interleaved <- arbitraryInterleave genControlFrame fs+ return $ FragmentedMessage msg interleaved+ -- return $ FragmentedMessage msg fs+ where+ makeFrames [] = []+ makeFrames [(ft, pl)] = [Frame True False False False ft pl]+ makeFrames ((ft, pl) : fr) =+ Frame False False False False ft pl : makeFrames fr++ genControlFrame = QC.elements+ [ Frame True False False False PingFrame "Herp"+ , Frame True False False False PongFrame "Derp"+ ]+++--------------------------------------------------------------------------------+arbitraryFragmentation :: BL.ByteString -> Gen [BL.ByteString]+arbitraryFragmentation bs = arbitraryFragmentation' bs+ where+ len :: Int+ len = fromIntegral $ BL.length bs+ arbitraryFragmentation' bs' = do+ -- TODO: we currently can't send packets of length 0. We should+ -- investigate why (regardless of the spec).+ n <- QC.choose (1, len - 1)+ let (l, r) = BL.splitAt (fromIntegral n) bs'+ case r of+ "" -> return [l]+ _ -> (l :) <$> arbitraryFragmentation' r+++--------------------------------------------------------------------------------+arbitraryInterleave :: Gen a -> [a] -> Gen [a]+arbitraryInterleave sep xs = fmap concat $ sequence $+ [sep'] ++ intersperse sep' [return [x] | x <- xs] ++ [sep']+ where+ sep' = QC.sized $ \size -> do+ num <- QC.choose (1, size)+ replicateM num sep
+ tests/haskell/Network/WebSockets/Tests/Util.hs view
@@ -0,0 +1,38 @@+--------------------------------------------------------------------------------+module Network.WebSockets.Tests.Util+ ( ArbitraryUtf8 (..)+ , arbitraryUtf8+ , arbitraryByteString+ ) where+++--------------------------------------------------------------------------------+import Control.Applicative ((<$>))+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TL+import Test.QuickCheck (Arbitrary (..), Gen)+++--------------------------------------------------------------------------------+import Network.WebSockets.Types+++--------------------------------------------------------------------------------+newtype ArbitraryUtf8 = ArbitraryUtf8 {unArbitraryUtf8 :: BL.ByteString}+ deriving (Eq, Ord, Show)+++--------------------------------------------------------------------------------+instance Arbitrary ArbitraryUtf8 where+ arbitrary = ArbitraryUtf8 <$> arbitraryUtf8+++--------------------------------------------------------------------------------+arbitraryUtf8 :: Gen BL.ByteString+arbitraryUtf8 = toLazyByteString . TL.encodeUtf8 . TL.pack <$> arbitrary+++--------------------------------------------------------------------------------+arbitraryByteString :: Gen BL.ByteString+arbitraryByteString = BL.pack <$> arbitrary
tests/haskell/TestSuite.hs view
@@ -1,12 +1,24 @@-import Test.Framework (defaultMain)--import qualified Network.WebSockets.Tests+--------------------------------------------------------------------------------+import qualified Network.WebSockets.Extensions.Tests+import qualified Network.WebSockets.Extensions.PermessageDeflate.Tests import qualified Network.WebSockets.Handshake.Tests-import qualified Network.WebSockets.Socket.Tests+import qualified Network.WebSockets.Http.Tests+import qualified Network.WebSockets.Hybi13.Demultiplex.Tests+import qualified Network.WebSockets.Mask.Tests+import qualified Network.WebSockets.Server.Tests+import qualified Network.WebSockets.Tests+import Test.Framework (defaultMain) ++-------------------------------------------------------------------------------- main :: IO () main = defaultMain- [ Network.WebSockets.Tests.tests+ [ Network.WebSockets.Extensions.Tests.tests+ , Network.WebSockets.Extensions.PermessageDeflate.Tests.tests , Network.WebSockets.Handshake.Tests.tests- , Network.WebSockets.Socket.Tests.tests+ , Network.WebSockets.Http.Tests.tests+ , Network.WebSockets.Hybi13.Demultiplex.Tests.tests+ , Network.WebSockets.Server.Tests.tests+ , Network.WebSockets.Mask.Tests.tests+ , Network.WebSockets.Tests.tests ]
websockets.cabal view
@@ -1,5 +1,5 @@ Name: websockets-Version: 0.6.0.4+Version: 0.13.0.0 Synopsis: A sensible and clean way to write WebSocket-capable servers in Haskell.@@ -7,10 +7,11 @@ Description: This library allows you to write WebSocket-capable servers. .- See an example: <http://jaspervdj.be/websockets/example.html>.+ An example server:+ <https://github.com/jaspervdj/websockets/blob/master/example/server.lhs> .- The API of the 'Network.WebSockets' module should also contain enough- information to get you started.+ An example client:+ <https://github.com/jaspervdj/websockets/blob/master/example/client.hs> . See also: .@@ -23,98 +24,220 @@ License: BSD3 License-file: LICENCE Copyright: (c) 2010-2011 Siniša Biđin- (c) 2011 Jasper Van der Jeugt+ (c) 2011-2018 Jasper Van der Jeugt (c) 2011 Steffen Schuldenzucker (c) 2011 Alex Lang Author: Siniša Biđin <sinisa@bidin.cc> Jasper Van der Jeugt <m@jaspervdj.be> Steffen Schuldenzucker <steffen.schuldenzucker@googlemail.com> Alex Lang <lang@tsurucapital.com>+ Domen Kožar Maintainer: Jasper Van der Jeugt <m@jaspervdj.be> Stability: experimental Category: Network Build-type: Simple-Cabal-version: >= 1.8+Cabal-version: >= 1.10 Homepage: http://jaspervdj.be/websockets Bug-reports: https://github.com/jaspervdj/websockets/issues +Extra-source-files:+ CHANGELOG++Source-repository head+ Type: git+ Location: https://github.com/jaspervdj/websockets++Flag Example+ Description: Build the example server+ Default: False+ Manual: True+ Library Hs-source-dirs: src- Ghc-options: -Wall+ Ghc-options: -Wall+ C-sources: cbits/cbits.c+ Default-language: Haskell2010 Exposed-modules: Network.WebSockets- Network.WebSockets.Internal- Network.WebSockets.Util.PubSub+ Network.WebSockets.Client+ Network.WebSockets.Connection+ Network.WebSockets.Connection.PingPong+ Network.WebSockets.Extensions+ Network.WebSockets.Stream+ -- Network.WebSockets.Util.PubSub TODO Other-modules:- Network.WebSockets.Handshake- Network.WebSockets.Handshake.Http- Network.WebSockets.Monad+ Network.WebSockets.Connection.Options+ Network.WebSockets.Extensions.Description+ Network.WebSockets.Extensions.PermessageDeflate+ Network.WebSockets.Extensions.StrictUnicode+ Network.WebSockets.Http+ Network.WebSockets.Hybi13+ Network.WebSockets.Hybi13.Demultiplex+ Network.WebSockets.Hybi13.Mask Network.WebSockets.Protocol- Network.WebSockets.Protocol.Hybi00- Network.WebSockets.Protocol.Hybi00.Internal- Network.WebSockets.Protocol.Hybi10- Network.WebSockets.Protocol.Hybi10.Demultiplex- Network.WebSockets.Protocol.Hybi10.Internal- Network.WebSockets.Protocol.Hybi10.Mask- Network.WebSockets.Protocol.Unsafe- Network.WebSockets.Socket+ Network.WebSockets.Server Network.WebSockets.Types Build-depends:- attoparsec >= 0.9 && < 0.11,- attoparsec-enumerator >= 0.2 && < 0.4,- base >= 4 && < 5,- base64-bytestring >= 0.1 && < 0.2,- binary >= 0.5 && < 0.6,- blaze-builder >= 0.3 && < 0.4,- blaze-builder-enumerator >= 0.2 && < 0.3,- bytestring >= 0.9 && < 0.10,- case-insensitive >= 0.3 && < 0.5,- containers >= 0.3 && < 0.6,- enumerator >= 0.4.13 && < 0.5,- mtl >= 2.0 && < 2.2,- network >= 2.3 && < 2.4,- network-enumerator >= 0.1 && < 0.2,- random >= 1.0 && < 1.1,- SHA >= 1.5 && < 1.6,- text >= 0.10 && < 0.12,- pureMD5 >= 0.2.2 && < 2.2+ async >= 2.2 && < 2.3,+ attoparsec >= 0.10 && < 0.15,+ base >= 4.14 && < 5,+ base64-bytestring >= 0.1 && < 1.3,+ binary >= 0.8.1 && < 0.11,+ bytestring >= 0.9 && < 0.13,+ case-insensitive >= 0.3 && < 1.3,+ containers >= 0.3 && < 0.7,+ network >= 2.3 && < 3.2,+ random >= 1.0.1 && < 1.3,+ SHA >= 1.5 && < 1.7,+ streaming-commons >= 0.1 && < 0.3,+ text >= 0.10 && < 2.2,+ entropy >= 0.2.1 && < 0.5 Test-suite websockets-tests- Type: exitcode-stdio-1.0- Hs-source-dirs: src tests/haskell- Main-is: TestSuite.hs- Ghc-options: -Wall+ Type: exitcode-stdio-1.0+ Hs-source-dirs: src tests/haskell+ Main-is: TestSuite.hs+ Ghc-options: -Wall+ C-sources: cbits/cbits.c+ Default-language: Haskell2010 + Other-modules:+ Network.WebSockets+ Network.WebSockets.Client+ Network.WebSockets.Connection+ Network.WebSockets.Connection.Options+ Network.WebSockets.Connection.PingPong+ Network.WebSockets.Extensions+ Network.WebSockets.Extensions.Description+ Network.WebSockets.Extensions.PermessageDeflate+ Network.WebSockets.Extensions.PermessageDeflate.Tests+ Network.WebSockets.Extensions.StrictUnicode+ Network.WebSockets.Extensions.Tests+ Network.WebSockets.Handshake.Tests+ Network.WebSockets.Http+ Network.WebSockets.Http.Tests+ Network.WebSockets.Hybi13+ Network.WebSockets.Hybi13.Demultiplex+ Network.WebSockets.Hybi13.Demultiplex.Tests+ Network.WebSockets.Hybi13.Mask+ Network.WebSockets.Mask.Tests+ Network.WebSockets.Protocol+ Network.WebSockets.Server+ Network.WebSockets.Server.Tests+ Network.WebSockets.Stream+ Network.WebSockets.Tests+ Network.WebSockets.Tests.Util+ Network.WebSockets.Types+ Paths_websockets+ Build-depends:- HUnit >= 1.2 && < 1.3,- QuickCheck >= 2.4 && < 2.5,- test-framework >= 0.4 && < 0.7,- test-framework-hunit >= 0.2 && < 0.3,- test-framework-quickcheck2 >= 0.2 && < 0.3,+ HUnit >= 1.2 && < 1.7,+ QuickCheck >= 2.7 && < 2.15,+ test-framework >= 0.4 && < 0.9,+ test-framework-hunit >= 0.2 && < 0.4,+ test-framework-quickcheck2 >= 0.2 && < 0.4, -- Copied from regular dependencies...- attoparsec >= 0.9 && < 0.11,- attoparsec-enumerator >= 0.2 && < 0.4,- base >= 4 && < 5,- base64-bytestring >= 0.1 && < 0.2,- binary >= 0.5 && < 0.6,- blaze-builder >= 0.3 && < 0.4,- blaze-builder-enumerator >= 0.2 && < 0.3,- bytestring >= 0.9 && < 0.10,- case-insensitive >= 0.3 && < 0.5,- containers >= 0.3 && < 0.6,- enumerator >= 0.4.13 && < 0.5,- mtl >= 2.0 && < 2.2,- network >= 2.3 && < 2.4,- network-enumerator >= 0.1 && < 0.2,- random >= 1.0 && < 1.1,- SHA >= 1.5 && < 1.6,- text >= 0.10 && < 0.12,- pureMD5 >= 0.2.2 && < 2.2+ async >= 2.2 && < 2.3,+ attoparsec >= 0.10 && < 0.15,+ base >= 4.14 && < 5,+ base64-bytestring >= 0.1 && < 1.3,+ binary >= 0.8.1 && < 0.11,+ bytestring >= 0.9 && < 0.13,+ case-insensitive >= 0.3 && < 1.3,+ containers >= 0.3 && < 0.7,+ network >= 2.3 && < 3.2,+ random >= 1.0 && < 1.3,+ SHA >= 1.5 && < 1.7,+ streaming-commons >= 0.1 && < 0.3,+ text >= 0.10 && < 2.2,+ entropy >= 0.2.1 && < 0.5 -Source-repository head- Type: git- Location: https://github.com/jaspervdj/websockets+Executable websockets-server-example+ If !flag(Example)+ Buildable: False++ Hs-source-dirs: example+ Main-is: server.lhs+ Ghc-options: -Wall+ Default-language: Haskell2010++ Build-depends:+ base,+ websockets,+ text++Executable websockets-client-example+ If !flag(Example)+ Buildable: False++ Hs-source-dirs: example+ Main-is: client.hs+ Ghc-options: -Wall+ Default-language: Haskell2010++ Build-depends:+ base,+ websockets,+ text,+ network,+ mtl++Executable websockets-autobahn+ If !flag(Example)+ Buildable: False++ Hs-source-dirs: tests/autobahn+ Main-is: server.hs+ Ghc-options: -Wall -threaded -O2 -rtsopts "-with-rtsopts=-N"+ Default-language: Haskell2010++ Other-modules:+ Paths_websockets++ Build-depends:+ websockets,+ -- Copied from regular dependencies...+ async >= 2.2 && < 2.3,+ attoparsec >= 0.10 && < 0.15,+ base >= 4.14 && < 5,+ base64-bytestring >= 0.1 && < 1.3,+ binary >= 0.8.1 && < 0.11,+ bytestring >= 0.9 && < 0.13,+ case-insensitive >= 0.3 && < 1.3,+ containers >= 0.3 && < 0.7,+ network >= 2.3 && < 3.2,+ random >= 1.0 && < 1.3,+ SHA >= 1.5 && < 1.7,+ text >= 0.10 && < 2.2,+ entropy >= 0.2.1 && < 0.5++Benchmark bench-mask+ Type: exitcode-stdio-1.0+ Main-is: mask.hs+ C-sources: cbits/cbits.c+ Hs-source-dirs: benchmarks, src+ Default-language: Haskell2010++ Other-modules:+ Network.WebSockets.Hybi13.Mask++ Build-depends:+ criterion,+ -- Copied from regular dependencies...+ async >= 2.2 && < 2.3,+ attoparsec >= 0.10 && < 0.15,+ base >= 4.14 && < 5,+ base64-bytestring >= 0.1 && < 1.3,+ binary >= 0.8.1 && < 0.11,+ bytestring >= 0.9 && < 0.13,+ case-insensitive >= 0.3 && < 1.3,+ containers >= 0.3 && < 0.7,+ network >= 2.3 && < 3.2,+ random >= 1.0 && < 1.3,+ SHA >= 1.5 && < 1.7,+ text >= 0.10 && < 2.2,+ entropy >= 0.2.1 && < 0.5