http-client 0.4.31.2 → 0.7.19
raw patch · 28 files changed
Files
- ChangeLog.md +239/−3
- Data/KeyedPool.hs +325/−0
- Network/HTTP/Client.hs +104/−25
- Network/HTTP/Client/Body.hs +69/−38
- Network/HTTP/Client/Connection.hs +112/−47
- Network/HTTP/Client/Cookies.hs +39/−7
- Network/HTTP/Client/Core.hs +130/−94
- Network/HTTP/Client/Headers.hs +83/−33
- Network/HTTP/Client/Internal.hs +2/−0
- Network/HTTP/Client/Manager.hs +119/−342
- Network/HTTP/Client/MultipartFormData.hs +35/−28
- Network/HTTP/Client/Request.hs +217/−135
- Network/HTTP/Client/Response.hs +84/−27
- Network/HTTP/Client/Types.hs +396/−127
- Network/HTTP/Client/Util.hs +9/−175
- Network/HTTP/Proxy.hs +380/−0
- README.md +1/−1
- http-client.cabal +37/−12
- publicsuffixlist/Network/PublicSuffixList/Serialize.hs +2/−2
- test-nonet/Network/HTTP/Client/BodySpec.hs +57/−15
- test-nonet/Network/HTTP/Client/ConnectionSpec.hs +23/−0
- test-nonet/Network/HTTP/Client/CookieSpec.hs +51/−2
- test-nonet/Network/HTTP/Client/HeadersSpec.hs +76/−8
- test-nonet/Network/HTTP/Client/RequestBodySpec.hs +4/−4
- test-nonet/Network/HTTP/Client/RequestSpec.hs +56/−16
- test-nonet/Network/HTTP/Client/ResponseSpec.hs +3/−3
- test-nonet/Network/HTTP/ClientSpec.hs +225/−80
- test/Network/HTTP/ClientSpec.hs +127/−30
ChangeLog.md view
@@ -1,11 +1,247 @@-## 0.4.31.2+# Changelog for http-client -* Use redirectCount set through managerModifyRequest [#244](https://github.com/snoyberg/http-client/pull/244)+## 0.7.19 -## 0.4.31.1+* Make mockable via `Network.HTTP.Client.Internal.requestAction` [#554](https://github.com/snoyberg/http-client/pull/554) +## 0.7.18++* Add the `managerSetMaxNumberHeaders` function to the `Client` module to configure `managerMaxNumberHeaders` in `ManagerSettings`.++## 0.7.17++* Add `managerSetMaxHeaderLength` to `Client` to change `ManagerSettings` `MaxHeaderLength`.++## 0.7.16++* Add `responseEarlyHints` field to `Response`, containing a list of all HTTP 103 Early Hints headers received from the server.+* Add `earlyHintHeadersReceived` callback to `Request`, which will be called on each HTTP 103 Early Hints header section received.++## 0.7.15++* Adds `shouldStripHeaderOnRedirectIfOnDifferentHostOnly` option to `Request` [#520](https://github.com/snoyberg/http-client/pull/520)++## 0.7.14++* Allow customizing max header length [#514](https://github.com/snoyberg/http-client/pull/514)++## 0.7.13++* Create the ability to redact custom header values to censor sensitive information++## 0.7.12++* Fix premature connection closing due to weak reference lifetimes [#490](https://github.com/snoyberg/http-client/pull/490)++## 0.7.11++* Allow making requests to raw IPv6 hosts [#477](https://github.com/snoyberg/http-client/pull/477)+* Catch "resource vanished" exception on initial response read [#480](https://github.com/snoyberg/http-client/pull/480)+* Search for reachable IP addresses asynchronously (RFC 6555, 8305) after calling `getAddrInfo` to reduce latency [#472](https://github.com/snoyberg/http-client/pull/472).++## 0.7.10++* Consume trailers and last CRLF of chunked body. The trailers are not exposed,+ unless the raw body is requested.++## 0.7.9++* Exceptions from streamed request body now cause the request to fail. Previously they were+ routed through onRequestBodyException and, by default, the IOExceptions were discarded.++## 0.7.8++* Include the original `Request` in the `Response`. Expose it via `getOriginalRequest`.++## 0.7.7++* Allow secure cookies for localhost without HTTPS [#460](https://github.com/snoyberg/http-client/pull/460)++## 0.7.6++* Add `applyBearerAuth` function [#457](https://github.com/snoyberg/http-client/pull/457/files)++## 0.7.5++* Force closing connections in case of exceptions throwing [#454](https://github.com/snoyberg/http-client/pull/454).++## 0.7.4++* Depend on base64-bytestring instead of memory [#453](https://github.com/snoyberg/http-client/pull/453)++## 0.7.3++* Added `withSocket` to `Network.HTTP.Client.Connection`.++## 0.7.2.1++* Fix bug in `useProxySecureWithoutConnect`.++## 0.7.2++* Add a new proxy mode, proxySecureWithoutConnect, for sending HTTPS requests in plain text to a proxy without using the CONNECT method.++## 0.7.1++* Remove `AI_ADDRCONFIG` [#400](https://github.com/snoyberg/http-client/issues/400)++## 0.7.0++* Remove Eq instances for Cookie, CookieJar, Response, Ord instance for Cookie [#435](https://github.com/snoyberg/http-client/pull/435)++## 0.6.4.1++* Win32 2.8 support [#430](https://github.com/snoyberg/http-client/pull/430)++## 0.6.4++* Avoid throwing an exception when a malformed HTTP header is received,+ to be as robust as commonly used HTTP clients.+ See [#398](https://github.com/snoyberg/http-client/issues/398)++## 0.6.3++* Detect response body termination before reading an extra null chunk+ when possible. This allows connections to be reused in some corner+ cases. See+ [#395](https://github.com/snoyberg/http-client/issues/395)++## 0.6.2++* Add `shouldStripHeaderOnRedirect` option to `Request` [#300](https://github.com/snoyberg/http-client/issues/300)++## 0.6.1.1++* Ensure that `Int` parsing doesn't overflow [#383](https://github.com/snoyberg/http-client/issues/383)++## 0.6.1++* Add `setUriEither` to `Network.HTTP.Client.Internal`++## 0.6.0++* Generalize `renderParts` over arbitrary applicative functors. One particular+ use case that is enabled by this change is that now `renderParts` can be used+ in pure code by using it in combination with `runIdentity`.++## 0.5.14++* Omit port for `getUri` when protocol is `http` and port is `80`, or when+ protocol is `https` and port is `443`+* Sending requests with invalid headers now throws InvalidRequestHeader exception++## 0.5.13.1++* Add a workaround for a cabal bug [haskell-infra/hackage-trustees#165](https://github.com/haskell-infra/hackage-trustees/issues/165)++## 0.5.13++* Adds `setRequestCheckStatus` and `throwErrorStatusCodes` functions.+ See [#304](https://github.com/snoyberg/http-client/issues/304)+* Add `withConnection` function.+ See [#352](https://github.com/snoyberg/http-client/pull/352).++## 0.5.12.1++* Make the chunked transfer-encoding detection case insensitive+ [#303](https://github.com/snoyberg/http-client/pull/303)+* Remove some unneeded language extensions+* Mark older versions of GHC as unsupported++## 0.5.12++* Added `requestFromURI` and `requestFromURI_` functions.+* Fixed non-TLS connections going though proxy [#337](https://github.com/snoyberg/http-client/issues/337)++## 0.5.11++* Replaced `base64-bytestring` dependency with `memory`.++## 0.5.10++* New function to partial escape query strings++## 0.5.9++* Add `Semigroup` instances for GHC 8.4 [#320](https://github.com/snoyberg/http-client/pull/320)++## 0.5.8++* Switch to the new STM-based manager+ [#254](https://github.com/snoyberg/http-client/pull/254)+* Redact sensitive headers [#318](https://github.com/snoyberg/http-client/pull/318)++## 0.5.7.1++* Code cleanup/delete dead code+* Compat with Win32 2.6 [#309](https://github.com/snoyberg/http-client/issues/309)++## 0.5.7.0++* Support for Windows system proxy settings+ [#274](https://github.com/snoyberg/http-client/pull/274)++## 0.5.6.1++* Revert socks5 and socks5h support from+ [#262](https://github.com/snoyberg/http-client/pull/262); the support was+ untested and did not work as intended.++## 0.5.6++* Added socks5 and socks5h support [#262](https://github.com/snoyberg/http-client/pull/262)++## 0.5.5++* http-client should allow to log requests and responses [#248](https://github.com/snoyberg/http-client/issues/248)++## 0.5.4++* Derive ‘Eq’ for ‘ResponseTimeout’ [#239](https://github.com/snoyberg/http-client/pull/239)++## 0.5.3.4++* Doc improvements++## 0.5.3.3++* Add missing colon in Host header [#235](https://github.com/snoyberg/http-client/pull/235)++## 0.5.3.2++* Minor doc updates++## 0.5.3.1+ * The closeConnection method for tls connections should not be called multiple times [#225](https://github.com/snoyberg/http-client/issues/225)++## 0.5.3++* Expose `makeConnection` and `socketConnection` as a stable API [#223](https://github.com/snoyberg/http-client/issues/223)++## 0.5.2++* Enable rawConnectionModifySocketSize to expose openSocketConnectionSize [#218](https://github.com/snoyberg/http-client/pull/218)++## 0.5.1++* Enable managerModifyRequest to modify redirectCount [#208](https://github.com/snoyberg/http-client/pull/208)++## 0.5.0.1++* Doc fix++## 0.5.0++* Remove `instance Default Request`+* Modify `instance IsString Request` to use `parseRequest` instead of `parseUrlThrow`+* Clean up the `HttpException` constructors+* Rename `checkStatus` to `checkResponse` and modify type+* Fix the ugly magic constant workaround for responseTimeout+* Remove `getConnectionWrapper`+* Add the `HttpExceptionRequest` wrapper so that all exceptions related to a+ request are thrown with that request's information ## 0.4.31
+ Data/KeyedPool.hs view
@@ -0,0 +1,325 @@+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Similar to Data.Pool from resource-pool, but resources are+-- identified by some key. To clarify semantics of this module:+--+-- * The pool holds onto and tracks idle resources. Active resources+-- (those checked out via 'takeKeyedPool') are not tracked at all by+-- 'KeyedPool' itself.+--+-- * The pool limits the number of idle resources per key and the+-- total number of idle resources.+--+-- * There is no limit placed on /active/ resources. As such: there+-- will be no delay when calling 'takeKeyedPool': it will either use+-- an idle resource already present, or create a new one+-- immediately.+--+-- * Once the garbage collector cleans up the 'kpAlive' value, the+-- pool will be shut down, by placing a 'PoolClosed' into the+-- 'kpVar' and destroying all existing idle connection.+--+-- * A reaper thread will destroy unused idle resources regularly. It+-- will stop running once 'kpVar' contains a 'PoolClosed' value.+--+-- * 'takeKeyedPool' is async exception safe, but relies on the+-- /caller/ to ensure prompt cleanup. See its comment for more+-- information.+module Data.KeyedPool+ ( KeyedPool+ , createKeyedPool+ , takeKeyedPool+ , Managed+ , managedResource+ , managedReused+ , managedRelease+ , keepAlive+ , Reuse (..)+ , dummyManaged+ ) where++import Control.Concurrent (forkIOWithUnmask, threadDelay)+import Control.Concurrent.STM+import Control.Exception (mask_, catch, SomeException)+import Control.Monad (join, unless, void)+import Data.Map (Map)+import Data.Maybe (isJust)+import qualified Data.Map.Strict as Map+import Data.Time (UTCTime, getCurrentTime, addUTCTime)+import Data.IORef (IORef, newIORef, mkWeakIORef, readIORef)+import qualified Data.Foldable as F+import GHC.Conc (unsafeIOToSTM)+import System.IO.Unsafe (unsafePerformIO)++data KeyedPool key resource = KeyedPool+ { kpCreate :: !(key -> IO resource)+ , kpDestroy :: !(resource -> IO ())+ , kpMaxPerKey :: !Int+ , kpMaxTotal :: !Int+ , kpVar :: !(TVar (PoolMap key resource))+ , kpAlive :: !(IORef ())+ }++data PoolMap key resource+ = PoolClosed+ | PoolOpen+ -- Total number of resources in the pool+ {-# UNPACK #-} !Int+ !(Map key (PoolList resource))+ deriving F.Foldable++-- | A non-empty list which keeps track of its own length and when+-- each resource was created.+data PoolList a+ = One a {-# UNPACK #-} !UTCTime+ | Cons+ a++ -- size of the list from this point and on+ {-# UNPACK #-} !Int++ {-# UNPACK #-} !UTCTime+ !(PoolList a)+ deriving F.Foldable++plistToList :: PoolList a -> [(UTCTime, a)]+plistToList (One a t) = [(t, a)]+plistToList (Cons a _ t plist) = (t, a) : plistToList plist++plistFromList :: [(UTCTime, a)] -> Maybe (PoolList a)+plistFromList [] = Nothing+plistFromList [(t, a)] = Just (One a t)+plistFromList xs =+ Just . snd . go $ xs+ where+ go [] = error "plistFromList.go []"+ go [(t, a)] = (2, One a t)+ go ((t, a):rest) =+ let (i, rest') = go rest+ i' = i + 1+ in i' `seq` (i', Cons a i t rest')++-- | Create a new 'KeyedPool' which will automatically clean up after+-- itself when all referenced to the 'KeyedPool' are gone. It will+-- also fork a reaper thread to regularly kill off unused resource.+createKeyedPool+ :: Ord key+ => (key -> IO resource) -- ^ create a new resource+ -> (resource -> IO ())+ -- ^ Destroy a resource. Note that exceptions thrown by this will be+ -- silently discarded. If you want reporting, please install an+ -- exception handler yourself.+ -> Int -- ^ number of resources per key to allow in the pool+ -> Int -- ^ number of resources to allow in the pool across all keys+ -> (SomeException -> IO ()) -- ^ what to do if the reaper throws an exception+ -> IO (KeyedPool key resource)+createKeyedPool create destroy maxPerKey maxTotal onReaperException = do+ var <- newTVarIO $ PoolOpen 0 Map.empty++ -- We use a different IORef for the weak ref instead of the var+ -- above since the reaper thread will always be holding onto a+ -- reference.+ alive <- newIORef ()+ void $ mkWeakIORef alive $ destroyKeyedPool' destroy var++ -- Make sure to fork _after_ we've established the mkWeakIORef. If+ -- we did it the other way around, it would be possible for an+ -- async exception to happen before our destroyKeyedPool' handler+ -- was installed, and then reap would have to rely on detecting an+ -- STM deadlock before it could ever exit. This way, the reap+ -- function will only start running when we're guaranteed that+ -- cleanup will be triggered.++ -- Ensure that we have a normal masking state in the new thread.+ _ <- forkIOWithUnmask $ \restore -> keepRunning $ restore $ reap destroy var+ return KeyedPool+ { kpCreate = create+ , kpDestroy = destroy+ , kpMaxPerKey = maxPerKey+ , kpMaxTotal = maxTotal+ , kpVar = var+ , kpAlive = alive+ }+ where+ keepRunning action =+ loop+ where+ loop = action `catch` \e -> onReaperException e >> loop++-- | Make a 'KeyedPool' inactive and destroy all idle resources.+destroyKeyedPool' :: (resource -> IO ())+ -> TVar (PoolMap key resource)+ -> IO ()+destroyKeyedPool' destroy var = do+ m <- atomically $ swapTVar var PoolClosed+ F.mapM_ (ignoreExceptions . destroy) m++-- | Run a reaper thread, which will destroy old resources. It will+-- stop running once our pool switches to PoolClosed, which is handled+-- via the mkWeakIORef in the creation of the pool.+reap :: forall key resource.+ Ord key+ => (resource -> IO ())+ -> TVar (PoolMap key resource)+ -> IO ()+reap destroy var =+ loop+ where+ loop = do+ threadDelay (5 * 1000 * 1000)+ join $ atomically $ do+ m'' <- readTVar var+ case m'' of+ PoolClosed -> return (return ())+ PoolOpen idleCount m+ | Map.null m -> retry+ | otherwise -> do+ (m', toDestroy) <- findStale idleCount m+ writeTVar var m'+ return $ do+ mask_ (mapM_ (ignoreExceptions . destroy) toDestroy)+ loop++ findStale :: Int+ -> Map key (PoolList resource)+ -> STM (PoolMap key resource, [resource])+ findStale idleCount m = do+ -- We want to make sure to get the time _after_ any delays+ -- occur due to the retry call above. Since getCurrentTime has+ -- no side effects outside of the STM block, this is a safe+ -- usage.+ now <- unsafeIOToSTM getCurrentTime+ let isNotStale time = 30 `addUTCTime` time >= now+ let findStale' toKeep toDestroy [] =+ (Map.fromList (toKeep []), toDestroy [])+ findStale' toKeep toDestroy ((key, plist):rest) =+ findStale' toKeep' toDestroy' rest+ where+ -- Note: By definition, the timestamps must be in+ -- descending order, so we don't need to traverse the+ -- whole list.+ (notStale, stale) = span (isNotStale . fst) $ plistToList plist+ toDestroy' = toDestroy . (map snd stale++)+ toKeep' =+ case plistFromList notStale of+ Nothing -> toKeep+ Just x -> toKeep . ((key, x):)+ let (toKeep, toDestroy) = findStale' id id (Map.toList m)+ let idleCount' = idleCount - length toDestroy+ return (PoolOpen idleCount' toKeep, toDestroy)++-- | Check out a value from the 'KeyedPool' with the given key.+--+-- This function will internally call 'mask_' to ensure async safety,+-- and will return a value which uses weak references to ensure that+-- the value is cleaned up. However, if you want to ensure timely+-- resource cleanup, you should bracket this operation together with+-- 'managedRelease'.+takeKeyedPool :: Ord key => KeyedPool key resource -> key -> IO (Managed resource)+takeKeyedPool kp key = mask_ $ join $ atomically $ do+ (m, mresource) <- fmap go $ readTVar (kpVar kp)+ writeTVar (kpVar kp) $! m+ return $ do+ resource <- maybe (kpCreate kp key) return mresource+ alive <- newIORef ()+ isReleasedVar <- newTVarIO False++ let release action = mask_ $ do+ isReleased <- atomically $ swapTVar isReleasedVar True+ unless isReleased $+ case action of+ Reuse -> putResource kp key resource+ DontReuse -> ignoreExceptions $ kpDestroy kp resource++ _ <- mkWeakIORef alive $ release DontReuse+ return Managed+ { _managedResource = resource+ , _managedReused = isJust mresource+ , _managedRelease = release+ , _managedAlive = alive+ }+ where+ go PoolClosed = (PoolClosed, Nothing)+ go pcOrig@(PoolOpen idleCount m) =+ case Map.lookup key m of+ Nothing -> (pcOrig, Nothing)+ Just (One a _) ->+ (PoolOpen (idleCount - 1) (Map.delete key m), Just a)+ Just (Cons a _ _ rest) ->+ (PoolOpen (idleCount - 1) (Map.insert key rest m), Just a)++-- | Try to return a resource to the pool. If too many resources+-- already exist, then just destroy it.+putResource :: Ord key => KeyedPool key resource -> key -> resource -> IO ()+putResource kp key resource = do+ now <- getCurrentTime+ join $ atomically $ do+ (m, action) <- fmap (go now) (readTVar (kpVar kp))+ writeTVar (kpVar kp) $! m+ return action+ where+ go _ PoolClosed = (PoolClosed, kpDestroy kp resource)+ go now pc@(PoolOpen idleCount m)+ | idleCount >= kpMaxTotal kp = (pc, kpDestroy kp resource)+ | otherwise = case Map.lookup key m of+ Nothing ->+ let cnt' = idleCount + 1+ m' = PoolOpen cnt' (Map.insert key (One resource now) m)+ in (m', return ())+ Just l ->+ let (l', mx) = addToList now (kpMaxPerKey kp) resource l+ cnt' = idleCount + maybe 1 (const 0) mx+ m' = PoolOpen cnt' (Map.insert key l' m)+ in (m', maybe (return ()) (kpDestroy kp) mx)++-- | Add a new element to the list, up to the given maximum number. If we're+-- already at the maximum, return the new value as leftover.+addToList :: UTCTime -> Int -> a -> PoolList a -> (PoolList a, Maybe a)+addToList _ i x l | i <= 1 = (l, Just x)+addToList now _ x l@One{} = (Cons x 2 now l, Nothing)+addToList now maxCount x l@(Cons _ currCount _ _)+ | maxCount > currCount = (Cons x (currCount + 1) now l, Nothing)+ | otherwise = (l, Just x)++-- | A managed resource, which can be returned to the 'KeyedPool' when+-- work with it is complete. Using garbage collection, it will default+-- to destroying the resource if the caller does not explicitly use+-- 'managedRelease'.+data Managed resource = Managed+ { _managedResource :: !resource+ , _managedReused :: !Bool+ , _managedRelease :: !(Reuse -> IO ())+ , _managedAlive :: !(IORef ())+ }++-- | Get the raw resource from the 'Managed' value.+managedResource :: Managed resource -> resource+managedResource = _managedResource++-- | Was this value taken from the pool?+managedReused :: Managed resource -> Bool+managedReused = _managedReused++-- | Release the resource, after which it is invalid to use the+-- 'managedResource' value. 'Reuse' returns the resource to the+-- pool; 'DontReuse' destroys it.+managedRelease :: Managed resource -> Reuse -> IO ()+managedRelease = _managedRelease++data Reuse = Reuse | DontReuse++-- | For testing purposes only: create a dummy Managed wrapper+dummyManaged :: resource -> Managed resource+dummyManaged resource = Managed+ { _managedResource = resource+ , _managedReused = False+ , _managedRelease = const (return ())+ , _managedAlive = unsafePerformIO (newIORef ())+ }++ignoreExceptions :: IO () -> IO ()+ignoreExceptions f = f `catch` \(_ :: SomeException) -> return ()++-- | Prevent the managed resource from getting released before you want to use.+keepAlive :: Managed resource -> IO ()+keepAlive = readIORef . _managedAlive
Network/HTTP/Client.hs view
@@ -1,8 +1,7 @@-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE CPP #-} -- | -- -- = Simpler API@@ -12,14 +11,14 @@ -- support for things like JSON request and response bodies. For most users, -- this will be an easier place to start. You can read the tutorial at: ----- https://github.com/commercialhaskell/jump/blob/master/doc/http-client.md+-- https://github.com/snoyberg/http-client/blob/master/TUTORIAL.md -- -- = Lower-level API -- -- This is the main entry point for using http-client. Used by itself, this -- module provides low-level access for streaming request and response bodies, -- and only non-secure HTTP connections. Helper packages such as http-conduit--- provided higher level streaming approaches, while other helper packages like+-- provide higher level streaming approaches, while other helper packages like -- http-client-tls provide secure connections. -- -- There are three core components to be understood here: requests, responses,@@ -41,7 +40,7 @@ -- application which will make a large number of requests to different hosts, -- and will never make more than one connection to a single host, then sharing -- a 'Manager' will result in idle connections being kept open longer than--- necessary. In such a situation, it makes sense to use 'withManager' around+-- necessary. In such a situation, it makes sense to use 'newManager' before -- each new request, to avoid running out of file descriptors. (Note that the -- 'managerIdleConnectionCount' setting mitigates the risk of leaking too many -- file descriptors.)@@ -67,7 +66,7 @@ -- be assumed to throw an 'HttpException' in the event of some problem, and all -- pure functions will be total. For example, 'withResponse', 'httpLbs', and -- 'BodyReader' can all throw exceptions. Functions like 'responseStatus' and--- 'applyBasicAuth' are guaranteed to be total (or there\'s a bug in the+-- 'applyBasicAuth' are guaranteed to be total (or there's a bug in the -- library). -- -- One thing to be cautioned about: the type of 'parseRequest' allows it to work in@@ -75,12 +74,6 @@ -- the case of an invalid URI. In addition, if you leverage the @IsString@ -- instance of the 'Request' value via @OverloadedStrings@, an invalid URI will -- result in a partial value. Caveat emptor!------ Non-2xx responses: the default behavior of all functions in http-client is--- to automatically perform up to 10 redirects (response codes 301, 302, 303,--- and 307), and to throw a 'StatusCodeException' on all responses whose status--- are not in the 2xx range. These behaviors can be overridden by the--- 'redirectCount' and 'checkStatus' settings on a request, respectively. module Network.HTTP.Client ( -- $example1 @@ -111,34 +104,51 @@ , managerTlsConnection , managerResponseTimeout , managerRetryableException- , managerWrapIOException+ , managerWrapException , managerIdleConnectionCount , managerModifyRequest+ , managerModifyResponse -- *** Manager proxy settings , managerSetProxy , managerSetInsecureProxy , managerSetSecureProxy+ , managerSetMaxHeaderLength+ , managerSetMaxNumberHeaders , ProxyOverride , proxyFromRequest , noProxy , useProxy+ , useProxySecureWithoutConnect , proxyEnvironment , proxyEnvironmentNamed , defaultProxy+ -- *** Response timeouts+ , ResponseTimeout+ , responseTimeoutMicro+ , responseTimeoutNone+ , responseTimeoutDefault -- *** Helpers , rawConnectionModifySocket+ , rawConnectionModifySocketSize -- * Request+ -- $parsing-request , parseUrl , parseUrlThrow , parseRequest , parseRequest_+ , requestFromURI+ , requestFromURI_ , defaultRequest- , applyBasicAuth+ , applyBearerAuth , urlEncodedBody , getUri , setRequestIgnoreStatus+ , setRequestCheckStatus , setQueryString+#if MIN_VERSION_http_types(0,12,1)+ , setQueryStringPartialEscape+#endif -- ** Request type and fields , Request , method@@ -153,10 +163,14 @@ , applyBasicProxyAuth , decompress , redirectCount- , checkStatus+ , shouldStripHeaderOnRedirect+ , shouldStripHeaderOnRedirectIfOnDifferentHostOnly+ , checkResponse , responseTimeout , cookieJar , requestVersion+ , redactHeaders+ , earlyHintHeadersReceived -- ** Request body , RequestBody (..) , Popper@@ -172,21 +186,36 @@ , responseHeaders , responseBody , responseCookieJar+ , getOriginalRequest+ , responseEarlyHints+ , throwErrorStatusCodes -- ** Response body , BodyReader , brRead , brReadSome , brConsume+ -- * Advanced connection creation+ , makeConnection+ , socketConnection -- * Misc , HttpException (..)+ , HttpExceptionContent (..) , Cookie (..)+ , equalCookie+ , equivCookie+ , compareCookies , CookieJar+ , equalCookieJar+ , equivCookieJar , Proxy (..)+ , withConnection+ , strippedHostName -- * Cookies , module Network.HTTP.Client.Cookies ) where import Network.HTTP.Client.Body+import Network.HTTP.Client.Connection (makeConnection, socketConnection, strippedHostName) import Network.HTTP.Client.Cookies import Network.HTTP.Client.Core import Network.HTTP.Client.Manager@@ -194,7 +223,6 @@ import Network.HTTP.Client.Response import Network.HTTP.Client.Types -import Data.Text (Text) import Data.IORef (newIORef, writeIORef, readIORef, modifyIORef) import qualified Data.ByteString.Lazy as L import Data.Foldable (Foldable)@@ -202,7 +230,7 @@ import Network.HTTP.Types (statusCode) import GHC.Generics (Generic) import Data.Typeable (Typeable)-import Control.Exception (bracket)+import Control.Exception (bracket, catch, handle, throwIO) -- | A datatype holding information on redirected requests and the final response. --@@ -222,7 +250,7 @@ -- -- Since 0.4.1 }- deriving (Functor, Traversable, Foldable, Show, Typeable, Generic)+ deriving (Functor, Data.Traversable.Traversable, Data.Foldable.Foldable, Show, Typeable, Generic) -- | A variant of @responseOpen@ which keeps a history of all redirects -- performed in the interim, together with the first 1024 bytes of their@@ -230,13 +258,18 @@ -- -- Since 0.4.1 responseOpenHistory :: Request -> Manager -> IO (HistoriedResponse BodyReader)-responseOpenHistory req0 man = do- reqRef <- newIORef req0+responseOpenHistory reqOrig man0 = handle (throwIO . toHttpException reqOrig) $ do+ reqRef <- newIORef reqOrig historyRef <- newIORef id let go req0 = do- (man, req) <- getModifiedRequestManager man req0- (req', res) <- httpRaw' req man+ (man, req) <- getModifiedRequestManager man0 req0+ (req', res') <- httpRaw' req man+ let res = res'+ { responseBody = handle (throwIO . toHttpException req0)+ (responseBody res')+ } case getRedirectedRequest+ req req' (responseHeaders res) (responseCookieJar res)@@ -245,9 +278,10 @@ Just req'' -> do writeIORef reqRef req'' body <- brReadSome (responseBody res) 1024+ `catch` handleClosedRead modifyIORef historyRef (. ((req, res { responseBody = body }):)) return (res, req'', True)- (_, res) <- httpRedirect' (redirectCount req0) go req0+ (_, res) <- httpRedirect' (redirectCount reqOrig) go reqOrig reqFinal <- readIORef reqRef history <- readIORef historyRef return HistoriedResponse@@ -288,7 +322,15 @@ managerSetProxy :: ProxyOverride -> ManagerSettings -> ManagerSettings managerSetProxy po = managerSetInsecureProxy po . managerSetSecureProxy po +-- @since 0.7.17+managerSetMaxHeaderLength :: Int -> ManagerSettings -> ManagerSettings+managerSetMaxHeaderLength l manager = manager+ { managerMaxHeaderLength = Just $ MaxHeaderLength l } +-- @since 0.7.18+managerSetMaxNumberHeaders :: Int -> ManagerSettings -> ManagerSettings+managerSetMaxNumberHeaders n manager = manager+ { managerMaxNumberHeaders = Just $ MaxNumberHeaders n } -- $example1 -- = Example Usage@@ -302,7 +344,7 @@ -- > main = do -- > manager <- newManager defaultManagerSettings -- >--- > request <- parseRequest "http://httpbin.org/post"+-- > request <- parseRequest "http://httpbin.org/get" -- > response <- httpLbs request manager -- > -- > putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response)@@ -315,6 +357,7 @@ -- > import Network.HTTP.Client -- > import Network.HTTP.Types.Status (statusCode) -- > import Data.Aeson (object, (.=), encode)+-- > import Data.Text (Text) -- > -- > main :: IO () -- > main = do@@ -322,6 +365,11 @@ -- > -- > -- Create the request -- > let requestObject = object ["name" .= "Michael", "age" .= 30]+-- > let requestObject = object+-- > [ "name" .= ("Michael" :: Text)+-- > , "age" .= (30 :: Int)+-- > ]+-- > -- > initialRequest <- parseRequest "http://httpbin.org/post" -- > let request = initialRequest { method = "POST", requestBody = RequestBodyLBS $ encode requestObject } -- >@@ -329,3 +377,34 @@ -- > putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response) -- > print $ responseBody response --++-- | Specify maximum time in microseconds the retrieval of response+-- headers is allowed to take+--+-- @since 0.5.0+responseTimeoutMicro :: Int -> ResponseTimeout+responseTimeoutMicro = ResponseTimeoutMicro++-- | Do not have a response timeout+--+-- @since 0.5.0+responseTimeoutNone :: ResponseTimeout+responseTimeoutNone = ResponseTimeoutNone++-- | Use the default response timeout+--+-- When used on a 'Request', means: use the manager's timeout value+--+-- When used on a 'ManagerSettings', means: default to 30 seconds+--+-- @since 0.5.0+responseTimeoutDefault :: ResponseTimeout+responseTimeoutDefault = ResponseTimeoutDefault++-- $parsing-request+--+-- The way you parse string of characters to construct a 'Request' will+-- determine whether exceptions will be thrown on non-2XX response status+-- codes. This is because the behavior is controlled by a setting in+-- 'Request' itself (see 'checkResponse') and different parsing functions+-- set it to different 'IO' actions.
Network/HTTP/Client/Body.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiWayIf #-} module Network.HTTP.Client.Body ( makeChunkedReader , makeLengthReader@@ -7,22 +8,22 @@ , makeUnlimitedReader , brConsume , brEmpty- , brAddCleanup+ , constBodyReader , brReadSome , brRead ) where import Network.HTTP.Client.Connection import Network.HTTP.Client.Types-import Control.Exception (throwIO, assert)-import Data.ByteString (ByteString, empty, uncons)+import Control.Exception (assert)+import Data.ByteString (empty, uncons) import Data.IORef import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Control.Monad (unless, when) import qualified Data.Streaming.Zlib as Z --- ^ Get a single chunk of data from the response body, or an empty+-- | Get a single chunk of data from the response body, or an empty -- bytestring if no more data is available. -- -- Note that in order to consume the entire request body, you will need to@@ -38,41 +39,43 @@ -- -- Since 0.4.20 brReadSome :: BodyReader -> Int -> IO L.ByteString-brReadSome brRead =+brReadSome brRead' = loop id where- loop front rem- | rem <= 0 = return $ L.fromChunks $ front []+ loop front rem'+ | rem' <= 0 = return $ L.fromChunks $ front [] | otherwise = do- bs <- brRead+ bs <- brRead' if S.null bs then return $ L.fromChunks $ front []- else loop (front . (bs:)) (rem - S.length bs)+ else loop (front . (bs:)) (rem' - S.length bs) brEmpty :: BodyReader brEmpty = return S.empty -brAddCleanup :: IO () -> BodyReader -> BodyReader-brAddCleanup cleanup brRead = do- bs <- brRead- when (S.null bs) cleanup- return bs+constBodyReader :: [S.ByteString] -> IO BodyReader+constBodyReader input = do+ iinput <- newIORef input+ return $ atomicModifyIORef iinput $ \input' ->+ case input' of+ [] -> ([], S.empty)+ x:xs -> (xs, x) -- | Strictly consume all remaining chunks of data from the stream. -- -- Since 0.1.0 brConsume :: BodyReader -> IO [S.ByteString]-brConsume brRead =+brConsume brRead' = go id where go front = do- x <- brRead+ x <- brRead' if S.null x then return $ front [] else go (front . (x:)) makeGzipReader :: BodyReader -> IO BodyReader-makeGzipReader brRead = do+makeGzipReader brRead' = do inf <- Z.initInflate $ Z.WindowBits 31 istate <- newIORef Nothing let goPopper popper = do@@ -88,9 +91,9 @@ else do writeIORef istate Nothing return bs- Z.PRError e -> throwIO $ HttpZlibException e+ Z.PRError e -> throwHttp $ HttpZlibException e start = do- bs <- brRead+ bs <- brRead' if S.null bs then return S.empty else do@@ -102,16 +105,25 @@ Nothing -> start Just popper -> goPopper popper -makeUnlimitedReader :: Connection -> IO BodyReader-makeUnlimitedReader Connection {..} = do+makeUnlimitedReader+ :: IO () -- ^ cleanup+ -> Connection+ -> IO BodyReader+makeUnlimitedReader cleanup Connection {..} = do icomplete <- newIORef False return $ do bs <- connectionRead- when (S.null bs) $ writeIORef icomplete True+ when (S.null bs) $ do+ writeIORef icomplete True+ cleanup return bs -makeLengthReader :: Int -> Connection -> IO BodyReader-makeLengthReader count0 Connection {..} = do+makeLengthReader+ :: IO () -- ^ cleanup+ -> Int+ -> Connection+ -> IO BodyReader+makeLengthReader cleanup count0 Connection {..} = do icount <- newIORef count0 return $ do count <- readIORef icount@@ -119,26 +131,34 @@ then return empty else do bs <- connectionRead- when (S.null bs) $ throwIO $ ResponseBodyTooShort (fromIntegral count0) (fromIntegral $ count0 - count)+ when (S.null bs) $ throwHttp $ ResponseBodyTooShort (fromIntegral count0) (fromIntegral $ count0 - count) case compare count $ S.length bs of LT -> do let (x, y) = S.splitAt count bs connectionUnread y writeIORef icount (-1)+ cleanup return x EQ -> do writeIORef icount (-1)+ cleanup return bs GT -> do writeIORef icount (count - S.length bs) return bs -makeChunkedReader :: Bool -- ^ raw- -> Connection- -> IO BodyReader-makeChunkedReader raw conn@Connection {..} = do+makeChunkedReader+ :: Maybe MaxHeaderLength+ -> IO () -- ^ cleanup+ -> Bool -- ^ raw+ -> Connection+ -> IO BodyReader+makeChunkedReader mhl cleanup raw conn@Connection {..} = do icount <- newIORef 0- return $ go icount+ return $ do+ bs <- go icount+ when (S.null bs) cleanup+ pure bs where go icount = do count0 <- readIORef icount@@ -148,8 +168,11 @@ else return (empty, count0) if count <= 0 then do+ -- count == -1 indicates that all chunks have been consumed writeIORef icount (-1)- return $ if count /= (-1) && raw then rawCount else empty+ if | count /= -1 && raw -> S.append rawCount <$> readTrailersRaw+ | count /= -1 -> consumeTrailers *> pure empty+ | otherwise -> pure empty else do (bs, count') <- readChunk count writeIORef icount count'@@ -162,7 +185,7 @@ readChunk 0 = return (empty, 0) readChunk remainder = do bs <- connectionRead- when (S.null bs) $ throwIO InvalidChunkHeaders+ when (S.null bs) $ throwHttp InvalidChunkHeaders case compare remainder $ S.length bs of LT -> do let (x, y) = S.splitAt remainder bs@@ -179,13 +202,13 @@ | otherwise = return (x, 0) requireNewline = do- bs <- connectionReadLine conn- unless (S.null bs) $ throwIO InvalidChunkHeaders+ bs <- connectionReadLine mhl conn+ unless (S.null bs) $ throwHttp InvalidChunkHeaders readHeader = do- bs <- connectionReadLine conn+ bs <- connectionReadLine mhl conn case parseHex bs of- Nothing -> throwIO InvalidChunkHeaders+ Nothing -> throwHttp InvalidChunkHeaders Just hex -> return (bs `S.append` "\r\n", hex) parseHex bs0 =@@ -195,8 +218,8 @@ _ -> Nothing parseHex' i bs = case uncons bs of- Just (w, bs)- | Just i' <- toI w -> parseHex' (i * 16 + i') bs+ Just (w, bs')+ | Just i' <- toI w -> parseHex' (i * 16 + i') bs' _ -> i toI w@@ -204,3 +227,11 @@ | 65 <= w && w <= 70 = Just $ fromIntegral w - 55 | 97 <= w && w <= 102 = Just $ fromIntegral w - 87 | otherwise = Nothing++ readTrailersRaw = do+ bs <- connectionReadLine mhl conn+ if S.null bs+ then pure "\r\n"+ else (bs `S.append` "\r\n" `S.append`) <$> readTrailersRaw++ consumeTrailers = connectionDropTillBlankLine mhl conn
Network/HTTP/Client/Connection.hs view
@@ -5,53 +5,67 @@ ( connectionReadLine , connectionReadLineWith , connectionDropTillBlankLine+ , connectionUnreadLine , dummyConnection , openSocketConnection , openSocketConnectionSize , makeConnection+ , socketConnection+ , withSocket+ , strippedHostName ) where import Data.ByteString (ByteString, empty) import Data.IORef import Control.Monad-import Control.Exception (throwIO)+import Control.Concurrent+import Control.Concurrent.Async import Network.HTTP.Client.Types-import Network.Socket (Socket, sClose, HostAddress)+import Network.Socket (Socket, HostAddress) import qualified Network.Socket as NS import Network.Socket.ByteString (sendAll, recv) import qualified Control.Exception as E import qualified Data.ByteString as S-import Data.Word (Word8)+import Data.Foldable (for_) import Data.Function (fix)+import Data.Maybe (listToMaybe)+import Data.Word (Word8) -connectionReadLine :: Connection -> IO ByteString-connectionReadLine conn = do+connectionReadLine :: Maybe MaxHeaderLength -> Connection -> IO ByteString+connectionReadLine mhl conn = do bs <- connectionRead conn- when (S.null bs) $ throwIO IncompleteHeaders- connectionReadLineWith conn bs+ when (S.null bs) $ throwHttp IncompleteHeaders+ connectionReadLineWith mhl conn bs -- | Keep dropping input until a blank line is found.-connectionDropTillBlankLine :: Connection -> IO ()-connectionDropTillBlankLine conn = fix $ \loop -> do- bs <- connectionReadLine conn+connectionDropTillBlankLine :: Maybe MaxHeaderLength -> Connection -> IO ()+connectionDropTillBlankLine mhl conn = fix $ \loop -> do+ bs <- connectionReadLine mhl conn unless (S.null bs) loop -connectionReadLineWith :: Connection -> ByteString -> IO ByteString-connectionReadLineWith conn bs0 =+connectionReadLineWith :: Maybe MaxHeaderLength -> Connection -> ByteString -> IO ByteString+connectionReadLineWith mhl conn bs0 = go bs0 id 0 where go bs front total = case S.break (== charLF) bs of (_, "") -> do let total' = total + S.length bs- when (total' > 4096) $ throwIO OverlongHeaders+ case fmap unMaxHeaderLength mhl of+ Nothing -> pure ()+ Just n -> when (total' > n) $ throwHttp OverlongHeaders bs' <- connectionRead conn- when (S.null bs') $ throwIO IncompleteHeaders+ when (S.null bs') $ throwHttp IncompleteHeaders go bs' (front . (bs:)) total' (x, S.drop 1 -> y) -> do unless (S.null y) $! connectionUnread conn y return $! killCR $! S.concat $! front [x] +connectionUnreadLine :: Connection -> ByteString -> IO ()+connectionUnreadLine conn line = do+ connectionUnread conn (S.pack [charCR, charLF])+ connectionUnread conn line+ charLF, charCR :: Word8 charLF = 10 charCR = 13@@ -62,7 +76,6 @@ | S.last bs == charCR = S.init bs | otherwise = bs - -- | For testing dummyConnection :: [ByteString] -- ^ input -> IO (Connection, IO [ByteString], IO [ByteString]) -- ^ conn, output, input@@ -79,6 +92,9 @@ , connectionClose = return () }, atomicModifyIORef ioutput $ \output -> ([], output), readIORef iinput) +-- | Create a new 'Connection' from a read, write, and close function.+--+-- @since 0.5.3 makeConnection :: IO ByteString -- ^ read -> (ByteString -> IO ()) -- ^ write -> IO () -- ^ close@@ -99,8 +115,7 @@ return $! Connection { connectionRead = do closed <- readIORef closedVar- when closed $- throwIO ConnectionClosed+ when closed $ throwHttp ConnectionClosed join $ atomicModifyIORef istack $ \stack -> case stack of x:xs -> (xs, return x)@@ -108,24 +123,27 @@ , connectionUnread = \x -> do closed <- readIORef closedVar- when closed $- throwIO ConnectionClosed+ when closed $ throwHttp ConnectionClosed atomicModifyIORef istack $ \stack -> (x:stack, ()) , connectionWrite = \x -> do closed <- readIORef closedVar- when closed $- throwIO ConnectionClosed+ when closed $ throwHttp ConnectionClosed w x , connectionClose = close } -socketConnection :: Socket -> Int -> IO Connection+-- | Create a new 'Connection' from a 'Socket'.+--+-- @since 0.5.3+socketConnection :: Socket+ -> Int -- ^ chunk size+ -> IO Connection socketConnection socket chunksize = makeConnection (recv socket chunksize) (sendAll socket)- (sClose socket)+ (NS.close socket) openSocketConnection :: (Socket -> IO ()) -> Maybe HostAddress@@ -140,14 +158,39 @@ -> String -- ^ host -> Int -- ^ port -> IO Connection-openSocketConnectionSize tweakSocket chunksize hostAddress host port = do- let hints = NS.defaultHints {- NS.addrFlags = [NS.AI_ADDRCONFIG]- , NS.addrSocketType = NS.Stream- }- addrs <- case hostAddress of+openSocketConnectionSize tweakSocket chunksize hostAddress' host' port' =+ withSocket tweakSocket hostAddress' host' port' $ \ sock ->+ socketConnection sock chunksize++-- | strippedHostName takes a URI host name, as extracted+-- by 'Network.URI.regName', and strips square brackets+-- around IPv6 addresses.+--+-- The result is suitable for passing to services such as+-- name resolution ('Network.Socket.getAddr').+--+-- @since+strippedHostName :: String -> String+strippedHostName hostName =+ case hostName of+ '[':'v':_ -> hostName -- IPvFuture, no obvious way to deal with this+ '[':rest ->+ case break (== ']') rest of+ (ipv6, "]") -> ipv6+ _ -> hostName -- invalid host name+ _ -> hostName++withSocket :: (Socket -> IO ())+ -> Maybe HostAddress+ -> String -- ^ host+ -> Int -- ^ port+ -> (Socket -> IO a)+ -> IO a+withSocket tweakSocket hostAddress' host' port' f = do+ let hints = NS.defaultHints { NS.addrSocketType = NS.Stream }+ addrs <- case hostAddress' of Nothing ->- NS.getAddrInfo (Just hints) (Just host) (Just $ show port)+ NS.getAddrInfo (Just hints) (Just $ strippedHostName host') (Just $ show port') Just ha -> return [NS.AddrInfo@@ -155,25 +198,47 @@ , NS.addrFamily = NS.AF_INET , NS.addrSocketType = NS.Stream , NS.addrProtocol = 6 -- tcp- , NS.addrAddress = NS.SockAddrInet (toEnum port) ha+ , NS.addrAddress = NS.SockAddrInet (toEnum port') ha , NS.addrCanonName = Nothing }] - firstSuccessful addrs $ \addr ->- E.bracketOnError- (NS.socket (NS.addrFamily addr) (NS.addrSocketType addr)- (NS.addrProtocol addr))- (NS.sClose)- (\sock -> do- NS.setSocketOption sock NS.NoDelay 1- tweakSocket sock- NS.connect sock (NS.addrAddress addr)- socketConnection sock chunksize)+ E.bracketOnError (firstSuccessful addrs $ openSocket tweakSocket) NS.close f +openSocket tweakSocket addr =+ E.bracketOnError+ (NS.socket (NS.addrFamily addr) (NS.addrSocketType addr)+ (NS.addrProtocol addr))+ NS.close+ (\sock -> do+ NS.setSocketOption sock NS.NoDelay 1+ tweakSocket sock+ NS.connect sock (NS.addrAddress addr)+ return sock)++-- Pick up an IP using an approximation of the happy-eyeballs algorithm:+-- https://datatracker.ietf.org/doc/html/rfc8305+-- firstSuccessful :: [NS.AddrInfo] -> (NS.AddrInfo -> IO a) -> IO a-firstSuccessful [] _ = error "getAddrInfo returned empty list"-firstSuccessful (a:as) cb =- cb a `E.catch` \(e :: E.IOException) ->- case as of- [] -> E.throwIO e- _ -> firstSuccessful as cb+firstSuccessful [] _ = error "getAddrInfo returned empty list"+firstSuccessful addresses cb = do+ result <- newEmptyMVar+ either E.throwIO pure =<<+ withAsync (tryAddresses result)+ (\_ -> takeMVar result)+ where+ -- https://datatracker.ietf.org/doc/html/rfc8305#section-5+ connectionAttemptDelay = 250 * 1000++ tryAddresses result = do+ z <- forConcurrently (zip addresses [0..]) $ \(addr, n) -> do+ when (n > 0) $ threadDelay $ n * connectionAttemptDelay+ tryAddress addr++ case listToMaybe (reverse z) of+ Just e@(Left _) -> tryPutMVar result e+ _ -> error $ "tryAddresses invariant violated: " ++ show addresses+ where+ tryAddress addr = do+ r :: Either E.IOException a <- E.try $! cb addr+ for_ r $ \_ -> tryPutMVar result r+ pure r
Network/HTTP/Client/Cookies.hs view
@@ -14,6 +14,7 @@ , removeExistingCookieFromCookieJar , domainMatches , isIpAddress+ , isPotentiallyTrustworthyOrigin , defaultPath ) where @@ -29,9 +30,9 @@ import qualified Network.PublicSuffixList.Lookup as PSL import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode)+import qualified Data.IP as IP+import Text.Read (readMaybe) -import qualified Network.HTTP.Client.Request as Req-import qualified Network.HTTP.Client.Response as Res import Network.HTTP.Client.Types as Req slash :: Integral a => a@@ -39,7 +40,7 @@ isIpAddress :: BS.ByteString -> Bool isIpAddress =- go 4+ go (4 :: Int) where go 0 bs = BS.null bs go rest bs =@@ -102,7 +103,7 @@ where (mc, lc) = removeExistingCookieFromCookieJarHelper cookie (expose cookie_jar') removeExistingCookieFromCookieJarHelper _ [] = (Nothing, []) removeExistingCookieFromCookieJarHelper c (c' : cs)- | c == c' = (Just c', cs)+ | c `equivCookie` c' = (Just c', cs) | otherwise = (cookie', c' : cookie_jar'') where (cookie', cookie_jar'') = removeExistingCookieFromCookieJarHelper c cs @@ -113,6 +114,37 @@ isPublicSuffix :: BS.ByteString -> Bool isPublicSuffix = PSL.isSuffix . decodeUtf8With lenientDecode +-- | Algorithm described in \"Secure Contexts\", Section 3.1, \"Is origin potentially trustworthy?\"+--+-- Note per RFC6265 section 5.4 user agent is free to define the meaning of "secure" protocol.+--+-- See:+-- https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy+isPotentiallyTrustworthyOrigin :: Bool -- ^ True if HTTPS+ -> BS.ByteString -- ^ Host+ -> Bool -- ^ Whether or not the origin is potentially trustworthy+isPotentiallyTrustworthyOrigin secure host+ | secure = True -- step 3+ | isLoopbackAddr4 = True -- step 4, part 1+ | isLoopbackAddr6 = True -- step 4, part 2+ | isLoopbackHostname = True -- step 5+ | otherwise = False+ where isLoopbackHostname =+ host == "localhost"+ || host == "localhost."+ || BS.isSuffixOf ".localhost" host+ || BS.isSuffixOf ".localhost." host+ isLoopbackAddr4 =+ fmap (take 1 . IP.fromIPv4) (readMaybe (S8.unpack host)) == Just [127]+ isLoopbackAddr6 =+ fmap IP.toHostAddress6 maddr6 == Just (0, 0, 0, 1)+ maddr6 = do+ (c1, rest1) <- S8.uncons host+ (rest2, c2) <- S8.unsnoc rest1+ case [c1, c2] of+ "[]" -> readMaybe (S8.unpack rest2)+ _ -> Nothing+ -- | This corresponds to the eviction algorithm described in Section 5.3 \"Storage Model\" evictExpiredCookies :: CookieJar -- ^ Input cookie jar -> UTCTime -- ^ Value that should be used as \"now\"@@ -123,7 +155,7 @@ insertCookiesIntoRequest :: Req.Request -- ^ The request to insert into -> CookieJar -- ^ Current cookie jar -> UTCTime -- ^ Value that should be used as \"now\"- -> (Req.Request, CookieJar) -- ^ (Ouptut request, Updated cookie jar (last-access-time is updated))+ -> (Req.Request, CookieJar) -- ^ (Output request, Updated cookie jar (last-access-time is updated)) insertCookiesIntoRequest request cookie_jar now | BS.null cookie_string = (request, cookie_jar') | otherwise = (request {Req.requestHeaders = cookie_header : purgedHeaders}, cookie_jar')@@ -145,12 +177,12 @@ condition2 = pathMatches (Req.path request) (cookie_path cookie) condition3 | not (cookie_secure_only cookie) = True- | otherwise = Req.secure request+ | otherwise = isPotentiallyTrustworthyOrigin (Req.secure request) (Req.host request) condition4 | not (cookie_http_only cookie) = True | otherwise = is_http_api matching_cookies = filter matching_cookie $ expose cookie_jar- output_cookies = map (\ c -> (cookie_name c, cookie_value c)) $ L.sort matching_cookies+ output_cookies = map (\ c -> (cookie_name c, cookie_value c)) $ L.sortBy compareCookies matching_cookies output_line = toByteString $ renderCookies $ output_cookies folding_function cookie_jar'' cookie = case removeExistingCookieFromCookieJar cookie cookie_jar'' of (Just c, cookie_jar''') -> insertIntoCookieJar (c {cookie_last_access_time = now}) cookie_jar'''
Network/HTTP/Client/Core.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Network.HTTP.Client.Core ( withResponse@@ -7,32 +6,35 @@ , httpNoBody , httpRaw , httpRaw'+ , requestAction , getModifiedRequestManager , responseOpen , responseClose- , applyCheckStatus , httpRedirect , httpRedirect'+ , withConnection+ , handleClosedRead ) where -#if !MIN_VERSION_base(4,6,0)-import Prelude hiding (catch)-#endif-import Network.HTTP.Client.Body-import Network.HTTP.Client.Cookies+import Network.HTTP.Types import Network.HTTP.Client.Manager+import Network.HTTP.Client.Types+import Network.HTTP.Client.Headers+import Network.HTTP.Client.Body import Network.HTTP.Client.Request import Network.HTTP.Client.Response-import Network.HTTP.Client.Types-import Network.HTTP.Types+import Network.HTTP.Client.Cookies import Data.Maybe (fromMaybe, isJust) import Data.Time+import Data.IORef import Control.Exception-import qualified Data.ByteString as S-import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L import Data.Monoid import Control.Monad (void)+import System.Timeout (timeout)+import System.IO.Unsafe (unsafePerformIO)+import Data.KeyedPool+import GHC.IO.Exception (IOException(..), IOErrorType(..)) -- | Perform a @Request@ using a connection acquired from the given @Manager@, -- and then provide the @Response@ to the given function. This function is@@ -94,45 +96,88 @@ Just cj -> do now <- getCurrentTime return $ insertCookiesIntoRequest req' (evictExpiredCookies cj now) now- Nothing -> return (req', mempty)- (timeout', (connRelease, ci, isManaged)) <- getConnectionWrapper- req- (responseTimeout' req)- (failedConnectionException req)- (getConn req m)-- -- Originally, we would only test for exceptions when sending the request,- -- not on calling @getResponse@. However, some servers seem to close- -- connections after accepting the request headers, so we need to check for- -- exceptions in both.- ex <- try $ do- cont <- requestBuilder (dropProxyAuthSecure req) ci+ Nothing -> return (req', Data.Monoid.mempty)+ res <- makeRequest req m+ case cookieJar req' of+ Just _ -> do+ now' <- getCurrentTime+ let (cookie_jar, _) = updateCookieJar res req now' cookie_jar'+ return (req, res {responseCookieJar = cookie_jar})+ Nothing -> return (req, res) - getResponse connRelease timeout' req ci cont+makeRequest+ :: Request+ -> Manager+ -> IO (Response BodyReader)+makeRequest req m = do+ action <- readIORef requestAction+ action req m - case (ex, isManaged) of- -- Connection was reused, and might have been closed. Try again- (Left e, Reused) | mRetryableException m e -> do- connRelease DontReuse- httpRaw' req m- -- Not reused, or a non-retry, so this is a real exception- (Left e, _) -> throwIO e- -- Everything went ok, so the connection is good. If any exceptions get- -- thrown in the response body, just throw them as normal.- (Right res, _) -> case cookieJar req' of- Just _ -> do- now' <- getCurrentTime- let (cookie_jar, _) = updateCookieJar res req now' cookie_jar'- return (req, res {responseCookieJar = cookie_jar})- Nothing -> return (req, res)+requestAction :: IORef (Request -> Manager -> IO (Response BodyReader))+{-# NOINLINE requestAction #-}+requestAction = unsafePerformIO (newIORef action) where+ action+ :: Request+ -> Manager+ -> IO (Response BodyReader)+ action req m = do+ (timeout', mconn) <- getConnectionWrapper+ (responseTimeout' req)+ (getConn req m) - responseTimeout' req- | rt == useDefaultTimeout = mResponseTimeout m- | otherwise = rt+ -- Originally, we would only test for exceptions when sending the request,+ -- not on calling @getResponse@. However, some servers seem to close+ -- connections after accepting the request headers, so we need to check for+ -- exceptions in both.+ ex <- try $ do+ cont <- requestBuilder (dropProxyAuthSecure req) (managedResource mconn)++ getResponse (mMaxHeaderLength m) (mMaxNumberHeaders m) timeout' req mconn cont++ case ex of+ -- Connection was reused, and might have been closed. Try again+ Left e | managedReused mconn && mRetryableException m e -> do+ managedRelease mconn DontReuse+ action req m+ -- Not reused, or a non-retry, so this is a real exception+ Left e -> do+ -- Explicitly release connection for all real exceptions:+ -- https://github.com/snoyberg/http-client/pull/454+ managedRelease mconn DontReuse+ throwIO e+ -- Everything went ok, so the connection is good. If any exceptions get+ -- thrown in the response body, just throw them as normal.+ Right res -> return res where- rt = responseTimeout req+ getConnectionWrapper mtimeout f =+ case mtimeout of+ Nothing -> fmap ((,) Nothing) f+ Just timeout' -> do+ before <- getCurrentTime+ mres <- timeout timeout' f+ case mres of+ Nothing -> throwHttp ConnectionTimeout+ Just mConn -> do+ now <- getCurrentTime+ let timeSpentMicro = diffUTCTime now before * 1000000+ remainingTime = round $ fromIntegral timeout' - timeSpentMicro+ if remainingTime <= 0+ then do+ managedRelease mConn DontReuse+ throwHttp ConnectionTimeout+ else return (Just remainingTime, mConn) + responseTimeout' req =+ case responseTimeout req of+ ResponseTimeoutDefault ->+ case mResponseTimeout m of+ ResponseTimeoutDefault -> Just 30000000+ ResponseTimeoutNone -> Nothing+ ResponseTimeoutMicro u -> Just u+ ResponseTimeoutNone -> Nothing+ ResponseTimeoutMicro u -> Just u+ -- | The used Manager can be overridden (by requestManagerOverride) and the used -- Request can be modified (through managerModifyRequest). This function allows -- to retrieve the possibly overridden Manager and the possibly modified@@ -176,64 +221,32 @@ -- -- Since 0.1.0 responseOpen :: Request -> Manager -> IO (Response BodyReader)-responseOpen req0 manager' = handle addTlsHostPort $ mWrapIOException manager $ do- (req, res) <-- if redirectCount req0 == 0- then httpRaw' req0 manager- else go (redirectCount req0) req0- maybe (return res) throwIO =<< applyCheckStatus req (checkStatus req) res+responseOpen inputReq manager' = do+ case validateHeaders (requestHeaders inputReq) of+ GoodHeaders -> return ()+ BadHeaders reason -> throwHttp $ InvalidRequestHeader reason+ (manager, req0) <- getModifiedRequestManager manager' inputReq+ wrapExc req0 $ mWrapException manager req0 $ do+ (req, res) <- go manager (redirectCount req0) req0+ checkResponse req req res+ mModifyResponse manager res+ { responseBody = wrapExc req0 (responseBody res)+ } where- manager = fromMaybe manager' (requestManagerOverride req0)-- addTlsHostPort (TlsException e) = throwIO $ TlsExceptionHostPort e (host req0) (port req0)- addTlsHostPort e = throwIO e+ wrapExc :: Request -> IO a -> IO a+ wrapExc req0 = handle $ throwIO . toHttpException req0 - go count req' = httpRedirect'+ go manager0 count req' = httpRedirect' count (\req -> do- (manager, modReq) <- getModifiedRequestManager manager req+ (manager, modReq) <- getModifiedRequestManager manager0 req (req'', res) <- httpRaw' modReq manager let mreq = if redirectCount modReq == 0 then Nothing- else getRedirectedRequest req'' (responseHeaders res) (responseCookieJar res) (statusCode (responseStatus res))+ else getRedirectedRequest req' req'' (responseHeaders res) (responseCookieJar res) (statusCode (responseStatus res)) return (res, fromMaybe req'' mreq, isJust mreq)) req' --- | Apply 'Request'\'s 'checkStatus' and return resulting exception if any.-applyCheckStatus- :: Request- -> (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException)- -> Response BodyReader- -> IO (Maybe SomeException)-applyCheckStatus req checkStatus' res =- case checkStatus' (responseStatus res) (responseHeaders res) (responseCookieJar res) of- Nothing -> return Nothing- Just exc -> do- exc' <-- case fromException exc of- Just (StatusCodeException s hdrs cookie_jar) -> do- lbs <- brReadSome (responseBody res) 1024- return $ toException $ StatusCodeException s (hdrs ++- [ ("X-Response-Body-Start", toStrict' lbs)- , ("X-Request-URL", S.concat- [ method req- , " "- , S8.pack $ show $ getUri req- ])- ]) cookie_jar- _ -> return exc- responseClose res- return (Just exc')- where-#ifndef MIN_VERSION_bytestring-#define MIN_VERSION_bytestring(x,y,z) 1-#endif-#if MIN_VERSION_bytestring(0,10,0)- toStrict' = L.toStrict-#else- toStrict' = S.concat . L.toChunks-#endif- -- | Redirect loop. httpRedirect :: Int -- ^ 'redirectCount'@@ -247,6 +260,17 @@ (res, mbReq) <- http0 req' return (res, fromMaybe req0 mbReq, isJust mbReq) +handleClosedRead :: SomeException -> IO L.ByteString+handleClosedRead se+ | Just ConnectionClosed <- fmap unHttpExceptionContentWrapper (fromException se)+ = return L.empty+ | Just (HttpExceptionRequest _ ConnectionClosed) <- fromException se+ = return L.empty+ | Just (IOError _ ResourceVanished _ _ _ _) <- fromException se+ = return L.empty+ | otherwise+ = throwIO se+ -- | Redirect loop. -- -- This extended version of 'httpRaw' also returns the Request potentially modified by @managerModifyRequest@.@@ -257,7 +281,7 @@ -> IO (Request, Response BodyReader) httpRedirect' count0 http' req0 = go count0 req0 [] where- go count _ ress | count < 0 = throwIO $ TooManyRedirects ress+ go count _ ress | count < 0 = throwHttp $ TooManyRedirects ress go count req' ress = do (res, req, isRedirect) <- http' req' if isRedirect then do@@ -270,7 +294,7 @@ -- The connection may already be closed, e.g. -- when using withResponseHistory. See -- https://github.com/snoyberg/http-client/issues/169- `catch` \(_ :: ConnectionClosed) -> return L.empty+ `Control.Exception.catch` handleClosedRead responseClose res -- And now perform the actual redirect@@ -285,3 +309,15 @@ -- Since 0.1.0 responseClose :: Response a -> IO () responseClose = runResponseClose . responseClose'++-- | Perform an action using a @Connection@ acquired from the given @Manager@.+--+-- You should use this only when you have to read and write interactively+-- through the connection (e.g. connection by the WebSocket protocol).+--+-- @since 0.5.13+withConnection :: Request -> Manager -> (Connection -> IO a) -> IO a+withConnection origReq man action = do+ mHttpConn <- getConn (mSetProxy man origReq) man+ action (managedResource mHttpConn) <* keepAlive mHttpConn+ `finally` managedRelease mHttpConn DontReuse
Network/HTTP/Client/Headers.hs view
@@ -1,38 +1,41 @@+{-# LANGUAGE DisambiguateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE ViewPatterns #-} module Network.HTTP.Client.Headers ( parseStatusHeaders+ , validateHeaders+ , HeadersValidationResult (..) ) where -import Control.Applicative ((<$>), (<*>))-import Control.Exception (throwIO)+import Control.Applicative as A ((<$>), (<*>)) import Control.Monad import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8 import qualified Data.CaseInsensitive as CI+import Data.Maybe (mapMaybe)+import Data.Monoid+import Data.Word (Word8) import Network.HTTP.Client.Connection import Network.HTTP.Client.Types-import Network.HTTP.Client.Util (timeout) import Network.HTTP.Types-import Data.Word (Word8)+import System.Timeout (timeout) -charLF, charCR, charSpace, charColon, charPeriod :: Word8-charLF = 10-charCR = 13+charSpace, charColon, charPeriod :: Word8 charSpace = 32 charColon = 58 charPeriod = 46 -parseStatusHeaders :: Connection -> Maybe Int -> Maybe (IO ()) -> IO StatusHeaders-parseStatusHeaders conn timeout' cont+parseStatusHeaders :: Maybe MaxHeaderLength -> Maybe MaxNumberHeaders -> Connection -> Maybe Int -> ([Header] -> IO ()) -> Maybe (IO ()) -> IO StatusHeaders+parseStatusHeaders mhl mnh conn timeout' onEarlyHintHeaders cont | Just k <- cont = getStatusExpectContinue k | otherwise = getStatus where withTimeout = case timeout' of Nothing -> id- Just t -> timeout t >=> maybe (throwIO ResponseTimeout) return+ Just t -> timeout t >=> maybe (throwHttp ResponseTimeout) return getStatus = withTimeout next where@@ -44,29 +47,36 @@ Just s -> return s Nothing -> sendBody >> getStatus + nextStatusHeaders :: IO (Maybe StatusHeaders) nextStatusHeaders = do- (s, v) <- nextStatusLine- if statusCode s == 100- then connectionDropTillBlankLine conn >> return Nothing- else Just . StatusHeaders s v <$> parseHeaders 0 id+ (s, v) <- nextStatusLine mhl+ if | statusCode s == 100 -> connectionDropTillBlankLine mhl conn >> return Nothing+ | statusCode s == 103 -> do+ earlyHeaders <- parseEarlyHintHeadersUntilFailure 0 id+ onEarlyHintHeaders earlyHeaders+ nextStatusHeaders >>= \case+ Nothing -> return Nothing+ Just (StatusHeaders s' v' earlyHeaders' reqHeaders) ->+ return $ Just $ StatusHeaders s' v' (earlyHeaders <> earlyHeaders') reqHeaders+ | otherwise -> (Just <$>) $ StatusHeaders s v mempty A.<$> parseHeaders 0 id - nextStatusLine :: IO (Status, HttpVersion)- nextStatusLine = do+ nextStatusLine :: Maybe MaxHeaderLength -> IO (Status, HttpVersion)+ nextStatusLine mhl = do -- Ensure that there is some data coming in. If not, we want to signal -- this as a connection problem and not a protocol problem. bs <- connectionRead conn- when (S.null bs) $ throwIO NoResponseDataReceived- connectionReadLineWith conn bs >>= parseStatus 3+ when (S.null bs) $ throwHttp NoResponseDataReceived+ connectionReadLineWith mhl conn bs >>= parseStatus mhl 3 - parseStatus :: Int -> S.ByteString -> IO (Status, HttpVersion)- parseStatus i bs | S.null bs && i > 0 = connectionReadLine conn >>= parseStatus (i - 1)- parseStatus _ bs = do+ parseStatus :: Maybe MaxHeaderLength -> Int -> S.ByteString -> IO (Status, HttpVersion)+ parseStatus mhl i bs | S.null bs && i > 0 = connectionReadLine mhl conn >>= parseStatus mhl (i - 1)+ parseStatus _ _ bs = do let (ver, bs2) = S.break (== charSpace) bs (code, bs3) = S.break (== charSpace) $ S.dropWhile (== charSpace) bs2 msg = S.dropWhile (== charSpace) bs3- case (,) <$> parseVersion ver <*> readInt code of+ case (,) <$> parseVersion ver A.<*> readInt code of Just (ver', code') -> return (Status code' msg, ver')- Nothing -> throwIO $ InvalidStatusLine bs+ Nothing -> throwHttp $ InvalidStatusLine bs stripPrefixBS x y | x `S.isPrefixOf` y = Just $ S.drop (S.length x) y@@ -81,19 +91,59 @@ Just (i, "") -> Just i _ -> Nothing - parseHeaders 100 _ = throwIO OverlongHeaders+ guardMaxNumberHeaders :: Int -> IO ()+ guardMaxNumberHeaders count = case fmap unMaxNumberHeaders mnh of+ Nothing -> pure ()+ Just n -> when (count >= n) $ throwHttp TooManyHeaderFields++ parseHeaders :: Int -> ([Header] -> [Header]) -> IO [Header] parseHeaders count front = do- line <- connectionReadLine conn+ guardMaxNumberHeaders count+ line <- connectionReadLine mhl conn if S.null line then return $ front []- else do- header <- parseHeader line- parseHeaders (count + 1) $ front . (header:)+ else+ parseHeader line >>= \case+ Just header ->+ parseHeaders (count + 1) $ front . (header:)+ Nothing ->+ -- Unparseable header line; rather than throwing+ -- an exception, ignore it for robustness.+ parseHeaders count front - parseHeader :: S.ByteString -> IO Header+ parseEarlyHintHeadersUntilFailure :: Int -> ([Header] -> [Header]) -> IO [Header]+ parseEarlyHintHeadersUntilFailure count front = do+ guardMaxNumberHeaders count+ line <- connectionReadLine mhl conn+ if S.null line+ then return $ front []+ else+ parseHeader line >>= \case+ Just header ->+ parseEarlyHintHeadersUntilFailure (count + 1) $ front . (header:)+ Nothing -> do+ connectionUnreadLine conn line+ return $ front []++ parseHeader :: S.ByteString -> IO (Maybe Header) parseHeader bs = do let (key, bs2) = S.break (== charColon) bs- when (S.null bs2) $ throwIO $ InvalidHeader bs- return (CI.mk $! strip key, strip $! S.drop 1 bs2)+ if S.null bs2+ then return Nothing+ else return (Just (CI.mk $! strip key, strip $! S.drop 1 bs2)) strip = S.dropWhile (== charSpace) . fst . S.spanEnd (== charSpace)++data HeadersValidationResult+ = GoodHeaders+ | BadHeaders S.ByteString -- contains a message with the reason++validateHeaders :: RequestHeaders -> HeadersValidationResult+validateHeaders headers =+ case mapMaybe validateHeader headers of+ [] -> GoodHeaders+ reasons -> BadHeaders (S8.unlines reasons)+ where+ validateHeader (k, v)+ | S8.elem '\n' v = Just ("Header " <> CI.original k <> " has newlines")+ | True = Nothing
Network/HTTP/Client/Internal.hs view
@@ -25,6 +25,7 @@ , module Network.HTTP.Client.Types -- * Various utilities , module Network.HTTP.Client.Util+ , dummyManaged ) where import Network.HTTP.Client.Body@@ -37,3 +38,4 @@ import Network.HTTP.Client.Response import Network.HTTP.Client.Types import Network.HTTP.Client.Util+import Data.KeyedPool (dummyManaged)
Network/HTTP/Client/Manager.hs view
@@ -1,18 +1,15 @@-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE BangPatterns #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-} module Network.HTTP.Client.Manager ( ManagerSettings (..) , newManager , closeManager , withManager , getConn- , failedConnectionException , defaultManagerSettings , rawConnectionModifySocket+ , rawConnectionModifySocketSize , proxyFromRequest , noProxy , useProxy@@ -20,52 +17,25 @@ , proxyEnvironmentNamed , defaultProxy , dropProxyAuthSecure+ , useProxySecureWithoutConnect ) where -#ifndef MIN_VERSION_base-#define MIN_VERSION_base(x,y,z) 1-#endif-#if !MIN_VERSION_base(4,6,0)-import Prelude hiding (catch)-#endif-import Control.Applicative ((<|>))-import Control.Arrow (first)-import Data.Monoid (mappend)-import System.IO (hClose, hFlush, IOMode(..))-import qualified Data.IORef as I-import qualified Data.Map as Map- import qualified Data.ByteString.Char8 as S8-import qualified Data.ByteString.Lazy as L -import qualified Blaze.ByteString.Builder as Blaze--import Data.Char (toLower) import Data.Text (Text)-import qualified Data.Text as T-import Data.Text.Read (decimal) -import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad (unless, join, when, void, mplus)-import Control.Exception (mask_, SomeException, bracket, catch, throwIO, fromException, mask, IOException, Exception (..), handle)-import Control.Concurrent (forkIO, threadDelay)-import Data.Time (UTCTime (..), Day (..), DiffTime, getCurrentTime, addUTCTime)-import Control.DeepSeq (deepseq)+import Control.Monad (unless)+import Control.Exception (throwIO, fromException, IOException, Exception (..), handle) import qualified Network.Socket as NS -import Data.Maybe (mapMaybe)-import System.IO (Handle)-import System.Mem.Weak (Weak, deRefWeak) import Network.HTTP.Types (status200) import Network.HTTP.Client.Types import Network.HTTP.Client.Connection import Network.HTTP.Client.Headers (parseStatusHeaders)-import Network.HTTP.Client.Request (applyBasicProxyAuth, extractBasicAuthInfo)-import Control.Concurrent.MVar (MVar, takeMVar, tryPutMVar, newEmptyMVar)-import System.Environment (getEnvironment)-import qualified Network.URI as U-import Control.Monad (guard)+import Network.HTTP.Proxy+import Data.KeyedPool+import Data.Maybe (isJust) -- | A value for the @managerRawConnection@ setting, but also allows you to -- modify the underlying @Socket@ to set additional settings. For a motivating@@ -78,11 +48,12 @@ -- | Same as @rawConnectionModifySocket@, but also takes in a chunk size. ----- Since 0.4.5+-- @since 0.5.2 rawConnectionModifySocketSize :: (NS.Socket -> IO ()) -> IO (Int -> Maybe NS.HostAddress -> String -> Int -> IO Connection) rawConnectionModifySocketSize = return . openSocketConnectionSize + -- | Default value for @ManagerSettings@. -- -- Note that this value does /not/ have support for SSL/TLS. If you need to@@ -94,14 +65,14 @@ defaultManagerSettings = ManagerSettings { managerConnCount = 10 , managerRawConnection = return $ openSocketConnection (const $ return ())- , managerTlsConnection = return $ \_ _ _ -> throwIO TlsNotSupported- , managerTlsProxyConnection = return $ \_ _ _ _ _ _ -> throwIO TlsNotSupported- , managerResponseTimeout = Just 30000000+ , managerTlsConnection = return $ \_ _ _ -> throwHttp TlsNotSupported+ , managerTlsProxyConnection = return $ \_ _ _ _ _ _ -> throwHttp TlsNotSupported+ , managerResponseTimeout = ResponseTimeoutDefault , managerRetryableException = \e -> case fromException e of Just (_ :: IOException) -> True _ ->- case fromException e of+ case fmap unHttpExceptionContentWrapper $ fromException e of -- Note: Some servers will timeout connections by accepting -- the incoming packets for the new request, but closing -- the connection as soon as we try to read. To make sure@@ -110,62 +81,21 @@ Just NoResponseDataReceived -> True Just IncompleteHeaders -> True _ -> False- , managerWrapIOException =+ , managerWrapException = \_req -> let wrapper se = case fromException se of- Just e -> toException $ InternalIOException e- Nothing -> se- in handle $ throwIO . wrapper+ Just (_ :: IOException) -> throwHttp $ InternalException se+ Nothing -> throwIO se+ in handle wrapper , managerIdleConnectionCount = 512 , managerModifyRequest = return+ , managerModifyResponse = return , managerProxyInsecure = defaultProxy , managerProxySecure = defaultProxy+ , managerMaxHeaderLength = Just $ MaxHeaderLength 4096+ , managerMaxNumberHeaders = Just $ MaxNumberHeaders 100 } -takeSocket :: Manager -> ConnKey -> IO (Maybe Connection)-takeSocket man key =- I.atomicModifyIORef (mConns man) go- where- go ManagerClosed = (ManagerClosed, Nothing)- go mcOrig@(ManagerOpen idleCount m) =- case Map.lookup key m of- Nothing -> (mcOrig, Nothing)- Just (One a _) ->- let mc = ManagerOpen (idleCount - 1) (Map.delete key m)- in mc `seq` (mc, Just a)- Just (Cons a _ _ rest) ->- let mc = ManagerOpen (idleCount - 1) (Map.insert key rest m)- in mc `seq` (mc, Just a)--putSocket :: Manager -> ConnKey -> Connection -> IO ()-putSocket man key ci = do- now <- getCurrentTime- join $ I.atomicModifyIORef (mConns man) (go now)- void $ tryPutMVar (mConnsBaton man) ()- where- go _ ManagerClosed = (ManagerClosed , connectionClose ci)- go now mc@(ManagerOpen idleCount m)- | idleCount >= mIdleConnectionCount man = (mc, connectionClose ci)- | otherwise = case Map.lookup key m of- Nothing ->- let cnt' = idleCount + 1- m' = ManagerOpen cnt' (Map.insert key (One ci now) m)- in m' `seq` (m', return ())- Just l ->- let (l', mx) = addToList now (mMaxConns man) ci l- cnt' = idleCount + maybe 1 (const 0) mx- m' = ManagerOpen cnt' (Map.insert key l' m)- in m' `seq` (m', maybe (return ()) connectionClose mx)---- | Add a new element to the list, up to the given maximum number. If we're--- already at the maximum, return the new value as leftover.-addToList :: UTCTime -> Int -> a -> NonEmptyList a -> (NonEmptyList a, Maybe a)-addToList _ i x l | i <= 1 = (l, Just x)-addToList now _ x l@One{} = (Cons x 2 now l, Nothing)-addToList now maxCount x l@(Cons _ currCount _ _)- | maxCount > currCount = (Cons x (currCount + 1) now l, Nothing)- | otherwise = (l, Just x)- -- | Create a 'Manager'. The @Manager@ will be shut down automatically via -- garbage collection. --@@ -179,79 +109,36 @@ newManager :: ManagerSettings -> IO Manager newManager ms = do NS.withSocketsDo $ return ()- rawConnection <- managerRawConnection ms- tlsConnection <- managerTlsConnection ms- tlsProxyConnection <- managerTlsProxyConnection ms- mapRef <- I.newIORef $! ManagerOpen 0 Map.empty- baton <- newEmptyMVar- wmapRef <- I.mkWeakIORef mapRef $ closeManager' mapRef httpProxy <- runProxyOverride (managerProxyInsecure ms) False httpsProxy <- runProxyOverride (managerProxySecure ms) True - _ <- forkIO $ reap baton wmapRef+ createConnection <- mkCreateConnection ms++ keyedPool <- createKeyedPool+ createConnection+ connectionClose+ (managerConnCount ms)+ (managerIdleConnectionCount ms)+ (const (return ())) -- could allow something in ManagerSettings to handle exceptions more nicely+ let manager = Manager- { mConns = mapRef- , mConnsBaton = baton- , mMaxConns = managerConnCount ms+ { mConns = keyedPool , mResponseTimeout = managerResponseTimeout ms- , mRawConnection = rawConnection- , mTlsConnection = tlsConnection- , mTlsProxyConnection = tlsProxyConnection , mRetryableException = managerRetryableException ms- , mWrapIOException = managerWrapIOException ms- , mIdleConnectionCount = managerIdleConnectionCount ms+ , mWrapException = managerWrapException ms , mModifyRequest = managerModifyRequest ms+ , mModifyResponse = managerModifyResponse ms , mSetProxy = \req -> if secure req then httpsProxy req else httpProxy req+ , mMaxHeaderLength = managerMaxHeaderLength ms+ , mMaxNumberHeaders = managerMaxNumberHeaders ms } return manager --- | Collect and destroy any stale connections.-reap :: MVar () -> Weak (I.IORef ConnsMap) -> IO ()-reap baton wmapRef =- mask_ loop- where- loop = do- threadDelay (5 * 1000 * 1000)- mmapRef <- deRefWeak wmapRef- case mmapRef of- Nothing -> return () -- manager is closed- Just mapRef -> goMapRef mapRef-- goMapRef mapRef = do- now <- getCurrentTime- let isNotStale time = 30 `addUTCTime` time >= now- (newMap, toDestroy) <- I.atomicModifyIORef mapRef $ \m ->- let (newMap, toDestroy) = findStaleWrap isNotStale m- in (newMap, (newMap, toDestroy))- mapM_ safeConnClose toDestroy- case newMap of- ManagerOpen _ m | not $ Map.null m -> return ()- _ -> takeMVar baton- loop- findStaleWrap _ ManagerClosed = (ManagerClosed, [])- findStaleWrap isNotStale (ManagerOpen idleCount m) =- let (x, y) = findStale isNotStale m- in (ManagerOpen (idleCount - length y) x, y)- findStale isNotStale =- findStale' id id . Map.toList- where- findStale' destroy keep [] = (Map.fromList $ keep [], destroy [])- findStale' destroy keep ((connkey, nelist):rest) =- findStale' destroy' keep' rest- where- -- Note: By definition, the timestamps must be in descending order,- -- so we don't need to traverse the whole list.- (notStale, stale) = span (isNotStale . fst) $ neToList nelist- destroy' = destroy . (map snd stale++)- keep' =- case neFromList notStale of- Nothing -> keep- Just x -> keep . ((connkey, x):)-+ {- FIXME why isn't this being used anymore? flushStaleCerts now = Map.fromList . mapMaybe flushStaleCerts' . Map.toList where@@ -283,23 +170,7 @@ seqDT :: DiffTime -> b -> b seqDT = seq--neToList :: NonEmptyList a -> [(UTCTime, a)]-neToList (One a t) = [(t, a)]-neToList (Cons a _ t nelist) = (t, a) : neToList nelist--neFromList :: [(UTCTime, a)] -> Maybe (NonEmptyList a)-neFromList [] = Nothing-neFromList [(t, a)] = Just (One a t)-neFromList xs =- Just . snd . go $ xs- where- go [] = error "neFromList.go []"- go [(t, a)] = (2, One a t)- go ((t, a):rest) =- let (i, rest') = go rest- i' = i + 1- in i' `seq` (i', Cons a i t rest')+ -} -- | Close all connections in a 'Manager'. --@@ -312,14 +183,6 @@ closeManager _ = return () {-# DEPRECATED closeManager "Manager will be closed for you automatically when no longer in use" #-} -closeManager' :: I.IORef ConnsMap- -> IO ()-closeManager' connsRef = mask_ $ do- !m <- I.atomicModifyIORef connsRef $ \x -> (ManagerClosed, x)- case m of- ManagerClosed -> return ()- ManagerOpen _ m -> mapM_ (nonEmptyMapM_ safeConnClose) $ Map.elems m- -- | Create, use and close a 'Manager'. -- -- Since 0.2.1@@ -327,129 +190,92 @@ withManager settings f = newManager settings >>= f {-# DEPRECATED withManager "Use newManager instead" #-} -safeConnClose :: Connection -> IO ()-safeConnClose ci = connectionClose ci `catch` \(_ :: IOException) -> return ()--nonEmptyMapM_ :: Monad m => (a -> m ()) -> NonEmptyList a -> m ()-nonEmptyMapM_ f (One x _) = f x-nonEmptyMapM_ f (Cons x _ _ l) = f x >> nonEmptyMapM_ f l---- | This function needs to acquire a @ConnInfo@- either from the @Manager@ or--- via I\/O, and register it with the @ResourceT@ so it is guaranteed to be--- either released or returned to the manager.-getManagedConn- :: Manager- -> ConnKey- -> IO Connection- -> IO (ConnRelease, Connection, ManagedConn)--- We want to avoid any holes caused by async exceptions, so let's mask.-getManagedConn man key open = mask $ \restore -> do- -- Try to take the socket out of the manager.- mci <- takeSocket man key- (ci, isManaged) <-- case mci of- -- There wasn't a matching connection in the manager, so create a- -- new one.- Nothing -> do- ci <- restore open- return (ci, Fresh)- -- Return the existing one- Just ci -> return (ci, Reused)-- -- When we release this connection, we can either reuse it (put it back in- -- the manager) or not reuse it (close the socket). We set up a mutable- -- reference to track what we want to do. By default, we say not to reuse- -- it, that way if an exception is thrown, the connection won't be reused.- toReuseRef <- I.newIORef DontReuse- wasReleasedRef <- I.newIORef False-- -- When the connection is explicitly released, we update our toReuseRef to- -- indicate what action should be taken, and then call release.- let connRelease r = do- I.writeIORef toReuseRef r- releaseHelper-- releaseHelper = mask $ \restore -> do- wasReleased <- I.atomicModifyIORef wasReleasedRef $ \x -> (True, x)- unless wasReleased $ do- toReuse <- I.readIORef toReuseRef- restore $ case toReuse of- Reuse -> putSocket man key ci- DontReuse -> connectionClose ci-- return (connRelease, ci, isManaged)---- | Create an exception to be thrown if the connection for the given request--- fails.-failedConnectionException :: Request -> HttpException-failedConnectionException req =- FailedConnectionException host' port'- where- (_, host', port') = getConnDest req--getConnDest :: Request -> (Bool, String, Int)-getConnDest req =- case proxy req of- Just p -> (True, S8.unpack (proxyHost p), proxyPort p)- Nothing -> (False, S8.unpack $ host req, port req)- -- | Drop the Proxy-Authorization header from the request if we're using a -- secure proxy. dropProxyAuthSecure :: Request -> Request dropProxyAuthSecure req- | secure req && useProxy = req+ | secure req && useProxy' = req { requestHeaders = filter (\(k, _) -> k /= "Proxy-Authorization") (requestHeaders req) } | otherwise = req where- (useProxy, _, _) = getConnDest req+ useProxy' = isJust (proxy req) getConn :: Request -> Manager- -> IO (ConnRelease, Connection, ManagedConn)+ -> IO (Managed Connection) getConn req m -- Stop Mac OS X from getting high: -- https://github.com/snoyberg/http-client/issues/40#issuecomment-39117909- | S8.null h = throwIO $ InvalidDestinationHost h- | otherwise =- getManagedConn m (ConnKey connKeyHost connport (host req) (port req) (secure req)) $- wrapConnectExc $ go connaddr connhost connport+ | S8.null h = throwHttp $ InvalidDestinationHost h+ | otherwise = takeKeyedPool (mConns m) connkey where h = host req- (useProxy, connhost, connport) = getConnDest req- (connaddr, connKeyHost) =- case (hostAddress req, useProxy) of- (Just ha, False) -> (Just ha, HostAddress ha)- _ -> (Nothing, HostName $ T.pack connhost)+ connkey = connKey req +connKey :: Request -> ConnKey+connKey req@Request { proxy = Nothing, secure = False } =+ CKRaw (hostAddress req) (host req) (port req)+connKey req@Request { proxy = Nothing, secure = True } =+ CKSecure (hostAddress req) (host req) (port req)+connKey Request { proxy = Just p, secure = False } =+ CKRaw Nothing (proxyHost p) (proxyPort p)+connKey req@Request { proxy = Just p, secure = True,+ proxySecureMode = ProxySecureWithConnect } =+ CKProxy+ (proxyHost p)+ (proxyPort p)+ (lookup "Proxy-Authorization" (requestHeaders req))+ (host req)+ (port req)+connKey Request { proxy = Just p, secure = True,+ proxySecureMode = ProxySecureWithoutConnect } =+ CKRaw Nothing (proxyHost p) (proxyPort p)++mkCreateConnection :: ManagerSettings -> IO (ConnKey -> IO Connection)+mkCreateConnection ms = do+ rawConnection <- managerRawConnection ms+ tlsConnection <- managerTlsConnection ms+ tlsProxyConnection <- managerTlsProxyConnection ms++ return $ \ck -> wrapConnectExc $ case ck of+ CKRaw connaddr connhost connport ->+ rawConnection connaddr (S8.unpack connhost) connport+ CKSecure connaddr connhost connport ->+ tlsConnection connaddr (S8.unpack connhost) connport+ CKProxy connhost connport mProxyAuthHeader ultHost ultPort ->+ let proxyAuthorizationHeader = maybe+ ""+ (\h' -> S8.concat ["Proxy-Authorization: ", h', "\r\n"])+ mProxyAuthHeader+ hostHeader = S8.concat ["Host: ", ultHost, ":", (S8.pack $ show ultPort), "\r\n"]+ connstr = S8.concat+ [ "CONNECT "+ , ultHost+ , ":"+ , S8.pack $ show ultPort+ , " HTTP/1.1\r\n"+ , proxyAuthorizationHeader+ , hostHeader+ , "\r\n"+ ]+ parse conn = do+ let mhl = managerMaxHeaderLength ms+ mnh = managerMaxNumberHeaders ms+ StatusHeaders status _ _ _ <- parseStatusHeaders mhl mnh conn Nothing (\_ -> return ()) Nothing+ unless (status == status200) $+ throwHttp $ ProxyConnectException ultHost ultPort status+ in tlsProxyConnection+ connstr+ parse+ (S8.unpack ultHost)+ Nothing -- we never have a HostAddress we can use+ (S8.unpack connhost)+ connport+ where wrapConnectExc = handle $ \e ->- throwIO $ FailedConnectionException2 connhost connport (secure req)- (toException (e :: IOException))- go =- case (secure req, useProxy) of- (False, _) -> mRawConnection m- (True, False) -> mTlsConnection m- (True, True) ->- let ultHost = host req- ultPort = port req- proxyAuthorizationHeader = maybe "" (\h -> S8.concat ["Proxy-Authorization: ", h, "\r\n"]) . lookup "Proxy-Authorization" $ requestHeaders req- hostHeader = S8.concat ["Host: ", ultHost, (S8.pack $ show ultPort), "\r\n"]- connstr = S8.concat- [ "CONNECT "- , ultHost- , ":"- , S8.pack $ show ultPort- , " HTTP/1.1\r\n"- , proxyAuthorizationHeader- , hostHeader- , "\r\n"- ]- parse conn = do- sh@(StatusHeaders status _ _) <- parseStatusHeaders conn Nothing Nothing- unless (status == status200) $- throwIO $ ProxyConnectException ultHost ultPort $ Right $ StatusCodeException status [] (CJ [])- in mTlsProxyConnection m connstr parse (S8.unpack ultHost)+ throwHttp $ ConnectionFailure (toException (e :: IOException)) -- | Get the proxy settings from the @Request@ itself. --@@ -469,6 +295,15 @@ useProxy :: Proxy -> ProxyOverride useProxy p = ProxyOverride $ const $ return $ \req -> req { proxy = Just p } +-- | Send secure requests to the proxy in plain text rather than using CONNECT,+-- regardless of the value in the @Request@.+--+-- @since 0.7.2+useProxySecureWithoutConnect :: Proxy -> ProxyOverride+useProxySecureWithoutConnect p = ProxyOverride $+ const $ return $ \req -> req { proxy = Just p,+ proxySecureMode = ProxySecureWithoutConnect }+ -- | Get the proxy settings from the default environment variable (@http_proxy@ -- for insecure, @https_proxy@ for secure). If no variable is set, then fall -- back to the given value. @Nothing@ is equivalent to 'noProxy', @Just@ is@@ -477,13 +312,8 @@ -- Since 0.4.7 proxyEnvironment :: Maybe Proxy -- ^ fallback if no environment set -> ProxyOverride-proxyEnvironment mp = ProxyOverride $ \secure ->- envHelper (envName secure) $ maybe EHNoProxy EHUseProxy mp--envName :: Bool -- ^ secure?- -> Text-envName False = "http_proxy"-envName True = "https_proxy"+proxyEnvironment mp = ProxyOverride $ \secure' ->+ systemProxyHelper Nothing (httpProtocol secure') $ maybe EHNoProxy EHUseProxy mp -- | Same as 'proxyEnvironment', but instead of default environment variable -- names, allows you to set your own name.@@ -493,65 +323,12 @@ :: Text -- ^ environment variable name -> Maybe Proxy -- ^ fallback if no environment set -> ProxyOverride-proxyEnvironmentNamed name =- ProxyOverride . const . envHelper name- . maybe EHNoProxy EHUseProxy+proxyEnvironmentNamed name mp = ProxyOverride $ \secure' ->+ systemProxyHelper (Just name) (httpProtocol secure') $ maybe EHNoProxy EHUseProxy mp -- | The default proxy settings for a manager. In particular: if the @http_proxy@ (or @https_proxy@) environment variable is set, use it. Otherwise, use the values in the @Request@. -- -- Since 0.4.7 defaultProxy :: ProxyOverride-defaultProxy = ProxyOverride $ \secure ->- envHelper (envName secure) EHFromRequest--data EnvHelper = EHFromRequest- | EHNoProxy- | EHUseProxy Proxy--envHelper :: Text -> EnvHelper -> IO (Request -> Request)-envHelper name eh = do- env <- getEnvironment- let lenv = Map.fromList $ map (first $ T.toLower . T.pack) env- lookupEnvVar n = lookup (T.unpack n) env <|> Map.lookup n lenv- noProxyDomains = domainSuffixes (lookupEnvVar "no_proxy")- case lookupEnvVar name of- Nothing -> return noEnvProxy- Just "" -> return noEnvProxy- Just str -> do- let invalid = throwIO $ InvalidProxyEnvironmentVariable name (T.pack str)- (p, muserpass) <- maybe invalid return $ do- uri <- case U.parseURI str of- Just u | U.uriScheme u == "http:" -> return u- _ -> U.parseURI $ "http://" ++ str-- guard $ U.uriScheme uri == "http:"- guard $ null (U.uriPath uri) || U.uriPath uri == "/"- guard $ null $ U.uriQuery uri- guard $ null $ U.uriFragment uri-- auth <- U.uriAuthority uri- port <-- case U.uriPort auth of- "" -> Just 80- ':':rest ->- case decimal $ T.pack rest of- Right (p, "") -> Just p- _ -> Nothing- _ -> Nothing-- Just $ (Proxy (S8.pack $ U.uriRegName auth) port, extractBasicAuthInfo uri)- return $ \req ->- if host req `hasDomainSuffixIn` noProxyDomains- then noEnvProxy req- else maybe id (uncurry applyBasicProxyAuth) muserpass- req { proxy = Just p }- where noEnvProxy = case eh of- EHFromRequest -> id- EHNoProxy -> \req -> req { proxy = Nothing }- EHUseProxy p -> \req -> req { proxy = Just p }- prefixed s | S8.head s == '.' = s- | otherwise = S8.cons '.' s- domainSuffixes Nothing = []- domainSuffixes (Just "") = []- domainSuffixes (Just no_proxy) = [prefixed $ S8.dropWhile (== ' ') suffix | suffix <- S8.split ',' (S8.pack (map toLower no_proxy)), not (S8.null suffix)]- hasDomainSuffixIn host = any (`S8.isSuffixOf` prefixed (S8.map toLower host))+defaultProxy = ProxyOverride $ \secure' ->+ systemProxyHelper Nothing (httpProtocol secure') EHFromRequest
Network/HTTP/Client/MultipartFormData.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP, OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} -- | This module handles building multipart/form-data. Example usage: -- -- > {-# LANGUAGE OverloadedStrings #-}@@ -10,7 +10,7 @@ -- > -- > import Control.Monad -- >--- > main = withSocketsDo $ void $ withManager defaultManagerSettings $ \m -> do+-- > main = void $ withManager defaultManagerSettings $ \m -> do -- > req1 <- parseRequest "http://random-cat-photo.net/cat.jpg" -- > res <- httpLbs req1 m -- > req2 <- parseRequest "http://example.org/~friedrich/blog/addPost.hs"@@ -24,6 +24,7 @@ ( -- * Part type Part+ ,PartM ,partName ,partFilename ,partContentType@@ -77,17 +78,19 @@ import Control.Monad import Data.ByteString.Lazy.Internal (defaultChunkSize) +type Part = PartM IO+ -- | A single part of a multipart message.-data Part = Part+data PartM m = Part { partName :: Text -- ^ Name of the corresponding \<input\> , partFilename :: Maybe String -- ^ A file name, if this is an attached file , partContentType :: Maybe MimeType -- ^ Content type , partHeaders :: [Header] -- ^ List of additional headers- , partGetBody :: IO RequestBody -- ^ Action in m which returns the body+ , partGetBody :: m RequestBody -- ^ Action in m which returns the body -- of a message. } -instance Show Part where+instance Show (PartM m) where showsPrec d (Part n f c h _) = showParen (d>=11) $ showString "Part " . showsPrec 11 n@@ -104,19 +107,21 @@ -- -- The 'Part' does not have a file name or content type associated -- with it.-partBS :: Text -- ^ Name of the corresponding \<input\>.+partBS :: Applicative m+ => Text -- ^ Name of the corresponding \<input\>. -> BS.ByteString -- ^ The body for this 'Part'.- -> Part-partBS n b = Part n mempty mempty mempty $ return $ RequestBodyBS b+ -> PartM m+partBS n b = Part n Data.Monoid.mempty mempty mempty $ pure $ RequestBodyBS b -- | Make a 'Part' whose content is a lazy 'BL.ByteString'. -- -- The 'Part' does not have a file name or content type associated -- with it.-partLBS :: Text -- ^ Name of the corresponding \<input\>.+partLBS :: Applicative m+ => Text -- ^ Name of the corresponding \<input\>. -> BL.ByteString -- ^ The body for this 'Part'.- -> Part-partLBS n b = Part n mempty mempty mempty $ return $ RequestBodyLBS b+ -> PartM m+partLBS n b = Part n mempty mempty mempty $ pure $ RequestBodyLBS b -- | Make a 'Part' from a file. --@@ -179,12 +184,13 @@ -- > partFileRequestBody "file" mempty mempty -- -- The 'Part' does not have a content type associated with it.-partFileRequestBody :: Text -- ^ Name of the corresponding \<input\>.+partFileRequestBody :: Applicative m+ => Text -- ^ Name of the corresponding \<input\>. -> FilePath -- ^ File name to supply to the server. -> RequestBody -- ^ Data to upload.- -> Part+ -> PartM m partFileRequestBody n f rqb =- partFileRequestBodyM n f $ return rqb+ partFileRequestBodyM n f $ pure rqb -- | Construct a 'Part' from action returning the 'RequestBody' --@@ -195,8 +201,8 @@ -- The 'Part' does not have a content type associated with it. partFileRequestBodyM :: Text -- ^ Name of the corresponding \<input\>. -> FilePath -- ^ File name to supply to the server.- -> IO RequestBody -- ^ Action that will supply data to upload.- -> Part+ -> m RequestBody -- ^ Action that will supply data to upload.+ -> PartM m partFileRequestBodyM n f rqb = Part n (Just f) (Just $ defaultMimeLookup $ pack f) mempty rqb @@ -205,12 +211,13 @@ cp bs = RequestBodyBuilder (fromIntegral $ BS.length bs) $ copyByteString bs -- | Add a list of additional headers to this 'Part'.-addPartHeaders :: Part -> [Header] -> Part+addPartHeaders :: PartM m -> [Header] -> PartM m addPartHeaders p hs = p { partHeaders = partHeaders p <> hs } -renderPart :: BS.ByteString -- ^ Boundary between parts.- -> Part -> IO RequestBody-renderPart boundary (Part name mfilename mcontenttype hdrs get) = liftM render get+renderPart :: Functor m+ => BS.ByteString -- ^ Boundary between parts.+ -> PartM m -> m RequestBody+renderPart boundary (Part name mfilename mcontenttype hdrs get) = render <$> get where render renderBody = cp "--" <> cp boundary <> cp "\r\n" <> cp "Content-Disposition: form-data; name=\""@@ -225,7 +232,7 @@ <> cp "Content-Type: " <> cp ct _ -> mempty)- <> foldMap (\(k, v) ->+ <> Data.Foldable.foldMap (\(k, v) -> cp "\r\n" <> cp (CI.original k) <> cp ": "@@ -234,9 +241,10 @@ <> renderBody <> cp "\r\n" -- | Combine the 'Part's to form multipart/form-data body-renderParts :: BS.ByteString -- ^ Boundary between parts.- -> [Part] -> IO RequestBody-renderParts boundary parts = (fin . mconcat) `liftM` mapM (renderPart boundary) parts+renderParts :: Applicative m+ => BS.ByteString -- ^ Boundary between parts.+ -> [PartM m] -> m RequestBody+renderParts boundary parts = (fin . mconcat) <$> traverse (renderPart boundary) parts where fin = (<> cp "--" <> cp boundary <> cp "--\r\n") -- | Generate a boundary simillar to those generated by WebKit-based browsers.@@ -273,13 +281,12 @@ formDataBodyWithBoundary boundary a b -- | Add form data with supplied boundary-formDataBodyWithBoundary :: BS.ByteString -> [Part] -> Request -> IO Request+formDataBodyWithBoundary :: Applicative m => BS.ByteString -> [PartM m] -> Request -> m Request formDataBodyWithBoundary boundary parts req = do- body <- renderParts boundary parts- return $ req+ (\ body -> req { method = methodPost , requestHeaders = (hContentType, "multipart/form-data; boundary=" <> boundary) : Prelude.filter (\(x, _) -> x /= hContentType) (requestHeaders req) , requestBody = body- }+ }) <$> renderParts boundary parts
Network/HTTP/Client/Request.hs view
@@ -4,7 +4,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE BangPatterns #-}- {-# OPTIONS_GHC -fno-warn-orphans #-} module Network.HTTP.Client.Request@@ -12,36 +11,45 @@ , parseUrlThrow , parseRequest , parseRequest_+ , requestFromURI+ , requestFromURI_ , defaultRequest , setUriRelative , getUri , setUri+ , setUriEither , browserDecompress , alwaysDecompress , addProxy , applyBasicAuth , applyBasicProxyAuth+ , applyBearerAuth , urlEncodedBody , needsGunzip , requestBuilder- , useDefaultTimeout , setRequestIgnoreStatus+ , setRequestCheckStatus , setQueryString+#if MIN_VERSION_http_types(0,12,1)+ , setQueryStringPartialEscape+#endif , streamFile , observedStreamFile , extractBasicAuthInfo+ , throwErrorStatusCodes+ , addProxySecureWithoutConnect ) where import Data.Int (Int64)-import Data.Maybe (fromMaybe, isJust, isNothing)-import Data.Monoid (mempty, mappend)+import Data.Maybe (fromMaybe, isNothing)+import Data.Monoid (mempty, mappend, (<>)) import Data.String (IsString(..)) import Data.Char (toLower)-import Control.Applicative ((<$>))-import Control.Monad (when, unless, guard)+import Control.Applicative as A ((<$>))+import Control.Monad (unless, guard)+import Control.Monad.IO.Class (MonadIO, liftIO) import Numeric (showHex)--import Data.Default.Class (Default (def))+import qualified Data.Set as Set import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString, toByteStringIO, flush) import Blaze.ByteString.Builder.Char8 (fromChar, fromShow)@@ -52,22 +60,18 @@ import Data.ByteString.Lazy.Internal (defaultChunkSize) import qualified Network.HTTP.Types as W-import Network.URI (URI (..), URIAuth (..), parseURI, relativeTo, escapeURIString, unEscapeString, isAllowedInURI, isReserved)+import Network.URI (URI (..), URIAuth (..), parseURI, relativeTo, escapeURIString, unEscapeString, isAllowedInURI) -import Control.Monad.IO.Class (liftIO)-import Control.Exception (Exception, toException, throw, throwIO, IOException)+import Control.Exception (throw, throwIO, IOException) import qualified Control.Exception as E import qualified Data.CaseInsensitive as CI import qualified Data.ByteString.Base64 as B64 +import Network.HTTP.Client.Body import Network.HTTP.Client.Types import Network.HTTP.Client.Util-import Network.HTTP.Client.Connection -import Network.HTTP.Client.Util (readDec, (<>))-import Data.Time.Clock import Control.Monad.Catch (MonadThrow, throwM)-import Data.IORef import System.IO (withBinaryFile, hTell, hFileSize, Handle, IOMode (ReadMode)) import Control.Monad (liftM)@@ -80,32 +84,37 @@ parseUrl = parseUrlThrow {-# DEPRECATED parseUrl "Please use parseUrlThrow, parseRequest, or parseRequest_ instead" #-} --- | Same as 'parseRequest', except will throw an 'HttpException' in--- the event of a non-2XX response.+-- | Same as 'parseRequest', except will throw an 'HttpException' in the+-- event of a non-2XX response. This uses 'throwErrorStatusCodes' to+-- implement 'checkResponse'. -- -- @since 0.4.30 parseUrlThrow :: MonadThrow m => String -> m Request-parseUrlThrow s' =- case parseURI (encode s) of- Just uri -> liftM setMethod (setUri def uri)- Nothing -> throwM $ InvalidUrlException s "Invalid URL"+parseUrlThrow =+ liftM yesThrow . parseRequest where- encode = escapeURIString isAllowedInURI- (mmethod, s) =- case break (== ' ') s' of- (x, ' ':y) | all (\c -> 'A' <= c && c <= 'Z') x -> (Just x, y)- _ -> (Nothing, s')-- setMethod req =- case mmethod of- Nothing -> req- Just m -> req { method = S8.pack m }+ yesThrow req = req { checkResponse = throwErrorStatusCodes } +-- | Throws a 'StatusCodeException' wrapped in 'HttpExceptionRequest',+-- if the response's status code indicates an error (if it isn't 2xx).+-- This can be used to implement 'checkResponse'.+--+-- @since 0.5.13+throwErrorStatusCodes :: MonadIO m => Request -> Response BodyReader -> m ()+throwErrorStatusCodes req res = do+ let W.Status sci _ = responseStatus res+ if 200 <= sci && sci < 300+ then return ()+ else liftIO $ do+ chunk <- brReadSome (responseBody res) 1024+ let res' = fmap (const ()) res+ let ex = StatusCodeException res' (L.toStrict chunk)+ throwIO $ HttpExceptionRequest req ex -- | Convert a URL into a 'Request'. ----- This defaults some of the values in 'Request', such as setting 'method' to--- GET and 'requestHeaders' to @[]@.+-- This function defaults some of the values in 'Request', such as setting 'method' to+-- @"GET"@ and 'requestHeaders' to @[]@. -- -- Since this function uses 'MonadThrow', the return monad can be anything that is -- an instance of 'MonadThrow', such as 'IO' or 'Maybe'.@@ -113,39 +122,68 @@ -- You can place the request method at the beginning of the URL separated by a -- space, e.g.: ----- @@@--- parseRequeset "POST http://httpbin.org/post"--- @@@+-- @+-- parseRequest "POST http://httpbin.org/post"+-- @ -- -- Note that the request method must be provided as all capital letters. --+-- A 'Request' created by this function won't cause exceptions on non-2XX+-- response status codes.+--+-- To create a request which throws on non-2XX status codes, see 'parseUrlThrow'+-- -- @since 0.4.30 parseRequest :: MonadThrow m => String -> m Request-parseRequest =- liftM noThrow . parseUrlThrow+parseRequest s' =+ case parseURI (encode s) of+ Just uri -> liftM setMethod (setUri defaultRequest uri)+ Nothing -> throwM $ InvalidUrlException s "Invalid URL" where- noThrow req = req { checkStatus = \_ _ _ -> Nothing }+ encode = escapeURIString isAllowedInURI+ (mmethod, s) =+ case break (== ' ') s' of+ (x, ' ':y) | all (\c -> 'A' <= c && c <= 'Z') x -> (Just x, y)+ _ -> (Nothing, s') --- | Same as 'parseRequest', but in the cases of a parse error--- generates an impure exception. Mostly useful for static strings which--- are known to be correctly formatted.+ setMethod req =+ case mmethod of+ Nothing -> req+ Just m -> req { method = S8.pack m }++-- | Same as 'parseRequest', but parse errors cause an impure exception.+-- Mostly useful for static strings which are known to be correctly+-- formatted. parseRequest_ :: String -> Request parseRequest_ = either throw id . parseRequest +-- | Convert a 'URI' into a 'Request'.+--+-- This can fail if the given 'URI' is not absolute, or if the+-- 'URI' scheme is not @"http"@ or @"https"@. In these cases the function+-- will throw an error via 'MonadThrow'.+--+-- This function defaults some of the values in 'Request', such as setting 'method' to+-- @"GET"@ and 'requestHeaders' to @[]@.+--+-- A 'Request' created by this function won't cause exceptions on non-2XX+-- response status codes.+--+-- @since 0.5.12+requestFromURI :: MonadThrow m => URI -> m Request+requestFromURI = setUri defaultRequest++-- | Same as 'requestFromURI', but if the conversion would fail,+-- throws an impure exception.+--+-- @since 0.5.12+requestFromURI_ :: URI -> Request+requestFromURI_ = either throw id . requestFromURI+ -- | Add a 'URI' to the request. If it is absolute (includes a host name), add -- it as per 'setUri'; if it is relative, merge it with the existing request. setUriRelative :: MonadThrow m => Request -> URI -> m Request-setUriRelative req uri =-#ifndef MIN_VERSION_network-#define MIN_VERSION_network(x,y,z) 1-#endif-#if MIN_VERSION_network(2,4,0)- setUri req $ uri `relativeTo` getUri req-#else- case uri `relativeTo` getUri req of- Just uri' -> setUri req uri'- Nothing -> throwM $ InvalidUrlException (show uri) "Invalid URL"-#endif+setUriRelative req uri = setUri req $ uri `relativeTo` getUri req -- | Extract a 'URI' from the request. --@@ -158,7 +196,7 @@ , uriAuthority = Just URIAuth { uriUserInfo = "" , uriRegName = S8.unpack $ host req- , uriPort = ':' : show (port req)+ , uriPort = port' } , uriPath = S8.unpack $ path req , uriQuery =@@ -167,6 +205,11 @@ _ -> S8.unpack $ queryString req , uriFragment = "" }+ where+ port'+ | secure req && (port req) == 443 = ""+ | not (secure req) && (port req) == 80 = ""+ | otherwise = ':' : show (port req) applyAnyUriBasedAuth :: URI -> Request -> Request applyAnyUriBasedAuth uri req =@@ -178,7 +221,7 @@ -- Return Nothing when there is no auth info in URI. extractBasicAuthInfo :: URI -> Maybe (S8.ByteString, S8.ByteString) extractBasicAuthInfo uri = do- userInfo <- uriUserInfo <$> uriAuthority uri+ userInfo <- uriUserInfo A.<$> uriAuthority uri guard (':' `elem` userInfo) let (username, ':':password) = break (==':') . takeWhile (/='@') $ userInfo return (toLiteral username, toLiteral password)@@ -187,9 +230,18 @@ -- | Validate a 'URI', then add it to the request. setUri :: MonadThrow m => Request -> URI -> m Request-setUri req uri = do+setUri req uri = either throwInvalidUrlException return (setUriEither req uri)+ where+ throwInvalidUrlException = throwM . InvalidUrlException (show uri)++-- | A variant of `setUri` that returns an error message on validation errors,+-- instead of propagating them with `throwM`.+--+-- @since 0.6.1+setUriEither :: Request -> URI -> Either String Request+setUriEither req uri = do sec <- parseScheme uri- auth <- maybe (failUri "URL must be absolute") return $ uriAuthority uri+ auth <- maybe (Left "URL must be absolute") return $ uriAuthority uri port' <- parsePort sec auth return $ applyAnyUriBasedAuth uri req { host = S8.pack $ uriRegName auth@@ -202,61 +254,32 @@ , queryString = S8.pack $ uriQuery uri } where- failUri :: MonadThrow m => String -> m a- failUri = throwM . InvalidUrlException (show uri)- parseScheme URI{uriScheme = scheme} = case map toLower scheme of "http:" -> return False "https:" -> return True- _ -> failUri "Invalid scheme"+ _ -> Left "Invalid scheme" parsePort sec URIAuth{uriPort = portStr} = case portStr of -- If the user specifies a port, then use it ':':rest -> maybe- (failUri "Invalid port")+ (Left "Invalid port") return- (readDec rest)+ (readPositiveInt rest) -- Otherwise, use the default port _ -> case sec of False {- HTTP -} -> return 80 True {- HTTPS -} -> return 443 -instance Show Request where- show x = unlines- [ "Request {"- , " host = " ++ show (host x)- , " port = " ++ show (port x)- , " secure = " ++ show (secure x)- , " requestHeaders = " ++ show (requestHeaders x)- , " path = " ++ show (path x)- , " queryString = " ++ show (queryString x)- --, " requestBody = " ++ show (requestBody x)- , " method = " ++ show (method x)- , " proxy = " ++ show (proxy x)- , " rawBody = " ++ show (rawBody x)- , " redirectCount = " ++ show (redirectCount x)- , " responseTimeout = " ++ show (responseTimeout x)- , " requestVersion = " ++ show (requestVersion x)- , "}"- ]---- | Magic value to be placed in a 'Request' to indicate that we should use the--- timeout value in the @Manager@.+-- | A default request value, a GET request of localhost/:80, with an+-- empty request body. ----- Since 1.9.3-useDefaultTimeout :: Maybe Int-useDefaultTimeout = Just (-3425)---- | A default request value+-- Note that the default 'checkResponse' does nothing. -- -- @since 0.4.30 defaultRequest :: Request-defaultRequest = def { checkStatus = \_ _ _ -> Nothing }--instance Default Request where- def = Request+defaultRequest = Request { host = "localhost" , port = 80 , secure = False@@ -270,40 +293,29 @@ , rawBody = False , decompress = browserDecompress , redirectCount = 10- , checkStatus = \s@(W.Status sci _) hs cookie_jar ->- if 200 <= sci && sci < 300- then Nothing- else Just $ toException $ StatusCodeException s hs cookie_jar- , responseTimeout = useDefaultTimeout- , getConnectionWrapper = \mtimeout exc f ->- case mtimeout of- Nothing -> fmap ((,) Nothing) f- Just timeout' -> do- before <- getCurrentTime- mres <- timeout timeout' f- case mres of- Nothing -> throwIO exc- Just res -> do- now <- getCurrentTime- let timeSpentMicro = diffUTCTime now before * 1000000- remainingTime = round $ fromIntegral timeout' - timeSpentMicro- if remainingTime <= 0- then throwIO exc- else return (Just remainingTime, res)- , cookieJar = Just def+ , checkResponse = \_ _ -> return ()+ , responseTimeout = ResponseTimeoutDefault+ , cookieJar = Just Data.Monoid.mempty , requestVersion = W.http11 , onRequestBodyException = \se -> case E.fromException se of Just (_ :: IOException) -> return () Nothing -> throwIO se , requestManagerOverride = Nothing+ , shouldStripHeaderOnRedirect = const False+ , shouldStripHeaderOnRedirectIfOnDifferentHostOnly = False+ , proxySecureMode = ProxySecureWithConnect+ , redactHeaders = Set.singleton "Authorization"+ , earlyHintHeadersReceived = \_ -> return () } +-- | Parses a URL via 'parseRequest_'+--+-- /NOTE/: Prior to version 0.5.0, this instance used 'parseUrlThrow'+-- instead. instance IsString Request where- fromString s =- case parseUrl s of- Left e -> throw e- Right r -> r+ fromString = parseRequest_+ {-# INLINE fromString #-} -- | Always decompress a compressed stream. alwaysDecompress :: S.ByteString -> Bool@@ -313,19 +325,46 @@ browserDecompress :: S.ByteString -> Bool browserDecompress = (/= "application/x-tar") +-- | Build a basic-auth header value+buildBasicAuth ::+ S8.ByteString -- ^ Username+ -> S8.ByteString -- ^ Password+ -> S8.ByteString+buildBasicAuth user passwd =+ S8.append "Basic " (B64.encode (S8.concat [ user, ":", passwd ]))+ -- | Add a Basic Auth header (with the specified user name and password) to the -- given Request. Ignore error handling: -- -- > applyBasicAuth "user" "pass" $ parseRequest_ url --+-- NOTE: The function @applyDigestAuth@ is provided by the @http-client-tls@+-- package instead of this package due to extra dependencies. Please use that+-- package if you need to use digest authentication.+-- -- Since 0.1.0 applyBasicAuth :: S.ByteString -> S.ByteString -> Request -> Request applyBasicAuth user passwd req = req { requestHeaders = authHeader : requestHeaders req } where- authHeader = (CI.mk "Authorization", basic)- basic = S8.append "Basic " (B64.encode $ S8.concat [ user, ":", passwd ])+ authHeader = (CI.mk "Authorization", buildBasicAuth user passwd) +-- | Build a bearer-auth header value+buildBearerAuth ::+ S8.ByteString -- ^ Token+ -> S8.ByteString+buildBearerAuth token =+ S8.append "Bearer " token++-- | Add a Bearer Auth header to the given 'Request'+--+-- @since 0.7.6+applyBearerAuth :: S.ByteString -> Request -> Request+applyBearerAuth bearerToken req =+ req { requestHeaders = authHeader : requestHeaders req }+ where+ authHeader = (CI.mk "Authorization", buildBearerAuth bearerToken)+ -- | Add a proxy to the Request so that the Request when executed will use -- the provided proxy. --@@ -334,6 +373,13 @@ addProxy hst prt req = req { proxy = Just $ Proxy hst prt } ++-- | Send secure requests to the proxy in plain text rather than using CONNECT.+--+-- @since 0.7.2+addProxySecureWithoutConnect :: Request -> Request+addProxySecureWithoutConnect req = req { proxySecureMode = ProxySecureWithoutConnect }+ -- | Add a Proxy-Authorization header (with the specified username and -- password) to the given 'Request'. Ignore error handling: --@@ -345,8 +391,7 @@ applyBasicProxyAuth user passwd req = req { requestHeaders = authHeader : requestHeaders req } where- authHeader = (CI.mk "Proxy-Authorization", basic)- basic = S8.append "Basic " (B64.encode $ S8.concat [ user , ":", passwd ])+ authHeader = (CI.mk "Proxy-Authorization", buildBasicAuth user passwd) -- | Add url-encoded parameters to the 'Request'. --@@ -374,6 +419,19 @@ && ("content-encoding", "gzip") `elem` hs' && decompress req (fromMaybe "" $ lookup "content-type" hs') +data EncapsulatedPopperException = EncapsulatedPopperException E.SomeException+ deriving (Show)+instance E.Exception EncapsulatedPopperException++-- | Encapsulate a thrown exception into a custom type+--+-- During streamed body sending, both the Popper and the connection may throw IO exceptions;+-- however, we don't want to route the Popper exceptions through onRequestBodyException.+-- https://github.com/snoyberg/http-client/issues/469+encapsulatePopperException :: IO a -> IO a+encapsulatePopperException action =+ action `E.catch` (\(ex :: E.SomeException) -> E.throwIO (EncapsulatedPopperException ex))+ requestBuilder :: Request -> Connection -> IO (Maybe (IO ())) requestBuilder req Connection {..} = do (contentLength, sendNow, sendLater) <- toTriple (requestBody req)@@ -382,9 +440,12 @@ else sendNow >> return Nothing where expectContinue = Just "100-continue" == lookup "Expect" (requestHeaders req)- checkBadSend f = f `E.catch` onRequestBodyException req+ checkBadSend f = f `E.catches` [+ E.Handler (\(EncapsulatedPopperException ex) -> throwIO ex)+ , E.Handler (onRequestBodyException req)+ ] writeBuilder = toByteStringIO connectionWrite- writeHeadersWith contentLength = writeBuilder . (builder contentLength `mappend`)+ writeHeadersWith contentLength = writeBuilder . (builder contentLength `Data.Monoid.mappend`) flushHeaders contentLength = writeHeadersWith contentLength flush toTriple (RequestBodyLBS lbs) = do@@ -420,16 +481,16 @@ toTriple (RequestBodyIO mbody) = mbody >>= toTriple writeStream mlen withStream =- withStream (loop 0) + withStream (loop 0) where loop !n stream = do- bs <- stream+ bs <- encapsulatePopperException stream if S.null bs- then case mlen of + then case mlen of -- If stream is chunked, no length argument Nothing -> connectionWrite "0\r\n\r\n" -- Not chunked - validate length argument- Just len -> unless (len == n) $ throwIO $ WrongRequestBodyStreamSize (fromIntegral len) (fromIntegral n)+ Just len -> unless (len == n) $ throwHttp $ WrongRequestBodyStreamSize (fromIntegral len) (fromIntegral n) else do connectionWrite $ if (isNothing mlen) -- Chunked@@ -441,7 +502,6 @@ else bs loop (n + (S.length bs)) stream - hh | port req == 80 && not (secure req) = host req | port req == 443 && secure req = host req@@ -451,10 +511,17 @@ | secure req = fromByteString "https://" | otherwise = fromByteString "http://" - requestHostname- | isJust (proxy req) && not (secure req)- = requestProtocol <> fromByteString hh- | otherwise = mempty+ requestHostname (Request { proxy = Nothing }) = mempty+ requestHostname (Request { proxy = Just _,+ secure = False }) =+ requestProtocol <> fromByteString hh+ requestHostname (Request { proxy = Just _,+ secure = True,+ proxySecureMode = ProxySecureWithConnect }) = mempty+ requestHostname (Request { proxy = Just _,+ secure = True,+ proxySecureMode = ProxySecureWithoutConnect }) =+ requestProtocol <> fromByteString hh contentLengthHeader (Just contentLength') = if method req `elem` ["GET", "HEAD"] && contentLength' == 0@@ -484,7 +551,7 @@ builder contentLength = fromByteString (method req) <> fromByteString " "- <> requestHostname+ <> requestHostname req <> (case S8.uncons $ path req of Just ('/', _) -> fromByteString $ path req _ -> fromChar '/' <> fromByteString (path req))@@ -515,13 +582,28 @@ -- -- @since 0.4.29 setRequestIgnoreStatus :: Request -> Request-setRequestIgnoreStatus req = req { checkStatus = \_ _ _ -> Nothing }+setRequestIgnoreStatus req = req { checkResponse = \_ _ -> return () } +-- | Modify the request so that non-2XX status codes generate a runtime+-- 'StatusCodeException', by using 'throwErrorStatusCodes'+--+-- @since 0.5.13+setRequestCheckStatus :: Request -> Request+setRequestCheckStatus req = req { checkResponse = throwErrorStatusCodes }+ -- | Set the query string to the given key/value pairs. -- -- Since 0.3.6 setQueryString :: [(S.ByteString, Maybe S.ByteString)] -> Request -> Request setQueryString qs req = req { queryString = W.renderQuery True qs }++#if MIN_VERSION_http_types(0,12,1)+-- | Set the query string to the given key/value pairs.+--+-- @since 0.5.10+setQueryStringPartialEscape :: [(S.ByteString, [W.EscapeItem])] -> Request -> Request+setQueryStringPartialEscape qs req = req { queryString = W.renderQueryPartialEscape True qs }+#endif -- | Send a file as the request body. --
Network/HTTP/Client/Response.hs view
@@ -1,23 +1,20 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE RecordWildCards #-} module Network.HTTP.Client.Response ( getRedirectedRequest , getResponse , lbsResponse+ , getOriginalRequest ) where -import Control.Monad ((>=>), when)--import Control.Exception (throwIO)-+import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L--import Data.Default.Class (def)+import qualified Data.CaseInsensitive as CI+import Control.Arrow (second) -import Data.Maybe (isJust)+import Data.Monoid (mempty)+import Data.List (nubBy) import qualified Network.HTTP.Types as W import Network.URI (parseURIReference, escapeURIString, isAllowedInURI)@@ -28,6 +25,7 @@ import Network.HTTP.Client.Util import Network.HTTP.Client.Body import Network.HTTP.Client.Headers+import Data.KeyedPool -- | If a request is a redirection (status code 3xx) this function will create -- a new request from the old request, the server headers returned with the@@ -46,18 +44,18 @@ -- > (\req' -> do -- > res <- http req'{redirectCount=0} man -- > modify (\rqs -> req' : rqs)--- > return (res, getRedirectedRequest req' (responseHeaders res) (responseCookieJar res) (W.statusCode (responseStatus res))+-- > return (res, getRedirectedRequest req req' (responseHeaders res) (responseCookieJar res) (W.statusCode (responseStatus res)) -- > ) -- > 'lift' -- > req -- > applyCheckStatus (checkStatus req) res -- > return redirectRequests-getRedirectedRequest :: Request -> W.ResponseHeaders -> CookieJar -> Int -> Maybe Request-getRedirectedRequest req hs cookie_jar code+getRedirectedRequest :: Request -> Request -> W.ResponseHeaders -> CookieJar -> Int -> Maybe Request+getRedirectedRequest origReq req hs cookie_jar code | 300 <= code && code < 400 = do l' <- lookup "location" hs let l = escapeURIString isAllowedInURI (S8.unpack l')- req' <- setUriRelative req =<< parseURIReference l+ req' <- fmap stripHeaders <$> setUriRelative req =<< parseURIReference l return $ if code == 302 || code == 303 -- According to the spec, this should *only* be for status code@@ -72,8 +70,40 @@ else req' {cookieJar = cookie_jar'} | otherwise = Nothing where+ cookie_jar' :: Maybe CookieJar cookie_jar' = fmap (const cookie_jar) $ cookieJar req + hostDiffer :: Request -> Bool+ hostDiffer req = host origReq /= host req++ shouldStripOnlyIfHostDiffer :: Bool+ shouldStripOnlyIfHostDiffer = shouldStripHeaderOnRedirectIfOnDifferentHostOnly req++ mergeHeaders :: W.RequestHeaders -> W.RequestHeaders -> W.RequestHeaders+ mergeHeaders lhs rhs = nubBy (\(a, _) (a', _) -> a == a') (lhs ++ rhs)++ stripHeaders :: Request -> Request+ stripHeaders r = do+ case (hostDiffer r, shouldStripOnlyIfHostDiffer) of+ (True, True) -> stripHeaders' r+ (True, False) -> stripHeaders' r+ (False, False) -> stripHeaders' r+ (False, True) -> do+ -- We need to check if we have omitted headers in previous+ -- request chain. Consider request chain:+ --+ -- 1. example-1.com+ -- 2. example-2.com (we may have removed some headers here from 1)+ -- 3. example-1.com (since we are back at same host as 1, we need re-add stripped headers)+ --+ let strippedHeaders = filter (shouldStripHeaderOnRedirect r . fst) (requestHeaders origReq)+ r{requestHeaders = mergeHeaders (requestHeaders r) strippedHeaders}++ stripHeaders' :: Request -> Request+ stripHeaders' r = r{requestHeaders =+ filter (not . shouldStripHeaderOnRedirect req . fst) $+ requestHeaders r}+ -- | Convert a 'Response' that has a 'Source' body to one with a lazy -- 'L.ByteString' body. lbsResponse :: Response BodyReader -> IO (Response L.ByteString)@@ -83,21 +113,30 @@ { responseBody = L.fromChunks bss } -getResponse :: ConnRelease+getResponse :: Maybe MaxHeaderLength+ -> Maybe MaxNumberHeaders -> Maybe Int -> Request- -> Connection+ -> Managed Connection -> Maybe (IO ()) -- ^ Action to run in case of a '100 Continue'. -> IO (Response BodyReader)-getResponse connRelease timeout' req@(Request {..}) conn cont = do- StatusHeaders s version hs <- parseStatusHeaders conn timeout' cont- let mcl = lookup "content-length" hs >>= readDec . S8.unpack- isChunked = ("transfer-encoding", "chunked") `elem` hs+getResponse mhl mnh timeout' req@(Request {..}) mconn cont = do+ let conn = managedResource mconn+ StatusHeaders s version earlyHs hs <- parseStatusHeaders mhl mnh conn timeout' earlyHintHeadersReceived cont+ let mcl = lookup "content-length" hs >>= readPositiveInt . S8.unpack+ isChunked = ("transfer-encoding", CI.mk "chunked") `elem` map (second CI.mk) hs -- should we put this connection back into the connection manager? toPut = Just "close" /= lookup "connection" hs && version > W.HttpVersion 1 0- cleanup bodyConsumed = connRelease $ if toPut && bodyConsumed then Reuse else DontReuse+ cleanup bodyConsumed = do+ managedRelease mconn $ if toPut && bodyConsumed then Reuse else DontReuse+ -- Keep alive the `Managed Connection` until we're done with it, to prevent an early+ -- collection.+ -- Reasoning: as long as someone holds a reference to the explicit cleanup,+ -- we shouldn't perform an implicit cleanup.+ keepAlive mconn + body <- -- RFC 2616 section 4.4_1 defines responses that must not include a body if hasNoBody method (W.statusCode s) || (mcl == Just 0 && not isChunked)@@ -107,21 +146,39 @@ else do body1 <- if isChunked- then makeChunkedReader rawBody conn+ then makeChunkedReader mhl (cleanup True) rawBody conn else case mcl of- Just len -> makeLengthReader len conn- Nothing -> makeUnlimitedReader conn- body2 <- if needsGunzip req hs+ Just len -> makeLengthReader (cleanup True) len conn+ Nothing -> makeUnlimitedReader (cleanup True) conn+ if needsGunzip req hs then makeGzipReader body1 else return body1- return $ brAddCleanup (cleanup True) body2 return Response { responseStatus = s , responseVersion = version , responseHeaders = hs , responseBody = body- , responseCookieJar = def+ , responseCookieJar = Data.Monoid.mempty , responseClose' = ResponseClose (cleanup False)+ , responseOriginalRequest = req {requestBody = ""}+ , responseEarlyHints = earlyHs }++-- | Does this response have no body?+hasNoBody :: ByteString -- ^ request method+ -> Int -- ^ status code+ -> Bool+hasNoBody "HEAD" _ = True+hasNoBody _ 204 = True+hasNoBody _ 304 = True+hasNoBody _ i = 100 <= i && i < 200++-- | Retrieve the orignal 'Request' from a 'Response'+--+-- Note that the 'requestBody' is not available and always set to empty.+--+-- @since 0.7.8+getOriginalRequest :: Response a -> Request+getOriginalRequest = responseOriginalRequest
Network/HTTP/Client/Types.hs view
@@ -1,25 +1,31 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} module Network.HTTP.Client.Types ( BodyReader , Connection (..) , StatusHeaders (..)- , ConnectionClosed (..) , HttpException (..)+ , HttpExceptionContent (..)+ , unHttpExceptionContentWrapper+ , throwHttp+ , toHttpException , Cookie (..)+ , equalCookie+ , equivCookie+ , compareCookies , CookieJar (..)+ , equalCookieJar+ , equivCookieJar , Proxy (..) , RequestBody (..) , Popper , NeedsPopper , GivesPopper , Request (..)- , ConnReuse (..)- , ConnRelease- , ManagedConn (..) , Response (..) , ResponseClose (..) , Manager (..)@@ -31,19 +37,23 @@ , ConnKey (..) , ProxyOverride (..) , StreamFileStatus (..)+ , ResponseTimeout (..)+ , ProxySecureMode (..)+ , MaxHeaderLength (..)+ , MaxNumberHeaders (..) ) where import qualified Data.Typeable as T (Typeable) import Network.HTTP.Types-import Control.Exception (Exception, IOException, SomeException)+import Control.Exception (Exception, SomeException, throwIO) import Data.Word (Word64) import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Blaze.ByteString.Builder (Builder, fromLazyByteString, fromByteString, toLazyByteString) import Data.Int (Int64)-import Data.Default.Class import Data.Foldable (Foldable)-import Data.Monoid+import Data.Monoid (Monoid(..))+import Data.Semigroup (Semigroup(..)) import Data.String (IsString, fromString) import Data.Time (UTCTime) import Data.Traversable (Traversable)@@ -51,12 +61,12 @@ import Network.Socket (HostAddress) import Data.IORef import qualified Network.Socket as NS-import qualified Data.IORef as I import qualified Data.Map as Map+import qualified Data.Set as Set import Data.Text (Text) import Data.Streaming.Zlib (ZlibException)-import Control.Concurrent.MVar (MVar) import Data.CaseInsensitive as CI+import Data.KeyedPool (KeyedPool) -- | An @IO@ action that represents an incoming response body coming from the -- server. Data provided by this action has already been gunzipped and@@ -77,48 +87,129 @@ -- ^ Send data to server , connectionClose :: IO () -- ^ Close connection. Any successive operation on the connection- -- (exept closing) should fail with `ConnectionClosed` exception.+ -- (except closing) should fail with `ConnectionClosed` exception. -- It is allowed to close connection multiple times. } deriving T.Typeable -data StatusHeaders = StatusHeaders Status HttpVersion RequestHeaders+data StatusHeaders = StatusHeaders Status HttpVersion RequestHeaders RequestHeaders deriving (Show, Eq, Ord, T.Typeable) -data ConnectionClosed = ConnectionClosed- deriving (Eq, Show, T.Typeable)+-- | A newtype wrapper which is not exported from this library but is an+-- instance of @Exception@. This allows @HttpExceptionContent@ to be thrown+-- (via this wrapper), but users of the library can't accidentally try to catch+-- it (when they /should/ be trying to catch 'HttpException').+--+-- @since 0.5.0+newtype HttpExceptionContentWrapper = HttpExceptionContentWrapper+ { unHttpExceptionContentWrapper :: HttpExceptionContent+ }+ deriving (Show, T.Typeable)+instance Exception HttpExceptionContentWrapper -instance Exception ConnectionClosed+throwHttp :: HttpExceptionContent -> IO a+throwHttp = throwIO . HttpExceptionContentWrapper -data HttpException = StatusCodeException Status ResponseHeaders CookieJar- | InvalidUrlException String String- | TooManyRedirects [Response L.ByteString] -- ^ List of encountered responses containing redirects in reverse chronological order; including last redirect, which triggered the exception and was not followed.- | UnparseableRedirect (Response L.ByteString) -- ^ Response containing unparseable redirect.- | TooManyRetries- | HttpParserException String- | HandshakeFailed+toHttpException :: Request -> HttpExceptionContentWrapper -> HttpException+toHttpException req (HttpExceptionContentWrapper e) = HttpExceptionRequest req e++-- | An exception which may be generated by this library+--+-- @since 0.5.0+data HttpException+ = HttpExceptionRequest Request HttpExceptionContent+ -- ^ Most exceptions are specific to a 'Request'. Inspect the+ -- 'HttpExceptionContent' value for details on what occurred.+ --+ -- @since 0.5.0+ | InvalidUrlException String String+ -- ^ A URL (first field) is invalid for a given reason+ -- (second argument).+ --+ -- @since 0.5.0+ deriving (Show, T.Typeable)+instance Exception HttpException++data HttpExceptionContent+ = StatusCodeException (Response ()) S.ByteString+ -- ^ Generated by the @parseUrlThrow@ function when the+ -- server returns a non-2XX response status code.+ --+ -- May include the beginning of the response body.+ --+ -- @since 0.5.0+ | TooManyRedirects [Response L.ByteString]+ -- ^ The server responded with too many redirects for a+ -- request.+ --+ -- Contains the list of encountered responses containing+ -- redirects in reverse chronological order; including last+ -- redirect, which triggered the exception and was not+ -- followed.+ --+ -- @since 0.5.0 | OverlongHeaders+ -- ^ Too many total bytes in the HTTP header were returned+ -- by the server.+ --+ -- @since 0.5.0+ | TooManyHeaderFields+ -- ^ Too many HTTP header fields were returned by the server.+ --+ -- @since 0.7.18 | ResponseTimeout- | FailedConnectionException String Int- -- ^ host/port+ -- ^ The server took too long to return a response. This can+ -- be altered via 'responseTimeout' or+ -- 'managerResponseTimeout'. --- -- Note that in old versions of http-client and- -- http-conduit, this exception would indicate a failed- -- attempt to create a connection. However, since (at least)- -- http-client 0.4, it indicates a timeout occurred while- -- trying to establish the connection. For more information- -- on this, see:+ -- @since 0.5.0+ | ConnectionTimeout+ -- ^ Attempting to connect to the server timed out. --- -- <https://github.com/snoyberg/http-client/commit/b86b1cdd91e56ee33150433dedb32954d2082621#commitcomment-10718689>- | FailedConnectionException2 String Int Bool SomeException -- ^ host\/port\/secure- | ExpectedBlankAfter100Continue+ -- @since 0.5.0+ | ConnectionFailure SomeException+ -- ^ An exception occurred when trying to connect to the+ -- server.+ --+ -- @since 0.5.0 | InvalidStatusLine S.ByteString+ -- ^ The status line returned by the server could not be parsed.+ --+ -- @since 0.5.0 | InvalidHeader S.ByteString- | InternalIOException IOException- | ProxyConnectException S.ByteString Int (Either S.ByteString HttpException) -- ^ host/port+ -- ^ The given response header line could not be parsed+ --+ -- @since 0.5.0+ | InvalidRequestHeader S.ByteString+ -- ^ The given request header is not compliant (e.g. has newlines)+ --+ -- @since 0.5.14+ | InternalException SomeException+ -- ^ An exception was raised by an underlying library when+ -- performing the request. Most often, this is caused by a+ -- failing socket action or a TLS exception.+ --+ -- @since 0.5.0+ | ProxyConnectException S.ByteString Int Status+ -- ^ A non-200 status code was returned when trying to+ -- connect to the proxy server on the given host and port.+ --+ -- @since 0.5.0 | NoResponseDataReceived- | TlsException SomeException+ -- ^ No response data was received from the server at all.+ -- This exception may deserve special handling within the+ -- library, since it may indicate that a pipelining has been+ -- used, and a connection thought to be open was in fact+ -- closed.+ --+ -- @since 0.5.0 | TlsNotSupported+ -- ^ Exception thrown when using a @Manager@ which does not+ -- have support for secure connections. Typically, you will+ -- want to use @tlsManagerSettings@ from @http-client-tls@+ -- to overcome this.+ --+ -- @since 0.5.0 | WrongRequestBodyStreamSize Word64 Word64 -- ^ The request body provided did not match the expected size. --@@ -126,36 +217,45 @@ -- -- @since 0.4.31 | ResponseBodyTooShort Word64 Word64- -- ^ Expected size/actual size.+ -- ^ The returned response body is too short. Provides the+ -- expected size and actual size. --- -- Since 1.9.4+ -- @since 0.5.0 | InvalidChunkHeaders- -- ^+ -- ^ A chunked response body had invalid headers. --- -- Since 1.9.4+ -- @since 0.5.0 | IncompleteHeaders+ -- ^ An incomplete set of response headers were returned.+ --+ -- @since 0.5.0 | InvalidDestinationHost S.ByteString+ -- ^ The host we tried to connect to is invalid (e.g., an+ -- empty string). | HttpZlibException ZlibException- -- ^+ -- ^ An exception was thrown when inflating a response body. --- -- Since 0.3+ -- @since 0.5.0 | InvalidProxyEnvironmentVariable Text Text- -- ^ Environment name and value- --- -- Since 0.4.7- | ResponseLengthAndChunkingBothUsed- -- ^ Detect a case where both the @content-length@ header- -- and @transfer-encoding: chunked@ are used. Since 0.4.8.+ -- ^ Values in the proxy environment variable were invalid.+ -- Provides the environment variable name and its value. --- -- Since 0.4.11 this exception isn't thrown anymore.- | TlsExceptionHostPort SomeException S.ByteString Int- -- ^ TLS exception, together with the host and port+ -- @since 0.5.0+ | ConnectionClosed+ -- ^ Attempted to use a 'Connection' which was already closed --- -- @since 0.4.24+ -- @since 0.5.0+ | InvalidProxySettings Text+ -- ^ Proxy settings are not valid (Windows specific currently)+ -- @since 0.5.7 deriving (Show, T.Typeable)-instance Exception HttpException +-- Purposely not providing this instance, since we don't want users to+-- accidentally try and catch these exceptions instead of HttpException+--+-- instance Exception HttpExceptionContent + -- This corresponds to the description of a cookie detailed in Section 5.3 \"Storage Model\" data Cookie = Cookie { cookie_name :: S.ByteString@@ -175,44 +275,93 @@ newtype CookieJar = CJ { expose :: [Cookie] } deriving (Read, Show, T.Typeable) --- This corresponds to step 11 of the algorithm described in Section 5.3 \"Storage Model\"-instance Eq Cookie where- (==) a b = name_matches && domain_matches && path_matches- where name_matches = cookie_name a == cookie_name b- domain_matches = CI.foldCase (cookie_domain a) == CI.foldCase (cookie_domain b)- path_matches = cookie_path a == cookie_path b+-- | Instead of '(==)'.+--+-- Since there was some confusion in the history of this library about how the 'Eq' instance+-- should work, it was removed for clarity, and replaced by 'equal' and 'equiv'. 'equal'+-- gives you equality of all fields of the 'Cookie' record.+--+-- @since 0.7.0+equalCookie :: Cookie -> Cookie -> Bool+equalCookie a b = and+ [ cookie_name a == cookie_name b+ , cookie_value a == cookie_value b+ , cookie_expiry_time a == cookie_expiry_time b+ , cookie_domain a == cookie_domain b+ , cookie_path a == cookie_path b+ , cookie_creation_time a == cookie_creation_time b+ , cookie_last_access_time a == cookie_last_access_time b+ , cookie_persistent a == cookie_persistent b+ , cookie_host_only a == cookie_host_only b+ , cookie_secure_only a == cookie_secure_only b+ , cookie_http_only a == cookie_http_only b+ ] -instance Ord Cookie where- compare c1 c2+-- | Equality of name, domain, path only. This corresponds to step 11 of the algorithm+-- described in Section 5.3 \"Storage Model\". See also: 'equal'.+--+-- @since 0.7.0+equivCookie :: Cookie -> Cookie -> Bool+equivCookie a b = name_matches && domain_matches && path_matches+ where name_matches = cookie_name a == cookie_name b+ domain_matches = CI.foldCase (cookie_domain a) == CI.foldCase (cookie_domain b)+ path_matches = cookie_path a == cookie_path b++-- | Instead of @instance Ord Cookie@. See 'equalCookie', 'equivCookie'.+--+-- @since 0.7.0+compareCookies :: Cookie -> Cookie -> Ordering+compareCookies c1 c2 | S.length (cookie_path c1) > S.length (cookie_path c2) = LT | S.length (cookie_path c1) < S.length (cookie_path c2) = GT | cookie_creation_time c1 > cookie_creation_time c2 = GT | otherwise = LT -instance Default CookieJar where- def = CJ []+-- | See 'equalCookie'.+--+-- @since 0.7.0+equalCookieJar :: CookieJar -> CookieJar -> Bool+equalCookieJar (CJ cj1) (CJ cj2) = and $ zipWith equalCookie cj1 cj2 -instance Eq CookieJar where- (==) cj1 cj2 = (DL.sort $ expose cj1) == (DL.sort $ expose cj2)+-- | See 'equalCookieJar', 'equalCookie'.+--+-- @since 0.7.0+equivCookieJar :: CookieJar -> CookieJar -> Bool+equivCookieJar cj1 cj2 = and $+ zipWith equivCookie (DL.sortBy compareCookies $ expose cj1) (DL.sortBy compareCookies $ expose cj2) --- | Since 1.9-instance Monoid CookieJar where- mempty = def- (CJ a) `mappend` (CJ b) = CJ (DL.nub $ DL.sortBy compare' $ a `mappend` b)- where compare' c1 c2 =+instance Semigroup CookieJar where+ (CJ a) <> (CJ b) = CJ (DL.nubBy equivCookie $ DL.sortBy mostRecentFirst $ a <> b)+ where mostRecentFirst c1 c2 = -- inverse so that recent cookies are kept by nub over older if cookie_creation_time c1 > cookie_creation_time c2 then LT else GT +-- | Since 1.9+instance Data.Monoid.Monoid CookieJar where+ mempty = CJ []+#if !(MIN_VERSION_base(4,11,0))+ mappend = (<>)+#endif+ -- | Define a HTTP proxy, consisting of a hostname and port number. data Proxy = Proxy- { proxyHost :: S.ByteString -- ^ The host name of the HTTP proxy.+ { proxyHost :: S.ByteString -- ^ The host name of the HTTP proxy in URI format. IPv6 addresses in square brackets. , proxyPort :: Int -- ^ The port number of the HTTP proxy. } deriving (Show, Read, Eq, Ord, T.Typeable) +-- | Define how to make secure connections using a proxy server.+data ProxySecureMode =+ ProxySecureWithConnect+ -- ^ Use the HTTP CONNECT verb to forward a secure connection through the proxy.+ | ProxySecureWithoutConnect+ -- ^ Send the request directly to the proxy with an https URL. This mode can be+ -- used to offload TLS handling to a trusted local proxy.+ deriving (Show, Read, Eq, Ord, T.Typeable)+ -- | When using one of the 'RequestBodyStream' \/ 'RequestBodyStreamChunked' -- constructors, you must ensure that the 'GivesPopper' can be called multiple -- times. Usually this is not a problem.@@ -241,9 +390,14 @@ fromString str = RequestBodyBS (fromString str) instance Monoid RequestBody where mempty = RequestBodyBS S.empty- mappend x0 y0 =+#if !(MIN_VERSION_base(4,11,0))+ mappend = (<>)+#endif++instance Semigroup RequestBody where+ x0 <> y0 = case (simplify x0, simplify y0) of- (Left (i, x), Left (j, y)) -> RequestBodyBuilder (i + j) (x `mappend` y)+ (Left (i, x), Left (j, y)) -> RequestBodyBuilder (i + j) (x <> y) (Left x, Right y) -> combine (builderToStream x) y (Right x, Left y) -> combine x (builderToStream y) (Right x, Right y) -> combine x y@@ -277,6 +431,7 @@ simplify (RequestBodyBuilder len b) = Left (len, b) simplify (RequestBodyStream i gp) = Right (Just i, gp) simplify (RequestBodyStreamChunked gp) = Right (Nothing, gp)+simplify (RequestBodyIO _mbody) = error "FIXME No support for Monoid on RequestBodyIO" builderToStream :: (Int64, Builder) -> (Maybe Int64, GivesPopper ()) builderToStream (len, builder) =@@ -345,6 +500,9 @@ -- ^ Requested host name, used for both the IP address to connect to and -- the @host@ request header. --+ -- This is in URI format, with raw IPv6 addresses enclosed in square brackets.+ -- Use 'strippedHostName' when making network connections.+ -- -- Since 0.1.0 , port :: Int -- ^ The port to connect to. Also used for generating the @host@ request header.@@ -412,28 +570,19 @@ -- no redirects. Default value: 10. -- -- Since 0.1.0- , checkStatus :: Status -> ResponseHeaders -> CookieJar -> Maybe SomeException- -- ^ Check the status code. Note that this will run after all redirects are- -- performed. Default: return a @StatusCodeException@ on non-2XX responses.- --- -- Since 0.1.0- , responseTimeout :: Maybe Int- -- ^ Number of microseconds to wait for a response. If- -- @Nothing@, will wait indefinitely. Default: use- -- 'managerResponseTimeout' (which by default is 30 seconds).+ , checkResponse :: Request -> Response BodyReader -> IO ()+ -- ^ Check the response immediately after receiving the status and headers.+ -- This can be useful for throwing exceptions on non-success status codes. --- -- Since 0.1.0- , getConnectionWrapper :: Maybe Int- -> HttpException- -> IO (ConnRelease, Connection, ManagedConn)- -> IO (Maybe Int, (ConnRelease, Connection, ManagedConn))- -- ^ Wraps the calls for getting new connections. This can be useful for- -- instituting some kind of timeouts. The first argument is the value of- -- @responseTimeout@. Second argument is the exception to be thrown on- -- failure.+ -- In previous versions of http-client, this went under the name+ -- @checkStatus@, but was renamed to avoid confusion about the new default+ -- behavior (doing nothing). --- -- Default: If @responseTimeout@ is @Nothing@, does nothing. Otherwise,- -- institutes timeout, and returns remaining time for @responseTimeout@.+ -- @since 0.5.0+ , responseTimeout :: ResponseTimeout+ -- ^ Number of microseconds to wait for a response (see 'ResponseTimeout'+ -- for more information). Default: use 'managerResponseTimeout' (which by+ -- default is 30 seconds). -- -- Since 0.1.0 , cookieJar :: Maybe CookieJar@@ -464,15 +613,78 @@ -- dealing with implicit global managers, such as in @Network.HTTP.Simple@ -- -- @since 0.4.28++ , shouldStripHeaderOnRedirect :: HeaderName -> Bool+ -- ^ Decide whether a header must be stripped from the request+ -- when following a redirect. Default: keep all headers intact.+ --+ -- @since 0.6.2++ , shouldStripHeaderOnRedirectIfOnDifferentHostOnly :: Bool+ -- ^ Decide whether a header must be stripped from the request+ -- when following a redirect, if host differs from previous request+ -- in redirect chain. Default: false (always strip regardless of host change)+ --+ -- @since 0.7.15++ , proxySecureMode :: ProxySecureMode+ -- ^ How to proxy an HTTPS request.+ --+ -- Default: Use HTTP CONNECT.+ --+ -- @since 0.7.2++ , redactHeaders :: Set.Set HeaderName+ -- ^ List of header values being redacted in case we show Request.+ --+ -- @since 0.7.13++ , earlyHintHeadersReceived :: [Header] -> IO ()+ -- ^ Called every time an HTTP 103 Early Hints header section is received from the server.+ --+ -- @since 0.7.16 } deriving T.Typeable -data ConnReuse = Reuse | DontReuse- deriving T.Typeable+-- | How to deal with timing out on retrieval of response headers.+--+-- @since 0.5.0+data ResponseTimeout+ = ResponseTimeoutMicro !Int+ -- ^ Wait the given number of microseconds for response headers to+ -- load, then throw an exception+ | ResponseTimeoutNone+ -- ^ Wait indefinitely+ | ResponseTimeoutDefault+ -- ^ Fall back to the manager setting ('managerResponseTimeout') or, in its+ -- absence, Wait 30 seconds and then throw an exception.+ deriving (Eq, Show) -type ConnRelease = ConnReuse -> IO ()+instance Show Request where+ show x = unlines+ [ "Request {"+ , " host = " ++ show (host x)+ , " port = " ++ show (port x)+ , " secure = " ++ show (secure x)+ , " requestHeaders = " ++ show (DL.map (redactSensitiveHeader $ redactHeaders x) (requestHeaders x))+ , " path = " ++ show (path x)+ , " queryString = " ++ show (queryString x)+ --, " requestBody = " ++ show (requestBody x)+ , " method = " ++ show (method x)+ , " proxy = " ++ show (proxy x)+ , " rawBody = " ++ show (rawBody x)+ , " redirectCount = " ++ show (redirectCount x)+ , " responseTimeout = " ++ show (responseTimeout x)+ , " requestVersion = " ++ show (requestVersion x)+ , " proxySecureMode = " ++ show (proxySecureMode x)+ , "}"+ ] -data ManagedConn = Fresh | Reused+redactSensitiveHeader :: Set.Set HeaderName -> Header -> Header+redactSensitiveHeader toRedact h@(name, _) =+ if name `Set.member` toRedact+ then (name, "<REDACTED>")+ else h -- | A simple representation of the HTTP response. --@@ -506,15 +718,30 @@ -- be impossible. -- -- Since 0.1.0+ , responseOriginalRequest :: Request+ -- ^ Holds original @Request@ related to this @Response@ (with an empty body).+ -- This field is intentionally not exported directly, but made available+ -- via @getOriginalRequest@ instead.+ --+ -- Since 0.7.8+ , responseEarlyHints :: ResponseHeaders+ -- ^ Early response headers sent by the server, as part of an HTTP+ -- 103 Early Hints section.+ --+ -- Since 0.7.16 }- deriving (Show, Eq, T.Typeable, Functor, Foldable, Traversable)+ deriving (Show, T.Typeable, Functor, Data.Foldable.Foldable, Data.Traversable.Traversable) +-- Purposely not providing this instance. It used to use 'equivCookieJar'+-- semantics before 0.7.0, but should, if anything, use 'equalCookieJar'+-- semantics.+--+-- instance Exception Eq+ newtype ResponseClose = ResponseClose { runResponseClose :: IO () } deriving T.Typeable instance Show ResponseClose where show _ = "ResponseClose"-instance Eq ResponseClose where- _ == _ = True -- | Settings for a @Manager@. Please use the 'defaultManagerSettings' function and then modify -- individual settings. For more information, see <http://www.yesodweb.com/book/settings-types>.@@ -529,7 +756,6 @@ -- ^ Create an insecure connection. -- -- Since 0.1.0- -- FIXME in the future, combine managerTlsConnection and managerTlsProxyConnection , managerTlsConnection :: IO (Maybe NS.HostAddress -> String -> Int -> IO Connection) -- ^ Create a TLS connection. Default behavior: throw an exception that TLS is not supported. --@@ -538,13 +764,13 @@ -- ^ Create a TLS proxy connection. Default behavior: throw an exception that TLS is not supported. -- -- Since 0.2.2- , managerResponseTimeout :: Maybe Int- -- ^ Default timeout (in microseconds) to be applied to requests which do- -- not provide a timeout value.+ , managerResponseTimeout :: ResponseTimeout+ -- ^ Default timeout to be applied to requests which do not provide a+ -- timeout value. -- -- Default is 30 seconds --- -- Since 0.1.0+ -- @since 0.5.0 , managerRetryableException :: SomeException -> Bool -- ^ Exceptions for which we should retry our request if we were reusing an -- already open connection. In the case of IOExceptions, for example, we@@ -552,19 +778,21 @@ -- new one. -- -- Since 0.1.0- , managerWrapIOException :: forall a. IO a -> IO a+ , managerWrapException :: forall a. Request -> IO a -> IO a -- ^ Action wrapped around all attempted @Request@s, usually used to wrap -- up exceptions in library-specific types. --- -- Default: wrap all @IOException@s in the @InternalIOException@ constructor.+ -- Default: wrap all @IOException@s in the @InternalException@ constructor. --- -- Since 0.1.0+ -- @since 0.5.0 , managerIdleConnectionCount :: Int -- ^ Total number of idle connection to keep open at a given time. -- -- This limit helps deal with the case where you are making a large number -- of connections to different hosts. Without this limit, you could run out- -- of file descriptors.+ -- of file descriptors. Additionally, it can be set to zero to prevent+ -- reuse of any connections. Doing this is useful when the server your application+ -- is talking to sits behind a load balancer. -- -- Default: 512 --@@ -572,9 +800,18 @@ , managerModifyRequest :: Request -> IO Request -- ^ Perform the given modification to a @Request@ before performing it. --+ -- This function may be called more than once during request processing.+ -- see https://github.com/snoyberg/http-client/issues/350+ -- -- Default: no modification -- -- Since 0.4.4+ , managerModifyResponse :: Response BodyReader -> IO (Response BodyReader)+ -- ^ Perform the given modification to a @Response@ after receiving it.+ --+ -- Default: no modification+ --+ -- @since 0.5.5 , managerProxyInsecure :: ProxyOverride -- ^ How HTTP proxy server settings should be discovered. --@@ -587,6 +824,18 @@ -- Default: respect the @proxy@ value on the @Request@ itself. -- -- Since 0.4.7+ , managerMaxHeaderLength :: Maybe MaxHeaderLength+ -- ^ Configure the maximum size, in bytes, of an HTTP header field.+ --+ -- Default: 4096+ --+ -- @since 0.7.17+ , managerMaxNumberHeaders :: Maybe MaxNumberHeaders+ -- ^ Configure the maximum number of HTTP header fields.+ --+ -- Default: 100+ --+ -- @since 0.7.18 } deriving T.Typeable @@ -604,27 +853,17 @@ -- -- Since 0.1.0 data Manager = Manager- { mConns :: I.IORef ConnsMap- -- ^ @Nothing@ indicates that the manager is closed.- , mConnsBaton :: MVar ()- -- ^ Used to indicate to the reaper thread that it has some work to do.- -- This must be filled every time a connection is returned to the manager.- -- While redundant with the @IORef@ above, this allows us to have the- -- reaper thread fully blocked instead of running every 5 seconds when- -- there are no connections to manage.- , mMaxConns :: Int- -- ^ This is a per-@ConnKey@ value.- , mResponseTimeout :: Maybe Int+ { mConns :: KeyedPool ConnKey Connection+ , mResponseTimeout :: ResponseTimeout -- ^ Copied from 'managerResponseTimeout'- , mRawConnection :: Maybe NS.HostAddress -> String -> Int -> IO Connection- , mTlsConnection :: Maybe NS.HostAddress -> String -> Int -> IO Connection- , mTlsProxyConnection :: S.ByteString -> (Connection -> IO ()) -> String -> Maybe NS.HostAddress -> String -> Int -> IO Connection , mRetryableException :: SomeException -> Bool- , mWrapIOException :: forall a. IO a -> IO a- , mIdleConnectionCount :: Int+ , mWrapException :: forall a. Request -> IO a -> IO a , mModifyRequest :: Request -> IO Request , mSetProxy :: Request -> Request+ , mModifyResponse :: Response BodyReader -> IO (Response BodyReader) -- ^ See 'managerProxy'+ , mMaxHeaderLength :: Maybe MaxHeaderLength+ , mMaxNumberHeaders :: Maybe MaxNumberHeaders } deriving T.Typeable @@ -650,7 +889,21 @@ -- | @ConnKey@ consists of a hostname, a port and a @Bool@ -- specifying whether to use SSL.-data ConnKey = ConnKey ConnHost Int S.ByteString Int Bool+data ConnKey+ = CKRaw (Maybe HostAddress) {-# UNPACK #-} !S.ByteString !Int+ | CKSecure (Maybe HostAddress) {-# UNPACK #-} !S.ByteString !Int+ | CKProxy+ {-# UNPACK #-} !S.ByteString+ !Int++ -- Proxy-Authorization request header+ (Maybe S.ByteString)++ -- ultimate host+ {-# UNPACK #-} !S.ByteString++ -- ultimate port+ !Int deriving (Eq, Show, Ord, T.Typeable) -- | Status of streaming a request body from a file.@@ -662,3 +915,19 @@ , thisChunkSize :: Int } deriving (Eq, Show, Ord, T.Typeable)++-- | The maximum header size in bytes.+--+-- @since 0.7.14+newtype MaxHeaderLength = MaxHeaderLength+ { unMaxHeaderLength :: Int+ }+ deriving (Eq, Show, Ord, Num, Enum, Bounded, T.Typeable)++-- | The maximum number of header fields.+--+-- @since 0.7.18+newtype MaxNumberHeaders = MaxNumberHeaders+ { unMaxNumberHeaders :: Int+ }+ deriving (Eq, Show, Ord, Num, Enum, Bounded, T.Typeable)
Network/HTTP/Client/Util.hs view
@@ -1,181 +1,15 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-} module Network.HTTP.Client.Util- ( hGetSome- , (<>)- , readDec- , hasNoBody- , fromStrict- , timeout+ ( readPositiveInt ) where -import Data.Monoid (Monoid, mappend)--import qualified Data.ByteString.Char8 as S8--#ifndef MIN_VERSION_bytestring-#define MIN_VERSION_bytestring(x,y,z) 1-#endif--#if MIN_VERSION_bytestring(0,10,0)-import Data.ByteString.Lazy (fromStrict)-#else-import qualified Data.ByteString.Lazy as L-import qualified Data.ByteString as S-#endif--import qualified Data.Text as T-import qualified Data.Text.Read-import System.Timeout (timeout)--import System.IO.Unsafe (unsafePerformIO)-import Control.Exception (mask_, Exception, throwTo, try, finally, SomeException, assert)-import Control.Monad (join, when, void)-import Control.Concurrent (myThreadId, threadDelay, forkIO)-import Data.IORef-import Data.Function (fix)-import Data.Typeable (Typeable)--#ifndef MIN_VERSION_base-#define MIN_VERSION_base(x,y,z) 1-#endif-#if MIN_VERSION_base(4,3,0)-import Data.ByteString (hGetSome)-#else-import GHC.IO.Handle.Types-import System.IO (hWaitForInput, hIsEOF)-import System.IO.Error (mkIOError, illegalOperationErrorType)---- | Like 'hGet', except that a shorter 'ByteString' may be returned--- if there are not enough bytes immediately available to satisfy the--- whole request. 'hGetSome' only blocks if there is no data--- available, and EOF has not yet been reached.-hGetSome :: Handle -> Int -> IO S.ByteString-hGetSome hh i- | i > 0 = let- loop = do- s <- S.hGetNonBlocking hh i- if not (S.null s)- then return s- else do eof <- hIsEOF hh- if eof then return s- else hWaitForInput hh (-1) >> loop- -- for this to work correctly, the- -- Handle should be in binary mode- -- (see GHC ticket #3808)- in loop- | i == 0 = return S.empty- | otherwise = illegalBufferSize hh "hGetSome" i--illegalBufferSize :: Handle -> String -> Int -> IO a-illegalBufferSize handle fn sz =- ioError (mkIOError illegalOperationErrorType msg (Just handle) Nothing)- --TODO: System.IO uses InvalidArgument here, but it's not exported :-(- where- msg = fn ++ ": illegal ByteString size " ++ showsPrec 9 sz []-#endif--infixr 5 <>-(<>) :: Monoid m => m -> m -> m-(<>) = mappend--readDec :: Integral i => String -> Maybe i-readDec s =- case Data.Text.Read.decimal $ T.pack s of- Right (i, t)- | T.null t -> Just i- _ -> Nothing--hasNoBody :: S8.ByteString -- ^ request method- -> Int -- ^ status code- -> Bool-hasNoBody "HEAD" _ = True-hasNoBody _ 204 = True-hasNoBody _ 304 = True-hasNoBody _ i = 100 <= i && i < 200--#if !MIN_VERSION_bytestring(0,10,0)-{-# INLINE fromStrict #-}-fromStrict :: S.ByteString -> L.ByteString-fromStrict x = L.fromChunks [x]-#endif---- Disabling the custom timeout code for now. See: https://github.com/snoyberg/http-client/issues/116-{--data TimeoutHandler = TimeoutHandler {-# UNPACK #-} !TimeSpec (IO ())-newtype TimeoutManager = TimeoutManager (IORef ([TimeoutHandler], Bool))--newTimeoutManager :: IO TimeoutManager-newTimeoutManager = fmap TimeoutManager $ newIORef ([], False)--timeoutManager :: TimeoutManager-timeoutManager = unsafePerformIO newTimeoutManager-{-# NOINLINE timeoutManager #-}--spawnWorker :: TimeoutManager -> IO ()-spawnWorker (TimeoutManager ref) = void $ forkIO $ fix $ \loop -> do- threadDelay 500000- join $ atomicModifyIORef ref $ \(hs, isCleaning) -> assert (not isCleaning) $- if null hs- then (([], False), return ())- else (([], True), ) $ do- now <- getTime Monotonic- front <- go now id hs- atomicModifyIORef ref $ \(hs', isCleaning') ->- assert isCleaning' $ ((front hs', False), ())- loop- where- go now =- go'- where- go' front [] = return front- go' front (h@(TimeoutHandler time action):hs)- | time < now = do- _ :: Either SomeException () <- try action- go' front hs- | otherwise = go' (front . (h:)) hs--addHandler :: TimeoutManager -> TimeoutHandler -> IO ()-addHandler man@(TimeoutManager ref) h = mask_ $ join $ atomicModifyIORef ref- $ \(hs, isCleaning) ->- let hs' = h : hs- action- | isCleaning || not (null hs) = return ()- | otherwise = spawnWorker man- in ((hs', isCleaning), action)---- | Has same semantics as @System.Timeout.timeout@, but implemented in such a--- way to avoid high-concurrency contention issues. See:------ https://github.com/snoyberg/http-client/issues/98-timeout :: Int -> IO a -> IO (Maybe a)-timeout delayU inner = do- TimeSpec nowS nowN <- getTime Monotonic- let (delayS, delayU') = delayU `quotRem` 1000000- delayN = delayU' * 1000- stopN' = nowN + delayN- stopS' = nowS + delayS- (stopN, stopS)- | stopN' > 1000000000 = (stopN' - 1000000000, stopS' + 1)- | otherwise = (stopN', stopS')- toStop = TimeSpec stopS stopN- toThrow <- newIORef True- tid <- myThreadId- let handler = TimeoutHandler toStop $ do- toThrow' <- readIORef toThrow- when toThrow' $ throwTo tid TimeoutTriggered- eres <- try $ do- addHandler timeoutManager handler- inner `finally` writeIORef toThrow False- return $ case eres of- Left TimeoutTriggered -> Nothing- Right x -> Just x+import Text.Read (readMaybe)+import Control.Monad (guard) -data TimeoutTriggered = TimeoutTriggered- deriving (Show, Typeable)-instance Exception TimeoutTriggered--}+-- | Read a positive 'Int', accounting for overflow+readPositiveInt :: String -> Maybe Int+readPositiveInt s = do+ i <- readMaybe s+ guard $ i >= 0+ Just i
+ Network/HTTP/Proxy.hs view
@@ -0,0 +1,380 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-}++{-+Copyright (c) 2002, Warrick Gray+Copyright (c) 2002-2005, Ian Lynagh+Copyright (c) 2003-2006, Bjorn Bringert+Copyright (c) 2004, Andre Furtado+Copyright (c) 2004-2005, Dominic Steinitz+Copyright (c) 2007, Robin Bate Boerop+Copyright (c) 2008-2010, Sigbjorn Finne+Copyright (c) 2009, Eric Kow+Copyright (c) 2010, Antoine Latter+Copyright (c) 2004, 2010-2011, Ganesh Sittampalam+Copyright (c) 2011, Duncan Coutts+Copyright (c) 2011, Matthew Gruen+Copyright (c) 2011, Jeremy Yallop+Copyright (c) 2011, Eric Hesselink+Copyright (c) 2011, Yi Huang+Copyright (c) 2011, Tom Lokhorst+Copyright (c) 2017, Vassil Keremidchiev++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * The names of contributors may not be used to endorse or promote+ products derived from this software without specific prior+ written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+-}++module Network.HTTP.Proxy( ProxyProtocol(..), EnvHelper(..),+ systemProxyHelper, envHelper,+ httpProtocol,+ ProxySettings ) where++import qualified Control.Applicative as A+import Control.Arrow (first)+import Control.Monad (guard)+import qualified Data.ByteString.Char8 as S8+import Data.Char (toLower)+import qualified Data.Map as Map+import qualified Data.Text as T+import Data.Text.Read (decimal)+import Network.HTTP.Client.Request (applyBasicProxyAuth,+ extractBasicAuthInfo)+import Network.HTTP.Client.Types (HttpExceptionContent (..),+ Proxy (..), Request (..),+ throwHttp)+import qualified Network.URI as U+import System.Environment (getEnvironment)++#if defined(mingw32_HOST_OS)+import Control.Exception (IOException, bracket, catch, try)+import Control.Monad (join, liftM, mplus, when)+import Data.List (isInfixOf, isPrefixOf)+import Foreign (Storable (peek, sizeOf), alloca,+ castPtr, toBool)+import Network.URI (parseAbsoluteURI)+import Safe (readDef)+import System.IO+import System.Win32.Registry (hKEY_CURRENT_USER, rEG_DWORD,+ regCloseKey, regOpenKey,+ regQueryValue, regQueryValueEx)+import System.Win32.Types (DWORD, HKEY)+#endif++type EnvName = T.Text+type HostAddress = S8.ByteString+type UserName = S8.ByteString+type Password = S8.ByteString++-- There are other proxy protocols like SOCKS, FTP, etc.+data ProxyProtocol = HTTPProxy | HTTPSProxy++instance Show ProxyProtocol where+ show HTTPProxy = "http"+ show HTTPSProxy = "https"++data ProxySettings = ProxySettings { _proxyHost :: Proxy,+ _proxyAuth :: Maybe (UserName, Password) }+ deriving Show++httpProtocol :: Bool -> ProxyProtocol+httpProtocol True = HTTPSProxy+httpProtocol False = HTTPProxy++data EnvHelper = EHFromRequest+ | EHNoProxy+ | EHUseProxy Proxy++headJust :: [Maybe a] -> Maybe a+headJust [] = Nothing+headJust (Nothing:xs) = headJust xs+headJust ((y@(Just _)):_) = y++systemProxyHelper :: Maybe T.Text -> ProxyProtocol -> EnvHelper -> IO (Request -> Request)+systemProxyHelper envOveride prot eh = do+ let envName' Nothing = envName prot+ envName' (Just name) = name++ modifier <- envHelper (envName' envOveride)++-- Under Windows try first env. variables override then Windows proxy settings+#if defined(mingw32_HOST_OS)+ modifier' <- systemProxy prot+ let modifiers = [modifier, modifier']+#else+ let modifiers = [modifier]+#endif++ let chooseMod :: Request -> Maybe ProxySettings+ chooseMod req = headJust . map (\m -> m . host $ req) $ modifiers++ noEnvProxy = case eh of+ EHFromRequest -> id+ EHNoProxy -> \req -> req { proxy = Nothing }+ EHUseProxy p -> \req -> req { proxy = Just p }++ let result req = toRequest . chooseMod $ req where+ toRequest Nothing = noEnvProxy req+ toRequest (Just (ProxySettings p muserpass)) = maybe id (uncurry applyBasicProxyAuth) muserpass+ req { proxy = Just p }+ return result+++#if defined(mingw32_HOST_OS)+windowsProxyString :: ProxyProtocol -> IO (Maybe (String, String))+windowsProxyString proto = do+ mProxy <- registryProxyString+ return $ do+ (proxies, exceptions) <- mProxy+ protoProxy <- parseWindowsProxy proto proxies+ return (protoProxy, exceptions)++registryProxyLoc :: (HKEY,String)+registryProxyLoc = (hive, path)+ where+ -- some sources say proxy settings should be at+ -- HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows+ -- \CurrentVersion\Internet Settings\ProxyServer+ -- but if the user sets them with IE connection panel they seem to+ -- end up in the following place:+ hive = hKEY_CURRENT_USER+ path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"++-- read proxy settings from the windows registry; this is just a best+-- effort and may not work on all setups.+registryProxyString :: IO (Maybe (String, String))+registryProxyString = catch+ (bracket (uncurry regOpenKey registryProxyLoc) regCloseKey $ \hkey -> do+ enable <- toBool . maybe 0 id A.<$> regQueryValueDWORD hkey "ProxyEnable"+ if enable+ then do+#if MIN_VERSION_Win32(2, 6, 0) && !MIN_VERSION_Win32(2, 8, 0)+ server <- regQueryValue hkey "ProxyServer"+ exceptions <- try $ regQueryValue hkey "ProxyOverride" :: IO (Either IOException String)+#else+ server <- regQueryValue hkey (Just "ProxyServer")+ exceptions <- try $ regQueryValue hkey (Just "ProxyOverride") :: IO (Either IOException String)+#endif+ return $ Just (server, either (const "") id exceptions)+ else return Nothing)+ hideError where+ hideError :: IOException -> IO (Maybe (String, String))+ hideError _ = return Nothing++-- the proxy string is in the format "http=x.x.x.x:yyyy;https=...;ftp=...;socks=..."+-- even though the following article indicates otherwise+-- https://support.microsoft.com/en-us/kb/819961+--+-- to be sure, parse strings where each entry in the ';'-separated list above is+-- either in the format "protocol=..." or "protocol://..."+parseWindowsProxy :: ProxyProtocol -> String -> Maybe String+parseWindowsProxy proto s =+ case proxies of+ x:_ -> Just x+ _ -> Nothing+ where+ parts = split ';' s+ pr x = case break (== '=') x of+ (p, []) -> p -- might be in format http://+ (p, u) -> p ++ "://" ++ drop 1 u++ protoPrefix = (show proto) ++ "://"+ proxies = filter (isPrefixOf protoPrefix) . map pr $ parts++ split :: Eq a => a -> [a] -> [[a]]+ split _ [] = []+ split a xs = case break (a ==) xs of+ (ys, []) -> [ys]+ (ys, _:zs) -> ys:split a zs++-- Extract proxy settings from Windows registry. This is a standard way in Windows OS.+systemProxy :: ProxyProtocol -> IO (HostAddress -> Maybe ProxySettings)+systemProxy proto = do+ let isURLlocal "127.0.0.1" = True+ isURLlocal "localhost" = True+ isURLlocal _ = False++ hasLocal exceptions = "<local>" `isInfixOf` exceptions++ settings <- fetchProxy proto+ return $ \url -> do+ (proxy, exceptions) <- settings++ -- Skip proxy for local hosts if it's enabled in IE settings+ -- TODO Implement skipping for address patterns, like (*.google.com)+ if (isURLlocal url && hasLocal exceptions) || (url `S8.isInfixOf` (S8.pack exceptions)) then Nothing+ else Just proxy++-- | @fetchProxy flg@ gets the local proxy settings and parse the string+-- into a @Proxy@ value.+-- Proxy settings are sourced from IE/WinInet's proxy+-- setting in the Registry.+fetchProxy :: ProxyProtocol -> IO (Maybe (ProxySettings, String))+fetchProxy proto = do+ mstr <- windowsProxyString proto+ case mstr of+ Nothing -> return Nothing+ Just (proxy, except) -> case parseProxy proto proxy of+ Just p -> return $ Just (p, except)+ Nothing ->+ throwHttp . InvalidProxySettings . T.pack . unlines $+ [ "Invalid http proxy uri: " ++ show proxy+ , "proxy uri must be http with a hostname"+ , "ignoring http proxy, trying a direct connection"+ ]++-- | @parseProxy str@ translates a proxy server string into a @ProxySettings@ value;+-- returns @Nothing@ if not well-formed.+parseProxy :: ProxyProtocol -> String -> Maybe ProxySettings+parseProxy proto str = join+ . fmap (uri2proxy proto)+ $ parseHttpURI str+ `mplus` parseHttpURI (protoPrefix ++ str)+ where+ protoPrefix = (show proto) ++ "://"+ parseHttpURI str' =+ case parseAbsoluteURI str' of+ Just uri@U.URI{U.uriAuthority = Just{}} -> Just (fixUserInfo uri)+ _ -> Nothing++ -- Note: we need to be able to parse non-URIs like @\"wwwcache.example.com:80\"@+ -- which lack the @\"http://\"@ URI scheme. The problem is that+ -- @\"wwwcache.example.com:80\"@ is in fact a valid URI but with scheme+ -- @\"wwwcache.example.com:\"@, no authority part and a path of @\"80\"@.+ --+ -- So our strategy is to try parsing as normal uri first and if it lacks the+ -- 'uriAuthority' then we try parsing again with a @\"http://\"@ prefix.+ --++-- | @dropWhileTail p ls@ chops off trailing elements from @ls@+-- until @p@ returns @False@.+dropWhileTail :: (a -> Bool) -> [a] -> [a]+dropWhileTail f ls =+ case foldr chop Nothing ls of { Just xs -> xs; Nothing -> [] }+ where+ chop x (Just xs) = Just (x:xs)+ chop x _+ | f x = Nothing+ | otherwise = Just [x]++-- | @chopAtDelim elt ls@ breaks up @ls@ into two at first occurrence+-- of @elt@; @elt@ is elided too. If @elt@ does not occur, the second+-- list is empty and the first is equal to @ls@.+chopAtDelim :: Eq a => a -> [a] -> ([a],[a])+chopAtDelim elt xs =+ case break (==elt) xs of+ (_,[]) -> (xs,[])+ (as,_:bs) -> (as,bs)++-- | tidy up user portion, don't want the trailing "\@".+fixUserInfo :: U.URI -> U.URI+fixUserInfo uri = uri{ U.uriAuthority = f `fmap` U.uriAuthority uri }+ where+ f a@U.URIAuth{U.uriUserInfo=s} = a{U.uriUserInfo=dropWhileTail (=='@') s}++defaultHTTPport :: ProxyProtocol -> Int+defaultHTTPport HTTPProxy = 80+defaultHTTPport HTTPSProxy = 443++uri2proxy :: ProxyProtocol -> U.URI -> Maybe ProxySettings+uri2proxy proto uri@U.URI{ U.uriAuthority = Just (U.URIAuth auth' hst prt) } =+ if (show proto ++ ":") == U.uriScheme uri then+ Just (ProxySettings (Proxy (S8.pack hst) (port prt)) auth) else Nothing+ where+ port (':':xs) = readDef (defaultHTTPport proto) xs+ port _ = (defaultHTTPport proto)++ auth =+ case auth' of+ [] -> Nothing+ as -> Just ((S8.pack . U.unEscapeString $ usr), (S8.pack . U.unEscapeString $ pwd))+ where+ (usr,pwd) = chopAtDelim ':' as++uri2proxy _ _ = Nothing++regQueryValueDWORD :: HKEY -> String -> IO (Maybe DWORD)+regQueryValueDWORD hkey name = alloca $ \ptr -> do+ key <- regQueryValueEx hkey name (castPtr ptr) (sizeOf (undefined :: DWORD))+ if key == rEG_DWORD then+ Just A.<$> peek ptr+ else return Nothing++-- defined(mingw32_HOST_OS)+#endif++envName :: ProxyProtocol -> EnvName+envName proto = T.pack $ show proto ++ "_proxy"++-- Extract proxy settings from environment variables. This is a standard way in Linux.+envHelper :: EnvName -> IO (HostAddress -> Maybe ProxySettings)+envHelper name = do+ env <- getEnvironment+ let lenv = Map.fromList $ map (first $ T.toLower . T.pack) env+ lookupEnvVar n = lookup (T.unpack n) env A.<|> Map.lookup n lenv+ noProxyDomains = domainSuffixes (lookupEnvVar "no_proxy")++ case lookupEnvVar name of+ Nothing -> return . const $ Nothing+ Just "" -> return . const $ Nothing+ Just str -> do+ let invalid = throwHttp $ InvalidProxyEnvironmentVariable name (T.pack str)+ (p, muserpass) <- maybe invalid return $ do+ let allowedScheme x = x == "http:"+ uri <- case U.parseURI str of+ Just u | allowedScheme (U.uriScheme u) -> return u+ _ -> U.parseURI $ "http://" ++ str++ guard $ allowedScheme $ U.uriScheme uri+ guard $ null (U.uriPath uri) || U.uriPath uri == "/"+ guard $ null $ U.uriQuery uri+ guard $ null $ U.uriFragment uri++ auth <- U.uriAuthority uri+ port' <-+ case U.uriPort auth of+ "" -> Just 80+ ':':rest ->+ case decimal $ T.pack rest of+ Right (p, "") -> Just p+ _ -> Nothing+ _ -> Nothing++ Just (Proxy (S8.pack $ U.uriRegName auth) port', extractBasicAuthInfo uri)+ return $ \hostRequest ->+ if hostRequest `hasDomainSuffixIn` noProxyDomains+ then Nothing+ else Just $ ProxySettings p muserpass+ where prefixed s | S8.head s == '.' = s+ | otherwise = S8.cons '.' s+ domainSuffixes Nothing = []+ domainSuffixes (Just "") = []+ domainSuffixes (Just no_proxy) = [prefixed $ S8.dropWhile (== ' ') suffix | suffix <- S8.split ',' (S8.pack (map toLower no_proxy)), not (S8.null suffix)]+ hasDomainSuffixIn host' = any (`S8.isSuffixOf` prefixed (S8.map toLower host'))
README.md view
@@ -2,7 +2,7 @@ =========== Full tutorial docs are available at:-https://github.com/commercialhaskell/jump/blob/master/doc/http-client.md+https://github.com/snoyberg/http-client/blob/master/TUTORIAL.md An HTTP client engine, intended as a base layer for more user-friendly packages.
http-client.cabal view
@@ -1,6 +1,6 @@ name: http-client-version: 0.4.31.2-synopsis: An HTTP client engine, intended as a base layer for more user-friendly packages.+version: 0.7.19+synopsis: An HTTP client engine description: Hackage documentation generation is not reliable. For up to date documentation, please see: <http://www.stackage.org/package/http-client>. homepage: https://github.com/snoyberg/http-client license: MIT@@ -31,24 +31,25 @@ Network.HTTP.Client.Response Network.HTTP.Client.Types Network.HTTP.Client.Util+ Network.HTTP.Proxy Network.PublicSuffixList.Lookup Network.PublicSuffixList.Types Network.PublicSuffixList.Serialize Network.PublicSuffixList.DataStructure- build-depends: base >= 4.5 && < 5- , bytestring >= 0.9+ Data.KeyedPool+ build-depends: base >= 4.10 && < 5+ , bytestring >= 0.10 , text >= 0.11 , http-types >= 0.8 , blaze-builder >= 0.3- , data-default-class , time >= 1.2- , network >= 2.3- , streaming-commons >= 0.1.0.2 && < 0.2- , containers+ , network >= 2.4+ , streaming-commons >= 0.1.0.2 && < 0.3+ , containers >= 0.5 , transformers- , deepseq >= 1.3 && <1.5+ , deepseq >= 1.3 && <1.6 , case-insensitive >= 1.0- , base64-bytestring >= 1.0 && <1.1+ , base64-bytestring >= 1.0 , cookie , exceptions >= 0.4 , array@@ -56,10 +57,29 @@ , filepath , mime-types , ghc-prim+ , stm >= 2.3+ , iproute >= 1.7.5+ , async >= 2.0 if flag(network-uri) build-depends: network >= 2.6, network-uri >= 2.6 else build-depends: network < 2.6++ if !impl(ghc>=8.0)+ build-depends: semigroups >= 0.16.1++ -- See build failure at https://travis-ci.org/snoyberg/http-client/jobs/359573631+ if impl(ghc < 7.10)+ -- Disable building with GHC before 8.0.2.+ -- Due to a cabal bug, do not use buildable: False,+ -- but instead give it an impossible constraint.+ -- See: https://github.com/haskell-infra/hackage-trustees/issues/165+ build-depends: unsupported-ghc-version > 1 && < 1+++ if os(mingw32)+ build-depends: Win32, safe+ default-language: Haskell2010 test-suite spec@@ -68,6 +88,7 @@ hs-source-dirs: test default-language: Haskell2010 other-modules: Network.HTTP.ClientSpec+ build-tool-depends: hspec-discover:hspec-discover build-depends: base , http-client , hspec@@ -82,7 +103,6 @@ , transformers , deepseq , case-insensitive- , base64-bytestring , zlib , async , streaming-commons >= 0.1.1@@ -93,6 +113,9 @@ type: exitcode-stdio-1.0 hs-source-dirs: test-nonet default-language: Haskell2010+ ghc-options: -threaded+ if os(windows)+ cpp-options: -DWINDOWS other-modules: Network.HTTP.ClientSpec Network.HTTP.Client.ResponseSpec Network.HTTP.Client.BodySpec@@ -100,11 +123,14 @@ Network.HTTP.Client.RequestSpec Network.HTTP.Client.RequestBodySpec Network.HTTP.Client.CookieSpec+ Network.HTTP.Client.ConnectionSpec+ build-tool-depends: hspec-discover:hspec-discover build-depends: base , http-client , hspec , monad-control , bytestring+ , cookie , text , http-types , blaze-builder@@ -115,7 +141,6 @@ , transformers , deepseq , case-insensitive- , base64-bytestring , zlib , async , streaming-commons >= 0.1.1
publicsuffixlist/Network/PublicSuffixList/Serialize.hs view
@@ -48,13 +48,13 @@ putTree = putMap . children putMap :: Map T.Text (Tree T.Text) -> Builder-putMap m = foldMap putPair (Map.toList m) `mappend` fromWord8 0+putMap m = Data.Foldable.foldMap putPair (Map.toList m) `mappend` fromWord8 0 putPair :: (T.Text, Tree T.Text) -> Builder putPair (x, y) = putText x `mappend` putTree y putText :: T.Text -> Builder-putText t = fromText t `mappend` fromWord8 0+putText t = fromText t `Data.Monoid.mappend` fromWord8 0 putDataStructure :: DataStructure -> BS.ByteString putDataStructure (x, y) = toByteString $ putTree x `mappend` putTree y
test-nonet/Network/HTTP/Client/BodySpec.hs view
@@ -12,59 +12,101 @@ main = hspec spec brComplete :: BodyReader -> IO Bool-brComplete brRead = do- xs <- brRead+brComplete brRead' = do+ xs <- brRead' return (xs == "") spec :: Spec spec = describe "BodySpec" $ do it "chunked, single" $ do (conn, _, input) <- dummyConnection- [ "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"+ [ "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\nnot consumed" ]- reader <- makeChunkedReader False conn+ reader <- makeChunkedReader Nothing (return ()) False conn body <- brConsume reader S.concat body `shouldBe` "hello world" input' <- input S.concat input' `shouldBe` "not consumed" brComplete reader `shouldReturn` True + it "chunked, single, with trailers" $ do+ (conn, _, input) <- dummyConnection+ [ "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: ignored\r\nbut: consumed\r\n\r\nnot consumed"+ ]+ reader <- makeChunkedReader Nothing (return ()) False conn+ body <- brConsume reader+ S.concat body `shouldBe` "hello world"+ input' <- input+ S.concat input' `shouldBe` "not consumed"+ brComplete reader `shouldReturn` True+ it "chunked, pieces" $ do (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack- "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"- reader <- makeChunkedReader False conn+ "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\nnot consumed"+ reader <- makeChunkedReader Nothing (return ()) False conn body <- brConsume reader S.concat body `shouldBe` "hello world" input' <- input S.concat input' `shouldBe` "not consumed" brComplete reader `shouldReturn` True + it "chunked, pieces, with trailers" $ do+ (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack+ "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: ignored\r\nbut: consumed\r\n\r\nnot consumed"+ reader <- makeChunkedReader Nothing (return ()) False conn+ body <- brConsume reader+ S.concat body `shouldBe` "hello world"+ input' <- input+ S.concat input' `shouldBe` "not consumed"+ brComplete reader `shouldReturn` True+ it "chunked, raw" $ do (conn, _, input) <- dummyConnection- [ "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"+ [ "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\nnot consumed" ]- reader <- makeChunkedReader True conn+ reader <- makeChunkedReader Nothing (return ()) True conn body <- brConsume reader- S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n"+ S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n" input' <- input S.concat input' `shouldBe` "not consumed" brComplete reader `shouldReturn` True + it "chunked, raw, with trailers" $ do+ (conn, _, input) <- dummyConnection+ [ "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: returned\r\nin-raw: body\r\n\r\nnot consumed"+ ]+ reader <- makeChunkedReader Nothing (return ()) True conn+ body <- brConsume reader+ S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: returned\r\nin-raw: body\r\n\r\n"+ input' <- input+ S.concat input' `shouldBe` "not consumed"+ brComplete reader `shouldReturn` True+ it "chunked, pieces, raw" $ do (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack- "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"- reader <- makeChunkedReader True conn+ "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\nnot consumed"+ reader <- makeChunkedReader Nothing (return ()) True conn body <- brConsume reader- S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n"+ S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n" input' <- input S.concat input' `shouldBe` "not consumed" brComplete reader `shouldReturn` True + it "chunked, pieces, raw, with trailers" $ do+ (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack+ "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: returned\r\nin-raw: body\r\n\r\nnot consumed"+ reader <- makeChunkedReader Nothing (return ()) True conn+ body <- brConsume reader+ S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\ntrailers-are: returned\r\nin-raw: body\r\n\r\n"+ input' <- input+ S.concat input' `shouldBe` "not consumed"+ brComplete reader `shouldReturn` True+ it "length, single" $ do (conn, _, input) <- dummyConnection [ "hello world done" ]- reader <- makeLengthReader 11 conn+ reader <- makeLengthReader (return ()) 11 conn body <- brConsume reader S.concat body `shouldBe` "hello world" input' <- input@@ -74,7 +116,7 @@ it "length, pieces" $ do (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack "hello world done"- reader <- makeLengthReader 11 conn+ reader <- makeLengthReader (return ()) 11 conn body <- brConsume reader S.concat body `shouldBe` "hello world" input' <- input@@ -85,7 +127,7 @@ let orig = L.fromChunks $ replicate 5000 "Hello world!" origZ = compress orig (conn, _, input) <- dummyConnection $ L.toChunks origZ ++ ["ignored"]- reader' <- makeLengthReader (fromIntegral $ L.length origZ) conn+ reader' <- makeLengthReader (return ()) (fromIntegral $ L.length origZ) conn reader <- makeGzipReader reader' body <- brConsume reader L.fromChunks body `shouldBe` orig
+ test-nonet/Network/HTTP/Client/ConnectionSpec.hs view
@@ -0,0 +1,23 @@+module Network.HTTP.Client.ConnectionSpec where++import Network.HTTP.Client (strippedHostName)+import Test.Hspec++spec :: Spec+spec = do+ describe "strippedHostName" $ do+ it "passes along a normal domain name" $ do+ strippedHostName "example.com" `shouldBe` "example.com"+ it "passes along an IPv4 address" $ do+ strippedHostName "127.0.0.1" `shouldBe` "127.0.0.1"+ it "strips brackets of an IPv4 address" $ do+ strippedHostName "[::1]" `shouldBe` "::1"+ strippedHostName "[::127.0.0.1]" `shouldBe` "::127.0.0.1"++ describe "pathological cases" $ do+ -- just need to handle these gracefully, it's unclear+ -- what the result should be+ it "doesn't touch future ip address formats" $ do+ strippedHostName "[v2.huh]" `shouldBe` "[v2.huh]"+ it "doesn't strip trailing stuff" $ do+ strippedHostName "[::1]foo" `shouldBe` "[::1]foo"
test-nonet/Network/HTTP/Client/CookieSpec.hs view
@@ -1,10 +1,13 @@ {-# LANGUAGE OverloadedStrings #-} module Network.HTTP.Client.CookieSpec where +import Control.Monad (when)+import Data.Monoid import Data.Time.Clock import Network.HTTP.Client.Internal-import Network.HTTP.Types import Test.Hspec+import qualified Data.Time as DT+import qualified Web.Cookie as WC main :: IO () main = hspec spec@@ -15,7 +18,7 @@ now <- getCurrentTime let cookie1 = Cookie "test" "value" now "doMain.Org" "/" now now False False False False cookie2 = Cookie "test" "value" now "DOMAIn.ORg" "/" now now False False False False- cookie1 `shouldBe` cookie2+ cookie1 `shouldSatisfy` (equivCookie cookie2) it "domainMatches - case insensitive" $ do domainMatches "www.org" "www.org" `shouldBe` True@@ -25,3 +28,49 @@ it "domainMatches - case insensitive, partial" $ do domainMatches "www.org" "xxx.www.org" `shouldBe` False domainMatches "xxx.www.org" "WWW.ORG" `shouldBe` True++ describe "equalCookie vs. equivCookie" $ do+ let make :: IO Cookie+ make = do+ now <- DT.getCurrentTime+ req <- parseRequest "http://www.example.com/path"+ let Just cky = generateCookie (WC.parseSetCookie raw) req now True+ raw = "somename=somevalue.v=1.k=1.d=1590419679.t=u.l=s.u=8b2734ae-9dd1-11ea-bd7f-3bcf5b8d5d2a.r=795e71b5; " <>+ "Path=/access; Domain=example.com; HttpOnly; Secure"+ return cky++ modifications :: [(String, Cookie -> Cookie, Bool)]+ modifications+ = [ ("cookie_name", \cky -> cky { cookie_name = "othername" }, True)+ , ("cookie_value", \cky -> cky { cookie_value = "othervalue" }, False)+ , ("cookie_expiry_time", \cky -> cky { cookie_expiry_time = DT.addUTCTime 60 $ cookie_expiry_time cky }, False)+ , ("cookie_domain", \cky -> cky { cookie_domain = cookie_domain cky <> ".com" }, True)+ , ("cookie_path", \cky -> cky { cookie_path = cookie_path cky <> "/sub" }, True)+ , ("cookie_creation_time", \cky -> cky { cookie_creation_time = DT.addUTCTime 60 $ cookie_creation_time cky }, False)+ , ("cookie_last_access_time", \cky -> cky { cookie_last_access_time = DT.addUTCTime 60 $ cookie_last_access_time cky }, False)+ , ("cookie_persistent", \cky -> cky { cookie_persistent = not $ cookie_persistent cky }, False)+ , ("cookie_host_only", \cky -> cky { cookie_host_only = not $ cookie_host_only cky }, False)+ , ("cookie_secure_only", \cky -> cky { cookie_secure_only = not $ cookie_secure_only cky }, False)+ , ("cookie_http_only", \cky -> cky { cookie_http_only = not $ cookie_http_only cky }, False)+ ]++ check :: (String, Cookie -> Cookie, Bool) -> Spec+ check (msg, f, countsForEquiv) = it msg $ do+ cky <- make+ cky `equalCookie` f cky `shouldBe` False+ when countsForEquiv $ cky `equivCookie` f cky `shouldBe` False++ check `mapM_` modifications++ it "isPotentiallyTrustworthyOrigin" $ do+ isPotentiallyTrustworthyOrigin True "" `shouldBe` True+ let untrusty = ["example", "example.", "example.com", "foolocalhost", "1.1.1.1", "::1", "[::2]"]+ trusty =+ [ "127.0.0.1", "127.0.0.2", "127.127.127.127"+ , "[::1]", "[0:0:0:0:0:0:0:1]"+ , "localhost", "localhost."+ , "a.b.c.localhost", "a.b.c.localhost."+ ]+ or (map (isPotentiallyTrustworthyOrigin False) untrusty) `shouldBe` False+ and (map (isPotentiallyTrustworthyOrigin False) trusty) `shouldBe` True+
test-nonet/Network/HTTP/Client/HeadersSpec.hs view
@@ -1,6 +1,9 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module Network.HTTP.Client.HeadersSpec where +import Control.Concurrent.MVar+import qualified Data.Sequence as Seq import Network.HTTP.Client.Internal import Network.HTTP.Types import Test.Hspec@@ -20,8 +23,8 @@ , "\nignored" ] (connection, _, _) <- dummyConnection input- statusHeaders <- parseStatusHeaders connection Nothing Nothing- statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1)+ statusHeaders <- parseStatusHeaders Nothing Nothing connection Nothing (\_ -> return ()) Nothing+ statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1) mempty [ ("foo", "bar") , ("baz", "bin") ]@@ -34,8 +37,8 @@ ] (conn, out, _) <- dummyConnection input let sendBody = connectionWrite conn "data"- statusHeaders <- parseStatusHeaders conn Nothing (Just sendBody)- statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1) [ ("foo", "bar") ]+ statusHeaders <- parseStatusHeaders Nothing Nothing conn Nothing (\_ -> return ()) (Just sendBody)+ statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1) [] [ ("foo", "bar") ] out >>= (`shouldBe` ["data"]) it "Expect: 100-continue (failure)" $ do@@ -44,8 +47,8 @@ ] (conn, out, _) <- dummyConnection input let sendBody = connectionWrite conn "data"- statusHeaders <- parseStatusHeaders conn Nothing (Just sendBody)- statusHeaders `shouldBe` StatusHeaders status417 (HttpVersion 1 1) []+ statusHeaders <- parseStatusHeaders Nothing Nothing conn Nothing (\_ -> return ()) (Just sendBody)+ statusHeaders `shouldBe` StatusHeaders status417 (HttpVersion 1 1) [] [] out >>= (`shouldBe` []) it "100 Continue without expectation is OK" $ do@@ -56,7 +59,72 @@ , "result" ] (conn, out, inp) <- dummyConnection input- statusHeaders <- parseStatusHeaders conn Nothing Nothing- statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1) [ ("foo", "bar") ]+ statusHeaders <- parseStatusHeaders Nothing Nothing conn Nothing (\_ -> return ()) Nothing+ statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1) [] [ ("foo", "bar") ] out >>= (`shouldBe` []) inp >>= (`shouldBe` ["result"])++ it "103 early hints" $ do+ let input =+ [ "HTTP/1.1 103 Early Hints\r\n"+ , "Link: </foo.js>\r\n"+ , "Link: </bar.js>\r\n\r\n"+ , "HTTP/1.1 200 OK\r\n"+ , "Content-Type: text/html\r\n\r\n"+ , "<div></div>"+ ]+ (conn, _, inp) <- dummyConnection input++ callbackResults :: MVar (Seq.Seq [Header]) <- newMVar mempty+ let onEarlyHintHeader h = modifyMVar_ callbackResults (return . (Seq.|> h))++ statusHeaders <- parseStatusHeaders Nothing Nothing conn Nothing onEarlyHintHeader Nothing+ statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1)+ [("Link", "</foo.js>")+ , ("Link", "</bar.js>")+ ]+ [("Content-Type", "text/html")+ ]++ inp >>= (`shouldBe` ["<div></div>"])++ readMVar callbackResults+ >>= (`shouldBe` Seq.fromList [+ [("Link", "</foo.js>")+ , ("Link", "</bar.js>")+ ]])++ it "103 early hints (multiple sections)" $ do+ let input =+ [ "HTTP/1.1 103 Early Hints\r\n"+ , "Link: </foo.js>\r\n"+ , "Link: </bar.js>\r\n\r\n"+ , "HTTP/1.1 103 Early Hints\r\n"+ , "Link: </baz.js>\r\n\r\n"+ , "HTTP/1.1 200 OK\r\n"+ , "Content-Type: text/html\r\n\r\n"+ , "<div></div>"+ ]+ (conn, _, inp) <- dummyConnection input++ callbackResults :: MVar (Seq.Seq [Header]) <- newMVar mempty+ let onEarlyHintHeader h = modifyMVar_ callbackResults (return . (Seq.|> h))++ statusHeaders <- parseStatusHeaders Nothing Nothing conn Nothing onEarlyHintHeader Nothing+ statusHeaders `shouldBe` StatusHeaders status200 (HttpVersion 1 1)+ [("Link", "</foo.js>")+ , ("Link", "</bar.js>")+ , ("Link", "</baz.js>")+ ]+ [("Content-Type", "text/html")+ ]++ inp >>= (`shouldBe` ["<div></div>"])++ readMVar callbackResults+ >>= (`shouldBe` Seq.fromList [+ [("Link", "</foo.js>")+ , ("Link", "</bar.js>")+ ]+ , [("Link", "</baz.js>")]+ ])
test-nonet/Network/HTTP/Client/RequestBodySpec.hs view
@@ -7,8 +7,8 @@ import System.IO import Data.IORef import qualified Data.ByteString as BS-import Network.HTTP.Client (streamFile, parseUrl, requestBody)-import Network.HTTP.Client.Internal (dummyConnection, Connection, connectionWrite, requestBuilder)+import Network.HTTP.Client (streamFile, parseUrlThrow, requestBody)+import Network.HTTP.Client.Internal (dummyConnection, connectionWrite, requestBuilder) import System.Directory (getTemporaryDirectory) spec :: Spec@@ -19,11 +19,11 @@ withBinaryFile path ReadMode $ \h' -> do conn <- verifyFileConnection h' - req0 <- parseUrl "http://example.com"+ req0 <- parseUrlThrow "http://example.com" body <- streamFile path let req = req0 { requestBody = body } - requestBuilder req conn+ _ <- requestBuilder req conn hIsEOF h' `shouldReturn` True where withTmpFile = bracket getTmpFile closeTmpFile
test-nonet/Network/HTTP/Client/RequestSpec.hs view
@@ -2,44 +2,84 @@ module Network.HTTP.Client.RequestSpec where import Blaze.ByteString.Builder (fromByteString)-import Control.Applicative ((<$>))+import Control.Applicative as A ((<$>)) import Control.Monad (join, forM_, (<=<)) import Data.IORef import Data.Maybe (isJust, fromMaybe, fromJust)-import qualified Data.ByteString.Char8 as S8-import Network.HTTP.Client (parseUrl, requestHeaders, applyBasicProxyAuth) import Network.HTTP.Client.Internal-import Network.URI (URI(..), URIAuth(..), parseURI) +import Network.URI (URI(..), URIAuth(..), parseURI) import Test.Hspec+import Data.Monoid ((<>))+import Network.HTTP.Client (defaultRequest)+import Data.List (isInfixOf) spec :: Spec spec = do describe "case insensitive scheme" $ do- forM_ ["http://example.com", "httP://example.com", "HttP://example.com", "HttPs://example.com"] $ \url ->- it url $ case parseUrl url of+ forM_ ["http://example.com", "httP://example.com", "HttP://example.com", "HttPs://example.com"] $ \url -> do+ it url $ case parseUrlThrow url of Nothing -> error "failed" Just _ -> return () :: IO ()- forM_ ["ftp://example.com"] $ \url ->- it url $ case parseUrl url of+ it ("URI " ++ url) $ do+ case parseURI url of+ Nothing -> error ("invalid test URI: " ++ url)+ Just uri ->+ case requestFromURI uri of+ Nothing -> error "failed"+ Just _ -> return () :: IO ()+ forM_ ["ftp://example.com"] $ \url -> do+ it url $ case parseUrlThrow url of Nothing -> return () :: IO () Just req -> error $ show req+ it ("URI " ++ url) $ do+ case parseURI url of+ Nothing -> error ("invalid test URI: " ++ url)+ Just uri ->+ case requestFromURI uri of+ Nothing -> return () :: IO ()+ Just req -> error (show req) + describe "authentication in url" $ do it "passes validation" $ do- case parseUrl "http://agent:topsecret@example.com" of+ case parseUrlThrow "http://agent:topsecret@example.com" of Nothing -> error "failed" Just _ -> return () :: IO () it "add username/password to headers section" $ do- let request = parseUrl "http://user:pass@example.com"- field = join $ lookup "Authorization" . requestHeaders <$> request+ let request = parseUrlThrow "http://user:pass@example.com"+ field = join $ lookup "Authorization" . requestHeaders A.<$> request requestHostnameWithoutAuth = "example.com" (uriRegName $ fromJust $ uriAuthority $ getUri $ fromJust request) `shouldBe` requestHostnameWithoutAuth field `shouldSatisfy` isJust field `shouldBe` Just "Basic dXNlcjpwYXNz" + describe "getUri" $ do+ context "when protocol is http and port is 80" $ do+ it "omits port" $ do+ let url = "http://example.com/"+ request <- parseRequest url+ show (getUri request) `shouldBe` url++ context "when protocol is https and port is 443" $ do+ it "omits port" $ do+ let url = "https://example.com/"+ request <- parseRequest url+ show (getUri request) `shouldBe` url++ context "when protocol is https and port is 80" $ do+ it "does not omit port" $ do+ let url = "https://example.com:80/"+ request <- parseRequest url+ show (getUri request) `shouldBe` url++ describe "Show Request" $+ it "redacts authorization header content" $ do+ let request = defaultRequest { requestHeaders = [("Authorization", "secret")] }+ isInfixOf "secret" (show request) `shouldBe` False+ describe "applyBasicProxyAuth" $ do- let request = applyBasicProxyAuth "user" "pass" <$> parseUrl "http://example.org"+ let request = applyBasicProxyAuth "user" "pass" <$> parseUrlThrow "http://example.org" field = join $ lookup "Proxy-Authorization" . requestHeaders <$> request it "Should add a proxy-authorization header" $ do field `shouldSatisfy` isJust@@ -67,16 +107,16 @@ describe "requestBuilder" $ do it "sends the full request, combining headers and body in the non-streaming case" $ do- let Just req = parseUrl "http://localhost"+ let Just req = parseUrlThrow "http://localhost" let req' = req { method = "PUT", path = "foo" } (conn, out, _) <- dummyConnection [] forM_ (bodies `zip` out1) $ \(b, o) -> do cont <- requestBuilder (req' { requestBody = b } ) conn- (const "<IO ()>" <$> cont) `shouldBe` Nothing+ (const ("<IO ()>" :: String) <$> cont) `shouldBe` Nothing out >>= (`shouldBe` o) it "sends only headers and returns an action for the body on 'Expect: 100-continue'" $ do- let Just req = parseUrl "http://localhost"+ let Just req = parseUrlThrow "http://localhost" let req' = req { requestHeaders = [("Expect", "100-continue")] , method = "PUT" , path = "foo"@@ -114,7 +154,7 @@ popper dat = do r <- newIORef dat- return . atomicModifyIORef' r $ \xs ->+ return . atomicModifyIORef r $ \xs -> case xs of (x:xs') -> (xs', x) [] -> ([], "")
test-nonet/Network/HTTP/Client/ResponseSpec.hs view
@@ -16,8 +16,8 @@ spec :: Spec spec = describe "ResponseSpec" $ do- let getResponse' conn = getResponse (const $ return ()) Nothing req conn Nothing- Just req = parseUrl "http://localhost"+ let getResponse' conn = getResponse Nothing Nothing Nothing req (dummyManaged conn) Nothing+ req = parseRequest_ "http://localhost" it "basic" $ do (conn, _, _) <- dummyConnection [ "HTTP/1.1 200 OK\r\n"@@ -59,7 +59,7 @@ , "Transfer-encoding: chunked\r\n\r\n" , "5\r\nHello\r" , "\n2\r\n W"- , "\r\n4 ignored\r\norld\r\n0\r\nHTTP/1.1"+ , "\r\n4 ignored\r\norld\r\n0\r\n\r\nHTTP/1.1" ] Response {..} <- getResponse' conn responseStatus `shouldBe` status200
test-nonet/Network/HTTP/ClientSpec.hs view
@@ -1,82 +1,135 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Network.HTTP.ClientSpec where -import Control.Concurrent (forkIO, threadDelay)+import Control.Concurrent (threadDelay, yield) import Control.Concurrent.Async (withAsync) import qualified Control.Concurrent.Async as Async-import Control.Exception (bracket, catch, IOException)-import Control.Monad (forever, replicateM_, void)-import Network.HTTP.Client+import Control.Exception (bracket, throwIO, ErrorCall(..))+import qualified Control.Exception as E+import Control.Monad (forever, replicateM_, when, unless)+import Network.HTTP.Client hiding (port)+import qualified Network.HTTP.Client as NC+import qualified Network.HTTP.Client.Internal as Internal import Network.HTTP.Types (status413)-import Network.Socket (sClose)+import Network.HTTP.Types.Header+import qualified Network.Socket as NS import Test.Hspec import qualified Data.Streaming.Network as N import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as SL import Data.ByteString.Lazy.Char8 () -- orphan instance+import Data.IORef+import System.Mem (performGC) +-- See: https://github.com/snoyberg/http-client/issues/111#issuecomment-366526660+notWindows :: Monad m => m () -> m ()+#ifdef WINDOWS+notWindows _ = return ()+#else+notWindows x = x+#endif++crlf :: S.ByteString+crlf = "\r\n"+ main :: IO () main = hspec spec -redirectServer :: (Int -> IO a) -> IO a-redirectServer inner = bracket+silentIOError :: IO () -> IO ()+silentIOError a = a `E.catch` \e -> do+ let _ = e :: IOError+ return ()++redirectServerToDifferentHost :: Maybe Int -> (Int -> IO a) -> IO a+redirectServerToDifferentHost maxRedirects inner = bracket (N.bindRandomPortTCP "*4")- (sClose . snd)+ (NS.close . snd) $ \(port, lsocket) -> withAsync (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app) (const $ inner port)+ where+ redirect ad = do+ N.appWrite ad "HTTP/1.1 301 Redirect\r\nLocation: http://example.com\r\ncontent-length: 5\r\n\r\n"+ threadDelay 10000+ N.appWrite ad "hello\r\n"+ threadDelay 10000+ app ad = Async.race_+ (silentIOError $ forever (N.appRead ad))+ (silentIOError $ case maxRedirects of+ Nothing -> forever $ redirect ad+ Just n ->+ replicateM_ n (redirect ad) >>+ N.appWrite ad "HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello\r\n")++redirectServer :: Maybe Int+ -- ^ If Just, stop redirecting after that many hops.+ -> (Int -> IO a) -> IO a+redirectServer maxRedirects inner = bracket+ (N.bindRandomPortTCP "*4")+ (NS.close . snd)+ $ \(port, lsocket) -> withAsync+ (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app)+ (const $ inner port) where- app ad = do- forkIO $ forever $ N.appRead ad- forever $ do- N.appWrite ad "HTTP/1.1 301 Redirect\r\nLocation: /\r\ncontent-length: 5\r\n\r\n"- threadDelay 10000- N.appWrite ad "hello\r\n"- threadDelay 10000+ redirect ad = do+ N.appWrite ad "HTTP/1.1 301 Redirect\r\nLocation: /\r\ncontent-length: 5\r\n\r\n"+ threadDelay 10000+ N.appWrite ad "hello\r\n"+ threadDelay 10000+ app ad = Async.race_+ (silentIOError $ forever (N.appRead ad))+ (silentIOError $ case maxRedirects of+ Nothing -> forever $ redirect ad+ Just n ->+ replicateM_ n (redirect ad) >>+ N.appWrite ad "HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello\r\n") redirectCloseServer :: (Int -> IO a) -> IO a redirectCloseServer inner = bracket (N.bindRandomPortTCP "*4")- (sClose . snd)+ (NS.close . snd) $ \(port, lsocket) -> withAsync (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app) (const $ inner port) where app ad = do Async.race_- (forever (N.appRead ad))- (N.appWrite ad "HTTP/1.1 301 Redirect\r\nLocation: /\r\nConnection: close\r\n\r\nhello")- N.appCloseConnection ad+ (silentIOError $ forever (N.appRead ad))+ (silentIOError $ N.appWrite ad "HTTP/1.1 301 Redirect\r\nLocation: /\r\nConnection: close\r\n\r\nhello")+ case N.appRawSocket ad of+ Nothing -> error "appRawSocket failed"+ Just s -> NS.shutdown s NS.ShutdownSend bad100Server :: Bool -- ^ include extra headers? -> (Int -> IO a) -> IO a bad100Server extraHeaders inner = bracket (N.bindRandomPortTCP "*4")- (sClose . snd)+ (NS.close . snd) $ \(port, lsocket) -> withAsync (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app) (const $ inner port) where- app ad = do- forkIO $ forever $ N.appRead ad- forever $ do+ app ad = Async.race_+ (silentIOError $ forever $ N.appRead ad)+ (silentIOError $ forever $ do N.appWrite ad $ S.concat [ "HTTP/1.1 100 Continue\r\n" , if extraHeaders then "foo:bar\r\nbaz: bin\r\n" else "" , "\r\nHTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello\r\n" ]- threadDelay 10000+ threadDelay 10000) earlyClose413 :: (Int -> IO a) -> IO a earlyClose413 inner = bracket (N.bindRandomPortTCP "*4")- (sClose . snd)+ (NS.close . snd) $ \(port, lsocket) -> withAsync (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app) (const $ inner port) where- app ad = do+ app ad = silentIOError $ do let readHeaders front = do newBS <- N.appRead ad let bs = S.append front newBS@@ -100,14 +153,16 @@ lengthZeroAndChunkZero = serveWith "HTTP/1.1 200 OK\r\ncontent-length: 0\r\ntransfer-encoding: chunked\r\n\r\n0\r\n\r\n" serveWith :: S.ByteString -> (Int -> IO a) -> IO a-serveWith resp inner = bracket- (N.bindRandomPortTCP "*4")- (sClose . snd)- $ \(port, lsocket) -> withAsync- (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app)- (const $ inner port)+serveWith resp inner = do+ (port, lsocket) <- (N.bindRandomPortTCP "*4")+ res <- Async.race+ (N.runTCPServer (N.serverSettingsTCPSocket lsocket) app)+ (inner port)+ case res of+ Left () -> error $ "serveWith: got Left"+ Right x -> return x where- app ad = do+ app ad = silentIOError $ do let readHeaders front = do newBS <- N.appRead ad let bs = S.append front newBS@@ -119,8 +174,7 @@ getChunkedResponse :: Int -> Manager -> IO (Response SL.ByteString) getChunkedResponse port' man = flip httpLbs man "http://localhost"- { port = port'- , checkStatus = \_ _ _ -> Nothing+ { NC.port = port' , requestBody = RequestBodyStreamChunked ($ return (S.replicate 100000 65)) } @@ -128,77 +182,168 @@ spec = describe "Client" $ do describe "fails on empty hostnames #40" $ do let test url = it url $ do- req <- parseUrl url+ req <- parseUrlThrow url man <- newManager defaultManagerSettings _ <- httpLbs req man `shouldThrow` \e -> case e of- InvalidDestinationHost "" -> True+ HttpExceptionRequest _ (InvalidDestinationHost "") -> True _ -> False return () mapM_ test ["http://", "https://", "http://:8000", "https://:8001"]- it "redirecting #41" $ redirectServer $ \port -> do- req' <- parseUrl $ "http://127.0.0.1:" ++ show port+ it "headers can be stripped on redirect" $ redirectServer (Just 5) $ \port -> do+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ let req = req' { requestHeaders = [(hAuthorization, "abguvatgbfrrurer")]+ , redirectCount = 10+ , shouldStripHeaderOnRedirect = (== hAuthorization)+ }+ man <- newManager defaultManagerSettings+ withResponseHistory req man $ \hr -> do+ print $ map (requestHeaders . fst) $ hrRedirects hr+ mapM_ (\r -> requestHeaders r `shouldBe` []) $+ map fst $ tail $ hrRedirects hr+ it "does strips header on redirect, if hosts are different and set to strip them if host differ" $ redirectServerToDifferentHost (Just 1) $ \port -> do+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ let req = req' { requestHeaders = [(hAuthorization, "abguvatgbfrrurer")]+ , redirectCount = 10+ , shouldStripHeaderOnRedirect = (== hAuthorization)+ , shouldStripHeaderOnRedirectIfOnDifferentHostOnly = True+ }+ man <- newManager defaultManagerSettings+ withResponseHistory req man $ \hr -> do+ print $ map (requestHeaders . fst) $ hrRedirects hr+ mapM_ (\r -> requestHeaders r `shouldBe` []) $+ map fst $ tail $ hrRedirects hr+ it "does NOT strips header on redirect, if hosts are same and set to strip them if host differ" $ redirectServerToDifferentHost (Just 1) $ \port -> do+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ let req = req' { requestHeaders = [(hAuthorization, "abguvatgbfrrurer")]+ , redirectCount = 10+ , shouldStripHeaderOnRedirect = (== hAuthorization)+ , shouldStripHeaderOnRedirectIfOnDifferentHostOnly = True+ }+ man <- newManager defaultManagerSettings+ withResponseHistory req man $ \hr -> do+ print $ map (requestHeaders . fst) $ hrRedirects hr+ mapM_ (\r -> requestHeaders r `shouldBe` [("Authorization","abguvatgbfrrurer")]) $+ map fst $ tail $ hrRedirects hr+ it "redirecting #41" $ redirectServer Nothing $ \port -> do+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port let req = req' { redirectCount = 1 }- withManager defaultManagerSettings $ \man -> replicateM_ 10 $ do+ man <- newManager defaultManagerSettings+ replicateM_ 10 $ do httpLbs req man `shouldThrow` \e -> case e of- TooManyRedirects _ -> True+ HttpExceptionRequest _ (TooManyRedirects _) -> True _ -> False- it "redirectCount=0" $ redirectServer $ \port -> do- req' <- parseUrl $ "http://127.0.0.1:" ++ show port+ it "redirectCount=0" $ redirectServer Nothing $ \port -> do+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port let req = req' { redirectCount = 0 }- withManager defaultManagerSettings $ \man -> replicateM_ 10 $ do+ man <- newManager defaultManagerSettings+ replicateM_ 10 $ do httpLbs req man `shouldThrow` \e -> case e of- StatusCodeException{} -> True+ HttpExceptionRequest _ StatusCodeException{} -> True _ -> False it "connecting to missing server gives nice error message" $ do (port, socket) <- N.bindRandomPortTCP "*4"- sClose socket- req <- parseUrl $ "http://127.0.0.1:" ++ show port- withManager defaultManagerSettings $ \man ->- httpLbs req man `shouldThrow` \e ->- case e of- FailedConnectionException2 "127.0.0.1" port' False _ -> port == port'- _ -> False+ NS.close socket+ req <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ man <- newManager defaultManagerSettings+ httpLbs req man `shouldThrow` \e ->+ case e of+ HttpExceptionRequest req' (ConnectionFailure _)+ -> host req == host req'+ && NC.port req == NC.port req'+ _ -> False describe "extra headers after 100 #49" $ do let test x = it (show x) $ bad100Server x $ \port -> do- req <- parseUrl $ "http://127.0.0.1:" ++ show port- withManager defaultManagerSettings $ \man -> replicateM_ 10 $ do- x <- httpLbs req man- responseBody x `shouldBe` "hello"+ req <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ man <- newManager defaultManagerSettings+ replicateM_ 10 $ do+ x' <- httpLbs req man+ responseBody x' `shouldBe` "hello" test False test True - it "early close on a 413" $ earlyClose413 $ \port' -> do- withManager defaultManagerSettings $ \man -> do- res <- getChunkedResponse port' man- responseBody res `shouldBe` "goodbye"- responseStatus res `shouldBe` status413+ notWindows $ it "early close on a 413" $ earlyClose413 $ \port' -> do+ man <- newManager defaultManagerSettings+ res <- getChunkedResponse port' man+ responseBody res `shouldBe` "goodbye"+ responseStatus res `shouldBe` status413 - it "length zero and chunking zero #108" $ lengthZeroAndChunkZero $ \port' -> do- withManager defaultManagerSettings $ \man -> do- res <- getChunkedResponse port' man- responseBody res `shouldBe` ""+ notWindows $ it "length zero and chunking zero #108" $ lengthZeroAndChunkZero $ \port' -> do+ man <- newManager defaultManagerSettings+ res <- getChunkedResponse port' man+ responseBody res `shouldBe` "" - it "length zero and chunking" $ lengthZeroAndChunked $ \port' -> do- withManager defaultManagerSettings $ \man -> do- res <- getChunkedResponse port' man- responseBody res `shouldBe` "Wikipedia in\r\n\r\nchunks."+ notWindows $ it "length zero and chunking" $ lengthZeroAndChunked $ \port' -> do+ man <- newManager defaultManagerSettings+ res <- getChunkedResponse port' man+ responseBody res `shouldBe` "Wikipedia in\r\n\r\nchunks." - it "length and chunking" $ lengthAndChunked $ \port' -> do- withManager defaultManagerSettings $ \man -> do- res <- getChunkedResponse port' man- responseBody res `shouldBe` "Wikipedia in\r\n\r\nchunks."+ notWindows $ it "length and chunking" $ lengthAndChunked $ \port' -> do+ man <- newManager defaultManagerSettings+ res <- getChunkedResponse port' man+ responseBody res `shouldBe` "Wikipedia in\r\n\r\nchunks." - it "withResponseHistory and redirect" $ redirectCloseServer $ \port -> do+ notWindows $ it "withResponseHistory and redirect" $ redirectCloseServer $ \port -> do -- see https://github.com/snoyberg/http-client/issues/169- req' <- parseUrl $ "http://127.0.0.1:" ++ show port+ req' <- parseUrlThrow $ "http://127.0.0.1:" ++ show port let req = req' {redirectCount = 1}- withManager defaultManagerSettings $ \man -> do- withResponseHistory req man (const $ return ())- `shouldThrow` \e ->+ man <- newManager defaultManagerSettings+ withResponseHistory req man (const $ return ())+ `shouldThrow` \e -> case e of- TooManyRedirects _ -> True- _ -> False+ HttpExceptionRequest _ (TooManyRedirects _) -> True+ _ -> False++ it "should not write to closed connection" $ do+ -- see https://github.com/snoyberg/http-client/issues/225+ closedRef <- newIORef False+ okRef <- newIORef True+ let checkStatus = do+ closed <- readIORef closedRef+ when closed $ do+ writeIORef okRef False++ conn <- makeConnection+ (return S.empty)+ (const checkStatus)+ (checkStatus >> writeIORef closedRef True)++ Internal.connectionClose conn++ -- let GC release the connection and run finalizers+ performGC+ yield+ performGC++ ok <- readIORef okRef+ unless ok $+ throwIO (ErrorCall "already closed")++ it "does not allow port overflow #383" $ do+ case parseRequest "https://o_O:18446744072699450606" of+ Left _ -> pure () :: IO ()+ Right req -> error $ "Invalid request: " ++ show req++ it "too many header fields" $ do+ let message = S.concat $+ ["HTTP/1.1 200 OK", crlf] <> replicate 120 ("foo: bar" <> crlf) <> [crlf, "body"]++ serveWith message $ \port -> do+ man <- newManager $ managerSetMaxNumberHeaders 120 defaultManagerSettings+ req <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ httpLbs req man `shouldThrow` \e -> case e of+ HttpExceptionRequest _ TooManyHeaderFields -> True+ _otherwise -> False++ it "not too many header fields" $ do+ let message = S.concat $+ ["HTTP/1.1 200 OK", crlf] <> replicate 120 ("foo: bar" <> crlf) <> [crlf, "body"]++ serveWith message $ \port -> do+ man <- newManager $ managerSetMaxNumberHeaders 121 defaultManagerSettings+ req <- parseUrlThrow $ "http://127.0.0.1:" ++ show port+ res <- httpLbs req man+ responseBody res `shouldBe` "body"
test/Network/HTTP/ClientSpec.hs view
@@ -1,59 +1,156 @@ {-# LANGUAGE OverloadedStrings #-} module Network.HTTP.ClientSpec where -import Control.Exception (toException)-import Network (withSocketsDo)-import Network.HTTP.Client-import Network.HTTP.Types (found302, status200, status405)-import Test.Hspec-import Data.ByteString.Lazy.Char8 ()+import Control.Concurrent (threadDelay)+import qualified Data.ByteString.Char8 as BS+import Network.HTTP.Client+import Network.HTTP.Client.Internal+import Network.HTTP.Types (status200, found302, status405)+import Network.HTTP.Types.Status+import qualified Network.Socket as NS+import Test.Hspec+import Control.Applicative ((<$>))+import Data.ByteString.Lazy.Char8 () -- orphan instance+import System.Mem (performGC) main :: IO () main = hspec spec spec :: Spec spec = describe "Client" $ do- it "works" $ withSocketsDo $ do- req <- parseUrl "http://httpbin.org/"+ it "works" $ do+ req <- parseUrlThrow "http://httpbin.org/" man <- newManager defaultManagerSettings res <- httpLbs req man responseStatus res `shouldBe` status200 + -- Test the failure condition described in https://github.com/snoyberg/http-client/issues/489+ it "keeps connection alive long enough" $ do+ req <- parseUrlThrow "http://httpbin.org/"+ man <- newManager defaultManagerSettings+ res <- responseOpen req man+ responseStatus res `shouldBe` status200+ let+ getChunk = responseBody res+ drainAll = do+ chunk <- getChunk+ if BS.null chunk then pure () else drainAll++ -- The returned `BodyReader` used to not contain a reference to the `Managed Connection`,+ -- only to the extracted connection and to the release action. Therefore, triggering a GC+ -- would close the connection even though we were not done reading.+ performGC+ -- Not ideal, but weak finalizers run on a separate thread, so it's racing with our drain+ -- call+ threadDelay 500000++ drainAll+ -- Calling `responseClose res` here prevents the early collection from happening in this+ -- test, but in a larger production application that did involve a `responseClose`, it still+ -- occurred.+ describe "method in URL" $ do- it "success" $ withSocketsDo $ do- req <- parseUrl "POST http://httpbin.org/post"+ it "success" $ do+ req <- parseUrlThrow "POST http://httpbin.org/post" man <- newManager defaultManagerSettings res <- httpLbs req man responseStatus res `shouldBe` status200 - it "failure" $ withSocketsDo $ do- req' <- parseUrl "PUT http://httpbin.org/post"- let req = req'- { checkStatus = \_ _ _ -> Nothing- }+ it "failure" $ do+ req <- parseRequest "PUT http://httpbin.org/post" man <- newManager defaultManagerSettings res <- httpLbs req man responseStatus res `shouldBe` status405+ describe "bearer auth" $ do+ it "success" $ do+ initialReq <- parseUrlThrow "http://httpbin.org/bearer"+ let finalReq = applyBearerAuth "token" initialReq+ man <- newManager defaultManagerSettings+ res <- httpLbs finalReq man+ responseStatus res `shouldBe` status200+ it "failure" $ do+ req <- parseRequest "http://httpbin.org/bearer"+ man <- newManager defaultManagerSettings+ res <- httpLbs req man+ responseStatus res `shouldBe` status401 - describe "managerModifyRequest" $ do+ describe "redirects" $ do+ xit "follows redirects" $ do+ req <- parseRequest "http://httpbin.org/redirect-to?url=http://httpbin.org"+ man <- newManager defaultManagerSettings+ res <- httpLbs req man+ responseStatus res `shouldBe` status200 - it "can set port to 80" $ do- let modify req = return req { port = 80 }- settings = defaultManagerSettings { managerModifyRequest = modify }- withManager settings $ \man -> do+ xit "allows to disable redirect following" $ do+ req <- (\ r -> r{ redirectCount = 0 }) <$>+ parseRequest "http://httpbin.org/redirect-to?url=http://httpbin.org"+ man <- newManager defaultManagerSettings+ res <- httpLbs req man+ responseStatus res `shouldBe` found302++ context "managerModifyResponse" $ do+ it "allows to modify the response status code" $ do+ let modify :: Response BodyReader -> IO (Response BodyReader)+ modify res = do+ return res {+ responseStatus = (responseStatus res) {+ statusCode = 201+ }+ }+ settings = defaultManagerSettings { managerModifyResponse = modify }+ man <- newManager settings+ res <- httpLbs "http://httpbin.org" man+ (statusCode.responseStatus) res `shouldBe` 201++ it "modifies the response body" $ do+ let modify :: Response BodyReader -> IO (Response BodyReader)+ modify res = do+ reader <- constBodyReader [BS.pack "modified response body"]+ return res {+ responseBody = reader+ }+ settings = defaultManagerSettings { managerModifyResponse = modify }+ man <- newManager settings+ res <- httpLbs "http://httpbin.org" man+ responseBody res `shouldBe` "modified response body"++ context "managerModifyRequest" $ do+ it "port" $ do+ let modify req = return req { port = 80 }+ settings = defaultManagerSettings { managerModifyRequest = modify }+ man <- newManager settings res <- httpLbs "http://httpbin.org:1234" man responseStatus res `shouldBe` status200 - it "can set 'checkStatus' to throw StatusCodeException" $ do- let modify req = return req { checkStatus = \s hs cj -> Just $ toException $ StatusCodeException s hs cj }- settings = defaultManagerSettings { managerModifyRequest = modify }- withManager settings $ \man ->+ it "checkResponse" $ do+ let modify req = return req { checkResponse = \_ _ -> error "some exception" }+ settings = defaultManagerSettings { managerModifyRequest = modify }+ man <- newManager settings httpLbs "http://httpbin.org" man `shouldThrow` anyException - it "can set redirectCount to 0 to prevent following redirects" $ do- let modify req = return req { redirectCount = 0 }- settings = defaultManagerSettings { managerModifyRequest = modify }- man <- newManager settings- httpLbs "http://httpbin.org/redirect-to?url=foo" man `shouldThrow` ( \ (StatusCodeException s _ _) -> s == found302)-+ xit "redirectCount" $ do+ let modify req = return req { redirectCount = 0 }+ settings = defaultManagerSettings { managerModifyRequest = modify }+ man <- newManager settings+ response <- httpLbs "http://httpbin.org/redirect-to?url=foo" man+ responseStatus response `shouldBe` found302 + -- skipped because CI doesn't have working IPv6+ xdescribe "raw IPV6 address as hostname" $ do+ it "works" $ do+ -- We rely on example.com serving a web page over IPv6.+ -- The request (currently) actually ends up as 404 due to+ -- virtual hosting, but we just care that the networking+ -- side works.+ (addr:_) <- NS.getAddrInfo+ (Just NS.defaultHints { NS.addrFamily = NS.AF_INET6 })+ (Just "example.com")+ (Just "http")+ -- ipv6Port will be of the form [::1]:80, which is good enough+ -- for our purposes; ideally we'd easily get just the ::1.+ let ipv6Port = show $ NS.addrAddress addr+ ipv6Port `shouldStartWith` "["+ req <- parseUrlThrow $ "http://" ++ ipv6Port+ man <- newManager defaultManagerSettings+ _ <- httpLbs (setRequestIgnoreStatus req) man+ return ()