http-conduit-browser 1.6.6 → 1.7.0
raw patch · 4 files changed
+551/−568 lines, 4 filesdep ~http-conduit
Dependency ranges changed: http-conduit
Files
- Network/HTTP/Conduit/Browser.hs +546/−3
- Network/HTTP/Conduit/Browser2.hs +0/−551
- http-conduit-browser.cabal +4/−13
- test/main.hs +1/−1
Network/HTTP/Conduit/Browser.hs view
@@ -1,5 +1,548 @@+{-# LANGUAGE CPP, ScopedTypeVariables, FlexibleContexts #-}+-- | This module is designed to work similarly to the Network.Browser module in the HTTP package.+-- The idea is that there are two new types defined: 'BrowserState' and 'BrowserAction'. The+-- purpose of this module is to make it easy to describe a browsing session, including navigating+-- to multiple pages, and have things like cookie jar updates work as expected as you browse+-- around.+--+-- BrowserAction is a monad that handles all your browser-related activities. This monad is+-- actually implemented as a specialization of the State monad, over the BrowserState type. The+-- BrowserState type has various bits of information that a web browser keeps, such as a current+-- cookie jar, the number of times to retry a request on failure, HTTP proxy information, etc. In+-- the BrowserAction monad, there is one BrowserState at any given time, and you can modify it by+-- using the convenience functions in this module.+--+-- A special kind of modification of the current browser state is the action of making a HTTP+-- request. This will do the request according to the params in the current BrowserState, as well+-- as modifying the current state with, for example, an updated cookie jar and location.+--+-- To use this module, you would bind together a series of BrowserActions (This simulates the user+-- clicking on links or using a settings dialogue etc.) to describe your browsing session. When+-- you've described your session, you call 'browse' on your top-level BrowserAction to actually+-- convert your actions into the ResourceT IO monad.+--+-- Here is an example program:+--+-- > import qualified Data.ByteString as B+-- > import qualified Data.ByteString.Lazy as LB+-- > import qualified Data.ByteString.UTF8 as UB+-- > import Data.Conduit+-- > import Network.HTTP.Conduit+-- > import Network.HTTP.Conduit.Browser+-- > +-- > -- The web request to log in to a service+-- > req1 :: IO (Request (ResourceT IO))+-- > req1 = do+-- > req <- parseUrl "http://www.myurl.com/login.php"+-- > return $ urlEncodedBody [ (UB.fromString "name", UB.fromString "litherum")+-- > , (UB.fromString "pass", UB.fromString "S33kRe7")+-- > ] req+-- > +-- > -- Once authenticated, run this request+-- > req2 :: IO (Request m')+-- > req2 = parseUrl "http://www.myurl.com/main.php"+-- > +-- > -- Bind two BrowserActions together+-- > action :: Request (ResourceT IO) -> Request (ResourceT IO) -> BrowserAction (Response LB.ByteString)+-- > action r1 r2 = do+-- > _ <- makeRequestLbs r1+-- > makeRequestLbs r2+-- > +-- > main :: IO ()+-- > main = do+-- > man <- newManager def+-- > r1 <- req1+-- > r2 <- req2+-- > out <- runResourceT $ browse man $ action r1 r2+-- > putStrLn $ UB.toString $ B.concat $ LB.toChunks $ responseBody out+ module Network.HTTP.Conduit.Browser- ( module Network.HTTP.Conduit.Browser2- ) where+ (+ -- * Main+ BrowserAction+ , GenericBrowserAction+ , browse+ , parseRelativeUrl+ , makeRequest+ , makeRequestLbs+ -- * Browser state+ , BrowserState+ , defaultState+ , getBrowserState+ , setBrowserState+ , withBrowserState+ -- ** Manager+ , Manager+ , getManager+ , setManager+ -- ** Location+ -- | The last visited url (similar to the location bar in mainstream browsers).+ -- Location is updated on every request.+ --+ -- default: @Nothing@+ , getLocation+ , setLocation+ , withLocation+ -- ** Cookies+ -- *** Cookie jar+ -- | All the cookies!+ , getCookieJar+ , setCookieJar+ , withCookieJar+ -- *** Cookie filter+ -- | Each new Set-Cookie the browser encounters will pass through this filter.+ -- Only cookies that pass the filter (and are already valid) will be allowed into the cookie jar+ --+ -- default: @const $ const $ return True@+ , getCookieFilter+ , setCookieFilter+ , withCookieFilter+ -- ** Proxies+ -- *** HTTP+ -- | An optional proxy to send all requests through+ -- if Nothing uses Request's 'proxy'+ --+ -- default: @Nothing@+ , getCurrentProxy+ , setCurrentProxy+ , withCurrentProxy+ -- *** SOCKS+ -- | An optional SOCKS proxy to send all requests through+ -- if Nothing uses Request's 'socksProxy'+ --+ -- default: @Nothing@+ , getCurrentSocksProxy+ , setCurrentSocksProxy+ , withCurrentSocksProxy+ -- ** Redirects+ -- | The number of redirects to allow.+ -- if Nothing uses Request's 'redirectCount'+ --+ -- default: @Nothing@+ , getMaxRedirects+ , setMaxRedirects+ , withMaxRedirects+ -- ** Retries+ -- | The number of times to retry a failed connection+ --+ -- default: @0@+ , getMaxRetryCount+ , setMaxRetryCount+ , withMaxRetryCount+ -- ** Timeout+ -- | Number of microseconds to wait for a response.+ -- if Nothing uses Request's 'responseTimeout'+ --+ -- default: @Nothing@+ , getTimeout+ , setTimeout+ , withTimeout+ -- ** Authorities+ -- | A user-provided function that provides optional authorities.+ -- This function gets run on all requests before they get sent out.+ -- The output of this function is applied to the request.+ --+ -- default: @const Nothing@+ , getAuthorities+ , setAuthorities+ , withAuthorities+ -- ** Headers+ -- *** Override headers+ -- | Specifies Headers that should be added to 'Request',+ -- these will override Headers already specified in 'requestHeaders'.+ --+ -- > do insertOverrideHeader ("User-Agent", "rat")+ -- > insertOverrideHeader ("Connection", "keep-alive")+ -- > makeRequest def{requestHeaders = [("User-Agent", "kitten"), ("Accept", "everything/digestible")]}+ -- > > User-Agent: rat+ -- > > Accept: everything/digestible+ -- > > Connection: keep-alive+ , getOverrideHeaders+ , setOverrideHeaders+ , withOverrideHeaders+ , getOverrideHeader+ , setOverrideHeader+ , insertOverrideHeader+ , deleteOverrideHeader+ , withOverrideHeader+ -- *** User agent+ -- | What string to report our user-agent as.+ -- if Nothing will not send user-agent unless one is specified in 'Request'+ --+ -- > getUserAgent = lookup "User-Agent" overrideHeaders+ -- > setUserAgent a = insertOverrideHeader ("User-Agent", a)+ --+ -- default: @Just \"http-conduit\"@+ , getUserAgent+ , setUserAgent+ , withUserAgent+ -- ** Error handling+ -- | Function to check the status code. Note that this will run after all redirects are performed.+ -- if Nothing uses Request's 'checkStatus'+ --+ -- default: @Nothing@+ , getCheckStatus+ , setCheckStatus+ , withCheckStatus+ )+ where -import Network.HTTP.Conduit.Browser2+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as S8 (unpack)+import qualified Data.ByteString.Lazy as L+import Control.Monad.State+import Control.Exception+import qualified Control.Exception.Lifted as LE+import Data.Conduit+#if !MIN_VERSION_base(4,6,0)+import Prelude hiding (catch)+#endif+import qualified Network.HTTP.Types as HT+import qualified Network.HTTP.Types.Header as HT+import Network.Socks5 (SocksConf)+import Network.URI (URI (..), URIAuth (..), parseRelativeReference, relativeTo, uriToString)+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.Maybe (catMaybes, fromMaybe)+import qualified Data.Map as Map++import Network.HTTP.Conduit+import Control.Monad.Trans.Resource (liftResourceT)+import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Failure (Failure)++data BrowserState = BrowserState+ { currentLocation :: Maybe URI+ , maxRedirects :: Maybe Int+ , maxRetryCount :: Int+ , timeout :: Maybe Int+ , authorities :: Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)+ , cookieFilter :: Request (ResourceT IO) -> Cookie -> IO Bool+ , cookieJar :: CookieJar+ , currentProxy :: Maybe Proxy+ , currentSocksProxy :: Maybe SocksConf+ , overrideHeaders :: Map.Map HT.HeaderName BS.ByteString+ , browserCheckStatus :: Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException)+ , manager :: Manager+ } ++defaultState :: Manager -> BrowserState+defaultState m = BrowserState { currentLocation = Nothing+ , maxRedirects = Nothing+ , maxRetryCount = 0+ , timeout = Nothing+ , authorities = const Nothing+ , cookieFilter = const $ const $ return True+ , cookieJar = def+ , currentProxy = Nothing+ , currentSocksProxy = Nothing+ , overrideHeaders = Map.singleton HT.hUserAgent (fromString "http-conduit")+ , browserCheckStatus = Nothing+ , manager = m+ }++type BrowserAction = GenericBrowserAction (ResourceT IO)++type GenericBrowserAction m = StateT BrowserState m++-- | Do the browser action with the given manager+browse :: Monad m => Manager -> GenericBrowserAction m a -> m a+browse m act = evalStateT act (defaultState m)++-- | Convert an URL relative to current Location into a 'Request'+--+-- Will throw 'InvalidUrlException' on parse failures or if your Location is 'Nothing' (e.g. you haven't made any requests before)+parseRelativeUrl :: Failure HttpException m => String -> GenericBrowserAction m (Request m')+parseRelativeUrl url = maybe err (parseUrl . use) . currentLocation =<< get+ where err = throw $ InvalidUrlException url "Invalid URL"+ uri = fromMaybe err $ parseRelativeReference url+ use = flip (uriToString id) "" . fromMaybe err . relativeTo uri++-- | Make a request, using all the state in the current BrowserState+makeRequest :: (MonadBaseControl IO m, MonadResource m) => Request (ResourceT IO) -> GenericBrowserAction m (Response (ResumableSource (ResourceT IO) BS.ByteString))+makeRequest request = do+ BrowserState+ { maxRetryCount = max_retry_count+ , maxRedirects = max_redirects+ , timeout = time_out+ , currentProxy = current_proxy+ , currentSocksProxy = current_socks_proxy+ , overrideHeaders = override_headers+ , browserCheckStatus = current_check_status+ } <- get+ retryHelper (applyOverrideHeaders override_headers $+ request { redirectCount = 0+ , proxy = maybe (proxy request) Just current_proxy+ , socksProxy = maybe (socksProxy request) Just current_socks_proxy+ , checkStatus = \ _ _ -> Nothing+ , responseTimeout = maybe (responseTimeout request) Just time_out+ }) max_retry_count+ (fromMaybe (redirectCount request) max_redirects)+ (fromMaybe (checkStatus request) current_check_status)+ Nothing+ where retryHelper request' retry_count max_redirects check_status e+ | retry_count < 0 = case e of+ Just e' -> throw e'+ Nothing -> throw TooManyRetries+ | otherwise = do+ resp <- LE.catch (if max_redirects==0+ then (\(_,a,_) -> a) `fmap` performRequest request'+ else runRedirectionChain request' max_redirects [])+ (\ (e'::HttpException) -> retryHelper request' (retry_count - 1) max_redirects check_status $ Just $ toException e')+ case check_status (responseStatus resp) (responseHeaders resp) of+ Nothing -> return resp+ Just e' -> retryHelper request' (retry_count - 1) max_redirects check_status (Just e')+ performRequest request' = 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 <- liftResourceT $ http request'' manager'+ (cookie_jar'', response) <- liftIO $ updateMyCookieJar res request'' now cookie_jar' cookie_filter+ put $ s { cookieJar = cookie_jar''+ , currentLocation = Just $ getUri request''+ }+ return (request'', res, response)+ runRedirectionChain request' redirect_count ress+ | redirect_count == (-1) = throw . TooManyRedirects =<< mapM (liftIO . runResourceT . lbsResponse) ress+ | otherwise = do+ (request'', res, response) <- performRequest request'+ let code = HT.statusCode (responseStatus response)+ if code >= 300 && code < 400+ then do request''' <- case getRedirectedRequest request'' (responseHeaders response) code of+ Just a -> return a+ Nothing -> throw . UnparseableRedirect =<< (liftIO $ runResourceT $ lbsResponse response)+ runRedirectionChain request''' (redirect_count - 1) (res:ress)+ else return res+ applyAuthorities auths request' = case auths request' of+ Just (user, pass) -> applyBasicAuth user pass request'+ Nothing -> request'++-- | Make a request and pack the result as a lazy bytestring.+--+-- Note: Even though this function returns a lazy bytestring, it does not+-- utilize lazy I/O, and therefore the entire response body will live in memory.+-- If you want constant memory usage, you'll need to use the conduit package and+-- 'makeRequest' directly. +makeRequestLbs :: (MonadBaseControl IO m, MonadResource m) => Request (ResourceT IO) -> GenericBrowserAction m (Response L.ByteString)+makeRequestLbs = liftIO . runResourceT . lbsResponse <=< makeRequest++applyOverrideHeaders :: Map.Map HT.HeaderName BS.ByteString -> Request a -> Request a+applyOverrideHeaders ov request' = request' {requestHeaders = x $ requestHeaders request'}+ where x r = Map.toList $ Map.union ov (Map.fromList r)++updateMyCookieJar :: Response a -> Request (ResourceT IO) -> UTCTime -> CookieJar -> (Request (ResourceT IO) -> Cookie -> IO Bool) -> IO (CookieJar, Response a)+updateMyCookieJar 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 {responseHeaders = other_headers})+ where (set_cookie_headers, other_headers) = partition ((== (mk $ fromString "Set-Cookie")) . fst) $ 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++-- | You can save and restore the state at will+getBrowserState :: Monad m => GenericBrowserAction m BrowserState+getBrowserState = get+setBrowserState :: Monad m => BrowserState -> GenericBrowserAction m ()+setBrowserState = put+withBrowserState :: Monad m => BrowserState -> GenericBrowserAction m a -> GenericBrowserAction m a+withBrowserState s a = do+ current <- get+ put s+ out <- a+ put current+ return out++getLocation :: Monad m => GenericBrowserAction m (Maybe URI)+getLocation = get >>= \ a -> return $ currentLocation a+setLocation :: Monad m => Maybe URI -> GenericBrowserAction m ()+setLocation b = get >>= \ a -> put a {currentLocation = b}+withLocation :: Monad m => Maybe URI -> GenericBrowserAction m a -> GenericBrowserAction m a+withLocation a b = do+ current <- getLocation+ setLocation a+ out <- b+ setLocation current+ return out++getMaxRedirects :: Monad m => GenericBrowserAction m (Maybe Int)+getMaxRedirects = get >>= \ a -> return $ maxRedirects a+setMaxRedirects :: Monad m => Maybe Int -> GenericBrowserAction m ()+setMaxRedirects b = get >>= \ a -> put a {maxRedirects = b}+withMaxRedirects :: Monad m => Maybe Int -> GenericBrowserAction m a -> GenericBrowserAction m a+withMaxRedirects a b = do+ current <- getMaxRedirects+ setMaxRedirects a+ out <- b+ setMaxRedirects current+ return out++getMaxRetryCount :: Monad m => GenericBrowserAction m Int+getMaxRetryCount = get >>= \ a -> return $ maxRetryCount a+setMaxRetryCount :: Monad m => Int -> GenericBrowserAction m ()+setMaxRetryCount b = get >>= \ a -> put a {maxRetryCount = b}+withMaxRetryCount :: Monad m => Int -> GenericBrowserAction m a -> GenericBrowserAction m a+withMaxRetryCount a b = do+ current <- getMaxRetryCount+ setMaxRetryCount a+ out <- b+ setMaxRetryCount current+ return out++getTimeout :: Monad m => GenericBrowserAction m (Maybe Int)+getTimeout = get >>= \ a -> return $ timeout a+setTimeout :: Monad m => Maybe Int -> GenericBrowserAction m ()+setTimeout b = get >>= \ a -> put a {timeout = b}+withTimeout :: Monad m => Maybe Int -> GenericBrowserAction m a -> GenericBrowserAction m a+withTimeout a b = do+ current <- getTimeout+ setTimeout a+ out <- b+ setTimeout current+ return out++getAuthorities :: Monad m => GenericBrowserAction m (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString))+getAuthorities = get >>= \ a -> return $ authorities a+setAuthorities :: Monad m => (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)) -> GenericBrowserAction m ()+setAuthorities b = get >>= \ a -> put a {authorities = b}+withAuthorities :: Monad m => (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)) -> GenericBrowserAction m a -> GenericBrowserAction m a+withAuthorities a b = do+ current <- getAuthorities+ setAuthorities a+ out <- b+ setAuthorities current+ return out++getCookieFilter :: Monad m => GenericBrowserAction m (Request (ResourceT IO) -> Cookie -> IO Bool)+getCookieFilter = get >>= \ a -> return $ cookieFilter a+setCookieFilter :: Monad m => (Request (ResourceT IO) -> Cookie -> IO Bool) -> GenericBrowserAction m ()+setCookieFilter b = get >>= \ a -> put a {cookieFilter = b}+withCookieFilter :: Monad m => (Request (ResourceT IO) -> Cookie -> IO Bool) -> GenericBrowserAction m a -> GenericBrowserAction m a+withCookieFilter a b = do+ current <- getCookieFilter+ setCookieFilter a+ out <- b+ setCookieFilter current+ return out++getCookieJar :: Monad m => GenericBrowserAction m CookieJar+getCookieJar = get >>= \ a -> return $ cookieJar a+setCookieJar :: Monad m => CookieJar -> GenericBrowserAction m ()+setCookieJar b = get >>= \ a -> put a {cookieJar = b}+withCookieJar :: Monad m => CookieJar -> GenericBrowserAction m a -> GenericBrowserAction m a+withCookieJar a b = do+ current <- getCookieJar+ setCookieJar a+ out <- b+ setCookieJar current+ return out++getCurrentProxy :: Monad m => GenericBrowserAction m (Maybe Proxy)+getCurrentProxy = get >>= \ a -> return $ currentProxy a+setCurrentProxy :: Monad m => Maybe Proxy -> GenericBrowserAction m ()+setCurrentProxy b = get >>= \ a -> put a {currentProxy = b}+withCurrentProxy :: Monad m => Maybe Proxy -> GenericBrowserAction m a -> GenericBrowserAction m a+withCurrentProxy a b = do+ current <- getCurrentProxy+ setCurrentProxy a+ out <- b+ setCurrentProxy current+ return out++getCurrentSocksProxy :: Monad m => GenericBrowserAction m (Maybe SocksConf)+getCurrentSocksProxy = get >>= \ a -> return $ currentSocksProxy a+setCurrentSocksProxy :: Monad m => Maybe SocksConf -> GenericBrowserAction m ()+setCurrentSocksProxy b = get >>= \ a -> put a {currentSocksProxy = b}+withCurrentSocksProxy :: Monad m => Maybe SocksConf -> GenericBrowserAction m a -> GenericBrowserAction m a+withCurrentSocksProxy a b = do+ current <- getCurrentSocksProxy+ setCurrentSocksProxy a+ out <- b+ setCurrentSocksProxy current+ return out++getOverrideHeaders :: Monad m => GenericBrowserAction m HT.RequestHeaders+getOverrideHeaders = get >>= \ a -> return $ Map.toList $ overrideHeaders a+setOverrideHeaders :: Monad m => HT.RequestHeaders -> GenericBrowserAction m ()+setOverrideHeaders b = do+ current_user_agent <- getUserAgent+ get >>= \ a -> put a {overrideHeaders = Map.fromList b}+ setUserAgent current_user_agent+withOverrideHeaders:: Monad m => HT.RequestHeaders -> GenericBrowserAction m a -> GenericBrowserAction m a+withOverrideHeaders a b = do+ current <- getOverrideHeaders+ setOverrideHeaders a+ out <- b+ setOverrideHeaders current+ return out+getOverrideHeader :: Monad m => HT.HeaderName -> GenericBrowserAction m (Maybe BS.ByteString)+getOverrideHeader b = get >>= \ a -> return $ Map.lookup b (overrideHeaders a)+setOverrideHeader :: Monad m => HT.HeaderName -> Maybe BS.ByteString -> GenericBrowserAction m ()+setOverrideHeader b Nothing = deleteOverrideHeader b+setOverrideHeader b (Just c) = insertOverrideHeader (b, c)+insertOverrideHeader :: Monad m => HT.Header -> GenericBrowserAction m ()+insertOverrideHeader (b, c) = get >>= \ a -> put a {overrideHeaders = Map.insert b c (overrideHeaders a)}+deleteOverrideHeader :: Monad m => HT.HeaderName -> GenericBrowserAction m ()+deleteOverrideHeader b = get >>= \ a -> put a {overrideHeaders = Map.delete b (overrideHeaders a)}+withOverrideHeader :: Monad m => HT.Header -> GenericBrowserAction m a -> GenericBrowserAction m a+withOverrideHeader (a,b) c = do+ current <- getOverrideHeader a+ insertOverrideHeader (a,b)+ out <- c+ setOverrideHeader a current+ return out++getUserAgent :: Monad m => GenericBrowserAction m (Maybe BS.ByteString)+getUserAgent = get >>= \ a -> return $ Map.lookup HT.hUserAgent (overrideHeaders a)+setUserAgent :: Monad m => Maybe BS.ByteString -> GenericBrowserAction m ()+setUserAgent Nothing = deleteOverrideHeader HT.hUserAgent+setUserAgent (Just b) = insertOverrideHeader (HT.hUserAgent, b)+withUserAgent :: Monad m => Maybe BS.ByteString -> GenericBrowserAction m a -> GenericBrowserAction m a+withUserAgent a b = do+ current <- getUserAgent+ setUserAgent a+ out <- b+ setUserAgent current+ return out++getCheckStatus :: Monad m => GenericBrowserAction m (Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException))+getCheckStatus = get >>= \ a -> return $ browserCheckStatus a+setCheckStatus :: Monad m => Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException) -> GenericBrowserAction m ()+setCheckStatus b = get >>= \ a -> put a {browserCheckStatus = b}+withCheckStatus :: Monad m => Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException) -> GenericBrowserAction m a -> GenericBrowserAction m a+withCheckStatus a b = do+ current <- getCheckStatus+ setCheckStatus a+ out <- b+ setCheckStatus current+ return out++getManager :: Monad m => GenericBrowserAction m Manager+getManager = get >>= \ a -> return $ manager a+setManager :: Monad m => Manager -> GenericBrowserAction m ()+setManager b = get >>= \ a -> put a {manager = b}++-- | Extract a 'URI' from the request.+-- Canibalised from Network.HTTP.Conduit.Request, should be made visible there.+getUri :: Request m' -> URI+getUri req = URI+ { uriScheme = if secure req+ then "https:"+ else "http:"+ , uriAuthority = Just URIAuth+ { uriUserInfo = ""+ , uriRegName = S8.unpack $ host req+ , uriPort = ':' : show (port req)+ }+ , uriPath = S8.unpack $ path req+ , uriQuery = S8.unpack $ queryString req+ , uriFragment = ""+ }
− Network/HTTP/Conduit/Browser2.hs
@@ -1,551 +0,0 @@-{-# LANGUAGE CPP, ScopedTypeVariables, FlexibleContexts #-}--- | This module is designed to work similarly to the Network.Browser module in the HTTP package.--- The idea is that there are two new types defined: 'BrowserState' and 'BrowserAction'. The--- purpose of this module is to make it easy to describe a browsing session, including navigating--- to multiple pages, and have things like cookie jar updates work as expected as you browse--- around.------ BrowserAction is a monad that handles all your browser-related activities. This monad is--- actually implemented as a specialization of the State monad, over the BrowserState type. The--- BrowserState type has various bits of information that a web browser keeps, such as a current--- cookie jar, the number of times to retry a request on failure, HTTP proxy information, etc. In--- the BrowserAction monad, there is one BrowserState at any given time, and you can modify it by--- using the convenience functions in this module.------ A special kind of modification of the current browser state is the action of making a HTTP--- request. This will do the request according to the params in the current BrowserState, as well--- as modifying the current state with, for example, an updated cookie jar and location.------ To use this module, you would bind together a series of BrowserActions (This simulates the user--- clicking on links or using a settings dialogue etc.) to describe your browsing session. When--- you've described your session, you call 'browse' on your top-level BrowserAction to actually--- convert your actions into the ResourceT IO monad.------ Note that the module will be renamed to Network.HTTP.Conduit.Browser a month or so after release.--- This is to give users migration time without name clashes.------ Here is an example program:------ > import qualified Data.ByteString as B--- > import qualified Data.ByteString.Lazy as LB--- > import qualified Data.ByteString.UTF8 as UB--- > import Data.Conduit--- > import Network.HTTP.Conduit--- > import Network.HTTP.Conduit.Browser--- > --- > -- The web request to log in to a service--- > req1 :: IO (Request (ResourceT IO))--- > req1 = do--- > req <- parseUrl "http://www.myurl.com/login.php"--- > return $ urlEncodedBody [ (UB.fromString "name", UB.fromString "litherum")--- > , (UB.fromString "pass", UB.fromString "S33kRe7")--- > ] req--- > --- > -- Once authenticated, run this request--- > req2 :: IO (Request m')--- > req2 = parseUrl "http://www.myurl.com/main.php"--- > --- > -- Bind two BrowserActions together--- > action :: Request (ResourceT IO) -> Request (ResourceT IO) -> BrowserAction (Response LB.ByteString)--- > action r1 r2 = do--- > _ <- makeRequestLbs r1--- > makeRequestLbs r2--- > --- > main :: IO ()--- > main = do--- > man <- newManager def--- > r1 <- req1--- > r2 <- req2--- > out <- runResourceT $ browse man $ action r1 r2--- > putStrLn $ UB.toString $ B.concat $ LB.toChunks $ responseBody out--module Network.HTTP.Conduit.Browser2- (- -- * Main- BrowserAction- , GenericBrowserAction- , browse- , parseRelativeUrl- , makeRequest- , makeRequestLbs- -- * Browser state- , BrowserState- , defaultState- , getBrowserState- , setBrowserState- , withBrowserState- -- ** Manager- , Manager- , getManager- , setManager- -- ** Location- -- | The last visited url (similar to the location bar in mainstream browsers).- -- Location is updated on every request.- --- -- default: @Nothing@- , getLocation- , setLocation- , withLocation- -- ** Cookies- -- *** Cookie jar- -- | All the cookies!- , getCookieJar- , setCookieJar- , withCookieJar- -- *** Cookie filter- -- | Each new Set-Cookie the browser encounters will pass through this filter.- -- Only cookies that pass the filter (and are already valid) will be allowed into the cookie jar- --- -- default: @const $ const $ return True@- , getCookieFilter- , setCookieFilter- , withCookieFilter- -- ** Proxies- -- *** HTTP- -- | An optional proxy to send all requests through- -- if Nothing uses Request's 'proxy'- --- -- default: @Nothing@- , getCurrentProxy- , setCurrentProxy- , withCurrentProxy- -- *** SOCKS- -- | An optional SOCKS proxy to send all requests through- -- if Nothing uses Request's 'socksProxy'- --- -- default: @Nothing@- , getCurrentSocksProxy- , setCurrentSocksProxy- , withCurrentSocksProxy- -- ** Redirects- -- | The number of redirects to allow.- -- if Nothing uses Request's 'redirectCount'- --- -- default: @Nothing@- , getMaxRedirects- , setMaxRedirects- , withMaxRedirects- -- ** Retries- -- | The number of times to retry a failed connection- --- -- default: @0@- , getMaxRetryCount- , setMaxRetryCount- , withMaxRetryCount- -- ** Timeout- -- | Number of microseconds to wait for a response.- -- if Nothing uses Request's 'responseTimeout'- --- -- default: @Nothing@- , getTimeout- , setTimeout- , withTimeout- -- ** Authorities- -- | A user-provided function that provides optional authorities.- -- This function gets run on all requests before they get sent out.- -- The output of this function is applied to the request.- --- -- default: @const Nothing@- , getAuthorities- , setAuthorities- , withAuthorities- -- ** Headers- -- *** Override headers- -- | Specifies Headers that should be added to 'Request',- -- these will override Headers already specified in 'requestHeaders'.- --- -- > do insertOverrideHeader ("User-Agent", "rat")- -- > insertOverrideHeader ("Connection", "keep-alive")- -- > makeRequest def{requestHeaders = [("User-Agent", "kitten"), ("Accept", "everything/digestible")]}- -- > > User-Agent: rat- -- > > Accept: everything/digestible- -- > > Connection: keep-alive- , getOverrideHeaders- , setOverrideHeaders- , withOverrideHeaders- , getOverrideHeader- , setOverrideHeader- , insertOverrideHeader- , deleteOverrideHeader- , withOverrideHeader- -- *** User agent- -- | What string to report our user-agent as.- -- if Nothing will not send user-agent unless one is specified in 'Request'- --- -- > getUserAgent = lookup "User-Agent" overrideHeaders- -- > setUserAgent a = insertOverrideHeader ("User-Agent", a)- --- -- default: @Just \"http-conduit\"@- , getUserAgent- , setUserAgent- , withUserAgent- -- ** Error handling- -- | Function to check the status code. Note that this will run after all redirects are performed.- -- if Nothing uses Request's 'checkStatus'- --- -- default: @Nothing@- , getCheckStatus- , setCheckStatus- , withCheckStatus- )- where--import qualified Data.ByteString as BS-import qualified Data.ByteString.Char8 as S8 (unpack)-import qualified Data.ByteString.Lazy as L-import Control.Monad.State-import Control.Exception-import qualified Control.Exception.Lifted as LE-import Data.Conduit-#if !MIN_VERSION_base(4,6,0)-import Prelude hiding (catch)-#endif-import qualified Network.HTTP.Types as HT-import qualified Network.HTTP.Types.Header as HT-import Network.Socks5 (SocksConf)-import Network.URI (URI (..), URIAuth (..), parseRelativeReference, relativeTo, uriToString)-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.Maybe (catMaybes, fromMaybe)-import qualified Data.Map as Map--import Network.HTTP.Conduit-import Control.Monad.Trans.Resource (liftResourceT)-import Control.Monad.Trans.Control (MonadBaseControl)-import Control.Failure (Failure)--data BrowserState = BrowserState- { currentLocation :: Maybe URI- , maxRedirects :: Maybe Int- , maxRetryCount :: Int- , timeout :: Maybe Int- , authorities :: Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)- , cookieFilter :: Request (ResourceT IO) -> Cookie -> IO Bool- , cookieJar :: CookieJar- , currentProxy :: Maybe Proxy- , currentSocksProxy :: Maybe SocksConf- , overrideHeaders :: Map.Map HT.HeaderName BS.ByteString- , browserCheckStatus :: Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException)- , manager :: Manager- } --defaultState :: Manager -> BrowserState-defaultState m = BrowserState { currentLocation = Nothing- , maxRedirects = Nothing- , maxRetryCount = 0- , timeout = Nothing- , authorities = const Nothing- , cookieFilter = const $ const $ return True- , cookieJar = def- , currentProxy = Nothing- , currentSocksProxy = Nothing- , overrideHeaders = Map.singleton HT.hUserAgent (fromString "http-conduit")- , browserCheckStatus = Nothing- , manager = m- }--type BrowserAction = GenericBrowserAction (ResourceT IO)--type GenericBrowserAction m = StateT BrowserState m---- | Do the browser action with the given manager-browse :: Monad m => Manager -> GenericBrowserAction m a -> m a-browse m act = evalStateT act (defaultState m)---- | Convert an URL relative to current Location into a 'Request'------ Will throw 'InvalidUrlException' on parse failures or if your Location is 'Nothing' (e.g. you haven't made any requests before)-parseRelativeUrl :: Failure HttpException m => String -> GenericBrowserAction m (Request m')-parseRelativeUrl url = maybe err (parseUrl . use) . currentLocation =<< get- where err = throw $ InvalidUrlException url "Invalid URL"- uri = fromMaybe err $ parseRelativeReference url- use = flip (uriToString id) "" . fromMaybe err . relativeTo uri---- | Make a request, using all the state in the current BrowserState-makeRequest :: (MonadBaseControl IO m, MonadResource m) => Request (ResourceT IO) -> GenericBrowserAction m (Response (ResumableSource (ResourceT IO) BS.ByteString))-makeRequest request = do- BrowserState- { maxRetryCount = max_retry_count- , maxRedirects = max_redirects- , timeout = time_out- , currentProxy = current_proxy- , currentSocksProxy = current_socks_proxy- , overrideHeaders = override_headers- , browserCheckStatus = current_check_status- } <- get- retryHelper (applyOverrideHeaders override_headers $- request { redirectCount = 0- , proxy = maybe (proxy request) Just current_proxy- , socksProxy = maybe (socksProxy request) Just current_socks_proxy- , checkStatus = \ _ _ -> Nothing- , responseTimeout = maybe (responseTimeout request) Just time_out- }) max_retry_count- (fromMaybe (redirectCount request) max_redirects)- (fromMaybe (checkStatus request) current_check_status)- Nothing- where retryHelper request' retry_count max_redirects check_status e- | retry_count < 0 = case e of- Just e' -> throw e'- Nothing -> throw TooManyRetries- | otherwise = do- resp <- LE.catch (if max_redirects==0- then (\(_,a,_) -> a) `fmap` performRequest request'- else runRedirectionChain request' max_redirects [])- (\ (e'::HttpException) -> retryHelper request' (retry_count - 1) max_redirects check_status $ Just $ toException e')- case check_status (responseStatus resp) (responseHeaders resp) of- Nothing -> return resp- Just e' -> retryHelper request' (retry_count - 1) max_redirects check_status (Just e')- performRequest request' = 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 <- liftResourceT $ http request'' manager'- (cookie_jar'', response) <- liftIO $ updateMyCookieJar res request'' now cookie_jar' cookie_filter- put $ s { cookieJar = cookie_jar''- , currentLocation = Just $ getUri request''- }- return (request'', res, response)- runRedirectionChain request' redirect_count ress- | redirect_count == (-1) = throw . TooManyRedirects =<< mapM (liftIO . runResourceT . lbsResponse) ress- | otherwise = do- (request'', res, response) <- performRequest request'- let code = HT.statusCode (responseStatus response)- if code >= 300 && code < 400- then do request''' <- case getRedirectedRequest request'' (responseHeaders response) code of- Just a -> return a- Nothing -> throw . UnparseableRedirect =<< (liftIO $ runResourceT $ lbsResponse response)- runRedirectionChain request''' (redirect_count - 1) (res:ress)- else return res- applyAuthorities auths request' = case auths request' of- Just (user, pass) -> applyBasicAuth user pass request'- Nothing -> request'---- | Make a request and pack the result as a lazy bytestring.------ Note: Even though this function returns a lazy bytestring, it does not--- utilize lazy I/O, and therefore the entire response body will live in memory.--- If you want constant memory usage, you'll need to use the conduit package and--- 'makeRequest' directly. -makeRequestLbs :: (MonadBaseControl IO m, MonadResource m) => Request (ResourceT IO) -> GenericBrowserAction m (Response L.ByteString)-makeRequestLbs = liftIO . runResourceT . lbsResponse <=< makeRequest--applyOverrideHeaders :: Map.Map HT.HeaderName BS.ByteString -> Request a -> Request a-applyOverrideHeaders ov request' = request' {requestHeaders = x $ requestHeaders request'}- where x r = Map.toList $ Map.union ov (Map.fromList r)--updateMyCookieJar :: Response a -> Request (ResourceT IO) -> UTCTime -> CookieJar -> (Request (ResourceT IO) -> Cookie -> IO Bool) -> IO (CookieJar, Response a)-updateMyCookieJar 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 {responseHeaders = other_headers})- where (set_cookie_headers, other_headers) = partition ((== (mk $ fromString "Set-Cookie")) . fst) $ 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---- | You can save and restore the state at will-getBrowserState :: Monad m => GenericBrowserAction m BrowserState-getBrowserState = get-setBrowserState :: Monad m => BrowserState -> GenericBrowserAction m ()-setBrowserState = put-withBrowserState :: Monad m => BrowserState -> GenericBrowserAction m a -> GenericBrowserAction m a-withBrowserState s a = do- current <- get- put s- out <- a- put current- return out--getLocation :: Monad m => GenericBrowserAction m (Maybe URI)-getLocation = get >>= \ a -> return $ currentLocation a-setLocation :: Monad m => Maybe URI -> GenericBrowserAction m ()-setLocation b = get >>= \ a -> put a {currentLocation = b}-withLocation :: Monad m => Maybe URI -> GenericBrowserAction m a -> GenericBrowserAction m a-withLocation a b = do- current <- getLocation- setLocation a- out <- b- setLocation current- return out--getMaxRedirects :: Monad m => GenericBrowserAction m (Maybe Int)-getMaxRedirects = get >>= \ a -> return $ maxRedirects a-setMaxRedirects :: Monad m => Maybe Int -> GenericBrowserAction m ()-setMaxRedirects b = get >>= \ a -> put a {maxRedirects = b}-withMaxRedirects :: Monad m => Maybe Int -> GenericBrowserAction m a -> GenericBrowserAction m a-withMaxRedirects a b = do- current <- getMaxRedirects- setMaxRedirects a- out <- b- setMaxRedirects current- return out--getMaxRetryCount :: Monad m => GenericBrowserAction m Int-getMaxRetryCount = get >>= \ a -> return $ maxRetryCount a-setMaxRetryCount :: Monad m => Int -> GenericBrowserAction m ()-setMaxRetryCount b = get >>= \ a -> put a {maxRetryCount = b}-withMaxRetryCount :: Monad m => Int -> GenericBrowserAction m a -> GenericBrowserAction m a-withMaxRetryCount a b = do- current <- getMaxRetryCount- setMaxRetryCount a- out <- b- setMaxRetryCount current- return out--getTimeout :: Monad m => GenericBrowserAction m (Maybe Int)-getTimeout = get >>= \ a -> return $ timeout a-setTimeout :: Monad m => Maybe Int -> GenericBrowserAction m ()-setTimeout b = get >>= \ a -> put a {timeout = b}-withTimeout :: Monad m => Maybe Int -> GenericBrowserAction m a -> GenericBrowserAction m a-withTimeout a b = do- current <- getTimeout- setTimeout a- out <- b- setTimeout current- return out--getAuthorities :: Monad m => GenericBrowserAction m (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString))-getAuthorities = get >>= \ a -> return $ authorities a-setAuthorities :: Monad m => (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)) -> GenericBrowserAction m ()-setAuthorities b = get >>= \ a -> put a {authorities = b}-withAuthorities :: Monad m => (Request (ResourceT IO) -> Maybe (BS.ByteString, BS.ByteString)) -> GenericBrowserAction m a -> GenericBrowserAction m a-withAuthorities a b = do- current <- getAuthorities- setAuthorities a- out <- b- setAuthorities current- return out--getCookieFilter :: Monad m => GenericBrowserAction m (Request (ResourceT IO) -> Cookie -> IO Bool)-getCookieFilter = get >>= \ a -> return $ cookieFilter a-setCookieFilter :: Monad m => (Request (ResourceT IO) -> Cookie -> IO Bool) -> GenericBrowserAction m ()-setCookieFilter b = get >>= \ a -> put a {cookieFilter = b}-withCookieFilter :: Monad m => (Request (ResourceT IO) -> Cookie -> IO Bool) -> GenericBrowserAction m a -> GenericBrowserAction m a-withCookieFilter a b = do- current <- getCookieFilter- setCookieFilter a- out <- b- setCookieFilter current- return out--getCookieJar :: Monad m => GenericBrowserAction m CookieJar-getCookieJar = get >>= \ a -> return $ cookieJar a-setCookieJar :: Monad m => CookieJar -> GenericBrowserAction m ()-setCookieJar b = get >>= \ a -> put a {cookieJar = b}-withCookieJar :: Monad m => CookieJar -> GenericBrowserAction m a -> GenericBrowserAction m a-withCookieJar a b = do- current <- getCookieJar- setCookieJar a- out <- b- setCookieJar current- return out--getCurrentProxy :: Monad m => GenericBrowserAction m (Maybe Proxy)-getCurrentProxy = get >>= \ a -> return $ currentProxy a-setCurrentProxy :: Monad m => Maybe Proxy -> GenericBrowserAction m ()-setCurrentProxy b = get >>= \ a -> put a {currentProxy = b}-withCurrentProxy :: Monad m => Maybe Proxy -> GenericBrowserAction m a -> GenericBrowserAction m a-withCurrentProxy a b = do- current <- getCurrentProxy- setCurrentProxy a- out <- b- setCurrentProxy current- return out--getCurrentSocksProxy :: Monad m => GenericBrowserAction m (Maybe SocksConf)-getCurrentSocksProxy = get >>= \ a -> return $ currentSocksProxy a-setCurrentSocksProxy :: Monad m => Maybe SocksConf -> GenericBrowserAction m ()-setCurrentSocksProxy b = get >>= \ a -> put a {currentSocksProxy = b}-withCurrentSocksProxy :: Monad m => Maybe SocksConf -> GenericBrowserAction m a -> GenericBrowserAction m a-withCurrentSocksProxy a b = do- current <- getCurrentSocksProxy- setCurrentSocksProxy a- out <- b- setCurrentSocksProxy current- return out--getOverrideHeaders :: Monad m => GenericBrowserAction m HT.RequestHeaders-getOverrideHeaders = get >>= \ a -> return $ Map.toList $ overrideHeaders a-setOverrideHeaders :: Monad m => HT.RequestHeaders -> GenericBrowserAction m ()-setOverrideHeaders b = do- current_user_agent <- getUserAgent- get >>= \ a -> put a {overrideHeaders = Map.fromList b}- setUserAgent current_user_agent-withOverrideHeaders:: Monad m => HT.RequestHeaders -> GenericBrowserAction m a -> GenericBrowserAction m a-withOverrideHeaders a b = do- current <- getOverrideHeaders- setOverrideHeaders a- out <- b- setOverrideHeaders current- return out-getOverrideHeader :: Monad m => HT.HeaderName -> GenericBrowserAction m (Maybe BS.ByteString)-getOverrideHeader b = get >>= \ a -> return $ Map.lookup b (overrideHeaders a)-setOverrideHeader :: Monad m => HT.HeaderName -> Maybe BS.ByteString -> GenericBrowserAction m ()-setOverrideHeader b Nothing = deleteOverrideHeader b-setOverrideHeader b (Just c) = insertOverrideHeader (b, c)-insertOverrideHeader :: Monad m => HT.Header -> GenericBrowserAction m ()-insertOverrideHeader (b, c) = get >>= \ a -> put a {overrideHeaders = Map.insert b c (overrideHeaders a)}-deleteOverrideHeader :: Monad m => HT.HeaderName -> GenericBrowserAction m ()-deleteOverrideHeader b = get >>= \ a -> put a {overrideHeaders = Map.delete b (overrideHeaders a)}-withOverrideHeader :: Monad m => HT.Header -> GenericBrowserAction m a -> GenericBrowserAction m a-withOverrideHeader (a,b) c = do- current <- getOverrideHeader a- insertOverrideHeader (a,b)- out <- c- setOverrideHeader a current- return out--getUserAgent :: Monad m => GenericBrowserAction m (Maybe BS.ByteString)-getUserAgent = get >>= \ a -> return $ Map.lookup HT.hUserAgent (overrideHeaders a)-setUserAgent :: Monad m => Maybe BS.ByteString -> GenericBrowserAction m ()-setUserAgent Nothing = deleteOverrideHeader HT.hUserAgent-setUserAgent (Just b) = insertOverrideHeader (HT.hUserAgent, b)-withUserAgent :: Monad m => Maybe BS.ByteString -> GenericBrowserAction m a -> GenericBrowserAction m a-withUserAgent a b = do- current <- getUserAgent- setUserAgent a- out <- b- setUserAgent current- return out--getCheckStatus :: Monad m => GenericBrowserAction m (Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException))-getCheckStatus = get >>= \ a -> return $ browserCheckStatus a-setCheckStatus :: Monad m => Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException) -> GenericBrowserAction m ()-setCheckStatus b = get >>= \ a -> put a {browserCheckStatus = b}-withCheckStatus :: Monad m => Maybe (HT.Status -> HT.ResponseHeaders -> Maybe SomeException) -> GenericBrowserAction m a -> GenericBrowserAction m a-withCheckStatus a b = do- current <- getCheckStatus- setCheckStatus a- out <- b- setCheckStatus current- return out--getManager :: Monad m => GenericBrowserAction m Manager-getManager = get >>= \ a -> return $ manager a-setManager :: Monad m => Manager -> GenericBrowserAction m ()-setManager b = get >>= \ a -> put a {manager = b}---- | Extract a 'URI' from the request.--- Canibalised from Network.HTTP.Conduit.Request, should be made visible there.-getUri :: Request m' -> URI-getUri req = URI- { uriScheme = if secure req- then "https:"- else "http:"- , uriAuthority = Just URIAuth- { uriUserInfo = ""- , uriRegName = S8.unpack $ host req- , uriPort = ':' : show (port req)- }- , uriPath = S8.unpack $ path req- , uriQuery = S8.unpack $ queryString req- , uriFragment = ""- }
http-conduit-browser.cabal view
@@ -1,5 +1,5 @@ name: http-conduit-browser-version: 1.6.6+version: 1.7.0 license: BSD3 license-file: LICENSE author: Myles C. Maxfield <myles.maxfield@gmail.com>@@ -8,9 +8,7 @@ description: This package creates a monad representing things that browsers do, letting you elegantly describe a browsing session. This package wraps- the http-conduit package by Michael Snoyman. Note that the module will- be renamed to Network.HTTP.Conduit.Browser a month or so after release.- This is to give users migration time without name clashes.+ the http-conduit package by Michael Snoyman. category: Web, Conduit stability: Stable cabal-version: >= 1.8@@ -22,11 +20,9 @@ type: git location: git://github.com/exbb2/http-conduit-browser.git -flag new-http-conduit- default: True- library build-depends: base >= 4 && < 5+ , http-conduit >= 1.7.0 , data-default , cookie , utf8-string@@ -43,12 +39,7 @@ , failure , monad-control , resourcet >= 0.3 && < 0.5- if flag(new-http-conduit)- build-depends: http-conduit >= 1.7 && < 1.9- exposed-modules: Network.HTTP.Conduit.Browser- else- build-depends: http-conduit >= 1.6.1 && < 1.7- exposed-modules: Network.HTTP.Conduit.Browser2+ exposed-modules: Network.HTTP.Conduit.Browser ghc-options: -Wall test-suite test
test/main.hs view
@@ -9,7 +9,7 @@ import Network.Wai hiding (requestBody) import Network.Wai.Handler.Warp (run) import Network.HTTP.Conduit-import Network.HTTP.Conduit.Browser2+import Network.HTTP.Conduit.Browser import Data.ByteString.Base64 (encode) import Data.Typeable (Typeable) import Control.Concurrent (forkIO, killThread)