packages feed

HAppS-Server-0.9.2: src/HAppS/Server/Cookie.hs

{-# OPTIONS -fglasgow-exts #-}
-- http://tools.ietf.org/html/rfc2109
module HAppS.Server.Cookie
    ( Cookie(..), mkCookie, mkCookieHeader
    , getCookies, getCookie )
    where
{-
    (Cookie(..), setCookie, setCookieEx,
     sesCookie, delSesCookie, delSesCookie'
--     getCookies, getCookie, getCookieValue
    ) where
-}
import Control.Monad(when, zipWithM_)
import qualified Data.ByteString.Char8 as C
import Data.Char
import Data.List
import Data.Generics
import HAppS.Util.Common (Seconds)
--import HAppS.Server.HTTP.Types
--import HAppS.State
import Text.ParserCombinators.ReadP

data Cookie = Cookie
    { cookieVersion :: String
    , cookiePath    :: String
    , cookieDomain  :: String
    , cookieName    :: String
    , cookieValue   :: String
    } deriving(Show,Eq,Read,Typeable,Data)

mkCookie :: String -> String -> Cookie
mkCookie key val = Cookie "1" "/" "" key val

{-
-- | Set a cookie in the Result with empty path and domain.
--   See 'setCookieEx' for more details.
--setCookie :: Monad m => Seconds -> String -> String -> Result -> m Result
--setCookie sec key val = setCookieEx sec (Cookie "1" "" "" key val)

-- | Set a session cookie.
--   This should not live over browser restarts.
sesCookie :: Monad m => String -> String -> Result -> m Result
sesCookie key val = setCookieEx (-1) (Cookie "1" "/" "" key val)

-- | Delete a session cookie.
delSesCookie :: Monad m => String -> Result -> m Result
delSesCookie key = setCookieEx 0 (Cookie "1" "/" "" key "")

-}

-- | Set a Cookie in the Result.
-- The values are escaped as per RFC 2109, but some browsers may
-- have buggy support for cookies containing e.g. @\'\"\'@ or @\' \'@.
mkCookieHeader :: Seconds -> Cookie -> String
mkCookieHeader sec cookie =
    let l = [("Domain=",s cookieDomain)
            ,("Max-Age=",if sec < 0 then "" else show sec)
            ,("Path=", cookiePath cookie)
            ,("Version=", s cookieVersion)]
        s f | f cookie == "" = ""
        s f   = '\"' : concatMap e (f cookie) ++ "\""
        e c | fctl c || c == '"' = ['\\',c]
            | otherwise          = [c]
    in concat $ intersperse ";" ((cookieName cookie++"="++s cookieValue):[ (k++v) | (k,v) <- l, "" /= v ])

-- FIXME: validate at cookie creation.
validateCookie :: Monad m => Cookie -> m ()
validateCookie cookie
    = do when (null key || any (not . ftoken) key) $ fail ("Invalid cookie name: "++show key)
         let f n xs = when (any (not . fchar) xs) $ fail ("setCookieEx: "++n++": invalid character in value: "++show (xs))
         zipWithM_ f ["cookieValue","cookieDomain","cookieVersion"] $ map ($ cookie) [cookieValue, cookieDomain, cookieVersion]
         return ()
    where key = cookieName cookie

{-
-- | Deprecated. This will be deleted in future.
delSesCookie' :: (Monad m) => String -> m Result -> m Result
delSesCookie' key res = setCookieEx 0 (Cookie "1" "/" "" key "") =<< res
-}

{--setCookieEx' sec cook r0 = 
  let key = cookieName cook
      w = concat $ intersperse ";" ((key++"="++s cookieValue):[ (k++v) | (k,v) <- l, "" /= v ])
      l = [("Domain=",s cookieDomain)
          ,("Max-Age=",if sec < 0 then "" else show sec)
          ,("Path=", cookiePath cook)
          ,("Version=", s cookieVersion)]
      s f | f cook == "" = ""
      s f   = '\"' : concatMap e (f cook) ++ "\""
      e c = case () of
              _ | fctl c || c == '"' -> ['\\',c]
                | otherwise          -> [c]
      new = maybe w (\ss -> w++"\r\nSet-Cookie: "++C.unpack ss) $ getRsHeader "Set-Cookie" r0
      in 
      setHeader "Set-Cookie" new r0

delSesCookie' key = setCookieEx' 0 (Cookie "1" "/" "" key "")
--}
{- Cookie syntax:
   av-pairs        =       av-pair *(";" av-pair)
   av-pair         =       attr ["=" value]        ; optional value
   attr            =       token
   value           =       word
   word            =       token | quoted-string
-}

gmany  p = gmany1 p <++ return []
gmany1 p = do x  <- p
              xs <- gmany1 p <++ return []
              return (x:xs)
gskipMany1 p = p >> (gskipMany p <++ return ())
gskipMany  p = gskipMany1 p <++ return ()

fctl         = \ch -> ch == chr 127 || ch <= chr 31
fseparator   = \ch -> ch `elem` "()<>@,;:\\\"[]?={} \t" -- ignore '/' here
fchar        = \ch -> ch <= chr 127
ftoken       = \ch -> fchar ch && not (fctl ch || fseparator ch)
lws          = ((char '\r' >> char '\n') <++ return ' ') >> gskipMany (satisfy (\ch -> ch == ' ' || ch == '\t'))
token        = gmany $ satisfy ftoken
quotedString = do char '"'  -- " stupid emacs syntax highlighting
                  x <- many ((char '\\' >> satisfy fchar) <++ (satisfy $ \ch -> ch /= '"' && fchar ch && (ch == ' ' || ch == '\t' || not (fctl ch))))
                  char '"' -- " stupid emacs syntax highlighting
                  return x
word = quotedString <++ token

avPair = do
  k <- token
  lws >> char '=' >> lws
  v <- word
  return (low k,v)

sep = lws >> satisfy (\ch -> ch == ',' || ch == ';') >> lws

cookies :: ReadP [Cookie]
cookies = do
  let kpw n = do lws
                 (k,v) <- avPair
                 if k == n then return v else fail "Invalid key"
  ver <- ((kpw "$version" <~ sep) <++ return "")
  let ci = do (k,v) <- avPair
              p <- (sep >> kpw "$path")   <++ return ""
              d <- (sep >> kpw "$domain") <++ return ""
              return $ Cookie ver p d k v
  x  <- lws >> ci
  xs <- gmany (sep >> ci) <~ lws
  return (x:xs)

(<~) :: Monad m => m a -> m b -> m a
(<~) a b = do x <- a; b; return x

{- Debugging stuff
-- deb x = look >>= \s -> trace ("parse @ "++x++ ": "++show s) (return ())

t = mapM_ (\c -> putStrLn "" >> putStrLn c >> parse c >>= print)  cs
q a b = C.putStrLn  =<< getRsHeader "Set-Cookie" =<< setCookie 400 a b =<< sresult 200 "ok"
cs = ["$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\""
     ,"$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\""
     ,"$Version=\"1\"; Customer=\"WILE_\"; $Path=\"/acme\"; Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; Shipping=\"FedEx\"; $Path=\"/acme\""
     ,"$Version=\"1\";           Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\";       Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\""
     , "foo=bar"
     , "foo=\"bar\""
     , "foo=\"bar\\Zquote\""
     , "ses=; foo=\"bar\""
     , "foo=\"bar\"; ses="
     , "email=\"abcdefghi333@alexjacobson.com\"; session=427922315; ses=\"hii\""
     , "Path=/; ses=\"hii\"; session=427922315; email=\"abcdefghi333@alexjacobson.com\"; sessionId=\"552550384\""
     ]
-}


parse :: Monad m => String -> m [Cookie]
parse i = case readP_to_S cookies i of
            [(res,"")] -> return res
            xs         -> fail ("Invalid cookie syntax!: at position "++show (length i - length xs)++" input "++show i)

-- | Get all cookies from the HTTP request. The cookies are ordered per RFC from
-- the most specific to the least specific. Multiple cookies with the same
-- name are allowed to exist.
getCookies :: Monad m => C.ByteString -> m [Cookie]
getCookies header | C.null header = return []
                  | otherwise     = parse (C.unpack header)


-- | Get the most specific cookie with the given name. Fails if there is no such
-- cookie or if the browser did not escape cookies in a proper fashion.
-- Browser support for escaping cookies properly is very diverse.
getCookie :: Monad m => String -> C.ByteString -> m Cookie
getCookie s h = do cs <- getCookies h
                   case filter ((==) (low s) . cookieName) cs of
                     [r] -> return r
                     _   -> fail ("getCookie: " ++ show s)

low :: String -> String
low = map toLower
{-
-- | Get the value of the cookie that would be returned by 'getCookie'.
getCookieValue :: Monad m => String -> Request -> m String
getCookieValue s r = return . cookieValue =<< getCookie s r
-}