packages feed

network-simple-ws 0.1 → 0.2

raw patch · 4 files changed

+135/−50 lines, 4 filesdep +asyncsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: async

API changes (from Hackage documentation)

+ Network.Simple.WS: close :: MonadIO m => Connection -> Word16 -> ByteString -> m ()
- Network.Simple.WS: recv :: MonadIO m => Connection -> m ByteString
+ Network.Simple.WS: recv :: MonadIO m => Connection -> m (Either (Word16, ByteString) ByteString)

Files

− Setup.hs
@@ -1,4 +0,0 @@-#! /usr/bin/env nix-shell-#! nix-shell ./shell.nix -i runghc-import Distribution.Simple-main = defaultMain
changelog.md view
@@ -1,3 +1,21 @@+# Version 0.2++* /COMPILER ASSISTED BREAKING CHANGE:/ `send` and `recv` now use+  lazy `ByteString`s. ++* /COMPILER ASSISTED BREAKING CHANGE:/ `recv`'s close semantics have +  improved. See its documentation for details.++* Added `close`.++* Depend on `websockets >=0.12.6`.+++# Version 0.1.1++* Removed accidental `print`.++ # Version 0.1  * First release.
network-simple-ws.cabal view
@@ -1,28 +1,31 @@+cabal-version: 2.4 name: network-simple-ws-version: 0.1+version: 0.2 synopsis: Simple interface to WebSockets. description: Simple interface to WebSockets.-homepage: https://github.com/k0001/network-simple-ws-bug-reports: https://github.com/k0001/network-simple-ws/issues-license: BSD3+homepage: https://hackage.haskell.org/package/network-simple-ws+bug-reports: https://github.com/k0001/network-simple/issues+license: BSD-3-Clause license-file: LICENSE author: Renzo Carbonara maintainer: renλren.zone copyright: Copyright (c) Renzo Carbonara 2018 category: Network build-type: Simple-cabal-version: >=1.8 extra-source-files: README.md changelog.md  source-repository head   type: git-  location: https://github.com/k0001/network-simple-ws+  location: https://github.com/k0001/network-simple+  subdir: network-simple-ws  library+  default-language:  Haskell2010   hs-source-dirs: src   ghc-options: -Wall -O2   exposed-modules: Network.Simple.WS   build-depends:+    async,     base >=4.7 && <5.0,     bytestring,     case-insensitive,
src/Network/Simple/WS.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -12,9 +14,11 @@ -- intreresting from a client's point of view. Server side support will come -- later. module Network.Simple.WS- ( W.Connection- , send+ ( -- * Sending and receiving+   W.Connection  , recv+ , send+ , close    -- * Client side  , connect  , connectSOCKS5@@ -24,6 +28,8 @@  ) where  +import Control.Concurrent.Async (Async)+import qualified Control.Concurrent.Async as Async import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Control.Exception.Safe as Ex import Data.Bifunctor (first)@@ -32,10 +38,12 @@ import qualified Data.ByteString.Lazy as BL import qualified Data.CaseInsensitive as CI import Data.Foldable (traverse_)-import Data.Function (fix)+import Data.Word+import GHC.IO.Exception as IO  import qualified Network.Simple.TCP as T import qualified Network.WebSockets as W+import qualified Network.WebSockets.Connection as W (pingThread) import qualified Network.WebSockets.Stream as W (Stream, makeStream, close)  --------------------------------------------------------------------------------@@ -57,23 +65,25 @@   -- (e.g., @[(\"Authorization\", \"Basic dXNlcjpwYXNzd29yZA==\")]@).   -> ((W.Connection, T.SockAddr) -> m r)   -- ^ Computation to run after establishing a WebSockets to the remote-  -- server. Takes the WebSockets connection and remote end address.+  -- server. Takes the WebSockets 'W.Connection' and remote end address.   -> m r connect hn sn res hds act = do   T.connect hn sn $ \(sock, saddr) -> do     Ex.bracket (streamFromSocket sock) (liftIO . W.close) $ \stream -> do       conn <- clientConnectionFromStream stream hn sn res hds-      liftIO (W.forkPingThread conn 30)-      act (conn, saddr)+      withAsync (W.pingThread conn 30 (pure ())) $ \_ ->+        act (conn, saddr)  -- | Like 'connect', but connects to the destination server through a SOCKS5 -- proxy. connectSOCKS5   :: (MonadIO m, Ex.MonadMask m)-  => T.HostName -- ^ SOCKS5 proxy server hostname or IP address.-  -> T.ServiceName -- ^ SOCKS5 proxy server service port name or number.+  => T.HostName+  -- ^ SOCKS5 proxy server hostname or IP address.+  -> T.ServiceName+  -- ^ SOCKS5 proxy server service port name or number.   -> T.HostName-  -- ^ Destination WebSocket server hostname or IP address. We connect to this+  -- ^ Destination WebSockets server hostname or IP address. We connect to this   -- host /through/ the SOCKS5 proxy specified in the previous arguments.   --   -- Note that if hostname resolution on this 'T.HostName' is necessary, it@@ -95,20 +105,21 @@  -> m r connectSOCKS5 phn psn dhn dsn res hds act = do   T.connectSOCKS5 phn psn dhn dsn $ \(sock, pa, da) -> do-    liftIO (print (pa, da))     Ex.bracket (streamFromSocket sock) (liftIO . W.close) $ \stream -> do       conn <- clientConnectionFromStream stream dhn dsn res hds-      liftIO (W.forkPingThread conn 30)-      act (conn, pa, da)+      withAsync (W.pingThread conn 30 (pure ())) $ \_ ->+        act (conn, pa, da) --- | Obtain a 'W.Connection' to the specified 'Uri' over the given 'W.Stream',+-- | Obtain a 'W.Connection' to the specified URI over the given 'W.Stream', -- connected to either a WebSockets server, or a Secure WebSockets server. clientConnectionFromStream   :: MonadIO m-  => W.Stream -- ^ Stream on which to establish the WebSockets connection.+  => W.Stream+  -- ^ Stream on which to establish the WebSockets connection.   -> T.HostName   -- ^ WebSockets server host name (e.g., @\"www.example.com\"@ or IP address).-  -> T.ServiceName -- ^ WebSockets server port (e.g., @\"443\"@ or @\"www\"@).+  -> T.ServiceName+  -- ^ WebSockets server port (e.g., @\"443\"@ or @\"www\"@).   -> B.ByteString   -- ^ WebSockets resource (e.g., @\"/foo\/qux?bar=wat&baz\"@).   --@@ -116,7 +127,8 @@   -> [(B.ByteString, B.ByteString)]   -- ^ Extra HTTP Headers   -- (e.g., @[(\"Authorization\", \"Basic dXNlcjpwYXNzd29yZA==\")]@).-  -> m W.Connection -- ^ Established WebSockets connection+  -> m W.Connection+  -- ^ Established WebSockets connection clientConnectionFromStream stream hn sn res hds = liftIO $ do   let res' :: String = '/' : dropWhile (=='/') (B8.unpack res)       hds' :: W.Headers = map (first CI.mk) hds@@ -137,36 +149,92 @@ streamFromSocket sock = liftIO $ do   W.makeStream (T.recv sock 4096) (traverse_ (T.sendLazy sock)) --- | Receive bytes from the remote end.+-- | Receive a single full WebSockets message from the remote end as a lazy+-- 'BL.ByteString' (potentially 'BL.empty'). ----- Returns a strict 'BL.ByteString'.+-- Throws 'IO.IOException' if there is an unexpected 'W.Connection' error. ----- Returns an empty string when the remote end gracefully closes the connection.+-- If the remote end requested the 'W.Connection' to be closed, then 'Left'+-- will be returned instead, with a close code and reason description.+--+-- * See https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 for details+-- about the close codes.+--+-- * Do not use 'recv' after receiving a close request.+--+-- * If you receive a close request after after having sent a close request+-- yourself (see 'close'), then the WebSocket 'W.Connection' is+-- considered closed and you can proceed to close the underlying transport.+--+-- * If you didn't send a close request before, then you may continue to use+-- 'send', but you are expected to perform 'close' as soon as possible in order+-- to indicate a graceful closing of the connection. --- Note: The WebSockets protocol supports the silly idea of sending text, rather--- than bytes, over the socket. We don't support that. If necessary, users can--- find support for this in the `websockets` library.-recv :: MonadIO m => W.Connection -> m B.ByteString-{-# INLINABLE recv #-}-recv c = liftIO $ fix $ \k -> do-  ea <- Ex.try (W.receiveDataMessage c)-  case ea of-     Right (W.Binary bl) -> if BL.null bl then k else pure (BL.toStrict bl)-     Right (W.Text bl _) -> if BL.null bl then k else pure (BL.toStrict bl)-     Left (W.CloseRequest 1000 _) -> pure B.empty-     Left (W.CloseRequest 1001 _) -> pure B.empty-     Left e -> Ex.throw e+-- Note: The WebSockets protocol supports the silly idea of sending text+-- rather than bytes. We don't support that. If necessary, you can find support+-- for this in the `websockets` library.+recv :: MonadIO m+     => W.Connection+     -> m (Either (Word16, BL.ByteString) BL.ByteString)+recv conn = liftIO $ Ex.try (W.receiveDataMessage conn) >>= \case+  Right (W.Binary !bl) -> pure $ Right bl+  Right (W.Text !bl _) -> pure $ Right bl+  Left (W.CloseRequest !w !bl) -> pure $ Left (w, bl)+  Left W.ConnectionClosed ->+    Ex.throw $ ioe "recv" IO.ResourceVanished "Connection closed"+  Left (W.ParseException s) ->+    Ex.throw $ ioe "recv" IO.ProtocolError ("WebSocket parsing error: " <> s)+  Left (W.UnicodeException s) ->+    Ex.throw $ ioe "recv" IO.ProtocolError ("WebSocket UTF-8 error: " <> s) --- | Send bytes to the remote end.+-- | Send a lazy 'BL.ByteString' (potentially 'BL.empty') to the remote end as+-- a single WebSockets message, in potentially multiple frames. ----- Takes a lazy 'BL.ByteString'.+-- If there is an issue with the 'W.Connection', an exception originating from+-- the underlying 'W.Stream' will be thrown. --- Note: The WebSockets protocol supports the silly idea of sending text, rather--- than bytes, over the socket. We don't support that. If necessary, users can+-- Note: The WebSockets protocol supports the silly idea of sending text rather+-- than bytes. We don't support that. If necessary, users can -- find support for this in the `websockets` library. send :: MonadIO m => W.Connection -> BL.ByteString -> m ()-{-# INLINABLE send #-}-send c = \bl -> case BL.null bl of-  True -> pure ()-  False -> liftIO (W.sendDataMessage c (W.Binary bl))+send conn = liftIO . W.sendDataMessage conn . W.Binary +-- | Send a close request to the remote end.+--+-- After sending this request you should not use 'send' anymore, but you+-- should still continue to call 'recv' to process any pending incomming+-- messages. As soon as 'recv' returns 'Left', you can consider the WebSocket+-- 'W.Connection' closed and can proceed to close the underlying transport.+--+-- If there is an issue with the 'W.Connection', an exception originating from+-- the underlying 'W.Stream' will be thrown.+close :: MonadIO m+      => W.Connection+      -> Word16        -- ^ Close code.+      -> BL.ByteString -- ^ Reason for closing.+      -> m ()+close conn w bl = liftIO $ W.sendCloseCode conn w bl++-- | Like 'Async.async', but generalized to 'Ex.MonadMask' and 'MonadIO'.+withAsync+  :: (Ex.MonadMask m, MonadIO m)+  => IO a+  -> (Async a -> m b)+  -> m b+withAsync io = Ex.bracket+  (liftIO $ Async.asyncWithUnmask (\u -> u io))+  (liftIO . Async.uninterruptibleCancel)++-- | Construct a 'IO.IOError' relevant to this module.+ioe :: String  -- ^ Location+    -> IO.IOErrorType+    -> String  -- ^ Description+    -> IO.IOError+ioe l t s = IO.IOError+  { IO.ioe_type = t+  , IO.ioe_location = "Network.Simple.WS." <> l+  , IO.ioe_description = s+  , IO.ioe_errno = Nothing+  , IO.ioe_handle = Nothing+  , IO.ioe_filename = Nothing+  }