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