{-# LANGUAGE NoImplicitPrelude #-}

{- |
    Wuss is a library that lets you easily create secure WebSocket clients over
    the WSS protocol. It is a small addition to
    <https://hackage.haskell.org/package/websockets the websockets package>
    and is adapted from existing solutions by
    <https://gist.github.com/jaspervdj/7198388 @jaspervdj>,
    <https://gist.github.com/mpickering/f1b7ba3190a4bb5884f3 @mpickering>, and
    <https://gist.github.com/elfenlaid/7b5c28065e67e4cf0767 @elfenlaid>.

    == Example

    > import Wuss
    >
    > import Control.Concurrent (forkIO)
    > import Control.Monad (forever, unless, void)
    > import Data.Text (Text, pack)
    > import Network.WebSockets (ClientApp, receiveData, sendClose, sendTextData)
    >
    > main :: IO ()
    > main = runSecureClient "echo.websocket.org" 443 "/" ws
    >
    > ws :: ClientApp ()
    > ws connection = do
    >     putStrLn "Connected!"
    >
    >     void . forkIO . forever $ do
    >         message <- receiveData connection
    >         print (message :: Text)
    >
    >     let loop = do
    >             line <- getLine
    >             unless (null line) $ do
    >                 sendTextData connection (pack line)
    >                 loop
    >     loop
    >
    >     sendClose connection (pack "Bye!")
-}
module Wuss
  ( runSecureClient
  , runSecureClientWith
  , Config(..)
  , defaultConfig
  , runSecureClientWithConfig
  ) where

import qualified Control.Applicative as Applicative
import qualified Control.Exception as Exception
import qualified Data.Bool as Bool
import qualified Data.ByteString as StrictBytes
import qualified Data.ByteString.Lazy as LazyBytes
import qualified Data.Maybe as Maybe
import qualified Data.String as String
import qualified Network.Connection as Connection
import qualified Network.Socket as Socket
import qualified Network.WebSockets as WebSockets
import qualified Network.WebSockets.Stream as Stream
import qualified System.IO as IO
import qualified System.IO.Error as IO.Error


{- |
    A secure replacement for 'Network.WebSockets.runClient'.

    >>> let app _connection = return ()
    >>> runSecureClient "echo.websocket.org" 443 "/" app
-}
runSecureClient
  :: Socket.HostName -- ^ Host
  -> Socket.PortNumber -- ^ Port
  -> String.String -- ^ Path
  -> WebSockets.ClientApp a -- ^ Application
  -> IO.IO a
runSecureClient :: HostName -> PortNumber -> HostName -> ClientApp a -> IO a
runSecureClient HostName
host PortNumber
port HostName
path ClientApp a
app = do
  let options :: ConnectionOptions
options = ConnectionOptions
WebSockets.defaultConnectionOptions
  HostName
-> PortNumber
-> HostName
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
forall a.
HostName
-> PortNumber
-> HostName
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
runSecureClientWith HostName
host PortNumber
port HostName
path ConnectionOptions
options [] ClientApp a
app


{- |
    A secure replacement for 'Network.WebSockets.runClientWith'.

    >>> let options = defaultConnectionOptions
    >>> let headers = []
    >>> let app _connection = return ()
    >>> runSecureClientWith "echo.websocket.org" 443 "/" options headers app

    If you want to run a secure client without certificate validation, use
    'Network.WebSockets.runClientWithStream'. For example:

    > let host = "echo.websocket.org"
    > let port = 443
    > let path = "/"
    > let options = defaultConnectionOptions
    > let headers = []
    > let tlsSettings = TLSSettingsSimple
    >     -- This is the important setting.
    >     { settingDisableCertificateValidation = True
    >     , settingDisableSession = False
    >     , settingUseServerName = False
    >     }
    > let connectionParams = ConnectionParams
    >     { connectionHostname = host
    >     , connectionPort = port
    >     , connectionUseSecure = Just tlsSettings
    >     , connectionUseSocks = Nothing
    >     }
    >
    > context <- initConnectionContext
    > connection <- connectTo context connectionParams
    > stream <- makeStream
    >     (fmap Just (connectionGetChunk connection))
    >     (maybe (return ()) (connectionPut connection . toStrict))
    > runClientWithStream stream host path options headers $ \ connection -> do
    >     -- Do something with the connection.
    >     return ()
-}
runSecureClientWith
  :: Socket.HostName -- ^ Host
  -> Socket.PortNumber -- ^ Port
  -> String.String -- ^ Path
  -> WebSockets.ConnectionOptions -- ^ Options
  -> WebSockets.Headers -- ^ Headers
  -> WebSockets.ClientApp a -- ^ Application
  -> IO.IO a
runSecureClientWith :: HostName
-> PortNumber
-> HostName
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
runSecureClientWith HostName
host PortNumber
port HostName
path ConnectionOptions
options Headers
headers ClientApp a
app = do
  let config :: Config
config = Config
defaultConfig
  HostName
-> PortNumber
-> HostName
-> Config
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
forall a.
HostName
-> PortNumber
-> HostName
-> Config
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
runSecureClientWithConfig HostName
host PortNumber
port HostName
path Config
config ConnectionOptions
options Headers
headers ClientApp a
app


-- | Configures a secure WebSocket connection.
newtype Config = Config
    { Config -> Connection -> IO ByteString
connectionGet :: Connection.Connection -> IO.IO StrictBytes.ByteString
    -- ^ How to get bytes from the connection. Typically
    -- 'Connection.connectionGetChunk', but could be something else like
    -- 'Connection.connectionGetLine'.
    }


-- | The default 'Config' value used by 'runSecureClientWith'.
defaultConfig :: Config
defaultConfig :: Config
defaultConfig = do
  Config :: (Connection -> IO ByteString) -> Config
Config { connectionGet :: Connection -> IO ByteString
connectionGet = Connection -> IO ByteString
Connection.connectionGetChunk }


-- | Runs a secure WebSockets client with the given 'Config'.
runSecureClientWithConfig
  :: Socket.HostName -- ^ Host
  -> Socket.PortNumber -- ^ Port
  -> String.String -- ^ Path
  -> Config -- ^ Config
  -> WebSockets.ConnectionOptions -- ^ Options
  -> WebSockets.Headers -- ^ Headers
  -> WebSockets.ClientApp a -- ^ Application
  -> IO.IO a
runSecureClientWithConfig :: HostName
-> PortNumber
-> HostName
-> Config
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
runSecureClientWithConfig HostName
host PortNumber
port HostName
path Config
config ConnectionOptions
options Headers
headers ClientApp a
app = do
  ConnectionContext
context <- IO ConnectionContext
Connection.initConnectionContext
  IO Connection
-> (Connection -> IO ()) -> (Connection -> IO a) -> IO a
forall a b c. IO a -> (a -> IO b) -> (a -> IO c) -> IO c
Exception.bracket
    (ConnectionContext -> ConnectionParams -> IO Connection
Connection.connectTo ConnectionContext
context (HostName -> PortNumber -> ConnectionParams
connectionParams HostName
host PortNumber
port))
    Connection -> IO ()
Connection.connectionClose
    (\Connection
connection -> do
      Stream
stream <- IO (Maybe ByteString) -> (Maybe ByteString -> IO ()) -> IO Stream
Stream.makeStream
        (Config -> Connection -> IO (Maybe ByteString)
reader Config
config Connection
connection)
        (Connection -> Maybe ByteString -> IO ()
writer Connection
connection)
      Stream
-> HostName
-> HostName
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
forall a.
Stream
-> HostName
-> HostName
-> ConnectionOptions
-> Headers
-> ClientApp a
-> IO a
WebSockets.runClientWithStream Stream
stream HostName
host HostName
path ConnectionOptions
options Headers
headers ClientApp a
app
    )


connectionParams
  :: Socket.HostName -> Socket.PortNumber -> Connection.ConnectionParams
connectionParams :: HostName -> PortNumber -> ConnectionParams
connectionParams HostName
host PortNumber
port = do
  ConnectionParams :: HostName
-> PortNumber
-> Maybe TLSSettings
-> Maybe ProxySettings
-> ConnectionParams
Connection.ConnectionParams
    { connectionHostname :: HostName
Connection.connectionHostname = HostName
host
    , connectionPort :: PortNumber
Connection.connectionPort = PortNumber
port
    , connectionUseSecure :: Maybe TLSSettings
Connection.connectionUseSecure = TLSSettings -> Maybe TLSSettings
forall a. a -> Maybe a
Maybe.Just TLSSettings
tlsSettings
    , connectionUseSocks :: Maybe ProxySettings
Connection.connectionUseSocks = Maybe ProxySettings
forall a. Maybe a
Maybe.Nothing
    }


tlsSettings :: Connection.TLSSettings
tlsSettings :: TLSSettings
tlsSettings = do
  TLSSettingsSimple :: Bool -> Bool -> Bool -> TLSSettings
Connection.TLSSettingsSimple
    { settingDisableCertificateValidation :: Bool
Connection.settingDisableCertificateValidation = Bool
Bool.False
    , settingDisableSession :: Bool
Connection.settingDisableSession = Bool
Bool.False
    , settingUseServerName :: Bool
Connection.settingUseServerName = Bool
Bool.False
    }


reader
  :: Config
  -> Connection.Connection
  -> IO.IO (Maybe.Maybe StrictBytes.ByteString)
reader :: Config -> Connection -> IO (Maybe ByteString)
reader Config
config Connection
connection = IO (Maybe ByteString)
-> (IOError -> IO (Maybe ByteString)) -> IO (Maybe ByteString)
forall a. IO a -> (IOError -> IO a) -> IO a
IO.Error.catchIOError
  (do
    ByteString
chunk <- Config -> Connection -> IO ByteString
connectionGet Config
config Connection
connection
    Maybe ByteString -> IO (Maybe ByteString)
forall (f :: * -> *) a. Applicative f => a -> f a
Applicative.pure (ByteString -> Maybe ByteString
forall a. a -> Maybe a
Maybe.Just ByteString
chunk)
  )
  (\IOError
e -> if IOError -> Bool
IO.Error.isEOFError IOError
e
    then Maybe ByteString -> IO (Maybe ByteString)
forall (f :: * -> *) a. Applicative f => a -> f a
Applicative.pure Maybe ByteString
forall a. Maybe a
Maybe.Nothing
    else IOError -> IO (Maybe ByteString)
forall e a. Exception e => e -> IO a
Exception.throwIO IOError
e
  )


writer
  :: Connection.Connection -> Maybe.Maybe LazyBytes.ByteString -> IO.IO ()
writer :: Connection -> Maybe ByteString -> IO ()
writer Connection
connection Maybe ByteString
maybeBytes = do
  case Maybe ByteString
maybeBytes of
    Maybe ByteString
Maybe.Nothing -> do
      () -> IO ()
forall (f :: * -> *) a. Applicative f => a -> f a
Applicative.pure ()
    Maybe.Just ByteString
bytes -> do
      Connection -> ByteString -> IO ()
Connection.connectionPut Connection
connection (ByteString -> ByteString
LazyBytes.toStrict ByteString
bytes)