packages feed

wreq 0.2.0.0 → 0.3.0.0

raw patch · 31 files changed

+2035/−456 lines, 31 filesdep +PSQueuedep +QuickCheckdep +aeson-qqdep ~http-clientdep ~lensdep ~snap-server

Dependencies added: PSQueue, QuickCheck, aeson-qq, base16-bytestring, byteable, cryptohash, ghc-prim, hashable, network-info, old-locale, test-framework-quickcheck2, transformers, unix-compat, unordered-containers, uuid

Dependency ranges changed: http-client, lens, snap-server, time

Files

Network/Wreq.hs view
@@ -56,6 +56,9 @@     -- ** DELETE     , delete     , deleteWith+    -- ** Custom Method+    , customMethod+    , customMethodWith     -- * Incremental consumption of responses     -- ** GET     , foldGet@@ -72,13 +75,17 @@     , Lens.params     , Lens.cookie     , Lens.cookies+    , Lens.checkStatus+     -- ** Authentication     -- $auth     , Auth+    , AWSAuthVersion(..)     , Lens.auth     , basicAuth     , oauth2Bearer     , oauth2Token+    , awsAuth     -- ** Proxy settings     , Proxy(Proxy)     , Lens.proxy@@ -116,6 +123,7 @@     , Lens.responseStatus     , Lens.Status     , Lens.statusCode+    , Lens.statusMessage     -- ** Link headers     , Lens.Link     , Lens.linkURL@@ -136,13 +144,13 @@      -- * Parsing responses     , Lens.atto+    , Lens.atto_     ) where  import Control.Lens ((.~), (&)) import Control.Monad (unless) import Control.Monad.Catch (MonadThrow(throwM)) import Data.Aeson (FromJSON)-import Data.ByteString.Char8 () import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Encoding (encodeUtf8)@@ -157,10 +165,9 @@ import qualified Data.Text as T import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Client.MultipartFormData as Form-import qualified Network.HTTP.Types as HTTP-import qualified Network.Wreq.Internal.Lens as Int import qualified Network.Wreq.Lens as Lens import qualified Network.Wreq.Types as Wreq+import qualified Data.ByteString.Char8 as BC8  -- | Issue a GET request. --@@ -194,7 +201,7 @@ -- >>> r ^? responseBody . key "url" -- Just (String "http://httpbin.org/get?foo=bar") getWith :: Options -> String -> IO (Response L.ByteString)-getWith opts url = request id opts url readResponse+getWith opts url = runRead =<< prepareGet opts url  -- | Issue a POST request. --@@ -224,9 +231,7 @@ -- >>> r ^? responseBody . key "url" -- Just (String "http://httpbin.org/post?foo=bar") postWith :: Postable a => Options -> String -> a -> IO (Response L.ByteString)-postWith opts url payload =-  requestIO (postPayload payload . (Int.method .~ HTTP.methodPost)) opts url-    readResponse+postWith opts url payload = runRead =<< preparePost opts url payload  -- | Issue a HEAD request. --@@ -256,7 +261,7 @@ -- >>> r ^? responseHeader "Connection" -- Just "keep-alive" headWith :: Options -> String -> IO (Response ())-headWith = emptyMethodWith HTTP.methodHead+headWith opts url = runIgnore =<< prepareHead opts url  -- | Issue a PUT request. put :: Putable a => String -> a -> IO (Response L.ByteString)@@ -264,9 +269,7 @@  -- | Issue a PUT request, using the supplied 'Options'. putWith :: Putable a => Options -> String -> a -> IO (Response L.ByteString)-putWith opts url payload =-  requestIO (putPayload payload . (Int.method .~ HTTP.methodPut)) opts url-    readResponse+putWith opts url payload = runRead =<< preparePut opts url payload  -- | Issue an OPTIONS request. --@@ -289,7 +292,7 @@ --'optionsWith' opts \"http:\/\/httpbin.org\/get\" -- @ optionsWith :: Options -> String -> IO (Response ())-optionsWith = emptyMethodWith HTTP.methodOptions+optionsWith opts url = runIgnore =<< prepareOptions opts url  -- | Issue a DELETE request. --@@ -302,7 +305,7 @@ -- >>> r <- delete "http://httpbin.org/delete" -- >>> r ^. responseStatus . statusCode -- 200-delete :: String -> IO (Response ())+delete :: String -> IO (Response L.ByteString) delete = deleteWith defaults  -- | Issue a DELETE request, using the supplied 'Options'.@@ -318,14 +321,45 @@ -- >>> r <- deleteWith opts "http://httpbin.org/delete" -- >>> r ^. responseStatus . statusCode -- 200-deleteWith :: Options -> String -> IO (Response ())-deleteWith = emptyMethodWith HTTP.methodDelete+deleteWith :: Options -> String -> IO (Response L.ByteString)+deleteWith opts url = runRead =<< prepareDelete opts url +-- | Issue a custom-method request+--+-- Example:+-- @+-- 'customMethod' \"PATCH\" \"http:\/\/httpbin.org\/patch\"+-- @+--+-- >>> r <- customMethod "PATCH" "http://httpbin.org/patch"+-- >>> r ^. responseStatus . statusCode+-- 200+customMethod :: String -> String -> IO (Response L.ByteString)+customMethod method url = customMethodWith method defaults url++-- | Issue a custom request method request, using the supplied 'Options'.+--+-- Example:+--+-- @+--let opts = 'defaults' '&' 'Lens.redirects' '.~' 0+--'customMethodWith' \"PATCH\" opts \"http:\/\/httpbin.org\/patch\"+-- @+--+-- >>> let opts = defaults & redirects .~ 0+-- >>> r <- customMethodWith "PATCH" opts "http://httpbin.org/patch"+-- >>> r ^. responseStatus . statusCode+-- 200+customMethodWith :: String -> Options -> String -> IO (Response L.ByteString)+customMethodWith method opts url = runRead =<< prepareMethod methodBS opts url+  where+    methodBS = BC8.pack method+ foldGet :: (a -> S.ByteString -> IO a) -> a -> String -> IO a foldGet f z url = foldGetWith defaults f z url  foldGetWith :: Options -> (a -> S.ByteString -> IO a) -> a -> String -> IO a-foldGetWith opts f z0 url = request id opts url (foldResponseBody f z0)+foldGetWith opts f z0 url = request return opts url (foldResponseBody f z0)  -- | Convert the body of an HTTP response from JSON to a suitable -- Haskell type.@@ -405,18 +439,22 @@ -- Example (note the use of TLS): -- -- @---let opts = 'defaults' '&' 'Lens.auth' '.~' 'basicAuth' \"user\" \"pass\"+--let opts = 'defaults' '&' 'Lens.auth' '?~' 'basicAuth' \"user\" \"pass\" --'getWith' opts \"https:\/\/httpbin.org\/basic-auth\/user\/pass\" -- @ ----- >>> let opts = defaults & auth .~ basicAuth "user" "pass"+-- Note here the use of the 'Control.Lens.?~' setter to turn an 'Auth'+-- into a 'Maybe' 'Auth', to make the type of the RHS compatible with+-- the 'Lens.auth' lens.+--+-- >>> let opts = defaults & auth ?~ basicAuth "user" "pass" -- >>> r <- getWith opts "https://httpbin.org/basic-auth/user/pass" -- >>> r ^? responseBody . key "authenticated" -- Just (Bool True) basicAuth :: S.ByteString       -- ^ Username.           -> S.ByteString       -- ^ Password.-          -> Maybe Auth-basicAuth user pass = Just (BasicAuth user pass)+          -> Auth+basicAuth = BasicAuth  -- | An OAuth2 bearer token. This is treated by many services as the -- equivalent of a username and password.@@ -424,11 +462,11 @@ -- Example (note the use of TLS): -- -- @---let opts = 'defaults' '&' 'Lens.auth' '.~' 'oauth2Bearer' \"1234abcd\"+--let opts = 'defaults' '&' 'Lens.auth' '?~' 'oauth2Bearer' \"1234abcd\" --'getWith' opts \"https:\/\/public-api.wordpress.com\/rest\/v1\/me\/\" -- @-oauth2Bearer :: S.ByteString -> Maybe Auth-oauth2Bearer token = Just (OAuth2Bearer token)+oauth2Bearer :: S.ByteString -> Auth+oauth2Bearer = OAuth2Bearer  -- | A not-quite-standard OAuth2 bearer token (that seems to be used -- only by GitHub). This will be treated by whatever services accept@@ -437,26 +475,37 @@ -- Example (note the use of TLS): -- -- @---let opts = 'defaults' '&' 'Lens.auth' '.~' 'oauth2Token' \"abcd1234\"+--let opts = 'defaults' '&' 'Lens.auth' '?~' 'oauth2Token' \"abcd1234\" --'getWith' opts \"https:\/\/api.github.com\/user\" -- @-oauth2Token :: S.ByteString -> Maybe Auth-oauth2Token token = Just (OAuth2Token token)+oauth2Token :: S.ByteString -> Auth+oauth2Token = OAuth2Token +-- | AWS v4 request signature.+--+-- Example (note the use of TLS):+--+-- @+--let opts = 'defaults' '&' 'Lens.auth' '?~' 'awsAuth AWSv4' \"key\" \"secret\"+--'getWith' opts \"https:\/\/dynamodb.us-west-2.amazonaws.com\"+-- @+awsAuth :: AWSAuthVersion -> S.ByteString -> S.ByteString -> Auth+awsAuth = AWSAuth+ -- | Proxy configuration. -- -- Example: -- -- @---let opts = 'defaults' '&' 'Lens.proxy' '.~' 'httpProxy' \"localhost\" 8000+--let opts = 'defaults' '&' 'Lens.proxy' '?~' 'httpProxy' \"localhost\" 8000 --'getWith' opts \"http:\/\/httpbin.org\/get\" -- @ ----- (You may wonder why this function returns a 'Maybe Proxy'. This--- allows it to be easily used on the right hand side of an operation,--- as above, without its result needing to be wrapped in 'Just'.)-httpProxy :: S.ByteString -> Int -> Maybe Proxy-httpProxy host port = Just (Proxy host port)+-- Note here the use of the 'Control.Lens.?~' setter to turn a 'Proxy'+-- into a 'Maybe' 'Proxy', to make the type of the RHS compatible with+-- the 'Lens.proxy' lens.+httpProxy :: S.ByteString -> Int -> Proxy+httpProxy = Proxy  -- | Make a 'Part' whose content is a strict 'T.Text', encoded as -- UTF-8.
+ Network/Wreq/Cache.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE CPP, DeriveDataTypeable, DeriveFunctor, DeriveGeneric,+    OverloadedStrings, RecordWildCards #-}++module Network.Wreq.Cache+    (+      shouldCache+    , validateEntry+    , cacheStore+    ) where++import Control.Applicative+import Control.Lens ((^?), (^.), (^..), folded, non, pre, to)+import Control.Monad (guard)+import Data.Attoparsec.ByteString.Char8 as A+import Data.CaseInsensitive (mk)+import Data.Foldable (forM_)+import Data.HashSet (HashSet)+import Data.Hashable (Hashable)+import Data.IntSet (IntSet)+import Data.IORef (newIORef)+import Data.List (sort)+import Data.Maybe (listToMaybe)+import Data.Monoid (First(..), mconcat)+import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)+import Data.Time.Format (parseTime)+import Data.Typeable (Typeable)+import GHC.Generics (Generic)+import Network.HTTP.Types (HeaderName, Method)+import Network.Wreq.Internal.Lens+import Network.Wreq.Internal.Types+import Network.Wreq.Lens+import System.Locale (defaultTimeLocale)+import qualified Data.ByteString.Char8 as B+import qualified Data.HashSet as HashSet+import qualified Data.IntSet as IntSet+import qualified Network.Wreq.Cache.Store as Store++#if MIN_VERSION_base(4,6,0)+import Data.IORef (atomicModifyIORef')+#else+import Data.IORef (IORef, atomicModifyIORef)++atomicModifyIORef' :: IORef a -> (a -> (a, b)) -> IO b+atomicModifyIORef' = atomicModifyIORef+#endif++cacheStore :: Int -> IO (Run body -> Run body)+cacheStore capacity = do+  cache <- newIORef (Store.empty capacity)+  return $ \run req -> do+    let url = reqURL req+    before <- getCurrentTime+    mresp <- atomicModifyIORef' cache $ \s ->+      case Store.lookup url s of+        Nothing       -> (s, Nothing)+        Just (ce, s') ->+          case validateEntry before ce of+            n@Nothing -> (Store.delete url s, n)+            resp      -> (s', resp)+    case mresp of+      Just resp -> return resp+      Nothing -> do+        resp <- run req+        after <- getCurrentTime+        forM_ (shouldCache after req resp) $ \ce ->+          atomicModifyIORef' cache $ \s -> (Store.insert url ce s, ())+        return resp++cacheableStatuses :: IntSet+cacheableStatuses = IntSet.fromList [200, 203, 300, 301, 410]++cacheableMethods :: HashSet Method+cacheableMethods = HashSet.fromList ["GET", "HEAD", "OPTIONS"]++possiblyCacheable :: Request -> Response body -> Bool+possiblyCacheable req resp =+    (req ^. method) `HashSet.member` cacheableMethods &&+    (resp ^. responseStatus . statusCode) `IntSet.member` cacheableStatuses++computeExpiration :: UTCTime -> [CacheResponse Seconds] -> Maybe UTCTime+computeExpiration now crs = do+  guard $ and [NoCache [] `notElem` crs, NoStore `notElem` crs]+  age <- listToMaybe $ sort [age | MaxAge age <- crs]+  return $! fromIntegral age `addUTCTime` now++validateEntry :: UTCTime -> CacheEntry body -> Maybe (Response body)+validateEntry now CacheEntry{..} =+  case entryExpires of+    Nothing          -> Just entryResponse+    Just e | e > now -> Just entryResponse+    _                -> Nothing++shouldCache :: UTCTime -> Req -> Response body -> Maybe (CacheEntry body)+shouldCache now (Req _ req) resp = do+  guard (possiblyCacheable req resp)+  let crs = resp ^.. responseHeader "Cache-Control" . atto_ parseCacheResponse .+                     folded . to simplifyCacheResponse+      dateHeader name = responseHeader name . to parseDate . folded+      mexpires = case crs of+                   [] -> resp ^? dateHeader "Expires"+                   _  -> computeExpiration now crs+      created = resp ^. pre (dateHeader "Date") . non now+  case mexpires of+    Just expires | expires <= created                -> empty+    Nothing      | req ^. method == "GET" &&+                   not (B.null (req ^. queryString)) -> empty+    _ -> return $ CacheEntry created mexpires resp++type Seconds = Int++data CacheResponse age = Public+                       | Private [HeaderName]+                       | NoCache [HeaderName]+                       | NoStore+                       | NoTransform+                       | MustRevalidate+                       | ProxyRevalidate+                       | MaxAge age+                       | SMaxAge age+                       | Extension+                       deriving (Eq, Show, Functor, Typeable, Generic)++instance Hashable age => Hashable (CacheResponse age)++simplifyCacheResponse :: CacheResponse age -> CacheResponse age+simplifyCacheResponse (Private _) = Private []+simplifyCacheResponse (NoCache _) = NoCache []+simplifyCacheResponse cr          = cr++parseCacheResponse :: A.Parser [CacheResponse Seconds]+parseCacheResponse = commaSep1 body+  where+    body = "public" *> pure Public+       <|> "private" *> (Private <$> (eq headerNames <|> pure []))+       <|> "no-cache" *> (NoCache <$> (eq headerNames <|> pure []))+       <|> "no-store" *> pure NoStore+       <|> "no-transform" *> pure NoTransform+       <|> "must-revalidate" *> pure MustRevalidate+       <|> "proxy-revalidate" *> pure ProxyRevalidate+       <|> "max-age" *> eq (MaxAge <$> decimal)+       <|> "s-maxage" *> eq (SMaxAge <$> decimal)+    headerNames = A.char '"' *> commaSep1 hdr <* A.char '"'+    hdr = mk <$> A.takeWhile1 (inClass "a-zA-Z0-9_-")+    commaSep1 p = (p <* skipSpace) `sepBy1` (A.char ',' *> skipSpace)+    eq p = skipSpace *> A.char '=' *> skipSpace *> p++parseDate :: B.ByteString -> Maybe UTCTime+parseDate s = getFirst . mconcat . map tryout $ [+    "%a, %d %b %Y %H:%M:%S %Z"+  , "%A, %d-%b-%y %H:%M:%S %Z"+  , "%a %b %e %H:%M:%S %Y"+  ]+  where tryout fmt = First $ parseTime defaultTimeLocale fmt (B.unpack s)
+ Network/Wreq/Cache/Store.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE BangPatterns, DeriveFunctor, RecordWildCards #-}++module Network.Wreq.Cache.Store+    (+      Store+    , empty+    , insert+    , delete+    , lookup+    , fromList+    , toList+    ) where++import Data.Hashable (Hashable)+import Data.Int (Int64)+import Data.List (foldl')+import Prelude hiding (lookup, map)+import qualified Data.HashMap.Lazy as HM+import qualified Data.PSQueue as PSQ++type Epoch = Int64++data Store k v = Store {+    capacity :: {-# UNPACK #-} !Int+  , size     :: {-# UNPACK #-} !Int+  , epoch    :: {-# UNPACK #-} !Epoch+  , lru      :: !(PSQ.PSQ k Epoch)+  , map      :: !(HM.HashMap k v)+  }++instance (Show k, Show v, Ord k, Hashable k) => Show (Store k v) where+    show st = "fromList " ++ show (toList st)++empty :: Ord k => Int -> Store k v+empty cap+  | cap <= 0  = error "empty: invalid capacity"+  | otherwise = Store cap 0 0 PSQ.empty HM.empty+{-# INLINABLE empty #-}++insert :: (Ord k, Hashable k) => k -> v -> Store k v -> Store k v+insert k v st@Store{..}+  | size < capacity || present =+    st { size  = if present then size else size + 1+       , epoch = epoch + 1+       , lru   = PSQ.insert k epoch lru+       , map   = HM.insert k v map+       }+  | otherwise =+      let Just (mink PSQ.:-> _, lru0) = PSQ.minView lru+      in st { epoch = epoch + 1+            , lru   = PSQ.insert k epoch lru0+            , map   = HM.insert k v $ if mink == k+                                      then map+                                      else HM.delete mink map+            }+  where present = k `HM.member` map+{-# INLINABLE insert #-}++lookup :: (Ord k, Hashable k) => k -> Store k v -> Maybe (v, Store k v)+lookup k st@Store{..} = do+  v <- HM.lookup k map+  let !st' = st { epoch = epoch + 1, lru = PSQ.insert k epoch lru }+  return (v, st')+{-# INLINABLE lookup #-}++delete :: (Ord k, Hashable k) => k -> Store k v -> Store k v+delete k st@Store{..}+  | k `HM.member` map =+    st { size = size - 1+       , lru  = PSQ.delete k lru+       , map  = HM.delete k map+       }+  | otherwise = st+{-# INLINABLE delete #-}++fromList :: (Ord k, Hashable k) => Int -> [(k, v)] -> Store k v+fromList = foldl' (flip (uncurry insert)) . empty+{-# INLINABLE fromList #-}++toList :: (Ord k, Hashable k) => Store k v -> [(k, v)]+toList Store{..} = [(k,v) | (k PSQ.:-> _) <- PSQ.toList lru,+                            let v = map HM.! k]+{-# INLINABLE toList #-}
Network/Wreq/Internal.hs view
@@ -9,7 +9,15 @@     , ignoreResponse     , readResponse     , request-    , requestIO+    , prepareGet+    , preparePost+    , runRead+    , prepareHead+    , runIgnore+    , prepareOptions+    , preparePut+    , prepareDelete+    , prepareMethod     ) where  import Control.Applicative ((<$>))@@ -22,7 +30,8 @@ import Network.HTTP.Client.Internal (Proxy(..), Request, Response(..), addProxy) import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.Wreq.Internal.Lens (setHeader)-import Network.Wreq.Types (Auth(..), Options(..))+import Network.Wreq.Internal.Types (Mgr, Req(..), Run)+import Network.Wreq.Types (Auth(..), Options(..), Postable(..), Putable(..)) import Prelude hiding (head) import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as Char8@@ -30,7 +39,8 @@ import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Types as HTTP import qualified Network.Wreq.Internal.Lens as Lens-import qualified Network.Wreq.Lens as Lens+import qualified Network.Wreq.Internal.AWS as AWS (signRequest)+import qualified Network.Wreq.Lens as Lens hiding (checkStatus)  -- This mess allows this module to continue to load during interactive -- development in ghci :-(@@ -38,6 +48,7 @@ import Paths_wreq (version) #else import Data.Version (Version(..))+version :: Version version = Version [0] ["wip"] #endif @@ -46,13 +57,14 @@  defaults :: Options defaults = Options {-    manager   = Left defaultManagerSettings-  , proxy     = Nothing-  , auth      = Nothing-  , headers   = [("User-Agent", userAgent)]-  , params    = []-  , redirects = 10-  , cookies   = HTTP.createCookieJar []+    manager     = Left defaultManagerSettings+  , proxy       = Nothing+  , auth        = Nothing+  , headers     = [("User-Agent", userAgent)]+  , params      = []+  , redirects   = 10+  , cookies     = HTTP.createCookieJar []+  , checkStatus = Nothing   }   where userAgent = "haskell wreq-" <> Char8.pack (showVersion version) @@ -63,7 +75,7 @@  emptyMethodWith :: HTTP.Method -> Options -> String -> IO (Response ()) emptyMethodWith method opts url =-  request (Lens.method .~ method) opts url ignoreResponse+  request (return . (Lens.method .~ method)) opts url ignoreResponse  ignoreResponse :: Response BodyReader -> IO (Response ()) ignoreResponse resp = (Lens.responseBody .~ ()) <$> readResponse resp@@ -82,25 +94,30 @@             then return z             else f z bs >>= go -requestIO :: (Request -> IO Request) -> Options -> String-          -> (Response BodyReader -> IO a) -> IO a-requestIO modify opts url body =-    either (flip HTTP.withManager go) go (manager opts)+request :: (Request -> IO Request) -> Options -> String+        -> (Response BodyReader -> IO a) -> IO a+request modify opts url act = run (manager opts) act =<< prepare modify opts url++run :: Mgr -> (Response BodyReader -> IO a) -> Request -> IO a+run emgr act req = either (flip HTTP.withManager go) go emgr+  where go mgr = HTTP.withResponse req mgr act++prepare :: (Request -> IO Request) -> Options -> String -> IO Request+prepare modify opts url = do+  signRequest =<< modify =<< frob <$> HTTP.parseUrl url   where-    go mgr = do-      let frob req = req-                   & Lens.requestHeaders %~ (headers opts ++)+    frob req = req & Lens.requestHeaders %~ (headers opts ++)                    & setQuery opts                    & setAuth opts                    & setProxy opts+                   & setCheckStatus opts                    & setRedirects opts                    & Lens.cookieJar .~ Just (cookies opts)-      req <- modify =<< (frob <$> HTTP.parseUrl url)-      HTTP.withResponse req mgr body--request :: (Request -> Request) -> Options -> String-        -> (Response BodyReader -> IO a) -> IO a-request f = requestIO (return . f)+    signRequest :: Request -> IO Request+    signRequest = maybe return f $ auth opts+      where+        f (AWSAuth versn key secret) = AWS.signRequest versn key secret+        f _ = return  setQuery :: Options -> Request -> Request setQuery opts =@@ -117,7 +134,43 @@     f (BasicAuth user pass) = HTTP.applyBasicAuth user pass     f (OAuth2Bearer token)  = setHeader "Authorization" ("Bearer " <> token)     f (OAuth2Token token)   = setHeader "Authorization" ("token " <> token)+    -- for AWS request signature, see Internal/AWS+    f (AWSAuth _ _ _)       = id  setProxy :: Options -> Request -> Request setProxy = maybe id f . proxy   where f (Proxy host port) = addProxy host port++setCheckStatus :: Options -> Request -> Request+setCheckStatus = maybe id f . checkStatus+  where f cs = ( & Lens.checkStatus .~ cs)++prepareGet :: Options -> String -> IO Req+prepareGet opts url = Req (manager opts) <$> prepare return opts url++runRead :: Run L.ByteString+runRead (Req mgr req) = run mgr readResponse req++preparePost :: Postable a => Options -> String -> a -> IO Req+preparePost opts url payload = Req (manager opts) <$>+  prepare (postPayload payload . (Lens.method .~ HTTP.methodPost)) opts url++prepareMethod :: HTTP.Method -> Options -> String -> IO Req+prepareMethod method opts url = Req (manager opts) <$>+  prepare (return . (Lens.method .~ method)) opts url++prepareHead :: Options -> String -> IO Req+prepareHead = prepareMethod HTTP.methodHead++runIgnore :: Run ()+runIgnore (Req mgr req) = run mgr ignoreResponse req++prepareOptions :: Options -> String -> IO Req+prepareOptions = prepareMethod HTTP.methodOptions++preparePut :: Putable a => Options -> String -> a -> IO Req+preparePut opts url payload = Req (manager opts) <$>+  prepare (putPayload payload . (Lens.method .~ HTTP.methodPut)) opts url++prepareDelete :: Options -> String -> IO Req+prepareDelete = prepareMethod HTTP.methodDelete
+ Network/Wreq/Internal/AWS.hs view
@@ -0,0 +1,189 @@+{-# LANGUAGE OverloadedStrings, BangPatterns #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Network.Wreq.Internal.AWS+    (+      signRequest+    , addTmpPayloadHashHeader+    ) where++import Control.Applicative ((<$>))+import Control.Lens ((%~), (^.), (&), to)+import Crypto.MAC (hmac, hmacGetDigest)+import Data.ByteString.Base16 as HEX (encode)+import Data.Byteable (toBytes)+import Data.Char (toLower)+import Data.List (sort)+import Data.Maybe (fromJust)+import Data.Monoid ((<>))+import Data.Time.Clock (getCurrentTime)+import Data.Time.Format (formatTime)+import Data.Time.LocalTime (utc, utcToLocalTime)+import Network.HTTP.Types (parseSimpleQuery, urlEncode)+import Network.Wreq.Internal.Lens+import Network.Wreq.Internal.Types (AWSAuthVersion(..))+import System.Locale (defaultTimeLocale)+import qualified Crypto.Hash as CT (HMAC, SHA256)+import qualified Crypto.Hash.SHA256 as SHA256 (hash, hashlazy)+import qualified Data.ByteString.Char8 as S+import qualified Data.CaseInsensitive  as CI (CI, original)+import qualified Data.HashSet as HashSet+import qualified Network.HTTP.Client as HTTP++-- Sign requests following the AWS v4 request signing specification:+-- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html+--+-- Runscope Inc. Traffic Inspector support:+-- We support (optionally) sending requests through the Runscope+-- (http://www.runscope.com) Traffic Inspector. If given a Runscope+-- URL to an AWS service, we will extract and correctly sign the+-- request for the underlying AWS service. We support Runscope buckets+-- with and without Bucket Authorization enabled+-- ("Runscope-Bucket-Auth").+--+-- Q: how do we get the payload hash to the signRequest function?+--+-- A: we use a (temporary) HTTP header to 'tunnel' the payload hash to+-- the signing function.  For POST and PUT requests, the+-- Network.Wreq.Types.payload function adds a HTTP header (name+-- defined in 'tmpPayloadHashHeader'). The+-- Network.Wreq.Internal.AWS.signRequest function reads the value of+-- the header and then removes it from the request.  For GET, HEAD,+-- and (currently) DELETE that carry no body, we use "" per AWS+-- documentation Item 6: "use empty string" in+-- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html++-- TODO: adjust when DELETE supports a body or PATCH is added+signRequest :: AWSAuthVersion -> S.ByteString -> S.ByteString ->+               Request -> IO Request+signRequest AWSv4 = signRequestV4++signRequestV4 :: S.ByteString -> S.ByteString -> Request -> IO Request+signRequestV4 key secret request = do+  !ts <- timestamp                         -- YYYYMMDDT242424Z, UTC based+  let origHost = request ^. host          -- potentially w/ runscope bucket+      runscopeBucketAuth =+        lookup "Runscope-Bucket-Auth" $ request ^. requestHeaders+      noRunscopeHost = removeRunscope origHost -- rm Runscope for signing+      (service, region) = serviceAndRegion noRunscopeHost+      date = S.takeWhile (/= 'T') ts      -- YYYYMMDD+      hashedPayload+        | request ^. method `elem` ["POST", "PUT"] =+          fromJust . lookup tmpPayloadHashHeader $ request ^. requestHeaders+        | otherwise = HEX.encode $ SHA256.hash ""+      -- add common v4 signing headers, service specific headers, and+      -- drop tmp header and Runscope-Bucket-Auth header (if present).+      req = request & requestHeaders %~+            (([ ("host", noRunscopeHost)+              , ("x-amz-date", ts)] +++              [("x-amz-content-sha256", hashedPayload) | service == "s3"]) ++)+            . deleteKey tmpPayloadHashHeader -- drop tmp header+            -- Runscope (correctly) doesn't send Bucket Auth header to AWS,+            -- remove it from the headers we sign. Adding back in at the end.+            . deleteKey "Runscope-Bucket-Auth"+  -- task 1+  let hl = req ^. requestHeaders . to sort+      signedHeaders = S.intercalate ";" . map (lowerCI . fst) $ hl+      canonicalReq = S.intercalate "\n" [+          req ^. method             -- step 1+        , req ^. path               -- step 2+        ,   S.intercalate "&"       -- step 3b, incl. sort+            -- urlEncode True (QS) to encode ':' and '/' (e.g. in AWS arns)+          . map (\(k,v) -> urlEncode True k <> "=" <> urlEncode True v)+          . sort $+          parseSimpleQuery $ req ^. queryString+        ,   S.unlines                -- step 4, incl. sort+          . map (\(k,v) -> lowerCI k <> ":" <> trimHeaderValue v) $ hl+        , signedHeaders             -- step 5+        , hashedPayload             -- step 6, handles empty payload+        ]+  -- task 2+  let dateScope = S.intercalate "/" [date, region, service, "aws4_request"]+      stringToSign = S.intercalate "\n" [+          "AWS4-HMAC-SHA256"+        , ts+        , dateScope+        , HEX.encode $ SHA256.hash canonicalReq+        ]+  -- task 3, steps 1 and 2+  let signature = ("AWS4" <> secret) &+                  hmac' date & hmac' region & hmac' service &+                  hmac' "aws4_request" & hmac' stringToSign & HEX.encode+      authorization = S.intercalate ", " [+          "AWS4-HMAC-SHA256 Credential=" <> key <> "/" <> dateScope+        , "SignedHeaders=" <> signedHeaders+        , "Signature=" <> signature+        ]+  -- Add the AWS Authorization header.+  -- Restore the Host header to the Runscope endpoint+  -- so they can proxy accordingly (if used, otherwise this is a nop).+  -- Add the Runscope Bucket Auth header back in, if it was set originally.+  return $ setHeader "host" origHost+        <$> maybe id (setHeader "Runscope-Bucket-Auth") runscopeBucketAuth+        <$> setHeader "authorization" authorization $ req+  where+    lowerCI = S.map toLower . CI.original+    trimHeaderValue =+      id -- FIXME, see step 4, whitespace trimming but not in double+         -- quoted sections, AWS spec.+    timestamp = render <$> getCurrentTime+      where render = S.pack . formatTime defaultTimeLocale "%Y%m%dT%H%M%SZ" .+                     utcToLocalTime utc -- UTC printable: YYYYMMDDTHHMMSSZ+    hmac' s k = toBytes (hmacGetDigest h)+      where h = hmac k s :: (CT.HMAC CT.SHA256)++addTmpPayloadHashHeader :: Request -> IO Request+addTmpPayloadHashHeader req = do+  let payloadHash = case HTTP.requestBody req of+        HTTP.RequestBodyBS bs ->+          HEX.encode $ SHA256.hash bs+        HTTP.RequestBodyLBS lbs ->+          HEX.encode $ SHA256.hashlazy lbs+        _ -> error "addTmpPayloadHashHeader: unexpected request body type"+  return $ setHeader tmpPayloadHashHeader payloadHash req++tmpPayloadHashHeader :: CI.CI S.ByteString+tmpPayloadHashHeader = "X-LOCAL-CONTENT-HASH-HEADER-746352"+                       -- 746352 to reduce collision risk++-- Per AWS documentation at:+--   http://docs.aws.amazon.com/general/latest/gr/rande.html+-- For example: "dynamodb.us-east-1.amazonaws.com" -> ("dynamodb", "us-east-1")+serviceAndRegion :: S.ByteString -> (S.ByteString, S.ByteString)+serviceAndRegion endpoint+  -- For s3, use /<bucket> style access, as opposed to+  -- <bucket>.s3... in the hostname.+  | endpoint `elem` ["s3.amazonaws.com", "s3-external-1.amazonaws.com"] =+    ("s3", "us-east-1")+  | servicePrefix '-' endpoint == "s3" =+    -- format: e.g. s3-us-west-2.amazonaws.com+    let region = S.takeWhile (/= '.') $ S.drop 3 endpoint -- drop "s3-"+    in ("s3", region)+    -- not s3+  | svc `HashSet.member` noRegion =+    (svc, "us-east-1")+  | otherwise =+    let service:region:_ = S.split '.' endpoint+    in (service, region)+  where+    svc = servicePrefix '.' endpoint+    servicePrefix c = S.map toLower . S.takeWhile (/= c)+    noRegion = HashSet.fromList ["iam", "sts", "importexport", "route53",+                                 "cloudfront"]++-- If the hostname doesn't end in runscope.net, return the original.+-- For a hostname that includes runscope.net:+-- given  sqs-us--east--1-amazonaws-com-<BUCKET>.runscope.net+-- return sqs.us-east-1.amazonaws.com+removeRunscope :: S.ByteString -> S.ByteString+removeRunscope hostname+  | ".runscope.net" `S.isSuffixOf` hostname =+    S.concat . Prelude.map (p2 . p1) . S.group -- decode+    -- drop suffix "-<BUCKET>.runscope.net" before decoding+    . S.reverse . S.tail . S.dropWhile (/= '-') . S.reverse+    $ hostname+  | otherwise = hostname+    where p1 "-"   = "."+          p1 other = other+          p2 "--"   = "-"+          p2 other  = other
Network/Wreq/Internal/Lens.hs view
@@ -12,6 +12,8 @@     , queryString     , requestHeaders     , requestBody+    , requestVersion+    , onRequestBodyException     , proxy     , hostAddress     , rawBody@@ -21,10 +23,14 @@     , checkStatus     , getConnectionWrapper     , cookieJar+    , seshCookies+    , seshManager+    , seshRun     -- * Useful functions     , assoc     , assoc2     , setHeader+    , deleteKey     ) where  import Control.Lens hiding (makeLenses)@@ -32,10 +38,12 @@ import Network.HTTP.Client (Request) import Network.HTTP.Types (HeaderName) import Network.Wreq.Lens.Machinery (makeLenses)+import Network.Wreq.Internal.Types (Session) import qualified Data.ByteString as S import qualified Network.HTTP.Client as HTTP  makeLenses ''HTTP.Request+makeLenses ''Session  assoc :: (Eq k) => k -> IndexedTraversal' k [(k, a)] a assoc i = traverse . itraversed . index i@@ -48,5 +56,7 @@              _1 (f . fmap snd) . partition ((==k) . fst)  setHeader :: HeaderName -> S.ByteString -> Request -> Request-setHeader name value = requestHeaders %~ ((name,value) :) .-                       filter ((/= name) . fst)+setHeader name value = requestHeaders %~ ((name,value) :) . deleteKey name++deleteKey :: (Eq a) => a -> [(a,b)] -> [(a,b)]+deleteKey key = filter ((/= key) . fst)
Network/Wreq/Internal/Types.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, GADTs, RecordWildCards #-}+{-# LANGUAGE DeriveDataTypeable, DeriveFunctor, FlexibleInstances, GADTs,+    OverloadedStrings, RankNTypes, RecordWildCards #-}  -- | -- Module      : Network.Wreq.Internal.Types@@ -15,7 +16,9 @@     (     -- * Client configuration       Options(..)+    , Mgr     , Auth(..)+    , AWSAuthVersion(..)     -- * Request payloads     , Payload(..)     , Postable(..)@@ -28,24 +31,40 @@     , Link(..)     -- * Errors     , JSONError(..)+    -- * Request types+    , Req(..)+    , reqURL+    -- * Sessions+    , Session(..)+    , Run+    , Body(..)+    -- * Caches+    , CacheEntry(..)     ) where -import Control.Exception (Exception)+import Control.Concurrent.MVar (MVar)+import Control.Exception (Exception, SomeException)+import Data.Monoid ((<>), mconcat) import Data.Text (Text)+import Data.Time.Clock (UTCTime) import Data.Typeable (Typeable) import Network.HTTP.Client (CookieJar, Manager, ManagerSettings, Request,                             RequestBody, destroyCookieJar)-import Network.HTTP.Client.Internal (Proxy)-import Network.HTTP.Types (Header)+import Network.HTTP.Client.Internal (Response, Proxy)+import Network.HTTP.Types (Header, Status, ResponseHeaders) import Prelude hiding (head)-import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Client as HTTP  -- | A MIME content type, e.g. @\"application/octet-stream\"@. type ContentType = S.ByteString +type Mgr = Either ManagerSettings Manager+ -- | Options for configuring a client. data Options = Options {-    manager :: Either ManagerSettings Manager+    manager :: Mgr   -- ^ Either configuration for a 'Manager', or an actual 'Manager'.   --   -- If only 'ManagerSettings' are provided, then by default a new@@ -130,6 +149,13 @@   -- etc.), this field will be used only for the /first/ HTTP request   -- to be issued during a 'Network.Wreq.Session.Session'. Any changes   -- changes made for subsequent requests will be ignored.+  , checkStatus :: +    Maybe (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException)+  -- ^ Function that checks the status code and potentially returns an exception.+  --+  -- This defaults to 'Nothing', which will just use the default of+  -- 'Network.HTTP.Client.Request' which throws a 'StatusException' if the status+  -- is not 2XX.    } deriving (Typeable)  -- | Supported authentication types.@@ -148,8 +174,15 @@             -- to be used only by GitHub). This is treated by whoever             -- accepts it as the equivalent of a username and             -- password.+          | AWSAuth AWSAuthVersion S.ByteString S.ByteString+            -- ^ Amazon Web Services request signing+            -- AWSAuthVersion key secret           deriving (Eq, Show, Typeable) +data AWSAuthVersion = AWSv4+                      -- ^ AWS request signing version 4+                    deriving (Eq, Show)+ instance Show Options where   show (Options{..}) = concat ["Options { "                               , "manager = ", case manager of@@ -218,3 +251,49 @@       linkURL :: S.ByteString     , linkParams :: [(S.ByteString, S.ByteString)]     } deriving (Eq, Show, Typeable)++-- | A request that is ready to be submitted.+data Req = Req Mgr Request++reqURL :: Req -> S.ByteString+reqURL (Req _ req) = mconcat [+    if https then "https" else "http"+  , "://"+  , HTTP.host req+  , case (HTTP.port req, https) of+      (80, False) -> ""+      (443, True) -> ""+      (p, _)      -> S.pack (show p)+  , HTTP.path req+  , case HTTP.queryString req of+      qs | S.null qs -> ""+         | otherwise -> "?" <> qs+  ]+  where https = HTTP.secure req++-- | A function that runs a request and returns the associated+-- response.+type Run body = Req -> IO (Response body)++-- | A session that spans multiple requests.+data Session = Session {+      seshCookies :: MVar CookieJar+    , seshManager :: Manager+    , seshRun :: Session -> Run Body -> Run Body+    }++instance Show Session where+    show _ = "Session"++data CacheEntry body = CacheEntry {+    entryCreated  :: UTCTime+  , entryExpires  :: Maybe UTCTime+  , entryResponse :: Response body+  } deriving (Functor)++data Body = NoBody+          | StringBody L.ByteString+          | ReaderBody HTTP.BodyReader++instance Show (CacheEntry body) where+    show _ = "CacheEntry"
Network/Wreq/Lens.hs view
@@ -45,6 +45,7 @@     , params     , cookie     , cookies+    , checkStatus      -- ** Proxy setup     , Proxy@@ -95,10 +96,13 @@      -- * Parsing     , atto+    , atto_     ) where +import Control.Applicative ((<*))+import Control.Exception (SomeException) import Control.Lens (Fold, Lens, Lens', Traversal', folding)-import Data.Attoparsec (Parser, parseOnly)+import Data.Attoparsec.ByteString (Parser, endOfInput, parseOnly) import Data.ByteString (ByteString) import Data.Text (Text) import Data.Time.Clock (UTCTime)@@ -126,6 +130,20 @@ --'Network.HTTP.Client.OpenSSL.withOpenSSL' $ --  'Network.Wreq.getWith' opts \"https:\/\/httpbin.org\/get\" -- @+--+-- In this example, we also set the response timeout to 10000 microseconds:+--+-- @+--import "OpenSSL.Session" ('OpenSSL.Session.context')+--import "Network.HTTP.Client.OpenSSL"+--import "Network.HTTP.Client" ('Network.HTTP.Client.defaultManagerSettings', 'Network.HTTP.Client.managerResponseTimeout')+--+--let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'manager' 'Control.Lens..~' Left ('Network.HTTP.Client.OpenSSL.opensslManagerSettings' 'OpenSSL.Session.context')+--                    'Control.Lens.&' 'manager' 'Control.Lens..~' Left ('Network.HTTP.Client.defaultManagerSettings' { 'Network.HTTP.Client.managerResponseTimeout' = Just 10000 } )+--+--'Network.HTTP.Client.OpenSSL.withOpenSSL' $+--  'Network.Wreq.getWith' opts \"https:\/\/httpbin.org\/get\"+-- @ manager :: Lens' Options (Either ManagerSettings Manager) manager = TH.manager @@ -134,9 +152,13 @@ -- Example: -- -- @---let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'proxy' 'Control.Lens..~' 'Network.Wreq.httpProxy' \"localhost\" 8000+--let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'proxy' 'Control.Lens.?~' 'Network.Wreq.httpProxy' \"localhost\" 8000 --'Network.Wreq.getWith' opts \"http:\/\/httpbin.org\/get\" -- @+--+-- Note here the use of the 'Control.Lens.?~' setter to turn a 'Proxy'+-- into a 'Maybe' 'Proxy', to make the type of the RHS compatible with+-- the 'Lens.proxy' lens. proxy :: Lens' Options (Maybe Proxy) proxy = TH.proxy @@ -205,6 +227,10 @@ redirects :: Lens' Options Int redirects = TH.redirects +-- | A lens to get the optional status check function+checkStatus :: Lens' Options (Maybe (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException))+checkStatus = TH.checkStatus+ -- | A traversal onto the cookie with the given name, if one exists. cookie :: ByteString -> Traversal' Options Cookie cookie = TH.cookie@@ -418,6 +444,17 @@ -- ["GET","HEAD","OPTIONS"] atto :: Parser a -> Fold ByteString a atto = folding . parseOnly++-- | The same as 'atto', but ensures that the parser consumes the+-- entire input.+--+-- Equivalent to:+--+-- @+--'atto_' myParser = 'atto' (myParser '<*' 'endOfInput')+-- @+atto_ :: Parser a -> Fold ByteString a+atto_ p = atto (p <* endOfInput)  -- $setup --
Network/Wreq/Lens/Machinery.hs view
@@ -13,8 +13,8 @@ defaultRules :: LensRules defaultRules = lensRules -fieldName :: (String -> String) -> [Name] -> Name -> [DefName]-fieldName f _ name = [TopName . mkName . f . nameBase $ name]+fieldName :: (String -> String) -> Name -> [Name] -> Name -> [DefName]+fieldName f _ _ name = [TopName . mkName . f . nameBase $ name]  makeLenses :: Name -> Q [Dec] makeLenses = makeLensesWith (defaultRules & lensField .~ fieldName id)
Network/Wreq/Lens/TH.hs view
@@ -15,6 +15,7 @@     , redirects     , cookie     , cookies+    , checkStatus      , HTTP.Cookie     , cookieName@@ -57,6 +58,7 @@     , partFilename     , partContentType     , partGetBody+    , partHeaders     ) where  import Control.Lens hiding (makeLenses)
Network/Wreq/Session.hs view
@@ -1,9 +1,10 @@-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RankNTypes, RecordWildCards #-}  module Network.Wreq.Session     (       Session     , withSession+    , withSessionWith     -- * HTTP verbs     , get     , post@@ -18,31 +19,33 @@     , optionsWith     , putWith     , deleteWith+    -- * Extending a session+    , Lens.seshRun     ) where -import Control.Concurrent.MVar (MVar, modifyMVar, newMVar)-import Control.Lens ((&), (.~), (^.))-import Network.Wreq (Options, Response, defaults)-import Network.Wreq.Internal (defaultManagerSettings)-import Network.Wreq.Types (Postable, Putable)+import Control.Concurrent.MVar (modifyMVar, newMVar)+import Control.Lens ((&), (?~), (^.))+import Network.Wreq (Options, Response)+import Network.Wreq.Internal+import Network.Wreq.Internal.Types (Body(..), Req(..), Session(..))+import Network.Wreq.Types (Postable, Putable, Run) import Prelude hiding (head) import qualified Data.ByteString.Lazy as L import qualified Network.HTTP.Client as HTTP import qualified Network.Wreq as Wreq--data Session = Session {-      seshCookies :: MVar HTTP.CookieJar-    , seshManager :: HTTP.Manager-    }--instance Show Session where-    show _ = "Session"+import qualified Network.Wreq.Internal.Lens as Lens  withSession :: (Session -> IO a) -> IO a-withSession act = do+withSession = withSessionWith defaultManagerSettings++withSessionWith :: HTTP.ManagerSettings -> (Session -> IO a) -> IO a+withSessionWith settings act = do   mv <- newMVar $ HTTP.createCookieJar []-  HTTP.withManager defaultManagerSettings $ \mgr ->-    act Session { seshCookies = mv, seshManager = mgr }+  HTTP.withManager settings $ \mgr ->+    act Session { seshCookies = mv+                , seshManager = mgr+                , seshRun = runWith+                }  get :: Session -> String -> IO (Response L.ByteString) get = getWith defaults@@ -59,39 +62,44 @@ put :: Putable a => Session -> String -> a -> IO (Response L.ByteString) put = putWith defaults -delete :: Session -> String -> IO (Response ())+delete :: Session -> String -> IO (Response L.ByteString) delete = deleteWith defaults  getWith :: Options -> Session -> String -> IO (Response L.ByteString)-getWith opts sesh url =-  override opts sesh $ \opts' -> Wreq.getWith opts' url+getWith opts sesh url = run string sesh =<< prepareGet opts url  postWith :: Postable a => Options -> Session -> String -> a          -> IO (Response L.ByteString) postWith opts sesh url payload =-  override opts sesh $ \opts' -> Wreq.postWith opts' url payload+  run string sesh =<< preparePost opts url payload  headWith :: Options -> Session -> String -> IO (Response ())-headWith opts sesh url =-  override opts sesh $ \opts' -> Wreq.headWith opts' url+headWith opts sesh url = run ignore sesh =<< prepareHead opts url  optionsWith :: Options -> Session -> String -> IO (Response ())-optionsWith opts sesh url =-  override opts sesh $ \opts' -> Wreq.optionsWith opts' url+optionsWith opts sesh url = run ignore sesh =<< prepareOptions opts url  putWith :: Putable a => Options -> Session -> String -> a         -> IO (Response L.ByteString)-putWith opts sesh url payload =-  override opts sesh $ \opts' -> Wreq.putWith opts' url payload+putWith opts sesh url payload = run string sesh =<< preparePut opts url payload -deleteWith :: Options -> Session -> String -> IO (Response ())-deleteWith opts sesh url =-  override opts sesh $ \opts' -> Wreq.deleteWith opts' url+deleteWith :: Options -> Session -> String -> IO (Response L.ByteString)+deleteWith opts sesh url = run string sesh =<< prepareDelete opts url -override :: Options -> Session -> (Options -> IO (Response body))-         -> IO (Response body)-override opts Session{..} act =+runWith :: Session -> Run Body -> Run Body+runWith Session{..} act (Req _ req) =   modifyMVar seshCookies $ \cj -> do-    resp <- act $ opts & Wreq.cookies .~ cj &-                         Wreq.manager .~ Right seshManager+    resp <- act (Req (Right seshManager) (req & Lens.cookieJar ?~ cj))     return (resp ^. Wreq.responseCookieJar, resp)++type Mapping a = (Body -> a, a -> Body, Run a)++run :: Mapping a -> Session -> Run a+run (to,from,act) sesh =+  fmap (fmap to) . seshRun sesh sesh (fmap (fmap from) . act)++string :: Mapping L.ByteString+string = (\(StringBody s) -> s, StringBody, runRead)++ignore :: Mapping ()+ignore = (\_ -> (), const NoBody, runIgnore)
Network/Wreq/Types.hs view
@@ -17,6 +17,7 @@     -- * Client configuration       Options(..)     , Auth(..)+    , AWSAuthVersion(..)     -- * Request payloads     , Payload(..)     , Postable(..)@@ -29,6 +30,10 @@     , Link(..)     -- * Errors     , JSONError(..)+    -- * Request handling+    , Req+    , reqURL+    , Run     ) where  import Control.Lens ((&), (.~))@@ -46,6 +51,7 @@ import qualified Data.Text.Lazy.Builder as TL import qualified Network.HTTP.Client as HTTP import qualified Network.Wreq.Internal.Lens as Lens+import qualified Network.Wreq.Internal.AWS as AWS (addTmpPayloadHashHeader)  instance Postable Part where     postPayload a = postPayload [a]@@ -134,6 +140,7 @@     renderFormValue (Just a) = renderFormValue a     renderFormValue Nothing  = "" -payload :: ContentType -> HTTP.RequestBody -> Request -> IO Request-payload ct body req = return $ req & Lens.setHeader "Content-Type" ct &-                      Lens.requestBody .~ body+payload :: S.ByteString -> HTTP.RequestBody -> Request -> IO Request+payload ct body req = AWS.addTmpPayloadHashHeader $ req+                    & Lens.setHeader "Content-Type" ct+                    & Lens.requestBody .~ body
README.md view
@@ -3,7 +3,6 @@ `wreq` is a library that makes HTTP client programming in Haskell easy. - # Features  * Simple but powerful `lens`-based API@@ -23,6 +22,14 @@  * Basic and OAuth2 bearer authentication +* Amazon Web Services (AWS) request signing (Version 4)++* AWS signing supports sending requests through the+  [Runscope Inc.](https://www.runscope.com) Traffic Inspector++# Tutorials++See [the tutorials](http://www.serpentine.com/wreq/) for a quick-start.  # Is it done? 
TODO.md view
@@ -17,3 +17,5 @@  * Some poor sod needs to add digest authentication to `http-client` so   we can use it++* Cache management
changelog view
@@ -1,5 +1,15 @@ -*- markdown -*- +2014-12-02 0.3.0.0++* Support for Amazon Web Services request signing++* New customMethod, customMethodWith functions allow use of arbitrary+  HTTP verbs++* httpProxy, basicAuth, oauth2Bearer, oauth2Token: removed Maybe from+  result types, changed documentation to suggest use of (?~)+ 2014-08-25 0.2.0.0  * Support for lens 4.4
examples/UploadPaste.hs view
@@ -19,7 +19,8 @@ import Data.Monoid (mempty) import Network.Wreq (FormParam((:=)), post, responseBody) import Network.Wreq.Types (FormValue(..))-import Options.Applicative as Opts hiding ((&), header)+import Options.Applicative as Opts+import Options.Applicative.Types (readerAsk) import System.FilePath (takeExtension, takeFileName) import Text.HTML.TagSoup import qualified Data.ByteString.Char8 as B@@ -97,8 +98,9 @@  -- Try to match a user-supplied name to a Language type, looking at -- both full names and filename extensions.-readLanguage :: Monad m => String -> m Language-readLanguage l = do+readLanguage :: ReadM Language+readLanguage = do+  l <- readerAsk   let ll = toLower <$> l       ms = [lang | (suffixes, lang) <- languages,             ll == (toLower <$> show lang) || ll `elem` (tail <$> suffixes)]@@ -158,9 +160,9 @@           (optional . fmap Channel . strOption $            long "channel" <> short 'c' <> metavar "CHANNEL" <>            help "name of IRC channel to announce") <*>-          (optional . nullOption $+          (optional . option readLanguage $            long "language" <> short 'l' <> metavar "LANG" <>-           help "language to use" <> reader readLanguage) <*>+           help "language to use") <*>           (Opts.argument str $ metavar "PATH" <>                                help "file to upload") <*>           (pure ())
examples/wreq-examples.cabal view
@@ -32,13 +32,14 @@   default-language: Haskell98    build-depends:-    aeson >= 0.7.0.3,+    aeson,     ansi-wl-pprint >= 0.6.6,     base >= 4.5 && < 5,     bytestring,     filepath,     lens,-    optparse-applicative,+    optparse-applicative >= 0.11,+    mtl,     tagsoup,     text,     wreq
httpbin/HttpBin.hs view
@@ -1,124 +1,7 @@-{-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS_GHC -fno-warn-missing-signatures #-}---- TBD: basic-auth, gzip- module Main (main) where -import Control.Applicative ((<$>))-import Data.Aeson (Value(..), eitherDecode, object, toJSON)-import Data.Aeson.Encode.Pretty (Config(..), encodePretty')-import qualified Data.ByteString.Base64 as B64-import Data.ByteString.Char8 (pack)-import Data.CaseInsensitive (original)-import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))-import Data.Text.Encoding (decodeUtf8)-import Data.Text.Read (decimal)-import Snap.Core-import Snap.Http.Server-import Snap.Util.GZip (withCompression)-import qualified Data.ByteString.Char8 as B-import qualified Data.Map as Map-import qualified Data.Text.Lazy.Encoding as Lazy--get = respond return--post = respond $ \obj -> do-  body <- readRequestBody 65536-  return $ obj <> [("data", toJSON (Lazy.decodeUtf8 body))] <>-           case eitherDecode body of-             Left _    -> [("json", Null)]-             Right val -> [("json", val)]--put = post--delete = respond return--status = do-  val <- (fromMaybe 200 . rqIntParam "val") <$> getRequest-  let code | val >= 200 && val <= 505 = val-           | otherwise                = 400-  modifyResponse $ setResponseCode code--gzip =-  localRequest (setHeader "Accept-Encoding" "gzip") . withCompression .-  respond $ \obj -> return $ obj <> [("gzipped", Bool True)]--setCookies = do-  params <- rqQueryParams <$> getRequest-  modifyResponse . foldr (.) id . map addResponseCookie $-    [Cookie k v Nothing Nothing (Just "/") False False-     | (k,vs) <- Map.toList params, v <- vs]-  redirect "/cookies"--redirect_ = do-  req <- getRequest-  let n   = fromMaybe (-1::Int) . rqIntParam "n" $ req-      prefix = B.reverse . B.dropWhile (/='/') . B.reverse . rqURI $ req-  case undefined of-    _| n > 1     -> redirect $ prefix <> pack (show (n-1))-     | n == 1    -> redirect "/get"-     | otherwise -> modifyResponse $ setResponseCode 400--basicAuth = do-  req <- getRequest-  let unauthorized = modifyResponse $-                     setHeader "WWW-Authenticate" "Basic realm=\"Fake Realm\"" .-                     setResponseCode 401-  case (rqParam "user" req, rqParam "pass" req) of-    (Just [user], Just [passwd]) | not (':' `B.elem` user) ->-      case getHeader "Authorization" (headers req) of-        Nothing -> unauthorized-        Just auth -> do-          let expected = "Basic " <> B64.encode (user <> ":" <> passwd)-          if auth /= expected-            then unauthorized-            else writeJSON [ ("user", toJSON (B.unpack user))-                           , ("authenticated", Bool True) ]-    _ -> modifyResponse $ setResponseCode 400--rqIntParam name req =-  case rqParam name req of-    Just (str:_) -> case decimal (decodeUtf8 str) of-                      Right (n, "") -> Just n-                      _             -> Nothing-    _            -> Nothing--writeJSON obj = do-  modifyResponse $ setContentType "application/json"-  writeLBS . (<> "\n") . encodePretty' (Config 2 compare) . object $ obj--respond act = do-  req <- getRequest-  let step m k v = Map.insert (decodeUtf8 k) (decodeUtf8 (head v)) m-      params = Map.foldlWithKey' step Map.empty .-               rqQueryParams $ req-      wibble (k,v) = (decodeUtf8 (original k), decodeUtf8 v)-      rqHeaders = headers req-      hdrs = Map.fromList . map wibble . listHeaders $ rqHeaders-      url = case getHeader "Host" rqHeaders of-              Nothing   -> []-              Just host -> [("url", toJSON . decodeUtf8 $-                                    "http://" <> host <> rqURI req)]-  writeJSON =<< act ([ ("args", toJSON params)-                     , ("headers", toJSON hdrs)-                     , ("origin", toJSON . decodeUtf8 . rqRemoteAddr $ req)-                     ] <> url)+import HttpBin.Server (serve)+import Snap.Http.Server.Config (commandLineConfig) -main = do-  cfg <- commandLineConfig-       . setAccessLog ConfigNoLog-       . setErrorLog ConfigNoLog-       $ defaultConfig-  httpServe cfg $ route [-      ("/get", methods [GET,HEAD] get)-    , ("/post", method POST post)-    , ("/put", method PUT put)-    , ("/delete", method DELETE delete)-    , ("/redirect/:n", redirect_)-    , ("/status/:val", status)-    , ("/gzip", methods [GET,HEAD] gzip)-    , ("/cookies/set", methods [GET,HEAD] setCookies)-    , ("/basic-auth/:user/:pass", methods [GET,HEAD] basicAuth)-    ]+main :: IO ()+main = serve commandLineConfig
+ httpbin/HttpBin/Server.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}++-- TBD: basic-auth, gzip++module HttpBin.Server (serve) where++import Control.Applicative ((<$>))+import Control.Monad.IO.Class (liftIO)+import Data.Aeson (Value(..), eitherDecode, object, toJSON)+import Data.Aeson.Encode.Pretty (Config(..), encodePretty')+import Data.ByteString.Char8 (pack)+import Data.CaseInsensitive (original)+import Data.Maybe (catMaybes, fromMaybe)+import Data.Monoid ((<>))+import Data.Text.Encoding (decodeUtf8)+import Data.Text.Read (decimal)+import Data.UUID (toASCIIBytes)+import Data.UUID.V4 (nextRandom)+import Snap.Core+import Snap.Http.Server as Snap+import Snap.Util.GZip (withCompression)+import System.PosixCompat.Time (epochTime)+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Char8 as B+import qualified Data.Map as Map+import qualified Data.Text.Lazy.Encoding as Lazy++get = respond return++post = respond $ \obj -> do+  body <- readRequestBody 65536+  return $ obj <> [("data", toJSON (Lazy.decodeUtf8 body))] <>+           case eitherDecode body of+             Left _    -> [("json", Null)]+             Right val -> [("json", val)]++put = post++delete = respond return++status = do+  val <- (fromMaybe 200 . rqIntParam "val") <$> getRequest+  let code | val >= 200 && val <= 505 = val+           | otherwise                = 400+  modifyResponse $ setResponseCode code++gzip =+  localRequest (setHeader "Accept-Encoding" "gzip") . withCompression .+  respond $ \obj -> return $ obj <> [("gzipped", Bool True)]++setCookies = do+  params <- rqQueryParams <$> getRequest+  modifyResponse . foldr (.) id . map addResponseCookie $+    [Cookie k v Nothing Nothing (Just "/") False False+     | (k,vs) <- Map.toList params, v <- vs]+  redirect "/cookies"++listCookies = do+  cks <- rqCookies <$> getRequest+  let cs = [(decodeUtf8 (cookieName c),+             toJSON (decodeUtf8 (cookieValue c))) | c <- cks]+  respond $ \obj -> return $ obj <> [("cookies", object cs)]++redirect_ = do+  req <- getRequest+  let n   = fromMaybe (-1::Int) . rqIntParam "n" $ req+      prefix = B.reverse . B.dropWhile (/='/') . B.reverse . rqURI $ req+  case undefined of+    _| n > 1     -> redirect $ prefix <> pack (show (n-1))+     | n == 1    -> redirect "/get"+     | otherwise -> modifyResponse $ setResponseCode 400++unauthorized = modifyResponse $+               setHeader "WWW-Authenticate" "Basic realm=\"Fake Realm\"" .+               setResponseCode 401++simpleAuth expect = do+  req <- getRequest+  case expect req of+    Nothing -> modifyResponse $ setResponseCode 400+    Just (expected, resp) ->+      case getHeader "Authorization" (headers req) of+        Nothing -> unauthorized+        Just auth | auth == expected -> writeJSON $+                                        resp <> [("authenticated", Bool True)]+                  | otherwise -> unauthorized++basicAuth = simpleAuth $ \req ->+  case (rqParam "user" req, rqParam "pass" req) of+    (Just [user], Just [passwd]) | not (':' `B.elem` user) ->+      Just ("Basic " <> B64.encode (user <> ":" <> passwd),+            [("user", toJSON (B.unpack user))])+    _ -> Nothing++oauth2token = simpleAuth $ \req ->+  case (rqParam "kind" req, rqParam "token" req) of+    (Just [kind], Just [token]) ->+      Just (kind <> " " <> token,+            [("token", toJSON (B.unpack token))])+    _ -> Nothing++cache = do+  hdrs <- headers <$> getRequest+  let cond = not . null . catMaybes . map (flip getHeader hdrs) $+             ["If-Modified-Since", "If-None-Match"]+  if cond+    then modifyResponse $ setResponseCode 304+    else do+      now <- liftIO $ formatHttpTime =<< epochTime+      uuid <- liftIO nextRandom+      modifyResponse $ setHeader "Last-Modified" now .+                       setHeader "ETag" (toASCIIBytes uuid)+      respond return++rqIntParam name req =+  case rqParam name req of+    Just (str:_) -> case decimal (decodeUtf8 str) of+                      Right (n, "") -> Just n+                      _             -> Nothing+    _            -> Nothing++writeJSON obj = do+  modifyResponse $ setContentType "application/json"+  writeLBS . (<> "\n") . encodePretty' (Config 2 compare) . object $ obj++respond act = do+  req <- getRequest+  let step m k v = Map.insert (decodeUtf8 k) (decodeUtf8 (head v)) m+      params = Map.foldlWithKey' step Map.empty .+               rqQueryParams $ req+      wibble (k,v) = (decodeUtf8 (original k), decodeUtf8 v)+      rqHeaders = headers req+      hdrs = Map.fromList . map wibble . listHeaders $ rqHeaders+      url = case getHeader "Host" rqHeaders of+              Nothing   -> []+              Just host -> [("url", toJSON . decodeUtf8 $+                                    "http://" <> host <> rqURI req)]+  writeJSON =<< act ([ ("args", toJSON params)+                     , ("headers", toJSON hdrs)+                     , ("origin", toJSON . decodeUtf8 . rqRemoteAddr $ req)+                     ] <> url)++serve mkConfig = do+  cfg <- mkConfig+       . setAccessLog ConfigNoLog+       . setErrorLog ConfigNoLog+       $ defaultConfig+  httpServe cfg $ route [+      ("/get", methods [GET,HEAD] get)+    , ("/post", method POST post)+    , ("/put", method PUT put)+    , ("/delete", method DELETE delete)+    , ("/redirect/:n", redirect_)+    , ("/status/:val", status)+    , ("/gzip", methods [GET,HEAD] gzip)+    , ("/cookies/set", methods [GET,HEAD] setCookies)+    , ("/cookies", methods [GET,HEAD] listCookies)+    , ("/basic-auth/:user/:pass", methods [GET,HEAD] basicAuth)+    , ("/oauth2/:kind/:token", methods [GET,HEAD] oauth2token)+    , ("/cache", methods [GET,HEAD] cache)+    ]
+ tests/AWS.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE ScopedTypeVariables #-}++{-+A set of end to end Amazon Web Services (AWS) tests to make sure we+can access a number of AWS services that use various AWS request+formats.++These tests help us guard against errors we may otherwise introduce+while refactoring or extending Wreq. The tests are not meant to+exercise the features of the respective AWS services exhaustively.++** ASSUMPTIONS **+To configure and run these tests you need an AWS account. We assume+that you are familiar with AWS concepts and the charging model.++** ENABLING AWS TESTS **+For now, enable AWS tests by setting the WREQ_AWS_ACCESS_KEY_ID+env variable per below.++TODO| To enable AWS tests use the `-faws` flag as part of+TODO|   $ cabal configure --enable-tests -faws ...+TODO| To capture code coverage information, add the `-fdeveloper` flag.++** REQUIRED CLIENT CONFIGURATION **+The tests require two environment variables:+   $ /bin/env WREQ_AWS_ACCESS_KEY_ID='...' \+              WREQ_AWS_SECRET_ACCESS_KEY='...' \+       cabal test++** CHARGES/COST **+These tests may incur small amounts of AWS charges for the minimum+DynamoDB IOs per second they provision and for the messages sent to+AWS SQS and objects stored in S3. These charges consume only a tiny+fraction of the AWS free tier allowance (if not used up otherwise).++** AWS REGIONS **+Tests are executed against the AWS Region `us-west-2` by default. You+can change the region by setting the AWS_REGION environment variable+(e.g. /bin/env WREQ_AWS_REGION=eu-west-1 cabal test).++In the case of S3, we translate 'us-east-1' to+'s3-external-1.amazonaws.com' denoting the Virginia (only) endpoint.+(see http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).++** AWS USER AND LEAST PRIVILEDGE POLICY **+The file `tests/AWS/policy.json` contains the least priviledge "AWS+Identity and Access (IAM)" policy sufficient to run these tests. It is+a best security practice to run the tests using an AWS IAM user you+created specifically for this purpose. Use the AWS IAM Management+Console to create a new user, get the WREQ_AWS_ACCESS_KEY and+WREQ_AWS_SECRET_KEY for that user and apply the policy to the user to+limit its priviledges.++**AVOID AWS RESOURCE NAME COLLISIONS IN CONCURRENT TESTS**+To run tests concurrently in same AWS account, set the environment+variable WREQ_AWS_TEST_PREFIX to a unique string for each test client+or machine. The default prefix used for all resources created+(e.g. DynamoDB tables, SQS queues, S3 buckets, etc.) is+`deleteWreqTest`.+-}++module AWS (tests) where++import Control.Exception as E (IOException, catch)+import Control.Lens+import Data.ByteString.Char8 as BS8 (pack)+import Data.IORef (newIORef)+import Network.Info (getNetworkInterfaces, mac)+import Network.Wreq+import System.Environment (getEnv)+import Test.Framework (Test, testGroup)+import qualified AWS.DynamoDB (tests)+import qualified AWS.IAM (tests)+import qualified AWS.S3 (tests)+import qualified AWS.SQS (tests)++tests :: IO Test+tests = do+  -- TODO - use ... configure -faws ... in the future+  --        but couldn't figure out (yet) how to get+  --        a hold of the flag value in test code.+  -- Workaround: for now, the presence of the+  --   WREQ_AWS_ACCESS_KEY_ID+  -- env variable enables the tests.+  flag <- (getEnv "WREQ_AWS_ACCESS_KEY_ID" >> return True) `E.catch`+          \(_::IOException) -> return False+  tests0 flag++tests0 :: Bool -> IO Test+tests0 False =+  return $ testGroup "aws" [] -- skip AWS tests+tests0 True = do+  region <- env "us-west-2" "WREQ_AWS_REGION"+  key <- BS8.pack `fmap` getEnv "WREQ_AWS_ACCESS_KEY_ID"+  secret <- BS8.pack `fmap` getEnv "WREQ_AWS_SECRET_ACCESS_KEY"+  let baseopts = defaults & auth ?~ awsAuth AWSv4 key secret+  prefix <- env "deleteWreqTest" "WREQ_AWS_TEST_PREFIX"+  sqsTestState <- newIORef "missing"+  uniq <- uniqueMachineId+  return $ testGroup "aws" [+      AWS.DynamoDB.tests (prefix ++ "DynamoDB") region baseopts+    , AWS.IAM.tests (prefix ++ "IAM") region baseopts+    , AWS.SQS.tests (prefix ++ "SQS") region baseopts sqsTestState+      -- S3 buckets are global entities and the namespace shared among+      -- all AWS customers. We will use a unique id based on the MAC+      -- address of our client to avoid naming conflicts among different+      -- developers running the tests.+    , AWS.S3.tests (prefix ++ "S3" ++ uniq) region baseopts+    ]++-- return a globally unique machine id (uses a MAC address)+uniqueMachineId :: IO String+uniqueMachineId = do+  l <- (filter $ (/=) "00:00:00:00:00:00" . show . mac) `fmap`+         getNetworkInterfaces+  return $ concatMap (\c -> if c == ':' then [] else [c])+         . show+         . mac+         . head $ l++env :: String -> String -> IO String+env defVal name = getEnv name `E.catch` \(_::IOException) -> return defVal
+ tests/AWS/DynamoDB.hs view
@@ -0,0 +1,169 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module AWS.DynamoDB (tests) where++import Control.Concurrent (threadDelay)+import Control.Lens+import Data.Aeson.Lens (key, _String, values, _Double)+import Data.Aeson.QQ+import Data.Text as T (pack)+import Network.Wreq+import System.Timeout (timeout)+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool, assertFailure)++-- FIXME: retry create call in case the table is in DELETING state+-- from a previous test run (error 'Table already exists: ...'). For+-- now 'create' testcase and all others will fails. Rerun when ongoing+-- delete operation is complete.++tests :: String -> String -> Options -> Test+tests prefix region baseopts = testGroup "dynamodb" [+    testCase "createTable" $ createTable prefix region baseopts+  , testCase "listTables" $ listTables prefix region baseopts+  , testCase "awaitTableActive"  $ awaitTableActive prefix region baseopts+  , testCase "putItem" $ putItem prefix region baseopts+  , testCase "getItem" $ getItem prefix region baseopts+  , testCase "deleteItem" $ deleteItem prefix region baseopts+  , testCase "deleteTable" $ deleteTable prefix region baseopts -- call last+  ]++createTable :: String -> String -> Options -> IO ()+createTable prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.CreateTable"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  r <- postWith opts (url region) $+         [aesonQQ| {+             "TableName": #{prefix ++ tablename},+             "KeySchema": [+               { "AttributeName": "name", "KeyType": "HASH" },+               { "AttributeName": "age", "KeyType": "RANGE" }+             ],+             "AttributeDefinitions": [+               { "AttributeName": "name", "AttributeType": "S" },+               { "AttributeName": "age", "AttributeType": "S" }+             ],+             "ProvisionedThroughput": {+               "ReadCapacityUnits": 1,+               "WriteCapacityUnits": 1+             }+         } |]+  assertBool "createTables 200" $ r ^. responseStatus . statusCode == 200+  assertBool "createTables OK" $ r ^. responseStatus . statusMessage == "OK"+  assertBool "createTables status CREATING" $+    r ^. responseBody . key "TableDescription" . key "TableStatus" . _String == "CREATING"+  assertBool "createTables no items in new table" $+    r ^? responseBody . key "TableDescription" . key "ItemCount" . _Double == Just 0++listTables :: String -> String -> Options -> IO ()+listTables prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.ListTables"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  -- FIXME avoid limit to keep tests from failing if there are > tables?+  r <- postWith opts (url region) [aesonQQ| { "Limit": 100 } |]+  assertBool "listTables 200" $ r ^. responseStatus . statusCode == 200+  assertBool "listTables OK" $ r ^. responseStatus . statusMessage == "OK"+  assertBool "listTables contains test table" $+    elem (T.pack $ prefix ++ tablename)+         (r ^.. responseBody . key "TableNames" . values . _String)++awaitTableActive :: String -> String -> Options -> IO ()+awaitTableActive prefix region baseopts = do+  let dur = 45 -- typically ACTIVE in 20s or less (us-west-2, Sept 2014)+  res <- timeout (dur*1000*1000) check+  case res of+    Nothing ->+      assertFailure $ "timeout: table not ACTIVE after " ++ show dur ++ "s"+    Just () ->+      return () -- PASS+  where+    check = do+      let opts = baseopts+                 & header "X-Amz-Target" .~ ["DynamoDB_20120810.DescribeTable"]+                 & header "Content-Type" .~ ["application/x-amz-json-1.0"]+      r <- postWith opts (url region)+             [aesonQQ| { "TableName": #{prefix ++ tablename} } |]+      assertBool "awaitTableActive 200" $ r ^. responseStatus . statusCode == 200+      assertBool "awaitTableActive OK" $ r ^. responseStatus . statusMessage == "OK"+      -- Prelude.putStr "."+      case r ^. responseBody . key "Table" . key "TableStatus" . _String of+        "ACTIVE" ->+          return ()+        _ -> do+          threadDelay $ 5*1000*1000 -- 5 sleep+          check++deleteTable :: String -> String -> Options -> IO ()+deleteTable prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.DeleteTable"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  r <- postWith opts (url region) $+         [aesonQQ| { "TableName": #{prefix ++ tablename} } |]+  assertBool "deleteTable 200" $ r ^. responseStatus . statusCode == 200+  assertBool "deleteTable OK" $ r ^. responseStatus . statusMessage == "OK"++putItem :: String -> String -> Options -> IO ()+putItem prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.PutItem"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  r <- postWith opts (url region) $+         [aesonQQ| {+           "TableName": #{prefix ++ tablename},+           "Item": {+             "name": { "S": "someone" },+             "age": {"S": "whatever"},+             "bar": {"S": "baz"}+           }+         } |]+  assertBool "putItem 200" $ r ^. responseStatus . statusCode == 200+  assertBool "putItem OK" $ r ^. responseStatus . statusMessage == "OK"++getItem :: String -> String -> Options -> IO ()+getItem prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.GetItem"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  r <- postWith opts (url region) $+         [aesonQQ| {+           "TableName": #{prefix ++ tablename},+           "Key": {+             "name": { "S": "someone" },+             "age": {"S": "whatever"}+           },+           "AttributesToGet": [ "bar" ],+           "ConsistentRead": true,+           "ReturnConsumedCapacity": "TOTAL"+         } |]+  assertBool "getItem 200" $ r ^. responseStatus . statusCode == 200+  assertBool "getItem OK" $ r ^. responseStatus . statusMessage == "OK"+  assertBool "getItem baz value is bar" $+    r ^. responseBody . key "Item" . key "bar" . key "S" . _String == "baz"++deleteItem :: String -> String -> Options -> IO ()+deleteItem prefix region baseopts = do+  let opts = baseopts+             & header "X-Amz-Target" .~ ["DynamoDB_20120810.DeleteItem"]+             & header "Content-Type" .~ ["application/x-amz-json-1.0"]+  r <- postWith opts (url region) $+         [aesonQQ| {+           "TableName": #{prefix ++ tablename},+           "Key": {+             "name": { "S": "someone" },+             "age": {"S": "whatever"}+           },+           "ReturnValues": "ALL_OLD"+         } |]+  assertBool "getItem 200" $ r ^. responseStatus . statusCode == 200+  assertBool "getItem OK" $ r ^. responseStatus . statusMessage == "OK"++url :: String -> String+url region =+  "https://dynamodb." ++ region ++ ".amazonaws.com/"++tablename :: String+tablename =+  "test"
+ tests/AWS/IAM.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings #-}+module AWS.IAM (tests) where++import Control.Lens+import Network.Wreq+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool)++tests :: String -> String -> Options -> Test+tests prefix region baseopts = testGroup "iam" [+    testCase "listUsers"  $ listUsers prefix region baseopts+  ]++listUsers :: String -> String -> Options -> IO ()+listUsers _prefix region baseopts = do+  let opts = baseopts+             & param  "Action"  .~ ["ListUsers"]+             & param  "Version" .~ ["2010-05-08"]+             & header "Accept"  .~ ["application/json"]+  r <- getWith opts (url region)+  assertBool "listUsers 200" $ r ^. responseStatus . statusCode == 200+  assertBool "listUsers OK" $ r ^. responseStatus . statusMessage == "OK"++url :: String -> String+url _ =+  "https://iam.amazonaws.com/" -- not region specific
+ tests/AWS/S3.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module AWS.S3 (tests) where++import Control.Lens+import Data.Aeson.QQ+import Data.Char (toLower)+import Data.Monoid ((<>))+import Network.Wreq+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool)+import qualified Data.ByteString.Char8 as BS8 (ByteString, pack)++-- FIXME: retry create call in case we get the S3 specific "A+-- conflicting conditional operation is currently in progress against+-- this resource. Please try again." error from a previous test run+-- that is still deleting the test bucket. For now the 'create'+-- testcase and all others will fails.++tests :: String -> String -> Options -> Test+tests prefix region baseopts = let+  lowerPrefix = map toLower prefix+  in testGroup "s3" [+        testCase "createBucket" $ createBucket lowerPrefix region baseopts+      , testCase "putObjectJSON" $ putObjectJSON lowerPrefix region baseopts+      , testCase "getObjectJSON" $ getObjectJSON lowerPrefix region baseopts+      , testCase "deleteObjectJSON" $ deleteObjectJSON lowerPrefix region baseopts+      , testCase "deleteBucket" $ deleteBucket lowerPrefix region baseopts -- call last+      ]++createBucket :: String -> String -> Options -> IO ()+createBucket prefix region baseopts = do+  r <- putWith baseopts (url region ++ prefix ++ "testbucket") $+         locationConstraint region+  assertBool "createBucket 200" $ r ^. responseStatus . statusCode == 200+  assertBool "createBucket OK" $ r ^. responseStatus . statusMessage == "OK"++deleteBucket :: String -> String -> Options -> IO ()+deleteBucket prefix region baseopts = do+  r <- deleteWith baseopts (url region ++ prefix ++ "testbucket")+  assertBool "deleteBucket 204 - no content" $+    r ^. responseStatus . statusCode == 204+  assertBool "deleteBucket OK" $+    r ^. responseStatus . statusMessage == "No Content"++putObjectJSON :: String -> String -> Options -> IO ()+putObjectJSON prefix region baseopts = do+  -- S3 write object, incl. correct content-type, uses /bucket/object syntax+  r <- putWith baseopts (url region ++ prefix ++ "testbucket/blabla-json") $+         [aesonQQ| { "test": "key", "testdata": [ 1, 2, 3 ] } |]+  assertBool "putObjectJSON 200" $ r ^. responseStatus . statusCode == 200+  assertBool "putObjectJSON OK" $ r ^. responseStatus . statusMessage == "OK"++getObjectJSON :: String -> String -> Options -> IO ()+getObjectJSON prefix region baseopts = do+  r <- getWith baseopts (url region ++ prefix ++ "testbucket/blabla-json")+  assertBool "getObjectJSON 200" $ r ^. responseStatus . statusCode == 200+  assertBool "getObjectJSON OK" $ r ^. responseStatus . statusMessage == "OK"++deleteObjectJSON :: String -> String -> Options -> IO ()+deleteObjectJSON prefix region baseopts = do+  r <- deleteWith baseopts (url region ++ prefix ++ "testbucket/blabla-json")+  assertBool "deleteObjectJSON 204 - no content" $+    r ^. responseStatus . statusCode == 204+  assertBool "deleteObjectJSON OK" $+    r ^. responseStatus . statusMessage == "No Content"++-- see http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region+url :: String -> String+url "us-east-1" = "https://s3.amazonaws.com/" -- uses 'classic'+url region      = "https://s3-" ++ region ++ ".amazonaws.com/"++-- see http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html+locationConstraint :: String -> BS8.ByteString+locationConstraint "us-east-1" = "" -- no loc needed for classic and Virginia+locationConstraint region = "<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><LocationConstraint>" <> BS8.pack region <> "</LocationConstraint></CreateBucketConfiguration>"
+ tests/AWS/SQS.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE OverloadedStrings #-}+module AWS.SQS (tests) where++import Control.Lens+import Data.Aeson.Lens (key, _String, values)+import Data.IORef (IORef, readIORef, writeIORef)+import Data.Text as T (Text, pack, unpack, split)+import Network.Wreq+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool)++-- FIXME: retry create call in case we get the SQS specific "wait 1+-- min after delete" error from a previous test run. For now the+-- 'create' testcase and all others will fails. Rerun after awaiting+-- the SQS 1 min window.++tests :: String -> String -> Options -> IORef String -> Test+tests prefix region baseopts sqsTestState = testGroup "sqs" [+    testCase "createQueue" $ createQueue prefix region baseopts sqsTestState+  , testCase "listQueues"  $ listQueues prefix region baseopts+  , testCase "sendMessage" $ sendMessage prefix region baseopts sqsTestState+  , testCase "receiveMessage" $ receiveMessage prefix region baseopts sqsTestState+  , testCase "deleteQueue" $ deleteQueue prefix region baseopts sqsTestState+  ]++createQueue :: String -> String -> Options -> IORef String -> IO ()+createQueue prefix region baseopts sqsTestState = do+  let opts = baseopts+             & param  "Action" .~ ["CreateQueue"]+             & param  "QueueName" .~ [T.pack $ prefix ++ queuename]+             & param  "Version" .~ ["2009-02-01"]+             & header "Accept" .~ ["application/json"]+  r <- getWith opts (url region)+  assertBool "listQueues 200" $ r ^. responseStatus . statusCode == 200+  assertBool "listQueues OK" $ r ^. responseStatus . statusMessage == "OK"+  let qurl = r ^. responseBody . key "CreateQueueResponse"+                              . key "CreateQueueResult"+                              . key "QueueUrl"+                              . _String+  writeIORef sqsTestState $ acctFromQueueUrl qurl++listQueues :: String -> String -> Options -> IO ()+listQueues prefix region baseopts = do+  let opts = baseopts+             & param  "Action"  .~ ["ListQueues"]+             & param  "Version" .~ ["2009-02-01"]+             & header "Accept"  .~ ["application/json"]+  r <- getWith opts (url region)+  assertBool "listQueues 200" $ r ^. responseStatus . statusCode == 200+  assertBool "listQueues OK" $ r ^. responseStatus . statusMessage == "OK"+  let qurls = r ^.. responseBody . key "ListQueuesResponse" .+                                   key "ListQueuesResult" .+                                   key "queueUrls" .+                                   values . _String+  -- url of form: https://sqs.<region>.amazon.com/<acct>/<queuename>+  let qurls' = map (T.unpack . last . T.split (=='/')) qurls+  assertBool "listQueues contains test queue" $+    elem (prefix ++ queuename) qurls'++deleteQueue :: String -> String -> Options -> IORef String -> IO ()+deleteQueue prefix region baseopts sqsTestState = do+  acct <- readIORef sqsTestState+  let opts = baseopts+             & param "Action" .~ ["DeleteQueue"]+             & param "Version" .~ ["2009-02-01"]+             & header "Accept" .~ ["application/json"]+  r <- getWith opts (url region ++ acct ++ "/" ++ prefix ++ queuename)+  assertBool "deleteQueues 200" $ r ^. responseStatus . statusCode == 200+  assertBool "deleteQueues OK" $ r ^. responseStatus . statusMessage == "OK"++sendMessage :: String -> String -> Options -> IORef String -> IO ()+sendMessage prefix region baseopts sqsTestState = do+  acct <- readIORef sqsTestState+  let opts = baseopts+             & param "Action" .~ ["SendMessage"]+             & param "Version" .~ ["2012-11-05"]+             & param "MessageBody" .~ ["uffda"]+             & header "Accept" .~ ["application/json"]+  r <- getWith opts (url region ++ acct ++ "/" ++ prefix ++ queuename)+  assertBool "sendMessage 200" $ r ^. responseStatus . statusCode == 200+  assertBool "sendMessage OK" $ r ^. responseStatus . statusMessage == "OK"++receiveMessage :: String -> String -> Options -> IORef String -> IO ()+receiveMessage prefix region baseopts sqsTestState = do+  acct <- readIORef sqsTestState+  let opts = baseopts+             & param "Action" .~ ["ReceiveMessage"]+             & param "Version" .~ ["2009-02-01"]+             & header "Accept" .~ ["application/json"]+  r <- getWith opts (url region ++ acct ++ "/" ++ prefix ++ queuename)+  let [msg] = map T.unpack $ r ^.. responseBody . -- we sent only 1 message+                key "ReceiveMessageResponse" .+                key "ReceiveMessageResult" .+                key "messages" .+                values .+                key "Body" .+                _String+  assertBool "receiveMessage 200" $ r ^. responseStatus . statusCode == 200+  assertBool "receiveMessage OK" $ r ^. responseStatus . statusMessage == "OK"+  assertBool "receiveMessage match content" $ msg == "uffda"++url :: String -> String+url region =+  "https://sqs." ++ region ++ ".amazonaws.com/"++queuename :: String+queuename =+  "test"++-- url of form: https://sqs.<region>.amazon.com/<acct>/<queuename>+acctFromQueueUrl :: T.Text -> String+acctFromQueueUrl qurl =+  case T.split (=='/') qurl of+    _:_:_:acct:_ ->+      T.unpack acct+    _ ->+      "dummy"
+ tests/Properties/Store.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE RecordWildCards #-}++module Properties.Store+    (+      tests+    ) where++import Data.Functor ((<$>))+import Data.Hashable (Hashable)+import Data.List (foldl', sort, sortBy)+import Data.Maybe (listToMaybe)+import Data.Ord (comparing)+import Network.Wreq.Cache.Store as S+import Test.Framework (Test)+import Test.Framework.Providers.QuickCheck2+import Test.QuickCheck (Positive(..), Small(..))++data StoreModel k v = StoreModel {+    smCap :: Int+  , smGen :: Int+  , smSize :: Int+  , smList :: [(k,v,Int)]+  } deriving (Show)++emptySM :: Int -> StoreModel k v+emptySM n = StoreModel n 0 0 []++insertSM :: Eq k => k -> v -> StoreModel k v -> StoreModel k v+insertSM k v sm@StoreModel{..}+  | smSize < smCap || present =+    sm { smGen = smGen + 1+       , smSize = if present then smSize else smSize + 1+       , smList = (k,v,smGen) : [x | x@(kk,_,_) <- smList, kk /= k]+       }+  | otherwise =+    sm { smGen = smGen + 1+       , smList = (k,v,smGen) : tail (sortBy (comparing $ \(_,_,g) -> g) smList)+       }+  where present = any (\(kk,_,_) -> k == kk) smList++lookupSM :: Eq k => k -> StoreModel k v -> Maybe (v, StoreModel k v)+lookupSM k sm@StoreModel{..} = listToMaybe+                               [(v, sm') | (kk,v,_) <- smList, k == kk]+  where sm' = sm { smGen = smGen + 1+                 , smList = [(kk,v,if kk == k then smGen else g)+                            | (kk,v,g) <- smList]+                 }++fromListSM :: Eq k => Int -> [(k,v)] -> StoreModel k v+fromListSM = foldl' (flip (uncurry insertSM)) . emptySM++toListSM :: StoreModel k v -> [(k,v)]+toListSM sm = [(k,v) | (k,v,_) <- smList sm]++unS :: (Ord k, Hashable k, Ord v) => S.Store k v -> [(k,v)]+unS = sort . S.toList++unM :: (Ord k, Ord v) => StoreModel k v -> [(k,v)]+unM = sort . toListSM++type N = Positive (Small Int)++unN :: N -> Int+unN (Positive (Small n)) = n++t_fromList :: N -> [(Char,Char)] -> Bool+t_fromList n xs = unS (S.fromList (unN n) xs) == unM (fromListSM (unN n) xs)++t_lookup :: N -> Char -> [(Char,Char)] -> Bool+t_lookup n k xs = (fmap unS <$> S.lookup k s) == (fmap unM <$> lookupSM k m)+  where+    s = S.fromList (unN n) xs+    m = fromListSM (unN n) xs++t_lookup1 :: N -> Char -> Char -> [(Char, Char)] -> Bool+t_lookup1 n k v xs = t_lookup n k ((k,v):xs)++tests :: [Test]+tests = [+    testProperty "t_fromList" t_fromList+  , testProperty "t_lookup" t_lookup+  , testProperty "t_lookup1" t_lookup1+  ]
tests/Tests.hs view
@@ -1,207 +1,14 @@-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}-{-# OPTIONS_GHC -fno-warn-incomplete-patterns -fno-warn-missing-signatures-    -fno-warn-unused-binds #-}- module Main (main) where -import Control.Applicative ((<$>))-import Control.Exception (Exception)-import Control.Lens ((^.), (^?), (.~), (&))-import Control.Monad (unless, void)-import Data.Aeson (Value(..), object)-import Data.Aeson.Lens (key)-import Data.ByteString (ByteString)-import Data.Char (toUpper)-import Data.Maybe (isJust)-import Data.Monoid ((<>))-import Data.Text (pack)-import Network.HTTP.Client (HttpException(..))-import Network.HTTP.Types.Status (status200, status401)-import Network.HTTP.Types.Version (http11)-import Network.Wreq-import Network.Wreq.Lens-import qualified Network.Wreq.Session as Session-import System.IO (hClose, hPutStr)-import System.IO.Temp (withSystemTempFile)-import Test.Framework (defaultMain, testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit (assertBool, assertEqual, assertFailure)-import qualified Control.Exception as E-import qualified Data.Text as T--basicGet site = do-  r <- get (site "/get")-  assertBool "GET request has User-Agent header" $-    isJust (r ^. responseBody ^? key "headers" . key "User-Agent")-  -- test the various lenses-  assertEqual "GET succeeds" status200 (r ^. responseStatus)-  assertEqual "GET succeeds 200" 200 (r ^. responseStatus . statusCode)-  assertEqual "GET succeeds OK" "OK" (r ^. responseStatus . statusMessage)-  assertEqual "GET response has HTTP/1.1 version" http11 (r ^. responseVersion)-  assertBool "GET response has Content-Type header" $-    isJust (r ^? responseHeader "Content-Type")-  assertBool "GET response has Date header" $-    isJust (lookup "Date" <$> r ^? responseHeaders)--basicPost site = do-  r <- post (site "/post") ("wibble" :: ByteString) >>= asValue-  let body = r ^. responseBody-  assertEqual "POST succeeds" status200 (r ^. responseStatus)-  assertEqual "POST echoes input" (Just "wibble") (body ^? key "data")-  assertEqual "POST is binary" (Just "application/octet-stream")-                               (body ^? key "headers" . key "Content-Type")--multipartPost site =-  withSystemTempFile "foo.html" $ \name handle -> do-    hPutStr handle "<!DOCTYPE html><html></html"-    hClose handle-    r <- post (site "/post") (partFile "html" name)-    assertEqual "POST succeeds" status200 (r ^. responseStatus)--basicHead site = do-  r <- head_ (site "/get")-  assertEqual "HEAD succeeds" status200 (r ^. responseStatus)--basicPut site = do-  r <- put (site "/put") ("wibble" :: ByteString)-  assertEqual "PUT succeeds" status200 (r ^. responseStatus)--basicDelete site = do-  r <- delete (site "/delete")-  assertEqual "DELETE succeeds" status200 (r ^. responseStatus)--throwsStatusCode site =-    assertThrows "404 causes exception to be thrown" inspect $-    head_ (site "/status/404")-  where inspect e = case e of-                      StatusCodeException _ _ _ -> return ()-                      _ -> assertFailure "unexpected exception thrown"--getBasicAuth site = do-  let opts = defaults & auth .~ basicAuth "user" "passwd"-  r <- getWith opts (site "/basic-auth/user/passwd")-  assertEqual "basic auth GET succeeds" status200 (r ^. responseStatus)-  let inspect e = case e of-                    StatusCodeException status _ _ ->-                      assertEqual "failed basic auth failed GET gives 401"-                        status401 status-  assertThrows "basic auth GET fails if password is bad" inspect $-    getWith opts (site "/basic-auth/user/asswd")--getRedirect site = do-  r <- get (site "/redirect/3")-  let stripProto = T.dropWhile (/=':')-      smap f (String s) = String (f s)-  assertEqual "redirect goes to /get"-    (Just . String . stripProto . pack . site $ "/get")-    (smap stripProto <$> (r ^. responseBody ^? key "url"))--getParams site = do-  let opts1 = defaults & param "foo" .~ ["bar"]-  r1 <- getWith opts1 (site "/get")-  assertEqual "params set correctly 1" (Just (object [("foo","bar")]))-    (r1 ^. responseBody ^? key "args")-  let opts2 = defaults & params .~ [("quux","baz")]-  r2 <- getWith opts2 (site "/get")-  assertEqual "params set correctly 2" (Just (object [("quux","baz")]))-    (r2 ^. responseBody ^? key "args")-  r3 <- getWith opts2 (site "/get?whee=wat")-  assertEqual "correctly handle mix of params from URI and Options"-    (Just (object [("quux","baz"),("whee","wat")]))-    (r3 ^. responseBody ^? key "args")--getHeaders site = do-  let opts = defaults & header "X-Wibble" .~ ["bar"]-  r <- getWith opts (site "/get")-  assertEqual "extra header set correctly"-    (Just "bar")-    (r ^. responseBody ^? key "headers" . key "X-Wibble")--getGzip site = do-  r <- get (site "/gzip")-  assertEqual "gzip decoded for us" (Just (Bool True))-    (r ^. responseBody ^? key "gzipped")--headRedirect site =-  assertThrows "HEAD of redirect throws exception" inspect $-    head_ (site "/redirect/3")-  where inspect e = case e of-                      StatusCodeException status _ _ ->-                        let code = status ^. statusCode-                        in assertBool "code is redirect"-                           (code >= 300 && code < 400)--redirectOverflow site =-  assertThrows "GET with too many redirects throws exception" inspect $-    getWith (defaults & redirects .~ 3) (site "/redirect/5")-  where inspect e = case e of TooManyRedirects _ -> return ()--invalidURL _site = do-  let noProto (InvalidUrlException _ _) = return ()-  assertThrows "exception if no protocol" noProto (get "wheeee")-  let noHost (InvalidDestinationHost _) = return ()-  assertThrows "exception if no host" noHost (get "http://")--funkyScheme site = do-  -- schemes are case insensitive, per RFC 3986 section 3.1-  let (scheme, rest) = break (==':') $ site "/get"-  void . get $ map toUpper scheme <> rest--cookiesSet site = do-  r <- get (site "/cookies/set?x=y")-  assertEqual "cookies are set correctly" (Just "y")-    (r ^? responseCookie "x" . cookieValue)--cookieSession site = Session.withSession $ \s -> do-  void $ Session.get s (site "/cookies/set?foo=bar")-  r <- Session.get s (site "/cookies")-  assertEqual "cookies are set correctly" (Just "bar")-    (r ^? responseCookie "foo" . cookieValue)-  assertEqual "whee" (Just "bar")-    (r ^. responseBody ^? key "cookies" . key "foo")--getWithManager site = withManager $ \opts -> do-  void $ getWith opts (site "/get?a=b")-  void $ getWith opts (site "/get?b=c")--assertThrows :: (Show e, Exception e) => String -> (e -> IO ()) -> IO a -> IO ()-assertThrows desc inspect act = do-  let myInspect e = inspect e `E.catch` \(ee :: E.PatternMatchFail) ->-        assertFailure (desc <> ": unexpected exception (" <>-                       show e <> "): " <> show ee)-  caught <- (act >> return False) `E.catch` \e -> myInspect e >> return True-  unless caught (assertFailure desc)+import Test.Framework (testGroup)+import UnitTests (testWith)+import qualified AWS (tests)+import qualified Properties.Store -testsWith site = [-    testGroup "basic" [-      testCase "get" $ basicGet site-    , testCase "post" $ basicPost site-    , testCase "head" $ basicHead site-    , testCase "put" $ basicPut site-    , testCase "delete" $ basicDelete site-    , testCase "404" $ throwsStatusCode site-    , testCase "headRedirect" $ headRedirect site-    , testCase "redirectOverflow" $ redirectOverflow site-    , testCase "invalidURL" $ invalidURL site-    , testCase "funkyScheme" $ funkyScheme site-    ]-  , testGroup "fancy" [-      testCase "basic auth" $ getBasicAuth site-    , testCase "redirect" $ getRedirect site-    , testCase "params" $ getParams site-    , testCase "headers" $ getHeaders site-    , testCase "gzip" $ getGzip site-    , testCase "cookiesSet" $ cookiesSet site-    , testCase "cookieSession" $ cookieSession site-    , testCase "getWithManager" $ getWithManager site+main :: IO ()+main = do+  awsTests <- AWS.tests+  testWith [+      testGroup "store" Properties.Store.tests+    , awsTests     ]-  ]--tests = [-    testGroup "http" $ testsWith ("http://httpbin.org" <>)-  , testGroup "https" $ testsWith ("https://httpbin.org" <>)-  ]--main = defaultMain tests--localtest = defaultMain (testsWith ("http://localhost:8000" <>))
+ tests/UnitTests.hs view
@@ -0,0 +1,327 @@+{-# LANGUAGE OverloadedStrings, RankNTypes, RecordWildCards,+    ScopedTypeVariables #-}+{-# OPTIONS_GHC -fno-warn-incomplete-patterns -fno-warn-missing-signatures+    -fno-warn-unused-binds #-}++module UnitTests (testWith) where++import Control.Applicative ((<$>))+import Control.Concurrent (forkIO, killThread)+import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import Control.Exception (Exception, toException)+import Control.Lens ((^.), (^?), (.~), (?~), (&))+import Control.Monad (unless, void)+import Data.Aeson (Value(..), object)+import Data.Aeson.Lens (key)+import Data.ByteString (ByteString)+import Data.Char (toUpper)+import Data.Maybe (isJust)+import Data.Monoid ((<>))+import HttpBin.Server (serve)+import Network.HTTP.Client (HttpException(..))+import Network.HTTP.Types.Status (Status(Status), status200, status401)+import Network.HTTP.Types.Version (http11)+import Network.Wreq hiding+  (get, post, head_, put, options, delete,+   getWith, postWith, headWith, putWith, optionsWith, deleteWith)+import Network.Wreq.Lens+import Network.Wreq.Types (Postable, Putable)+import Snap.Http.Server.Config+import System.IO (hClose, hPutStr)+import System.IO.Temp (withSystemTempFile)+import Test.Framework (Test, defaultMain, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (assertBool, assertEqual, assertFailure)+import qualified Control.Exception as E+import qualified Data.Text as T+import qualified Network.Wreq.Session as Session+import qualified Data.ByteString.Lazy as L+import qualified Network.Wreq as Wreq++data Verb = Verb {+    get :: String -> IO (Response L.ByteString)+  , getWith :: Options -> String -> IO (Response L.ByteString)+  , post :: Postable a => String -> a -> IO (Response L.ByteString)+  , postWith :: Postable a => Options -> String -> a+             -> IO (Response L.ByteString)+  , head_ :: String -> IO (Response ())+  , headWith :: Options -> String -> IO (Response ())+  , put :: Putable a => String -> a -> IO (Response L.ByteString)+  , putWith :: Putable a => Options -> String -> a -> IO (Response L.ByteString)+  , options :: String -> IO (Response ())+  , optionsWith :: Options -> String -> IO (Response ())+  , delete :: String -> IO (Response L.ByteString)+  , deleteWith :: Options -> String -> IO (Response L.ByteString)+  }++basic :: Verb+basic = Verb { get = Wreq.get, getWith = Wreq.getWith, post = Wreq.post+             , postWith = Wreq.postWith, head_ = Wreq.head_+             , headWith = Wreq.headWith, put = Wreq.put+             , putWith = Wreq.putWith, options = Wreq.options+             , optionsWith = Wreq.optionsWith, delete = Wreq.delete+             , deleteWith = Wreq.deleteWith }++session :: Session.Session -> Verb+session s = Verb { get = Session.get s+                 , getWith = flip Session.getWith s+                 , post = Session.post s+                 , postWith = flip Session.postWith s+                 , head_ = Session.head_ s+                 , headWith = flip Session.headWith s+                 , put = Session.put s+                 , putWith = flip Session.putWith s+                 , options = Session.options s+                 , optionsWith = flip Session.optionsWith s+                 , delete = Session.delete s+                 , deleteWith = flip Session.deleteWith s }++basicGet Verb{..} site = do+  r <- get (site "/get")+  assertBool "GET request has User-Agent header" $+    isJust (r ^. responseBody ^? key "headers" . key "User-Agent")+  -- test the various lenses+  assertEqual "GET succeeds" status200 (r ^. responseStatus)+  assertEqual "GET succeeds 200" 200 (r ^. responseStatus . statusCode)+  assertEqual "GET succeeds OK" "OK" (r ^. responseStatus . statusMessage)+  assertEqual "GET response has HTTP/1.1 version" http11 (r ^. responseVersion)+  assertBool "GET response has Content-Type header" $+    isJust (r ^? responseHeader "Content-Type")+  assertBool "GET response has Date header" $+    isJust (lookup "Date" <$> r ^? responseHeaders)++basicPost Verb{..} site = do+  r <- post (site "/post") ("wibble" :: ByteString) >>= asValue+  let body = r ^. responseBody+  assertEqual "POST succeeds" status200 (r ^. responseStatus)+  assertEqual "POST echoes input" (Just "wibble") (body ^? key "data")+  assertEqual "POST is binary" (Just "application/octet-stream")+                               (body ^? key "headers" . key "Content-Type")++multipartPost Verb{..} site =+  withSystemTempFile "foo.html" $ \name handle -> do+    hPutStr handle "<!DOCTYPE html><html></html"+    hClose handle+    r <- post (site "/post") (partFile "html" name)+    assertEqual "POST succeeds" status200 (r ^. responseStatus)++basicHead Verb{..} site = do+  r <- head_ (site "/get")+  assertEqual "HEAD succeeds" status200 (r ^. responseStatus)++basicPut Verb{..} site = do+  r <- put (site "/put") ("wibble" :: ByteString)+  assertEqual "PUT succeeds" status200 (r ^. responseStatus)++basicDelete Verb{..} site = do+  r <- delete (site "/delete")+  assertEqual "DELETE succeeds" status200 (r ^. responseStatus)++throwsStatusCode Verb{..} site =+    assertThrows "404 causes exception to be thrown" inspect $+    head_ (site "/status/404")+  where inspect e = case e of+                      StatusCodeException _ _ _ -> return ()+                      _ -> assertFailure "unexpected exception thrown"++getBasicAuth Verb{..} site = do+  let opts = defaults & auth ?~ basicAuth "user" "passwd"+  r <- getWith opts (site "/basic-auth/user/passwd")+  assertEqual "basic auth GET succeeds" status200 (r ^. responseStatus)+  let inspect e = case e of+                    StatusCodeException status _ _ ->+                      assertEqual "basic auth failed GET gives 401"+                        status401 status+  assertThrows "basic auth GET fails if password is bad" inspect $+    getWith opts (site "/basic-auth/user/asswd")++getOAuth2 Verb{..} kind ctor site = do+  let opts = defaults & auth ?~ ctor "token1234"+  r <- getWith opts (site $ "/oauth2/" <> kind <> "/token1234")+  assertEqual ("oauth2 " <> kind <> " GET succeeds")+    status200 (r ^. responseStatus)+  let inspect e = case e of+                    StatusCodeException status _ _ ->+                      assertEqual ("oauth2 " <> kind <> " failed GET gives 401")+                        status401 status+  assertThrows ("oauth2 " <> kind <> " GET fails if token is bad") inspect $+    getWith opts (site $ "/oauth2/" <> kind <> "/token123")++getRedirect Verb{..} site = do+  r <- get (site "/redirect/3")+  let stripProto = T.dropWhile (/=':')+      smap f (String s) = String (f s)+  assertEqual "redirect goes to /get"+    (Just . String . stripProto . T.pack . site $ "/get")+    (smap stripProto <$> (r ^. responseBody ^? key "url"))++getParams Verb{..} site = do+  let opts1 = defaults & param "foo" .~ ["bar"]+  r1 <- getWith opts1 (site "/get")+  assertEqual "params set correctly 1" (Just (object [("foo","bar")]))+    (r1 ^. responseBody ^? key "args")+  let opts2 = defaults & params .~ [("quux","baz")]+  r2 <- getWith opts2 (site "/get")+  assertEqual "params set correctly 2" (Just (object [("quux","baz")]))+    (r2 ^. responseBody ^? key "args")+  r3 <- getWith opts2 (site "/get?whee=wat")+  assertEqual "correctly handle mix of params from URI and Options"+    (Just (object [("quux","baz"),("whee","wat")]))+    (r3 ^. responseBody ^? key "args")++getHeaders Verb{..} site = do+  let opts = defaults & header "X-Wibble" .~ ["bar"]+  r <- getWith opts (site "/get")+  assertEqual "extra header set correctly"+    (Just "bar")+    (r ^. responseBody ^? key "headers" . key "X-Wibble")++getCheckStatus Verb {..} site = do+  let opts = defaults & checkStatus .~ (Just customCs)+  r <- getWith opts (site "/status/404")+  assertThrows "Non 404 throws error" inspect $+    getWith opts (site "/get")+  assertEqual "Status 404" +    404+    (r ^. responseStatus . statusCode)+  where +    customCs (Status 404 _) _ _ = Nothing +    customCs s h cj             = Just . toException . StatusCodeException s h $ cj++    inspect e = case e of+      (StatusCodeException (Status sc _) _ _) -> +        assertEqual "200 Status Error" sc 200 ++getGzip Verb{..} site = do+  r <- get (site "/gzip")+  assertEqual "gzip decoded for us" (Just (Bool True))+    (r ^. responseBody ^? key "gzipped")++headRedirect Verb{..} site =+  assertThrows "HEAD of redirect throws exception" inspect $+    head_ (site "/redirect/3")+  where inspect e = case e of+                      StatusCodeException status _ _ ->+                        let code = status ^. statusCode+                        in assertBool "code is redirect"+                           (code >= 300 && code < 400)++redirectOverflow Verb{..} site =+  assertThrows "GET with too many redirects throws exception" inspect $+    getWith (defaults & redirects .~ 3) (site "/redirect/5")+  where inspect e = case e of TooManyRedirects _ -> return ()++invalidURL Verb{..} _site = do+  let noProto (InvalidUrlException _ _) = return ()+  assertThrows "exception if no protocol" noProto (get "wheeee")+  let noHost (InvalidDestinationHost _) = return ()+  assertThrows "exception if no host" noHost (get "http://")++funkyScheme Verb{..} site = do+  -- schemes are case insensitive, per RFC 3986 section 3.1+  let (scheme, rest) = break (==':') $ site "/get"+  void . get $ map toUpper scheme <> rest++cookiesSet Verb{..} site = do+  r <- get (site "/cookies/set?x=y")+  assertEqual "cookies are set correctly" (Just "y")+    (r ^? responseCookie "x" . cookieValue)++cookieSession site = Session.withSession $ \s -> do+  void $ Session.get s (site "/cookies/set?foo=bar")+  r <- Session.get s (site "/cookies")+  assertEqual "cookies are set correctly" (Just "bar")+    (r ^? responseCookie "foo" . cookieValue)+  assertEqual "whee" (Just "bar")+    (r ^. responseBody ^? key "cookies" . key "foo")++getWithManager site = withManager $ \opts -> do+  void $ Wreq.getWith opts (site "/get?a=b")+  void $ Wreq.getWith opts (site "/get?b=c")++assertThrows :: (Show e, Exception e) => String -> (e -> IO ()) -> IO a -> IO ()+assertThrows desc inspect act = do+  let myInspect e = inspect e `E.catch` \(ee :: E.PatternMatchFail) ->+        assertFailure (desc <> ": unexpected exception (" <>+                       show e <> "): " <> show ee)+  caught <- (act >> return False) `E.catch` \e -> myInspect e >> return True+  unless caught (assertFailure desc)++commonTestsWith verb site = [+    testGroup "basic" [+      testCase "get" $ basicGet verb site+    , testCase "post" $ basicPost verb site+    , testCase "head" $ basicHead verb site+    , testCase "put" $ basicPut verb site+    , testCase "delete" $ basicDelete verb site+    , testCase "404" $ throwsStatusCode verb site+    , testCase "headRedirect" $ headRedirect verb site+    , testCase "redirectOverflow" $ redirectOverflow verb site+    , testCase "invalidURL" $ invalidURL verb site+    , testCase "funkyScheme" $ funkyScheme verb site+    ]+  , testGroup "fancy" [+      testCase "basic auth" $ getBasicAuth verb site+    , testCase "redirect" $ getRedirect verb site+    , testCase "params" $ getParams verb site+    , testCase "headers" $ getHeaders verb site+    , testCase "gzip" $ getGzip verb site+    , testCase "cookiesSet" $ cookiesSet verb site+    , testCase "getWithManager" $ getWithManager site+    , testCase "cookieSession" $ cookieSession site+    , testCase "getCheckStatus" $ getCheckStatus verb site+    ]+  ]++-- Snap responds incorrectly to HEAD (by sending a response body),+-- thereby killing http-client's ability to continue a session.+-- https://github.com/snapframework/snap-core/issues/192+snapHeadSessionBug site = Session.withSession $ \s -> do+  basicHead (session s) site+  -- will crash with (InvalidStatusLine "0")+  basicGet (session s) site++httpbinTestsWith verb site = commonTestsWith verb site <> [+  ]++-- Tests that our local httpbin clone doesn't yet support.+httpbinTests verb = [testGroup "httpbin" [+    testGroup "http" $ httpbinTestsWith verb ("http://httpbin.org" <>)+  , testGroup "https" $ httpbinTestsWith verb ("https://httpbin.org" <>)+  ]]++-- Tests that httpbin.org doesn't support.+localTests verb site = commonTestsWith verb site <> [+    testCase "oauth2 Bearer" $ getOAuth2 verb "Bearer" oauth2Bearer site+  , testCase "oauth2 token" $ getOAuth2 verb "token" oauth2Token site+  ]++startServer = do+  started <- newEmptyMVar+  let go n | n >= 100 = putMVar started Nothing+           | otherwise = do+        let port = 8000 + n+            startedUp p = putMVar started (Just ("http://localhost:" <> p))+            mkCfg = return . setBind ("localhost") . setPort port .+                    setVerbose False .+                    setStartupHook (const (startedUp (show port)))+        serve mkCfg `E.catch` \(_::E.IOException) -> go (n+1)+  tid <- forkIO $ go 0+  (,) tid <$> takeMVar started++testWith :: [Test] -> IO ()+testWith tests = do+  (tid, mserv) <- startServer+  Session.withSession $ \s ->+    flip E.finally (killThread tid) .+    defaultMain $ tests <>+                  [ testGroup "plain" $ httpbinTests basic+                  , testGroup "session" $ httpbinTests (session s)] <>+      case mserv of+        Nothing -> []+        Just binding -> [+            testGroup "localhost" [+              testGroup "plain" $ localTests basic (binding <>)+            , testGroup "session" $ localTests (session s) (binding <>)+            ]+          ]
wreq.cabal view
@@ -1,5 +1,5 @@ name:                wreq-version:             0.2.0.0+version:             0.3.0.0 synopsis:            An easy-to-use HTTP client library. description:   .@@ -49,25 +49,37 @@  -- disable doctests with -f-doctest flag doctest+  description: enable doctest tests   default: True   manual: True  -- enable httpbin with -fhttpbin flag httpbin+  description: enable httpbin test daemon   default: False   manual: True +flag developer+  description: build in developer mode+  default: False+  manual: True+ library   ghc-options: -Wall -fwarn-tabs -funbox-strict-fields+  if flag(developer)+    ghc-options:  -Werror   default-language: Haskell98    exposed-modules:     Network.Wreq+    Network.Wreq.Cache+    Network.Wreq.Cache.Store     Network.Wreq.Lens     Network.Wreq.Session     Network.Wreq.Types   other-modules:     Network.Wreq.Internal+    Network.Wreq.Internal.AWS     Network.Wreq.Internal.Lens     Network.Wreq.Internal.Link     Network.Wreq.Internal.Types@@ -75,27 +87,40 @@     Network.Wreq.Lens.TH     Paths_wreq   build-depends:+    PSQueue >= 1.1,     aeson >= 0.7.0.3,     attoparsec >= 0.11.1.0,     base >= 4.5 && < 5,+    base16-bytestring,+    byteable,     bytestring >= 0.9,+    case-insensitive,+    containers,+    cryptohash,     exceptions >= 0.5,-    http-client >= 0.3.1.1,+    ghc-prim,+    hashable,+    http-client >= 0.4.3,     http-client-tls >= 0.2,     http-types >= 0.8,-    lens >= 4.4,+    lens >= 4.5,     lens-aeson,     mime-types,+    old-locale,     template-haskell,     text,-    time+    time,+    unordered-containers  -- A convenient server for testing locally, or if httpbin.org is down. executable httpbin   hs-source-dirs: httpbin-  main-is:        HttpBin.hs-  ghc-options:    -Wall -fwarn-tabs -threaded+  ghc-options:    -Wall -fwarn-tabs -threaded -rtsopts+  if flag(developer)+    ghc-options:  -Werror   default-language: Haskell98+  main-is:        HttpBin.hs+  other-modules:  HttpBin.Server    if !flag(httpbin)     buildable: False@@ -109,29 +134,56 @@       case-insensitive,       containers,       snap-core,-      snap-server >= 0.9.4.2,-      text+      snap-server >= 0.9.4.4,+      text,+      transformers,+      unix-compat,+      uuid  test-suite tests   type:           exitcode-stdio-1.0-  hs-source-dirs: tests+  hs-source-dirs: httpbin tests   main-is:        Tests.hs-  ghc-options:    -Wall -fwarn-tabs -funbox-strict-fields -threaded+  ghc-options:    -Wall -fwarn-tabs -funbox-strict-fields -threaded -rtsopts+  if flag(developer)+    ghc-options:  -Werror   default-language: Haskell98+  other-modules:+    Properties.Store+    UnitTests+    AWS+    AWS.DynamoDB+    AWS.IAM+    AWS.S3+    AWS.SQS    build-depends:     HUnit,+    QuickCheck >= 2.7,     aeson,+    aeson-pretty >= 0.7.1,+    aeson-qq,     base >= 4.5 && < 5,+    base64-bytestring,     bytestring,+    case-insensitive,+    containers,+    hashable,     http-client,     http-types,-    lens >= 4.4,+    lens,     lens-aeson,+    network-info,+    snap-core,+    snap-server >= 0.9.4.4,     temporary,     test-framework,     test-framework-hunit,+    test-framework-quickcheck2,     text,+    transformers,+    unix-compat,+    uuid,     wreq  test-suite doctest@@ -139,6 +191,8 @@   hs-source-dirs: tests   main-is:        DocTests.hs   ghc-options:    -Wall -fwarn-tabs -threaded+  if flag(developer)+    ghc-options:  -Werror   default-language: Haskell98    if !flag(doctest)
www/Makefile view
@@ -3,13 +3,15 @@ files := index.html tutorial.html deps = bootstrap-custom.css background.jpg install := $(files) $(deps)+destdir := $(HOME)/public_html/wreq  all: $(files)  install: $(files)-	-mkdir -p $(HOME)/public_html/wreq-	cp -a $(install) $(HOME)/public_html/wreq-	cp -a $(bootstrap) $(HOME)/public_html/wreq+	-mkdir -p $(destdir)+	cp -a $(install) $(destdir)+	cp -a $(bootstrap) $(destdir)+	-chcon -R -t httpd_sys_content_t $(destdir)  %.html: %.md template.html $(deps) 	pandoc $< -o $@ --smart --template template.html \
www/index.md view
@@ -27,7 +27,11 @@  * Basic and OAuth2 bearer authentication +* Amazon Web Services (AWS) request signing (Version 4) +* AWS signing supports sending requests through the+  [Runscope Inc.](https://www.runscope.com) Traffic Inspector+ # Whirlwind tour  ~~~~ {.haskell}@@ -123,7 +127,7 @@  I'd like to thank Edward Kmett and Shachaf Ben-Kiki for tirelessly answering my never-ending stream of-[lens](https://lens.github.io/)-related questions in `#haskell-lens#`.+[lens](https://lens.github.io/)-related questions in `#haskell-lens`.  I also want to thank Michael Snoyman for being so quick with helpful responses to bug reports and pull requests against his excellent
www/tutorial.md view
@@ -120,7 +120,7 @@ ~~~~ {.haskell} ghci> import Data.Aeson.Lens (_String, key) ghci> let opts = defaults & param "foo" .~ ["bar", "quux"]-ghci> r <- getWith opts "http://httpbin.org"+ghci> r <- getWith opts "http://httpbin.org/get" ghci> r ^. responseBody . key "url" . _String "http://httpbin.org/get?foo=bar&foo=quux" ~~~~@@ -386,6 +386,7 @@ [`partFileSource`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#t:partFileSource).  ~~~~ {.haskell}+ghci> import Data.Aeson.Lens (members) ghci> r <- post "http://httpbin.org/post" (partFile "file" "hello.hs") ghci> r ^.. responseBody . key "files" . members . _String ["main = putStrLn \"hello\"\n"]@@ -430,7 +431,7 @@ [`HttpException`](http://hackage.haskell.org/package/http-client/docs/Network-HTTP-Client.html#t:HttpException).  ~~~~ {.haskell}-ghci> r <- get "http://httpbin.org/basic-auth/user/pass+ghci> r <- get "http://httpbin.org/basic-auth/user/pass" *** Exception: StatusCodeException (Status {statusCode = 401, {-...-} ~~~~ @@ -439,18 +440,70 @@ retry.)  ~~~~ {.haskell}-ghci> let opts = defaults & auth .~ basicAuth "user" "pass"+ghci> let opts = defaults & auth ?~ basicAuth "user" "pass" ghci> r <- getWith opts "https://httpbin.org/basic-auth/user/pass" ghci> r ^. responseBody "{\n  \"authenticated\": true,\n  \"user\": \"user\"\n}" ~~~~ +<div class="alert alert-info">+We use the+[`?~`](http://hackage.haskell.org/package/lens/docs/Control-Lens-Setter.html#v:-63--126-)+ operator to turn an [`Auth`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#t:Auth)+into a `Maybe Auth` here, to make the type of value on the right hand+side compatible with the+[`auth`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#v:auth)+lens.+</div>+ For OAuth2 bearer authentication, `wreq` supports two flavours: [`oauth2Bearer`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#v:oauth2Bearer) is the standard bearer token, while [`oauth2Token`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#v:oauth2Token) is GitHub's variant.  These tokens are equivalent in value to a username and password.++## Amazon Web Services (AWS)+To authenticate to Amazon Web Services (AWS), we use+[`awsAuth`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq.html#v:awsAuth). In+this example, we set the `Accept` header to request JSON, as opposed+to XML output from AWS.++~~~~ {.haskell}+ghci> let opts = defaults & auth ?~ awsAuth AWSv4 "key" "secret"+                          & header "Accept" .~ ["application/json"]+ghci> r <- getWith opts "https://sqs.us-east-1.amazonaws.com/?Action=ListQueues"+ghci> r ^. responseBody+"{\"ListQueuesResponse\":{\"ListQueuesResult\":{\"queueUrls\": ... }"+~~~~++## Runscope support for Amazon Web Services (AWS) requests+To send requests to AWS through the [Runscope Inc.](https://www.runscope.com)+Traffic Inspector, convert the AWS service URL to a Runscope Bucket URL+using the "URL Helper" section in the Runscope dashboard (as you+would for other HTTP endpoints). Then invoke the AWS service as+before.  For example, if your Runscope bucket key is+`7kh11example`, call AWS like so:++~~~~ {.haskell}+ghci> let opts = defaults & auth ?~ awsAuth AWSv4 "key" "secret"+                          & header "Accept" .~ ["application/json"]+ghci> r <- getWith opts "https://sqs-us--east--1-amazonaws-com-7kh11example.runscope.net/?Action=ListQueues"+ghci> r ^. responseBody+"{\"ListQueuesResponse\":{\"ListQueuesResult\":{\"queueUrls\": ... }"+~~~~++If you enabled "Require Authentication Token" in the "Bucket Settings"+of your Runscope dashboard, set the `Runscope-Bucket-Auth` header like so:++~~~~ {.haskell}+ghci> let opts = defaults & auth ?~ awsAuth AWSv4 "key" "secret"+                          & header "Accept" .~ ["application/json"]+                          & header "Runscope-Bucket-Auth" .~ ["1example-1111-4yyyy-zzzz-xxxxxxxx"]+ghci> r <- getWith opts "https://sqs-us--east--1-amazonaws-com-7kh11example.runscope.net/?Action=ListQueues"+ghci> r ^. responseBody+"{\"ListQueuesResponse\":{\"ListQueuesResult\":{\"queueUrls\": ... }"+~~~~   # Error handling