module Network.HTTP.Conduit.Browser
( BrowserState
, BrowserAction
, browse
, makeRequest
, defaultState
, getBrowserState
, setBrowserState
, withBrowserState
, getMaxRedirects
, setMaxRedirects
, getMaxRetryCount
, setMaxRetryCount
, getAuthorities
, setAuthorities
, getCookieFilter
, setCookieFilter
, getCookieJar
, setCookieJar
, getCurrentProxy
, setCurrentProxy
, getUserAgent
, setUserAgent
, getManager
, setManager
)
where
import qualified Data.ByteString as BS
import Control.Monad.State
import Control.Exception
import qualified Control.Exception.Lifted as LE
import Data.Conduit
import Prelude hiding (catch)
import qualified Network.HTTP.Types as HT
import Data.Time.Clock (getCurrentTime, UTCTime)
import Data.CaseInsensitive (mk)
import Data.ByteString.UTF8 (fromString)
import Data.List (partition)
import Web.Cookie (parseSetCookie)
import Data.Default (def)
import Data.Maybe (catMaybes)
import Network.HTTP.Conduit.Cookies hiding (updateCookieJar)
import Network.HTTP.Conduit.Request
import Network.HTTP.Conduit.Response
import Network.HTTP.Conduit.Manager
import qualified Network.HTTP.Conduit as HC
data BrowserState = BrowserState
{ maxRedirects :: Int
, maxRetryCount :: Int
, authorities :: Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)
, cookieFilter :: Request (ResourceT IO) -> Cookie -> IO Bool
, cookieJar :: CookieJar
, currentProxy :: Maybe Proxy
, userAgent :: BS.ByteString
, manager :: Manager
}
defaultState :: Manager -> BrowserState
defaultState m = BrowserState { maxRedirects = 10
, maxRetryCount = 1
, authorities = \ _ -> Nothing
, cookieFilter = \ _ _ -> return True
, cookieJar = def
, currentProxy = Nothing
, userAgent = fromString "http-conduit"
, manager = m
}
type BrowserAction = StateT BrowserState (ResourceT IO)
browse :: Manager -> BrowserAction a -> ResourceT IO a
browse m act = evalStateT act (defaultState m)
makeRequest :: Request (ResourceT IO) -> BrowserAction (Response (Source (ResourceT IO) BS.ByteString))
makeRequest request = do
BrowserState
{ maxRetryCount = max_retry_count
, currentProxy = current_proxy
, userAgent = user_agent
} <- get
retryHelper (applyUserAgent user_agent $
request { redirectCount = 0
, proxy = current_proxy
, checkStatus = \ _ _ -> Nothing
}) max_retry_count Nothing
where retryHelper request' retry_count e
| retry_count == 0 = case e of
Just e' -> throw e'
Nothing -> throw TooManyRedirects
| otherwise = do
BrowserState {maxRedirects = max_redirects} <- get
resp <- LE.catch (runRedirectionChain request' max_redirects)
(\ e' -> retryHelper request' (retry_count 1) (Just (e' :: HttpException)))
let code = HT.statusCode $ HC.responseStatus resp
if code < 200 || code >= 300
then retryHelper request' (retry_count 1) (Just $ HC.StatusCodeException (HC.responseStatus resp) (HC.responseHeaders resp))
else return resp
runRedirectionChain request' redirect_count
| redirect_count == 0 = throw TooManyRedirects
| otherwise = do
s@(BrowserState { manager = manager'
, authorities = auths
, cookieJar = cookie_jar
, cookieFilter = cookie_filter
}) <- get
now <- liftIO getCurrentTime
let (request'', cookie_jar') = insertCookiesIntoRequest
(applyAuthorities auths request')
(evictExpiredCookies cookie_jar now) now
res <- lift $ HC.http request'' manager'
(cookie_jar'', response) <- liftIO $ updateCookieJar res request'' now cookie_jar' cookie_filter
put $ s {cookieJar = cookie_jar''}
let code = HT.statusCode (HC.responseStatus response)
if code >= 300 && code < 400
then runRedirectionChain (case HC.getRedirectedRequest request'' (responseHeaders response) code of
Just a -> a
Nothing -> throw HC.UnparseableRedirect) (redirect_count 1)
else return res
applyAuthorities auths request' = case auths request' of
Just (user, pass) -> applyBasicAuth user pass request'
Nothing -> request'
applyUserAgent ua request' = request' {requestHeaders = (k, ua) : hs}
where hs = filter ((/= k) . fst) $ requestHeaders request'
k = mk $ fromString "User-Agent"
updateCookieJar :: Response a -> Request (ResourceT IO) -> UTCTime -> CookieJar -> (Request (ResourceT IO) -> Cookie -> IO Bool) -> IO (CookieJar, Response a)
updateCookieJar response request' now cookie_jar cookie_filter = do
filtered_cookies <- filterM (cookie_filter request') $ catMaybes $ map (\ sc -> generateCookie sc request' now True) set_cookies
return (cookieJar' filtered_cookies, response {HC.responseHeaders = other_headers})
where (set_cookie_headers, other_headers) = partition ((== (mk $ fromString "Set-Cookie")) . fst) $ HC.responseHeaders response
set_cookie_data = map snd set_cookie_headers
set_cookies = map parseSetCookie set_cookie_data
cookieJar' = foldl (\ cj c -> insertCheckedCookie c cj True) cookie_jar
getBrowserState :: BrowserAction BrowserState
getBrowserState = get
setBrowserState :: BrowserState -> BrowserAction ()
setBrowserState = put
withBrowserState :: BrowserState -> BrowserAction a -> BrowserAction a
withBrowserState s a = do
current <- get
put s
out <- a
put current
return out
getMaxRedirects :: BrowserAction Int
getMaxRedirects = get >>= \ a -> return $ maxRedirects a
setMaxRedirects :: Int -> BrowserAction ()
setMaxRedirects b = get >>= \ a -> put a {maxRedirects = b}
getMaxRetryCount :: BrowserAction Int
getMaxRetryCount = get >>= \ a -> return $ maxRetryCount a
setMaxRetryCount :: Int -> BrowserAction ()
setMaxRetryCount b = get >>= \ a -> put a {maxRetryCount = b}
getAuthorities :: BrowserAction (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString))
getAuthorities = get >>= \ a -> return $ authorities a
setAuthorities :: (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)) -> BrowserAction ()
setAuthorities b = get >>= \ a -> put a {authorities = b}
getCookieFilter :: BrowserAction (Request (ResourceT IO) -> Cookie -> IO Bool)
getCookieFilter = get >>= \ a -> return $ cookieFilter a
setCookieFilter :: (Request (ResourceT IO) -> Cookie -> IO Bool) -> BrowserAction ()
setCookieFilter b = get >>= \ a -> put a {cookieFilter = b}
getCookieJar :: BrowserAction CookieJar
getCookieJar = get >>= \ a -> return $ cookieJar a
setCookieJar :: CookieJar -> BrowserAction ()
setCookieJar b = get >>= \ a -> put a {cookieJar = b}
getCurrentProxy :: BrowserAction (Maybe Proxy)
getCurrentProxy = get >>= \ a -> return $ currentProxy a
setCurrentProxy :: Maybe Proxy -> BrowserAction ()
setCurrentProxy b = get >>= \ a -> put a {currentProxy = b}
getUserAgent :: BrowserAction BS.ByteString
getUserAgent = get >>= \ a -> return $ userAgent a
setUserAgent :: BS.ByteString -> BrowserAction ()
setUserAgent b = get >>= \ a -> put a {userAgent = b}
getManager :: BrowserAction Manager
getManager = get >>= \ a -> return $ manager a
setManager :: Manager -> BrowserAction ()
setManager b = get >>= \ a -> put a {manager = b}