packages feed

http-conduit 1.2.5 → 1.2.6

raw patch · 8 files changed

+514/−273 lines, 8 filesdep +mtldep +network-conduitPVP ok

version bump matches the API change (PVP)

Dependencies added: mtl, network-conduit

API changes (from Hackage documentation)

+ Network.HTTP.Conduit: OverlongHeaders :: HttpException
+ Network.HTTP.Conduit: TooManyRetries :: HttpException
+ Network.HTTP.Conduit: UnparseableRedirect :: HttpException
+ Network.HTTP.Conduit: generateCookie :: SetCookie -> Request m -> UTCTime -> Bool -> Maybe Cookie
+ Network.HTTP.Conduit: insertCheckedCookie :: Cookie -> CookieJar -> Bool -> CookieJar
+ Network.HTTP.Conduit.Browser: browse :: Manager -> BrowserAction a -> ResourceT IO a
+ Network.HTTP.Conduit.Browser: data BrowserState
+ Network.HTTP.Conduit.Browser: defaultState :: Manager -> BrowserState
+ Network.HTTP.Conduit.Browser: getAuthorities :: BrowserAction (Request IO -> Maybe (ByteString, ByteString))
+ Network.HTTP.Conduit.Browser: getBrowserState :: BrowserAction BrowserState
+ Network.HTTP.Conduit.Browser: getCookieFilter :: BrowserAction (Request IO -> Cookie -> IO Bool)
+ Network.HTTP.Conduit.Browser: getCookieJar :: BrowserAction CookieJar
+ Network.HTTP.Conduit.Browser: getCurrentProxy :: BrowserAction (Maybe Proxy)
+ Network.HTTP.Conduit.Browser: getManager :: BrowserAction Manager
+ Network.HTTP.Conduit.Browser: getMaxRedirects :: BrowserAction Int
+ Network.HTTP.Conduit.Browser: getMaxRetryCount :: BrowserAction Int
+ Network.HTTP.Conduit.Browser: getUserAgent :: BrowserAction ByteString
+ Network.HTTP.Conduit.Browser: makeRequest :: Request IO -> BrowserAction (Response (Source IO ByteString))
+ Network.HTTP.Conduit.Browser: setAuthorities :: (Request IO -> Maybe (ByteString, ByteString)) -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setBrowserState :: BrowserState -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setCookieFilter :: (Request IO -> Cookie -> IO Bool) -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setCookieJar :: CookieJar -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setCurrentProxy :: Maybe Proxy -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setManager :: Manager -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setMaxRedirects :: Int -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setMaxRetryCount :: Int -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: setUserAgent :: ByteString -> BrowserAction ()
+ Network.HTTP.Conduit.Browser: type BrowserAction = StateT BrowserState (ResourceT IO)
+ Network.HTTP.Conduit.Browser: withBrowserState :: BrowserState -> BrowserAction a -> BrowserAction a

Files

