{-# LANGUAGE OverloadedStrings #-}
module Network.HTTP.Client.Cookies
( updateCookieJar
, receiveSetCookie
, generateCookie
, insertCheckedCookie
, insertCookiesIntoRequest
, computeCookieString
, evictExpiredCookies
, createCookieJar
, destroyCookieJar
, pathMatches
, removeExistingCookieFromCookieJar
, domainMatches
, isIpAddress
, defaultPath
) where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as S8
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 qualified Network.PublicSuffixList.Lookup as PSL
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Network.HTTP.Client.Types as Req
slash :: Integral a => a
slash = 47
isIpAddress :: BS.ByteString -> Bool
isIpAddress =
go (4 :: Int)
where
go 0 bs = BS.null bs
go rest bs =
case S8.readInt x of
Just (i, x') | BS.null x' && i >= 0 && i < 256 -> go (rest - 1) y
_ -> False
where
(x, y') = BS.break (== 46) bs
y = BS.drop 1 y'
domainMatches :: BS.ByteString
-> BS.ByteString
-> Bool
domainMatches string' domainString'
| string == domainString = True
| BS.length string < BS.length domainString + 1 = False
| domainString `BS.isSuffixOf` string && BS.singleton (BS.last difference) == "." && not (isIpAddress string) = True
| otherwise = False
where difference = BS.take (BS.length string - BS.length domainString) string
string = CI.foldCase string'
domainString = CI.foldCase domainString'
defaultPath :: Req.Request -> BS.ByteString
defaultPath req
| BS.null uri_path = "/"
| BS.singleton (BS.head uri_path) /= "/" = "/"
| BS.count slash uri_path <= 1 = "/"
| otherwise = BS.reverse $ BS.tail $ BS.dropWhile (/= slash) $ BS.reverse uri_path
where uri_path = Req.path req
pathMatches :: BS.ByteString -> BS.ByteString -> Bool
pathMatches requestPath cookiePath
| cookiePath == path' = True
| cookiePath `BS.isPrefixOf` path' && BS.singleton (BS.last cookiePath) == "/" = True
| cookiePath `BS.isPrefixOf` path' && BS.singleton (BS.head remainder) == "/" = True
| otherwise = False
where remainder = BS.drop (BS.length cookiePath) requestPath
path' = case S8.uncons requestPath of
Just ('/', _) -> requestPath
_ -> '/' `S8.cons` requestPath
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
rejectPublicSuffixes :: Bool
rejectPublicSuffixes = True
isPublicSuffix :: BS.ByteString -> Bool
isPublicSuffix = PSL.isSuffix . decodeUtf8With lenientDecode
evictExpiredCookies :: CookieJar
-> UTCTime
-> CookieJar
evictExpiredCookies cookie_jar' now = CJ $ filter (\ cookie -> cookie_expiry_time cookie >= now) $ expose cookie_jar'
insertCookiesIntoRequest :: Req.Request
-> CookieJar
-> UTCTime
-> (Req.Request, CookieJar)
insertCookiesIntoRequest request cookie_jar now
| BS.null cookie_string = (request, cookie_jar')
| otherwise = (request {Req.requestHeaders = cookie_header : purgedHeaders}, cookie_jar')
where purgedHeaders = L.deleteBy (\ (a, _) (b, _) -> a == b) (CI.mk $ "Cookie", BS.empty) $ Req.requestHeaders request
(cookie_string, cookie_jar') = computeCookieString request cookie_jar now True
cookie_header = (CI.mk $ "Cookie", cookie_string)
computeCookieString :: Req.Request
-> CookieJar
-> UTCTime
-> Bool
-> (BS.ByteString, CookieJar)
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 = CI.foldCase (Req.host request) == CI.foldCase (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
updateCookieJar :: Response a
-> Request
-> UTCTime
-> CookieJar
-> (CookieJar, Response a)
updateCookieJar response request now cookie_jar = (cookie_jar', response { responseHeaders = other_headers })
where (set_cookie_headers, other_headers) = L.partition ((== (CI.mk $ "Set-Cookie")) . fst) $ 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
receiveSetCookie :: SetCookie
-> Req.Request
-> UTCTime
-> Bool
-> CookieJar
-> CookieJar
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
insertCheckedCookie :: Cookie
-> CookieJar
-> Bool
-> CookieJar
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)
generateCookie :: SetCookie
-> Req.Request
-> UTCTime
-> Bool
-> Maybe 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') == "." = Nothing
| has_a_character && BS.singleton (BS.head domain') == "." = 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 && has_a_character && isPublicSuffix domain'
has_a_character = not (BS.null 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 (365000 `addDays` utctDay now) (secondsToDiffTime 0)
getPath (Just p) = p
getPath Nothing = defaultPath request
getPersistent = isJust (setCookieExpires set_cookie) || isJust (setCookieMaxAge set_cookie)