rustls-0.2.1.0: src/Rustls.hs
-- | TLS bindings for [Rustls](https://github.com/rustls/rustls) via
-- [rustls-ffi](https://github.com/rustls/rustls-ffi).
--
-- See the [README on GitHub](https://github.com/amesgen/hs-rustls/tree/main/rustls)
-- for setup instructions.
--
-- Currently, most of the functionality exposed by rustls-ffi is available,
-- while rustls-ffi is still missing some more niche Rustls features.
--
-- Also see [http-client-rustls](https://hackage.haskell.org/package/http-client-rustls)
-- for making HTTPS requests using
-- [http-client](https://hackage.haskell.org/package/http-client) and Rustls.
--
-- == Client example
--
-- Suppose you have already opened a 'Network.Socket.Socket' to @example.org@,
-- port 443 (see e.g. the examples at "Network.Socket"). This small example
-- showcases how to perform a simple HTTP GET request:
--
-- >>> :set -XOverloadedStrings
-- >>> import qualified Rustls
-- >>> import Network.Socket (Socket)
-- >>> import Data.Acquire (withAcquire)
-- >>> :{
-- example :: Socket -> IO ()
-- example socket = do
-- -- It is encouraged to share a single `clientConfig` when creating multiple
-- -- TLS connections.
-- clientConfig <-
-- Rustls.buildClientConfig =<< Rustls.defaultClientConfigBuilder
-- let backend = Rustls.mkSocketBackend socket
-- newConnection =
-- Rustls.newClientConnection backend clientConfig "example.org"
-- withAcquire newConnection $ \conn -> do
-- Rustls.writeBS conn "GET /"
-- recv <- Rustls.readBS conn 1000 -- max number of bytes to read
-- print recv
-- :}
--
-- == Using 'Acquire'
--
-- Some API functions (like 'newClientConnection' and 'newServerConnection')
-- return an 'Acquire' from
-- [resourcet](https://hackage.haskell.org/package/resourcet), as it is a
-- convenient abstraction for exposing a value that should be consumed in a
-- "bracketed" manner.
--
-- Usually, it can be used via 'Data.Acquire.with' or 'withAcquire', or via
-- 'allocateAcquire' when a 'Control.Monad.Trans.Resource.MonadResource'
-- constraint is available. If you really need the extra flexibility, you can
-- also access separate @open…@ and @close…@ functions by reaching for
-- "Data.Acquire.Internal".
module Rustls
( -- * Client
-- ** Builder
ClientConfigBuilder (..),
defaultClientConfigBuilder,
ServerCertVerifier (..),
-- ** Config
ClientConfig,
clientConfigLogCallback,
buildClientConfig,
-- ** Open a connection
newClientConnection,
-- * Server
-- ** Builder
ServerConfigBuilder (..),
defaultServerConfigBuilder,
ClientCertVerifier (..),
ClientCertVerifierPolicy (..),
-- ** Config
ServerConfig,
serverConfigLogCallback,
buildServerConfig,
-- ** Open a connection
newServerConnection,
-- * Connection
Connection,
Side (..),
-- ** Read and write
readBS,
writeBS,
-- ** Handshaking
handshake,
HandshakeQuery,
getALPNProtocol,
getTLSVersion,
getNegotiatedCipherSuite,
getSNIHostname,
getPeerCertificate,
-- ** Closing
sendCloseNotify,
-- ** Logging
LogCallback,
newLogCallback,
LogLevel (..),
-- ** Raw 'Ptr'-based API
readPtr,
writePtr,
-- * Misc
version,
-- ** Backend
Backend (..),
mkSocketBackend,
mkByteStringBackend,
-- ** Crypto provider
CryptoProvider,
getDefaultCryptoProvider,
setCryptoProviderCipherSuites,
cryptoProviderCipherSuites,
cryptoProviderTLSVersions,
-- ** Types
ALPNProtocol (..),
PEMCertificates (..),
PEMCertificateParsing (..),
CertifiedKey (..),
DERCertificate (..),
CertificateRevocationList (..),
TLSVersion (TLS12, TLS13, unTLSVersion),
CipherSuite (..),
NegotiatedCipherSuite (..),
-- ** Exceptions
RustlsException,
isCertError,
RustlsLogException (..),
)
where
import Control.Concurrent (forkFinally, killThread)
import Control.Concurrent.MVar
import Control.Exception qualified as E
import Control.Monad (forever, void, when)
import Control.Monad.Except (MonadError (..), liftEither)
import Control.Monad.IO.Class
import Control.Monad.Trans.Cont
import Control.Monad.Trans.Reader
import Data.Acquire
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.ByteString.Internal qualified as BI
import Data.ByteString.Unsafe qualified as BU
import Data.Coerce
import Data.Containers.ListUtils (nubOrd)
import Data.Foldable (for_, toList)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Foreign qualified as T
import Data.Traversable (for)
import Data.Word
import Foreign hiding (void)
import Foreign.C
import GHC.Conc (reportError)
import GHC.Generics (Generic)
import Rustls.Internal
import Rustls.Internal.FFI (ConstPtr (..), TLSVersion (..))
import Rustls.Internal.FFI qualified as FFI
import System.IO.Unsafe (unsafePerformIO)
-- $setup
-- >>> import Control.Monad.IO.Class
-- >>> import Data.Acquire
-- | Combined version string of Rustls and rustls-ffi, as well as the Rustls
-- cryptography provider.
--
-- >>> version
-- "rustls-ffi/0.14.1/rustls/0.23.18/aws-lc-rs"
version :: Text
version = unsafePerformIO $ alloca \strPtr -> do
FFI.hsVersion strPtr
strToText =<< peek strPtr
{-# NOINLINE version #-}
buildCryptoProvider :: Ptr FFI.CryptoProviderBuilder -> IO CryptoProvider
buildCryptoProvider builder = alloca \cryptoProviderPtr -> do
rethrowR =<< FFI.cryptoProviderBuilderBuild builder cryptoProviderPtr
ConstPtr cryptoProviderPtr <- peek cryptoProviderPtr
CryptoProvider <$> newForeignPtr FFI.cryptoProviderFree cryptoProviderPtr
-- | Get the process-wide default Rustls cryptography provider.
getDefaultCryptoProvider :: (MonadIO m) => m CryptoProvider
getDefaultCryptoProvider = liftIO $ E.mask_ $ evalContT do
builderPtr <- ContT alloca
builder <-
ContT $
E.bracketOnError
do
-- This actually also sets the process-wide default crypto provider if
-- not already set, which is a side effect.
rethrowR =<< FFI.cryptoProviderBuilderNewFromDefault builderPtr
peek builderPtr
FFI.cryptoProviderBuilderFree
liftIO $ buildCryptoProvider builder
-- | Create a derived 'CryptoProvider' by restricting the cipher suites to the
-- ones in the given list.
setCryptoProviderCipherSuites ::
(MonadError RustlsException m) =>
-- | Must be a subset of 'cryptoProviderCipherSuites'. Only the
-- 'cipherSuiteID' is used.
[CipherSuite] ->
CryptoProvider ->
m CryptoProvider
setCryptoProviderCipherSuites cipherSuites cryptoProvider =
liftEither $ unsafePerformIO $ E.try $ E.mask_ $ evalContT do
cryptoProviderPtr <- withCryptoProvider cryptoProvider
builder <-
ContT $
E.bracketOnError
(FFI.cryptoProviderBuilderNewWithBase cryptoProviderPtr)
FFI.cryptoProviderBuilderFree
let filteredCipherSuites =
[ cipherSuitePtr
| i <- [1 .. len],
let cipherSuitePtr =
FFI.cryptoProviderCiphersuitesGet cryptoProviderPtr (i - 1)
cipherSuiteID = FFI.supportedCipherSuiteGetSuite cipherSuitePtr,
cipherSuiteID `Set.member` cipherSuiteIDs
]
where
len = FFI.cryptoProviderCiphersuitesLen cryptoProviderPtr
cipherSuiteIDs = Set.fromList $ cipherSuiteID <$> cipherSuites
(csPtr, csLen) <- ContT \cb -> withArrayLen filteredCipherSuites \len ptr ->
cb (ConstPtr ptr, intToCSize len)
liftIO $ rethrowR =<< FFI.cryptoProviderBuilderSetCipherSuites builder csPtr csLen
liftIO $ buildCryptoProvider builder
withCryptoProvider :: CryptoProvider -> ContT a IO (ConstPtr FFI.CryptoProvider)
withCryptoProvider =
fmap ConstPtr . ContT . withForeignPtr . unCryptoProvider
-- | Get the cipher suites supported by the given cryptography provider.
cryptoProviderCipherSuites :: CryptoProvider -> [CipherSuite]
cryptoProviderCipherSuites cryptoProvider = unsafePerformIO $ evalContT do
cryptoProviderPtr <- withCryptoProvider cryptoProvider
liftIO do
let len = FFI.cryptoProviderCiphersuitesLen cryptoProviderPtr
for [1 .. len] \i -> do
let cipherSuitePtr = FFI.cryptoProviderCiphersuitesGet cryptoProviderPtr (i - 1)
cipherSuiteID = FFI.supportedCipherSuiteGetSuite cipherSuitePtr
cipherSuiteName <-
alloca \strPtr -> do
FFI.hsSupportedCipherSuiteGetName cipherSuitePtr strPtr
strToText =<< peek strPtr
cipherSuiteTLSVersion <-
FFI.hsSupportedCiphersuiteProtocolVersion cipherSuitePtr
pure CipherSuite {..}
-- | Get all TLS versions supported by at least one of the cipher suites
-- supported by the given cryptography provider.
cryptoProviderTLSVersions :: CryptoProvider -> [TLSVersion]
cryptoProviderTLSVersions =
nubOrd
. fmap cipherSuiteTLSVersion
. cryptoProviderCipherSuites
-- | A 'ClientConfigBuilder' with good defaults, using the OS certificate store.
defaultClientConfigBuilder :: (MonadIO m) => m ClientConfigBuilder
defaultClientConfigBuilder = do
cryptoProvider <- getDefaultCryptoProvider
pure
ClientConfigBuilder
{ clientConfigCryptoProvider = cryptoProvider,
clientConfigServerCertVerifier = PlatformServerCertVerifier,
clientConfigALPNProtocols = [],
clientConfigEnableSNI = True,
clientConfigCertifiedKeys = []
}
withCertifiedKeys :: [CertifiedKey] -> ContT a IO (ConstPtr (ConstPtr FFI.CertifiedKey), CSize)
withCertifiedKeys certifiedKeys = do
certKeys <- for certifiedKeys withCertifiedKey
ContT \cb -> withArrayLen certKeys \len ptr -> cb (ConstPtr ptr, intToCSize len)
where
withCertifiedKey CertifiedKey {..} = do
(certPtr, certLen) <- ContT $ BU.unsafeUseAsCStringLen certificateChain
(privPtr, privLen) <- ContT $ BU.unsafeUseAsCStringLen privateKey
certKeyPtr <- ContT alloca
liftIO do
rethrowR
=<< FFI.certifiedKeyBuild
(ConstPtr $ castPtr certPtr)
(intToCSize certLen)
(ConstPtr $ castPtr privPtr)
(intToCSize privLen)
certKeyPtr
peek certKeyPtr
withALPNProtocols :: [ALPNProtocol] -> ContT a IO (ConstPtr FFI.SliceBytes, CSize)
withALPNProtocols bss = do
bsPtrs <- for (coerce bss) withSliceBytes
ContT \cb -> withArrayLen bsPtrs \len bsPtr -> cb (ConstPtr bsPtr, intToCSize len)
where
withSliceBytes bs = do
(buf, len) <- ContT $ BU.unsafeUseAsCStringLen bs
pure $ FFI.SliceBytes (castPtr buf) (intToCSize len)
configBuilderNew ::
( ConstPtr FFI.CryptoProvider ->
ConstPtr TLSVersion ->
CSize ->
Ptr (Ptr configBuilder) ->
IO FFI.Result
) ->
CryptoProvider ->
IO (Ptr configBuilder)
configBuilderNew configBuilderNewCustom cryptoProvider = evalContT do
cryptoProviderPtr <- withCryptoProvider cryptoProvider
builderPtr <- ContT alloca
(tlsVersionsLen, tlsVersionsPtr) <-
ContT \cb -> withArrayLen (cryptoProviderTLSVersions cryptoProvider) \len ptr ->
cb (intToCSize len, ConstPtr ptr)
liftIO do
rethrowR
=<< configBuilderNewCustom
cryptoProviderPtr
tlsVersionsPtr
tlsVersionsLen
builderPtr
peek builderPtr
withRootCertStore :: [PEMCertificates] -> ContT a IO (ConstPtr FFI.RootCertStore)
withRootCertStore certs = do
storeBuilder <-
ContT $ E.bracket FFI.rootCertStoreBuilderNew FFI.rootCertStoreBuilderFree
let isStrict :: PEMCertificateParsing -> CBool
isStrict =
fromBool @CBool . \case
PEMCertificateParsingStrict -> True
PEMCertificateParsingLax -> False
for_ certs \case
PEMCertificatesInMemory bs parsing -> do
(buf, len) <- ContT $ BU.unsafeUseAsCStringLen bs
liftIO $
rethrowR
=<< FFI.rootCertStoreBuilderAddPem
storeBuilder
(ConstPtr $ castPtr buf)
(intToCSize len)
(isStrict parsing)
PemCertificatesFromFile path parsing -> do
pathPtr <- ContT $ withCString path
liftIO $
rethrowR
=<< FFI.rootCertStoreBuilderLoadRootsFromFile
storeBuilder
(ConstPtr pathPtr)
(isStrict parsing)
storePtr <- ContT alloca
let buildRootCertStore = do
liftIO $ rethrowR =<< FFI.rootCertStoreBuilderBuild storeBuilder storePtr
peek storePtr
ContT $ E.bracket buildRootCertStore FFI.rootCertStoreFree
-- | Build a 'ClientConfigBuilder' into a 'ClientConfig'.
--
-- This is a relatively expensive operation, so it is a good idea to share one
-- 'ClientConfig' when creating multiple 'Connection's.
buildClientConfig :: (MonadIO m) => ClientConfigBuilder -> m ClientConfig
buildClientConfig ClientConfigBuilder {..} = liftIO . E.mask_ $ evalContT do
builder <-
ContT $
E.bracketOnError
( configBuilderNew
FFI.clientConfigBuilderNewCustom
clientConfigCryptoProvider
)
FFI.clientConfigBuilderFree
cryptoProviderPtr <- withCryptoProvider clientConfigCryptoProvider
scv <- case clientConfigServerCertVerifier of
PlatformServerCertVerifier ->
withCryptoProvider clientConfigCryptoProvider
>>= liftIO . FFI.platformServerCertVerifierWithProvider
ServerCertVerifier {..} -> do
rootCertStore <- withRootCertStore $ toList serverCertVerifierCertificates
scvb <-
ContT $
E.bracket
(FFI.webPkiServerCertVerifierBuilderNewWithProvider cryptoProviderPtr rootCertStore)
FFI.webPkiServerCertVerifierBuilderFree
crls :: [CStringLen] <-
for serverCertVerifierCRLs $
ContT . BU.unsafeUseAsCStringLen . unCertificateRevocationList
liftIO $ for_ crls \(ptr, len) ->
FFI.webPkiServerCertVerifierBuilderAddCrl
scvb
(ConstPtr (castPtr ptr))
(intToCSize len)
scvPtr <- ContT alloca
let buildScv = do
rethrowR =<< FFI.webPkiServerCertVerifierBuilderBuild scvb scvPtr
peek scvPtr
ContT $ E.bracket buildScv FFI.serverCertVerifierFree
liftIO $ FFI.clientConfigBuilderSetServerVerifier builder (ConstPtr scv)
(alpnPtr, len) <- withALPNProtocols clientConfigALPNProtocols
liftIO $ rethrowR =<< FFI.clientConfigBuilderSetALPNProtocols builder alpnPtr len
liftIO $
FFI.clientConfigBuilderSetEnableSNI builder (fromBool @CBool clientConfigEnableSNI)
(ptr, len) <- withCertifiedKeys clientConfigCertifiedKeys
liftIO $ rethrowR =<< FFI.clientConfigBuilderSetCertifiedKey builder ptr len
let clientConfigLogCallback = Nothing
clientConfigPtrPtr <- ContT alloca
liftIO do
rethrowR =<< FFI.clientConfigBuilderBuild builder clientConfigPtrPtr
clientConfigPtr <-
newForeignPtr FFI.clientConfigFree . unConstPtr
=<< peek clientConfigPtrPtr
pure ClientConfig {..}
-- | Build a 'ServerConfigBuilder' into a 'ServerConfig'.
--
-- This is a relatively expensive operation, so it is a good idea to share one
-- 'ServerConfig' when creating multiple 'Connection's.
buildServerConfig :: (MonadIO m) => ServerConfigBuilder -> m ServerConfig
buildServerConfig ServerConfigBuilder {..} = liftIO . E.mask_ $ evalContT do
builder <-
ContT $
E.bracketOnError
( configBuilderNew
FFI.serverConfigBuilderNewCustom
serverConfigCryptoProvider
)
FFI.serverConfigBuilderFree
cryptoProviderPtr <- withCryptoProvider serverConfigCryptoProvider
(alpnPtr, len) <- withALPNProtocols serverConfigALPNProtocols
liftIO $ rethrowR =<< FFI.serverConfigBuilderSetALPNProtocols builder alpnPtr len
liftIO $
rethrowR
=<< FFI.serverConfigBuilderSetIgnoreClientOrder
builder
(fromBool @CBool serverConfigIgnoreClientOrder)
(ptr, len) <- withCertifiedKeys (NE.toList serverConfigCertifiedKeys)
liftIO $ rethrowR =<< FFI.serverConfigBuilderSetCertifiedKeys builder ptr len
for_ serverConfigClientCertVerifier \ClientCertVerifier {..} -> do
roots <- withRootCertStore $ NE.toList clientCertVerifierCertificates
ccvb <-
ContT $
E.bracket
(FFI.webPkiClientCertVerifierBuilderNewWithProvider cryptoProviderPtr roots)
FFI.webPkiClientCertVerifierBuilderFree
crls :: [CStringLen] <-
for clientCertVerifierCRLs $
ContT . BU.unsafeUseAsCStringLen . unCertificateRevocationList
liftIO do
case clientCertVerifierPolicy of
AllowAnyAuthenticatedClient -> pure ()
AllowAnyAnonymousOrAuthenticatedClient ->
rethrowR =<< FFI.webPkiClientCertVerifierBuilderAllowUnauthenticated ccvb
for_ crls \(ptr, len) ->
FFI.webPkiClientCertVerifierBuilderAddCrl
ccvb
(ConstPtr (castPtr ptr))
(intToCSize len)
ccvPtr <- ContT alloca
let buildCcv = do
rethrowR =<< FFI.webPkiClientCertVerifierBuilderBuild ccvb ccvPtr
peek ccvPtr
ccv <- ContT $ E.bracket buildCcv FFI.clientCertVerifierFree
liftIO $ FFI.serverConfigBuilderSetClientVerifier builder (ConstPtr ccv)
serverConfigPtrPtr <- ContT alloca
liftIO do
rethrowR =<< FFI.serverConfigBuilderBuild builder serverConfigPtrPtr
serverConfigPtr <-
newForeignPtr FFI.serverConfigFree . unConstPtr
=<< peek serverConfigPtrPtr
let serverConfigLogCallback = Nothing
pure ServerConfig {..}
-- | A 'ServerConfigBuilder' with good defaults.
defaultServerConfigBuilder ::
(MonadIO m) => NonEmpty CertifiedKey -> m ServerConfigBuilder
defaultServerConfigBuilder certifiedKeys = do
cryptoProvider <- getDefaultCryptoProvider
pure
ServerConfigBuilder
{ serverConfigCryptoProvider = cryptoProvider,
serverConfigCertifiedKeys = certifiedKeys,
serverConfigALPNProtocols = [],
serverConfigIgnoreClientOrder = False,
serverConfigClientCertVerifier = Nothing
}
-- | Allocate a new logging callback, taking a 'LogLevel' and a message.
--
-- If it throws an exception, it will be wrapped in a 'RustlsLogException' and
-- passed to 'reportError'.
--
-- 🚫 Make sure that its lifetime encloses those of the 'Connection's which you
-- configured to use it.
newLogCallback :: (LogLevel -> Text -> IO ()) -> Acquire LogCallback
newLogCallback cb = fmap LogCallback . flip mkAcquire freeHaskellFunPtr $
FFI.mkLogCallback \_ (ConstPtr logParamsPtr) -> ignoreExceptions do
FFI.LogParams {..} <- peek logParamsPtr
let logLevel = case rustlsLogParamsLevel of
FFI.LogLevel 1 -> Right LogLevelError
FFI.LogLevel 2 -> Right LogLevelWarn
FFI.LogLevel 3 -> Right LogLevelInfo
FFI.LogLevel 4 -> Right LogLevelDebug
FFI.LogLevel 5 -> Right LogLevelTrace
l -> Left l
case logLevel of
Left l -> report $ E.SomeException $ RustlsUnknownLogLevel l
Right logLevel -> do
msg <- strToText rustlsLogParamsMessage
cb logLevel msg `E.catch` report
where
report = reportError . E.SomeException . RustlsLogException
newConnection ::
Backend ->
ForeignPtr config ->
Maybe LogCallback ->
(ConstPtr config -> Ptr (Ptr FFI.Connection) -> IO FFI.Result) ->
Acquire (Connection side)
newConnection backend configPtr logCallback connectionNew =
mkAcquire acquire release
where
acquire = do
conn <-
alloca \connPtrPtr ->
withForeignPtr configPtr \cfgPtr -> liftIO do
rethrowR =<< connectionNew (ConstPtr cfgPtr) connPtrPtr
peek connPtrPtr
ioMsgReq <- newEmptyMVar
ioMsgRes <- newEmptyMVar
lenPtr <- malloc
let readWriteCallback toBuf _ud buf len iPtr = do
putMVar ioMsgRes $ UsingBuffer (toBuf buf) len iPtr
Done ioResult <- takeMVar ioMsgReq
pure ioResult
readCallback <- FFI.mkReadCallback $ readWriteCallback id
writeCallback <- FFI.mkWriteCallback $ readWriteCallback unConstPtr
let freeCallback = do
freeHaskellFunPtr readCallback
freeHaskellFunPtr writeCallback
interact = forever do
Request readOrWrite <- takeMVar ioMsgReq
let readOrWriteTls = case readOrWrite of
Read -> flip FFI.connectionReadTls readCallback
Write -> flip FFI.connectionWriteTls writeCallback
_ <- readOrWriteTls conn nullPtr lenPtr
putMVar ioMsgRes DoneFFI
interactThread <- forkFinally interact (const freeCallback)
for_ logCallback $ FFI.connectionSetLogCallback conn . unLogCallback
Connection <$> newMVar Connection' {..}
release (Connection c) = do
Just Connection' {..} <- tryTakeMVar c
FFI.connectionFree conn
free lenPtr
killThread interactThread
-- | Initialize a TLS connection as a client.
newClientConnection ::
Backend ->
ClientConfig ->
-- | Hostname.
Text ->
Acquire (Connection Client)
newClientConnection b ClientConfig {..} hostname =
newConnection b clientConfigPtr clientConfigLogCallback \configPtr connPtrPtr ->
T.withCString hostname \hostnamePtr ->
FFI.clientConnectionNew configPtr (ConstPtr hostnamePtr) connPtrPtr
-- | Initialize a TLS connection as a server.
newServerConnection ::
Backend ->
ServerConfig ->
Acquire (Connection Server)
newServerConnection b ServerConfig {..} =
newConnection b serverConfigPtr serverConfigLogCallback FFI.serverConnectionNew
-- | Ensure that the connection is handshaked. It is only necessary to call this
-- if you want to obtain connection information. You can do so by providing a
-- 'HandshakeQuery'.
--
-- >>> :{
-- getALPNAndTLSVersion ::
-- MonadIO m =>
-- Connection side ->
-- m (Maybe ALPNProtocol, TLSVersion)
-- getALPNAndTLSVersion conn =
-- handshake conn $ (,) <$> getALPNProtocol <*> getTLSVersion
-- :}
handshake :: (MonadIO m) => Connection side -> HandshakeQuery side a -> m a
handshake conn (HandshakeQuery query) = liftIO do
withConnection conn \c -> do
_ <- completePriorIO c
runReaderT query c
-- | Get the negotiated ALPN protocol, if any.
getALPNProtocol :: HandshakeQuery side (Maybe ALPNProtocol)
getALPNProtocol = handshakeQuery \Connection' {conn, lenPtr} ->
alloca \bufPtrPtr -> do
FFI.connectionGetALPNProtocol (ConstPtr conn) bufPtrPtr lenPtr
ConstPtr bufPtr <- peek bufPtrPtr
len <- peek lenPtr
!alpn <- B.packCStringLen (castPtr bufPtr, cSizeToInt len)
pure $ if B.null alpn then Nothing else Just $ ALPNProtocol alpn
-- | Get the negotiated TLS protocol version.
getTLSVersion :: HandshakeQuery side TLSVersion
getTLSVersion = handshakeQuery \Connection' {conn} -> do
!ver <- FFI.connectionGetProtocolVersion (ConstPtr conn)
when (unTLSVersion ver == 0) $
fail "internal rustls error: no protocol version negotiated"
pure ver
-- | Get the negotiated cipher suite.
getNegotiatedCipherSuite :: HandshakeQuery side NegotiatedCipherSuite
getNegotiatedCipherSuite = handshakeQuery \Connection' {conn} -> do
negotiatedCipherSuiteID <-
FFI.connectionGetNegotiatedCipherSuite (ConstPtr conn)
when (negotiatedCipherSuiteID == 0) $
fail "internal rustls error: no cipher suite negotiated"
negotiatedCipherSuiteName <- alloca \strPtr -> do
FFI.connectionGetNegotiatedCipherSuiteName (ConstPtr conn) strPtr
strToText =<< peek strPtr
when (T.null negotiatedCipherSuiteName) $
fail "internal rustls error: no cipher suite negotiated"
pure NegotiatedCipherSuite {..}
-- | Get the SNI hostname set by the client, if any.
getSNIHostname :: HandshakeQuery Server (Maybe Text)
getSNIHostname = handshakeQuery \Connection' {conn, lenPtr} ->
let go n = allocaBytes (cSizeToInt n) \bufPtr -> do
res <- FFI.serverConnectionGetSNIHostname (ConstPtr conn) bufPtr n lenPtr
if res == FFI.resultInsufficientSize
then go (2 * n)
else do
rethrowR res
len <- peek lenPtr
!sni <- T.peekCStringLen (castPtr bufPtr, cSizeToInt len)
pure $ if T.null sni then Nothing else Just sni
in go 16
-- | A DER-encoded certificate.
newtype DERCertificate = DERCertificate {unDERCertificate :: ByteString}
deriving stock (Show, Eq, Ord, Generic)
-- | Get the @i@-th certificate provided by the peer.
--
-- Index @0@ is the end entity certificate. Higher indices are certificates in
-- the chain. Requesting an index higher than what is available returns
-- 'Nothing'.
getPeerCertificate :: CSize -> HandshakeQuery side (Maybe DERCertificate)
getPeerCertificate i = handshakeQuery \Connection' {conn, lenPtr} -> do
certPtr <- FFI.connectionGetPeerCertificate (ConstPtr conn) i
if certPtr == ConstPtr nullPtr
then pure Nothing
else alloca \bufPtrPtr -> do
rethrowR =<< FFI.certificateGetDER certPtr bufPtrPtr lenPtr
ConstPtr bufPtr <- peek bufPtrPtr
len <- cSizeToInt <$> peek lenPtr
!bs <- B.packCStringLen (castPtr bufPtr, len)
pure $ Just $ DERCertificate bs
-- | Send a @close_notify@ warning alert. This informs the peer that the
-- connection is being closed.
sendCloseNotify :: (MonadIO m) => Connection side -> m ()
sendCloseNotify conn = liftIO $
withConnection conn \c@Connection' {conn} -> do
FFI.connectionSendCloseNotify conn
void $ completeIO c
-- | Read data from the Rustls 'Connection' into the given buffer.
readPtr :: (MonadIO m) => Connection side -> Ptr Word8 -> CSize -> m CSize
readPtr conn buf len = liftIO $
withConnection conn \c@Connection' {..} -> do
completePriorIO c
loopWhileTrue $
getWantsRead c >>= \case
True -> (NotEOF ==) <$> completeIO c
False -> pure False
rethrowR =<< FFI.connectionRead conn buf len lenPtr
peek lenPtr
-- | Read data from the Rustls 'Connection' into a 'ByteString'. The result will
-- not be longer than the given length.
readBS ::
(MonadIO m) =>
Connection side ->
-- | Maximum result length. Note that a buffer of this size will be allocated.
Int ->
m ByteString
readBS conn maxLen = liftIO $
BI.createAndTrim maxLen \buf ->
cSizeToInt <$> readPtr conn buf (intToCSize maxLen)
-- | Write data to the Rustls 'Connection' from the given buffer.
writePtr :: (MonadIO m) => Connection side -> Ptr Word8 -> CSize -> m CSize
writePtr conn buf len = liftIO $
withConnection conn \c@Connection' {..} -> do
completePriorIO c
rethrowR =<< FFI.connectionWrite conn buf len lenPtr
_ <- completeIO c
peek lenPtr
-- | Write a 'ByteString' to the Rustls 'Connection'.
writeBS :: (MonadIO m) => Connection side -> ByteString -> m ()
writeBS conn bs = liftIO $ BU.unsafeUseAsCStringLen bs go
where
go (buf, len) = do
written <- cSizeToInt <$> writePtr conn (castPtr buf) (intToCSize len)
when (written < len) $
go (buf `plusPtr` len, len - written)