snap 0.7 → 0.8.0
raw patch · 83 files changed
+3325/−1671 lines, 83 filesdep −Cryptodep −safedep −skeindep ~aesondep ~filepathdep ~heist
Dependencies removed: Crypto, safe, skein
Dependency ranges changed: aeson, filepath, heist, mwc-random, old-time, snap-core, snap-server, stm, template-haskell, unordered-containers
Files
- project_template/barebones/foo.cabal +2/−2
- project_template/default/foo.cabal +3/−3
- project_template/default/resources/static/screen.css +0/−26
- project_template/default/resources/templates/echo.tpl +0/−13
- project_template/default/resources/templates/index.tpl +0/−32
- project_template/default/snaplets/heist/templates/echo.tpl +13/−0
- project_template/default/snaplets/heist/templates/index.tpl +32/−0
- project_template/default/src/Application.hs +11/−10
- project_template/default/src/Main.hs +54/−63
- project_template/default/src/Site.hs +9/−11
- project_template/default/static/screen.css +26/−0
- project_template/tutorial/foo.cabal +3/−3
- snap.cabal +56/−30
- src/Control/Access/RoleBased/Checker.hs +224/−0
- src/Control/Access/RoleBased/Internal/Role.hs +87/−0
- src/Control/Access/RoleBased/Internal/RoleMap.hs +60/−0
- src/Control/Access/RoleBased/Internal/Rule.hs +37/−0
- src/Control/Access/RoleBased/Internal/Types.hs +34/−0
- src/Control/Access/RoleBased/Role.hs +25/−0
- src/Control/Access/RoleBased/Types.hs +14/−0
- src/Data/RBAC/Checker.hs +0/−223
- src/Data/RBAC/Internal/Role.hs +0/−81
- src/Data/RBAC/Internal/RoleMap.hs +0/−58
- src/Data/RBAC/Internal/Rule.hs +0/−31
- src/Data/RBAC/Internal/Types.hs +0/−33
- src/Data/RBAC/Role.hs +0/−24
- src/Data/RBAC/Types.hs +0/−14
- src/Snap/Loader/Devel.hs +85/−63
- src/Snap/Loader/Devel/Evaluator.hs +25/−29
- src/Snap/Loader/Devel/Signal.hs +20/−6
- src/Snap/Loader/Devel/TreeWatcher.hs +5/−6
- src/Snap/Loader/Prod.hs +2/−0
- src/Snap/Snaplet.hs +1/−0
- src/Snap/Snaplet/Auth.hs +17/−14
- src/Snap/Snaplet/Auth/AuthManager.hs +53/−55
- src/Snap/Snaplet/Auth/Backends/JsonFile.hs +161/−178
- src/Snap/Snaplet/Auth/Handlers.hs +233/−186
- src/Snap/Snaplet/Auth/SpliceHelpers.hs +13/−6
- src/Snap/Snaplet/Auth/Types.hs +185/−83
- src/Snap/Snaplet/Heist.hs +30/−25
- src/Snap/Snaplet/HeistNoClass.hs +87/−42
- src/Snap/Snaplet/Internal/Initializer.hs +68/−30
- src/Snap/Snaplet/Internal/Types.hs +30/−4
- src/Snap/Snaplet/Session.hs +54/−37
- src/Snap/Snaplet/Session/Backends/CookieSession.hs +111/−82
- src/Snap/Snaplet/Session/Common.hs +44/−19
- src/Snap/Snaplet/Session/SecureCookie.hs +25/−31
- src/Snap/Starter.hs +20/−7
- src/Snap/StarterTH.hs +10/−6
- test/foosnaplet/snaplet.cfg +2/−0
- test/foosnaplet/templates/foopage.tpl +1/−0
- test/non-cabal-appdir/bad.tpl +1/−0
- test/non-cabal-appdir/db.cfg +3/−0
- test/non-cabal-appdir/good.tpl +1/−0
- test/non-cabal-appdir/log/placeholder +0/−0
- test/non-cabal-appdir/snaplet.cfg +6/−0
- test/non-cabal-appdir/snaplets/baz/snaplet.cfg +2/−0
- test/non-cabal-appdir/snaplets/baz/templates/bazconfig.tpl +1/−0
- test/non-cabal-appdir/snaplets/baz/templates/bazpage.tpl +1/−0
- test/non-cabal-appdir/snaplets/embedded/extra-templates/extra.tpl +1/−0
- test/non-cabal-appdir/snaplets/embedded/snaplets/heist/templates/embeddedpage.tpl +1/−0
- test/non-cabal-appdir/snaplets/heist/templates/index.tpl +1/−0
- test/non-cabal-appdir/snaplets/heist/templates/page.tpl +8/−0
- test/non-cabal-appdir/snaplets/heist/templates/session.tpl +1/−0
- test/non-cabal-appdir/snaplets/heist/templates/splicepage.tpl +1/−0
- test/runTestsAndCoverage.sh +2/−4
- test/snap-testsuite.cabal +40/−42
- test/suite/AppMain.hs +9/−0
- test/suite/Blackbox/App.hs +98/−0
- test/suite/Blackbox/BarSnaplet.hs +53/−0
- test/suite/Blackbox/Common.hs +23/−0
- test/suite/Blackbox/EmbeddedSnaplet.hs +58/−0
- test/suite/Blackbox/FooSnaplet.hs +41/−0
- test/suite/Blackbox/Tests.hs +309/−0
- test/suite/Blackbox/Types.hs +32/−0
- test/suite/NestTest.hs +78/−0
- test/suite/SafeCWD.hs +30/−0
- test/suite/Snap/Snaplet/Internal/LensT/Tests.hs +119/−0
- test/suite/Snap/Snaplet/Internal/Lensed/Tests.hs +123/−0
- test/suite/Snap/Snaplet/Internal/RST/Tests.hs +67/−0
- test/suite/Snap/Snaplet/Internal/Tests.hs +124/−0
- test/suite/Snap/TestCommon.hs +64/−33
- test/suite/TestSuite.hs +55/−26
project_template/barebones/foo.cabal view
@@ -19,8 +19,8 @@ bytestring >= 0.9.1 && < 0.10, MonadCatchIO-transformers >= 0.2.1 && < 0.3, mtl >= 2 && < 3,- snap-core == 0.7.*,- snap-server == 0.7.*+ snap-core == 0.8.*,+ snap-server == 0.8.* if impl(ghc >= 6.12.0) ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2
project_template/default/foo.cabal view
@@ -26,9 +26,9 @@ heist >= 0.7 && < 0.8, MonadCatchIO-transformers >= 0.2.1 && < 0.3, mtl >= 2 && < 3,- snap == 0.7.*,- snap-core == 0.7.*,- snap-server == 0.7.*,+ snap == 0.8.*,+ snap-core == 0.8.*,+ snap-server == 0.8.*, text >= 0.11 && < 0.12, time >= 1.1 && < 1.5, xmlhtml == 0.1.*
− project_template/default/resources/static/screen.css
@@ -1,26 +0,0 @@-html {- padding: 0;- margin: 0;- background-color: #ffffff;- font-family: Verdana, Helvetica, sans-serif;-}-body {- padding: 0;- margin: 0;-}-a {- text-decoration: underline;-}-a :hover {- cursor: pointer;- text-decoration: underline;-}-img {- border: none;-}-#content {- padding-left: 1em;-}-#info {- font-size: 60%;-}
− project_template/default/resources/templates/echo.tpl
@@ -1,13 +0,0 @@-<html>- <head>- <title>Echo Page</title>- </head>- <body>- <div id="content">- <h1>Is there an echo in here?</h1>- </div>- <p>You wanted me to say this?</p>- <p>"<message/>"</p>- <p><a href="/">Return</a></p>- </body>-</html>
− project_template/default/resources/templates/index.tpl
@@ -1,32 +0,0 @@-<html>- <head>- <title>Snap web server</title>- <link rel="stylesheet" type="text/css" href="/screen.css"/>- </head>- <body>- <div id="content">- <h1>It works!</h1>- <p>- This is a simple demo page served using- <a href="http://snapframework.com/docs/tutorials/heist">Heist</a>- and the <a href="http://snapframework.com/">Snap</a> web framework.- </p>- <p>- Echo test:- <a href="/echo/cats">cats</a>- <a href="/echo/dogs">dogs</a>- <a href="/echo/fish">fish</a>- </p>- <table id="info">- <tr>- <td>Config generated at:</td>- <td><start-time/></td>- </tr>- <tr>- <td>Page generated at:</td>- <td><current-time/></td>- </tr>- </table>- </div>- </body>-</html>
+ project_template/default/snaplets/heist/templates/echo.tpl view
@@ -0,0 +1,13 @@+<html>+ <head>+ <title>Echo Page</title>+ </head>+ <body>+ <div id="content">+ <h1>Is there an echo in here?</h1>+ </div>+ <p>You wanted me to say this?</p>+ <p>"<message/>"</p>+ <p><a href="/">Return</a></p>+ </body>+</html>
+ project_template/default/snaplets/heist/templates/index.tpl view
@@ -0,0 +1,32 @@+<html>+ <head>+ <title>Snap web server</title>+ <link rel="stylesheet" type="text/css" href="/screen.css"/>+ </head>+ <body>+ <div id="content">+ <h1>It works!</h1>+ <p>+ This is a simple demo page served using+ <a href="http://snapframework.com/docs/tutorials/heist">Heist</a>+ and the <a href="http://snapframework.com/">Snap</a> web framework.+ </p>+ <p>+ Echo test:+ <a href="/echo/cats">cats</a>+ <a href="/echo/dogs">dogs</a>+ <a href="/echo/fish">fish</a>+ </p>+ <table id="info">+ <tr>+ <td>Config generated at:</td>+ <td><start-time/></td>+ </tr>+ <tr>+ <td>Page generated at:</td>+ <td><current-time/></td>+ </tr>+ </table>+ </div>+ </body>+</html>
project_template/default/src/Application.hs view
@@ -1,29 +1,30 @@ {-# LANGUAGE TemplateHaskell #-} -{---This module defines our application's state type and an alias for its handler-monad.---}-+------------------------------------------------------------------------------+-- | This module defines our application's state type and an alias for its+-- handler monad.+-- module Application where +------------------------------------------------------------------------------ import Data.Lens.Template import Data.Time.Clock- import Snap.Snaplet import Snap.Snaplet.Heist +------------------------------------------------------------------------------ data App = App { _heist :: Snaplet (Heist App) , _startTime :: UTCTime } -type AppHandler = Handler App App- makeLens ''App instance HasHeist App where heistLens = subSnaplet heist+++------------------------------------------------------------------------------+type AppHandler = Handler App App+
project_template/default/src/Main.hs view
@@ -1,18 +1,15 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} module Main where +------------------------------------------------------------------------------ import Control.Exception (SomeException, try)- import qualified Data.Text as T- import Snap.Http.Server import Snap.Snaplet import Snap.Core- import System.IO- import Site #ifdef DEVELOPMENT@@ -22,65 +19,60 @@ #endif -{-|--This is the entry point for this web server application. It supports-easily switching between interpreting source and running statically-compiled code.--In either mode, the generated program should be run from the root of-the project tree. When it is run, it locates its templates, static-content, and source files in development mode, relative to the current-working directory.--When compiled with the development flag, only changes to the-libraries, your cabal file, or this file should require a recompile to-be picked up. Everything else is interpreted at runtime. There are a-few consequences of this.--First, this is much slower. Running the interpreter takes a-significant chunk of time (a couple tenths of a second on the author's-machine, at this time), regardless of the simplicity of the loaded-code. In order to recompile and re-load server state as infrequently-as possible, the source directories are watched for updates, as are-any extra directories specified below.--Second, the generated server binary is MUCH larger, since it links in-the GHC API (via the hint library).--Third, and the reason you would ever want to actually compile with-development mode, is that it enables a faster development cycle. You-can simply edit a file, save your changes, and hit reload to see your-changes reflected immediately.--When this is compiled without the development flag, all the actions-are statically compiled in. This results in faster execution, a-smaller binary size, and having to recompile the server for any code-change.---}+------------------------------------------------------------------------------+-- | This is the entry point for this web server application. It supports+-- easily switching between interpreting source and running statically compiled+-- code.+--+-- In either mode, the generated program should be run from the root of the+-- project tree. When it is run, it locates its templates, static content, and+-- source files in development mode, relative to the current working directory.+--+-- When compiled with the development flag, only changes to the libraries, your+-- cabal file, or this file should require a recompile to be picked up.+-- Everything else is interpreted at runtime. There are a few consequences of+-- this.+--+-- First, this is much slower. Running the interpreter takes a significant+-- chunk of time (a couple tenths of a second on the author's machine, at this+-- time), regardless of the simplicity of the loaded code. In order to+-- recompile and re-load server state as infrequently as possible, the source+-- directories are watched for updates, as are any extra directories specified+-- below.+--+-- Second, the generated server binary is MUCH larger, since it links in the+-- GHC API (via the hint library).+--+-- Third, and the reason you would ever want to actually compile with+-- development mode, is that it enables a faster development cycle. You can+-- simply edit a file, save your changes, and hit reload to see your changes+-- reflected immediately.+--+-- When this is compiled without the development flag, all the actions are+-- statically compiled in. This results in faster execution, a smaller binary+-- size, and having to recompile the server for any code change.+-- main :: IO () main = do- -- depending on the version of loadSnapTH in scope, this either- -- enables dynamic reloading, or compiles it without. The last- -- argument to loadSnapTH is a list of additional directories to- -- watch for changes to trigger reloads in development mode. It- -- doesn't need to include source directories, those are picked up- -- automatically by the splice.+ -- Depending on the version of loadSnapTH in scope, this either enables+ -- dynamic reloading, or compiles it without. The last argument to+ -- loadSnapTH is a list of additional directories to watch for changes to+ -- trigger reloads in development mode. It doesn't need to include source+ -- directories, those are picked up automatically by the splice. (conf, site, cleanup) <- $(loadSnapTH [| getConf |] 'getActions- ["resources/templates"])+ ["snaplets/heist/templates"]) _ <- try $ httpServe conf $ site :: IO (Either SomeException ()) cleanup --- | This action loads the config used by this application. The--- loaded config is returned as the first element of the tuple--- produced by the loadSnapTH Splice. The type is not solidly fixed,--- though it must be an IO action that produces the same type as--- 'getActions' takes. It also must be an instance of Typeable. If--- the type of this is changed, a full recompile will be needed to+------------------------------------------------------------------------------+-- | This action loads the config used by this application. The loaded config+-- is returned as the first element of the tuple produced by the loadSnapTH+-- Splice. The type is not solidly fixed, though it must be an IO action that+-- produces the same type as 'getActions' takes. It also must be an instance of+-- Typeable. If the type of this is changed, a full recompile will be needed to -- pick up the change, even in development mode. -- -- This action is only run once, regardless of whether development or@@ -89,16 +81,15 @@ getConf = commandLineConfig defaultConfig --- | This function generates the the site handler and cleanup action--- from the configuration. In production mode, this action is only--- run once. In development mode, this action is run whenever the--- application is reloaded.+------------------------------------------------------------------------------+-- | This function generates the the site handler and cleanup action from the+-- configuration. In production mode, this action is only run once. In+-- development mode, this action is run whenever the application is reloaded. -- -- Development mode also makes sure that the cleanup actions are run--- appropriately before shutdown. The cleanup action returned from--- loadSnapTH should still be used after the server has stopped--- handling requests, as the cleanup actions are only automatically--- run when a reload is triggered.+-- appropriately before shutdown. The cleanup action returned from loadSnapTH+-- should still be used after the server has stopped handling requests, as the+-- cleanup actions are only automatically run when a reload is triggered. -- -- This sample doesn't actually use the config passed in, but more -- sophisticated code might.
project_template/default/src/Site.hs view
@@ -1,17 +1,15 @@ {-# LANGUAGE OverloadedStrings #-} -{-|--This is where all the routes and handlers are defined for your site. The-'app' function is the initializer that combines everything together and-is exported by this module.---}-+------------------------------------------------------------------------------+-- | This module is where all the routes and handlers are defined for your+-- site. The 'app' function is the initializer that combines everything+-- together and is exported by this module.+-- module Site ( app ) where +------------------------------------------------------------------------------ import Control.Applicative import Control.Monad.Trans import Control.Monad.State@@ -26,7 +24,7 @@ import Snap.Util.FileServe import Text.Templating.Heist import Text.XmlHtml hiding (render)-+------------------------------------------------------------------------------ import Application @@ -77,7 +75,7 @@ routes = [ ("/", index) , ("/echo/:stuff", echo) , ("", with heist heistServe)- , ("", serveDirectory "resources/static")+ , ("", serveDirectory "static") ] ------------------------------------------------------------------------------@@ -85,7 +83,7 @@ app :: SnapletInit App App app = makeSnaplet "app" "An snaplet example application." Nothing $ do sTime <- liftIO getCurrentTime- h <- nestSnaplet "heist" heist $ heistInit "resources/templates"+ h <- nestSnaplet "heist" heist $ heistInit "templates" addRoutes routes return $ App h sTime
+ project_template/default/static/screen.css view
@@ -0,0 +1,26 @@+html {+ padding: 0;+ margin: 0;+ background-color: #ffffff;+ font-family: Verdana, Helvetica, sans-serif;+}+body {+ padding: 0;+ margin: 0;+}+a {+ text-decoration: underline;+}+a :hover {+ cursor: pointer;+ text-decoration: underline;+}+img {+ border: none;+}+#content {+ padding-left: 1em;+}+#info {+ font-size: 60%;+}
project_template/tutorial/foo.cabal view
@@ -19,9 +19,9 @@ bytestring >= 0.9.1 && < 0.10, MonadCatchIO-transformers >= 0.2.1 && < 0.3, mtl >= 2 && < 3,- snap == 0.7.*,- snap-core == 0.7.*,- snap-server == 0.7.*+ snap == 0.8.*,+ snap-core == 0.8.*,+ snap-server == 0.8.* if impl(ghc >= 6.12.0) ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2
snap.cabal view
@@ -1,5 +1,5 @@ name: snap-version: 0.7+version: 0.8.0 synopsis: Snap: A Haskell Web Framework: project starter executable and glue code library description: Snap Framework project starter executable and glue code library license: BSD3@@ -24,9 +24,9 @@ project_template/default/foo.cabal, project_template/default/log/access.log, project_template/default/log/error.log,- project_template/default/resources/static/screen.css,- project_template/default/resources/templates/echo.tpl,- project_template/default/resources/templates/index.tpl,+ project_template/default/static/screen.css,+ project_template/default/snaplets/heist/templates/echo.tpl,+ project_template/default/snaplets/heist/templates/index.tpl, project_template/default/src/Application.hs, project_template/default/src/Main.hs, project_template/default/src/Site.hs,@@ -40,8 +40,38 @@ extra/logo.gif, test/snap-testsuite.cabal, test/runTestsAndCoverage.sh,+ test/suite/TestSuite.hs,+ test/suite/NestTest.hs, test/suite/Snap/TestCommon.hs,- test/suite/TestSuite.hs+ test/suite/Snap/Snaplet/Internal/Lensed/Tests.hs,+ test/suite/Snap/Snaplet/Internal/RST/Tests.hs,+ test/suite/Snap/Snaplet/Internal/Tests.hs,+ test/suite/Snap/Snaplet/Internal/LensT/Tests.hs,+ test/suite/Blackbox/Types.hs,+ test/suite/Blackbox/FooSnaplet.hs,+ test/suite/Blackbox/BarSnaplet.hs,+ test/suite/Blackbox/Common.hs,+ test/suite/Blackbox/EmbeddedSnaplet.hs,+ test/suite/Blackbox/Tests.hs,+ test/suite/Blackbox/App.hs,+ test/suite/SafeCWD.hs,+ test/suite/AppMain.hs,+ test/non-cabal-appdir/db.cfg,+ test/non-cabal-appdir/bad.tpl,+ test/non-cabal-appdir/snaplets/baz/templates/bazpage.tpl,+ test/non-cabal-appdir/snaplets/baz/templates/bazconfig.tpl,+ test/non-cabal-appdir/snaplets/baz/snaplet.cfg,+ test/non-cabal-appdir/snaplets/embedded/extra-templates/extra.tpl,+ test/non-cabal-appdir/snaplets/embedded/snaplets/heist/templates/embeddedpage.tpl,+ test/non-cabal-appdir/snaplets/heist/templates/index.tpl,+ test/non-cabal-appdir/snaplets/heist/templates/session.tpl,+ test/non-cabal-appdir/snaplets/heist/templates/splicepage.tpl,+ test/non-cabal-appdir/snaplets/heist/templates/page.tpl,+ test/non-cabal-appdir/good.tpl,+ test/non-cabal-appdir/log/placeholder,+ test/non-cabal-appdir/snaplet.cfg,+ test/foosnaplet/templates/foopage.tpl,+ test/foosnaplet/snaplet.cfg Flag hint Description: Support dynamic project reloading via hint@@ -59,16 +89,17 @@ Snap.Snaplet.Auth, Snap.Snaplet.Auth.Backends.JsonFile, Snap.Snaplet.Session,+ Snap.Snaplet.Session.Common, Snap.Snaplet.Session.Backends.CookieSession other-modules:- Data.RBAC.Checker,- Data.RBAC.Role,- Data.RBAC.Types,- Data.RBAC.Internal.Role,- Data.RBAC.Internal.RoleMap,- Data.RBAC.Internal.Rule,- Data.RBAC.Internal.Types,+ Control.Access.RoleBased.Checker,+ Control.Access.RoleBased.Role,+ Control.Access.RoleBased.Types,+ Control.Access.RoleBased.Internal.Role,+ Control.Access.RoleBased.Internal.RoleMap,+ Control.Access.RoleBased.Internal.Rule,+ Control.Access.RoleBased.Internal.Types, Snap.Snaplet.Auth.AuthManager, Snap.Snaplet.Auth.Types, Snap.Snaplet.Auth.Handlers,@@ -79,7 +110,6 @@ Snap.Snaplet.Internal.Lensed, Snap.Snaplet.Internal.RST, Snap.Snaplet.Internal.Types- Snap.Snaplet.Session.Common, Snap.Snaplet.Session.SecureCookie, Snap.Snaplet.Session.SessionManager @@ -99,9 +129,8 @@ unix >= 2.2.0.0 && < 2.6 build-depends:- Crypto >= 4.2 && < 4.3, MonadCatchIO-transformers >= 0.2 && < 0.3,- aeson >= 0.4 && < 0.5,+ aeson >= 0.6 && < 0.7, attoparsec >= 0.10 && < 0.11, base >= 4 && < 5, bytestring >= 0.9.1 && < 0.10,@@ -113,25 +142,22 @@ directory-tree >= 0.10 && < 0.11, data-lens >= 2.0.1 && < 2.1, data-lens-template >= 2.1 && < 2.2,- filepath >= 1.1 && < 1.3,+ filepath >= 1.1 && < 1.4, hashable >= 1.1 && < 1.2,- heist >= 0.7 && < 0.8,+ heist >= 0.7 && < 0.9, logict >= 0.4.2 && < 0.6, mtl > 2.0 && < 2.1,- mwc-random >= 0.8 && < 0.11,- old-time >= 1.0 && < 1.1,+ mwc-random >= 0.8 && < 0.13, pwstore-fast >= 2.2 && < 2.3,- safe >= 0.3 && < 0.4,- skein >= 0.1.0.3 && < 0.2,- snap-core >= 0.7 && < 0.8,- snap-server >= 0.7 && < 0.8,- stm >= 2.2 && < 2.3,+ snap-core >= 0.8 && < 0.9,+ snap-server >= 0.8 && < 0.9,+ stm >= 2.2 && < 2.4, syb >= 0.1 && < 0.4,- template-haskell >= 2.2 && < 2.7,+ template-haskell >= 2.2 && < 2.8, text >= 0.11 && < 0.12, time >= 1.1 && < 1.5, transformers >= 0.2 && < 0.3,- unordered-containers >= 0.1.4 && < 0.2,+ unordered-containers >= 0.1.4 && < 0.3, utf8-string >= 0.3 && < 0.4, vector >= 0.7.1 && < 0.10, vector-algorithms >= 0.4 && < 0.6,@@ -175,10 +201,10 @@ containers >= 0.3 && < 0.5, directory >= 1.0 && < 1.2, directory-tree >= 0.10 && < 0.11,- filepath >= 1.1 && < 1.3,- old-time >= 1.0 && < 1.1,- snap-server >= 0.7 && < 0.8,- template-haskell >= 2.2 && < 2.7,+ filepath >= 1.1 && < 1.4,+ old-time >= 1.0 && < 1.2,+ snap-server >= 0.8 && < 0.9,+ template-haskell >= 2.2 && < 2.8, text >= 0.11 && < 0.12 ghc-prof-options: -prof -auto-all
+ src/Control/Access/RoleBased/Checker.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}++module Control.Access.RoleBased.Checker where++------------------------------------------------------------------------------+import Control.Monad+import Control.Monad.Logic+import Control.Monad.Reader+import Control.Monad.State.Lazy+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import Data.Maybe (fromMaybe, isJust)+import Data.Text (Text)+------------------------------------------------------------------------------+import Control.Access.RoleBased.Internal.RoleMap (RoleMap)+import qualified Control.Access.RoleBased.Internal.RoleMap as RM+import Control.Access.RoleBased.Internal.Types+import Control.Access.RoleBased.Role+++------------------------------------------------------------------------------+type RoleBuilder a = StateT RoleMap RoleMonad a+++------------------------------------------------------------------------------+applyRule :: Role -> Rule -> [Role]+applyRule r (Rule _ f) = f r+++------------------------------------------------------------------------------+applyRuleSet :: Role -> RuleSet -> [Role]+applyRuleSet r (RuleSet m) = f r+ where+ f = fromMaybe (const []) $ M.lookup (_roleName r) m+++------------------------------------------------------------------------------+checkUnseen :: Role -> RoleBuilder ()+checkUnseen role = do+ m <- get+ if isJust $ RM.lookup role m then mzero else return ()+++------------------------------------------------------------------------------+checkSeen :: Role -> RoleBuilder ()+checkSeen = lnot . checkUnseen+++------------------------------------------------------------------------------+markSeen :: Role -> RoleBuilder ()+markSeen role = modify $ RM.insert role+++------------------------------------------------------------------------------+isum :: (MonadLogic m, MonadPlus m) => [m a] -> m a+isum l = case l of+ [] -> mzero+ (x:xs) -> x `interleave` isum xs+++------------------------------------------------------------------------------+-- | Given a set of roles to check, and a set of implication rules describing+-- how a given role inherits from other roles, this function produces a stream+-- of expanded Roles. If a Role is seen twice, expandRoles mzeros.+expandRoles :: [Rule] -> [Role] -> RoleMonad Role+expandRoles rules roles0 = evalStateT (go roles0) RM.empty+ where+ ruleSet = rulesToSet rules++ go roles = isum $ map expandOne roles++ expandOne role = do+ checkUnseen role+ markSeen role+ return role `interleave` go newRoles++ where+ newRoles = applyRuleSet role ruleSet+++------------------------------------------------------------------------------+hasRole :: Role -> RuleChecker ()+hasRole r = RuleChecker $ do+ ch <- ask+ once $ go ch+ where+ go gen = do+ r' <- lift gen+ if r `matches` r' then return () else mzero+++------------------------------------------------------------------------------+missingRole :: Role -> RuleChecker ()+missingRole = lnot . hasRole+++------------------------------------------------------------------------------+hasAllRoles :: [Role] -> RuleChecker ()+hasAllRoles rs = RuleChecker $ do+ ch <- ask+ lift $ once $ go ch $ RM.fromList rs+ where+ go gen !st = do+ mr <- msplit gen+ maybe mzero+ (\(r,gen') -> let st' = RM.delete r st+ in if RM.null st'+ then return ()+ else go gen' st')+ mr+++------------------------------------------------------------------------------+hasAnyRoles :: [Role] -> RuleChecker ()+hasAnyRoles rs = RuleChecker $ do+ ch <- ask+ lift $ once $ go ch+ where+ st = RM.fromList rs+ go gen = do+ mr <- msplit gen+ maybe mzero+ (\(r,gen') -> if isJust $ RM.lookup r st+ then return ()+ else go gen')+ mr+++------------------------------------------------------------------------------+runRuleChecker :: [Rule]+ -> [Role]+ -> RuleChecker a+ -> Bool+runRuleChecker rules roles (RuleChecker f) =+ case outs of+ [] -> False+ _ -> True+ where+ (RoleMonad st) = runReaderT f $ expandRoles rules roles+ outs = observeMany 1 st+++------------------------------------------------------------------------------+mkRule :: Text -> (Role -> [Role]) -> Rule+mkRule = Rule+++------------------------------------------------------------------------------+implies :: Role -> [Role] -> Rule+implies src dest = Rule (_roleName src)+ (\role -> if role `matches` src then dest else [])+++------------------------------------------------------------------------------+impliesWith :: Role -> (HashMap Text RoleValue -> [Role]) -> Rule+impliesWith src f = Rule (_roleName src)+ (\role -> if src `matches` role+ then f $ _roleData role+ else [])+++------------------------------------------------------------------------------+-- Testing code follows: TODO: move into test suite+++testRules :: [Rule]+testRules = [ "user" `implies` ["guest", "can_post"]+ , "superuser" `implies` [ "user"+ , "can_moderate"+ , "can_administrate"]+ , "superuser" `implies` [ addRoleData "arg" "*" "with_arg" ]+ , "with_arg" `impliesWith` \dat ->+ maybe [] (\arg -> [addRoleData "arg" arg "dependent_arg"]) $+ M.lookup "arg" dat+ , "superuser" `implies` [ addRoleData "arg1" "a" $+ addRoleData "arg2" "b" "multi_args" ]+ ]++tX :: RuleChecker () -> Bool+tX f = runRuleChecker testRules ["superuser"] f++t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12,t13,t14,t15,t16,t17 :: Bool+t1 = tX $ hasAnyRoles ["guest","userz"]++t2 = tX $ hasAllRoles ["guest","userz"]++t3 = tX $ hasAllRoles ["guest","user"]++t4 = tX $ hasRole "can_administrate"++t5 = tX $ hasRole "lkfdhjkjfhds"++t6 = tX $ do+ hasRole "guest"+ hasRole "superuser"++t7 = tX $ do+ hasRole "zzzzz"+ hasRole "superuser"++t8 = tX $ hasRole $ addRoleData "arg" "*" "dependent_arg"++t9 = tX $ hasRole "multi_args"++t10 = tX $ hasRole $ addRoleData "arg2" "b" "multi_args"++t11 = tX $ hasRole $ addRoleData "arg2" "z" "multi_args"++t12 = tX $ hasAllRoles [addRoleData "arg2" "b" "multi_args"]++t13 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"+ , addRoleData "arg2" "b" "multi_args" ]++t14 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"+ , addRoleData "arg2" "aaa" "multi_args" ]++t15 = tX $ missingRole "jflsdkjf"++t16 = tX $ do+ missingRole "fdjlksjlf"+ hasRole "multi_args"++t17 = tX $ missingRole "multi_args"
+ src/Control/Access/RoleBased/Internal/Role.hs view
@@ -0,0 +1,87 @@+module Control.Access.RoleBased.Internal.Role where++------------------------------------------------------------------------------+import Control.Monad.ST+import Data.Hashable+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import Data.String+import Data.Text (Text)+import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Merge as VA+++------------------------------------------------------------------------------+data RoleValue = RoleBool Bool+ | RoleText Text+ | RoleInt Int+ | RoleDouble Double+ deriving (Ord, Eq, Show)+++------------------------------------------------------------------------------+instance IsString RoleValue where+ fromString = RoleText . fromString+++------------------------------------------------------------------------------+instance Hashable RoleValue where+ hashWithSalt salt (RoleBool e) = hashWithSalt salt e `combine` 7+ hashWithSalt salt (RoleText t) = hashWithSalt salt t `combine` 196613+ hashWithSalt salt (RoleInt i) = hashWithSalt salt i `combine` 12582917+ hashWithSalt salt (RoleDouble d) =+ hashWithSalt salt d `combine` 1610612741+++------------------------------------------------------------------------------+data Role = Role {+ _roleName :: Text+ , _roleData :: HashMap Text RoleValue+ }+ deriving (Eq, Show)+++------------------------------------------------------------------------------+instance IsString Role where+ fromString s = Role (fromString s) M.empty+++------------------------------------------------------------------------------+toSortedList :: (Ord k, Ord v) => HashMap k v -> [(k,v)]+toSortedList m = runST $ do+ v <- V.unsafeThaw $ V.fromList $ M.toList m+ VA.sort v+ v' <- V.unsafeFreeze v+ return $ V.toList v'+++------------------------------------------------------------------------------+instance Hashable Role where+ hashWithSalt salt (Role nm dat) =+ h $ hashWithSalt salt nm+ where+ h s = hashWithSalt s $ toSortedList dat+++------------------------------------------------------------------------------+data RoleValueMeta = RoleBoolMeta+ | RoleTextMeta+ | RoleEnumMeta [Text]+ | RoleIntMeta+ | RoleDoubleMeta+++------------------------------------------------------------------------------+data RoleDataDefinition = RoleDataDefinition {+ _roleDataName :: Text+ , _roleValueMeta :: RoleValueMeta+ , _roleDataDescription :: Text+ }+++------------------------------------------------------------------------------+data RoleMetadata = RoleMetadata {+ _roleMetadataName :: Text+ , _roleDescription :: Text+ , _roleDataDefs :: [RoleDataDefinition]+ }
+ src/Control/Access/RoleBased/Internal/RoleMap.hs view
@@ -0,0 +1,60 @@+module Control.Access.RoleBased.Internal.RoleMap where++------------------------------------------------------------------------------+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import Data.HashSet (HashSet)+import qualified Data.HashSet as S+import Data.List (find, foldl')+import Data.Text (Text)+------------------------------------------------------------------------------+import Control.Access.RoleBased.Role+import Control.Access.RoleBased.Internal.Types+++------------------------------------------------------------------------------+newtype RoleMap = RoleMap (HashMap Text (HashSet Role))+++------------------------------------------------------------------------------+fromList :: [Role] -> RoleMap+fromList = RoleMap . foldl' ins M.empty+ where+ ins m role =+ M.insertWith S.union (_roleName role) (S.singleton role) m+++------------------------------------------------------------------------------+lookup :: Role -> RoleMap -> Maybe Role+lookup role (RoleMap m) = find (`matches` role) l+ where+ l = maybe [] S.toList $ M.lookup (_roleName role) m+++------------------------------------------------------------------------------+delete :: Role -> RoleMap -> RoleMap+delete role (RoleMap m) = RoleMap $ maybe m upd $ M.lookup rNm m+ where+ rNm = _roleName role+ upd s = maybe m+ (\r -> let s' = S.delete r s+ in if S.null s'+ then M.delete rNm m+ else M.insert rNm s' m)+ (find (`matches` role) $ S.toList s)+++------------------------------------------------------------------------------+insert :: Role -> RoleMap -> RoleMap+insert role (RoleMap m) =+ RoleMap $ M.insertWith S.union (_roleName role) (S.singleton role) m+++------------------------------------------------------------------------------+empty :: RoleMap+empty = RoleMap M.empty+++------------------------------------------------------------------------------+null :: RoleMap -> Bool+null (RoleMap m) = M.null m
+ src/Control/Access/RoleBased/Internal/Rule.hs view
@@ -0,0 +1,37 @@+module Control.Access.RoleBased.Internal.Rule where++------------------------------------------------------------------------------+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import Data.List (foldl')+import Data.Monoid+import Data.Text (Text)+------------------------------------------------------------------------------+import Control.Access.RoleBased.Internal.Role+++------------------------------------------------------------------------------+data Rule = Rule Text (Role -> [Role])+++------------------------------------------------------------------------------+newtype RuleSet = RuleSet (HashMap Text (Role -> [Role]))+++------------------------------------------------------------------------------+instance Monoid RuleSet where+ mempty = RuleSet M.empty+ (RuleSet m1) `mappend` (RuleSet m2) = RuleSet $ M.foldlWithKey' ins m2 m1+ where+ combine f1 f2 r = f1 r ++ f2 r+ ins m k v = M.insertWith combine k v m+++------------------------------------------------------------------------------+ruleToSet :: Rule -> RuleSet+ruleToSet (Rule nm f) = RuleSet $ M.singleton nm f+++------------------------------------------------------------------------------+rulesToSet :: [Rule] -> RuleSet+rulesToSet = foldl' mappend (RuleSet M.empty) . map ruleToSet
+ src/Control/Access/RoleBased/Internal/Types.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Control.Access.RoleBased.Internal.Types+ ( module Control.Access.RoleBased.Internal.Role+ , module Control.Access.RoleBased.Internal.Rule+ , RoleMonad(..)+ , RuleChecker(..)+ ) where++------------------------------------------------------------------------------+import Control.Applicative+import Control.Monad.Reader+import Control.Monad.Logic+------------------------------------------------------------------------------+import Control.Access.RoleBased.Internal.Role+import Control.Access.RoleBased.Internal.Rule+++------------------------------------------------------------------------------+-- TODO: should the monads be transformers here? If they were, you could check+-- more complex predicates here+++------------------------------------------------------------------------------+newtype RoleMonad a = RoleMonad { _unRC :: Logic a }+ deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)+++------------------------------------------------------------------------------+newtype RuleChecker a = RuleChecker (ReaderT (RoleMonad Role) RoleMonad a)+ deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)+
+ src/Control/Access/RoleBased/Role.hs view
@@ -0,0 +1,25 @@+module Control.Access.RoleBased.Role where++------------------------------------------------------------------------------+import qualified Data.HashMap.Strict as M+import Control.Access.RoleBased.Internal.Types+import Data.Text (Text)+++------------------------------------------------------------------------------+matches :: Role -> Role -> Bool+matches (Role a1 d1) (Role a2 d2) =+ a1 == a2 && dmatch (toSortedList d1) (toSortedList d2)+ where+ dmatch [] _ = True+ dmatch _ [] = False+ dmatch dds@(d:ds) (e:es) =+ case compare d e of+ LT -> False+ EQ -> dmatch ds es+ GT -> dmatch dds es+++------------------------------------------------------------------------------+addRoleData :: Text -> RoleValue -> Role -> Role+addRoleData k v (Role n d) = Role n $ M.insert k v d
+ src/Control/Access/RoleBased/Types.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Control.Access.RoleBased.Types+ ( Role(..) -- fixme: remove (..)+ , RoleValue(..) -- fixme+ , RoleValueMeta(..)+ , RoleDataDefinition(..)+ , RoleMetadata(..)+ , Rule+ , RuleChecker+ ) where++import Control.Access.RoleBased.Internal.Types
− src/Data/RBAC/Checker.hs
@@ -1,223 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.RBAC.Checker where--import Control.Monad-import Control.Monad.Logic-import Control.Monad.Reader-import Control.Monad.State.Lazy-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M-import Data.Maybe (fromMaybe, isJust)-import Data.Text (Text)--import Data.RBAC.Internal.RoleMap (RoleMap)-import qualified Data.RBAC.Internal.RoleMap as RM-import Data.RBAC.Internal.Types-import Data.RBAC.Role----------------------------------------------------------------------------------type RoleBuilder a = StateT RoleMap RoleMonad a----------------------------------------------------------------------------------applyRule :: Role -> Rule -> [Role]-applyRule r (Rule _ f) = f r----------------------------------------------------------------------------------applyRuleSet :: Role -> RuleSet -> [Role]-applyRuleSet r (RuleSet m) = f r- where- f = fromMaybe (const []) $ M.lookup (_roleName r) m----------------------------------------------------------------------------------checkUnseen :: Role -> RoleBuilder ()-checkUnseen role = do- m <- get- if isJust $ RM.lookup role m then mzero else return ()----------------------------------------------------------------------------------checkSeen :: Role -> RoleBuilder ()-checkSeen = lnot . checkUnseen----------------------------------------------------------------------------------markSeen :: Role -> RoleBuilder ()-markSeen role = modify $ RM.insert role----------------------------------------------------------------------------------isum :: (MonadLogic m, MonadPlus m) => [m a] -> m a-isum l = case l of- [] -> mzero- (x:xs) -> x `interleave` isum xs------------------------------------------------------------------------------------ | Given a set of roles to check, and a set of implication rules describing--- how a given role inherits from other roles, this function produces a stream--- of expanded Roles. If a Role is seen twice, expandRoles mzeros.-expandRoles :: [Rule] -> [Role] -> RoleMonad Role-expandRoles rules roles0 = evalStateT (go roles0) RM.empty- where- ruleSet = rulesToSet rules-- go roles = isum $ map expandOne roles-- expandOne role = do- checkUnseen role- markSeen role- return role `interleave` go newRoles-- where- newRoles = applyRuleSet role ruleSet----------------------------------------------------------------------------------hasRole :: Role -> RuleChecker ()-hasRole r = RuleChecker $ do- ch <- ask- once $ go ch- where- go gen = do- r' <- lift gen- if r `matches` r' then return () else mzero----------------------------------------------------------------------------------missingRole :: Role -> RuleChecker ()-missingRole = lnot . hasRole----------------------------------------------------------------------------------hasAllRoles :: [Role] -> RuleChecker ()-hasAllRoles rs = RuleChecker $ do- ch <- ask- lift $ once $ go ch $ RM.fromList rs- where- go gen !st = do- mr <- msplit gen- maybe mzero- (\(r,gen') -> let st' = RM.delete r st- in if RM.null st'- then return ()- else go gen' st')- mr----------------------------------------------------------------------------------hasAnyRoles :: [Role] -> RuleChecker ()-hasAnyRoles rs = RuleChecker $ do- ch <- ask- lift $ once $ go ch- where- st = RM.fromList rs- go gen = do- mr <- msplit gen- maybe mzero- (\(r,gen') -> if isJust $ RM.lookup r st- then return ()- else go gen')- mr----------------------------------------------------------------------------------runRuleChecker :: [Rule]- -> [Role]- -> RuleChecker a- -> Bool-runRuleChecker rules roles (RuleChecker f) =- case outs of- [] -> False- _ -> True- where- (RoleMonad st) = runReaderT f $ expandRoles rules roles- outs = observeMany 1 st----------------------------------------------------------------------------------mkRule :: Text -> (Role -> [Role]) -> Rule-mkRule = Rule----------------------------------------------------------------------------------implies :: Role -> [Role] -> Rule-implies src dest = Rule (_roleName src)- (\role -> if role `matches` src then dest else [])----------------------------------------------------------------------------------impliesWith :: Role -> (HashMap Text RoleValue -> [Role]) -> Rule-impliesWith src f = Rule (_roleName src)- (\role -> if src `matches` role- then f $ _roleData role- else [])------------------------------------------------------------------------------------ Testing code follows: TODO: move into test suite---testRules :: [Rule]-testRules = [ "user" `implies` ["guest", "can_post"]- , "superuser" `implies` [ "user"- , "can_moderate"- , "can_administrate"]- , "superuser" `implies` [ addRoleData "arg" "*" "with_arg" ]- , "with_arg" `impliesWith` \dat ->- maybe [] (\arg -> [addRoleData "arg" arg "dependent_arg"]) $- M.lookup "arg" dat- , "superuser" `implies` [ addRoleData "arg1" "a" $- addRoleData "arg2" "b" "multi_args" ]- ]--tX :: RuleChecker () -> Bool-tX f = runRuleChecker testRules ["superuser"] f--t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12,t13,t14,t15,t16,t17 :: Bool-t1 = tX $ hasAnyRoles ["guest","userz"]--t2 = tX $ hasAllRoles ["guest","userz"]--t3 = tX $ hasAllRoles ["guest","user"]--t4 = tX $ hasRole "can_administrate"--t5 = tX $ hasRole "lkfdhjkjfhds"--t6 = tX $ do- hasRole "guest"- hasRole "superuser"--t7 = tX $ do- hasRole "zzzzz"- hasRole "superuser"--t8 = tX $ hasRole $ addRoleData "arg" "*" "dependent_arg"--t9 = tX $ hasRole "multi_args"--t10 = tX $ hasRole $ addRoleData "arg2" "b" "multi_args"--t11 = tX $ hasRole $ addRoleData "arg2" "z" "multi_args"--t12 = tX $ hasAllRoles [addRoleData "arg2" "b" "multi_args"]--t13 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"- , addRoleData "arg2" "b" "multi_args" ]--t14 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"- , addRoleData "arg2" "aaa" "multi_args" ]--t15 = tX $ missingRole "jflsdkjf"--t16 = tX $ do- missingRole "fdjlksjlf"- hasRole "multi_args"--t17 = tX $ missingRole "multi_args"
− src/Data/RBAC/Internal/Role.hs
@@ -1,81 +0,0 @@-module Data.RBAC.Internal.Role where--import Control.Monad.ST-import Data.Hashable-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M-import Data.String-import Data.Text (Text)-import qualified Data.Vector as V-import qualified Data.Vector.Algorithms.Merge as VA----------------------------------------------------------------------------------data RoleValue = RoleBool Bool- | RoleText Text- | RoleInt Int- | RoleDouble Double- deriving (Ord, Eq, Show)---instance IsString RoleValue where- fromString = RoleText . fromString---instance Hashable RoleValue where- hashWithSalt salt (RoleBool e) = hashWithSalt salt e `combine` 7- hashWithSalt salt (RoleText t) = hashWithSalt salt t `combine` 196613- hashWithSalt salt (RoleInt i) = hashWithSalt salt i `combine` 12582917- hashWithSalt salt (RoleDouble d) =- hashWithSalt salt d `combine` 1610612741----------------------------------------------------------------------------------data Role = Role {- _roleName :: Text- , _roleData :: HashMap Text RoleValue- }- deriving (Eq, Show)---instance IsString Role where- fromString s = Role (fromString s) M.empty----------------------------------------------------------------------------------toSortedList :: (Ord k, Ord v) => HashMap k v -> [(k,v)]-toSortedList m = runST $ do- v <- V.unsafeThaw $ V.fromList $ M.toList m- VA.sort v- v' <- V.unsafeFreeze v- return $ V.toList v'----instance Hashable Role where- hashWithSalt salt (Role nm dat) =- h $ hashWithSalt salt nm- where- h s = hashWithSalt s $ toSortedList dat----------------------------------------------------------------------------------data RoleValueMeta = RoleBoolMeta- | RoleTextMeta- | RoleEnumMeta [Text]- | RoleIntMeta- | RoleDoubleMeta---data RoleDataDefinition = RoleDataDefinition {- _roleDataName :: Text- , _roleValueMeta :: RoleValueMeta- , _roleDataDescription :: Text- }---data RoleMetadata = RoleMetadata {- _roleMetadataName :: Text- , _roleDescription :: Text- , _roleDataDefs :: [RoleDataDefinition]- }
− src/Data/RBAC/Internal/RoleMap.hs
@@ -1,58 +0,0 @@-module Data.RBAC.Internal.RoleMap where--import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M-import Data.HashSet (HashSet)-import qualified Data.HashSet as S-import Data.List (find, foldl')-import Data.Text (Text)--import Data.RBAC.Role-import Data.RBAC.Internal.Types---newtype RoleMap = RoleMap (HashMap Text (HashSet Role))----------------------------------------------------------------------------------fromList :: [Role] -> RoleMap-fromList = RoleMap . foldl' ins M.empty- where- ins m role =- M.insertWith S.union (_roleName role) (S.singleton role) m----------------------------------------------------------------------------------lookup :: Role -> RoleMap -> Maybe Role-lookup role (RoleMap m) = find (`matches` role) l- where- l = maybe [] S.toList $ M.lookup (_roleName role) m----------------------------------------------------------------------------------delete :: Role -> RoleMap -> RoleMap-delete role (RoleMap m) = RoleMap $ maybe m upd $ M.lookup rNm m- where- rNm = _roleName role- upd s = maybe m- (\r -> let s' = S.delete r s- in if S.null s'- then M.delete rNm m- else M.insert rNm s' m)- (find (`matches` role) $ S.toList s)----------------------------------------------------------------------------------insert :: Role -> RoleMap -> RoleMap-insert role (RoleMap m) =- RoleMap $ M.insertWith S.union (_roleName role) (S.singleton role) m----------------------------------------------------------------------------------empty :: RoleMap-empty = RoleMap M.empty----------------------------------------------------------------------------------null :: RoleMap -> Bool-null (RoleMap m) = M.null m
− src/Data/RBAC/Internal/Rule.hs
@@ -1,31 +0,0 @@-module Data.RBAC.Internal.Rule where--import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M-import Data.List (foldl')-import Data.Monoid-import Data.Text (Text)--import Data.RBAC.Internal.Role---------------------------------------------------------------------------------data Rule = Rule Text (Role -> [Role])--newtype RuleSet = RuleSet (HashMap Text (Role -> [Role]))--instance Monoid RuleSet where- mempty = RuleSet M.empty- (RuleSet m1) `mappend` (RuleSet m2) = RuleSet $ M.foldlWithKey' ins m2 m1- where- combine f1 f2 r = f1 r ++ f2 r- ins m k v = M.insertWith combine k v m----------------------------------------------------------------------------------ruleToSet :: Rule -> RuleSet-ruleToSet (Rule nm f) = RuleSet $ M.singleton nm f----------------------------------------------------------------------------------rulesToSet :: [Rule] -> RuleSet-rulesToSet = foldl' mappend (RuleSet M.empty) . map ruleToSet
− src/Data/RBAC/Internal/Types.hs
@@ -1,33 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.RBAC.Internal.Types- ( module Data.RBAC.Internal.Role- , module Data.RBAC.Internal.Rule- , RoleMonad(..)- , RuleChecker(..)- ) where--import Control.Applicative-import Control.Monad.Reader-import Control.Monad.Logic--import Data.RBAC.Internal.Role-import Data.RBAC.Internal.Rule------------------------------------------------------------------------------------ TODO: should the monads be transformers here? If they were, you could check--- more complex predicates here----------------------------------------------------------------------------------newtype RoleMonad a = RoleMonad { _unRC :: Logic a }- deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)----------------------------------------------------------------------------------newtype RuleChecker a = RuleChecker (ReaderT (RoleMonad Role) RoleMonad a)- deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)-
− src/Data/RBAC/Role.hs
@@ -1,24 +0,0 @@-module Data.RBAC.Role where--import qualified Data.HashMap.Strict as M-import Data.RBAC.Internal.Types-import Data.Text (Text)----------------------------------------------------------------------------------matches :: Role -> Role -> Bool-matches (Role a1 d1) (Role a2 d2) =- a1 == a2 && dmatch (toSortedList d1) (toSortedList d2)- where- dmatch [] _ = True- dmatch _ [] = False- dmatch dds@(d:ds) (e:es) =- case compare d e of- LT -> False- EQ -> dmatch ds es- GT -> dmatch dds es----------------------------------------------------------------------------------addRoleData :: Text -> RoleValue -> Role -> Role-addRoleData k v (Role n d) = Role n $ M.insert k v d
− src/Data/RBAC/Types.hs
@@ -1,14 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.RBAC.Types- ( Role(..) -- fixme: remove (..)- , RoleValue(..) -- fixme- , RoleValueMeta(..)- , RoleDataDefinition(..)- , RoleMetadata(..)- , Rule- , RuleChecker- ) where--import Data.RBAC.Internal.Types
src/Snap/Loader/Devel.hs view
@@ -1,5 +1,7 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE CPP #-}++------------------------------------------------------------------------------ -- | This module includes the machinery necessary to use hint to load -- action code dynamically. It includes a Template Haskell function -- to gather the necessary compile-time information about code@@ -9,22 +11,18 @@ ( loadSnapTH ) where +------------------------------------------------------------------------------ #ifdef HINT_ENABLED import Control.Monad (liftM2)- import Data.Char (isAlphaNum) import Data.List import Data.Maybe (maybeToList) import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Typeable- import Language.Haskell.Interpreter hiding (lift, liftIO, typeOf) import Language.Haskell.Interpreter.Unsafe- import Language.Haskell.TH- import System.Environment (getArgs)- import Snap.Core import Snap.Loader.Devel.Signal import Snap.Loader.Devel.Evaluator@@ -33,49 +31,48 @@ import Language.Haskell.TH #endif + --------------------------------------------------------------------------------- | This function derives all the information necessary to use the--- interpreter from the compile-time environment, and compiles it in--- to the generated code.+-- | This function derives all the information necessary to use the interpreter+-- from the compile-time environment, and compiles it in to the generated code. -- -- This could be considered a TH wrapper around a function -- -- > loadSnap :: Typeable a => IO a -> (a -> IO (Snap (), IO ())) -- > -> [String] -> IO (a, Snap (), IO ()) ----- with a magical implementation. The [String] argument is a list of--- directories to watch for updates to trigger a reloading.--- Directories containing code should be automatically picked up by--- this splice.+-- with a magical implementation. The [String] argument is a list of+-- directories to watch for updates to trigger a reloading. Directories+-- containing code should be automatically picked up by this splice. ----- The generated splice executes the initialiser once, sets up the--- interpreter for the load function, and returns the initializer's--- result along with the interpreter's proxy handler and cleanup--- actions. The behavior of the proxy actions will change to reflect--- changes in the watched files, reinterpreting the load function as--- needed and applying it to the initializer result.+-- The generated splice executes the initialiser once, sets up the interpreter+-- for the load function, and returns the initializer's result along with the+-- interpreter's proxy handler and cleanup actions. The behavior of the proxy+-- actions will change to reflect changes in the watched files, reinterpreting+-- the load function as needed and applying it to the initializer result. ----- This will handle reloading the application successfully in most--- cases. The cases in which it is certain to fail are those--- involving changing the types of the initializer or the load--- function, or changing the compiler options required, such as by--- changing/adding dependencies in the project's .cabal file. In--- those cases, a full recompile will be needed.-loadSnapTH :: Q Exp -- ^ the initializer expression- -> Name -- ^ the name of the load function- -> [String] -- ^ a list of directories to watch in addition- -- to those containing code+-- This will handle reloading the application successfully in most cases. The+-- cases in which it is certain to fail are those involving changing the types+-- of the initializer or the load function, or changing the compiler options+-- required, such as by changing/adding dependencies in the project's .cabal+-- file. In those cases, a full recompile will be needed.+--+loadSnapTH :: Q Exp -- ^ the initializer expression+ -> Name -- ^ the name of the load function+ -> [String] -- ^ a list of directories to watch in addition+ -- to those containing code -> Q Exp #ifndef HINT_ENABLED-loadSnapTH _ _ _ = fail $ "Snap was built without hint support. Hint " ++- "support is necessary for development mode. " ++- "Please reinstall snap with hint support.\n\n " ++- " cabal install snap -fhint\n\n"+loadSnapTH _ _ _ = fail $+ concat [ "Snap was built without hint support. Hint "+ , "support is necessary for development mode. "+ , "Please reinstall snap with hint support.\n\n "+ , " cabal install snap -fhint\n\n" ] #else loadSnapTH initializer action additionalWatchDirs = do args <- runIO getArgs - let opts = getHintOpts args+ let opts = getHintOpts args srcPaths = additionalWatchDirs ++ getSrcPaths args -- The first line is an extra type check to ensure the arguments@@ -97,21 +94,32 @@ getHintOpts :: [String] -> [String] getHintOpts args = removeBad opts where- bad = ["-threaded", "-O"]+ --------------------------------------------------------------------------+ bad = ["-threaded", "-O"]++ -------------------------------------------------------------------------- removeBad = filter (\x -> not $ any (`isPrefixOf` x) bad) - hideAll = filter (== "-hide-all-packages") args+ --------------------------------------------------------------------------+ hideAll = filter (== "-hide-all-packages") args - srcOpts = filter (\x -> "-i" `isPrefixOf` x- && not ("-idist" `isPrefixOf` x)) args+ --------------------------------------------------------------------------+ srcOpts = filter (\x -> "-i" `isPrefixOf` x+ && not ("-idist" `isPrefixOf` x))+ args - toCopy = filter (not . isSuffixOf ".hs") $- dropWhile (not . ("-package" `isPrefixOf`)) args- copy = map (intercalate " ") . groupBy (\_ s -> not $ "-" `isPrefixOf` s)+ --------------------------------------------------------------------------+ toCopy = filter (not . isSuffixOf ".hs") $+ dropWhile (not . ("-package" `isPrefixOf`)) args - opts = hideAll ++ srcOpts ++ copy toCopy+ --------------------------------------------------------------------------+ copy = map (intercalate " ")+ . groupBy (\_ s -> not $ "-" `isPrefixOf` s) + --------------------------------------------------------------------------+ opts = concat [hideAll, srcOpts, copy toCopy] + ------------------------------------------------------------------------------ -- | This function extracts the source paths from the compilation args getSrcPaths :: [String] -> [String]@@ -121,41 +129,50 @@ --------------------------------------------------------------------------------- | This function creates the Snap handler that actually is--- responsible for doing the dynamic loading of actions via hint,--- given all of the configuration information that the interpreter--- needs. It also ensures safe concurrent access to the interpreter,--- and caches the interpreter results for a short time before allowing--- it to run again.+-- | This function creates the Snap handler that actually is responsible for+-- doing the dynamic loading of actions via hint, given all of the+-- configuration information that the interpreter needs. It also ensures safe+-- concurrent access to the interpreter, and caches the interpreter results for+-- a short time before allowing it to run again. ----- Generally, this won't be called manually. Instead, loadSnapTH will--- generate a call to it at compile-time, calculating all the--- arguments from its environment.+-- Generally, this won't be called manually. Instead, loadSnapTH will generate+-- a call to it at compile-time, calculating all the arguments from its+-- environment.+-- hintSnap :: Typeable a- => [String] -- ^ A list of command-line options for the interpreter- -> [String] -- ^ A list of modules that need to be- -- interpreted. This should contain only the- -- modules which contain the initialization,- -- cleanup, and handler actions. Everything else- -- they require will be loaded transitively.- -> [String] -- ^ A list of paths to watch for updates- -> String -- ^ The name of the function to load- -> a -- ^ The value to apply the loaded function to+ => [String]+ -- ^ A list of command-line options for the interpreter+ -> [String]+ -- ^ A list of modules that need to be interpreted. This should+ -- contain only the modules which contain the initialization,+ -- cleanup, and handler actions. Everything else they require will+ -- be loaded transitively.+ -> [String]+ -- ^ A list of paths to watch for updates+ -> String+ -- ^ The name of the function to load+ -> a+ -- ^ The value to apply the loaded function to -> IO (Snap (), IO ()) hintSnap opts modules srcPaths action value = protectedHintEvaluator initialize test loader+ where+ -------------------------------------------------------------------------- witness x = undefined $ x `asTypeOf` value :: HintLoadable - -- This is somewhat fragile, and probably can be cleaned up with a- -- future version of Typeable. For the moment, and- -- backwards-compatibility, this is the approach being taken.+ --------------------------------------------------------------------------+ -- This is somewhat fragile, and probably can be cleaned up with a future+ -- version of Typeable. For the moment, and backwards-compatibility, this+ -- is the approach being taken. witnessModules = map (reverse . drop 1 . dropWhile (/= '.') . reverse) . filter (elem '.') . groupBy typePart . show . typeOf $ witness + -------------------------------------------------------------------------- typePart x y = (isAlphaNum x && isAlphaNum y) || x == '.' || y == '.' + -------------------------------------------------------------------------- interpreter = do loadModules . nub $ modules setImports . nub $ "Prelude" : witnessModules ++ modules@@ -163,15 +180,20 @@ f <- interpret action witness return $ f value + -------------------------------------------------------------------------- loadInterpreter = unsafeRunInterpreterWithArgs opts interpreter + -------------------------------------------------------------------------- formatOnError (Left err) = error $ format err formatOnError (Right a) = a + -------------------------------------------------------------------------- loader = formatOnError `fmap` protectHandlers loadInterpreter + -------------------------------------------------------------------------- initialize = liftM2 (,) getCurrentTime $ getTreeStatus srcPaths + -------------------------------------------------------------------------- test (prevTime, ts) = do now <- getCurrentTime if diffUTCTime now prevTime < 3
src/Snap/Loader/Devel/Evaluator.hs view
@@ -5,16 +5,13 @@ , protectedHintEvaluator ) where -+------------------------------------------------------------------------------ import Control.Exception import Control.Monad (when) import Control.Monad.Trans (liftIO)- import Control.Concurrent (ThreadId, forkIO, myThreadId) import Control.Concurrent.MVar- import Prelude hiding (catch, init, any)- import Snap.Core (Snap) @@ -24,43 +21,41 @@ --------------------------------------------------------------------------------- | Convert an action to generate 'HintLoadable's into Snap and IO--- actions that handle periodic reloading. The resulting action will--- share initialized state until the next execution of the input--- action. At this time, the cleanup action will be executed.+-- | Convert an action to generate 'HintLoadable's into Snap and IO actions+-- that handle periodic reloading. The resulting action will share initialized+-- state until the next execution of the input action. At this time, the+-- cleanup action will be executed. ----- The first two arguments control when recompiles are done. The--- first argument is an action that is executed when compilation--- starts. The second is a function from the result of the first--- action to an action that determines whether the value from the--- previous compilation is still good. This abstracts out the--- strategy for determining when a cached result is no longer valid.+-- The first two arguments control when recompiles are done. The first argument+-- is an action that is executed when compilation starts. The second is a+-- function from the result of the first action to an action that determines+-- whether the value from the previous compilation is still good. This+-- abstracts out the strategy for determining when a cached result is no longer+-- valid. ----- If an exception is raised during the processing of the action, it--- will be thrown to all waiting threads, and for all requests made--- before the recompile condition is reached.+-- If an exception is raised during the processing of the action, it will be+-- thrown to all waiting threads, and for all requests made before the+-- recompile condition is reached. protectedHintEvaluator :: forall a. IO a -> (a -> IO Bool) -> IO HintLoadable -> IO (Snap (), IO ()) protectedHintEvaluator start test getInternals = do- -- The list of requesters waiting for a result. Contains the- -- ThreadId in case of exceptions, and an empty MVar awaiting a- -- successful result.+ -- The list of requesters waiting for a result. Contains the ThreadId in+ -- case of exceptions, and an empty MVar awaiting a successful result. readerContainer <- newReaderContainer - -- Contains the previous result and initialization value, and the- -- time it was stored, if a previous result has been computed.- -- The result stored is either the actual result and- -- initialization result, or the exception thrown by the- -- calculation.+ -- Contains the previous result and initialization value, and the time it+ -- was stored, if a previous result has been computed. The result stored is+ -- either the actual result and initialization result, or the exception+ -- thrown by the calculation. resultContainer <- newResultContainer - -- The model used for the above MVars in the returned action is- -- "keep them full, unless updating them." In every case, when- -- one of those MVars is emptied, the next action is to fill that- -- same MVar. This makes deadlocking on MVar wait impossible.+ -- The model used for the above MVars in the returned action is "keep them+ -- full, unless updating them." In every case, when one of those MVars is+ -- emptied, the next action is to fill that same MVar. This makes+ -- deadlocking on MVar wait impossible. let snap = do let waitForNewResult :: IO (Snap ()) waitForNewResult = do@@ -132,6 +127,7 @@ cleanup contents return (snap, clean)+ where newReaderContainer :: IO (MVar [(ThreadId, MVar (Snap ()))]) newReaderContainer = newMVar []
src/Snap/Loader/Devel/Signal.hs view
@@ -1,27 +1,37 @@ {-# LANGUAGE CPP #-} module Snap.Loader.Devel.Signal (protectHandlers) where +------------------------------------------------------------------------------ import Control.Exception (bracket) + #ifdef mingw32_HOST_OS-import GHC.ConsoleHandler as C + -------------+ -- windows --+ -------------+------------------------------------------------------------------------------+import GHC.ConsoleHandler as C saveHandlers :: IO C.Handler saveHandlers = C.installHandler Ignore - restoreHandlers :: C.Handler -> IO C.Handler restoreHandlers = C.installHandler+------------------------------------------------------------------------------ #else++ -----------+ -- posix --+ -----------+------------------------------------------------------------------------------ import qualified System.Posix.Signals as S helper :: S.Handler -> S.Signal -> IO S.Handler helper handler signal = S.installHandler signal handler Nothing - signals :: [S.Signal] signals = [ S.sigQUIT , S.sigINT@@ -29,15 +39,19 @@ , S.sigTERM ] - saveHandlers :: IO [S.Handler] saveHandlers = mapM (helper S.Ignore) signals - restoreHandlers :: [S.Handler] -> IO [S.Handler] restoreHandlers h = sequence $ zipWith helper h signals-+------------------------------------------------------------------------------ #endif++ ----------+ -- both --+ ----------+------------------------------------------------------------------------------ protectHandlers :: IO a -> IO a protectHandlers a = bracket saveHandlers restoreHandlers $ const a+------------------------------------------------------------------------------
src/Snap/Loader/Devel/TreeWatcher.hs view
@@ -1,14 +1,13 @@ module Snap.Loader.Devel.TreeWatcher- ( TreeStatus- , getTreeStatus- , checkTreeStatus- ) where+ ( TreeStatus+ , getTreeStatus+ , checkTreeStatus+ ) where +------------------------------------------------------------------------------ import Control.Applicative- import System.Directory import System.Directory.Tree- import System.Time
src/Snap/Loader/Prod.hs view
@@ -1,8 +1,10 @@ {-# LANGUAGE TemplateHaskell #-}+ module Snap.Loader.Prod ( loadSnapTH ) where +------------------------------------------------------------------------------ import Language.Haskell.TH
src/Snap/Snaplet.hs view
@@ -100,6 +100,7 @@ -- * Handlers , Handler , reloadSite+ , bracketHandler -- * Serving Applications , runSnaplet
src/Snap/Snaplet/Auth.hs view
@@ -1,22 +1,21 @@ {-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE OverloadedStrings #-}--{-|-- This module contains all the central authentication functionality.-- It exports a number of high-level functions to be used directly in your- application handlers.-- We also export a number of mid-level functions that- should be helpful when you are integrating with another way of confirming- the authentication of login requests.+{-# LANGUAGE OverloadedStrings #-} --}+------------------------------------------------------------------------------+-- |+--+-- This module contains all the central authentication functionality.+--+-- It exports a number of high-level functions to be used directly in your+-- application handlers.+--+-- We also export a number of mid-level functions that should be helpful when+-- you are integrating with another way of confirming the authentication of+-- login requests.+-- module Snap.Snaplet.Auth (- -- * Higher Level Handler Functions createUser , usernameExists@@ -53,6 +52,8 @@ , checkPassword , authenticatePassword , setPassword+ , encrypt+ , verify -- * Handlers , registerUser@@ -64,9 +65,11 @@ , addAuthSplices , ifLoggedIn , ifLoggedOut+ , loggedInUser ) where +------------------------------------------------------------------------------ import Snap.Snaplet.Auth.AuthManager import Snap.Snaplet.Auth.Handlers import Snap.Snaplet.Auth.SpliceHelpers
src/Snap/Snaplet/Auth/AuthManager.hs view
@@ -1,52 +1,49 @@--{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE DeriveDataTypeable #-}+------------------------------------------------------------------------------+-- | Internal module exporting AuthManager implementation.+--+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} module Snap.Snaplet.Auth.AuthManager--(- -- * AuthManager Datatype+ ( -- * AuthManager Datatype AuthManager(..) - -- * Backend Typeclass- , IAuthBackend(..)-- -- * Context-free Operations- , buildAuthUser--) where+ -- * Backend Typeclass+ , IAuthBackend(..) + -- * Context-free Operations+ , buildAuthUser+ ) where +------------------------------------------------------------------------------ import Data.ByteString (ByteString) import Data.Lens.Lazy-import Data.Time import Data.Text (Text)+import Data.Time import Web.ClientSession import Snap.Snaplet import Snap.Snaplet.Session+import Snap.Snaplet.Session.Common import Snap.Snaplet.Auth.Types + --------------------------------------------------------------------------------- | Create a new user from just a username and password+-- | Creates a new user from a username and password. -- -- May throw a "DuplicateLogin" if given username is not unique-buildAuthUser- :: (IAuthBackend r)- => r- -- ^ An auth backend- -> Text- -- ^ Username- -> ByteString- -- ^ Password- -> IO AuthUser+buildAuthUser :: IAuthBackend r =>+ r -- ^ An auth backend+ -> Text -- ^ Username+ -> ByteString -- ^ Password+ -> IO AuthUser buildAuthUser r unm pass = do now <- getCurrentTime let au = defAuthUser {- userLogin = unm- , userPassword = Nothing+ userLogin = unm+ , userPassword = Nothing , userCreatedAt = Just now , userUpdatedAt = Just now }@@ -59,44 +56,45 @@ -- -- Backend operations may throw 'BackendError's class IAuthBackend r where-- -- | Needs to create or update the given 'AuthUser' record- save :: r -> AuthUser -> IO AuthUser-- lookupByUserId :: r -> UserId -> IO (Maybe AuthUser)-- lookupByLogin :: r -> Text -> IO (Maybe AuthUser)-- lookupByRememberToken :: r -> Text -> IO (Maybe AuthUser)-- destroy :: r -> AuthUser -> IO ()+ -- | Create or update the given 'AuthUser' record. If the 'userId' in the+ -- 'AuthUser' already exists in the database, then that user's information+ -- should be updated. If it does not exist, then a new user should be+ -- created.+ save :: r -> AuthUser -> IO AuthUser+ lookupByUserId :: r -> UserId -> IO (Maybe AuthUser)+ lookupByLogin :: r -> Text -> IO (Maybe AuthUser)+ lookupByRememberToken :: r -> Text -> IO (Maybe AuthUser)+ destroy :: r -> AuthUser -> IO () ------------------------------------------------------------------------------ -- | Abstract data type holding all necessary information for auth operation data AuthManager b = forall r. IAuthBackend r => AuthManager {- backend :: r- -- ^ Storage back-end+ backend :: r+ -- ^ Storage back-end - , session :: Lens b (Snaplet SessionManager)- -- ^ A lens pointer to a SessionManager+ , session :: Lens b (Snaplet SessionManager)+ -- ^ A lens pointer to a SessionManager - , activeUser :: Maybe AuthUser- -- ^ A per-request logged-in user cache+ , activeUser :: Maybe AuthUser+ -- ^ A per-request logged-in user cache - , minPasswdLen :: Int- -- ^ Password length range+ , minPasswdLen :: Int+ -- ^ Password length range - , rememberCookieName :: ByteString- -- ^ Cookie name for the remember token+ , rememberCookieName :: ByteString+ -- ^ Cookie name for the remember token - , rememberPeriod :: Maybe Int- -- ^ Remember period in seconds. Defaults to 2 weeks.+ , rememberPeriod :: Maybe Int+ -- ^ Remember period in seconds. Defaults to 2 weeks. - , siteKey :: Key- -- ^ A unique encryption key used to encrypt remember cookie+ , siteKey :: Key+ -- ^ A unique encryption key used to encrypt remember cookie - , lockout :: Maybe (Int, NominalDiffTime)- -- ^ Lockout after x tries, re-allow entry after y seconds- }+ , lockout :: Maybe (Int, NominalDiffTime)+ -- ^ Lockout after x tries, re-allow entry after y seconds++ , randomNumberGenerator :: RNG+ -- ^ Random number generator+ }
src/Snap/Snaplet/Auth/Backends/JsonFile.hs view
@@ -32,35 +32,39 @@ import Snap.Snaplet.Auth.Types import Snap.Snaplet.Auth.AuthManager import Snap.Snaplet.Session+import Snap.Snaplet.Session.Common ------------------------------------------------------------------------------ -- | Initialize a JSON file backed 'AuthManager'-initJsonFileAuthManager- :: AuthSettings- -- ^ Authentication settings for your app- -> Lens b (Snaplet SessionManager)- -- ^ Lens into a 'SessionManager' auth snaplet will use- -> FilePath- -- ^ Where to store user data as JSON- -> SnapletInit b (AuthManager b)-initJsonFileAuthManager s l db =- makeSnaplet "JsonFileAuthManager"- "A snaplet providing user authentication using a JSON-file backend"- Nothing $ liftIO $ do- key <- getKey (asSiteKey s)- jsonMgr <- mkJsonAuthMgr db- return $ AuthManager {- backend = jsonMgr- , session = l- , activeUser = Nothing- , minPasswdLen = asMinPasswdLen s- , rememberCookieName = asRememberCookieName s- , rememberPeriod = asRememberPeriod s- , siteKey = key- , lockout = asLockout s- }+initJsonFileAuthManager :: AuthSettings+ -- ^ Authentication settings for your app+ -> Lens b (Snaplet SessionManager)+ -- ^ Lens into a 'SessionManager' auth snaplet will+ -- use+ -> FilePath+ -- ^ Where to store user data as JSON+ -> SnapletInit b (AuthManager b)+initJsonFileAuthManager s l db = do+ makeSnaplet+ "JsonFileAuthManager"+ "A snaplet providing user authentication using a JSON-file backend"+ Nothing $ liftIO $ do+ rng <- liftIO mkRNG+ key <- getKey (asSiteKey s)+ jsonMgr <- mkJsonAuthMgr db+ return $! AuthManager {+ backend = jsonMgr+ , session = l+ , activeUser = Nothing+ , minPasswdLen = asMinPasswdLen s+ , rememberCookieName = asRememberCookieName s+ , rememberPeriod = asRememberPeriod s+ , siteKey = key+ , lockout = asLockout s+ , randomNumberGenerator = rng+ } ------------------------------------------------------------------------------@@ -71,51 +75,55 @@ mkJsonAuthMgr fp = do db <- loadUserCache fp let db' = case db of- Left e -> error e+ Left e -> error e Right x -> x cache <- newTVarIO db'- return $ JsonFileAuthManager {++ return $! JsonFileAuthManager { memcache = cache- , dbfile = fp+ , dbfile = fp } +------------------------------------------------------------------------------ type UserIdCache = Map UserId AuthUser - instance ToJSON UserIdCache where toJSON m = toJSON $ HM.toList m - instance FromJSON UserIdCache where parseJSON = fmap HM.fromList . parseJSON -+------------------------------------------------------------------------------ type LoginUserCache = Map Text UserId +------------------------------------------------------------------------------ type RemTokenUserCache = Map Text UserId --- JSON user back-end stores the user data and indexes for login and token+------------------------------------------------------------------------------+-- | JSON user back-end stores the user data and indexes for login and token -- based logins. data UserCache = UserCache {- uidCache :: UserIdCache -- the actual datastore- , loginCache :: LoginUserCache -- fast lookup for login field- , tokenCache :: RemTokenUserCache -- fast lookup for remember tokens- , uidCounter :: Int -- user id counter+ uidCache :: UserIdCache -- ^ the actual datastore+ , loginCache :: LoginUserCache -- ^ fast lookup for login field+ , tokenCache :: RemTokenUserCache -- ^ fast lookup for remember tokens+ , uidCounter :: Int -- ^ user id counter } +------------------------------------------------------------------------------ defUserCache :: UserCache defUserCache = UserCache {- uidCache = HM.empty+ uidCache = HM.empty , loginCache = HM.empty , tokenCache = HM.empty , uidCounter = 0 } +------------------------------------------------------------------------------ loadUserCache :: FilePath -> IO (Either String UserCache) loadUserCache fp = do chk <- doesFileExist fp@@ -123,117 +131,136 @@ True -> do d <- B.readFile fp case Atto.parseOnly json d of- Left e -> return . Left $ "Can't open JSON auth backend. Error: " ++ e+ Left e -> return $! Left $+ "Can't open JSON auth backend. Error: " ++ e Right v -> case fromJSON v of- Error e -> return . Left $- "Malformed JSON auth data store. Error: " ++ e- Success db -> return $ Right db+ Error e -> return $! Left $+ "Malformed JSON auth data store. Error: " ++ e+ Success db -> return $! Right db False -> do putStrLn "User JSON datafile not found. Creating a new one." return $ Right defUserCache +------------------------------------------------------------------------------ data JsonFileAuthManager = JsonFileAuthManager { memcache :: TVar UserCache- , dbfile :: FilePath+ , dbfile :: FilePath } -instance IAuthBackend JsonFileAuthManager where-- save mgr u = do- now <- getCurrentTime+------------------------------------------------------------------------------+jsonFileSave :: JsonFileAuthManager -> AuthUser -> IO AuthUser+jsonFileSave mgr u = do+ now <- getCurrentTime oldByLogin <- lookupByLogin mgr (userLogin u)- oldById <- case userId u of- Nothing -> return Nothing- Just x -> lookupByUserId mgr x+ oldById <- case userId u of+ Nothing -> return Nothing+ Just x -> lookupByUserId mgr x+ res <- atomically $ do cache <- readTVar (memcache mgr)- res <- case userId u of- Nothing -> create cache now oldByLogin- Just _ -> update cache now oldById+ res <- case userId u of+ Nothing -> create cache now oldByLogin+ Just _ -> update cache now oldById case res of- Left e -> return $ Left e+ Left e -> return $! Left e Right (cache', u') -> do writeTVar (memcache mgr) cache'- return $ Right (cache', u')+ return $! Right $! (cache', u')+ case res of- Left e -> throw e+ Left e -> throw e Right (cache', u') -> do dumpToDisk cache'- return u'- where- create- :: UserCache- -> UTCTime- -> (Maybe AuthUser)- -> STM (Either BackendError (UserCache, AuthUser))- create cache now old = do- case old of- Just _ -> return $ Left DuplicateLogin- Nothing -> do- new <- do- let uid' = UserId . showT $ uidCounter cache + 1- let u' = u { userUpdatedAt = Just now, userId = Just uid' }- return $ cache {- uidCache = HM.insert uid' u' $ uidCache cache- , loginCache = HM.insert (userLogin u') uid' $ loginCache cache- , tokenCache = case userRememberToken u' of- Nothing -> tokenCache cache- Just x -> HM.insert x uid' $ tokenCache cache- , uidCounter = uidCounter cache + 1- }- return $ Right (new, getLastUser new)+ return $! u' + where+ --------------------------------------------------------------------------+ create :: UserCache+ -> UTCTime+ -> (Maybe AuthUser)+ -> STM (Either BackendError (UserCache, AuthUser))+ create cache now old = do+ case old of+ Just _ -> return $! Left DuplicateLogin+ Nothing -> do+ new <- do+ let uid' = UserId . showT $ uidCounter cache + 1+ let u' = u { userUpdatedAt = Just now, userId = Just uid' }+ return $! cache {+ uidCache = HM.insert uid' u' $ uidCache cache+ , loginCache = HM.insert (userLogin u') uid' $ loginCache cache+ , tokenCache = case userRememberToken u' of+ Nothing -> tokenCache cache+ Just x -> HM.insert x uid' $ tokenCache cache+ , uidCounter = uidCounter cache + 1+ }+ return $! Right (new, getLastUser new) - -- lookup old record, see what's changed and update indexes accordingly- update- :: UserCache- -> UTCTime- -> (Maybe AuthUser)- -> STM (Either BackendError (UserCache, AuthUser))- update cache now old =- case old of- Nothing -> return $ Left $- BackendError "User not found; should never happen"- Just x -> do- let oldLogin = userLogin x- let oldToken = userRememberToken x- let uid = fromJust $ userId u- let newLogin = userLogin u- let newToken = userRememberToken u- let lc = if oldLogin /= userLogin u- then HM.insert newLogin uid . HM.delete oldLogin $- loginCache cache- else loginCache cache- let tc = if oldToken /= newToken && isJust oldToken- then HM.delete (fromJust oldToken) $ loginCache cache- else tokenCache cache- let tc' = case newToken of- Just t -> HM.insert t uid tc- Nothing -> tc- let u' = u { userUpdatedAt = Just now }- let new = cache {- uidCache = HM.insert uid u' $ uidCache cache- , loginCache = lc- , tokenCache = tc'- }- return $ Right (new, u')+ --------------------------------------------------------------------------+ -- lookup old record, see what's changed and update indexes accordingly+ update :: UserCache+ -> UTCTime+ -> (Maybe AuthUser)+ -> STM (Either BackendError (UserCache, AuthUser))+ update cache now old =+ case old of+ Nothing -> return $! Left $+ BackendError "User not found; should never happen"+ Just x -> do+ let oldLogin = userLogin x+ let oldToken = userRememberToken x+ let uid = fromJust $ userId u+ let newLogin = userLogin u+ let newToken = userRememberToken u - -- Sync user database to disk- -- Need to implement a mutex here; simult syncs could screw things up- dumpToDisk c = LB.writeFile (dbfile mgr) (encode c)+ let lc = if oldLogin /= userLogin u+ then HM.insert newLogin uid $+ HM.delete oldLogin $+ loginCache cache+ else loginCache cache - -- Get's the last added user- getLastUser cache = maybe e id $ getUser cache uid- where uid = UserId . showT $ uidCounter cache- e = error "getLastUser failed. This should not happen."+ let tc = if oldToken /= newToken && isJust oldToken+ then HM.delete (fromJust oldToken) $ loginCache cache+ else tokenCache cache + let tc' = case newToken of+ Just t -> HM.insert t uid tc+ Nothing -> tc + let u' = u { userUpdatedAt = Just now }++ let new = cache {+ uidCache = HM.insert uid u' $ uidCache cache+ , loginCache = lc+ , tokenCache = tc'+ }++ return $! Right (new, u')++ --------------------------------------------------------------------------+ -- Sync user database to disk+ -- Need to implement a mutex here; simult syncs could screw things up+ dumpToDisk c = LB.writeFile (dbfile mgr) (encode c)++ --------------------------------------------------------------------------+ -- Gets the last added user+ getLastUser cache = maybe e id $ getUser cache uid+ where+ uid = UserId . showT $ uidCounter cache+ e = error "getLastUser failed. This should not happen."+++------------------------------------------------------------------------------+instance IAuthBackend JsonFileAuthManager where+ save = jsonFileSave+ destroy = error "JsonFile: destroy is not yet implemented" lookupByUserId mgr uid = withCache mgr f- where f cache = getUser cache uid+ where+ f cache = getUser cache uid lookupByLogin mgr login = withCache mgr f where@@ -243,33 +270,42 @@ lookupByRememberToken mgr token = withCache mgr f where f cache = getUid >>= getUser cache- where getUid = HM.lookup token (tokenCache cache)+ where+ getUid = HM.lookup token (tokenCache cache) +------------------------------------------------------------------------------ withCache :: JsonFileAuthManager -> (UserCache -> a) -> IO a withCache mgr f = atomically $ do cache <- readTVar $ memcache mgr- return $ f cache+ return $! f cache +------------------------------------------------------------------------------ getUser :: UserCache -> UserId -> Maybe AuthUser getUser cache uid = HM.lookup uid (uidCache cache) --------------------------------------------------------------------------------- JSON Instances----------------------------------------------------------------------------------+showT :: Int -> Text+showT = T.pack . show + --------------------+ -- JSON Instances --+ --------------------++------------------------------------------------------------------------------ instance ToJSON UserCache where toJSON uc = object- [ "uidCache" .= uidCache uc+ [ "uidCache" .= uidCache uc , "loginCache" .= loginCache uc , "tokenCache" .= tokenCache uc- , "uidCounter" .= uidCounter uc]+ , "uidCounter" .= uidCounter uc+ ] +------------------------------------------------------------------------------ instance FromJSON UserCache where parseJSON (Object v) = UserCache@@ -279,57 +315,4 @@ <*> v .: "uidCounter" parseJSON _ = error "Unexpected JSON input" -instance ToJSON AuthUser where- toJSON u = object- [ "uid" .= userId u- , "login" .= userLogin u- , "pw" .= userPassword u- , "activated_at" .= userActivatedAt u- , "suspended_at" .= userSuspendedAt u- , "remember_token" .= userRememberToken u- , "login_count" .= userLoginCount u- , "failed_login_count" .= userFailedLoginCount u- , "locked_until" .= userLockedOutUntil u- , "current_login_at" .= userCurrentLoginAt u- , "last_login_at" .= userLastLoginAt u- , "current_ip" .= userCurrentLoginIp u- , "last_ip" .= userLastLoginIp u- , "created_at" .= userCreatedAt u- , "updated_at" .= userUpdatedAt u- , "meta" .= userMeta u ] --instance FromJSON AuthUser where- parseJSON (Object v) = AuthUser- <$> v .: "uid"- <*> v .: "login"- <*> v .: "pw"- <*> v .: "activated_at"- <*> v .: "suspended_at"- <*> v .: "remember_token"- <*> v .: "login_count"- <*> v .: "failed_login_count"- <*> v .: "locked_until"- <*> v .: "current_login_at"- <*> v .: "last_login_at"- <*> v .: "current_ip"- <*> v .: "last_ip"- <*> v .: "created_at"- <*> v .: "updated_at"- <*> return []- <*> v .: "meta"- parseJSON _ = error "Unexpected JSON input"---instance ToJSON Password where- toJSON (Encrypted x) = toJSON x- toJSON (ClearText _) =- error "ClearText passwords can't be serialized into JSON"---instance FromJSON Password where- parseJSON = fmap Encrypted . parseJSON---showT :: Int -> Text-showT = T.pack . show
src/Snap/Snaplet/Auth/Handlers.hs view
@@ -1,29 +1,29 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE Rank2Types #-}--{-|-- Pre-packaged Handlers that deal with form submissions and standard use-cases- involving authentication.+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-} --}+------------------------------------------------------------------------------+-- | Pre-packaged Handlers that deal with form submissions and standard+-- use-cases involving authentication. module Snap.Snaplet.Auth.Handlers where +------------------------------------------------------------------------------ import Control.Applicative import Control.Monad.CatchIO (throw) import Control.Monad.State+import Control.Monad.Trans.Error+import Control.Monad.Trans.Maybe import Data.ByteString (ByteString) import Data.Lens.Lazy-import Data.Maybe (isJust)+import Data.Maybe (fromMaybe, isJust) import Data.Serialize hiding (get) import Data.Time import Data.Text.Encoding (decodeUtf8) import Data.Text (Text) import Web.ClientSession-+------------------------------------------------------------------------------ import Snap.Core import Snap.Snaplet import Snap.Snaplet.Auth.AuthManager@@ -31,119 +31,136 @@ import Snap.Snaplet.Session import Snap.Snaplet.Session.Common import Snap.Snaplet.Session.SecureCookie--- --------------------------------------------------------------------------------- Higher level functions------------------------------------------------------------------------------- + ----------------------------+ -- Higher level functions --+ ----------------------------+ ------------------------------------------------------------------------------ -- | Create a new user from just a username and password ----- May throw a "DuplicateLogin" if given username is not unique-createUser- :: Text -- Username- -> ByteString -- Password- -> Handler b (AuthManager b) AuthUser+-- May throw a "DuplicateLogin" if given username is not unique.+--+createUser :: Text -- ^ Username+ -> ByteString -- ^ Password+ -> Handler b (AuthManager b) AuthUser createUser unm pwd = withBackend (\r -> liftIO $ buildAuthUser r unm pwd) + ------------------------------------------------------------------------------ -- | Check whether a user with the given username exists.-usernameExists- :: Text- -- ^ The username to be checked- -> Handler b (AuthManager b) Bool-usernameExists username = withBackend $- \r -> liftIO $ isJust <$> lookupByLogin r username+--+usernameExists :: Text -- ^ The username to be checked+ -> Handler b (AuthManager b) Bool+usernameExists username =+ withBackend $ \r -> liftIO $ isJust <$> lookupByLogin r username + ------------------------------------------------------------------------------ -- | Lookup a user by her username, check given password and perform login-loginByUsername- :: ByteString -- ^ Username/login for user- -> Password -- ^ Should be ClearText- -> Bool -- ^ Set remember token?- -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+--+loginByUsername :: ByteString -- ^ Username/login for user+ -> Password -- ^ Should be ClearText+ -> Bool -- ^ Set remember token?+ -> Handler b (AuthManager b) (Either AuthFailure AuthUser) loginByUsername _ (Encrypted _) _ = error "Cannot login with encrypted password"-loginByUsername unm pwd rm = do- sk <- gets siteKey- cn <- gets rememberCookieName- rp <- gets rememberPeriod- withBackend $ loginByUsername' sk cn rp+loginByUsername unm pwd shouldRemember = do+ sk <- gets siteKey+ cn <- gets rememberCookieName+ rp <- gets rememberPeriod+ withBackend $ loginByUsername' sk cn rp+ where- loginByUsername' :: (IAuthBackend t)- => Key -> ByteString -> Maybe Int -> t- -> Handler b (AuthManager b)- (Either AuthFailure AuthUser)- loginByUsername' sk cn rp r = do- au <- liftIO $ lookupByLogin r (decodeUtf8 unm)- case au of- Nothing -> return $ Left UserNotFound- Just au' -> do- res <- checkPasswordAndLogin au' pwd- case res of- Left e -> return $ Left e- Right au'' -> do- case rm of- True -> do- token <- liftIO $ randomToken 64+ --------------------------------------------------------------------------+ loginByUsername' :: (IAuthBackend t) =>+ Key+ -> ByteString+ -> Maybe Int+ -> t+ -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+ loginByUsername' sk cn rp r =+ liftIO (lookupByLogin r $ decodeUtf8 unm) >>=+ maybe (return $! Left UserNotFound) found++ where+ ----------------------------------------------------------------------+ found user = checkPasswordAndLogin user pwd >>=+ either (return . Left) matched++ ----------------------------------------------------------------------+ matched user+ | shouldRemember = do+ token <- gets randomNumberGenerator >>=+ liftIO . randomToken 64+ setRememberToken sk cn rp token- let au''' = au''- { userRememberToken = Just (decodeUtf8 token) }- saveUser au'''- return $ Right au'''- False -> return $ Right au'' + let user' = user {+ userRememberToken = Just (decodeUtf8 token)+ } + saveUser user'+ return $! Right user'++ | otherwise = return $ Right user++ ------------------------------------------------------------------------------ -- | Remember user from the remember token if possible and perform login+-- loginByRememberToken :: Handler b (AuthManager b) (Maybe AuthUser)-loginByRememberToken = withBackend $ \r -> do- sk <- gets siteKey- rc <- gets rememberCookieName- rp <- gets rememberPeriod- token <- getRememberToken sk rc rp- au <- maybe (return Nothing)- (liftIO . lookupByRememberToken r . decodeUtf8) token- case au of- Just au' -> forceLogin au' >> return au- Nothing -> return Nothing+loginByRememberToken = withBackend $ \impl -> do+ key <- gets siteKey+ cookieName_ <- gets rememberCookieName+ period <- gets rememberPeriod + runMaybeT $ do+ token <- MaybeT $ getRememberToken key cookieName_ period+ user <- MaybeT $ liftIO $ lookupByRememberToken impl+ $ decodeUtf8 token+ lift $ forceLogin user+ return user + ------------------------------------------------------------------------------ -- | Logout the active user+-- logout :: Handler b (AuthManager b) () logout = do- s <- gets session- withTop s $ withSession s removeSessionUserId- rc <- gets rememberCookieName- forgetRememberToken rc- modify (\mgr -> mgr { activeUser = Nothing } )+ s <- gets session+ withTop s $ withSession s removeSessionUserId+ rc <- gets rememberCookieName+ forgetRememberToken rc+ modify $ \mgr -> mgr { activeUser = Nothing } ------------------------------------------------------------------------------ -- | Return the current user; trying to remember from cookie if possible.+-- currentUser :: Handler b (AuthManager b) (Maybe AuthUser) currentUser = cacheOrLookup $ withBackend $ \r -> do- s <- gets session- uid <- withTop s getSessionUserId- case uid of- Nothing -> loginByRememberToken- Just uid' -> liftIO $ lookupByUserId r uid'+ s <- gets session+ uid <- withTop s getSessionUserId+ case uid of+ Nothing -> loginByRememberToken+ Just uid' -> liftIO $ lookupByUserId r uid' ------------------------------------------------------------------------------ -- | Convenience wrapper around 'rememberUser' that returns a bool result+-- isLoggedIn :: Handler b (AuthManager b) Bool-isLoggedIn = isJust `fmap` currentUser+isLoggedIn = isJust <$> currentUser ------------------------------------------------------------------------------ -- | Create or update a given user -- -- May throw a 'BackendError' if something goes wrong.+-- saveUser :: AuthUser -> Handler b (AuthManager b) AuthUser saveUser u = withBackend $ liftIO . flip save u @@ -152,62 +169,78 @@ -- | Destroy the given user -- -- May throw a 'BackendError' if something goes wrong.+-- destroyUser :: AuthUser -> Handler b (AuthManager b) () destroyUser u = withBackend $ liftIO . flip destroy u ---------------------------------------------------------------------------------- Lower level helper functions-----------------------------------------------------------------------------------+ -----------------------------------+ -- Lower level helper functions --+ ----------------------------------- ------------------------------------------------------------------------------ -- | Mutate an 'AuthUser', marking failed authentication -- -- This will save the user to the backend.+-- markAuthFail :: AuthUser -> Handler b (AuthManager b) AuthUser markAuthFail u = withBackend $ \r -> do- lo <- gets lockout- incFailCtr u >>= checkLockout lo >>= liftIO . save r+ lo <- gets lockout+ incFailCtr u >>= checkLockout lo >>= liftIO . save r+ where- incFailCtr u' = return $ u'- { userFailedLoginCount = userFailedLoginCount u' + 1}- checkLockout lo u' = case lo of- Nothing -> return u'- Just (mx, wait) ->- if userFailedLoginCount u' >= mx- then do- now <- liftIO getCurrentTime- let reopen = addUTCTime wait now- return $ u' { userLockedOutUntil = Just reopen }- else return u'+ --------------------------------------------------------------------------+ incFailCtr u' = return $ u' {+ userFailedLoginCount = userFailedLoginCount u' + 1+ } + --------------------------------------------------------------------------+ checkLockout lo u' =+ case lo of+ Nothing -> return u'+ Just (mx, wait) ->+ if userFailedLoginCount u' >= mx+ then do+ now <- liftIO getCurrentTime+ let reopen = addUTCTime wait now+ return $! u' { userLockedOutUntil = Just reopen }+ else return u' + ------------------------------------------------------------------------------ -- | Mutate an 'AuthUser', marking successful authentication -- -- This will save the user to the backend.+-- markAuthSuccess :: AuthUser -> Handler b (AuthManager b) AuthUser-markAuthSuccess u = withBackend $ \r -> do- incLoginCtr u >>= updateIp >>= updateLoginTS- >>= resetFailCtr >>= liftIO . save r+markAuthSuccess u = withBackend $ \r ->+ incLoginCtr u >>=+ updateIp >>=+ updateLoginTS >>=+ resetFailCtr >>=+ liftIO . save r where+ -------------------------------------------------------------------------- incLoginCtr u' = return $ u' { userLoginCount = userLoginCount u' + 1 }++ -------------------------------------------------------------------------- updateIp u' = do- ip <- rqRemoteAddr `fmap` getRequest- return $ u' { userLastLoginIp = userCurrentLoginIp u'- , userCurrentLoginIp = Just ip }+ ip <- rqRemoteAddr <$> getRequest+ return $ u' { userLastLoginIp = userCurrentLoginIp u'+ , userCurrentLoginIp = Just ip }++ -------------------------------------------------------------------------- updateLoginTS u' = do- now <- liftIO getCurrentTime- return $- u' { userCurrentLoginAt = Just now- , userLastLoginAt = userCurrentLoginAt u' }- resetFailCtr u' = return $- u' { userFailedLoginCount = 0- , userLockedOutUntil = Nothing }+ now <- liftIO getCurrentTime+ return $+ u' { userCurrentLoginAt = Just now+ , userLastLoginAt = userCurrentLoginAt u' } + --------------------------------------------------------------------------+ resetFailCtr u' = return $ u' { userFailedLoginCount = 0+ , userLockedOutUntil = Nothing } + ------------------------------------------------------------------------------ -- | Authenticate and log the user into the current session if successful. --@@ -221,24 +254,27 @@ -- 2. Login the user into the current session -- -- 3. Mark success/failure of the authentication trial on the user record+-- checkPasswordAndLogin :: AuthUser -- ^ An existing user, somehow looked up from db -> Password -- ^ A ClearText password -> Handler b (AuthManager b) (Either AuthFailure AuthUser) checkPasswordAndLogin u pw =- case userLockedOutUntil u of- Just x -> do- now <- liftIO getCurrentTime- if now > x- then auth u- else return . Left $ LockedOut x- Nothing -> auth u+ case userLockedOutUntil u of+ Just x -> do+ now <- liftIO getCurrentTime+ if now > x+ then auth u+ else return . Left $ LockedOut x+ Nothing -> auth u+ where auth user = case authenticatePassword user pw of Just e -> do markAuthFail user return $ Left e+ Nothing -> do forceLogin user modify (\mgr -> mgr { activeUser = Just user })@@ -251,27 +287,27 @@ -- -- Meant to be used if you have other means of being sure that the person is -- who she says she is.-forceLogin- :: AuthUser- -- ^ An existing user, somehow looked up from db- -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+--+forceLogin :: AuthUser -- ^ An existing user, somehow looked up from db+ -> Handler b (AuthManager b) (Either AuthFailure AuthUser) forceLogin u = do- s <- gets session- withSession s $ do- case userId u of- Just x -> do- withTop s (setSessionUserId x)- return $ Right u- Nothing -> return . Left $- AuthError "forceLogin: Can't force the login of a user without userId"+ s <- gets session+ withSession s $ do+ case userId u of+ Just x -> do+ withTop s (setSessionUserId x)+ return $ Right u+ Nothing -> return . Left $+ AuthError $ "forceLogin: Can't force the login of a user "+ ++ "without userId" ---------------------------------------------------------------------------------- Internal, non-exported helpers----------------------------------------------------------------------------------+ ------------------------------------+ -- Internal, non-exported helpers --+ ------------------------------------ +------------------------------------------------------------------------------ getRememberToken :: (Serialize t, MonadSnap m) => Key -> ByteString@@ -280,6 +316,7 @@ getRememberToken sk rc rp = getSecureCookie rc sk rp +------------------------------------------------------------------------------ setRememberToken :: (Serialize t, MonadSnap m) => Key -> ByteString@@ -289,12 +326,14 @@ setRememberToken sk rc rp token = setSecureCookie rc sk rp token +------------------------------------------------------------------------------ forgetRememberToken :: MonadSnap m => ByteString -> m () forgetRememberToken rc = expireCookie rc (Just "/") ------------------------------------------------------------------------------ -- | Set the current user's 'UserId' in the active session+-- setSessionUserId :: UserId -> Handler b SessionManager () setSessionUserId (UserId t) = setInSession "__user_id" t @@ -307,6 +346,7 @@ ------------------------------------------------------------------------------ -- | Get the current user's 'UserId' from the active session+-- getSessionUserId :: Handler b SessionManager (Maybe UserId) getSessionUserId = do uid <- getFromSession "__user_id"@@ -318,53 +358,56 @@ -- -- Returns "Nothing" if check is successful and an "IncorrectPassword" error -- otherwise-authenticatePassword- :: AuthUser -- ^ Looked up from the back-end- -> Password -- ^ Check against this password- -> Maybe AuthFailure+--+authenticatePassword :: AuthUser -- ^ Looked up from the back-end+ -> Password -- ^ Check against this password+ -> Maybe AuthFailure authenticatePassword u pw = auth where- auth = case userPassword u of- Nothing -> Just PasswordMissing- Just upw -> check $ checkPassword pw upw+ auth = case userPassword u of+ Nothing -> Just PasswordMissing+ Just upw -> check $ checkPassword pw upw+ check b = if b then Nothing else Just IncorrectPassword ------------------------------------------------------------------------------ -- | Wrap lookups around request-local cache+-- cacheOrLookup :: Handler b (AuthManager b) (Maybe AuthUser) -- ^ Lookup action to perform if request local cache is empty -> Handler b (AuthManager b) (Maybe AuthUser) cacheOrLookup f = do- au <- gets activeUser- if isJust au- then return au- else do- au' <- f- modify (\mgr -> mgr { activeUser = au' })- return au'+ au <- gets activeUser+ if isJust au+ then return au+ else do+ au' <- f+ modify (\mgr -> mgr { activeUser = au' })+ return au' ------------------------------------------------------------------------------ -- | Register a new user by specifying login and password 'Param' fields+-- registerUser- :: ByteString -- Login field- -> ByteString -- Password field+ :: ByteString -- ^ Login field+ -> ByteString -- ^ Password field -> Handler b (AuthManager b) AuthUser registerUser lf pf = do- l <- fmap decodeUtf8 `fmap` getParam lf- p <- getParam pf- case liftM2 (,) l p of- Nothing -> throw PasswordMissing- Just (lgn, pwd) -> do- createUser lgn pwd+ l <- fmap decodeUtf8 <$> getParam lf+ p <- getParam pf+ case liftM2 (,) l p of+ Nothing -> throw PasswordMissing+ Just (lgn, pwd) -> createUser lgn pwd ------------------------------------------------------------------------------ -- | A 'MonadSnap' handler that processes a login form. -- -- The request paremeters are passed to 'performLogin'+-- loginUser :: ByteString -- ^ Username field@@ -378,26 +421,29 @@ -- ^ Upon success -> Handler b (AuthManager b) () loginUser unf pwdf remf loginFail loginSucc = do- username <- getParam unf- password <- getParam pwdf- remember <- maybe False (=="1") `fmap`- maybe (return Nothing) getParam remf- mMatch <- case password of- Nothing -> return $ Left PasswordMissing- Just password' -> do- case username of- Nothing -> return . Left $ AuthError "Username is missing"- Just username' -> do- loginByUsername username' (ClearText password') remember- either loginFail (const loginSucc) mMatch+ runErrorT go >>= either loginFail (const loginSucc) + where+ go = do+ mbUsername <- getParam unf+ mbPassword <- getParam pwdf+ remember <- (runMaybeT $ do+ field <- MaybeT $ return remf+ value <- MaybeT $ getParam field+ return $ value == "1"+ ) >>= return . fromMaybe False ++ password <- maybe (throwError PasswordMissing) return mbPassword+ username <- maybe (fail "Username is missing") return mbUsername+ lift $ loginByUsername username (ClearText password) remember++ ------------------------------------------------------------------------------ -- | Simple handler to log the user out. Deletes user from session.-logoutUser- :: Handler b (AuthManager b) ()- -- ^ What to do after logging out- -> Handler b (AuthManager b) ()+--+logoutUser :: Handler b (AuthManager b) () -- ^ What to do after logging out+ -> Handler b (AuthManager b) () logoutUser target = logout >> target @@ -407,17 +453,17 @@ -- -- This function has no DB cost - only checks to see if a user_id is present -- in the current session.-requireUser- :: Lens b (Snaplet (AuthManager b))- -- Lens reference to an "AuthManager"- -> Handler b v a- -- ^ Do this if no authenticated user is present.- -> Handler b v a- -- ^ Do this if an authenticated user is present.- -> Handler b v a+--+requireUser :: Lens b (Snaplet (AuthManager b))+ -- ^ Lens reference to an "AuthManager"+ -> Handler b v a+ -- ^ Do this if no authenticated user is present.+ -> Handler b v a+ -- ^ Do this if an authenticated user is present.+ -> Handler b v a requireUser auth bad good = do- loggedIn <- withTop auth isLoggedIn- if loggedIn then good else bad+ loggedIn <- withTop auth isLoggedIn+ if loggedIn then good else bad ------------------------------------------------------------------------------@@ -428,10 +474,11 @@ -- (AuthManager v) a and not a is because anything that uses the -- backend will return an IO something, which you can liftIO, or a -- Handler b (AuthManager v) a if it uses other handler things.-withBackend- :: (forall r. (IAuthBackend r) => r -> Handler b (AuthManager v) a)- -- ^ The function to run with the handler.+--+withBackend ::+ (forall r. (IAuthBackend r) => r -> Handler b (AuthManager v) a)+ -- ^ The function to run with the handler. -> Handler b (AuthManager v) a withBackend f = join $ do- (AuthManager bckend _ _ _ _ _ _ _) <- get- return $ f bckend+ (AuthManager backend_ _ _ _ _ _ _ _ _) <- get+ return $ f backend_
src/Snap/Snaplet/Auth/SpliceHelpers.hs view
@@ -15,6 +15,7 @@ addAuthSplices , ifLoggedIn , ifLoggedOut+ , loggedInUser ) where import Data.Lens.Lazy@@ -24,6 +25,7 @@ import Snap.Snaplet import Snap.Snaplet.Auth.AuthManager import Snap.Snaplet.Auth.Handlers+import Snap.Snaplet.Auth.Types import Snap.Snaplet.Heist @@ -41,6 +43,7 @@ addAuthSplices auth = addSplices [ ("ifLoggedIn", ifLoggedIn auth) , ("ifLoggedOut", ifLoggedOut auth)+ , ("loggedInUser", loggedInUser auth) ] @@ -49,9 +52,7 @@ -- present, this will run the contents of the node. -- -- > <ifLoggedIn> Show this when there is a logged in user </ifLoggedIn>-ifLoggedIn- :: Lens b (Snaplet (AuthManager b))- -> SnapletSplice b v+ifLoggedIn :: Lens b (Snaplet (AuthManager b)) -> SnapletSplice b v ifLoggedIn auth = do chk <- liftHandler $ withTop auth isLoggedIn case chk of@@ -64,12 +65,18 @@ -- not present, this will run the contents of the node. -- -- > <ifLoggedOut> Show this when there is a logged in user </ifLoggedOut>-ifLoggedOut- :: Lens b (Snaplet (AuthManager b))- -> SnapletSplice b v+ifLoggedOut :: Lens b (Snaplet (AuthManager b)) -> SnapletSplice b v ifLoggedOut auth = do chk <- liftHandler $ withTop auth isLoggedIn case chk of False -> liftHeist $ getParamNode >>= return . X.childNodes True -> return [] ++-------------------------------------------------------------------------------+-- | A splice that will simply print the current user's login, if+-- there is one.+loggedInUser :: Lens b (Snaplet (AuthManager b)) -> SnapletSplice b v+loggedInUser auth = do+ u <- liftHandler $ withTop auth currentUser+ liftHeist $ maybe (return []) (textSplice . userLogin) u
src/Snap/Snaplet/Auth/Types.hs view
@@ -1,20 +1,23 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} module Snap.Snaplet.Auth.Types where +------------------------------------------------------------------------------+import Control.Applicative import Control.Monad.CatchIO+import Control.Monad.Trans.Error+import Crypto.PasswordStore import Data.Aeson-import Data.ByteString (ByteString)-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HM-import Data.Hashable (Hashable)+import Data.ByteString (ByteString)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM+import Data.Hashable (Hashable) import Data.Time+import Data.Text (Text) import Data.Typeable-import Data.Text (Text)-import Crypto.PasswordStore ------------------------------------------------------------------------------@@ -22,21 +25,45 @@ -- returned from the db. data Password = ClearText ByteString | Encrypted ByteString- deriving (Read, Show, Ord, Eq)+ deriving (Read, Show, Ord, Eq) --------------------------------------------------------------------------------- Turn a 'ClearText' password into an 'Encrypted' password, ready to be--- stuffed into a database.+-- | Default strength level to pass into makePassword.+defaultStrength :: Int+defaultStrength = 12+++-------------------------------------------------------------------------------+-- | The underlying encryption function, in case you need it for+-- external processing.+encrypt :: ByteString -> IO ByteString+encrypt = flip makePassword defaultStrength+++-------------------------------------------------------------------------------+-- | The underlying verify function, in case you need it for external+-- processing.+verify + :: ByteString -- ^ Cleartext+ -> ByteString -- ^ Encrypted reference+ -> Bool+verify = verifyPassword +++------------------------------------------------------------------------------+-- | Turn a 'ClearText' password into an 'Encrypted' password, ready to+-- be stuffed into a database. encryptPassword :: Password -> IO Password encryptPassword p@(Encrypted {}) = return p-encryptPassword (ClearText p) = do- hashed <- makePassword p 12- return $ Encrypted hashed+encryptPassword (ClearText p) = Encrypted `fmap` encrypt p +------------------------------------------------------------------------------ checkPassword :: Password -> Password -> Bool-checkPassword (ClearText pw) (Encrypted pw') = verifyPassword pw pw'+checkPassword (ClearText pw) (Encrypted pw') = verify pw pw'+checkPassword (ClearText pw) (ClearText pw') = pw == pw'+checkPassword (Encrypted pw) (Encrypted pw') = pw == pw' checkPassword _ _ = error "checkPassword failed. Make sure you pass ClearText passwords" @@ -46,18 +73,17 @@ -- They may provide useful information to the developer, although it is -- generally not advisable to show the user the exact details about why login -- failed.-data AuthFailure =- UserNotFound- | IncorrectPassword- | PasswordMissing- | LockedOut UTCTime- -- ^ Locked out until given time- | AuthError String+data AuthFailure = UserNotFound+ | IncorrectPassword+ | PasswordMissing+ | LockedOut UTCTime -- ^ Locked out until given time+ | AuthError String deriving (Read, Show, Ord, Eq, Typeable) - instance Exception AuthFailure +instance Error AuthFailure where+ strMsg = AuthError ------------------------------------------------------------------------------ -- | Internal representation of a 'User'. By convention, we demand that the@@ -66,59 +92,61 @@ -- Think of this type as a secure, authenticated user. You should normally -- never see this type unless a user has been authenticated. newtype UserId = UserId { unUid :: Text }- deriving (Read,Show,Ord,Eq,FromJSON,ToJSON,Hashable)+ deriving ( Read, Show, Ord, Eq, FromJSON, ToJSON, Hashable ) +------------------------------------------------------------------------------ -- | This will be replaced by a role-based permission system. data Role = Role ByteString- deriving (Read,Show,Ord,Eq)+ deriving (Read, Show, Ord, Eq) ------------------------------------------------------------------------------ -- | Type representing the concept of a User in your application. data AuthUser = AuthUser- { userId :: Maybe UserId- , userLogin :: Text- , userPassword :: Maybe Password- , userActivatedAt :: Maybe UTCTime- , userSuspendedAt :: Maybe UTCTime- , userRememberToken :: Maybe Text- , userLoginCount :: Int- , userFailedLoginCount :: Int- , userLockedOutUntil :: Maybe UTCTime- , userCurrentLoginAt :: Maybe UTCTime- , userLastLoginAt :: Maybe UTCTime- , userCurrentLoginIp :: Maybe ByteString- , userLastLoginIp :: Maybe ByteString- , userCreatedAt :: Maybe UTCTime- , userUpdatedAt :: Maybe UTCTime- , userRoles :: [Role]- , userMeta :: HashMap Text Value- } deriving (Show,Eq)+ { userId :: Maybe UserId+ , userLogin :: Text+ , userPassword :: Maybe Password+ , userActivatedAt :: Maybe UTCTime+ , userSuspendedAt :: Maybe UTCTime+ , userRememberToken :: Maybe Text+ , userLoginCount :: Int+ , userFailedLoginCount :: Int+ , userLockedOutUntil :: Maybe UTCTime+ , userCurrentLoginAt :: Maybe UTCTime+ , userLastLoginAt :: Maybe UTCTime+ , userCurrentLoginIp :: Maybe ByteString+ , userLastLoginIp :: Maybe ByteString+ , userCreatedAt :: Maybe UTCTime+ , userUpdatedAt :: Maybe UTCTime+ , userRoles :: [Role]+ , userMeta :: HashMap Text Value+ }+ deriving (Show,Eq) ------------------------------------------------------------------------------ -- | Default AuthUser that has all empty values. defAuthUser :: AuthUser-defAuthUser = AuthUser {- userId = Nothing- , userLogin = ""- , userPassword = Nothing- , userActivatedAt = Nothing- , userSuspendedAt = Nothing- , userRememberToken = Nothing- , userLoginCount = 0- , userFailedLoginCount = 0- , userLockedOutUntil = Nothing- , userCurrentLoginAt = Nothing- , userLastLoginAt = Nothing- , userCurrentLoginIp = Nothing- , userLastLoginIp = Nothing- , userCreatedAt = Nothing- , userUpdatedAt = Nothing- , userRoles = []- , userMeta = HM.empty-}+defAuthUser = AuthUser+ { userId = Nothing+ , userLogin = ""+ , userPassword = Nothing+ , userActivatedAt = Nothing+ , userSuspendedAt = Nothing+ , userRememberToken = Nothing+ , userLoginCount = 0+ , userFailedLoginCount = 0+ , userLockedOutUntil = Nothing+ , userCurrentLoginAt = Nothing+ , userLastLoginAt = Nothing+ , userCurrentLoginIp = Nothing+ , userLastLoginIp = Nothing+ , userCreatedAt = Nothing+ , userUpdatedAt = Nothing+ , userRoles = []+ , userMeta = HM.empty+ } ------------------------------------------------------------------------------@@ -126,24 +154,28 @@ -- clear-text; it will be encrypted into a 'Encrypted'. setPassword :: AuthUser -> ByteString -> IO AuthUser setPassword au pass = do- pw <- Encrypted `fmap` (makePassword pass 12)- return $ au { userPassword = Just pw }+ pw <- Encrypted <$> makePassword pass defaultStrength+ return $! au { userPassword = Just pw } ------------------------------------------------------------------------------ -- | Authetication settings defined at initialization time data AuthSettings = AuthSettings {- asMinPasswdLen :: Int- -- ^ Currently not used/checked+ asMinPasswdLen :: Int+ -- ^ Currently not used/checked+ , asRememberCookieName :: ByteString- -- ^ Name of the desired remember cookie- , asRememberPeriod :: Maybe Int- -- ^ How long to remember when the option is used in rest of the API.- -- 'Nothing' means remember until end of session.- , asLockout :: Maybe (Int, NominalDiffTime)- -- ^ Lockout strategy: ([MaxAttempts], [LockoutDuration])- , asSiteKey :: FilePath- -- ^ Location of app's encryption key+ -- ^ Name of the desired remember cookie++ , asRememberPeriod :: Maybe Int+ -- ^ How long to remember when the option is used in rest of the API.+ -- 'Nothing' means remember until end of session.++ , asLockout :: Maybe (Int, NominalDiffTime)+ -- ^ Lockout strategy: ([MaxAttempts], [LockoutDuration])++ , asSiteKey :: FilePath+ -- ^ Location of app's encryption key } @@ -157,19 +189,89 @@ -- > asSiteKey = "site_key.txt" defAuthSettings :: AuthSettings defAuthSettings = AuthSettings {- asMinPasswdLen = 8+ asMinPasswdLen = 8 , asRememberCookieName = "_remember"- , asRememberPeriod = Just (2*7*24*60*60)- , asLockout = Nothing- , asSiteKey = "site_key.txt"+ , asRememberPeriod = Just (2*7*24*60*60)+ , asLockout = Nothing+ , asSiteKey = "site_key.txt" } -data BackendError =- DuplicateLogin- | BackendError String+------------------------------------------------------------------------------+data BackendError = DuplicateLogin+ | BackendError String deriving (Eq,Show,Read,Typeable) - instance Exception BackendError ++ --------------------+ -- JSON Instances --+ --------------------++------------------------------------------------------------------------------+instance ToJSON AuthUser where+ toJSON u = object+ [ "uid" .= userId u+ , "login" .= userLogin u+ , "pw" .= userPassword u+ , "activated_at" .= userActivatedAt u+ , "suspended_at" .= userSuspendedAt u+ , "remember_token" .= userRememberToken u+ , "login_count" .= userLoginCount u+ , "failed_login_count" .= userFailedLoginCount u+ , "locked_until" .= userLockedOutUntil u+ , "current_login_at" .= userCurrentLoginAt u+ , "last_login_at" .= userLastLoginAt u+ , "current_ip" .= userCurrentLoginIp u+ , "last_ip" .= userLastLoginIp u+ , "created_at" .= userCreatedAt u+ , "updated_at" .= userUpdatedAt u+ , "roles" .= userRoles u+ , "meta" .= userMeta u+ ]+++------------------------------------------------------------------------------+instance FromJSON AuthUser where+ parseJSON (Object v) = AuthUser+ <$> v .: "uid"+ <*> v .: "login"+ <*> v .: "pw"+ <*> v .: "activated_at"+ <*> v .: "suspended_at"+ <*> v .: "remember_token"+ <*> v .: "login_count"+ <*> v .: "failed_login_count"+ <*> v .: "locked_until"+ <*> v .: "current_login_at"+ <*> v .: "last_login_at"+ <*> v .: "current_ip"+ <*> v .: "last_ip"+ <*> v .: "created_at"+ <*> v .: "updated_at"+ <*> v .:? "roles" .!= []+ <*> v .: "meta"+ parseJSON _ = error "Unexpected JSON input"+++------------------------------------------------------------------------------+instance ToJSON Password where+ toJSON (Encrypted x) = toJSON x+ toJSON (ClearText _) =+ error "ClearText passwords can't be serialized into JSON"+++------------------------------------------------------------------------------+instance FromJSON Password where+ parseJSON = fmap Encrypted . parseJSON+++------------------------------------------------------------------------------+instance ToJSON Role where+ toJSON (Role x) = toJSON x+++------------------------------------------------------------------------------+instance FromJSON Role where+ parseJSON = fmap Role . parseJSON
src/Snap/Snaplet/Heist.hs view
@@ -1,9 +1,7 @@-{-|--The Heist snaplet makes it easy to add Heist to your application and use it in-other snaplets.---}+------------------------------------------------------------------------------+-- | The Heist snaplet makes it easy to add Heist to your application and use+-- it in other snaplets.+-- module Snap.Snaplet.Heist (@@ -44,17 +42,20 @@ , clearHeistCache ) where +------------------------------------------------------------------------------ import Prelude hiding (id, (.)) import Data.ByteString (ByteString) import Data.Lens.Lazy import Data.Text (Text) import Text.Templating.Heist-+------------------------------------------------------------------------------ import Snap.Snaplet- import qualified Snap.Snaplet.HeistNoClass as Unclassed-import Snap.Snaplet.HeistNoClass (Heist, heistInit- ,heistInit', clearHeistCache)+import Snap.Snaplet.HeistNoClass ( Heist+ , heistInit+ , heistInit'+ , clearHeistCache+ ) ------------------------------------------------------------------------------@@ -84,19 +85,23 @@ --------------------------------------------------------------------------------- | Adds templates to the Heist TemplateState. Other snaplets should use+-- | Adds templates to the Heist HeistState. Other snaplets should use -- this function to add their own templates. The templates are automatically -- read from the templates directory in the current snaplet's filesystem root. addTemplates :: HasHeist b => ByteString- -- ^ Path to templates (also the url prefix for their routes)+ -- ^ The url prefix for the template routes -> Initializer b v () addTemplates pfx = withTop' heistLens (Unclassed.addTemplates pfx) --------------------------------------------------------------------------------- | Adds templates to the Heist TemplateState, and lets you specify where--- they are found in the filesystem.+-- | Adds templates to the Heist HeistState, and lets you specify where+-- they are found in the filesystem. Note that the path to the template+-- directory is an absolute path. This allows you more flexibility in where+-- your templates are located, but means that you have to explicitly call+-- getSnapletFilePath if you want your snaplet to use templates within its+-- normal directory structure. addTemplatesAt :: HasHeist b => ByteString -- ^ URL prefix for template routes@@ -116,21 +121,21 @@ --------------------------------------------------------------------------------- | More general function allowing arbitrary TemplateState modification.+-- | More general function allowing arbitrary HeistState modification. -- Without this function you wouldn't be able to bind more complicated splices -- like the cache tag. modifyHeistTS :: (HasHeist b)- => (TemplateState (Handler b b) -> TemplateState (Handler b b))- -- ^ TemplateState modifying function+ => (HeistState (Handler b b) -> HeistState (Handler b b))+ -- ^ HeistState modifying function -> Initializer b v () modifyHeistTS = Unclassed.modifyHeistTS' heistLens --------------------------------------------------------------------------------- | Runs a function on with the Heist snaplet's 'TemplateState'.+-- | Runs a function on with the Heist snaplet's 'HeistState'. withHeistTS :: (HasHeist b)- => (TemplateState (Handler b b) -> a)- -- ^ TemplateState function to run+ => (HeistState (Handler b b) -> a)+ -- ^ HeistState function to run -> Handler b v a withHeistTS = Unclassed.withHeistTS' heistLens @@ -193,7 +198,7 @@ ------------------------------------------------------------------------------ -- | Runs an action with additional splices bound into the Heist--- 'TemplateState'.+-- 'HeistState'. withSplices :: HasHeist b => [(Text, Unclassed.SnapletSplice b v)] -- ^ Splices to bind@@ -204,14 +209,14 @@ --------------------------------------------------------------------------------- | Runs a handler with a modified 'TemplateState'. You might want to use+-- | Runs a handler with a modified 'HeistState'. You might want to use -- this if you had a set of splices which were customised for a specific -- action. To do that you would do: -- -- > heistLocal (bindSplices mySplices) handlerThatNeedsSplices heistLocal :: HasHeist b- => (TemplateState (Handler b b) -> TemplateState (Handler b b))- -- ^ TemplateState modifying function+ => (HeistState (Handler b b) -> HeistState (Handler b b))+ -- ^ HeistState modifying function -> Handler b v a -- ^ Handler to run -> Handler b v a@@ -220,7 +225,7 @@ -- $spliceSection -- As can be seen in the type signature of heistLocal, the internal--- TemplateState used by the heist snaplet is parameterized by (Handler b b).+-- HeistState used by the heist snaplet is parameterized by (Handler b b). -- The reasons for this are beyond the scope of this discussion, but the -- result is that 'lift' inside a splice only works with @Handler b b@ -- actions. When you're writing your own snaplets you obviously would rather
src/Snap/Snaplet/HeistNoClass.hs view
@@ -68,14 +68,15 @@ -- include this in your application state and use 'heistInit' to initialize -- it. The type parameter b will typically be the base state type for your -- application.+-- data Heist b = Heist- { _heistTS :: TemplateState (Handler b b)+ { _heistTS :: HeistState (Handler b b) , _heistCTS :: CacheTagState } -------------------------------------------------------------------------------changeTS :: (TemplateState (Handler a a) -> TemplateState (Handler a a))+changeTS :: (HeistState (Handler a a) -> HeistState (Handler a a)) -> Heist a -> Heist a changeTS f (Heist ts cts) = Heist (f ts) cts@@ -85,24 +86,26 @@ -- | Clears data stored by the cache tag. The cache tag automatically reloads -- its data when the specified TTL expires, but sometimes you may want to -- trigger a manual reload. This function lets you do that.+-- clearHeistCache :: Heist b -> IO () clearHeistCache = clearCacheTagState . _heistCTS ---------------------------------------------------------------------------------- SnapletSplice functions--------------------------------------------------------------------------------+ -----------------------------+ -- SnapletSplice functions --+ ----------------------------- ------------------------------------------------------------------------------ -- | This instance is here because we don't want the heist package to depend -- on anything from snap packages.+-- instance MonadSnap m => MonadSnap (HeistT m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ -- | Monad for working with Heist's API from within a snaplet.+-- newtype SnapletHeist b v a = SnapletHeist (ReaderT (Lens (Snaplet b) (Snaplet v)) (HeistT (Handler b b)) a) deriving ( Monad@@ -119,17 +122,20 @@ ------------------------------------------------------------------------------ -- | Type alias for convenience.+-- type SnapletSplice b v = SnapletHeist b v Template ------------------------------------------------------------------------------ -- | Runs the SnapletSplice.+-- runSnapletSplice :: (Lens (Snaplet b) (Snaplet v)) -> SnapletHeist b v a -> HeistT (Handler b b) a runSnapletSplice l (SnapletHeist m) = runReaderT m l +------------------------------------------------------------------------------ withSS :: (Lens (Snaplet b) (Snaplet v) -> Lens (Snaplet b) (Snaplet v')) -> SnapletHeist b v' a -> SnapletHeist b v a@@ -139,12 +145,14 @@ ------------------------------------------------------------------------------ -- | Lifts a HeistT action into SnapletHeist. Use this with all the functions -- from the Heist API.+-- liftHeist :: HeistT (Handler b b) a -> SnapletHeist b v a liftHeist = SnapletHeist . lift ------------------------------------------------------------------------------ -- | Common idiom for the combination of liftHandler and withTop.+-- liftWith :: (Lens (Snaplet b) (Snaplet v')) -> Handler b v' a -> SnapletHeist b v a@@ -153,6 +161,7 @@ ------------------------------------------------------------------------------ -- | Lifts a Handler into SnapletHeist.+-- liftHandler :: Handler b v a -> SnapletHeist b v a liftHandler m = do l <- ask@@ -161,10 +170,12 @@ ------------------------------------------------------------------------------ -- | Lifts a (Handler b b) into SnapletHeist.+-- liftAppHandler :: Handler b b a -> SnapletHeist b v a liftAppHandler = liftHeist . lift +------------------------------------------------------------------------------ instance MonadState v (SnapletHeist b v) where get = do l <- ask@@ -178,6 +189,7 @@ ------------------------------------------------------------------------------ -- | MonadSnaplet instance gives us access to the snaplet infrastructure.+-- instance MonadSnaplet SnapletHeist where getLens = ask with' l = withSS (l .)@@ -190,121 +202,144 @@ ------------------------------------------------------------------------------ -- | SnapletSplices version of bindSplices.+-- bindSnapletSplices :: (Lens (Snaplet b) (Snaplet v)) -> [(Text, SnapletSplice b v)]- -> TemplateState (Handler b b)- -> TemplateState (Handler b b)+ -> HeistState (Handler b b)+ -> HeistState (Handler b b) bindSnapletSplices l splices = bindSplices $ map (second $ runSnapletSplice l) splices ---------------------------------------------------------------------------------- Initializer functions--------------------------------------------------------------------------------+ ---------------------------+ -- Initializer functions --+ --------------------------- --------------------------------------------------------------------------------- | The 'Initializer' for 'Heist'. This function is a convenience wrapper--- around `heistInit'` that uses the default `emptyTemplateState` from Heist--- and sets up routes for all the templates.-heistInit :: FilePath- -- ^ Path to templates+-- | The 'Initializer' for 'Heist'. This function is a convenience wrapper+-- around `heistInit'` that uses the default `mempty` HeistState and sets up+-- routes for all the templates.+--+heistInit :: FilePath -- ^ Path to templates -> SnapletInit b (Heist b) heistInit templateDir = do makeSnaplet "heist" "" Nothing $ do- hs <- heistInitWorker templateDir emptyTemplateState+ hs <- heistInitWorker templateDir defaultHeistState addRoutes [ ("", heistServe) ] return hs ------------------------------------------------------------------------------ -- | A lower level 'Initializer' for 'Heist'. This initializer requires you--- to specify the initial TemplateState. It also does not add any routes for+-- to specify the initial HeistState. It also does not add any routes for -- templates, allowing you complete control over which templates get routed.+-- heistInit' :: FilePath -- ^ Path to templates- -> TemplateState (Handler b b)- -- ^ Initial TemplateState+ -> HeistState (Handler b b)+ -- ^ Initial HeistState -> SnapletInit b (Heist b)-heistInit' templateDir initialTemplateState =+heistInit' templateDir initialHeistState = makeSnaplet "heist" "" Nothing $- heistInitWorker templateDir initialTemplateState+ heistInitWorker templateDir initialHeistState ------------------------------------------------------------------------------ -- | Internal worker function used by variantsof heistInit. This is necessary -- because of the divide between SnapletInit and Initializer.+-- heistInitWorker :: FilePath- -> TemplateState (Handler b b)+ -> HeistState (Handler b b) -> Initializer b v (Heist b)-heistInitWorker templateDir initialTemplateState = do+heistInitWorker templateDir initialHeistState = do (cacheFunc, cts) <- liftIO mkCacheTag- let origTs = cacheFunc initialTemplateState- ts <- liftIO $ loadTemplates templateDir origTs >>=+ let origTs = cacheFunc initialHeistState+ snapletPath <- getSnapletFilePath+ let tDir = snapletPath </> templateDir+ ts <- liftIO $ loadTemplates tDir origTs >>= either error return printInfo $ T.pack $ unwords [ "...loaded" , (show $ length $ templateNames ts)- , "templates"+ , "templates from"+ , tDir ] return $ Heist ts cts +------------------------------------------------------------------------------+-- | Adds templates to the Heist HeistState. Other snaplets should use+-- this function to add their own templates. The templates are automatically+-- read from the templates directory in the current snaplet's filesystem root. addTemplates :: ByteString- -- ^ Path to templates (also the url prefix for their routes)+ -- ^ The url prefix for the template routes -> Initializer b (Heist b) () addTemplates urlPrefix = do snapletPath <- getSnapletFilePath addTemplatesAt urlPrefix (snapletPath </> "templates") +------------------------------------------------------------------------------+-- | Adds templates to the Heist HeistState, and lets you specify where+-- they are found in the filesystem. Note that the path to the template+-- directory is an absolute path. This allows you more flexibility in where+-- your templates are located, but means that you have to explicitly call+-- getSnapletFilePath if you want your snaplet to use templates within its+-- normal directory structure. addTemplatesAt :: ByteString -- ^ URL prefix for template routes -> FilePath -- ^ Path to templates -> Initializer b (Heist b) () addTemplatesAt urlPrefix templateDir = do- ts <- liftIO $ loadTemplates templateDir emptyTemplateState+ ts <- liftIO $ loadTemplates templateDir mempty >>= either error return+ rootUrl <- getSnapletRootURL+ let fullPrefix = U.toString rootUrl </> U.toString urlPrefix printInfo $ T.pack $ unwords [ "...adding" , (show $ length $ templateNames ts) , "templates from" , templateDir , "with route prefix"- , (U.toString urlPrefix) ++ "/"+ , fullPrefix ++ "/" ] addPostInitHook $ return . changeTS- (`mappend` addTemplatePathPrefix urlPrefix ts)+ (`mappend` addTemplatePathPrefix (U.fromString fullPrefix) ts) +------------------------------------------------------------------------------ modifyHeistTS' :: (Lens (Snaplet b) (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+ -> (HeistState (Handler b b) -> HeistState (Handler b b)) -> Initializer b v () modifyHeistTS' heist f = do _lens <- getLens withTop' heist $ addPostInitHook $ return . changeTS f +------------------------------------------------------------------------------ modifyHeistTS :: (Lens b (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+ -> (HeistState (Handler b b) -> HeistState (Handler b b)) -> Initializer b v () modifyHeistTS heist f = modifyHeistTS' (subSnaplet heist) f +------------------------------------------------------------------------------ withHeistTS' :: (Lens (Snaplet b) (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> a)+ -> (HeistState (Handler b b) -> a) -> Handler b v a withHeistTS' heist f = withTop' heist $ gets (f . _heistTS) +------------------------------------------------------------------------------ withHeistTS :: (Lens b (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> a)+ -> (HeistState (Handler b b) -> a) -> Handler b v a withHeistTS heist f = withHeistTS' (subSnaplet heist) f +------------------------------------------------------------------------------ addSplices' :: (Lens (Snaplet b) (Snaplet (Heist b))) -> [(Text, SnapletSplice b v)] -> Initializer b v ()@@ -314,16 +349,16 @@ return . changeTS (bindSnapletSplices _lens splices) +------------------------------------------------------------------------------ addSplices :: (Lens b (Snaplet (Heist b))) -> [(Text, SnapletSplice b v)] -> Initializer b v () addSplices heist splices = addSplices' (subSnaplet heist) splices ---------------------------------------------------------------------------------- Handler functions--------------------------------------------------------------------------------+ -----------------------+ -- Handler functions --+ ----------------------- ------------------------------------------------------------------------------ -- | Internal helper function for rendering.@@ -339,12 +374,14 @@ writeBuilder b +------------------------------------------------------------------------------ render :: ByteString -- ^ Name of the template -> Handler b (Heist b) () render t = renderHelper Nothing t +------------------------------------------------------------------------------ renderAs :: ByteString -- ^ Content type -> ByteString@@ -353,19 +390,22 @@ renderAs ct t = renderHelper (Just ct) t +------------------------------------------------------------------------------ heistServe :: Handler b (Heist b) () heistServe = ifTop (render "index") <|> (render . B.pack =<< getSafePath) +------------------------------------------------------------------------------ heistServeSingle :: ByteString -> Handler b (Heist b) () heistServeSingle t = render t <|> error ("Template " ++ show t ++ " not found.") +------------------------------------------------------------------------------ heistLocal' :: (Lens (Snaplet b) (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+ -> (HeistState (Handler b b) -> HeistState (Handler b b)) -> Handler b v a -> Handler b v a heistLocal' heist f m = do@@ -376,13 +416,15 @@ return res +------------------------------------------------------------------------------ heistLocal :: (Lens b (Snaplet (Heist b)))- -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+ -> (HeistState (Handler b b) -> HeistState (Handler b b)) -> Handler b v a -> Handler b v a heistLocal heist f m = heistLocal' (subSnaplet heist) f m +------------------------------------------------------------------------------ withSplices' :: (Lens (Snaplet b) (Snaplet (Heist b))) -> [(Text, SnapletSplice b v)] -> Handler b v a@@ -392,6 +434,7 @@ heistLocal' heist (bindSnapletSplices _lens splices) m +------------------------------------------------------------------------------ withSplices :: (Lens b (Snaplet (Heist b))) -> [(Text, SnapletSplice b v)] -> Handler b v a@@ -399,6 +442,7 @@ withSplices heist splices m = withSplices' (subSnaplet heist) splices m +------------------------------------------------------------------------------ renderWithSplices' :: (Lens (Snaplet b) (Snaplet (Heist b))) -> ByteString -> [(Text, SnapletSplice b v)]@@ -407,6 +451,7 @@ withSplices' heist splices $ withTop' heist $ render t +------------------------------------------------------------------------------ renderWithSplices :: (Lens b (Snaplet (Heist b))) -> ByteString -> [(Text, SnapletSplice b v)]
src/Snap/Snaplet/Internal/Initializer.hs view
@@ -146,11 +146,15 @@ printInfo "...setting up filesystem" liftIO $ createDirectoryIfMissing True targetDir srcDir <- liftIO getSnapletDataDir- (_ :/ dTree) <- liftIO $ readDirectoryWith B.readFile srcDir- let (topDir,snapletId) = splitFileName targetDir- _ <- liftIO $ writeDirectoryWith B.writeFile- (topDir :/ dTree { name = snapletId })+ liftIO $ readDirectoryWith (doCopy srcDir targetDir) srcDir return ()+ where+ doCopy srcRoot targetRoot filename = do+ createDirectoryIfMissing True directory+ copyFile filename to+ where+ to = targetRoot </> makeRelative srcRoot filename+ directory = dropFileName to ------------------------------------------------------------------------------@@ -286,7 +290,7 @@ -> Initializer b v (Snaplet v1) embedSnaplet rte l (SnapletInit snaplet) = bracketInit $ do curLens <- getLens- setupSnapletCall rte+ setupSnapletCall "" chroot rte (subSnaplet l . curLens) snaplet @@ -381,7 +385,11 @@ -- | Attaches an unload handler to the snaplet. The unload handler will be -- called when the server shuts down, or is reloaded. onUnload :: IO () -> Initializer b v ()-onUnload m = iModify (\v -> modL cleanup (m>>) v)+onUnload m = do+ cleanupRef <- iGets _cleanup+ liftIO $ atomicModifyIORef cleanupRef f+ where+ f curCleanup = (curCleanup >> m, ()) ------------------------------------------------------------------------------@@ -403,19 +411,18 @@ ------------------------------------------------------------------------------ -- | Builds an IO reload action for storage in the SnapletState.-mkReloader :: MVar (Snaplet b)+mkReloader :: FilePath+ -> MVar (Snaplet b) -> Initializer b b (Snaplet b)- -> IO (Either String String)-mkReloader mvar i = do- !res <- try $ runInitializer mvar i- either bad good res+ -> IO (Either Text Text)+mkReloader cwd mvar i = do+ !res <- runInitializer' mvar i cwd+ either (return . Left) good res where- bad e = do- return $ Left $ show (e :: SomeException) good (b,is) = do _ <- swapMVar mvar b msgs <- readIORef $ _initMessages is- return $ Right $ T.unpack msgs+ return $ Right msgs ------------------------------------------------------------------------------@@ -430,35 +437,65 @@ --------------------------------------------------------------------------------- |+-- | Internal function for running Initializers. If any exceptions were+-- thrown by the initializer, this function catches them, runs any cleanup+-- actions that had been registered, and returns an expanded error message+-- containing the exception details as well as all messages generated by the+-- initializer before the exception was thrown. runInitializer :: MVar (Snaplet b) -> Initializer b b (Snaplet b)- -> IO (Snaplet b, InitializerState b)-runInitializer mvar b@(Initializer i) = do+ -> IO (Either Text (Snaplet b, InitializerState b))+runInitializer mvar b = getCurrentDirectory >>= runInitializer' mvar b+++------------------------------------------------------------------------------+runInitializer' :: MVar (Snaplet b)+ -> Initializer b b (Snaplet b)+ -> FilePath+ -> IO (Either Text (Snaplet b, InitializerState b))+runInitializer' mvar b@(Initializer i) cwd = do userConfig <- load [Optional "snaplet.cfg"] let builtinHandlers = [("/admin/reload", reloadSite)]- let cfg = SnapletConfig [] "" Nothing "" userConfig [] Nothing- (mkReloader mvar b)+ let cfg = SnapletConfig [] cwd Nothing "" userConfig [] Nothing+ (mkReloader cwd mvar b) logRef <- newIORef ""- ((res, s), (Hook hook)) <- runWriterT $ LT.runLensT i id $- InitializerState True (return ()) builtinHandlers id cfg logRef- res' <- hook res- return (res', s)+ cleanupRef <- newIORef (return ()) + let body = do+ ((res, s), (Hook hook)) <- runWriterT $ LT.runLensT i id $+ InitializerState True cleanupRef builtinHandlers id cfg logRef+ res' <- hook res+ return $ Right (res', s) + handler e = do+ join $ readIORef cleanupRef+ logMessages <- readIORef logRef++ return $ Left $ T.unlines+ [ "Initializer threw an exception..."+ , T.pack $ show (e :: SomeException)+ , ""+ , "...but before it died it generated the following output:"+ , logMessages+ ]++ catch body handler++ ------------------------------------------------------------------------------ -- | Given a Snaplet initializer, produce the set of messages generated during -- initialization, a snap handler, and a cleanup action. runSnaplet :: SnapletInit b b -> IO (Text, Snap (), IO ()) runSnaplet (SnapletInit b) = do snapletMVar <- newEmptyMVar- (siteSnaplet, is) <- runInitializer snapletMVar b- putMVar snapletMVar siteSnaplet-- msgs <- liftIO $ readIORef $ _initMessages is- let handler = runBase (_hFilter is $ route $ _handlers is) snapletMVar-- return (msgs, handler, _cleanup is)+ eRes <- runInitializer snapletMVar b+ let go (siteSnaplet,is) = do+ putMVar snapletMVar siteSnaplet+ msgs <- liftIO $ readIORef $ _initMessages is+ let handler = runBase (_hFilter is $ route $ _handlers is) snapletMVar+ cleanupAction <- readIORef $ _cleanup is+ return (msgs, handler, cleanupAction)+ either (error . ('\n':) . T.unpack) go eRes ------------------------------------------------------------------------------@@ -486,6 +523,7 @@ config <- commandLineConfig startConfig (conf, site) <- combineConfig config handler+ createDirectoryIfMissing False "log" let serve = simpleHttpServe conf liftIO $ hPutStrLn stderr $ T.unpack msgs
src/Snap/Snaplet/Internal/Types.hs view
@@ -43,7 +43,7 @@ , _scRoutePattern :: Maybe ByteString -- ^ Holds the actual route pattern passed to addRoutes for the current -- handler. Nothing during initialization and before route dispatech.- , _reloader :: IO (Either String String) -- might change+ , _reloader :: IO (Either Text Text) -- might change } @@ -281,11 +281,13 @@ where bad msg = do writeText $ "Error reloading site!\n\n"- writeText $ T.pack msg+ writeText msg good msg = do- writeText $ T.pack msg+ writeText msg writeText $ "Site successfully reloaded.\n" failIfNotLocal m = do+ -- TODO/FIXME: grave error here; this code needs to check if we're+ -- behind a proxy. rip <- liftM rqRemoteAddr getRequest if not $ elem rip [ "127.0.0.1" , "localhost"@@ -295,11 +297,35 @@ ------------------------------------------------------------------------------+-- | This function brackets a Handler action in resource acquisition and+-- release. Like 'bracketSnap', this is provided because MonadCatchIO's+-- 'bracket' function doesn't work properly in the case of a short-circuit+-- return from the action being bracketed.+--+-- In order to prevent confusion regarding the effects of the+-- aquisition and release actions on the Handler state, this function+-- doesn't accept Handler actions for the acquire or release actions.+--+-- This function will run the release action in all cases where the+-- acquire action succeeded. This includes the following behaviors+-- from the bracketed Snap action.+--+-- 1. Normal completion+--+-- 2. Short-circuit completion, either from calling 'fail' or 'finishWith'+--+-- 3. An exception being thrown.+bracketHandler :: IO a -> (a -> IO x) -> (a -> Handler b v c) -> Handler b v c+bracketHandler begin end f = Handler . L.Lensed $ \l v b -> do+ bracketSnap begin end $ \a -> case f a of Handler m -> L.unlensed m l v b+++------------------------------------------------------------------------------ -- | Information about a partially constructed initializer. Used to -- automatically aggregate handlers and cleanup actions. data InitializerState b = InitializerState { _isTopLevel :: Bool- , _cleanup :: IO ()+ , _cleanup :: IORef (IO ()) , _handlers :: [(ByteString, Handler b b ())] -- ^ Handler routes built up and passed to route. , _hFilter :: Handler b b () -> Handler b b ()
src/Snap/Snaplet/Session.hs view
@@ -1,7 +1,5 @@ module Snap.Snaplet.Session--(- SessionManager+ ( SessionManager , withSession , commitSession , setInSession@@ -11,97 +9,116 @@ , sessionToList , resetSession , touchSession--) where+ ) where +------------------------------------------------------------------------------ import Control.Monad.State import Data.Lens.Lazy import Data.Text (Text)--import Snap.Snaplet import Snap.Core-+------------------------------------------------------------------------------+import Snap.Snaplet import Snap.Snaplet.Session.SessionManager ( SessionManager(..), ISessionManager(..) ) import qualified Snap.Snaplet.Session.SessionManager as SM-+------------------------------------------------------------------------------ +------------------------------------------------------------------------------ -- | Wrap around a handler, committing any changes in the session at the end-withSession :: (Lens b (Snaplet SessionManager))+--+withSession :: Lens b (Snaplet SessionManager) -> Handler b v a -> Handler b v a withSession l h = do- a <- h- withTop l commitSession- return a+ a <- h+ withTop l commitSession+ return a +------------------------------------------------------------------------------ -- | Commit changes to session within the current request cycle+-- commitSession :: Handler b SessionManager () commitSession = do- SessionManager b <- loadSession- liftSnap $ commit b+ SessionManager b <- loadSession+ liftSnap $ commit b +------------------------------------------------------------------------------ -- | Set a key-value pair in the current session+-- setInSession :: Text -> Text -> Handler b SessionManager () setInSession k v = do- SessionManager r <- loadSession- let r' = SM.insert k v r- put $ SessionManager r'+ SessionManager r <- loadSession+ let r' = SM.insert k v r+ put $ SessionManager r' +------------------------------------------------------------------------------ -- | Get a key from the current session+-- getFromSession :: Text -> Handler b SessionManager (Maybe Text) getFromSession k = do- SessionManager r <- loadSession- return $ SM.lookup k r+ SessionManager r <- loadSession+ return $ SM.lookup k r +------------------------------------------------------------------------------ -- | Remove a key from the current session+-- deleteFromSession :: Text -> Handler b SessionManager () deleteFromSession k = do- SessionManager r <- loadSession- let r' = SM.delete k r- put $ SessionManager r'+ SessionManager r <- loadSession+ let r' = SM.delete k r+ put $ SessionManager r' +------------------------------------------------------------------------------ -- | Returns a CSRF Token unique to the current session+-- csrfToken :: Handler b SessionManager Text csrfToken = do- mgr@(SessionManager r) <- loadSession- put mgr- return $ SM.csrf r+ mgr@(SessionManager r) <- loadSession+ put mgr+ return $ SM.csrf r +------------------------------------------------------------------------------ -- | Return session contents as an association list+-- sessionToList :: Handler b SessionManager [(Text, Text)] sessionToList = do- SessionManager r <- loadSession- return $ SM.toList r+ SessionManager r <- loadSession+ return $ SM.toList r +------------------------------------------------------------------------------ -- | Deletes the session cookie, effectively resetting the session+-- resetSession :: Handler b SessionManager () resetSession = do- SessionManager r <- loadSession- r' <- liftSnap $ SM.reset r- put $ SessionManager r'+ SessionManager r <- loadSession+ r' <- liftSnap $ SM.reset r+ put $ SessionManager r' +------------------------------------------------------------------------------ -- | Touch the session so the timeout gets refreshed+-- touchSession :: Handler b SessionManager () touchSession = do- SessionManager r <- loadSession- let r' = SM.touch r- put $ SessionManager r'+ SessionManager r <- loadSession+ let r' = SM.touch r+ put $ SessionManager r' +------------------------------------------------------------------------------ -- | Load the session into the manager+-- loadSession :: Handler b SessionManager SessionManager loadSession = do- SessionManager r <- get- r' <- liftSnap $ load r- return $ SessionManager r'+ SessionManager r <- get+ r' <- liftSnap $ load r+ return $ SessionManager r'
src/Snap/Snaplet/Session/Backends/CookieSession.hs view
@@ -1,11 +1,14 @@+------------------------------------------------------------------------------ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Snap.Snaplet.Session.Backends.CookieSession--( initCookieSessionManager ) where+ ( initCookieSessionManager+ ) where +------------------------------------------------------------------------------+import Control.Applicative import Control.Monad.Reader import Data.ByteString (ByteString) import Data.Generics@@ -15,146 +18,172 @@ import Data.Serialize (Serialize) import qualified Data.Serialize as S import Data.Text (Text)-import Web.ClientSession- import Snap.Core (Snap)+import Web.ClientSession+------------------------------------------------------------------------------ import Snap.Snaplet-import Snap.Snaplet.Session.Common (mkCSRFToken)+import Snap.Snaplet.Session.Common import Snap.Snaplet.Session.SessionManager import Snap.Snaplet.Session.SecureCookie +------------------------------------------------------------------------------ -- | Session data are kept in a 'HashMap' for this backend+-- type Session = HashMap Text Text +------------------------------------------------------------------------------ -- | This is what the 'Payload' will be for the CookieSession backend+-- data CookieSession = CookieSession- { csCSRFToken :: Text- , csSession :: Session-} deriving (Eq, Show)+ { csCSRFToken :: Text+ , csSession :: Session+ }+ deriving (Eq, Show) +------------------------------------------------------------------------------ instance Serialize CookieSession where- put (CookieSession a b) = S.put (a,b)- get = (\(a,b) -> CookieSession a b) `fmap` S.get+ put (CookieSession a b) = S.put (a,b)+ get = uncurry CookieSession <$> S.get -instance (Serialize k, Serialize v, Hashable k, Eq k) =>- Serialize (HashMap k v) where- put = S.put . HM.toList- get = HM.fromList `fmap` S.get+instance (Serialize k, Serialize v, Hashable k,+ Eq k) => Serialize (HashMap k v) where+ put = S.put . HM.toList+ get = HM.fromList <$> S.get -mkCookieSession :: IO CookieSession-mkCookieSession = do- t <- liftIO $ mkCSRFToken- return $ CookieSession t HM.empty+------------------------------------------------------------------------------+mkCookieSession :: RNG -> IO CookieSession+mkCookieSession rng = do+ t <- liftIO $ mkCSRFToken rng+ return $ CookieSession t HM.empty +------------------------------------------------------------------------------ -- | The manager data type to be stuffed into 'SessionManager'+-- data CookieSessionManager = CookieSessionManager {- session :: Maybe CookieSession- -- ^ Per request cache for 'CookieSession'-- , siteKey :: Key- -- ^ A long encryption key used for secure cookie transport-- , cookieName :: ByteString- -- ^ Cookie name for the session system-- , timeOut :: Maybe Int- -- ^ Session cookies will be considered "stale" after this many seconds.-} deriving (Show,Typeable)+ session :: Maybe CookieSession+ -- ^ Per request cache for 'CookieSession'+ , siteKey :: Key+ -- ^ A long encryption key used for secure cookie transport+ , cookieName :: ByteString+ -- ^ Cookie name for the session system+ , timeOut :: Maybe Int+ -- ^ Session cookies will be considered "stale" after this many+ -- seconds.+ , randomNumberGenerator :: RNG+ -- ^ handle to a random number generator+} deriving (Typeable) +------------------------------------------------------------------------------ loadDefSession :: CookieSessionManager -> IO CookieSessionManager-loadDefSession mgr@(CookieSessionManager ses _ _ _) = do- case ses of- Nothing -> do- ses' <- mkCookieSession- return $ mgr { session = Just ses' }- Just _ -> return mgr+loadDefSession mgr@(CookieSessionManager ses _ _ _ rng) =+ case ses of+ Nothing -> do ses' <- mkCookieSession rng+ return $! mgr { session = Just ses' }+ Just _ -> return mgr +------------------------------------------------------------------------------ modSession :: (Session -> Session) -> CookieSession -> CookieSession modSession f (CookieSession t ses) = CookieSession t (f ses) +------------------------------------------------------------------------------ -- | Initialize a cookie-backed session, returning a 'SessionManager' to be -- stuffed inside your application's state. This 'SessionManager' will enable -- the use of all session storage functionality defined in -- 'Snap.Snaplet.Session'+-- initCookieSessionManager- :: FilePath -- ^ Path to site-wide encryption key- -> ByteString -- ^ Session cookie name- -> Maybe Int -- ^ Session time-out (replay attack protection)- -> SnapletInit b SessionManager+ :: FilePath -- ^ Path to site-wide encryption key+ -> ByteString -- ^ Session cookie name+ -> Maybe Int -- ^ Session time-out (replay attack protection)+ -> SnapletInit b SessionManager initCookieSessionManager fp cn to =- makeSnaplet "CookieSession" "A snaplet providing sessions via HTTP cookies."- Nothing $ liftIO $ do- key <- getKey fp- return . SessionManager $ CookieSessionManager Nothing key cn to+ makeSnaplet "CookieSession"+ "A snaplet providing sessions via HTTP cookies."+ Nothing $ liftIO $ do+ key <- getKey fp+ rng <- liftIO mkRNG+ return $! SessionManager $ CookieSessionManager Nothing key cn to rng +------------------------------------------------------------------------------ instance ISessionManager CookieSessionManager where- load mgr@(CookieSessionManager r _ _ _) = do- case r of- Just _ -> return mgr- Nothing -> do- pl <- getPayload mgr- case pl of- Nothing -> liftIO $ loadDefSession mgr- Just (Payload x) -> do- let c = S.decode x- case c of- Left _ -> liftIO $ loadDefSession mgr- Right cs -> return $ mgr { session = Just cs } - commit mgr@(CookieSessionManager r _ _ _) = do- pl <- case r of- Just r' -> return . Payload $ S.encode r'- Nothing -> liftIO mkCookieSession >>= return . Payload . S.encode- setPayload mgr pl+ --------------------------------------------------------------------------+ load mgr@(CookieSessionManager r _ _ _ _) =+ case r of+ Just _ -> return mgr+ Nothing -> do+ pl <- getPayload mgr+ case pl of+ Nothing -> liftIO $ loadDefSession mgr+ Just (Payload x) -> do+ let c = S.decode x+ case c of+ Left _ -> liftIO $ loadDefSession mgr+ Right cs -> return $ mgr { session = Just cs } - reset mgr = do- cs <- liftIO mkCookieSession- return $ mgr { session = Just cs }+ --------------------------------------------------------------------------+ commit mgr@(CookieSessionManager r _ _ _ rng) = do+ pl <- case r of+ Just r' -> return . Payload $ S.encode r'+ Nothing -> liftIO (mkCookieSession rng) >>=+ return . Payload . S.encode+ setPayload mgr pl - touch = id+ --------------------------------------------------------------------------+ reset mgr = do+ cs <- liftIO $ mkCookieSession (randomNumberGenerator mgr)+ return $ mgr { session = Just cs } - insert k v mgr@(CookieSessionManager r _ _ _) = case r of- Just r' -> mgr { session = Just $ modSession (HM.insert k v) r' }- Nothing -> mgr+ --------------------------------------------------------------------------+ touch = id - lookup k (CookieSessionManager r _ _ _) = r >>= HM.lookup k . csSession+ --------------------------------------------------------------------------+ insert k v mgr@(CookieSessionManager r _ _ _ _) = case r of+ Just r' -> mgr { session = Just $ modSession (HM.insert k v) r' }+ Nothing -> mgr - delete k mgr@(CookieSessionManager r _ _ _) = case r of- Just r' -> mgr { session = Just $ modSession (HM.delete k) r' }- Nothing -> mgr+ --------------------------------------------------------------------------+ lookup k (CookieSessionManager r _ _ _ _) = r >>= HM.lookup k . csSession - csrf (CookieSessionManager r _ _ _) = case r of- Just r' -> csCSRFToken r'- Nothing -> ""+ --------------------------------------------------------------------------+ delete k mgr@(CookieSessionManager r _ _ _ _) = case r of+ Just r' -> mgr { session = Just $ modSession (HM.delete k) r' }+ Nothing -> mgr - toList (CookieSessionManager r _ _ _) = case r of- Just r' -> HM.toList . csSession $ r'- Nothing -> []+ --------------------------------------------------------------------------+ csrf (CookieSessionManager r _ _ _ _) = case r of+ Just r' -> csCSRFToken r'+ Nothing -> "" + --------------------------------------------------------------------------+ toList (CookieSessionManager r _ _ _ _) = case r of+ Just r' -> HM.toList . csSession $ r'+ Nothing -> [] +------------------------------------------------------------------------------ -- | A session payload to be stored in a SecureCookie. newtype Payload = Payload ByteString deriving (Eq, Show, Ord, Serialize) +------------------------------------------------------------------------------ -- | Get the current client-side value getPayload :: CookieSessionManager -> Snap (Maybe Payload) getPayload mgr = getSecureCookie (cookieName mgr) (siteKey mgr) (timeOut mgr) +------------------------------------------------------------------------------ -- | Set the client-side value setPayload :: CookieSessionManager -> Payload -> Snap ()-setPayload mgr x =- setSecureCookie (cookieName mgr) (siteKey mgr) (timeOut mgr) x--+setPayload mgr x = setSecureCookie (cookieName mgr) (siteKey mgr)+ (timeOut mgr) x
src/Snap/Snaplet/Session/Common.hs view
@@ -1,41 +1,66 @@-{-|-- This module contains functionality common among multiple back-ends.---}--module Snap.Snaplet.Session.Common where+------------------------------------------------------------------------------+-- | This module contains functionality common among multiple back-ends.+-- +module Snap.Snaplet.Session.Common+ ( RNG+ , mkRNG+ , withRNG+ , randomToken+ , mkCSRFToken+ ) where -import Numeric+------------------------------------------------------------------------------+import Control.Applicative+import Control.Concurrent+import Control.Monad import Data.Serialize import qualified Data.Serialize as S import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.Text.Encoding as T import Data.Text (Text)+import Numeric import System.Random.MWC ------------------------------------------------------------------------------+-- | High speed, mutable random number generator state+newtype RNG = RNG (MVar GenIO)++------------------------------------------------------------------------------+-- | Perform given action, mutating the RNG state+withRNG :: RNG+ -> (GenIO -> IO a)+ -> IO a+withRNG (RNG rng) m = withMVar rng m+++------------------------------------------------------------------------------+-- | Create a new RNG+mkRNG :: IO RNG+mkRNG = withSystemRandom (newMVar >=> return . RNG)+++------------------------------------------------------------------------------ -- | Generates a random salt of given length-randomToken :: Int -> IO ByteString-randomToken n =- let- mk :: GenIO -> IO Int- mk gen = uniformR (0,15) gen- in do- is <- withSystemRandom $ \gen -> sequence . take n . repeat $ mk gen+randomToken :: Int -> RNG -> IO ByteString+randomToken n rng = do+ is <- withRNG rng $ \gen -> sequence . take n . repeat $ mk gen return . B.pack . concat . map (flip showHex "") $ is+ where+ mk :: GenIO -> IO Int+ mk = uniformR (0,15) ------------------------------------------------------------------------------ -- | Generate a randomized CSRF token-mkCSRFToken :: IO Text-mkCSRFToken = T.decodeUtf8 `fmap` randomToken 40+mkCSRFToken :: RNG -> IO Text+mkCSRFToken rng = T.decodeUtf8 <$> randomToken 40 rng +------------------------------------------------------------------------------ instance Serialize Text where- put = S.put . T.encodeUtf8- get = T.decodeUtf8 `fmap` S.get+ put = S.put . T.encodeUtf8+ get = T.decodeUtf8 <$> S.get
src/Snap/Snaplet/Session/SecureCookie.hs view
@@ -1,34 +1,29 @@ {-# LANGUAGE OverloadedStrings #-}-{-|-- This is a support module meant to back all session back-end implementations.-- It gives us an encrypted and timestamped cookie that can store an arbitrary- serializable payload. For security, it will:-- * Encrypt its payload together with a timestamp.-- * Check the timestamp for session expiration everytime you read from the- cookie. This will limit intercept-and-replay attacks by disallowing- cookies older than the timeout threshold.---}+------------------------------------------------------------------------------+-- | This is a support module meant to back all session back-end+-- implementations.+--+-- It gives us an encrypted and timestamped cookie that can store an arbitrary+-- serializable payload. For security, it will:+--+-- * Encrypt its payload together with a timestamp.+--+-- * Check the timestamp for session expiration everytime you read from the+-- cookie. This will limit intercept-and-replay attacks by disallowing+-- cookies older than the timeout threshold. module Snap.Snaplet.Session.SecureCookie where +------------------------------------------------------------------------------ import Control.Applicative import Control.Monad import Control.Monad.Trans- import Data.ByteString (ByteString) import Data.Time import Data.Time.Clock.POSIX- import Data.Serialize-import Web.ClientSession- import Snap.Core-+import Web.ClientSession ------------------------------------------------------------------------------@@ -52,17 +47,17 @@ -> m (Maybe t) getSecureCookie name key timeout = do rqCookie <- getCookie name- rspCookie <- getResponseCookie name `fmap` getResponse+ rspCookie <- getResponseCookie name <$> getResponse let ck = rspCookie `mplus` rqCookie let val = fmap cookieValue ck >>= decrypt key >>= return . decode let val' = val >>= either (const Nothing) Just case val' of Nothing -> return Nothing Just (ts, t) -> do- to <- checkTimeout timeout ts- return $ case to of- True -> Nothing- False -> Just t+ to <- checkTimeout timeout ts+ return $ case to of+ True -> Nothing+ False -> Just t ------------------------------------------------------------------------------@@ -85,12 +80,11 @@ -- | Validate session against timeout policy. -- -- * If timeout is set to 'Nothing', never trigger a time-out.--- * Othwerwise, do a regular time-out check based on current time and given--- timestamp.+--+-- * Otherwise, do a regular time-out check based on current time and given+-- timestamp. checkTimeout :: (MonadSnap m) => Maybe Int -> UTCTime -> m Bool checkTimeout Nothing _ = return False-checkTimeout (Just x) t0 =- let x' = fromIntegral x- in do- t1 <- liftIO getCurrentTime- return $ t1 > addUTCTime x' t0+checkTimeout (Just x) t0 = do+ t1 <- liftIO getCurrentTime+ return $ t1 > addUTCTime (fromIntegral x) t0
src/Snap/Starter.hs view
@@ -13,8 +13,7 @@ import System.Console.GetOpt import System.FilePath --------------------------------------------------------------------------------import Snap.StarterTH+import Snap.StarterTH ------------------------------------------------------------------------------@@ -23,6 +22,7 @@ buildData "tDirDefault" "default" buildData "tDirTutorial" "tutorial" + ------------------------------------------------------------------------------ usage :: String usage = unlines@@ -59,59 +59,72 @@ ] +------------------------------------------------------------------------------ printUsage :: [String] -> IO () printUsage ("init":_) = putStrLn initUsage printUsage _ = putStrLn usage + ------------------------------------------------------------------------------ -- Only one option for now data Option = Help deriving (Show, Eq) +------------------------------------------------------------------------------ setup :: String -> ([FilePath], [(String, String)]) -> IO () setup projName tDir = do mapM createDirectory (fst tDir) mapM_ write (snd tDir) where+ -------------------------------------------------------------------------- write (f,c) = if isSuffixOf "foo.cabal" f then writeFile (projName ++ ".cabal") (insertProjName $ T.pack c) else writeFile f c++ -------------------------------------------------------------------------- isNameChar c = isAlphaNum c || c == '-'++ -------------------------------------------------------------------------- insertProjName c = T.unpack $ T.replace (T.pack "projname") (T.pack $ filter isNameChar projName) c + ------------------------------------------------------------------------------ initProject :: [String] -> IO () initProject args = do case getOpt Permute options args of (flags, other, [])- | Help `elem` flags -> do printUsage other- exitFailure- | otherwise -> go other+ | Help `elem` flags -> printUsage other >> exitFailure+ | otherwise -> go other+ (_, other, errs) -> do putStrLn $ concat errs printUsage other exitFailure where+ -------------------------------------------------------------------------- options = [ Option ['h'] ["help"] (NoArg Help) "Print this message" ] + -------------------------------------------------------------------------- go ("init":rest) = init' rest go _ = do putStrLn "Error: Invalid action!" putStrLn usage exitFailure + -------------------------------------------------------------------------- init' args' = do cur <- getCurrentDirectory- let dirs = splitDirectories cur+ let dirs = splitDirectories cur projName = last dirs- setup' = setup projName+ setup' = setup projName+ case args' of [] -> setup' tDirDefault ["barebones"] -> setup' tDirBareBones
src/Snap/StarterTH.hs view
@@ -14,11 +14,12 @@ ------------------------------------------------------------------------------ -- Convenience types type FileData = (String, String)-type DirData = FilePath+type DirData = FilePath --------------------------------------------------------------------------------- Gets all the directorys in a DirTree+-- Gets all the directories in a DirTree+-- getDirs :: [FilePath] -> DirTree a -> [FilePath] getDirs prefix (Dir n c) = (intercalate "/" (reverse (n:prefix))) : concatMap (getDirs (n:prefix)) c@@ -29,6 +30,7 @@ ------------------------------------------------------------------------------ -- Reads a directory and returns a tuple of the list of all directories -- encountered and a list of filenames and content strings.+-- readTree :: FilePath -> IO ([DirData], [FileData]) readTree dir = do d <- readDirectory $ dir </> "."@@ -39,7 +41,8 @@ --------------------------------------------------------------------------------- Calls readTree and returns it's value in a quasiquote.+-- Calls readTree and returns its value in a quasiquote.+-- dirQ :: FilePath -> Q Exp dirQ tplDir = do d <- runIO . readTree $ "project_template" </> tplDir@@ -49,10 +52,11 @@ ------------------------------------------------------------------------------ -- Creates a declaration assigning the specified name the value returned by -- dirQ.+-- buildData :: String -> FilePath -> Q [Dec] buildData dirName tplDir = do- let dir = mkName dirName-+ let dir = mkName dirName typeSig <- SigD dir `fmap` [t| ([String], [(String, String)]) |]- v <- valD (varP dir) (normalB $ dirQ tplDir) []+ v <- valD (varP dir) (normalB $ dirQ tplDir) []+ return [typeSig, v]
+ test/foosnaplet/snaplet.cfg view
@@ -0,0 +1,2 @@+fooSnapletField = "fooValue"+
+ test/foosnaplet/templates/foopage.tpl view
@@ -0,0 +1,1 @@+foo template page
+ test/non-cabal-appdir/bad.tpl view
@@ -0,0 +1,1 @@+<bad template
+ test/non-cabal-appdir/db.cfg view
@@ -0,0 +1,3 @@+dbServer = "localhost"+dbPort = 1234+
+ test/non-cabal-appdir/good.tpl view
@@ -0,0 +1,1 @@+Good template
+ test/non-cabal-appdir/log/placeholder view
+ test/non-cabal-appdir/snaplet.cfg view
@@ -0,0 +1,6 @@+topConfigField = "topConfigValue"++db {+import "db.cfg"+}+
+ test/non-cabal-appdir/snaplets/baz/snaplet.cfg view
@@ -0,0 +1,2 @@+barSnapletField = "barValue"+
+ test/non-cabal-appdir/snaplets/baz/templates/bazconfig.tpl view
@@ -0,0 +1,1 @@+baz config page <appconfig/> <fooconfig/>
+ test/non-cabal-appdir/snaplets/baz/templates/bazpage.tpl view
@@ -0,0 +1,1 @@+baz template page <barsplice/>
+ test/non-cabal-appdir/snaplets/embedded/extra-templates/extra.tpl view
@@ -0,0 +1,1 @@+This is an extra template
+ test/non-cabal-appdir/snaplets/embedded/snaplets/heist/templates/embeddedpage.tpl view
@@ -0,0 +1,1 @@+embedded snaplet page <asplice/>
+ test/non-cabal-appdir/snaplets/heist/templates/index.tpl view
@@ -0,0 +1,1 @@+index page
+ test/non-cabal-appdir/snaplets/heist/templates/page.tpl view
@@ -0,0 +1,8 @@+<html>+<head>+<title>Example App</title>+</head>+<body>+<content/>+</body>+</html>
+ test/non-cabal-appdir/snaplets/heist/templates/session.tpl view
@@ -0,0 +1,1 @@+<session/>
+ test/non-cabal-appdir/snaplets/heist/templates/splicepage.tpl view
@@ -0,0 +1,1 @@+splice page <appsplice/>
test/runTestsAndCoverage.sh view
@@ -22,8 +22,6 @@ $SUITE $* -killall -HUP snap-testsuite- DIR=dist/hpc rm -Rf $DIR@@ -50,8 +48,8 @@ EXCL="$EXCL --exclude=$m" done -rm -f non-cabal-appdir/templates/bad.tpl-rm -f non-cabal-appdir/templates/good.tpl+rm -f non-cabal-appdir/snaplets/heist/templates/bad.tpl+rm -f non-cabal-appdir/snaplets/heist/templates/good.tpl rm -fr non-cabal-appdir/snaplets/foosnaplet hpc markup $EXCL --destdir=$DIR snap-testsuite >/dev/null 2>&1
test/snap-testsuite.cabal view
@@ -8,34 +8,33 @@ main-is: TestSuite.hs build-depends:- Glob >= 0.5 && < 0.7,- HUnit >= 1.2 && < 2,- MonadCatchIO-transformers >= 0.2 && < 0.3,+ Glob >= 0.5 && < 0.8,+ HUnit >= 1.2 && < 2,+ MonadCatchIO-transformers >= 0.2 && < 0.3, QuickCheck >= 2.3.0.2,- attoparsec >= 0.10 && <0.11,- base >= 4 && < 5,- bytestring >= 0.9 && < 0.10,+ attoparsec >= 0.10 && <0.11,+ base >= 4 && < 5,+ bytestring >= 0.9 && < 0.10, containers >= 0.3,- data-lens >= 2.0.1 && < 2.1,- data-lens-template >= 2.1.1 && < 2.2,+ data-lens >= 2.0.1 && < 2.1,+ data-lens-template >= 2.1.1 && < 2.2, directory,- directory-tree >= 0.10 && < 0.11,+ directory-tree >= 0.10 && < 0.11, filepath,- heist >= 0.7 && < 0.8,+ heist >= 0.7 && < 0.9, http-enumerator >= 0.7.1.7 && < 0.8, mtl >= 2, process == 1.*,- snap-core >= 0.7 && < 0.8,- snap-server >= 0.7 && < 0.8,- test-framework >= 0.4 && < 0.5,- test-framework-hunit >= 0.2.5 && < 0.3,- test-framework-quickcheck2 >= 0.2.6 && < 0.3,- text >= 0.11 && < 0.12,+ snap-core >= 0.8 && < 0.9,+ snap-server >= 0.8 && < 0.9,+ test-framework >= 0.5 && < 0.6,+ test-framework-hunit >= 0.2.5 && < 0.3,+ test-framework-quickcheck2 >= 0.2.6 && < 0.3,+ text >= 0.11 && < 0.12, transformers >= 0.2, unix >= 2.2.0.0 && < 2.6,- utf8-string >= 0.3 && < 0.4,+ utf8-string >= 0.3 && < 0.4, template-haskell--- FIXME extensions: BangPatterns,@@ -65,32 +64,32 @@ main-is: AppMain.hs build-depends:- MonadCatchIO-transformers >= 0.2 && < 0.3,- attoparsec >= 0.10 && <0.11,- base >= 4 && < 5,- bytestring >= 0.9 && < 0.10,+ MonadCatchIO-transformers >= 0.2 && < 0.3,+ attoparsec >= 0.10 && <0.11,+ base >= 4 && < 5,+ bytestring >= 0.9 && < 0.10, cereal >= 0.3, clientsession >= 0.7.3.6 && <0.8,- configurator >= 0.1 && < 0.3,+ configurator >= 0.1 && < 0.3, containers >= 0.3,- data-lens >= 2.0.1 && < 2.1,- data-lens-template >= 2.1.1 && < 2.2,+ data-lens >= 2.0.1 && < 2.1,+ data-lens-template >= 2.1.1 && < 2.2, directory,- directory-tree >= 0.10 && < 0.11,+ directory-tree >= 0.10 && < 0.11, filepath, hashable >= 1.1,- heist >= 0.7 && < 0.8,+ heist >= 0.7 && < 0.9, mtl >= 2, mwc-random >= 0.8, process == 1.*,- snap-core >= 0.7 && < 0.8,- snap-server >= 0.7 && < 0.8,+ snap-core >= 0.8 && < 0.9,+ snap-server >= 0.8 && < 0.9, syb >= 0.1, time >= 1.1,- text >= 0.11 && < 0.12,+ text >= 0.11 && < 0.12, transformers >= 0.2, unordered-containers >= 0.1.4,- utf8-string >= 0.3 && < 0.4,+ utf8-string >= 0.3 && < 0.4, template-haskell --FIXME @@ -121,26 +120,25 @@ main-is: NestTest.hs build-depends:- MonadCatchIO-transformers >= 0.2 && < 0.3,- attoparsec >= 0.10 && <0.11,- base >= 4 && < 5,- bytestring >= 0.9 && < 0.10,+ MonadCatchIO-transformers >= 0.2 && < 0.3,+ attoparsec >= 0.10 && <0.11,+ base >= 4 && < 5,+ bytestring >= 0.9 && < 0.10, containers >= 0.3, data-lens >= 2.0.1 && < 2.1,- data-lens-template >= 2.1 && < 2.2,+ data-lens-template >= 2.1 && < 2.2, directory,- directory-tree >= 0.10 && < 0.11,+ directory-tree >= 0.10 && < 0.11, filepath,- heist >= 0.7 && < 0.8,+ heist >= 0.7 && < 0.9, mtl >= 2, process == 1.*,- snap-core >= 0.7 && < 0.8,- snap-server >= 0.7 && < 0.8,- text >= 0.11 && < 0.12,+ snap-core >= 0.8 && < 0.9,+ snap-server >= 0.8 && < 0.9,+ text >= 0.11 && < 0.12, transformers >= 0.2, utf8-string >= 0.3 && < 0.4, template-haskell- --FIXME extensions: BangPatterns,
+ test/suite/AppMain.hs view
@@ -0,0 +1,9 @@+module Main where++import Snap.Http.Server.Config+import Snap.Snaplet++import Blackbox.App++main :: IO ()+main = serveSnaplet defaultConfig app
+ test/suite/Blackbox/App.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Blackbox.App where++import Prelude hiding (lookup)++import Control.Applicative+import Control.Monad.Trans+import Data.Lens.Lazy+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Configurator+import Snap.Core+import Snap.Util.FileServe++import Snap.Snaplet+import Snap.Snaplet.Heist+import qualified Snap.Snaplet.HeistNoClass as HNC+import Text.Templating.Heist++import Blackbox.Common+import Blackbox.BarSnaplet+import Blackbox.FooSnaplet+import Blackbox.EmbeddedSnaplet+import Blackbox.Types+import Snap.Snaplet.Session+import Snap.Snaplet.Session.Backends.CookieSession+++routeWithSplice :: Handler App App ()+routeWithSplice = do+ str <- with foo getFooField+ writeText $ T.pack $ "routeWithSplice: "++str++routeWithConfig :: Handler App App ()+routeWithConfig = do+ cfg <- getSnapletUserConfig+ val <- liftIO $ lookup cfg "topConfigField"+ writeText $ "routeWithConfig: " `T.append` fromJust val+++sessionDemo :: Handler App App ()+sessionDemo = withSession session $ do+ with session $ do+ curVal <- getFromSession "foo"+ case curVal of+ Nothing -> do+ setInSession "foo" "bar"+ Just _ -> return ()+ list <- with session $ (T.pack . show) `fmap` sessionToList+ csrf <- with session $ (T.pack . show) `fmap` csrfToken+ HNC.renderWithSplices heist "session"+ [ ("session", liftHeist $ textSplice list)+ , ("csrf", liftHeist $ textSplice csrf) ]++sessionTest :: Handler App App ()+sessionTest = withSession session $ do+ q <- getParam "q"+ val <- case q of+ Just x -> do+ let x' = T.decodeUtf8 x+ with session $ setInSession "test" x'+ return x'+ Nothing -> maybe "" id `fmap` with session (getFromSession "test")+ writeText val++fooMod :: FooSnaplet -> FooSnaplet+fooMod f = f { fooField = fooField f ++ "z" }++app :: SnapletInit App App+app = makeSnaplet "app" "Test application" Nothing $ do+ hs <- nestSnaplet "heist" heist $ heistInit "templates"+ fs <- nestSnaplet "foo" foo fooInit+ bs <- nestSnaplet "" bar $ nameSnaplet "baz" $ barInit foo+ sm <- nestSnaplet "session" session $+ initCookieSessionManager "sitekey.txt" "_session" (Just (30 * 60))+ ns <- embedSnaplet "embed" embedded embeddedInit+ addSplices+ [("appsplice", liftHeist $ textSplice "contents of the app splice")]+ HNC.addSplices heist+ [("appconfig", shConfigSplice)]+ addRoutes [ ("/hello", writeText "hello world")+ , ("/routeWithSplice", routeWithSplice)+ , ("/routeWithConfig", routeWithConfig)+ , ("/public", serveDirectory "public")+ , ("/sessionDemo", sessionDemo)+ , ("/sessionTest", sessionTest)+ ]+ wrapHandlers (<|> heistServe)+ return $ App hs (modL snapletValue fooMod fs) bs sm ns+++
+ test/suite/Blackbox/BarSnaplet.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ExistentialQuantification #-}+module Blackbox.BarSnaplet where++import Prelude hiding (lookup)++import Control.Monad.State+import qualified Data.ByteString as B+import Data.Configurator+import Data.Lens.Lazy+import Data.Lens.Template+import Data.Maybe+import Data.Text (Text)+import Snap.Snaplet+import Snap.Snaplet.Heist+import Snap.Core+import Text.Templating.Heist++import Blackbox.Common+import Blackbox.FooSnaplet++data BarSnaplet b = BarSnaplet+ { _barField :: String+ , fooLens :: Lens b (Snaplet FooSnaplet)+ }++makeLens ''BarSnaplet++barsplice :: [(Text, SnapletHeist b v Template)]+barsplice = [("barsplice", liftHeist $ textSplice "contents of the bar splice")]++barInit :: HasHeist b+ => Lens b (Snaplet FooSnaplet)+ -> SnapletInit b (BarSnaplet b)+barInit l = makeSnaplet "barsnaplet" "An example snaplet called bar." Nothing $ do+ config <- getSnapletUserConfig+ addTemplates ""+ rootUrl <- getSnapletRootURL+ addRoutes [("barconfig", liftIO (lookup config "barSnapletField") >>= writeLBS . fromJust)+ ,("barrooturl", writeBS $ "url" `B.append` rootUrl)+ ,("bazpage2", renderWithSplices "bazpage" barsplice)+ ,("bazpage3", heistServeSingle "bazpage")+ ,("bazpage4", renderAs "text/html" "bazpage")+ ,("bazpage5", renderWithSplices "bazpage"+ [("barsplice", shConfigSplice)])+ ,("bazbadpage", heistServeSingle "cpyga")+ ,("bar/handlerConfig", handlerConfig)+ ]+ return $ BarSnaplet "bar snaplet data string" l+
+ test/suite/Blackbox/Common.hs view
@@ -0,0 +1,23 @@+module Blackbox.Common where++import qualified Data.Text as T+import Snap.Core+import Snap.Snaplet+import Snap.Snaplet.Heist+import Text.Templating.Heist++genericConfigString :: (MonadSnaplet m, Monad (m b v)) => m b v T.Text+genericConfigString = do+ a <- getSnapletAncestry+ b <- getSnapletFilePath+ c <- getSnapletName+ d <- getSnapletDescription+ e <- getSnapletRootURL+ return $ T.pack $ show (a,b,c,d,e)++handlerConfig :: Handler b v ()+handlerConfig = writeText =<< genericConfigString++shConfigSplice :: SnapletSplice b v+shConfigSplice = liftHeist . textSplice =<< genericConfigString+
+ test/suite/Blackbox/EmbeddedSnaplet.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Blackbox.EmbeddedSnaplet where++import Prelude hiding ((.))+import Control.Monad.State+import Data.Lens.Lazy+import Data.Lens.Template+import qualified Data.Text as T+import System.FilePath.Posix++import Snap.Snaplet+import Snap.Snaplet.Heist+import Text.Templating.Heist++-- If we universally quantify EmbeddedSnaplet to get rid of the type parameter+-- mkLabels throws an error "Can't reify a GADT data constructor"+data EmbeddedSnaplet = EmbeddedSnaplet+ { _embeddedHeist :: Snaplet (Heist EmbeddedSnaplet)+ , _embeddedVal :: Int+ }++makeLenses [''EmbeddedSnaplet]++instance HasHeist EmbeddedSnaplet where+ heistLens = subSnaplet embeddedHeist++embeddedInit :: SnapletInit EmbeddedSnaplet EmbeddedSnaplet+embeddedInit = makeSnaplet "embedded" "embedded snaplet" Nothing $ do+ hs <- nestSnaplet "heist" embeddedHeist $ heistInit "templates"++ -- This is the implementation of addTemplates, but we do it here manually+ -- to test coverage for addTemplatesAt.+ snapletPath <- getSnapletFilePath+ addTemplatesAt "onemoredir" (snapletPath </> "extra-templates")++ embeddedLens <- getLens+ addRoutes [("aoeuhtns", withSplices+ [("asplice", embeddedSplice embeddedLens)]+ (render "embeddedpage"))+ ]+ return $ EmbeddedSnaplet hs 42+++embeddedSplice :: (Lens (Snaplet b) (Snaplet EmbeddedSnaplet))+ -> SnapletHeist b v Template+embeddedSplice embeddedLens = do+ val <- liftWith embeddedLens $ gets _embeddedVal+ liftHeist $ textSplice $ T.pack $ "splice value" ++ (show val)+
+ test/suite/Blackbox/FooSnaplet.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-}+module Blackbox.FooSnaplet where++import Prelude hiding (lookup)+import Control.Monad.State+import Data.Configurator+import Data.Maybe+import qualified Data.Text as T+import Snap.Snaplet+import Snap.Snaplet.Heist+import Snap.Core+import Text.Templating.Heist++import Blackbox.Common++data FooSnaplet = FooSnaplet { fooField :: String }++fooInit :: HasHeist b => SnapletInit b FooSnaplet+fooInit = makeSnaplet "foosnaplet" "A demonstration snaplet called foo."+ (Just $ return "../foosnaplet") $ do+ config <- getSnapletUserConfig+ addTemplates ""+ addSplices+ [("foosplice", liftHeist $ textSplice "contents of the foo splice")]+ rootUrl <- getSnapletRootURL+ fp <- getSnapletFilePath+ name <- getSnapletName+ addSplices [("fooconfig", shConfigSplice)]+ addRoutes [("fooConfig", liftIO (lookup config "fooSnapletField") >>= writeLBS . fromJust)+ ,("fooRootUrl", writeBS rootUrl)+ ,("fooSnapletName", writeText $ fromMaybe "empty snaplet name" name)+ ,("fooFilePath", writeText $ T.pack fp)+ ,("handlerConfig", handlerConfig)+ ]+ return $ FooSnaplet "foo snaplet data string"++getFooField :: Handler b FooSnaplet String+getFooField = gets fooField+
+ test/suite/Blackbox/Tests.hs view
@@ -0,0 +1,309 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Blackbox.Tests+ ( tests+ , remove+ , removeDir+ ) where++------------------------------------------------------------------------------+import Control.Exception (catch, throwIO)+import Control.Monad+import Control.Monad.Trans+import qualified Data.ByteString.Char8 as S+import qualified Data.ByteString.Lazy.Char8 as L+import Data.Text.Lazy (Text)+import qualified Data.Text.Lazy.Encoding as T+import qualified Network.HTTP.Enumerator as HTTP+import Prelude hiding (catch)+import System.Directory+import System.FilePath+import Test.Framework (Test, testGroup)+import Test.Framework.Providers.HUnit+import Test.HUnit hiding (Test, path)++------------------------------------------------------------------------------+-- TODO: fix all the hardcoded ports and strings in here, order the functions+-- non-randomly+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+testName :: String -> String+testName uri = "internal/" ++ uri++------------------------------------------------------------------------------+requestTest :: String -> Text -> Test+requestTest url desired = testCase (testName url) $ requestTest' url desired+++------------------------------------------------------------------------------+assertRelativelyTheSame :: FilePath -> FilePath -> IO ()+assertRelativelyTheSame p expected = do+ b <- makeRelativeToCurrentDirectory p+ assertEqual ("expected " ++ expected) expected b+++------------------------------------------------------------------------------+testServerUri :: String+testServerUri = "http://127.0.0.1:9753"+++------------------------------------------------------------------------------+grab :: MonadIO m => String -> m L.ByteString+grab path = liftIO $ HTTP.simpleHttp $ testServerUri ++ path+++------------------------------------------------------------------------------+fooConfigPathTest :: Test+fooConfigPathTest = testCase (testName "foo/fooFilePath") $ do+ b <- liftM L.unpack $ grab "/foo/fooFilePath"+ assertRelativelyTheSame b "non-cabal-appdir/snaplets/foosnaplet"+++------------------------------------------------------------------------------+testWithCwd :: String+ -> (String -> L.ByteString -> Assertion)+ -> Test+testWithCwd uri f = testCase (testName uri) $+ testWithCwd' uri f+++------------------------------------------------------------------------------+testWithCwd' :: String+ -> (String -> L.ByteString -> Assertion)+ -> Assertion+testWithCwd' uri f = do+ b <- grab slashUri+ cwd <- getCurrentDirectory++ f cwd b++ where+ slashUri = "/" ++ uri+++------------------------------------------------------------------------------+fooHandlerConfigTest :: Test+fooHandlerConfigTest = testWithCwd "foo/handlerConfig" $ \cwd b -> do+ let response = L.fromChunks [ "([\"app\"],\""+ , S.pack cwd+ , "/non-cabal-appdir/snaplets/foosnaplet\","+ , "Just \"foosnaplet\",\"A demonstration "+ , "snaplet called foo.\",\"foo\")" ]+ assertEqual "" response b+++------------------------------------------------------------------------------+barHandlerConfigTest :: Test+barHandlerConfigTest = testWithCwd "bar/handlerConfig" $ \cwd b -> do+ let response = L.fromChunks [ "([\"app\"],\""+ , S.pack cwd+ , "/non-cabal-appdir/snaplets/baz\","+ , "Just \"baz\",\"An example snaplet called "+ , "bar.\",\"\")" ]+ assertEqual "" response b+++------------------------------------------------------------------------------+-- bazpage5 uses barsplice bound by renderWithSplices at request time+bazpage5Test :: Test+bazpage5Test = testWithCwd "bazpage5" $ \cwd b -> do+ let response = L.fromChunks [ "baz template page ([\"app\"],\""+ , S.pack cwd+ , "/non-cabal-appdir/snaplets/baz\","+ , "Just \"baz\",\"An example snaplet called "+ , "bar.\",\"\")\n" ]+ assertEqual "" response b+++------------------------------------------------------------------------------+-- bazconfig uses two splices, appconfig and fooconfig. appconfig is bound with+-- the non type class version of addSplices in the main app initializer.+-- fooconfig is bound by addSplices in fooInit.+bazConfigTest :: Test+bazConfigTest = testWithCwd "bazconfig" $ \cwd b -> do+ let response = L.fromChunks [+ "baz config page ([],\""+ , S.pack cwd+ , "/non-cabal-appdir\",Just \"app\","+ , "\"Test application\",\"\") "+ , "([\"app\"],\""+ , S.pack cwd+ , "/non-cabal-appdir/snaplets/foosnaplet\","+ , "Just \"foosnaplet\",\"A demonstration snaplet "+ , "called foo.\",\"foo\")\n"+ ]++ assertEqual "" response b+++------------------------------------------------------------------------------+expect404 :: String -> IO ()+expect404 url = action `catch` h+ where+ action = do+ HTTP.simpleHttp $ "http://127.0.0.1:9753/" ++ url+ assertFailure "expected 404"++ h e@(HTTP.StatusCodeException c _) | c == 404 = return ()+ | otherwise = throwIO e+ h e = throwIO e+++------------------------------------------------------------------------------+request404Test :: String -> Test+request404Test url = testCase (testName url) $ expect404 url+++------------------------------------------------------------------------------+requestTest' :: String -> Text -> IO ()+requestTest' url desired = do+ actual <- HTTP.simpleHttp $ "http://127.0.0.1:9753/" ++ url+ assertEqual url desired (T.decodeUtf8 actual)+++------------------------------------------------------------------------------+requestNoError :: String -> Text -> Test+requestNoError url desired =+ testCase (testName url) $ requestNoError' url desired+++------------------------------------------------------------------------------+requestNoError' :: String -> Text -> IO ()+requestNoError' url desired = do+ let fullUrl = "http://127.0.0.1:9753/" ++ url+ url' <- HTTP.parseUrl fullUrl+ HTTP.Response _ _ b <- liftIO $ HTTP.withManager $ HTTP.httpLbsRedirect url'+ assertEqual fullUrl desired (T.decodeUtf8 b)+++------------------------------------------------------------------------------+tests :: Test+tests = testGroup "non-cabal-tests"+ [ requestTest "hello" "hello world"+ , requestTest "index" "index page\n"+ , requestTest "" "index page\n"+ , requestTest "splicepage" "splice page contents of the app splice\n"+ , requestTest "routeWithSplice" "routeWithSplice: foo snaplet data stringz"+ , requestTest "routeWithConfig" "routeWithConfig: topConfigValue"+ , requestTest "foo/foopage" "foo template page\n"+ , requestTest "foo/fooConfig" "fooValue"+ , requestTest "foo/fooRootUrl" "foo"+ , requestTest "barconfig" "barValue"+ , requestTest "bazpage" "baz template page <barsplice></barsplice>\n"+ , requestTest "bazpage2" "baz template page contents of the bar splice\n"+ , requestTest "bazpage3" "baz template page <barsplice></barsplice>\n"+ , requestTest "bazpage4" "baz template page <barsplice></barsplice>\n"+ , requestTest "barrooturl" "url"+ , requestNoError "bazbadpage" "A web handler threw an exception. Details:\nTemplate \"cpyga\" not found."+ , requestTest "foo/fooSnapletName" "foosnaplet"++ , fooConfigPathTest++ -- Test the embedded snaplet+ , requestTest "embed/heist/embeddedpage" "embedded snaplet page <asplice></asplice>\n"+ , requestTest "embed/aoeuhtns" "embedded snaplet page splice value42\n"+ , requestTest "embed/heist/onemoredir/extra" "This is an extra template\n"++ -- This set of tests highlights the differences in the behavior of the+ -- get... functions from MonadSnaplet.+ , fooHandlerConfigTest+ , barHandlerConfigTest+ , bazpage5Test+ , bazConfigTest+ , requestTest "sessionDemo" "[(\"foo\",\"bar\")]\n"+ , reloadTest+ ]++remove :: FilePath -> IO ()+remove f = do+ exists <- doesFileExist f+ when exists $ removeFile f+++removeDir :: FilePath -> IO ()+removeDir d = do+ exists <- doesDirectoryExist d+ when exists $ removeDirectoryRecursive "non-cabal-appdir/snaplets/foosnaplet"+++------------------------------------------------------------------------------+reloadTest :: Test+reloadTest = testCase "internal/reload-test" $ do+ let goodTplOrig = "non-cabal-appdir" </> "good.tpl"+ let badTplOrig = "non-cabal-appdir" </> "bad.tpl"+ let goodTplNew = "non-cabal-appdir" </> "snaplets" </> "heist"+ </> "templates" </> "good.tpl"+ let badTplNew = "non-cabal-appdir" </> "snaplets" </> "heist"+ </> "templates" </> "bad.tpl"++ goodExists <- doesFileExist goodTplNew+ badExists <- doesFileExist badTplNew++ assertBool "good.tpl exists" (not goodExists)+ assertBool "bad.tpl exists" (not badExists)+ expect404 "bad"++ copyFile badTplOrig badTplNew+ expect404 "good"+ expect404 "bad"++ testWithCwd' "admin/reload" $ \cwd' b -> do+ let cwd = S.pack cwd'+ let response =+ L.fromChunks [ "Error reloading site!\n\nInitializer "+ , "threw an exception...\n"+ , cwd+ , "/non-cabal-appdir/snaplets/heist"+ , "/templates/bad.tpl \""+ , cwd+ , "/non-cabal-appdir/snaplets/heist/templates"+ , "/bad.tpl\" (line 2, column 1):\nunexpected "+ , "end of input\nexpecting \"=\", \"/\" or "+ , "\">\"\n\n\n...but before it died it generated "+ , "the following output:\nInitializing app @ /\n"+ , "Initializing heist @ /heist\n\n" ]+ assertEqual "admin/reload" response b++ remove badTplNew+ copyFile goodTplOrig goodTplNew++ testWithCwd' "admin/reload" $ \cwd' b -> do+ let cwd = S.pack cwd'+ let response =+ L.fromChunks [ "Initializing app @ /\n"+ , "Initializing heist @ /heist\n"+ , "...loaded 5 templates from "+ , cwd+ , "/non-cabal-appdir/snaplets/heist/templates\n"+ , "Initializing foosnaplet @ /foo\n"+ , "...adding 1 templates from "+ , cwd+ , "/non-cabal-appdir/snaplets/foosnaplet"+ , "/templates with route prefix foo/\n"+ , "Initializing baz @ /\n"+ , "...adding 2 templates from "+ , cwd+ , "/non-cabal-appdir/snaplets/baz/templates "+ , "with route prefix /\n"+ , "Initializing CookieSession @ /session\n"+ , "Initializing embedded @ /\n"+ , "Initializing heist @ /heist\n"+ , "...loaded 1 templates from "+ , cwd+ , "/non-cabal-appdir/snaplets/embedded"+ , "/snaplets/heist/templates\n"+ , "...adding 1 templates from "+ , cwd+ , "/non-cabal-appdir/snaplets/embedded"+ , "/extra-templates with route prefix "+ , "onemoredir/\n"+ , "Site successfully reloaded.\n"+ ]++ assertEqual "admin/reload" response b++ requestTest' "good" "Good template\n"+
+ test/suite/Blackbox/Types.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Blackbox.Types where+++import Data.Lens.Template++import Snap.Snaplet+import Snap.Snaplet.Heist++import Blackbox.FooSnaplet+import Blackbox.BarSnaplet+import Blackbox.EmbeddedSnaplet+import Snap.Snaplet.Session+++data App = App+ { _heist :: Snaplet (Heist App)+ , _foo :: Snaplet FooSnaplet+ , _bar :: Snaplet (BarSnaplet App)+ , _session :: Snaplet SessionManager+ , _embedded :: Snaplet EmbeddedSnaplet+ }++makeLenses [''App]++instance HasHeist App where heistLens = subSnaplet heist+
+ test/suite/NestTest.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Main where++import Prelude hiding ((.))+import Control.Monad.State+import Data.Lens.Lazy+import Data.Lens.Template+import qualified Data.Text as T+import Snap.Http.Server.Config+import Snap.Core+import Snap.Util.FileServe++import Snap.Snaplet+import Snap.Snaplet.Heist+import Text.Templating.Heist++-- If we universally quantify FooSnaplet to get rid of the type parameter+-- mkLabels throws an error "Can't reify a GADT data constructor"+data FooSnaplet = FooSnaplet+ { _fooHeist :: Snaplet (Heist FooSnaplet)+ , _fooVal :: Int+ }++makeLenses [''FooSnaplet]++instance HasHeist FooSnaplet where+ heistLens = subSnaplet fooHeist++fooInit :: SnapletInit FooSnaplet FooSnaplet+fooInit = makeSnaplet "foosnaplet" "foo snaplet" Nothing $ do+ hs <- nestSnaplet "heist" fooHeist $ heistInit "templates"+ addTemplates "foo"+ rootUrl <- getSnapletRootURL+ fooLens <- getLens+ addRoutes [("fooRootUrl", writeBS rootUrl)+ ,("aoeuhtns", renderWithSplices "foo/foopage"+ [("asplice", fooSplice fooLens)])+ ,("", heistServe)+ ]+ return $ FooSnaplet hs 42+++--fooSplice :: (Lens (Snaplet b) (Snaplet (FooSnaplet b)))+-- -> SnapletSplice (Handler b b)+fooSplice :: (Lens (Snaplet b) (Snaplet FooSnaplet))+ -> SnapletHeist b v Template+fooSplice fooLens = do+ val <- liftWith fooLens $ gets _fooVal+ liftHeist $ textSplice $ T.pack $ "splice value" ++ (show val)++------------------------------------------------------------------------------++data App = App+ { _foo :: Snaplet (FooSnaplet)+ }++makeLenses [''App]++app :: SnapletInit App App+app = makeSnaplet "app" "nested snaplet application" Nothing $ do+ fs <- embedSnaplet "foo" foo fooInit+ addRoutes [ ("/hello", writeText "hello world")+ , ("/public", serveDirectory "public")+ ]+ return $ App fs++main :: IO ()+main = serveSnaplet defaultConfig app+
+ test/suite/SafeCWD.hs view
@@ -0,0 +1,30 @@+module SafeCWD+ ( inDir+ , removeDirectoryRecursiveSafe+ ) where++import Control.Concurrent.QSem+import Control.Exception+import Control.Monad+import System.Directory+import System.IO.Unsafe++sem :: QSem+sem = unsafePerformIO $ newQSem 1++inDir :: Bool -> FilePath -> IO a -> IO a+inDir startClean dir action = bracket before after (const action)+ where+ before = do+ waitQSem sem+ cwd <- getCurrentDirectory+ when startClean $ removeDirectoryRecursiveSafe dir+ createDirectoryIfMissing True dir+ setCurrentDirectory dir+ return cwd+ after cwd = do+ setCurrentDirectory cwd+ signalQSem sem++removeDirectoryRecursiveSafe p =+ doesDirectoryExist p >>= flip when (removeDirectoryRecursive p)
+ test/suite/Snap/Snaplet/Internal/LensT/Tests.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE TemplateHaskell #-}++module Snap.Snaplet.Internal.LensT.Tests (tests) where++import Control.Applicative+import Control.Category+import Control.Monad.Identity+import Control.Monad.State.Strict+import Data.Lens.Template+import Prelude hiding (catch, (.))+import Test.Framework+import Test.Framework.Providers.HUnit+import Test.HUnit hiding (Test, path)+++------------------------------------------------------------------------------+import Snap.Snaplet.Internal.LensT+++------------------------------------------------------------------------------+data TestType = TestType {+ _int0 :: Int+ , _sub :: TestSubType+} deriving (Show)++data TestSubType = TestSubType {+ _sub0 :: Int+ , _sub1 :: Int+ , _bot :: TestBotType+} deriving (Show)++data TestBotType = TestBotType {+ _bot0 :: Int+} deriving (Show)++makeLenses [''TestType, ''TestSubType, ''TestBotType]+++------------------------------------------------------------------------------+defaultState :: TestType+defaultState = TestType 1 $ TestSubType 2 999 $ TestBotType 3+++------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Snap.Snaplet.Internal.LensT"+ [ testfmap+ , testApplicative+ , testMonadState+ ]+++------------------------------------------------------------------------------+testfmap :: Test+testfmap = testCase "lensed/fmap" $ do+-- x <- evalStateT (lensedAsState (fmap (*2) three) (bot . sub)) defaultState+ let x = fst $ runIdentity (runLensT (fmap (*2) three) (bot . sub) defaultState)+ assertEqual "fmap" 6 x++ let (y,s') = runIdentity (runLensT twiddle (bot . sub) defaultState)++ assertEqual "fmap2" (12 :: Int) y+ assertEqual "lens" (13 :: Int) $ _bot0 $ _bot $ _sub s'+ return ()++ where+-- three :: LensT TestType TestBotType IO Int+ three = return 3++ twiddle = do+ modify $ \(TestBotType x) -> TestBotType (x+10)+ fmap (+9) three+++------------------------------------------------------------------------------+testApplicative :: Test+testApplicative = testCase "lensed/applicative" $ do+-- x <- evalStateT (lensedAsState (pure (*2) <*> three) (bot . sub)) defaultState+ let x = fst $ runIdentity (runLensT (pure (*2) <*> three) (bot . sub) defaultState)+ assertEqual "fmap" 6 x++ let (y,s') = runIdentity (runLensT twiddle (bot . sub) defaultState)++ assertEqual "fmap2" (12::Int) y+ assertEqual "lens" 13 $ _bot0 $ _bot $ _sub s'+ return ()++ where+-- three :: LensT TestType TestBotType IO Int+ three = pure 3++ twiddle = do+ modify $ \(TestBotType x) -> TestBotType (x+10)+ pure [] *> (pure (+9) <*> three) <* pure []+++------------------------------------------------------------------------------+testMonadState :: Test+testMonadState = testCase "lens/MonadState" $ do+-- s <- execStateT (lensedAsState go (bot0 . bot . sub)) defaultState+ let s = snd $ runIdentity (runLensT go (bot0 . bot . sub) defaultState)++ assertEqual "bot0" 9 $ _bot0 $ _bot $ _sub s+ assertEqual "sub0" 3 $ _sub0 $ _sub s+ assertEqual "sub1" 999 $ _sub1 $ _sub s++ where+-- go :: LensT TestType Int IO ()+ go = do+ modify (*2)+ modify (+3)+ withTop sub go'++-- go' :: LensT TestType TestSubType IO ()+ go' = do+ a <- with sub0 get+ with sub0 $ put $ a+1++
+ test/suite/Snap/Snaplet/Internal/Lensed/Tests.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE TemplateHaskell #-}++module Snap.Snaplet.Internal.Lensed.Tests (tests) where++import Control.Applicative+import Control.Category+import Control.Exception+import Control.Monad.State.Strict+import Data.Lens.Template+import Prelude hiding (catch, (.))+import Test.Framework+import Test.Framework.Providers.HUnit+import Test.HUnit hiding (Test, path)+++------------------------------------------------------------------------------+import Snap.Snaplet.Internal.Lensed+++------------------------------------------------------------------------------+data TestType = TestType {+ _int0 :: Int+ , _sub :: TestSubType+} deriving (Show)++data TestSubType = TestSubType {+ _sub0 :: Int+ , _sub1 :: Int+ , _bot :: TestBotType+} deriving (Show)++data TestBotType = TestBotType {+ _bot0 :: Int+} deriving (Show)++makeLenses [''TestType, ''TestSubType, ''TestBotType]+++------------------------------------------------------------------------------+defaultState :: TestType+defaultState = TestType 1 $ TestSubType 2 999 $ TestBotType 3+++------------------------------------------------------------------------------+tests = testGroup "Snap.Snaplet.Internal.Lensed"+ [ testfmap+ , testApplicative+ , testMonadState+ ]+++------------------------------------------------------------------------------+testfmap :: Test+testfmap = testCase "lensed/fmap" $ do+ x <- evalStateT (lensedAsState (fmap (*2) three) (bot . sub)) defaultState+ assertEqual "fmap" 6 x++ (y,s') <- runStateT (lensedAsState twiddle (bot . sub)) defaultState++ assertEqual "fmap2" 12 y+ assertEqual "lens" 13 $ _bot0 $ _bot $ _sub s'+ return ()++ where+ three :: Lensed TestType TestBotType IO Int+ three = return 3++ twiddle = do+ modify $ \(TestBotType x) -> TestBotType (x+10)+ fmap (+9) three+++------------------------------------------------------------------------------+testApplicative :: Test+testApplicative = testCase "lensed/applicative" $ do+ x <- evalStateT (lensedAsState (pure (*2) <*> three) (bot . sub)) defaultState+ assertEqual "fmap" 6 x++ (y,s') <- runStateT (lensedAsState twiddle (bot . sub)) defaultState++ assertEqual "fmap2" (12::Int) y+ assertEqual "lens" 13 $ _bot0 $ _bot $ _sub s'+ return ()++ where+ three :: Lensed TestType TestBotType IO Int+ three = pure 3++ twiddle = do+ modify $ \(TestBotType x) -> TestBotType (x+10)+ pure [] *> (pure (+9) <*> three) <* pure []+++------------------------------------------------------------------------------+testMonadState :: Test+testMonadState = testCase "lens/MonadState" $ do+ s <- execStateT (lensedAsState go (bot0 . bot . sub)) defaultState++ assertEqual "bot0" 9 $ _bot0 $ _bot $ _sub s+ assertEqual "sub0" 3 $ _sub0 $ _sub s+ assertEqual "sub1" 1000 $ _sub1 $ _sub s++ where+ go :: Lensed TestType Int IO ()+ go = do+ modify (*2)+ modify (+3)+ withTop sub go'++ go' :: Lensed TestType TestSubType IO ()+ go' = do+ a <- with sub0 get+ with sub0 $ put $ a+1+ embed sub1 go''++ go'' :: Lensed TestSubType Int IO ()+ go'' = modify (+1)+++eat :: SomeException -> IO ()+eat _ = return ()++qqq = defaultMainWithArgs [tests] ["--plain"] `catch` eat
+ test/suite/Snap/Snaplet/Internal/RST/Tests.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TemplateHaskell #-}++module Snap.Snaplet.Internal.RST.Tests+ ( tests ) where++import Control.Applicative+import Control.Monad.Identity+import Control.Monad.Reader+import Control.Monad.State+import Prelude hiding (catch, (.))+import Test.Framework+import Test.Framework.Providers.HUnit+import Test.Framework.Providers.QuickCheck2+import Test.HUnit hiding (Test, path)++import Snap.Snaplet.Internal.RST+++tests :: Test+tests = testGroup "Snap.Snaplet.Internal.RST"+ [ testExec+ , testEval+ , testFail+ , testAlternative+ ]+++testEval :: Test+testEval = testProperty "RST/execRST" prop+ where+ prop x = runIdentity (evalRST m x undefined) == x+ m :: RST Int () Identity Int+ m = ask++testExec :: Test+testExec = testProperty "RST/execRST" prop+ where+ prop x = runIdentity (execRST m undefined x) == x+ m :: RST () Int Identity Int+ m = get++testFail :: Test+testFail = testCase "RST/fail" $+ assertEqual "RST fail" rstFail Nothing++testAlternative :: Test+testAlternative = testCase "RST/Alternative" $ do+ assertEqual "Alternative instance" rstAlt (Just (5, 1))+ assertEqual "Alternative instance" rstAlt2 (Just (5, 1))++addEnv :: Monad m => RST Int Int m ()+addEnv = do+ v <- ask+ modify (+v)++rstAlt :: Maybe (Int, Int)+rstAlt = runRST (addEnv >> (empty <|> (return 5))) 1 0++rstAlt2 :: Maybe (Int, Int)+rstAlt2 = runRST (addEnv >> ((return 5) <|> empty)) 1 0++rstFail :: Maybe Int+rstFail = evalRST (fail "foo") (0 :: Int) (0 :: Int)+
+ test/suite/Snap/Snaplet/Internal/Tests.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TemplateHaskell #-}++module Snap.Snaplet.Internal.Tests+ ( tests, initTest ) where++------------------------------------------------------------------------------+import Control.Monad+import Control.Monad.Trans+import Data.ByteString (ByteString)+import Data.Lens.Template+import Data.List+import Data.Text+import Prelude hiding (catch, (.))+import System.Directory+import Test.Framework+import Test.Framework.Providers.HUnit+import Test.HUnit hiding (Test, path)+------------------------------------------------------------------------------+import Snap.Snaplet.Internal.Initializer+import Snap.Snaplet.Internal.Types+++ ---------------------------------+ -- TODO: this module is a mess --+ ---------------------------------+++------------------------------------------------------------------------------+data Foo = Foo Int++data Bar = Bar Int+++data App = App+ { _foo :: Snaplet Foo+ , _bar :: Snaplet Bar+ }++makeLens ''App++--showConfig :: SnapletConfig -> IO ()+--showConfig c = do+-- putStrLn "SnapletConfig:"+-- print $ _scAncestry c+-- print $ _scFilePath c+-- print $ _scId c+-- print $ _scDescription c+-- print $ _scRouteContext c+-- putStrLn ""+++------------------------------------------------------------------------------+assertGet :: (MonadIO m, Show a, Eq a) => String -> m a -> a -> m ()+assertGet name getter val = do+ v <- getter+ liftIO $ assertEqual name val v+++------------------------------------------------------------------------------+configAssertions :: (MonadSnaplet m, MonadIO (m b v))+ => [Char]+ -> ([Text], FilePath, Maybe Text, Text, ByteString)+ -> m b v ()+configAssertions prefix (a,f,n,d,r) = do+ assertGet (prefix ++ "ancestry" ) getSnapletAncestry a+ assertGet (prefix ++ "file path" ) getSnapletFilePath f+ assertGet (prefix ++ "name" ) getSnapletName n+ assertGet (prefix ++ "description" ) getSnapletDescription d+ assertGet (prefix ++ "route context" ) getSnapletRootURL r+++------------------------------------------------------------------------------+appInit :: SnapletInit App App+appInit = makeSnaplet "app" "Test application" Nothing $ do+ cwd <- liftIO getCurrentDirectory++ configAssertions "root "+ ([], cwd, Just "app", "Test application", "")+ f <- nestSnaplet "foo" foo $ fooInit+ b <- nestSnaplet "bar" bar $ barInit+ return $ App f b+++------------------------------------------------------------------------------+fooInit :: SnapletInit b Foo+fooInit = makeSnaplet "foo" "Foo Snaplet" Nothing $ do+ cwd <- liftIO getCurrentDirectory+ let dir = cwd ++ "/snaplets/foo"++ configAssertions "foo "+ (["app"], dir, Just "foo", "Foo Snaplet", "foo")+ return $ Foo 42+++------------------------------------------------------------------------------+barInit :: SnapletInit b Bar+barInit = makeSnaplet "bar" "Bar Snaplet" Nothing $ do+ cwd <- liftIO getCurrentDirectory+ let dir = cwd ++ "/snaplets/bar"+ configAssertions "bar "+ (["app"], dir, Just "bar", "Bar Snaplet", "bar")+ return $ Bar 2+++------------------------------------------------------------------------------+initTest :: IO ()+initTest = do+ (out,_,_) <- runSnaplet appInit++ -- note from gdc: wtf?+ if out == "aoeu"+ then putStrLn "Something really strange"+ else return ()+++------------------------------------------------------------------------------+tests :: Test+tests = testGroup "Snap.Snaplet.Internal"+ [ testCase "initializer tests" initTest+ ]+
test/suite/Snap/TestCommon.hs view
@@ -2,23 +2,38 @@ module Snap.TestCommon where -import Control.Concurrent-import Control.Exception-import Control.Monad (forM_, when)-import Data.Maybe-import Data.Monoid-import Prelude hiding (catch)-import System.Cmd-import System.Directory-import System.Environment-import System.Exit-import System.FilePath-import System.FilePath.Glob-import System.Process hiding (cwd)+------------------------------------------------------------------------------+import Control.Concurrent ( threadDelay )+import Control.Exception ( ErrorCall(..)+ , SomeException+ , bracket+ , catch+ , throwIO+ )+import Control.Monad ( forM_ )+import Data.Maybe ( fromMaybe )+import Data.Monoid ( First(..), mconcat )+import Prelude hiding ( catch )+import System.Cmd ( system )+import System.Directory ( doesFileExist+ , getCurrentDirectory+ , findExecutable+ , removeFile+ )+import System.Environment ( getEnv )+import System.Exit ( ExitCode(..) )+import System.FilePath ( joinPath, splitPath, (</>) )+import System.FilePath.Glob ( compile, globDir1 )+import System.Process ( runCommand+ , terminateProcess+ , waitForProcess+ ) +------------------------------------------------------------------------------ import SafeCWD +------------------------------------------------------------------------------ testGeneratedProject :: String -- ^ project name and directory -> String -- ^ arguments to @snap init@ -> String -- ^ arguments to @cabal install@@@ -28,23 +43,25 @@ testGeneratedProject projName snapInitArgs cabalInstallArgs httpPort testAction = do cwd <- getCurrentDirectory- let segments = reverse $ splitPath cwd- projectPath = cwd </> "test-snap-exe" </> projName- snapRoot = joinPath $ reverse $ drop 1 segments- snapRepos = joinPath $ reverse $ drop 2 segments - sandbox = cwd </> "test-cabal-dev"-+ --------------------------------------------------------------------------+ let segments = reverse $ splitPath cwd+ projectPath = cwd </> "test-snap-exe" </> projName+ snapRoot = joinPath $ reverse $ drop 1 segments+ snapRepos = joinPath $ reverse $ drop 2 segments+ sandbox = cwd </> "test-cabal-dev" cabalDevArgs = "-s " ++ sandbox-- args = cabalDevArgs ++ " " ++ cabalInstallArgs + args = cabalDevArgs ++ " --reinstall " ++ cabalInstallArgs + ---------------------------------------------------------------------- initialize = do snapExe <- findSnap systemOrDie $ snapExe ++ " init " ++ snapInitArgs - snapCoreSrc <- fromEnv "SNAP_CORE_SRC" $ snapRepos </> "snap-core"- snapServerSrc <- fromEnv "SNAP_SERVER_SRC" $ snapRepos </> "snap-server"+ snapCoreSrc <- fromEnv "SNAP_CORE_SRC" $+ snapRepos </> "snap-core"+ snapServerSrc <- fromEnv "SNAP_SERVER_SRC" $+ snapRepos </> "snap-server" xmlhtmlSrc <- fromEnv "XMLHTML_SRC" $ snapRepos </> "xmlhtml" heistSrc <- fromEnv "HEIST_SRC" $ snapRepos </> "heist" let snapSrc = snapRoot@@ -54,8 +71,11 @@ forM_ [ snapCoreSrc, snapServerSrc, xmlhtmlSrc, heistSrc , snapSrc] $ \s ->- systemOrDie $ "cabal-dev " ++ cabalDevArgs- ++ " add-source " ++ s+ systemOrDie $ concat [ "cabal-dev "+ , cabalDevArgs+ , " add-source "+ , s+ ] systemOrDie $ "cabal-dev install " ++ args let cmd = ("." </> "dist" </> "build" </> projName </> projName)@@ -65,48 +85,59 @@ waitABit return pHandle + ---------------------------------------------------------------------- findSnap = do home <- fromEnv "HOME" "."- p1 <- gimmeIfExists $ snapRoot </> "dist" </> "build" </> "snap" </> "snap"- p2 <- gimmeIfExists $ home </> ".cabal" </> "bin" </> "snap"- p3 <- findExecutable "snap"+ p1 <- gimmeIfExists $ snapRoot </> "dist" </> "build"+ </> "snap" </> "snap"+ p2 <- gimmeIfExists $ home </> ".cabal" </> "bin" </> "snap"+ p3 <- findExecutable "snap" return $ fromMaybe (error "couldn't find snap executable") (getFirst $ mconcat $ map First [p1,p2,p3]) - putStrLn $ "Changing directory to "++projectPath+ --------------------------------------------------------------------------+ putStrLn $ "Changing directory to " ++ projectPath inDir True projectPath $ bracket initialize cleanup (const testAction) removeDirectoryRecursiveSafe projectPath- where + where+ -------------------------------------------------------------------------- fromEnv name def = do r <- getEnv name `catch` \(_::SomeException) -> return "" if r == "" then return def else return r + -------------------------------------------------------------------------- cleanup pHandle = do terminateProcess pHandle waitForProcess pHandle + -------------------------------------------------------------------------- waitABit = threadDelay $ 2*10^(6::Int) + -------------------------------------------------------------------------- pkgCleanUp d pkg = do paths <- globDir1 (compile $ "packages*conf/" ++ pkg ++ "-*") d- forM_ paths- (\x -> (rm x `catch` \(_::SomeException) -> return ()))+ forM_ paths $ \x ->+ rm x `catch` \(_::SomeException) -> return ()+ where rm x = do putStrLn $ "removing " ++ x removeFile x + -------------------------------------------------------------------------- gimmeIfExists p = do b <- doesFileExist p if b then return (Just p) else return Nothing +------------------------------------------------------------------------------ systemOrDie :: String -> IO () systemOrDie s = do putStrLn $ "Running \"" ++ s ++ "\"" system s >>= check+ where check ExitSuccess = return ()- check _ = throwIO $ ErrorCall $ "command failed: '" ++ s ++ "'"+ check _ = throwIO $ ErrorCall $ "command failed: '" ++ s ++ "'"
test/suite/TestSuite.hs view
@@ -1,54 +1,65 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module Main where +------------------------------------------------------------------------------ import Control.Concurrent import Control.Exception import Control.Monad import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.ByteString.Char8 as S import qualified Network.HTTP.Enumerator as HTTP+import Prelude hiding (catch) import Snap.Http.Server.Config import Snap.Snaplet+import System.IO import System.Posix.Process+import System.Posix.Signals import System.Posix.Types---import Test.Framework (defaultMain, Test) import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path)--import Snap.Http.Server (simpleHttpServe)+------------------------------------------------------------------------------ import Blackbox.App import qualified Blackbox.Tests+import Snap.Http.Server (simpleHttpServe) import qualified Snap.Snaplet.Internal.Lensed.Tests import qualified Snap.Snaplet.Internal.LensT.Tests import qualified Snap.Snaplet.Internal.RST.Tests import qualified Snap.Snaplet.Internal.Tests import Snap.TestCommon -import SafeCWD+import SafeCWD ------------------------------------------------------------------------------ main :: IO () main = do- Blackbox.Tests.remove "non-cabal-appdir/templates/bad.tpl"- Blackbox.Tests.remove "non-cabal-appdir/templates/good.tpl"+ Blackbox.Tests.remove+ "non-cabal-appdir/snaplets/heist/templates/bad.tpl"+ Blackbox.Tests.remove+ "non-cabal-appdir/snaplets/heist/templates/good.tpl" Blackbox.Tests.removeDir "non-cabal-appdir/snaplets/foosnaplet" - inDir False "non-cabal-appdir" startServer- threadDelay $ 2*10^(6::Int)- defaultMain tests+ (tid, mvar) <- inDir False "non-cabal-appdir" startServer+ defaultMain [tests] `finally` killThread tid - where tests = [ internalServerTests- , testDefault- , testBarebones- , testTutorial- ]+ putStrLn "waiting for termination mvar"+ takeMVar mvar + where tests = mutuallyExclusive $+ testGroup "snap" [ internalServerTests+ , testDefault+ , testBarebones+ , testTutorial+ ] ++------------------------------------------------------------------------------ internalServerTests :: Test internalServerTests =+ mutuallyExclusive $ testGroup "internal server tests" [ Blackbox.Tests.tests , Snap.Snaplet.Internal.Lensed.Tests.tests@@ -57,17 +68,33 @@ , Snap.Snaplet.Internal.Tests.tests ] -startServer :: IO ProcessID-startServer = forkProcess $ serve (setPort 9753 defaultConfig) app++------------------------------------------------------------------------------+startServer :: IO (ThreadId, MVar ())+startServer = do+ mvar <- newEmptyMVar+ t <- forkIO $ serve mvar (setPort 9753 defaultConfig) app+ threadDelay $ 2*10^(6::Int)+ return (t, mvar)+ where- serve config initializer = do- (_, handler, doCleanup) <- runSnaplet initializer- (conf, site) <- combineConfig config handler- _ <- try $ simpleHttpServe conf $ site- :: IO (Either SomeException ())- doCleanup+ serve mvar config initializer =+ flip finally (putMVar mvar ()) $+ handle handleErr $ do+ hPutStrLn stderr "initializing snaplet"+ (_, handler, doCleanup) <- runSnaplet initializer + flip finally doCleanup $ do+ (conf, site) <- combineConfig config handler+ hPutStrLn stderr "bringing up server"+ simpleHttpServe conf site+ hPutStrLn stderr "server killed" + handleErr :: SomeException -> IO ()+ handleErr e = hPutStrLn stderr $ "startServer exception: " ++ show e+++------------------------------------------------------------------------------ testBarebones :: Test testBarebones = testCase "snap/barebones" go where@@ -76,12 +103,13 @@ "" port testIt- port = 9990+ port = 9990 :: Int testIt = do body <- HTTP.simpleHttp $ "http://127.0.0.1:"++(show port) assertEqual "server not up" "hello world" body +------------------------------------------------------------------------------ testDefault :: Test testDefault = testCase "snap/default" go where@@ -90,7 +118,7 @@ "" port testIt- port = 9991+ port = 9991 :: Int testIt = do body <- liftM (S.concat . L.toChunks) $ HTTP.simpleHttp $ "http://127.0.0.1:"++(show port)@@ -98,6 +126,7 @@ $ "It works!" `S.isInfixOf` body +------------------------------------------------------------------------------ testTutorial :: Test testTutorial = testCase "snap/tutorial" go where@@ -106,7 +135,7 @@ "" port testIt- port = 9992+ port = 9992 :: Int testIt = do body <- HTTP.simpleHttp $ "http://127.0.0.1:"++(show port)++"/hello" assertEqual "server not up" "hello world" body