iterIO-0.2.2: Data/IterIO/HttpClient.hs
module Data.IterIO.HttpClient ( -- * Simple interface
simpleHttp, genSimpleHttp
, headRequest, getRequest, postRequest
-- * GET, HEAD wrappers
, simpleGetHttp, simpleGetHttps
, simpleHeadHttp, simpleHeadHttps
-- * Advanced interface
, HttpClient(..)
, mkHttpClient
, httpConnect
, inumHttpClient
, HttpResponseHandler
-- * Internal
, userAgent
, maxNrRedirects
, mkRequestToAbsUri
) where
import Prelude hiding (catch, head, div)
import Control.Monad (when, unless)
import Control.Exception
import Control.Monad.Trans
import Data.Maybe ( isNothing, fromJust)
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Network.Socket as Net
import qualified Network.BSD as Net
import qualified OpenSSL.Session as SSL
import Data.IterIO
import Data.IterIO.Http
import Data.IterIO.SSL
import Data.Version (showVersion)
import Paths_iterIO (version)
import Data.IORef
type L = L.ByteString
type S = S.ByteString
lazyfy :: S -> L
lazyfy = L.pack . S.unpack
catchIO :: IO a -> (IOException -> IO a) -> IO a
catchIO = catch
-- | User agent corresponding to this library.
userAgent :: String
userAgent = "haskell-iterIO/" ++ showVersion version
-- | An HTTP client.
data HttpClient = HttpClient {
hcSock :: !Net.Socket
-- ^ Socket
, hcSockAddr :: !Net.SockAddr
-- ^ Socket address
, hcSslCtx :: !(Maybe SSL.SSLContext)
-- ^ SSL context
, hcIsHttps :: !Bool
-- ^ Use SSL
}
-- | Maximum number of redirects. Defult: no redirect (0).
maxNrRedirects :: Int
maxNrRedirects = 0
-- | Given an HTTP client configuration, make the actual connection to
-- server.
httpConnect :: MonadIO m => HttpClient -> IO (Iter L m (), Onum L m a)
httpConnect hc = do
let s = hcSock hc
isHttps = hcIsHttps hc
when (isHttps && isNothing (hcSslCtx hc)) $
throwIO (userError "Need SSL context for HTTPS")
Net.connect s (hcSockAddr hc)
if hcIsHttps hc
then iterSSL (fromJust $ hcSslCtx hc) s False
else iterStream s
-- | Given the host, port, context, and \"is-https\" flag, create
-- a client value. The returned value can be used with 'httpConnect'
-- to get raw pipes to/from the server.
--
-- /Note:/ Some of this code is from the "HTTP" package.
mkHttpClient :: S -- ^ Host
-> Int -- ^ Port
-> Maybe SSL.SSLContext -- ^ SSL context
-> Bool -- ^ Is HTTPS
-> IO HttpClient
mkHttpClient host port ctx isHttps = withSocket $ \s -> do
Net.setSocketOption s Net.KeepAlive 1
hostA <- getHostAddr (S8.unpack host)
let a = Net.SockAddrInet (toEnum port) hostA
return HttpClient { hcSock = s
, hcSockAddr = a
, hcSslCtx = ctx
, hcIsHttps = isHttps }
where withSocket action = do
s <- Net.socket Net.AF_INET Net.Stream 6
catchIO (action s) (\e -> Net.sClose s >> ioError e)
getHostAddr h =
catchIO (Net.inet_addr h) $ \_ -> do
h' <- getHostByName_safe h
case Net.hostAddresses h' of
[] -> err $ "No addresses in host entry for " ++ show h
(ha:_) -> return ha
getHostByName_safe h =
catchIO (Net.getHostByName h) $ \_ ->
err $ "Failed to lookup " ++ show h
err = throwIO . userError
--
-- Simple interface wrappers
--
-- | Perform a simple HTTP GET request. No SSL support.
simpleGetHttp :: MonadIO m
=> String -- ^ URL
-> m (HttpResp m)
simpleGetHttp url = simpleHttp (getRequest url) L.empty Nothing
-- | Perform a simple HTTPS GET request.
simpleGetHttps :: MonadIO m
=> String -- ^ URL
-> SSL.SSLContext -- ^ SSL Context
-> m (HttpResp m)
simpleGetHttps url ctx = simpleHttp (getRequest url) L.empty (Just ctx)
-- | Perform a simple HTTP HEAD request. No SSL support.
simpleHeadHttp :: MonadIO m
=> String -- ^ URL
-> m (HttpResp m)
simpleHeadHttp url = simpleHttp (headRequest url) L.empty Nothing
-- | Perform a simple HTTPS HEAD request.
simpleHeadHttps :: MonadIO m
=> String -- ^ URL
-> SSL.SSLContext -- ^ SSL Context
-> m (HttpResp m)
simpleHeadHttps url ctx = simpleHttp (headRequest url) L.empty (Just ctx)
--
-- Simple interface
--
-- | Perform a simple HTTP request, given the the request header, body
-- and SSL context, if any.
simpleHttp :: MonadIO m
=> HttpReq () -- ^ Request header
-> L -- ^ Request body
-> Maybe SSL.SSLContext -- ^ SSL Context
-> m (HttpResp m)
simpleHttp req body ctx = genSimpleHttp req body ctx maxNrRedirects True
-- | Make a general HTTP request to host specified in the request.
-- If the request is over HTTPS, the SSL context must be provided.
-- The redirect count is used to limit the number of redirects
-- followed (when receiving a 3xx response); use 0 to return the
-- direct response. The @passCookies@ flag is used to guard cookies
-- on redirects: because @genSimpleHttp@ performs a \"single request\"
-- it does not parse \"Set-Cookie\" headers and so is unaware of the
-- cookie domain. Hence, the flag is used for the decision in passing
-- the cookie to the location of a redirect.
genSimpleHttp :: MonadIO m
=> HttpReq () -- ^ Request header
-> L -- ^ Message body
-> Maybe SSL.SSLContext -- ^ SSL Context
-> Int -- ^ Redirect count
-> Bool -- ^ Pass cookies
-> m (HttpResp m)
genSimpleHttp req body ctx redirectCount passCookies = do
let scheme = reqScheme req
isHttps = scheme == (S8.pack "https")
port <- maybe (defaultPort scheme) return $ reqPort req
client <- liftIO $ mkHttpClient (reqHost req) port ctx isHttps
(sIter,sOnum) <- liftIO $ httpConnect client
refResp <- liftIO $ newIORef Nothing
count <- liftIO $ newIORef 0
sOnum |$ inumHttpClient (req, body) (handler count refResp) .| sIter
mresp <- liftIO $ readIORef refResp
maybe err return mresp
where handler countRef refResp resp = do
liftIO $ writeIORef refResp (Just resp)
count <- liftIO $ do c <- readIORef countRef
writeIORef countRef (c+1)
return c
if count < redirectCount
then handleRedirect (req, body) passCookies resp
else return Nothing
defaultPort s | s == S8.pack "http" = return 80
| s == S8.pack "https" = return 443
| otherwise = liftIO . throwIO . userError $
"Unrecognized scheme" ++ S8.unpack s
err = liftIO . throwIO . userError $ "Request failed"
-- | Given a 3xx response and original request, handle the redirect.
-- Currently, only reponses with status codes 30[1237] and set
-- \"Location\" header are handled. Note that the request is made to
-- the same host, so a redirect to a different host will result in a
-- 4xx response.
handleRedirect :: MonadIO m
=> (HttpReq s, L ) -- ^ Original request
-> Bool -- ^ Pass cookies
-> HttpResp m -- ^ Response
-> Iter L m (Maybe (HttpReq s, L))
handleRedirect (req, body) passCookies resp =
if (respStatus resp `notElem` s300s) || (reqMethod req `notElem` meths)
then return Nothing
else doRedirect $ lookup (S8.pack "location") $ respHeaders resp
where s300s = [stat301, stat302, stat303, stat307]
meths = [S8.pack "GET", S8.pack "HEAD"]
doRedirect Nothing = return Nothing
doRedirect (Just url) = do
newReq <- mkRequestToAbsUri (lazyfy url) (reqMethod req)
let req' = newReq { reqHeaders = reqHeaders req
, reqCookies = if passCookies
then reqCookies req
else []
, reqContentType = reqContentType req
, reqContentLength = reqContentLength req
, reqTransferEncoding = reqTransferEncoding req
, reqIfModifiedSince = reqIfModifiedSince req
, reqSession = reqSession req }
return $ Just (req', body)
--
-- Create requests
--
-- | Create a simple HEAD request.
-- The @url@ must be an @absoluteURI@.
headRequest :: String -> HttpReq ()
headRequest url = fromJust $ mkRequestToAbsUri (L8.pack url) $ S8.pack "HEAD"
-- | Create a simple GET request.
-- The @url@ must be an @absoluteURI@.
getRequest :: String -> HttpReq ()
getRequest url = fromJust $ mkRequestToAbsUri (L8.pack url) $ S8.pack "GET"
-- | Given a URL, Content-Type, and message body, perform a simple
-- POST request. Note: message body must be properly encoded (e.g.,
-- URL-encoded if the Content-Type is
-- \"application\/x-www-form-urlencoded\").
postRequest :: String -- ^ URL
-> String -- ^ Content-Type header
-> L -- ^ Message body
-> HttpReq ()
postRequest url ct body =
let req = fromJust $ mkRequestToAbsUri (L8.pack url) $ S8.pack "POST"
in req { reqContentType = Just (S8.pack ct, [])
, reqContentLength = Just . fromIntegral . L8.length $ body }
-- | Createa generic HTTP request, given an absoluteURI:
-- If the URI is not absolute, the parser will fail.
mkRequestToAbsUri :: Monad m => L -> S -> m (HttpReq ())
mkRequestToAbsUri urlString method = do
(scheme, host, mport, path, query) <- enumPure urlString |$ absUri
return defaultHttpReq { reqScheme = scheme
, reqMethod = method
, reqPath = path
, reqPathLst = path2list path
, reqQuery = query
, reqHost = host
, reqPort = mport
, reqContentLength = Just 0
, reqVers = (1,1)
, reqHeaders = [hostHeader host, uaHeader]
}
where uaHeader = (S8.pack "User-Agent", S8.pack userAgent)
hostHeader host = (S8.pack "Host", host)
-- | An HTTP response handler used by HTTP clients.
type HttpResponseHandler m s =
HttpResp m -> Iter L m (Maybe (HttpReq s, L))
-- | Given an initial request, and a response handler,
-- create an inum that provides underlying functionality of an http
-- client.
inumHttpClient :: MonadIO m
=> (HttpReq s, L)
-> HttpResponseHandler m s -> Inum L L m a
inumHttpClient (req,body) respHandler = mkInumM $
tryI (irun $ enumHttpReq req body) >>=
either (fatal . fst) (const loop)
where loop = do eof <- atEOFI
unless eof doresp
doresp = do
resp <- liftI $ httpRespI
mreq <- catchI (liftI $ respHandler resp) errH
maybe (return ())
(\(req', body') ->
tryI (irun $ enumHttpReq req' body') >>=
either (fatal . fst) (const loop)) mreq
fatal (SomeException _) = return ()
errH (SomeException _) = return . return $ Nothing