openssl-streams-1.2.0.0: src/System/IO/Streams/SSL.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- | This module provides convenience functions for interfacing @io-streams@
-- with @HsOpenSSL@. It is intended to be imported @qualified@, e.g.:
--
-- @
-- import qualified "OpenSSL" as SSL
-- import qualified "OpenSSL.Session" as SSL
-- import qualified "System.IO.Streams.SSL" as SSLStreams
--
-- \ example :: IO ('InputStream' 'ByteString', 'OutputStream' 'ByteString')
-- example = SSL.'SSL.withOpenSSL' $ do
-- ctx <- SSL.'SSL.context'
-- SSL.'SSL.contextSetDefaultCiphers' ctx
--
-- \ \-\- Note: the location of the system certificates is system-dependent,
-- \-\- on Linux systems this is usually \"\/etc\/ssl\/certs\". This
-- \-\- step is optional if you choose to disable certificate verification
-- \-\- (not recommended!).
-- SSL.'SSL.contextSetCADirectory' ctx \"\/etc\/ssl\/certs\"
-- SSL.'SSL.contextSetVerificationMode' ctx $
-- SSL.'SSL.VerifyPeer' True True Nothing
-- SSLStreams.'connect' ctx "foo.com" 4444
-- @
--
module System.IO.Streams.SSL
( connect
, withConnection
, sslToStreams
) where
import qualified Control.Exception as E
import Control.Monad (void)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import Network.Socket (HostName, PortNumber)
import qualified Network.Socket as N
import OpenSSL.Session (SSL, SSLContext)
import qualified OpenSSL.Session as SSL
import System.IO.Streams (InputStream, OutputStream)
import qualified System.IO.Streams as Streams
------------------------------------------------------------------------------
bUFSIZ :: Int
bUFSIZ = 32752
------------------------------------------------------------------------------
-- | Given an existing HsOpenSSL 'SSL' connection, produces an 'InputStream' \/
-- 'OutputStream' pair.
sslToStreams :: SSL -- ^ SSL connection object
-> IO (InputStream ByteString, OutputStream ByteString)
sslToStreams ssl = do
is <- Streams.makeInputStream input
os <- Streams.makeOutputStream output
return $! (is, os)
where
input = do
s <- SSL.read ssl bUFSIZ
return $! if S.null s then Nothing else Just s
output Nothing = return $! ()
output (Just s) = SSL.write ssl s
------------------------------------------------------------------------------
-- | Convenience function for initiating an SSL connection to the given
-- @('HostName', 'PortNumber')@ combination.
--
-- Note that sending an end-of-file to the returned 'OutputStream' will not
-- close the underlying SSL connection; to do that, call:
--
-- @
-- SSL.'SSL.shutdown' ssl SSL.'SSL.Unidirectional'
-- maybe (return ()) 'N.close' $ SSL.'SSL.sslSocket' ssl
-- @
--
-- on the returned 'SSL' object.
connect :: SSLContext -- ^ SSL context. See the @HsOpenSSL@
-- documentation for information on creating
-- this.
-> HostName -- ^ hostname to connect to
-> PortNumber -- ^ port number to connect to
-> IO (InputStream ByteString, OutputStream ByteString, SSL)
connect ctx host port = do
-- Partial function here OK, network will throw an exception rather than
-- return the empty list here.
(addrInfo:_) <- N.getAddrInfo (Just hints) (Just host) (Just $ show port)
let family = N.addrFamily addrInfo
let socketType = N.addrSocketType addrInfo
let protocol = N.addrProtocol addrInfo
let address = N.addrAddress addrInfo
E.bracketOnError (N.socket family socketType protocol)
N.close
(\sock -> do N.connect sock address
ssl <- SSL.connection ctx sock
SSL.connect ssl
(is, os) <- sslToStreams ssl
return $! (is, os, ssl)
)
where
hints = N.defaultHints {
N.addrFlags = [N.AI_ADDRCONFIG, N.AI_NUMERICSERV]
, N.addrSocketType = N.Stream
}
------------------------------------------------------------------------------
-- | Convenience function for initiating an SSL connection to the given
-- @('HostName', 'PortNumber')@ combination. The socket and SSL connection are
-- closed and deleted after the user handler runs.
--
-- /Since: 1.2.0.0./
withConnection ::
SSLContext -- ^ SSL context. See the @HsOpenSSL@
-- documentation for information on creating
-- this.
-> HostName -- ^ hostname to connect to
-> PortNumber -- ^ port number to connect to
-> (InputStream ByteString -> OutputStream ByteString -> SSL -> IO a)
-- ^ Action to run with the new connection
-> IO a
withConnection ctx host port action = do
(addrInfo:_) <- N.getAddrInfo (Just hints) (Just host) (Just $ show port)
E.bracket (connectTo addrInfo) cleanup go
where
go (is, os, ssl, _) = action is os ssl
connectTo addrInfo = do
let family = N.addrFamily addrInfo
let socketType = N.addrSocketType addrInfo
let protocol = N.addrProtocol addrInfo
let address = N.addrAddress addrInfo
E.bracketOnError (N.socket family socketType protocol)
N.close
(\sock -> do N.connect sock address
ssl <- SSL.connection ctx sock
SSL.connect ssl
(is, os) <- sslToStreams ssl
return $! (is, os, ssl, sock))
cleanup (_, os, ssl, sock) = E.mask_ $ do
eatException $! Streams.write Nothing os
eatException $! SSL.shutdown ssl $! SSL.Unidirectional
eatException $! N.close sock
hints = N.defaultHints {
N.addrFlags = [N.AI_ADDRCONFIG, N.AI_NUMERICSERV]
, N.addrSocketType = N.Stream
}
eatException m = void m `E.catch` (\(_::E.SomeException) -> return $! ())