Network/HTTP/Conduit.hs view
@@ -93,6 +93,8 @@     , destroyCookieJar     , updateCookieJar     , receiveSetCookie+    , generateCookie+    , insertCheckedCookie     , insertCookiesIntoRequest     , computeCookieString     , evictExpiredCookies
+ Network/HTTP/Conduit/Browser.hs view
@@ -0,0 +1,196 @@+module Network.HTTP.Conduit.Browser+    ( BrowserState+    , BrowserAction+    , browse+    , makeRequest+    , defaultState+    , getBrowserState+    , setBrowserState+    , withBrowserState+    , getMaxRedirects +    , setMaxRedirects +    , getMaxRetryCount+    , setMaxRetryCount+    , getAuthorities  +    , setAuthorities  +    , getCookieFilter +    , setCookieFilter +    , getCookieJar    +    , setCookieJar    +    , getCurrentProxy +    , setCurrentProxy +    , getUserAgent    +    , setUserAgent    +    , getManager      +    , setManager+    )+  where++import qualified Data.ByteString as BS+import Control.Monad.State+import Control.Exception+import qualified Control.Exception.Lifted as LE+import Control.Monad.Trans.Resource+import Data.Conduit+import Prelude hiding (catch)+import qualified Network.HTTP.Types as HT+import Data.Time.Clock (getCurrentTime, UTCTime)+import Data.CaseInsensitive (mk)+import Data.ByteString.UTF8 (fromString)+import Data.List (partition)+import Web.Cookie (parseSetCookie)+import Data.Default (def)+import Data.Maybe (catMaybes)++import Network.HTTP.Conduit.Cookies hiding (updateCookieJar)+import Network.HTTP.Conduit.Request+import Network.HTTP.Conduit.Response+import Network.HTTP.Conduit.Manager+import qualified Network.HTTP.Conduit as HC++data BrowserState = BrowserState+  { maxRedirects        :: Int+  , maxRetryCount       :: Int+  , authorities         :: Request IO -> Maybe (BS.ByteString, BS.ByteString)+  , cookieFilter        :: Request IO -> Cookie -> IO Bool+  , cookieJar           :: CookieJar+  , currentProxy        :: Maybe Proxy+  , userAgent           :: BS.ByteString+  , manager             :: Manager+  } ++defaultState :: Manager -> BrowserState+defaultState m = BrowserState { maxRedirects = 10+                              , maxRetryCount = 1+                              , authorities = \ _ -> Nothing+                              , cookieFilter = \ _ _ -> return True+                              , cookieJar = def+                              , currentProxy = Nothing+                              , userAgent = fromString "http-conduit"+                              , manager = m+                              }++type BrowserAction = StateT BrowserState (ResourceT IO)++-- | Do the browser action with the given manager+browse :: Manager -> BrowserAction a -> ResourceT IO a+browse m act = evalStateT act (defaultState m)++-- | Make a request, using all the state in the current BrowserState+makeRequest :: Request IO -> BrowserAction (Response (Source IO BS.ByteString))+makeRequest request = do+  BrowserState+    { maxRetryCount = max_retry_count+    , currentProxy  = current_proxy+    , userAgent     = user_agent+    } <- get+  retryHelper (applyUserAgent user_agent $+    request { redirectCount = 0+            , proxy = current_proxy+            , checkStatus = \ _ _ -> Nothing+            }) max_retry_count Nothing+  where retryHelper request' retry_count e+          | retry_count == 0 = case e of+            Just e' -> throw e'+            Nothing -> throw TooManyRedirects+          | otherwise = do+              BrowserState {maxRedirects = max_redirects} <- get+              resp <- LE.catch (runRedirectionChain request' max_redirects)+                (\ e' -> retryHelper request' (retry_count - 1) (Just (e' :: HttpException)))+              let code = HT.statusCode $ HC.statusCode resp+              if code < 200 || code >= 300+                then retryHelper request' (retry_count - 1) (Just $ HC.StatusCodeException (HC.statusCode resp) (HC.responseHeaders resp))+                else return resp+        runRedirectionChain request' redirect_count+          | redirect_count == 0 = throw TooManyRedirects+          | otherwise = do+              s@(BrowserState { manager = manager'+                              , authorities = auths+                              , cookieJar = cookie_jar+                              , cookieFilter = cookie_filter+                              }) <- get+              now <- liftIO getCurrentTime+              let (request'', cookie_jar') = insertCookiesIntoRequest+                                              (applyAuthorities auths request')+                                              (evictExpiredCookies cookie_jar now) now+              res <- lift $ HC.http request'' manager'+              (cookie_jar'', response) <- liftIO $ updateCookieJar res request'' now cookie_jar' cookie_filter+              put $ s {cookieJar = cookie_jar''}+              let code = HT.statusCode (HC.statusCode response)+              if code >= 300 && code < 400+                then runRedirectionChain (case HC.getRedirectedRequest request'' (responseHeaders response) code of+                  Just a -> a+                  Nothing -> throw HC.UnparseableRedirect) (redirect_count - 1)+                else return res+        applyAuthorities auths request' = case auths request' of+          Just (user, pass) -> applyBasicAuth user pass request'+          Nothing -> request'+        applyUserAgent ua request' = request' {requestHeaders = (k, ua) : hs}+          where hs = filter ((/= k) . fst) $ requestHeaders request'+                k = mk $ fromString "User-Agent"++updateCookieJar :: Response a -> Request IO -> UTCTime -> CookieJar -> (Request IO -> Cookie -> IO Bool) -> IO (CookieJar, Response a)+updateCookieJar response request' now cookie_jar cookie_filter = do+  filtered_cookies <- filterM (cookie_filter request') $ catMaybes $ map (\ sc -> generateCookie sc request' now True) set_cookies+  return (cookieJar' filtered_cookies, response {HC.responseHeaders = other_headers})+  where (set_cookie_headers, other_headers) = partition ((== (mk $ fromString "Set-Cookie")) . fst) $ HC.responseHeaders response+        set_cookie_data = map snd set_cookie_headers+        set_cookies = map parseSetCookie set_cookie_data+        cookieJar' = foldl (\ cj c -> insertCheckedCookie c cj True) cookie_jar++-- | You can save and restore the state at will+getBrowserState :: BrowserAction BrowserState+getBrowserState = get+setBrowserState :: BrowserState -> BrowserAction ()+setBrowserState = put+withBrowserState :: BrowserState -> BrowserAction a -> BrowserAction a+withBrowserState s a = do+  current <- get+  put s+  out <- a+  put current+  return out++-- | The number of redirects to allow+getMaxRedirects    :: BrowserAction Int+getMaxRedirects    = get >>= \ a -> return $ maxRedirects a+setMaxRedirects    :: Int -> BrowserAction ()+setMaxRedirects  b = get >>= \ a -> put a {maxRedirects = b}+-- | The number of times to retry a failed connection+getMaxRetryCount   :: BrowserAction Int+getMaxRetryCount   = get >>= \ a -> return $ maxRetryCount a+setMaxRetryCount    :: Int -> BrowserAction ()+setMaxRetryCount b = get >>= \ a -> put a {maxRetryCount = b}+-- | A user-provided function that provides optional authorities.+-- This function gets run on all requests before they get sent out.+-- The output of this function is applied to the request.+getAuthorities     :: BrowserAction (Request IO -> Maybe (BS.ByteString, BS.ByteString))+getAuthorities     = get >>= \ a -> return $ authorities a+setAuthorities     :: (Request IO -> Maybe (BS.ByteString, BS.ByteString)) -> BrowserAction ()+setAuthorities   b = get >>= \ a -> put a {authorities = b}+-- | Each new Set-Cookie the browser encounters will pass through this filter.+-- Only cookies that pass the filter (and are already valid) will be allowed into the cookie jar+getCookieFilter    :: BrowserAction (Request IO -> Cookie -> IO Bool)+getCookieFilter    = get >>= \ a -> return $ cookieFilter a+setCookieFilter    :: (Request IO -> Cookie -> IO Bool) -> BrowserAction ()+setCookieFilter  b = get >>= \ a -> put a {cookieFilter = b}+-- | All the cookies!+getCookieJar       :: BrowserAction CookieJar+getCookieJar       = get >>= \ a -> return $ cookieJar a+setCookieJar       :: CookieJar -> BrowserAction ()+setCookieJar     b = get >>= \ a -> put a {cookieJar = b}+-- | An optional proxy to send all requests through+getCurrentProxy    :: BrowserAction (Maybe Proxy)+getCurrentProxy    = get >>= \ a -> return $ currentProxy a+setCurrentProxy    :: Maybe Proxy -> BrowserAction ()+setCurrentProxy  b = get >>= \ a -> put a {currentProxy = b}+-- | What string to report our user-agent as+getUserAgent       :: BrowserAction BS.ByteString+getUserAgent       = get >>= \ a -> return $ userAgent a+setUserAgent       :: BS.ByteString -> BrowserAction ()+setUserAgent     b = get >>= \ a -> put a {userAgent = b}+-- | The active manager, managing the connection pool+getManager         :: BrowserAction Manager+getManager         = get >>= \ a -> return $ manager a+setManager         :: Manager -> BrowserAction ()+setManager       b = get >>= \ a -> put a {manager = b}
Network/HTTP/Conduit/Cookies.hs view
@@ -1,14 +1,264 @@ -- | This module implements the algorithms described in RFC 6265 for the Network.HTTP.Conduit library.-module Network.HTTP.Conduit.Cookies-  ( Cookie(..)-  , CookieJar-  , createCookieJar-  , destroyCookieJar-  , updateCookieJar-  , receiveSetCookie-  , insertCookiesIntoRequest-  , computeCookieString-  , evictExpiredCookies-  ) where+module Network.HTTP.Conduit.Cookies where -import Network.HTTP.Conduit.Cookies.Internal+import qualified Network.HTTP.Types as W+import qualified Data.ByteString as BS+import qualified Data.ByteString.UTF8 as U+import Text.Regex+import Data.Maybe+import qualified Data.List as L+import Data.Time.Clock+import Data.Time.Calendar+import Web.Cookie+import qualified Data.CaseInsensitive as CI+import Blaze.ByteString.Builder+import Data.Default++import qualified Network.HTTP.Conduit.Request as Req+import qualified Network.HTTP.Conduit.Response as Res++slash :: Integral a => a+slash = 47 -- '/'++isIpAddress :: W.Ascii -> Bool+isIpAddress a = case strs of+  Just strs' -> helper strs'+  Nothing -> False+  where s = U.toString a+        regex = mkRegex "^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$"+        strs = matchRegex regex s+        helper l = length l == 4 && all helper2 l+        helper2 v = (read v :: Int) >= 0 && (read v :: Int) < 256++-- | This corresponds to the subcomponent algorithm entitled \"Domain Matching\" detailed+-- in section 5.1.3+domainMatches :: W.Ascii -> W.Ascii -> Bool+domainMatches string domainString+  | string == domainString = True+  | BS.length string < BS.length domainString + 1 = False+  | domainString `BS.isSuffixOf` string && BS.singleton (BS.last difference) == U.fromString "." && not (isIpAddress string) = True+  | otherwise = False+  where difference = BS.take (BS.length string - BS.length domainString) string++-- | This corresponds to the subcomponent algorithm entitled \"Paths\" detailed+-- in section 5.1.4+defaultPath :: Req.Request m -> W.Ascii+defaultPath req+  | BS.null uri_path = U.fromString "/"+  | BS.singleton (BS.head uri_path) /= U.fromString "/" = U.fromString "/"+  | BS.count slash uri_path <= 1 = U.fromString "/"+  | otherwise = BS.reverse $ BS.tail $ BS.dropWhile (/= slash) $ BS.reverse uri_path+  where uri_path = Req.path req++-- | This corresponds to the subcomponent algorithm entitled \"Path-Match\" detailed+-- in section 5.1.4+pathMatches :: W.Ascii -> W.Ascii -> Bool+pathMatches requestPath cookiePath+  | cookiePath == requestPath = True+  | cookiePath `BS.isPrefixOf` requestPath && BS.singleton (BS.last cookiePath) == U.fromString "/" = True+  | cookiePath `BS.isPrefixOf` requestPath && BS.singleton (BS.head remainder) == U.fromString "/" = True+  | otherwise = False+  where remainder = BS.drop (BS.length cookiePath) requestPath++-- This corresponds to the description of a cookie detailed in Section 5.3 \"Storage Model\"+data Cookie = Cookie+  { cookie_name :: W.Ascii+  , cookie_value :: W.Ascii+  , cookie_expiry_time :: UTCTime+  , cookie_domain :: W.Ascii+  , cookie_path :: W.Ascii+  , cookie_creation_time :: UTCTime+  , cookie_last_access_time :: UTCTime+  , cookie_persistent :: Bool+  , cookie_host_only :: Bool+  , cookie_secure_only :: Bool+  , cookie_http_only :: Bool+  }+  deriving (Show)+-- This corresponds to step 11 of the algorithm described in Section 5.3 \"Storage Model\"+instance Eq Cookie where+  (==) a b = name_matches && domain_matches && path_matches+    where name_matches = cookie_name a == cookie_name b+          domain_matches = cookie_domain a == cookie_domain b+          path_matches = cookie_path a == cookie_path b+instance Ord Cookie where+  compare c1 c2+    | BS.length (cookie_path c1) > BS.length (cookie_path c2) = LT+    | BS.length (cookie_path c1) < BS.length (cookie_path c2) = GT+    | cookie_creation_time c1 > cookie_creation_time c2 = GT+    | otherwise = LT++newtype CookieJar = CJ { expose :: [Cookie] }++-- | empty cookie jar+instance Default CookieJar where+  def = CJ []++instance Eq CookieJar where+  (==) cj1 cj2 = (L.sort $ expose cj1) == (L.sort $ expose cj2)++instance Show CookieJar where+  show = show . expose++createCookieJar :: [Cookie] -> CookieJar+createCookieJar = CJ++destroyCookieJar :: CookieJar -> [Cookie]+destroyCookieJar = expose++insertIntoCookieJar :: Cookie -> CookieJar -> CookieJar+insertIntoCookieJar cookie cookie_jar' = CJ $ cookie : cookie_jar+  where cookie_jar = expose cookie_jar'++removeExistingCookieFromCookieJar :: Cookie -> CookieJar -> (Maybe Cookie, CookieJar)+removeExistingCookieFromCookieJar cookie cookie_jar' = (mc, CJ lc)+  where (mc, lc) = removeExistingCookieFromCookieJarHelper cookie (expose cookie_jar')+        removeExistingCookieFromCookieJarHelper _ [] = (Nothing, [])+        removeExistingCookieFromCookieJarHelper c (c' : cs)+          | c == c' = (Just c', cs)+          | otherwise = (cookie', c' : cookie_jar'')+          where (cookie', cookie_jar'') = removeExistingCookieFromCookieJarHelper c cs++-- | Are we configured to reject cookies for domains such as \"com\"?+rejectPublicSuffixes :: Bool+rejectPublicSuffixes = True++isPublicSuffix :: W.Ascii -> Bool+isPublicSuffix _ = False++-- | This corresponds to the eviction algorithm described in Section 5.3 \"Storage Model\"+evictExpiredCookies :: CookieJar  -- ^ Input cookie jar+                    -> UTCTime    -- ^ Value that should be used as \"now\"+                    -> CookieJar  -- ^ Filtered cookie jar+evictExpiredCookies cookie_jar' now = CJ $ filter (\ cookie -> cookie_expiry_time cookie >= now) $ expose cookie_jar'++-- | This applies the 'computeCookieString' to a given Request+insertCookiesIntoRequest :: Req.Request m               -- ^ The request to insert into+                         -> CookieJar                   -- ^ Current cookie jar+                         -> UTCTime                     -- ^ Value that should be used as \"now\"+                         -> (Req.Request m, CookieJar)  -- ^ (Ouptut request, Updated cookie jar (last-access-time is updated))+insertCookiesIntoRequest request cookie_jar now = (request {Req.requestHeaders = cookie_header : purgedHeaders}, cookie_jar')+  where purgedHeaders = L.deleteBy (\ (a, _) (b, _) -> a == b) (CI.mk $ U.fromString "Cookie", BS.empty) $ Req.requestHeaders request+        (cookie_string, cookie_jar') = computeCookieString request cookie_jar now True+        cookie_header = (CI.mk $ U.fromString "Cookie", cookie_string)++-- | This corresponds to the algorithm described in Section 5.4 \"The Cookie Header\"+computeCookieString :: Req.Request m         -- ^ Input request+                    -> CookieJar             -- ^ Current cookie jar+                    -> UTCTime               -- ^ Value that should be used as \"now\"+                    -> Bool                  -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)+                    -> (W.Ascii, CookieJar)  -- ^ (Contents of a \"Cookie\" header, Updated cookie jar (last-access-time is updated))+computeCookieString request cookie_jar now is_http_api = (output_line, cookie_jar')+  where matching_cookie cookie = condition1 && condition2 && condition3 && condition4+          where condition1+                  | cookie_host_only cookie = Req.host request == cookie_domain cookie+                  | otherwise = domainMatches (Req.host request) (cookie_domain cookie)+                condition2 = pathMatches (Req.path request) (cookie_path cookie)+                condition3+                  | not (cookie_secure_only cookie) = True+                  | otherwise = Req.secure request+                condition4+                  | not (cookie_http_only cookie) = True+                  | otherwise = is_http_api+        matching_cookies = filter matching_cookie $ expose cookie_jar+        output_cookies =  map (\ c -> (cookie_name c, cookie_value c)) $ L.sort matching_cookies+        output_line = toByteString $ renderCookies $ output_cookies+        folding_function cookie_jar'' cookie = case removeExistingCookieFromCookieJar cookie cookie_jar'' of+          (Just c, cookie_jar''') -> insertIntoCookieJar (c {cookie_last_access_time = now}) cookie_jar'''+          (Nothing, cookie_jar''') -> cookie_jar'''+        cookie_jar' = foldl folding_function cookie_jar matching_cookies++-- | This applies 'receiveSetCookie' to a given Response+updateCookieJar :: Res.Response a               -- ^ Response received from server+                -> Req.Request m                -- ^ Request which generated the response+                -> UTCTime                      -- ^ Value that should be used as \"now\"+                -> CookieJar                    -- ^ Current cookie jar+                -> (CookieJar, Res.Response a)  -- ^ (Updated cookie jar with cookies from the Response, The response stripped of any \"Set-Cookie\" header)+updateCookieJar response request now cookie_jar = (cookie_jar', response {Res.responseHeaders = other_headers})+  where (set_cookie_headers, other_headers) = L.partition ((== (CI.mk $ U.fromString "Set-Cookie")) . fst) $ Res.responseHeaders response+        set_cookie_data = map snd set_cookie_headers+        set_cookies = map parseSetCookie set_cookie_data+        cookie_jar' = foldl (\ cj sc -> receiveSetCookie sc request now True cj) cookie_jar set_cookies++-- | This corresponds to the algorithm described in Section 5.3 \"Storage Model\"+-- This function consists of calling 'generateCookie' followed by 'insertCheckedCookie'.+-- Use this function if you plan to do both in a row.+-- 'generateCookie' and 'insertCheckedCookie' are only provided for more fine-grained control.+receiveSetCookie :: SetCookie      -- ^ The 'SetCookie' the cookie jar is receiving+                 -> Req.Request m  -- ^ The request that originated the response that yielded the 'SetCookie'+                 -> UTCTime        -- ^ Value that should be used as \"now\"+                 -> Bool           -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)+                 -> CookieJar      -- ^ Input cookie jar to modify+                 -> CookieJar      -- ^ Updated cookie jar+receiveSetCookie set_cookie request now is_http_api cookie_jar = case (do+  cookie <- generateCookie set_cookie request now is_http_api+  return $ insertCheckedCookie cookie cookie_jar is_http_api) of+  Just cj -> cj+  Nothing -> cookie_jar++-- | Insert a cookie created by generateCookie into the cookie jar (or not if it shouldn't be allowed in)+insertCheckedCookie :: Cookie    -- ^ The 'SetCookie' the cookie jar is receiving+                    -> CookieJar -- ^ Input cookie jar to modify+                    -> Bool      -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)+                    -> CookieJar -- ^ Updated (or not) cookie jar+insertCheckedCookie c cookie_jar is_http_api = case (do+  (cookie_jar', cookie') <- existanceTest c cookie_jar+  return $ insertIntoCookieJar cookie' cookie_jar') of+  Just cj -> cj+  Nothing -> cookie_jar+  where existanceTest cookie cookie_jar' = existanceTestHelper cookie $ removeExistingCookieFromCookieJar cookie cookie_jar'+        existanceTestHelper new_cookie (Just old_cookie, cookie_jar')+          | not is_http_api && cookie_http_only old_cookie = Nothing+          | otherwise = return (cookie_jar', new_cookie {cookie_creation_time = cookie_creation_time old_cookie})+        existanceTestHelper new_cookie (Nothing, cookie_jar') = return (cookie_jar', new_cookie)++-- | Turn a SetCookie into a Cookie, if it is valid+generateCookie :: SetCookie      -- ^ The 'SetCookie' we are encountering+               -> Req.Request m  -- ^ The request that originated the response that yielded the 'SetCookie'+               -> UTCTime        -- ^ Value that should be used as \"now\"+               -> Bool           -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)+               -> Maybe Cookie   -- ^ The optional output cookie+generateCookie set_cookie request now is_http_api = do+          domain_sanitized <- sanitizeDomain $ step4 (setCookieDomain set_cookie)+          domain_intermediate <- step5 domain_sanitized+          (domain_final, host_only') <- step6 domain_intermediate+          http_only' <- step10+          return $ Cookie { cookie_name = setCookieName set_cookie+                          , cookie_value = setCookieValue set_cookie+                          , cookie_expiry_time = getExpiryTime (setCookieExpires set_cookie) (setCookieMaxAge set_cookie)+                          , cookie_domain = domain_final+                          , cookie_path = getPath $ setCookiePath set_cookie+                          , cookie_creation_time = now+                          , cookie_last_access_time = now+                          , cookie_persistent = getPersistent+                          , cookie_host_only = host_only'+                          , cookie_secure_only = setCookieSecure set_cookie+                          , cookie_http_only = http_only'+                          }+  where sanitizeDomain domain'+          | has_a_character && BS.singleton (BS.last domain') == U.fromString "." = Nothing+          | has_a_character && BS.singleton (BS.head domain') == U.fromString "." = Just $ BS.tail domain'+          | otherwise = Just $ domain'+          where has_a_character = not (BS.null domain')+        step4 (Just set_cookie_domain) = set_cookie_domain+        step4 Nothing = BS.empty+        step5 domain'+          | firstCondition && domain' == (Req.host request) = return BS.empty+          | firstCondition = Nothing+          | otherwise = return domain'+          where firstCondition = rejectPublicSuffixes && isPublicSuffix domain'+        step6 domain'+          | firstCondition && not (domainMatches (Req.host request) domain') = Nothing+          | firstCondition = return (domain', False)+          | otherwise = return (Req.host request, True)+          where firstCondition = not $ BS.null domain'+        step10+          | not is_http_api && setCookieHttpOnly set_cookie = Nothing+          | otherwise = return $ setCookieHttpOnly set_cookie+        getExpiryTime :: Maybe UTCTime -> Maybe DiffTime -> UTCTime+        getExpiryTime _ (Just t) = (fromRational $ toRational t) `addUTCTime` now+        getExpiryTime (Just t) Nothing = t+        getExpiryTime Nothing Nothing= UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)+        getPath (Just p) = p+        getPath Nothing = defaultPath request+        getPersistent = isJust (setCookieExpires set_cookie) || isJust (setCookieMaxAge set_cookie)
− Network/HTTP/Conduit/Cookies/Internal.hs
@@ -1,247 +0,0 @@--- | This module implements the algorithms described in RFC 6265 for the Network.HTTP.Conduit library.-module Network.HTTP.Conduit.Cookies.Internal where--import qualified Network.HTTP.Types as W-import qualified Data.ByteString as BS-import qualified Data.ByteString.UTF8 as U-import Text.Regex-import Data.Maybe-import qualified Data.List as L-import Data.Time.Clock-import Data.Time.Calendar-import Web.Cookie-import qualified Data.CaseInsensitive as CI-import Blaze.ByteString.Builder-import Data.Default--import qualified Network.HTTP.Conduit.Request as Req-import qualified Network.HTTP.Conduit.Response as Res--slash :: Integral a => a-slash = 47 -- '/'--isIpAddress :: W.Ascii -> Bool-isIpAddress a = case strs of-  Just strs' -> helper strs'-  Nothing -> False-  where s = U.toString a-        regex = mkRegex "^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$"-        strs = matchRegex regex s-        helper l = length l == 4 && all helper2 l-        helper2 v = (read v :: Int) >= 0 && (read v :: Int) < 256---- | This corresponds to the subcomponent algorithm entitled \"Domain Matching\" detailed--- in section 5.1.3-domainMatches :: W.Ascii -> W.Ascii -> Bool-domainMatches string domainString-  | string == domainString = True-  | BS.length string < BS.length domainString + 1 = False-  | domainString `BS.isSuffixOf` string && BS.singleton (BS.last difference) == U.fromString "." && not (isIpAddress string) = True-  | otherwise = False-  where difference = BS.take (BS.length string - BS.length domainString) string---- | This corresponds to the subcomponent algorithm entitled \"Paths\" detailed--- in section 5.1.4-defaultPath :: Req.Request m -> W.Ascii-defaultPath req-  | BS.null uri_path = U.fromString "/"-  | BS.singleton (BS.head uri_path) /= U.fromString "/" = U.fromString "/"-  | BS.count slash uri_path <= 1 = U.fromString "/"-  | otherwise = BS.reverse $ BS.tail $ BS.dropWhile (/= slash) $ BS.reverse uri_path-  where uri_path = Req.path req---- | This corresponds to the subcomponent algorithm entitled \"Path-Match\" detailed--- in section 5.1.4-pathMatches :: W.Ascii -> W.Ascii -> Bool-pathMatches requestPath cookiePath-  | cookiePath == requestPath = True-  | cookiePath `BS.isPrefixOf` requestPath && BS.singleton (BS.last cookiePath) == U.fromString "/" = True-  | cookiePath `BS.isPrefixOf` requestPath && BS.singleton (BS.head remainder) == U.fromString "/" = True-  | otherwise = False-  where remainder = BS.drop (BS.length cookiePath) requestPath---- This corresponds to the description of a cookie detailed in Section 5.3 \"Storage Model\"-data Cookie = Cookie-  { cookie_name :: W.Ascii-  , cookie_value :: W.Ascii-  , cookie_expiry_time :: UTCTime-  , cookie_domain :: W.Ascii-  , cookie_path :: W.Ascii-  , cookie_creation_time :: UTCTime-  , cookie_last_access_time :: UTCTime-  , cookie_persistent :: Bool-  , cookie_host_only :: Bool-  , cookie_secure_only :: Bool-  , cookie_http_only :: Bool-  }-  deriving (Show)--- This corresponds to step 11 of the algorithm described in Section 5.3 \"Storage Model\"-instance Eq Cookie where-  (==) a b = name_matches && domain_matches && path_matches-    where name_matches = cookie_name a == cookie_name b-          domain_matches = cookie_domain a == cookie_domain b-          path_matches = cookie_path a == cookie_path b-instance Ord Cookie where-  compare c1 c2-    | BS.length (cookie_path c1) > BS.length (cookie_path c2) = LT-    | BS.length (cookie_path c1) < BS.length (cookie_path c2) = GT-    | cookie_creation_time c1 > cookie_creation_time c2 = GT-    | otherwise = LT--newtype CookieJar = CJ { expose :: [Cookie] }---- | empty cookie jar-instance Default CookieJar where-  def = CJ []--instance Eq CookieJar where-  (==) cj1 cj2 = (L.sort $ expose cj1) == (L.sort $ expose cj2)--instance Show CookieJar where-  show = show . expose--createCookieJar :: [Cookie] -> CookieJar-createCookieJar = CJ--destroyCookieJar :: CookieJar -> [Cookie]-destroyCookieJar = expose--insertIntoCookieJar :: Cookie -> CookieJar -> CookieJar-insertIntoCookieJar cookie cookie_jar' = CJ $ cookie : cookie_jar-  where cookie_jar = expose cookie_jar'--removeExistingCookieFromCookieJar :: Cookie -> CookieJar -> (Maybe Cookie, CookieJar)-removeExistingCookieFromCookieJar cookie cookie_jar' = (mc, CJ lc)-  where (mc, lc) = removeExistingCookieFromCookieJarHelper cookie (expose cookie_jar')-        removeExistingCookieFromCookieJarHelper _ [] = (Nothing, [])-        removeExistingCookieFromCookieJarHelper c (c' : cs)-          | c == c' = (Just c', cs)-          | otherwise = (cookie', c' : cookie_jar'')-          where (cookie', cookie_jar'') = removeExistingCookieFromCookieJarHelper c cs---- | Are we configured to reject cookies for domains such as \"com\"?-rejectPublicSuffixes :: Bool-rejectPublicSuffixes = True--isPublicSuffix :: W.Ascii -> Bool-isPublicSuffix _ = False---- | This corresponds to the eviction algorithm described in Section 5.3 \"Storage Model\"-evictExpiredCookies :: CookieJar  -- ^ Input cookie jar-                    -> UTCTime    -- ^ Value that should be used as \"now\"-                    -> CookieJar  -- ^ Filtered cookie jar-evictExpiredCookies cookie_jar' now = CJ $ filter (\ cookie -> cookie_expiry_time cookie >= now) $ expose cookie_jar'---- | This applies the 'computeCookieString' to a given Request-insertCookiesIntoRequest :: Req.Request m               -- ^ The request to insert into-                         -> CookieJar                   -- ^ Current cookie jar-                         -> UTCTime                     -- ^ Value that should be used as \"now\"-                         -> (Req.Request m, CookieJar)  -- ^ (Ouptut request, Updated cookie jar (last-access-time is updated))-insertCookiesIntoRequest request cookie_jar now = (request {Req.requestHeaders = cookie_header : purgedHeaders}, cookie_jar')-  where purgedHeaders = L.deleteBy (\ (a, _) (b, _) -> a == b) (CI.mk $ U.fromString "Cookie", BS.empty) $ Req.requestHeaders request-        (cookie_string, cookie_jar') = computeCookieString request cookie_jar now True-        cookie_header = (CI.mk $ U.fromString "Cookie", cookie_string)---- | This corresponds to the algorithm described in Section 5.4 \"The Cookie Header\"-computeCookieString :: Req.Request m         -- ^ Input request-                    -> CookieJar             -- ^ Current cookie jar-                    -> UTCTime               -- ^ Value that should be used as \"now\"-                    -> Bool                  -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)-                    -> (W.Ascii, CookieJar)  -- ^ (Contents of a \"Cookie\" header, Updated cookie jar (last-access-time is updated))-computeCookieString request cookie_jar now is_http_api = (output_line, cookie_jar')-  where matching_cookie cookie = condition1 && condition2 && condition3 && condition4-          where condition1-                  | cookie_host_only cookie = Req.host request == cookie_domain cookie-                  | otherwise = domainMatches (Req.host request) (cookie_domain cookie)-                condition2 = pathMatches (Req.path request) (cookie_path cookie)-                condition3-                  | not (cookie_secure_only cookie) = True-                  | otherwise = Req.secure request-                condition4-                  | not (cookie_http_only cookie) = True-                  | otherwise = is_http_api-        matching_cookies = filter matching_cookie $ expose cookie_jar-        output_cookies =  map (\ c -> (cookie_name c, cookie_value c)) $ L.sort matching_cookies-        output_line = toByteString $ renderCookies $ output_cookies-        folding_function cookie_jar'' cookie = case removeExistingCookieFromCookieJar cookie cookie_jar'' of-          (Just c, cookie_jar''') -> insertIntoCookieJar (c {cookie_last_access_time = now}) cookie_jar'''-          (Nothing, cookie_jar''') -> cookie_jar'''-        cookie_jar' = foldl folding_function cookie_jar matching_cookies---- | This applies 'receiveSetCookie' to a given Response-updateCookieJar :: Res.Response a               -- ^ Response received from server-                -> Req.Request m                -- ^ Request which generated the response-                -> UTCTime                      -- ^ Value that should be used as \"now\"-                -> CookieJar                    -- ^ Current cookie jar-                -> (CookieJar, Res.Response a)  -- ^ (Updated cookie jar with cookies from the Response, The response stripped of any \"Set-Cookie\" header)-updateCookieJar response request now cookie_jar = (cookie_jar', response {Res.responseHeaders = other_headers})-  where (set_cookie_headers, other_headers) = L.partition ((== (CI.mk $ U.fromString "Set-Cookie")) . fst) $ Res.responseHeaders response-        set_cookie_data = map snd set_cookie_headers-        set_cookies = map parseSetCookie set_cookie_data-        cookie_jar' = foldl (\ cj sc -> receiveSetCookie sc request now True cj) cookie_jar set_cookies---- | This corresponds to the algorithm described in Section 5.3 \"Storage Model\"-receiveSetCookie :: SetCookie      -- ^ The 'SetCookie' the cookie jar is receiving-                 -> Req.Request m  -- ^ The request that originated the response that yielded the 'SetCookie'-                 -> UTCTime        -- ^ Value that should be used as \"now\"-                 -> Bool           -- ^ Whether or not this request is coming from an \"http\" source (not javascript or anything like that)-                 -> CookieJar      -- ^ Input cookie jar to modify-                 -> CookieJar      -- ^ Updated cookie jar-receiveSetCookie set_cookie request now is_http_api cookie_jar = case result of-  Nothing -> cookie_jar-  Just cookie_jar' -> cookie_jar'-  where result :: Maybe CookieJar-        result = do-          cookie <- generateCookie-          (cookie_jar', cookie') <- existanceTest cookie cookie_jar-          return $ insertIntoCookieJar cookie' cookie_jar'-        generateCookie :: Maybe Cookie-        generateCookie = do-          domain_sanitized <- sanitizeDomain $ step4 (setCookieDomain set_cookie)-          domain_intermediate <- step5 domain_sanitized-          (domain_final, host_only') <- step6 domain_intermediate-          http_only' <- step10-          return $ Cookie { cookie_name = setCookieName set_cookie-                          , cookie_value = setCookieValue set_cookie-                          , cookie_expiry_time = getExpiryTime (setCookieExpires set_cookie) (setCookieMaxAge set_cookie)-                          , cookie_domain = domain_final-                          , cookie_path = getPath $ setCookiePath set_cookie-                          , cookie_creation_time = now-                          , cookie_last_access_time = now-                          , cookie_persistent = getPersistent-                          , cookie_host_only = host_only'-                          , cookie_secure_only = setCookieSecure set_cookie-                          , cookie_http_only = http_only'-                          }-        sanitizeDomain domain'-          | has_a_character && BS.singleton (BS.last domain') == U.fromString "." = Nothing-          | has_a_character && BS.singleton (BS.head domain') == U.fromString "." = Just $ BS.tail domain'-          | otherwise = Just $ domain'-          where has_a_character = not (BS.null domain')-        step4 (Just set_cookie_domain) = set_cookie_domain-        step4 Nothing = BS.empty-        step5 domain'-          | firstCondition && domain' == (Req.host request) = return BS.empty-          | firstCondition = Nothing-          | otherwise = return domain'-          where firstCondition = rejectPublicSuffixes && isPublicSuffix domain'-        step6 domain'-          | firstCondition && not (domainMatches (Req.host request) domain') = Nothing-          | firstCondition = return (domain', False)-          | otherwise = return (Req.host request, True)-          where firstCondition = not $ BS.null domain'-        step10-          | not is_http_api && setCookieHttpOnly set_cookie = Nothing-          | otherwise = return $ setCookieHttpOnly set_cookie-        getExpiryTime :: Maybe UTCTime -> Maybe DiffTime -> UTCTime-        getExpiryTime _ (Just t) = (fromRational $ toRational t) `addUTCTime` now-        getExpiryTime (Just t) Nothing = t-        getExpiryTime Nothing Nothing= UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)-        getPath (Just p) = p-        getPath Nothing = defaultPath request-        getPersistent = isJust (setCookieExpires set_cookie) || isJust (setCookieMaxAge set_cookie)-        existanceTest cookie cookie_jar' = existanceTestHelper cookie $ removeExistingCookieFromCookieJar cookie cookie_jar'-        existanceTestHelper new_cookie (Just old_cookie, cookie_jar')-          | not is_http_api && cookie_http_only old_cookie = Nothing-          | otherwise = return (cookie_jar', new_cookie {cookie_creation_time = cookie_creation_time old_cookie})-        existanceTestHelper new_cookie (Nothing, cookie_jar') = return (cookie_jar', new_cookie)
Network/HTTP/Conduit/Request.hs view
@@ -214,8 +214,11 @@ data HttpException = StatusCodeException W.Status W.ResponseHeaders                    | InvalidUrlException String String                    | TooManyRedirects+                   | UnparseableRedirect+                   | TooManyRetries                    | HttpParserException String                    | HandshakeFailed+                   | OverlongHeaders     deriving (Show, Typeable) instance Exception HttpException @@ -311,11 +314,6 @@     builder =             fromByteString (method req)             <> fromByteString " "-            <> (case proxy req of-                    Just{} ->-                        fromByteString (if secure req then "https://" else "http://")-                        <> fromByteString hh-                    Nothing -> mempty)             <> (case S8.uncons $ path req of                     Just ('/', _) -> fromByteString $ path req                     _ -> fromByteString "/" <> fromByteString (path req))
Network/HTTP/Conduit/Response.hs view
@@ -13,6 +13,9 @@ import Data.Typeable (Typeable) import Data.Monoid (mempty) +import Control.Exception (throwIO)+import Control.Monad.IO.Class (liftIO)+ import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L @@ -104,26 +107,42 @@         { responseBody = L.fromChunks bss         } +checkHeaderLength :: ResourceIO m => Int -> C.Sink S8.ByteString m a -> C.Sink S8.ByteString m a+checkHeaderLength len0 (C.SinkData pushI0 closeI0) =+    C.SinkData (push len0 pushI0) closeI0+  where+    push len pushI bs = do+        res <- pushI bs+        case res of+            C.Processing pushI' close+                | len' <= 0 -> liftIO $ throwIO OverlongHeaders+                | otherwise -> return $ C.Processing+                    (push len' pushI') close+            C.Done a b -> return $ C.Done a b+      where+        len' = len - S8.length bs+checkHeaderLength _ _ = error "checkHeaderLength"+ getResponse :: ResourceIO m             => ConnRelease m             -> Request m             -> C.BufferedSource m S8.ByteString             -> ResourceT m (Response (C.Source m S8.ByteString)) getResponse connRelease req@(Request {..}) bsrc = do-    ((_, sc, sm), hs) <- bsrc C.$$ sinkHeaders+    ((_, sc, sm), hs) <- bsrc C.$$ checkHeaderLength 4096 sinkHeaders     let s = W.Status sc sm     let hs' = map (first CI.mk) hs     let mcl = lookup "content-length" hs' >>= readDec . S8.unpack      -- should we put this connection back into the connection manager?     let toPut = Just "close" /= lookup "connection" hs'-    let cleanup = connRelease $ if toPut then Reuse else DontReuse+    let cleanup bodyConsumed = connRelease $ if toPut && bodyConsumed then Reuse else DontReuse      -- RFC 2616 section 4.4_1 defines responses that must not include a body     body <-         if hasNoBody method sc || mcl == Just 0             then do-                cleanup+                cleanup True                 return mempty             else do                 let bsrc' =@@ -144,18 +163,18 @@ -- | Add some cleanup code to the given 'C.Source'. General purpose -- function, could be included in conduit itself. addCleanup :: C.ResourceIO m-           => ResourceT m ()+           => (Bool -> ResourceT m ())            -> C.Source m a            -> C.Source m a addCleanup cleanup src = src     { C.sourcePull = do         res <- C.sourcePull src         case res of-            C.Closed -> cleanup >> return C.Closed+            C.Closed -> cleanup True >> return C.Closed             C.Open src' val -> return $ C.Open                 (addCleanup cleanup src')                 val     , C.sourceClose = do         C.sourceClose src-        cleanup+        cleanup False     }
http-conduit.cabal view
@@ -1,5 +1,5 @@ name:            http-conduit-version:         1.2.5+version:         1.2.6 license:         BSD3 license-file:    LICENSE author:          Michael Snoyman <michael@snoyman.com>@@ -47,12 +47,14 @@                  , time                  , cookie                >= 0.4     && < 0.5                  , regex-compat+                 , mtl     if flag(network-bytestring)         build-depends: network               >= 2.2.1   && < 2.2.3                      , network-bytestring    >= 0.1.3   && < 0.1.4     else         build-depends: network               >= 2.3     && < 2.4     exposed-modules: Network.HTTP.Conduit+                     Network.HTTP.Conduit.Browser     other-modules:   Network.HTTP.Conduit.Parser                      Network.HTTP.Conduit.ConnInfo                      Network.HTTP.Conduit.Request@@ -61,7 +63,6 @@                      Network.HTTP.Conduit.Chunk                      Network.HTTP.Conduit.Response                      Network.HTTP.Conduit.Cookies-                     Network.HTTP.Conduit.Cookies.Internal     ghc-options:     -Wall  test-suite test@@ -106,6 +107,7 @@                  , http-types                  , cookie                  , regex-compat+                 , network-conduit  source-repository head   type:     git
test/main.hs view
@@ -8,9 +8,14 @@ import Network.HTTP.Conduit import Control.Concurrent (forkIO, killThread) import Network.HTTP.Types-import Control.Exception (try, SomeException)+import Control.Exception.Lifted (try, SomeException) import Network.HTTP.Conduit.ConnInfo import CookieTest (cookieTest)+import Data.Conduit.Network (runTCPServer, ServerSettings (..))+import Data.Conduit (($$))+import Control.Monad.Trans.Resource (register)+import Control.Monad.IO.Class (liftIO)+import Data.Conduit.List (sourceList)  app :: Application app req =@@ -49,3 +54,19 @@             requireAllSocketsClosed             killThread tid2             killThread tid1+    describe "DOS protection" $ do+        it "overlong headers" $ do+            tid1 <- forkIO overLongHeaders+            withManager $ \manager -> do+                _ <- register $ killThread tid1+                let Just req1 = parseUrl "http://localhost:3004/"+                res1 <- try $ http req1 manager+                case res1 of+                    Left e -> liftIO $ show (e :: SomeException) @?= show OverlongHeaders+                    _ -> error "Shouldn't have worked"++overLongHeaders :: IO ()+overLongHeaders = runTCPServer (ServerSettings 3004 Nothing) $ \_ sink ->+    src $$ sink+  where+    src = sourceList $ "HTTP/1.0 200 OK\r\nfoo: " : repeat "bar"