Initial port to wai and wai-extra

This commit is contained in:
Michael Snoyman 2010-01-31 01:30:32 +02:00
parent 7505d9a054
commit 15712773a0
15 changed files with 139 additions and 449 deletions

View File

@ -1,60 +0,0 @@
module Hack.Middleware.CleanPath (cleanPath, splitPath) where
import Hack
import qualified Data.ByteString.Lazy as BS
import Web.Encodings
import Data.List.Split
-- | Performs redirects as per 'splitPath'.
cleanPath :: Middleware
cleanPath app env =
case splitPath $ pathInfo env of
Left p -> do
-- include the query string if there
let suffix =
case queryString env of
"" -> ""
q@('?':_) -> q
q -> '?' : q
return $! Response 303 [("Location", p ++ suffix)] BS.empty
Right _ -> app env
-- | Given a certain requested path, return either a corrected path
-- to redirect to or the tokenized path.
--
-- This code corrects for the following issues:
--
-- * It is missing a trailing slash, and there is no period after the
-- last slash.
--
-- * There are any doubled slashes.
splitPath :: String -> Either String [String]
splitPath s =
let corrected = ats $ rds s
in if corrected == s
then Right $ map decodeUrl $ filter (not . null)
$ splitOneOf "/" s
else Left corrected
-- | Remove double slashes
rds :: String -> String
rds [] = []
rds [x] = [x]
rds (a:b:c)
| a == '/' && b == '/' = rds (b:c)
| otherwise = a : rds (b:c)
-- | Add a trailing slash if it is missing. Empty string is left alone.
ats :: String -> String
ats [] = []
ats s =
if last s == '/' || dbs (reverse s)
then s
else s ++ "/"
-- | Is there a period before a slash here?
dbs :: String -> Bool
dbs ('/':_) = False
dbs ('.':_) = True
dbs (_:x) = dbs x
dbs [] = False

View File

