{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
-- | Support for making connections via the OpenSSL library.
module Network.HTTP.Client.OpenSSL
( withOpenSSL
, newOpenSSLManager
, opensslManagerSettings
, defaultMakeContext
, OpenSSLSettings(..)
, defaultOpenSSLSettings
) where
import Network.HTTP.Client
import Network.HTTP.Client.Internal
import Control.Exception
import Control.Monad.IO.Class
import Network.Socket.ByteString (sendAll, recv)
import OpenSSL
import qualified Data.ByteString as S
import qualified Network.Socket as N
import qualified OpenSSL.Session as SSL
import qualified OpenSSL.X509.SystemStore as SSL (contextLoadSystemCerts)
import Foreign.Storable (sizeOf)
-- | Create a new 'Manager' using 'opensslManagerSettings' and 'defaultMakeContext'
-- with 'defaultOpenSSLSettings'.
newOpenSSLManager :: MonadIO m => m Manager
newOpenSSLManager = liftIO $ do
-- sharing an SSL context between threads (without modifying it) is safe:
-- https://github.com/openssl/openssl/issues/2165
ctx <- defaultMakeContext defaultOpenSSLSettings
newManager $ opensslManagerSettings (pure ctx)
-- | Note that it is the caller's responsibility to pass in an appropriate context.
opensslManagerSettings :: IO SSL.SSLContext -> ManagerSettings
opensslManagerSettings mkContext = defaultManagerSettings
{ managerTlsConnection = do
ctx <- mkContext
return $ \ha' host' port' ->
withSocket (const $ return ()) ha' host' port' $ \sock ->
makeSSLConnection ctx sock host'
, managerTlsProxyConnection = do
ctx <- mkContext
return $ \connstr checkConn serverName _ha host' port' ->
withSocket (const $ return ()) Nothing host' port' $ \sock -> do
conn <- makeConnection
(recv sock bufSize)
(sendAll sock)
(return ())
connectionWrite conn connstr
checkConn conn
makeSSLConnection ctx sock serverName
, managerRetryableException = \se ->
case () of
()
| Just (_ :: SSL.ConnectionAbruptlyTerminated) <- fromException se -> True
| otherwise -> managerRetryableException defaultManagerSettings se
, managerWrapException = \req ->
let
wrap se
| Just (_ :: IOException) <- fromException se = se'
| Just (_ :: SSL.SomeSSLException) <- fromException se = se'
| Just (_ :: SSL.ConnectionAbruptlyTerminated) <- fromException se = se'
| Just (_ :: SSL.ProtocolError) <- fromException se = se'
| otherwise = se
where
se' = toException (HttpExceptionRequest req (InternalException se))
in
handle (throwIO . wrap)
}
where
makeSSLConnection ctx sock host = do
ssl <- SSL.connection ctx sock
SSL.setTlsextHostName ssl host
SSL.enableHostnameValidation ssl host
SSL.connect ssl
makeConnection
(SSL.read ssl bufSize `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> return S.empty)
-- Handling SSL.ConnectionAbruptlyTerminated as a stream end
-- (some sites terminate SSL connection right after returning the data).
(SSL.write ssl)
(N.close sock)
-- same as Data.ByteString.Lazy.Internal.defaultChunkSize
bufSize :: Int
bufSize = 32 * 1024 - overhead
where overhead = 2 * sizeOf (undefined :: Int)
defaultMakeContext :: OpenSSLSettings -> IO SSL.SSLContext
defaultMakeContext OpenSSLSettings{..} = do
ctx <- SSL.context
SSL.contextSetVerificationMode ctx osslSettingsVerifyMode
SSL.contextSetCiphers ctx osslSettingsCiphers
mapM_ (SSL.contextAddOption ctx) osslSettingsOptions
osslSettingsLoadCerts ctx
return ctx
data OpenSSLSettings = OpenSSLSettings
{ osslSettingsOptions :: [SSL.SSLOption]
, osslSettingsVerifyMode :: SSL.VerificationMode
, osslSettingsCiphers :: String
, osslSettingsLoadCerts :: SSL.SSLContext -> IO ()
}
-- | Default OpenSSL settings. In particular:
--
-- * SSLv2 and SSLv3 are disabled
-- * Hostname validation
-- * @DEFAULT@ cipher list
-- * Certificates loaded from OS-specific store
--
-- Note that these settings might change in the future.
defaultOpenSSLSettings :: OpenSSLSettings
defaultOpenSSLSettings = OpenSSLSettings
{ osslSettingsOptions =
[ SSL.SSL_OP_ALL -- enable bug workarounds
, SSL.SSL_OP_NO_SSLv2
, SSL.SSL_OP_NO_SSLv3
]
, osslSettingsVerifyMode = SSL.VerifyPeer
-- vpFailIfNoPeerCert and vpClientOnce are only relevant for servers
{ SSL.vpFailIfNoPeerCert = False
, SSL.vpClientOnce = False
, SSL.vpCallback = Nothing
}
, osslSettingsCiphers = "DEFAULT"
, osslSettingsLoadCerts = SSL.contextLoadSystemCerts
}