http-tower-hs-0.1.0.0: src/Network/HTTP/Tower/Client.hs
-- |
-- Module : Network.HTTP.Tower.Client
-- Description : HTTP client with composable middleware
-- License : MIT
--
-- Create an HTTP client and compose middleware using the @('|>')@ operator:
--
-- @
-- client <- 'newClient'
-- let configured = client
-- '|>' withRetry (constantBackoff 3 1.0)
-- '|>' withTimeout 5000
-- result <- 'runRequest' configured request
-- @
--
-- For mTLS (client certificate authentication):
--
-- @
-- client <- 'newClientWithTLS'
-- (Just \"certs\/ca.pem\")
-- (Just (\"certs\/client.pem\", \"certs\/client-key.pem\"))
-- @
module Network.HTTP.Tower.Client
( Client(..)
, HttpResponse
, newClient
, newClientWith
, newClientWithTLS
, runRequest
, applyMiddleware
, (|>)
) where
import Control.Exception.Safe (tryAny)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.X509.CertificateStore as X509
import qualified Network.Connection as Conn
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.TLS as TLS
import qualified Network.TLS as TLS.Core
import qualified Network.TLS.Extra.Cipher as TLS.Cipher
import qualified System.X509 as X509System
import Network.HTTP.Tower.Core (Service(..), Middleware)
import Network.HTTP.Tower.Error (ServiceError(..))
-- | Concrete response type used by the client (lazy bytestring body).
type HttpResponse = HTTP.Response LBS.ByteString
-- | An HTTP client with a composable middleware stack.
data Client = Client
{ clientService :: Service HTTP.Request HttpResponse
-- ^ The service with all middleware applied.
, clientManager :: HTTP.Manager
-- ^ The underlying connection manager.
}
-- | Create a client with default TLS settings. HTTPS works out of the box.
newClient :: IO Client
newClient = newClientWith TLS.tlsManagerSettings
-- | Create a client with custom manager settings.
--
-- Use this with 'Network.HTTP.Client.TLS.mkManagerSettings' for advanced
-- TLS configuration, proxies, etc.
newClientWith :: HTTP.ManagerSettings -> IO Client
newClientWith settings = do
mgr <- HTTP.newManager settings
let baseService = Service $ \req -> do
result <- tryAny $ HTTP.httpLbs req mgr
pure $ case result of
Left err -> Left (HttpError err)
Right resp -> Right resp
pure Client
{ clientService = baseService
, clientManager = mgr
}
-- | Create a client with custom TLS certificate configuration.
--
-- Supports:
--
-- * Custom CA bundle for server certificate verification
-- * Client certificate authentication (mTLS)
-- * Both, either, or neither
--
-- @
-- -- Custom CA only (verify server against your own CA)
-- client <- 'newClientWithTLS' (Just \"certs\/ca.pem\") Nothing
--
-- -- mTLS (client cert + custom CA)
-- client <- 'newClientWithTLS'
-- (Just \"certs\/ca.pem\")
-- (Just (\"certs\/client.pem\", \"certs\/client-key.pem\"))
--
-- -- mTLS with system CA store
-- client <- 'newClientWithTLS'
-- Nothing
-- (Just (\"certs\/client.pem\", \"certs\/client-key.pem\"))
-- @
newClientWithTLS
:: Maybe FilePath -- ^ Path to CA certificate (PEM). 'Nothing' uses system store.
-> Maybe (FilePath, FilePath) -- ^ Client cert and key paths (PEM) for mTLS. 'Nothing' for no client cert.
-> IO Client
newClientWithTLS mCaPath mClientCert = do
-- Load CA store
caStore <- case mCaPath of
Just caPath -> do
mStore <- X509.readCertificateStore caPath
case mStore of
Just store -> pure store
Nothing -> fail $ "Failed to load CA certificate: " <> caPath
Nothing -> X509System.getSystemCertificateStore
-- Load client credentials
credentials <- case mClientCert of
Just (certPath, keyPath) -> do
result <- TLS.Core.credentialLoadX509 certPath keyPath
case result of
Right cred -> pure (TLS.Core.Credentials [cred])
Left err -> fail $ "Failed to load client certificate: " <> err
Nothing -> pure (TLS.Core.Credentials [])
let defaults = TLS.Core.defaultParamsClient "" mempty
clientParams = defaults
{ TLS.Core.clientShared = (TLS.Core.clientShared defaults)
{ TLS.Core.sharedCAStore = caStore
, TLS.Core.sharedCredentials = credentials
}
, TLS.Core.clientSupported = (TLS.Core.clientSupported defaults)
{ TLS.Core.supportedCiphers = TLS.Cipher.ciphersuite_default
}
, TLS.Core.clientHooks = (TLS.Core.clientHooks defaults)
{ TLS.Core.onCertificateRequest = \_ ->
case credentials of
TLS.Core.Credentials (cred:_) -> pure (Just cred)
_ -> pure Nothing
}
}
tlsSettings = Conn.TLSSettings clientParams
managerSettings = TLS.mkManagerSettings tlsSettings Nothing
newClientWith managerSettings
-- | Execute a request through the client's middleware stack.
runRequest :: Client -> HTTP.Request -> IO (Either ServiceError HttpResponse)
runRequest client = runService (clientService client)
-- | Apply a middleware to a client, wrapping its existing service.
applyMiddleware :: Middleware HTTP.Request HttpResponse -> Client -> Client
applyMiddleware mw client = client { clientService = mw (clientService client) }
-- | Operator for fluent middleware application.
--
-- @
-- client <- 'newClient'
-- let configured = client
-- '|>' withRetry (constantBackoff 3 1.0)
-- '|>' withTimeout 5000
-- '|>' withLogging putStrLn
-- @
(|>) :: Client -> Middleware HTTP.Request HttpResponse -> Client
(|>) client mw = applyMiddleware mw client
infixl 1 |>