@ -1,113 +0,0 @@
module Hack.Middleware.ClientSession
( clientsession
-- * Generating keys
, Word256
, defaultKeyFile
, getKey
, getDefaultKey
) where
import Prelude hiding (exp)
import Hack
import Web.Encodings
import Data.List (partition, intercalate)
import Data.Function.Predicate (is, isn't, equals)
import Data.Maybe (fromMaybe, mapMaybe)
import Web.ClientSession
import Data.Time.Clock (getCurrentTime, UTCTime, addUTCTime)
import Data.Time.LocalTime () -- Show instance of UTCTime
import Data.Time.Format (formatTime) -- Read instance of UTCTime
import System.Locale (defaultTimeLocale)
import Control.Monad (guard)
-- | Automatic encrypting and decrypting of client session data.
--
-- Using the clientsession package, this middleware handles automatic
-- encryption, decryption, checking, expiration and renewal of whichever
-- cookies you ask it to. For example, if you tell it to deal with the
-- cookie \"IDENTIFIER\", it will do the following:
--
-- * When you specify an \"IDENTIFIER\" value in your 'Response', it will
-- encrypt the value, along with the session expiration date and the
-- REMOTE_HOST of the user. It will then be set as a cookie on the client.
--
-- * When there is an incoming \"IDENTIFIER\" cookie from the user, it will
-- decrypt it and check both the expiration date and the REMOTE_HOST. If
-- everything matches up, it will set the \"IDENTIFIER\" value in
-- 'hackHeaders'.
--
-- * If the client sent an \"IDENTIFIER\" and the application does not set
-- a new value, this will reset the cookie to a new expiration date. This
-- way, you do not have sessions timing out every 20 minutes.
--
-- As far as security: clientsesion itself handles hashing and encrypting
-- the data to make sure that the user can neither see not tamper with it.
clientsession :: [String] -- ^ list of cookies to intercept
-> Word256 -- ^ encryption key
-> Int -- ^ minutes to live
-> Middleware
clientsession cnames key minutesToLive app env = do
let initCookiesRaw :: String
initCookiesRaw = fromMaybe "" $ lookup "Cookie" $ http env
nonCookies :: [(String, String)]
nonCookies = filter (fst `isn't` (== "Cookie")) $ http env
initCookies :: [(String, String)]
initCookies = decodeCookies initCookiesRaw
cookies, interceptCookies :: [(String, String)]
(interceptCookies, cookies) = partition (fst `is` (`elem` cnames))
initCookies
cookiesRaw :: String
cookiesRaw = intercalate "; " $ map (\(k, v) -> k ++ "=" ++ v)
cookies
remoteHost' :: String
remoteHost' = remoteHost env
now <- getCurrentTime
let convertedCookies =
mapMaybe (decodeCookie key now remoteHost') interceptCookies
let env' = env { http = ("Cookie", cookiesRaw)
: filter (fst `equals` "Cookie") (http env)
++ nonCookies
, hackHeaders = hackHeaders env ++ convertedCookies
}
res <- app env'
let (interceptHeaders, headers') = partition (fst `is` (`elem` cnames))
$ headers res
let timeToLive :: Int
timeToLive = minutesToLive * 60
let exp = fromIntegral timeToLive `addUTCTime` now
let formattedExp = formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" exp
let oldCookies = filter (\(k, _) -> k `notElem` map fst interceptHeaders) convertedCookies
let newCookies = map (setCookie key exp formattedExp remoteHost') $
oldCookies ++ interceptHeaders
let res' = res { headers = newCookies ++ headers' }
return res'
setCookie :: Word256
-> UTCTime -- ^ expiration time
-> String -- ^ formatted expiration time
-> String -- ^ remote host
-> (String, String) -> (String, String)
setCookie key exp fexp rhost (cname, cval) =
("Set-Cookie", cname ++ "=" ++ val ++ "; path=/; expires=" ++ fexp)
where
val = encrypt key $ show $ Cookie exp rhost cval
data Cookie = Cookie UTCTime String String deriving (Show, Read)
decodeCookie :: Word256 -- ^ key
-> UTCTime -- ^ current time
-> String -- ^ remote host field
-> (String, String) -- ^ cookie pair
-> Maybe (String, String)
decodeCookie key now rhost (cname, encrypted) = do
decrypted <- decrypt key encrypted
(Cookie exp rhost' val) <- mread decrypted
guard $ exp > now
guard $ rhost' == rhost
guard $ val /= ""
return (cname, val)
mread :: (Monad m, Read a) => String -> m a
mread s =
case reads s of
[] -> fail $ "Reading of " ++ s ++ " failed"
((x, _):_) -> return x

View File

@ -1,44 +0,0 @@
---------------------------------------------------------
-- |
-- Module : Hack.Middleware.Gzip
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Unstable
-- Portability : portable
--
-- Automatic gzip compression of responses.
--
---------------------------------------------------------
module Hack.Middleware.Gzip (gzip) where
import Hack
import Codec.Compression.GZip (compress)
import Data.Maybe (fromMaybe)
import Data.List.Split (splitOneOf)
-- | Use gzip to compress the body of the response.
--
-- Analyzes the \"Accept-Encoding\" header from the client to determine
-- if gzip is supported.
--
-- Possible future enhancements:
--
-- * Only compress if the response is above a certain size.
--
-- * Add Content-Length.
--
-- * I read somewhere that \"the beast\" (MSIE) can\'t support compression
-- for Javascript files..
gzip :: Middleware
gzip app env = do
res <- app env
let enc = fromMaybe [] $ splitOneOf "," `fmap` lookup "Accept-Encoding"
(http env)
if "gzip" `elem` enc
then return res
{ body = compress $ body res
, headers = ("Content-Encoding", "gzip") : headers res
}
else return res

View File

@ -1,66 +0,0 @@
---------------------------------------------------------
-- |
-- Module : Hack.Middleware.Jsonp
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Unstable
-- Portability : portable
--
-- Automatic wrapping of JSON responses to convert into JSONP.
--
---------------------------------------------------------
module Hack.Middleware.Jsonp (jsonp) where
import Hack
import Web.Encodings (decodeUrlPairs)
import qualified Data.ByteString.Lazy.Char8 as B8
import Data.Maybe (fromMaybe)
import Data.List (isInfixOf)
-- | Wrap json responses in a jsonp callback.
--
-- Basically, if the user requested a \"text\/javascript\" and supplied a
-- \"callback\" GET parameter, ask the application for an
-- \"application/json\" response, then convern that into a JSONP response,
-- having a content type of \"text\/javascript\" and calling the specified
-- callback function.
jsonp :: Middleware
jsonp app env = do
let accept = fromMaybe "" $ lookup "Accept" $ http env
let gets = decodeUrlPairs $ queryString env
let callback :: Maybe String
callback =
if "text/javascript" `isInfixOf` accept
then lookup "callback" gets
else Nothing
let env' =
case callback of
Nothing -> env
Just _ -> env
{ http = changeVal "Accept"
"application/json"
$ http env
}
res <- app env'
let ctype = fromMaybe "" $ lookup "Content-Type" $ headers res
case callback of
Nothing -> return res
Just c ->
case ctype of
"application/json" -> return $ res
{ headers = changeVal "Content-Type"
"text/javascript"
$ headers res
, body = B8.concat
[ B8.pack c -- NOTE uses Latin-1 encoding.
, B8.singleton '('
, body res
, B8.singleton ')'
]
}
_ -> return res
changeVal :: String -> String -> [(String, String)] -> [(String, String)]
changeVal key val old = (key, val) : filter (\(k, _) -> k /= key) old

View File

@ -1,38 +0,0 @@
---------------------------------------------------------
-- |
-- Module : Hack.Middleware.MethodOverride
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Unstable
-- Portability : portable
--
-- Override the HTTP method based on either:
-- The X-HTTP-Method-Override header.
-- The _method_override GET parameter.
--
---------------------------------------------------------
module Hack.Middleware.MethodOverride (methodOverride) where
import Hack
import Web.Encodings (decodeUrlPairs)
import Data.Monoid (mappend)
import Data.Char
methodOverride :: Middleware
methodOverride app env = do
let mo1 = lookup "X-HTTP-Method-Override" $ http env
gets = decodeUrlPairs $ queryString env
mo2 = lookup "_method_override" gets
cm = requestMethod env
app $
case mo1 `mappend` mo2 of
Nothing -> env
Just nm -> env { requestMethod = safeRead cm $ map toUpper nm }
safeRead :: Read a => a -> String -> a
safeRead d s =
case reads s of
((x, _):_) -> x
[] -> d

View File

@ -3,12 +3,13 @@ module Test.Errors (testSuite) where
import Yesod
import Yesod.Helpers.Auth
import Hack
import Network.Wai
import Data.Default
import Data.List
import Test.Framework (testGroup, Test)
import Test.Framework.Providers.HUnit
import Test.HUnit hiding (Test)
import qualified Data.ByteString.Char8 as B8
data Errors = Errors
instance Yesod Errors where
@ -42,12 +43,14 @@ hasArgs = do
return (cs "", cs [a :: String, b])
caseErrorMessages :: Assertion
caseErrorMessages = do
app <- toHackApp Errors
res <- app $ def { pathInfo = "/denied/" }
caseErrorMessages = do return ()
{- FIXME
app <- toWaiApp Errors
res <- app $ def { pathInfo = B8.pack "/denied/" }
assertBool "/denied/" $ "Permission denied" `isInfixOf` show res
res' <- app $ def { pathInfo = "/needs-ident/" }
res' <- app $ def { pathInfo = B8.pack "/needs-ident/" }
assertBool "/needs-ident/" $ "IGNORED/auth/openid/" `isInfixOf` show res'
-}
{- FIXME this test is not yet ready
res3 <- app $ def { pathInfo = "/has-args/" }
assertBool "/has-args/" $ "secondParam" `isInfixOf` show res3

View File

@ -3,12 +3,14 @@
-- | Generic MIME type module. Could be spun off into its own package.
module Web.Mime
( ContentType (..)
, contentTypeFromBS
, typeByExt
, ext
) where
import Data.Function (on)
import Data.Convertible.Text
import Data.ByteString.Char8 (pack, ByteString, unpack)
data ContentType =
TypeHtml
@ -27,6 +29,9 @@ data ContentType =
| TypeOther String
deriving (Show)
instance ConvertSuccess ContentType ByteString where
convertSuccess = pack . cs
instance ConvertSuccess ContentType [Char] where
convertSuccess TypeHtml = "text/html; charset=utf-8"
convertSuccess TypePlain = "text/plain; charset=utf-8"
@ -46,6 +51,9 @@ instance ConvertSuccess ContentType [Char] where
instance Eq ContentType where
(==) = (==) `on` (cs :: ContentType -> String)
contentTypeFromBS :: ByteString -> ContentType
contentTypeFromBS = TypeOther . unpack
-- | Determine a mime-type based on the file extension.
typeByExt :: String -> ContentType
typeByExt "jpg" = TypeJpeg

View File

@ -41,6 +41,6 @@ import Yesod.Request
import Yesod.Yesod
import Yesod.Definitions
import Yesod.Handler
import Hack (Application)
import Network.Wai (Application)
import Yesod.Template
import Web.Mime

View File

@ -28,12 +28,14 @@ module Yesod.Definitions
, langKey
) where
import qualified Hack
import qualified Network.Wai as W
import Data.Convertible.Text
import Control.Exception (Exception)
import Data.Typeable (Typeable)
import Language.Haskell.TH.Syntax
import Data.ByteString.Char8 (pack, ByteString)
-- FIXME replace with Method?
data Verb = Get | Put | Delete | Post
deriving (Eq, Show, Enum, Bounded)
instance Lift Verb where
@ -48,10 +50,10 @@ newtype InvalidVerb = InvalidVerb String
deriving (Show, Typeable)
instance Exception InvalidVerb
instance ConvertSuccess Hack.RequestMethod Verb where
convertSuccess Hack.PUT = Put
convertSuccess Hack.DELETE = Delete
convertSuccess Hack.POST = Post
instance ConvertSuccess W.Method Verb where
convertSuccess W.PUT = Put
convertSuccess W.DELETE = Delete
convertSuccess W.POST = Post
convertSuccess _ = Get
type Resource = [String]
@ -78,8 +80,8 @@ authCookieName = "IDENTIFIER"
authDisplayName :: String
authDisplayName = "DISPLAY_NAME"
encryptedCookies :: [String]
encryptedCookies = [authDisplayName, authCookieName]
encryptedCookies :: [ByteString]
encryptedCookies = [pack authDisplayName, pack authCookieName]
langKey :: String
langKey = "_LANG"

View File

@ -50,6 +50,7 @@ import Control.Monad (liftM, ap)
import System.IO
import Data.Object.Html
import qualified Data.ByteString.Lazy as BL
import qualified Network.Wai as W
data HandlerData yesod = HandlerData RawRequest yesod
@ -110,9 +111,8 @@ runHandler handler eh rr y cts = do
let hs' = headers ++ hs
return $ Response (getStatus e) hs' ct c
let sendFile' ct fp = do
-- avoid lazy I/O by switching to WAI
c <- BL.readFile fp
return $ Response 200 headers ct $ cs c
return $ Response W.Status200 headers ct $ cs c
case contents of
HCError e -> handleError e
HCSpecial (Redirect rt loc) -> do
@ -123,7 +123,7 @@ runHandler handler eh rr y cts = do
(handleError . toErrorHandler)
HCContent a -> do
(ct, c) <- a cts
return $ Response 200 headers ct c
return $ Response W.Status200 headers ct c
safeEh :: ErrorResponse -> Handler yesod ChooseRep
safeEh er = do

View File

@ -29,9 +29,10 @@ import qualified Web.Authenticate.OpenId as OpenId
import Yesod
import Control.Monad.Attempt
import qualified Data.ByteString.Char8 as B8
import Data.Maybe (fromMaybe)
import qualified Hack
import qualified Network.Wai
import Data.Typeable (Typeable)
import Control.Exception (Exception, SomeException (..))
@ -221,18 +222,14 @@ authLogout = do
-- | Gets the identifier for a user if available.
maybeIdentifier :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
maybeIdentifier = do
env <- parseEnv
case lookup authCookieName $ Hack.hackHeaders env of
Nothing -> return Nothing
Just x -> return (Just x)
rr <- getRawRequest
return $ fmap cs $ lookup (B8.pack authCookieName) $ rawSession rr
-- | Gets the display name for a user if available.
displayName :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
displayName = do
env <- parseEnv
case lookup authDisplayName $ Hack.hackHeaders env of
Nothing -> return Nothing
Just x -> return (Just x)
rr <- getRawRequest
return $ fmap cs $ lookup (B8.pack authDisplayName) $ rawSession rr
-- | Gets the identifier for a user. If user is not logged in, redirects them
-- to the login page.
@ -254,11 +251,11 @@ authIdentifier = do
requestPath :: (Functor m, Monad m, RequestReader m) => m String
requestPath = do
env <- parseEnv
let q = case Hack.queryString env of
let q = case B8.unpack $ Network.Wai.queryString env of
"" -> ""
q'@('?':_) -> q'
q' -> '?' : q'
return $! dropSlash (Hack.pathInfo env) ++ q
return $! dropSlash (B8.unpack $ Network.Wai.pathInfo env) ++ q
where
dropSlash ('/':x) = x
dropSlash x = x

View File

@ -26,8 +26,7 @@ module Yesod.Request
, getParams
, postParams
, languages
-- * Building actual request
, Hack.RequestMethod (..)
, parseWaiRequest
-- * Parameter
, ParamType (..)
, ParamName
@ -38,10 +37,12 @@ module Yesod.Request
#endif
) where
import qualified Hack
import qualified Network.Wai as W
import qualified Network.Wai.Enumerator as WE
import Data.Function.Predicate (equals)
import Yesod.Definitions
import Web.Encodings
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Convertible.Text
import Control.Arrow ((***))
@ -50,8 +51,8 @@ import Data.Maybe (fromMaybe)
#if TEST
import Test.Framework (testGroup, Test)
import Test.Framework.Providers.HUnit
import Test.HUnit hiding (Test)
--import Test.Framework.Providers.HUnit
--import Test.HUnit hiding (Test)
#endif
data ParamType = GetParam | PostParam
@ -66,22 +67,22 @@ class RequestReader m where
languages :: (Functor m, RequestReader m) => m [Language]
languages = rawLangs `fmap` getRawRequest
-- | Get the raw 'Hack.Env' value.
parseEnv :: (Functor m, RequestReader m) => m Hack.Env
parseEnv = rawEnv `fmap` getRawRequest
-- | Get the raw 'W.Env' value.
parseEnv :: (Functor m, RequestReader m) => m W.Request
parseEnv = rawRequest `fmap` getRawRequest
-- | The raw information passed through Hack, cleaned up a bit.
-- | The raw information passed through W, cleaned up a bit.
data RawRequest = RawRequest
{ rawGetParams :: [(ParamName, ParamValue)]
, rawCookies :: [(ParamName, ParamValue)]
, rawSession :: [(B.ByteString, B.ByteString)]
-- when we switch to WAI, the following two should be combined and
-- wrapped in the IO monad
, rawPostParams :: [(ParamName, ParamValue)]
, rawFiles :: [(ParamName, FileInfo String BL.ByteString)]
, rawEnv :: Hack.Env
, rawRequest :: W.Request
, rawLangs :: [Language]
}
deriving Show
-- | All GET paramater values with the given name.
getParams :: RawRequest -> ParamName -> [ParamValue]
@ -101,27 +102,29 @@ postParams rr name = map snd
cookies :: RawRequest -> ParamName -> [ParamValue]
cookies rr name = map snd . filter (fst `equals` name) . rawCookies $ rr
instance ConvertSuccess Hack.Env RawRequest where
convertSuccess env =
let gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
clength = fromMaybe "0" $ lookup "Content-Length" $ Hack.http env
ctype = fromMaybe "" $ lookup "Content-Type" $ Hack.http env
convertFileInfo (FileInfo a b c) = FileInfo (cs a) (cs b) c
(posts, files) = map (convertSuccess *** convertSuccess) ***
parseWaiRequest :: W.Request -> [(B.ByteString, B.ByteString)] -> IO RawRequest
parseWaiRequest env session = do
let gets' = map (cs *** cs) $ decodeUrlPairs $ W.queryString env
let clength = maybe "0" cs $ lookup W.ReqContentLength
$ W.httpHeaders env
let ctype = maybe "" cs $ lookup W.ReqContentType $ W.httpHeaders env
let convertFileInfo (FileInfo a b c) = FileInfo (cs a) (cs b) c
inputLBS <- WE.toLBS $ W.requestBody env -- FIXME
let (posts, files) = map (convertSuccess *** convertSuccess) ***
map (convertSuccess *** convertFileInfo)
$ parsePost ctype clength
$ Hack.hackInput env
rawCookie = fromMaybe "" $ lookup "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)]
acceptLang = lookup "Accept-Language" $ Hack.http env
langs = maybe [] parseHttpAccept acceptLang
inputLBS
rawCookie = fromMaybe B.empty $ lookup W.Cookie $ W.httpHeaders env
cookies' = map (cs *** cs) $ decodeCookies rawCookie
acceptLang = lookup W.AcceptLanguage $ W.httpHeaders env
langs = map cs $ maybe [] parseHttpAccept acceptLang
langs' = case lookup langKey cookies' of
Nothing -> langs
Just x -> x : langs
langs'' = case lookup langKey gets' of
Nothing -> langs'
Just x -> x : langs'
in RawRequest gets' cookies' posts files env langs''
return $ RawRequest gets' cookies' session posts files env langs''
#if TEST
testSuite :: Test

View File

@ -39,8 +39,8 @@ module Yesod.Response
-- * Header
, Header (..)
, headerToPair
-- * Converting to Hack values
, responseToHackResponse
-- * Converting to WAI values
, responseToWaiResponse
#if TEST
-- * Tests
, testSuite
@ -50,15 +50,16 @@ module Yesod.Response
import Data.Time.Clock
import Data.Maybe (mapMaybe)
import Data.ByteString.Lazy (ByteString, toChunks, fromChunks)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.Text.Lazy (Text)
import qualified Data.Text as T
import Data.Object.Json
import Control.Monad (foldM)
import qualified Data.Text.Lazy.Encoding as DTLE
import Web.Encodings (formatW3)
import qualified Hack
import qualified Network.Wai as W
import qualified Network.Wai.Enumerator as WE
#if TEST
import Data.Object.Html hiding (testSuite)
@ -72,16 +73,17 @@ import Test.Framework (testGroup, Test)
import Web.Mime
data Content = Content (forall a. ((a -> B.ByteString -> IO a) -> a -> IO a))
data Content = ContentFile FilePath
| ContentEnum (forall a. W.Enumerator a)
instance ConvertSuccess B.ByteString Content where
convertSuccess bs = Content $ \f a -> f a bs
instance ConvertSuccess ByteString Content where
convertSuccess lbs = Content $ \f a -> foldM f a $ toChunks lbs
convertSuccess bs = ContentEnum $ \f a -> f a bs
instance ConvertSuccess L.ByteString Content where
convertSuccess = ContentEnum . WE.fromLBS
instance ConvertSuccess T.Text Content where
convertSuccess t = cs (cs t :: B.ByteString)
instance ConvertSuccess Text Content where
convertSuccess lt = cs (cs lt :: ByteString)
convertSuccess lt = cs (cs lt :: L.ByteString)
instance ConvertSuccess String Content where
convertSuccess s = cs (cs s :: Text)
instance ConvertSuccess HtmlDoc Content where
@ -94,8 +96,7 @@ type ChooseRep = [ContentType] -> IO (ContentType, Content)
-- | It would be nice to simplify 'Content' to the point where this is
-- unnecesary.
ioTextToContent :: IO Text -> Content
ioTextToContent iotext =
Content $ \f a -> iotext >>= foldM f a . toChunks . cs
ioTextToContent t = ContentEnum $ WE.fromLBS' $ fmap DTLE.encodeUtf8 t
-- | Any type which can be converted to representations.
class HasReps a where
@ -138,13 +139,13 @@ instance HasReps (Html, HtmlObject) where
]
-- | Data with a single representation.
staticRep :: ConvertSuccess x ByteString
staticRep :: ConvertSuccess x Content
=> ContentType
-> x
-> [(ContentType, Content)]
staticRep ct x = [(ct, cs (cs x :: ByteString))]
staticRep ct x = [(ct, cs x)]
data Response = Response Int [Header] ContentType Content
data Response = Response W.Status [Header] ContentType Content
-- | Different types of redirects.
data RedirectType = RedirectPermanent
@ -152,10 +153,10 @@ data RedirectType = RedirectPermanent
| RedirectSeeOther
deriving (Show, Eq)
getRedirectStatus :: RedirectType -> Int
getRedirectStatus RedirectPermanent = 301
getRedirectStatus RedirectTemporary = 302
getRedirectStatus RedirectSeeOther = 303
getRedirectStatus :: RedirectType -> W.Status
getRedirectStatus RedirectPermanent = W.Status301
getRedirectStatus RedirectTemporary = W.Status302
getRedirectStatus RedirectSeeOther = W.Status303
-- | Special types of responses which should short-circuit normal response
-- processing.
@ -173,11 +174,11 @@ data ErrorResponse =
| PermissionDenied
deriving (Show, Eq)
getStatus :: ErrorResponse -> Int
getStatus NotFound = 404
getStatus (InternalError _) = 500
getStatus (InvalidArgs _) = 400
getStatus PermissionDenied = 403
getStatus :: ErrorResponse -> W.Status
getStatus NotFound = W.Status404
getStatus (InternalError _) = W.Status500
getStatus (InvalidArgs _) = W.Status400
getStatus PermissionDenied = W.Status403
----- header stuff
-- | Headers to be added to a 'Result'.
@ -188,35 +189,31 @@ data Header =
deriving (Eq, Show)
-- | Convert Header to a key/value pair.
headerToPair :: Header -> IO (String, String)
headerToPair :: Header -> IO (W.ResponseHeader, B.ByteString)
headerToPair (AddCookie minutes key value) = do
now <- getCurrentTime
let expires = addUTCTime (fromIntegral $ minutes * 60) now
return ("Set-Cookie", key ++ "=" ++ value ++"; path=/; expires="
return (W.SetCookie, cs $ key ++ "=" ++ value ++"; path=/; expires="
++ formatW3 expires)
headerToPair (DeleteCookie key) = return
("Set-Cookie",
(W.SetCookie, cs $
key ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
headerToPair (Header key value) = return (key, value)
headerToPair (Header key value) =
return (W.responseHeaderFromBS $ cs key, cs value)
responseToHackResponse :: Response -> IO Hack.Response
responseToHackResponse (Response sc hs ct c) = do
responseToWaiResponse :: Response -> IO W.Response
responseToWaiResponse (Response sc hs ct c) = do
hs' <- mapM headerToPair hs
let hs'' = ("Content-Type", cs ct) : hs'
asLBS <- runContent c
return $ Hack.Response sc hs'' asLBS
runContent :: Content -> IO ByteString
runContent (Content c) = do
front <- c helper id
return $ fromChunks $ front []
where
helper :: ([B.ByteString] -> [B.ByteString])
-> B.ByteString
-> IO ([B.ByteString] -> [B.ByteString])
helper front bs = return $ front . (:) bs
let hs'' = (W.ContentType, cs ct) : hs'
return $ W.Response sc hs'' $ case c of
ContentFile fp -> Left fp
ContentEnum e -> Right e
#if TEST
runContent :: Content -> IO L.ByteString
runContent (ContentFile fp) = L.readFile fp
runContent (ContentEnum c) = WE.toLBS c
----- Testing
testSuite :: Test
testSuite = testGroup "Yesod.Response"

View File

@ -5,7 +5,7 @@ module Yesod.Yesod
, applyLayout'
, applyLayoutJson
, getApproot
, toHackApp
, toWaiApp
) where
import Data.Object.Html
@ -14,17 +14,18 @@ import Yesod.Response
import Yesod.Request
import Yesod.Definitions
import Yesod.Handler
import qualified Data.ByteString as B
import Data.Maybe (fromMaybe)
import Web.Mime
import Web.Encodings (parseHttpAccept)
import qualified Hack
import Hack.Middleware.CleanPath
import Hack.Middleware.ClientSession
import Hack.Middleware.Gzip
import Hack.Middleware.Jsonp
import Hack.Middleware.MethodOverride
import qualified Network.Wai as W
import Network.Wai.Middleware.CleanPath
import Network.Wai.Middleware.ClientSession
import Network.Wai.Middleware.Gzip
import Network.Wai.Middleware.Jsonp
import Network.Wai.Middleware.MethodOverride
class Yesod a where
-- | Please use the Quasi-Quoter, you\'ll be happier. For more information,
@ -86,8 +87,8 @@ defaultErrorHandler :: Yesod y
=> ErrorResponse
-> Handler y ChooseRep
defaultErrorHandler NotFound = do
rr <- getRawRequest
applyLayout' "Not Found" $ cs $ toHtmlObject [("Not found", show rr)]
--rr <- getRawRequest
applyLayout' "Not Found" $ cs $ toHtmlObject [("Not found", "FIXME")]
defaultErrorHandler PermissionDenied =
applyLayout' "Permission Denied" $ cs "Permission denied"
defaultErrorHandler (InvalidArgs ia) =
@ -100,28 +101,34 @@ defaultErrorHandler (InternalError e) =
[ ("Internal server error", e)
]
toHackApp :: Yesod y => y -> IO Hack.Application
toHackApp a = do
toWaiApp :: Yesod y => y -> IO W.Application
toWaiApp a = do
key <- encryptKey a
let app' = toHackApp' a
let mins = clientSessionDuration a
return $ gzip
$ cleanPath
$ jsonp
$ methodOverride
$ clientsession encryptedCookies key mins
app'
$ cleanPath
$ \thePath -> clientsession encryptedCookies key mins
$ toWaiApp' a thePath
toHackApp' :: Yesod y => y -> Hack.Env -> IO Hack.Response
toHackApp' y env = do
let (Right resource) = splitPath $ Hack.pathInfo env
types = httpAccept env
verb = cs $ Hack.requestMethod env
handler = resources resource verb
rr = cs env
toWaiApp' :: Yesod y
=> y
-> [B.ByteString]
-> [(B.ByteString, B.ByteString)]
-> W.Request
-> IO W.Response
toWaiApp' y resource session env = do
let types = httpAccept env
verb = cs $ W.requestMethod env :: Verb
handler = resources (map cs resource) verb
rr <- parseWaiRequest env session
res <- runHandler handler errorHandler rr y types
responseToHackResponse res
responseToWaiResponse res
httpAccept :: Hack.Env -> [ContentType]
httpAccept = map TypeOther . parseHttpAccept . fromMaybe ""
. lookup "Accept" . Hack.http
httpAccept :: W.Request -> [ContentType]
httpAccept = map contentTypeFromBS
. parseHttpAccept
. fromMaybe B.empty
. lookup W.Accept
. W.httpHeaders

View File

@ -32,7 +32,8 @@ library
build-depends: base >= 4 && < 5,
old-locale >= 1.0.0.1 && < 1.1,
time >= 1.1.3 && < 1.2,
hack == 2009.10.30,
wai >= 0.0.0 && < 0.1,
wai-extra >= 0.0.0 && < 0.1,
split >= 0.1.1 && < 0.2,
authenticate >= 0.4.0 && < 0.5,
predicates >= 0.1 && < 0.2,
@ -46,8 +47,6 @@ library
syb,
text >= 0.5 && < 0.6,
convertible-text >= 0.2.0 && < 0.3,
clientsession >= 0.0.1 && < 0.1,
zlib >= 0.5.2.0 && < 0.6,
HStringTemplate >= 0.6.2 && < 0.7,
data-object-json >= 0.0.0 && < 0.1,
attempt >= 0.2.1 && < 0.3,
@ -63,11 +62,6 @@ library
Yesod.Yesod
Yesod.Template
Data.Object.Html
Hack.Middleware.MethodOverride
Hack.Middleware.ClientSession
Hack.Middleware.Jsonp
Hack.Middleware.CleanPath
Hack.Middleware.Gzip
Yesod.Helpers.Auth
Yesod.Helpers.Static
Yesod.Helpers.AtomFeed