{-# LANGUAGE OverloadedStrings #-} module Web.Cookie ( -- * Server to client SetCookie (..) , parseSetCookie , renderSetCookie -- * Client to server , Cookies , parseCookies , renderCookies -- * Expires field , expiresFormat , formatCookieExpires , parseCookieExpires ) where import qualified Data.ByteString as S import Blaze.ByteString.Builder.Char8 (fromChar) import Data.Monoid (mempty, mappend, mconcat) import Data.Word (Word8) import Data.Time (UTCTime, formatTime, parseTime) import System.Locale (defaultTimeLocale) import Control.Arrow (first) import qualified Data.Ascii as A type Cookies = [(A.Ascii, A.Ascii)] -- | Decode the value of a \"Cookie\" request header into key/value pairs. parseCookies :: A.Ascii -> Cookies parseCookies = parseCookiesBS . A.toByteString parseCookiesBS :: S.ByteString -> Cookies parseCookiesBS s | S.null s = [] | otherwise = let (x, y) = breakDiscard 59 s -- semicolon in parseCookie x : parseCookiesBS y parseCookie :: S.ByteString -> (A.Ascii, A.Ascii) parseCookie s = let (key, value) = breakDiscard 61 s -- equals sign key' = S.dropWhile (== 32) key -- space in (A.unsafeFromByteString key', A.unsafeFromByteString value) breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString) breakDiscard w s = let (x, y) = S.breakByte w s in (x, S.drop 1 y) renderCookies :: Cookies -> A.AsciiBuilder renderCookies [] = mempty renderCookies cs = foldr1 go $ map renderCookie cs where go x y = x `mappend` A.unsafeFromBuilder (fromChar ';') `mappend` y renderCookie :: (A.Ascii, A.Ascii) -> A.AsciiBuilder renderCookie (k, v) = A.toAsciiBuilder k `mappend` A.unsafeFromBuilder (fromChar '=') `mappend` A.toAsciiBuilder v data SetCookie = SetCookie { setCookieName :: A.Ascii , setCookieValue :: A.Ascii , setCookiePath :: Maybe A.Ascii , setCookieExpires :: Maybe UTCTime , setCookieDomain :: Maybe A.Ascii } deriving (Eq, Show, Read) renderSetCookie :: SetCookie -> A.AsciiBuilder renderSetCookie sc = mconcat [ A.toAsciiBuilder $ setCookieName sc , A.unsafeFromBuilder $ fromChar '=' , A.toAsciiBuilder $ setCookieValue sc , case setCookiePath sc of Nothing -> mempty Just path -> A.toAsciiBuilder "; path=" `mappend` A.toAsciiBuilder path , case setCookieExpires sc of Nothing -> mempty Just e -> A.toAsciiBuilder "; expires=" `mappend` A.toAsciiBuilder (formatCookieExpires e) , case setCookieDomain sc of Nothing -> mempty Just d -> A.toAsciiBuilder "; domain=" `mappend` A.toAsciiBuilder d ] parseSetCookie :: A.Ascii -> SetCookie parseSetCookie a = SetCookie { setCookieName = key , setCookieValue = value , setCookiePath = lookup "path" pairs , setCookieExpires = lookup "expires" pairs >>= parseCookieExpires , setCookieDomain = lookup "domain" pairs } where (key, value, b) = parsePair $ A.toByteString a pairs = map (first $ A.toCIAscii) $ parsePairs b parsePair bs = let (k, bs') = breakDiscard 61 bs -- equals sign (v, bs'') = breakDiscard 59 bs' -- semicolon in (A.unsafeFromByteString k, A.unsafeFromByteString v, S.dropWhile (== 32) bs'') -- space parsePairs bs = if S.null bs then [] else let (k, v, bs') = parsePair bs in (k, v) : parsePairs bs' expiresFormat :: String expiresFormat = "%a, %d-%b-%Y %X GMT" -- | Format a 'UTCTime' for a cookie. formatCookieExpires :: UTCTime -> A.Ascii formatCookieExpires = A.unsafeFromString . formatTime defaultTimeLocale expiresFormat parseCookieExpires :: A.Ascii -> Maybe UTCTime parseCookieExpires = parseTime defaultTimeLocale expiresFormat . A.toString