batchd-0.1.1.0: src/Batchd/Client/Http.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
-- | This module contains definitions for HTTP\/HTTPS protocol handling.
module Batchd.Client.Http where
import Control.Exception
import Control.Monad.State
import qualified Data.ByteString.Lazy as L
import qualified Data.Text.Lazy.Encoding as TLE
import Data.Text.Format.Heavy
import Data.Default
import Data.List
import Data.Aeson as Aeson
import Data.X509.CertificateStore
import Network.HTTP.Client
import Network.HTTP.Types.Status
import Network.HTTP.Client.TLS
import Network.TLS as TLS
import Network.TLS.Extra.Cipher
import Network.Connection
import Batchd.Core.Common.Types
import Batchd.Core.Common.Localize
import Batchd.Common.Types
import Batchd.Common.Config (getPassword)
import Batchd.Client.Types as C
import Batchd.Client.Config
import Batchd.Client.CmdLine
import Batchd.Client.Monad
import Batchd.Client.Logging
-- | Create a Manager - a structure which contains all required information to
-- maintain HTTP connection, over TLS if required.
makeClientManager :: CmdLine -> Client Manager
makeClientManager opts = do
cfg <- liftIO loadClientConfig
url <- liftIO $ getManagerUrl opts cfg
if "https" `isPrefixOf` url
then mkMngr (ccDisableServerCertificateCheck cfg) (ccCertificate cfg) (ccKey cfg) (ccCertificate cfg)
else liftIO $ newManager defaultManagerSettings -- for HTTP everything is easy
where
mkMngr :: Bool -> Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> Client Manager
mkMngr disableServerCertCheck mbCertFile mbKeyFile mbCaFile = do
when disableServerCertCheck $ do
verbose (__ "Server certificate check disabled") ()
-- load Credentials from certificate and private key file (.pem and .key usually)
creds <- case (mbCertFile, mbKeyFile) of
(Just certFile, Just keyFile) -> do
verbose (__ "Loading HTTPS certificate: {}, key: {}") (certFile, keyFile)
r <- liftIO $ credentialLoadX509 certFile keyFile
case r of
Right cr -> return $ Just cr
Left err -> fail =<< (__sf "Can't load certificate/key: {}" (Single $ show err))
_ -> do
message (__ "Certificate or key file is not specified, try to access HTTPS without them") ()
return Nothing
-- load trusted CA store if specified
mbStore <- case mbCaFile of
Nothing -> return Nothing
Just caFile -> do
r <- liftIO $ readCertificateStore caFile
case r of
Just store -> return $ Just store
Nothing -> fail =<< (__s "cannot read specified CA store")
let shared = case mbStore of
Nothing -> def
Just store -> def { sharedCAStore = store }
let clientCertHook _ = return creds
skip _ _ _ _ = do
return []
-- handlers for TLS protocol events on client side
hooks =
if disableServerCertCheck
then def {
onCertificateRequest = clientCertHook, -- return loaded client certificate
onServerCertificate = skip
}
else def { onCertificateRequest = clientCertHook }
clientParams = (defaultParamsClient "" "")
{ clientHooks = hooks,
clientUseServerNameIndication = True, -- SNI
clientShared = shared,
-- This is probably to be configurable
clientSupported = def { supportedCiphers = ciphersuite_all, supportedVersions = [TLS12] }
}
tlsSettings = TLSSettings clientParams
liftIO $ newManager $ mkManagerSettings tlsSettings Nothing
-- | Obtain list of authentication methods supported by server.
-- This is done by requesting OPTIONS /.
getAuthMethods :: Client [AuthMethod]
getAuthMethods = do
mbMethods <- gets csAuthMethods
case mbMethods of
Just methods -> return methods
Nothing -> do
baseUrl <- getBaseUrl
methods <- doOptions baseUrl
modify $ \st -> st {csAuthMethods = Just methods}
return methods
-- | Obtain user name and password if required
obtainCredentials :: Client C.Credentials
obtainCredentials = do
cfg <- gets csConfig
opts@(CmdLine o _) <- gets csCmdline
methods <- getAuthMethods
verbose (__ "Supported authentication methods: {}") (Single $ show methods)
let needPassword = BasicAuth `elem` methods -- only basic auth of currently supported methods requires a password.
name <- liftIO $ getUserName opts cfg
pass <- if ccDisableAuth cfg || not needPassword
then do
debug (__ "Password is not required") ()
return ""
else do
mbPassword <- liftIO $ getConfigParam' (password o) "BATCH_PASSWORD" (ccPassword cfg)
case mbPassword of
Just p -> return p
Nothing -> liftIO $ getPassword =<< (__sf "{} password: " (Single name))
let creds = (name, pass)
-- remember credentials in client state
modify $ \st -> st {csCredentials = Just creds}
return creds
-- | Obain and cache user name and password, if required
getCredentials :: Client C.Credentials
getCredentials = do
mbCreds <- gets csCredentials
case mbCreds of
Just creds -> return creds
Nothing -> obtainCredentials
-- | Add authentication headers to HTTP request
applyAuth :: Request -> Client Request
applyAuth rq = do
methods <- getAuthMethods
-- liftIO $ print methods
if BasicAuth `elem` methods
then do
(name, password) <- getCredentials
return $ applyBasicAuth (stringToBstr name) (stringToBstr password) rq
else if HeaderAuth `elem` methods
then do
(name, _) <- getCredentials
return $ rq {requestHeaders = ("X-Auth-User", stringToBstr name) : requestHeaders rq}
else return rq
handleStatus :: Response L.ByteString -> Client L.ByteString
handleStatus rs = do
debug (__ "Received response: {}") (Single $ Shown rs)
verbose (__ "Server response code is {} {}") (statusCode $ responseStatus rs, statusMessage $ responseStatus rs)
if responseStatus rs == ok200
then return $ responseBody rs
else do
liftIO $ throw $ ClientException $ TLE.decodeUtf8 $ responseBody rs
allowAny :: Request -> Response BodyReader -> IO ()
allowAny _ _ = return ()
getManager :: Client Manager
getManager = do
mbManager <- gets csManager
case mbManager of
Just manager -> return manager
Nothing -> do
cmdline <- gets csCmdline
manager <- makeClientManager cmdline
modify $ \st -> st {csManager = Just manager}
return manager
-- Generic HTTP request wrapper, useful mostly for debugging
doHttp :: Request -> Manager -> Client L.ByteString
doHttp request manager = do
debug (__ "Sending request: {}") (Single $ Shown request)
handleStatus =<< (liftIO $ httpLbs request manager)
-- PUT request
doPut :: ToJSON a => String -> a -> Client ()
doPut urlStr object = do
manager <- getManager
url <- liftIO $ parseUrlThrow urlStr
let json = Aeson.encode object
debug (__ "Request JSON: {}") (Single json)
request <- applyAuth $ url {
method="PUT",
checkResponse = allowAny,
requestBody = RequestBodyLBS json
}
doHttp request manager
return ()
-- POST request
doPost :: ToJSON a => String -> a -> Client ()
doPost urlStr object = do
manager <- getManager
url <- liftIO $ parseUrlThrow urlStr
let json = Aeson.encode object
debug (__ "Request JSON: {}") (Single json)
request <- applyAuth $ url {
method="POST",
checkResponse = allowAny,
requestBody = RequestBodyLBS json
}
doHttp request manager
return ()
-- DELETE request
doDelete :: String -> Client ()
doDelete urlStr = do
manager <- getManager
url <- liftIO $ parseUrlThrow urlStr
request <- applyAuth $ url { method="DELETE",
checkResponse = allowAny
}
doHttp request manager
return ()
-- GET request
doGet :: FromJSON a => String -> Client a
doGet urlStr = do
manager <- getManager
url <- liftIO $ parseUrlThrow urlStr
request <- applyAuth $ url {checkResponse = allowAny}
responseLbs <- doHttp request manager
case Aeson.eitherDecode responseLbs of
Left err -> throwC =<< (__f "Can't parse server response for GET request: {}" (Single err))
Right res -> return res
-- OPTIONS request
doOptions :: FromJSON a => String -> Client a
doOptions urlStr = do
manager <- getManager
url <- liftIO $ parseUrlThrow urlStr
let request = url {checkResponse = allowAny, method = "OPTIONS"}
responseLbs <- doHttp request manager
case Aeson.eitherDecode responseLbs of
Left err -> throwC =<< (__f "Can't parse server response for OPTIONS request: {}" (Single err))
Right res -> return res