Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions to yesod-test to support the new CSRF middleware #1058

Merged
merged 1 commit into from
Aug 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions yesod-test/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.4.3.2

* Add `addTokenFromCookie` and `addTokenFromCookieNamedToHeaderNamed`, which support the new CSRF token middleware [#1058](https://github.com/yesodweb/yesod/pull/1058)
* Add `getRequestCookies`, which returns the cookies from the most recent request [#1058](https://github.com/yesodweb/yesod/pull/1058)

## 1.4.3.1

* Improved README
Expand Down
69 changes: 68 additions & 1 deletion yesod-test/Yesod/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ module Yesod.Test
, addToken_
, addNonce
, addNonce_
, addTokenFromCookie
, addTokenFromCookieNamedToHeaderNamed

-- * Assertions
, assertEqual
Expand All @@ -93,6 +95,7 @@ module Yesod.Test
-- * Grab information
, getTestYesod
, getResponse
, getRequestCookies
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the best spot for this function, either in the export list or in the source code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks fine to me


-- * Debug output
, printBody
Expand Down Expand Up @@ -133,6 +136,7 @@ import qualified Data.Map as M
import qualified Web.Cookie as Cookie
import qualified Blaze.ByteString.Builder as Builder
import Data.Time.Clock (getCurrentTime)
import Control.Applicative ((<$>))

-- | The state used in a single test case defined using 'yit'
--
Expand Down Expand Up @@ -589,6 +593,65 @@ addToken_ scope = do
addToken :: RequestBuilder site ()
addToken = addToken_ ""

-- | Calls 'addTokenFromCookieNamedToHeaderNamed' with the 'defaultCsrfCookieName' and 'defaultCsrfHeaderName'.
--
-- Use this function if you're using the CSRF middleware from "Yesod.Core" and haven't customized the cookie or header name.
--
-- ==== __Examples__
--
-- > request $ do
-- > addTokenFromCookie
--
-- Since 1.4.3.2
addTokenFromCookie :: RequestBuilder site ()
addTokenFromCookie = addTokenFromCookieNamedToHeaderNamed defaultCsrfCookieName defaultCsrfHeaderName

-- | Looks up the CSRF token stored in the cookie with the given name and adds it to the request headers. An error is thrown if the cookie can't be found.
--
-- Use this function if you're using the CSRF middleware from "Yesod.Core" and have customized the cookie or header name.
--
-- See "Yesod.Core.Handler" for details on this approach to CSRF protection.
--
-- ==== __Examples__
--
-- > import Data.CaseInsensitive (CI)
-- > request $ do
-- > addTokenFromCookieNamedToHeaderNamed "cookieName" (CI "headerName")
--
-- Since 1.4.3.2
addTokenFromCookieNamedToHeaderNamed :: ByteString -- ^ The name of the cookie
-> CI ByteString -- ^ The name of the header
-> RequestBuilder site ()
addTokenFromCookieNamedToHeaderNamed cookieName headerName = do
cookies <- getRequestCookies
case M.lookup cookieName cookies of
Just csrfCookie -> addRequestHeader (headerName, Cookie.setCookieValue csrfCookie)
Nothing -> failure $ T.concat
[ "addTokenFromCookieNamedToHeaderNamed failed to lookup CSRF cookie with name: "
, T.pack $ show cookieName
, ". Cookies were: "
, T.pack $ show cookies
]

-- | Returns the 'Cookies' from the most recent request. If a request hasn't been made, an error is raised.
--
-- ==== __Examples__
--
-- > request $ do
-- > cookies <- getRequestCookies
-- > liftIO $ putStrLn $ "Cookies are: " ++ show cookies
--
-- Since 1.4.3.2
getRequestCookies :: RequestBuilder site Cookies
getRequestCookies = do
requestBuilderData <- ST.get
headers <- case simpleHeaders <$> rbdResponse requestBuilderData of
Just h -> return h
Nothing -> failure "getRequestCookies: No request has been made yet; the cookies can't be looked up."

return $ M.fromList $ map (\c -> (Cookie.setCookieName c, c)) (parseSetCookies headers)


-- | Perform a POST request to @url@.
--
-- ==== __Examples__
Expand Down Expand Up @@ -759,7 +822,7 @@ request reqBuilder = do
{ httpVersion = H.http11
}
}) app
let newCookies = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ simpleHeaders response
let newCookies = parseSetCookies $ simpleHeaders response
cookies' = M.fromList [(Cookie.setCookieName c, c) | c <- newCookies] `M.union` cookies
ST.put $ YesodExampleData app site cookies' (Just response)
where
Expand Down Expand Up @@ -846,6 +909,10 @@ request reqBuilder = do
, queryString = urlQuery
}


parseSetCookies :: [H.Header] -> [Cookie.SetCookie]
parseSetCookies headers = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ headers

-- Yes, just a shortcut
failure :: (MonadIO a) => T.Text -> a b
failure reason = (liftIO $ HUnit.assertFailure $ T.unpack reason) >> error ""
Expand Down
47 changes: 47 additions & 0 deletions yesod-test/test/main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}

import Test.HUnit hiding (Test)
import Test.Hspec

Expand Down Expand Up @@ -166,6 +170,26 @@ main = hspec $ do
statusIs 200
printBody
bodyContains "Foo"
describe "CSRF with cookies/headers" $ yesodSpec CsrfApp $ do
yit "Should receive a CSRF cookie and add its value to the headers" $ do
get ("/" :: Text)
statusIs 200

request $ do
setMethod "POST"
setUrl ("/" :: Text)
addTokenFromCookie
statusIs 200
yit "Should 403 requests if we don't add the CSRF token" $ do
get ("/" :: Text)
statusIs 200

request $ do
setMethod "POST"
setUrl ("/" :: Text)
statusIs 403



instance RenderMessage LiteApp FormMessage where
renderMessage _ _ = defaultFormMessage
Expand Down Expand Up @@ -210,3 +234,26 @@ cookieApp = liteApp $ do
setMessage "Foo"
redirect ("/cookie/home" :: Text)
return ()

data CsrfApp = CsrfApp

mkYesod "CsrfApp" [parseRoutes|
/ HomeR GET POST
|]

instance Yesod CsrfApp where
yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware

getHomeR :: Handler Html
getHomeR = defaultLayout
[whamlet|
<p>
Welcome to my test application.
|]

postHomeR :: Handler Html
postHomeR = defaultLayout
[whamlet|
<p>
Welcome to my test application.
|]
2 changes: 1 addition & 1 deletion yesod-test/yesod-test.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ library
, time
, blaze-builder
, cookie
, yesod-core >= 1.4
, yesod-core >= 1.4.14

exposed-modules: Yesod.Test
Yesod.Test.CssQuery
Expand Down