Spock 0.9.0.1 → 0.10.0.0
raw patch · 12 files changed
+449/−137 lines, 12 filesdep ~timePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: time
API changes (from Hackage documentation)
- Web.Spock.Internal.SessionVault: instance IsSession (Session conn sess st)
- Web.Spock.Internal.SessionVault: unSessionVault :: SessionVault s -> Map (SessionKey s) s
- Web.Spock.Internal.Util: instance Eq ClientPreferredFormat
- Web.Spock.Internal.Util: instance Show ClientPreferredFormat
- Web.Spock.Safe: instance (Monad m, Functor m) => Applicative (SpockCtxT ctx m)
- Web.Spock.Safe: instance Functor m => Functor (SpockCtxT ctx m)
- Web.Spock.Safe: instance Monad m => Monad (SpockCtxT ctx m)
- Web.Spock.Safe: instance MonadIO m => MonadIO (SpockCtxT ctx m)
- Web.Spock.Safe: instance MonadTrans (SpockCtxT ctx)
- Web.Spock.Shared: cb_createConn :: ConnBuilder a -> IO a
- Web.Spock.Shared: cb_destroyConn :: ConnBuilder a -> a -> IO ()
- Web.Spock.Shared: cb_poolConfiguration :: ConnBuilder a -> PoolCfg
- Web.Spock.Shared: pc_keepOpenTime :: PoolCfg -> NominalDiffTime
- Web.Spock.Shared: pc_resPerStripe :: PoolCfg -> Int
- Web.Spock.Shared: pc_stripes :: PoolCfg -> Int
- Web.Spock.Shared: sc_cookieName :: SessionCfg a -> Text
- Web.Spock.Shared: sc_emptySession :: SessionCfg a -> a
- Web.Spock.Shared: sc_hooks :: SessionCfg a -> SessionHooks a
- Web.Spock.Shared: sc_housekeepingInterval :: SessionCfg a -> NominalDiffTime
- Web.Spock.Shared: sc_persistCfg :: SessionCfg a -> Maybe (SessionPersistCfg a)
- Web.Spock.Shared: sc_sessionExpandTTL :: SessionCfg a -> Bool
- Web.Spock.Shared: sc_sessionIdEntropy :: SessionCfg a -> Int
- Web.Spock.Shared: sc_sessionTTL :: SessionCfg a -> NominalDiffTime
- Web.Spock.Shared: setCookie' :: MonadIO m => Text -> Text -> UTCTime -> ActionCtxT ctx m ()
- Web.Spock.Shared: sh_removed :: SessionHooks a -> HashMap SessionId a -> IO ()
- Web.Spock.Shared: spc_database :: SpockCfg conn sess st -> PoolOrConn conn
- Web.Spock.Shared: spc_initialState :: SpockCfg conn sess st -> st
- Web.Spock.Shared: spc_load :: SessionPersistCfg a -> IO [(SessionId, UTCTime, a)]
- Web.Spock.Shared: spc_maxRequestSize :: SpockCfg conn sess st -> Maybe Word64
- Web.Spock.Shared: spc_sessionCfg :: SpockCfg conn sess st -> SessionCfg sess
- Web.Spock.Shared: spc_store :: SessionPersistCfg a -> [(SessionId, UTCTime, a)] -> IO ()
- Web.Spock.Shared: uf_contentType :: UploadedFile -> !Text
- Web.Spock.Shared: uf_name :: UploadedFile -> !Text
- Web.Spock.Shared: uf_tempLocation :: UploadedFile -> !FilePath
- Web.Spock.Simple: instance (Monad m, Functor m) => Applicative (SpockT m)
- Web.Spock.Simple: instance Eq SpockRoute
- Web.Spock.Simple: instance Functor m => Functor (SpockT m)
- Web.Spock.Simple: instance IsString SpockRoute
- Web.Spock.Simple: instance Monad m => Monad (SpockT m)
- Web.Spock.Simple: instance MonadIO m => MonadIO (SpockT m)
- Web.Spock.Simple: instance MonadTrans SpockT
- Web.Spock.Simple: instance Ord SpockRoute
- Web.Spock.Simple: instance Read SpockRoute
- Web.Spock.Simple: instance Show SpockRoute
+ Web.Spock.Internal.Cookies: CookieSettings :: CookieEOL -> ByteString -> Maybe ByteString -> Bool -> Bool -> CookieSettings
+ Web.Spock.Internal.Cookies: CookieValidFor :: NominalDiffTime -> CookieEOL
+ Web.Spock.Internal.Cookies: CookieValidForSession :: CookieEOL
+ Web.Spock.Internal.Cookies: CookieValidUntil :: UTCTime -> CookieEOL
+ Web.Spock.Internal.Cookies: [cs_EOL] :: CookieSettings -> CookieEOL
+ Web.Spock.Internal.Cookies: [cs_HTTPOnly] :: CookieSettings -> Bool
+ Web.Spock.Internal.Cookies: [cs_domain] :: CookieSettings -> Maybe ByteString
+ Web.Spock.Internal.Cookies: [cs_path] :: CookieSettings -> ByteString
+ Web.Spock.Internal.Cookies: [cs_secure] :: CookieSettings -> Bool
+ Web.Spock.Internal.Cookies: data CookieEOL
+ Web.Spock.Internal.Cookies: data CookieSettings
+ Web.Spock.Internal.Cookies: defaultCookieSettings :: CookieSettings
+ Web.Spock.Internal.Cookies: generateCookieHeaderString :: Text -> Text -> CookieSettings -> UTCTime -> ByteString
+ Web.Spock.Internal.Cookies: parseCookies :: ByteString -> [(Text, Text)]
+ Web.Spock.Internal.SessionVault: [unSessionVault] :: SessionVault s -> Map (SessionKey s) s
+ Web.Spock.Internal.SessionVault: instance Web.Spock.Internal.SessionVault.IsSession (Web.Spock.Internal.Types.Session conn sess st)
+ Web.Spock.Internal.Util: instance GHC.Classes.Eq Web.Spock.Internal.Util.ClientPreferredFormat
+ Web.Spock.Internal.Util: instance GHC.Show.Show Web.Spock.Internal.Util.ClientPreferredFormat
+ Web.Spock.Safe: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Web.Spock.Safe.SpockCtxT ctx m)
+ Web.Spock.Safe: instance Control.Monad.Trans.Class.MonadTrans (Web.Spock.Safe.SpockCtxT ctx)
+ Web.Spock.Safe: instance GHC.Base.Functor m => GHC.Base.Functor (Web.Spock.Safe.SpockCtxT ctx m)
+ Web.Spock.Safe: instance GHC.Base.Monad m => GHC.Base.Applicative (Web.Spock.Safe.SpockCtxT ctx m)
+ Web.Spock.Safe: instance GHC.Base.Monad m => GHC.Base.Monad (Web.Spock.Safe.SpockCtxT ctx m)
+ Web.Spock.Shared: CookieSettings :: CookieEOL -> ByteString -> Maybe ByteString -> Bool -> Bool -> CookieSettings
+ Web.Spock.Shared: CookieValidFor :: NominalDiffTime -> CookieEOL
+ Web.Spock.Shared: CookieValidForSession :: CookieEOL
+ Web.Spock.Shared: CookieValidUntil :: UTCTime -> CookieEOL
+ Web.Spock.Shared: [cb_createConn] :: ConnBuilder a -> IO a
+ Web.Spock.Shared: [cb_destroyConn] :: ConnBuilder a -> a -> IO ()
+ Web.Spock.Shared: [cb_poolConfiguration] :: ConnBuilder a -> PoolCfg
+ Web.Spock.Shared: [cs_EOL] :: CookieSettings -> CookieEOL
+ Web.Spock.Shared: [cs_HTTPOnly] :: CookieSettings -> Bool
+ Web.Spock.Shared: [cs_domain] :: CookieSettings -> Maybe ByteString
+ Web.Spock.Shared: [cs_path] :: CookieSettings -> ByteString
+ Web.Spock.Shared: [cs_secure] :: CookieSettings -> Bool
+ Web.Spock.Shared: [pc_keepOpenTime] :: PoolCfg -> NominalDiffTime
+ Web.Spock.Shared: [pc_resPerStripe] :: PoolCfg -> Int
+ Web.Spock.Shared: [pc_stripes] :: PoolCfg -> Int
+ Web.Spock.Shared: [sc_cookieName] :: SessionCfg a -> Text
+ Web.Spock.Shared: [sc_emptySession] :: SessionCfg a -> a
+ Web.Spock.Shared: [sc_hooks] :: SessionCfg a -> SessionHooks a
+ Web.Spock.Shared: [sc_housekeepingInterval] :: SessionCfg a -> NominalDiffTime
+ Web.Spock.Shared: [sc_persistCfg] :: SessionCfg a -> Maybe (SessionPersistCfg a)
+ Web.Spock.Shared: [sc_sessionExpandTTL] :: SessionCfg a -> Bool
+ Web.Spock.Shared: [sc_sessionIdEntropy] :: SessionCfg a -> Int
+ Web.Spock.Shared: [sc_sessionTTL] :: SessionCfg a -> NominalDiffTime
+ Web.Spock.Shared: [sh_removed] :: SessionHooks a -> HashMap SessionId a -> IO ()
+ Web.Spock.Shared: [spc_database] :: SpockCfg conn sess st -> PoolOrConn conn
+ Web.Spock.Shared: [spc_initialState] :: SpockCfg conn sess st -> st
+ Web.Spock.Shared: [spc_load] :: SessionPersistCfg a -> IO [(SessionId, UTCTime, a)]
+ Web.Spock.Shared: [spc_maxRequestSize] :: SpockCfg conn sess st -> Maybe Word64
+ Web.Spock.Shared: [spc_sessionCfg] :: SpockCfg conn sess st -> SessionCfg sess
+ Web.Spock.Shared: [spc_store] :: SessionPersistCfg a -> [(SessionId, UTCTime, a)] -> IO ()
+ Web.Spock.Shared: [uf_contentType] :: UploadedFile -> !Text
+ Web.Spock.Shared: [uf_name] :: UploadedFile -> !Text
+ Web.Spock.Shared: [uf_tempLocation] :: UploadedFile -> !FilePath
+ Web.Spock.Shared: data CookieEOL
+ Web.Spock.Shared: data CookieSettings
+ Web.Spock.Shared: defaultCookieSettings :: CookieSettings
+ Web.Spock.Shared: runSpockNoBanner :: Port -> IO Middleware -> IO ()
+ Web.Spock.Shared: sessionRegenerateId :: SpockActionCtx ctx conn sess st ()
+ Web.Spock.Shared: withBasicAuthData :: MonadIO m => (Maybe (Text, Text) -> ActionCtxT ctx m a) -> ActionCtxT ctx m a
+ Web.Spock.Simple: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Web.Spock.Simple.SpockT m)
+ Web.Spock.Simple: instance Control.Monad.Trans.Class.MonadTrans Web.Spock.Simple.SpockT
+ Web.Spock.Simple: instance Data.String.IsString Web.Spock.Simple.SpockRoute
+ Web.Spock.Simple: instance GHC.Base.Functor m => GHC.Base.Functor (Web.Spock.Simple.SpockT m)
+ Web.Spock.Simple: instance GHC.Base.Monad m => GHC.Base.Applicative (Web.Spock.Simple.SpockT m)
+ Web.Spock.Simple: instance GHC.Base.Monad m => GHC.Base.Monad (Web.Spock.Simple.SpockT m)
+ Web.Spock.Simple: instance GHC.Classes.Eq Web.Spock.Simple.SpockRoute
+ Web.Spock.Simple: instance GHC.Classes.Ord Web.Spock.Simple.SpockRoute
+ Web.Spock.Simple: instance GHC.Read.Read Web.Spock.Simple.SpockRoute
+ Web.Spock.Simple: instance GHC.Show.Show Web.Spock.Simple.SpockRoute
- Web.Spock.Safe: spockT :: MonadIO m => (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
+ Web.Spock.Safe: spockT :: (MonadIO m) => (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
- Web.Spock.Safe: subcomponent :: Monad m => Path [] -> SpockCtxT ctx m () -> SpockCtxT ctx m ()
+ Web.Spock.Safe: subcomponent :: Monad m => Path '[] -> SpockCtxT ctx m () -> SpockCtxT ctx m ()
- Web.Spock.Shared: requireBasicAuth :: MonadIO m => Text -> (Text -> Text -> m Bool) -> ActionCtxT ctx m a -> ActionCtxT ctx m a
+ Web.Spock.Shared: requireBasicAuth :: MonadIO m => Text -> (Text -> Text -> ActionCtxT ctx m b) -> (b -> ActionCtxT ctx m a) -> ActionCtxT ctx m a
- Web.Spock.Shared: setCookie :: MonadIO m => Text -> Text -> NominalDiffTime -> ActionCtxT ctx m ()
+ Web.Spock.Shared: setCookie :: MonadIO m => Text -> Text -> CookieSettings -> ActionCtxT ctx m ()
- Web.Spock.Simple: spockLimT :: MonadIO m => Maybe Word64 -> (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
+ Web.Spock.Simple: spockLimT :: (MonadIO m) => Maybe Word64 -> (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
- Web.Spock.Simple: spockT :: MonadIO m => (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
+ Web.Spock.Simple: spockT :: (MonadIO m) => (forall a. m a -> IO a) -> SpockT m () -> IO Middleware
Files
- README.md +14/−13
- Spock.cabal +5/−1
- src/Web/Spock/Internal/Cookies.hs +126/−0
- src/Web/Spock/Internal/CoreAction.hs +75/−72
- src/Web/Spock/Internal/SessionManager.hs +50/−36
- src/Web/Spock/Internal/Types.hs +2/−1
- src/Web/Spock/Internal/Wire.hs +14/−2
- src/Web/Spock/Shared.hs +17/−5
- test/Web/Spock/FrameworkSpecHelper.hs +15/−1
- test/Web/Spock/Internal/CookiesSpec.hs +92/−0
- test/Web/Spock/SafeSpec.hs +26/−3
- test/Web/Spock/SimpleSpec.hs +13/−3
README.md view
@@ -50,18 +50,15 @@ * csrf-protection * typesafe contexts -Benchmarks:--* https://github.com/philopon/apiary-benchmark-* https://github.com/agrafix/Spock-scotty-benchmark- ## Important Links * [Tutorial](https://www.spock.li/tutorial/) * [Type-safe routing in Spock](https://www.spock.li/2015/04/19/type-safe_routing.html) +* [Taking Authentication to the next Level](https://www.spock.li/2015/08/23/taking_authentication_to_the_next_level.html) ### Talks +* English: [Beginning Web Programming in Haskell (using Spock)](https://www.youtube.com/watch?v=GobPiGL9jJ4) (by Ollie Charles) * German: [Moderne typsichere Web-Entwicklung mit Haskell](https://dl.dropboxusercontent.com/u/15078797/talks/typesafe-webdev-2015.pdf) (by Alexander Thiemann) * German: [reroute-talk](https://github.com/timjb/reroute-talk) (by Tim Baumann) @@ -81,20 +78,19 @@ * Blaze bootstrap helpers [blaze-bootstrap](http://hackage.haskell.org/package/blaze-bootstrap) * digestive-forms bootstrap helpers [digestive-bootstrap](http://hackage.haskell.org/package/digestive-bootstrap) +### Benchmarks++Please note that these benchmarks might not be up to date anymore.++* https://github.com/philopon/apiary-benchmark+* https://github.com/agrafix/Spock-scotty-benchmark+ ## Example Projects * [funblog](https://github.com/agrafix/funblog) * [makeci](https://github.com/openbrainsrc/makeci) * [curry-recipes](https://github.com/timjb/reroute-talk/tree/06574561918b50c1809f1e24ec7faeff731fddcf/curry-recipes) -## Companies / Projects using Spock--* http://cp-med.com-* http://openbrain.co.uk-* http://findmelike.com-* https://www.tramcloud.net-* http://thitp.de- ## Notes Since version 0.7.0.0 Spock supports typesafe routing. If you wish to continue using the untyped version of Spock you can Use `Web.Spock.Simple`. The implementation of the routing is implemented in a separate haskell package called `reroute`.@@ -107,6 +103,11 @@ * Tim Baumann [Github](https://github.com/timjb) (lot's of help with typesafe routing) * Tom Nielsen [Github](https://github.com/glutamate) (much feedback and small improvements)+* ... and all other awesome [contributors](https://github.com/agrafix/Spock/graphs/contributors)!++## Hacking++Pull requests are welcome! Please consider creating an issue beforehand, so we can discuss what you would like to do. Code should be written in a consistent style throughout the project. Avoid whitespace that is sensible to conflicts. (E.g. alignment of `=` signs in functions definitions) Note that by sending a pull request you agree that your contribution can be released under the BSD3 License as part of the `Spock` package or related packages. ## Misc
Spock.cabal view
@@ -1,5 +1,5 @@ name: Spock-version: 0.9.0.1+version: 0.10.0.0 synopsis: Another Haskell web framework for rapid development description: This toolbox provides everything you need to get a quick start into web hacking with haskell: .@@ -37,6 +37,7 @@ hs-source-dirs: src exposed-modules: Web.Spock,+ Web.Spock.Internal.Cookies, Web.Spock.Internal.Util, Web.Spock.Internal.SessionVault, Web.Spock.Safe,@@ -90,6 +91,7 @@ main-is: Spec.hs other-modules: Web.Spock.FrameworkSpecHelper,+ Web.Spock.Internal.CookiesSpec, Web.Spock.Internal.UtilSpec, Web.Spock.Internal.SessionVaultSpec, Web.Spock.SafeSpec,@@ -97,6 +99,7 @@ build-depends: base, bytestring,+ base64-bytestring >=1.0, hspec >= 2.0, hspec-wai >= 0.6, http-types,@@ -104,6 +107,7 @@ stm, reroute, text,+ time, unordered-containers, wai, wai-extra
+ src/Web/Spock/Internal/Cookies.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+module Web.Spock.Internal.Cookies+ ( CookieSettings(..)+ , defaultCookieSettings+ , CookieEOL(..)+ , generateCookieHeaderString+ , parseCookies+ )+where++import Data.Monoid ((<>))+import Data.Time+import qualified Data.ByteString.Char8 as BS+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Network.HTTP.Types.URI as URI (urlEncode, urlDecode)+#if MIN_VERSION_time(1,5,0)+#else+import System.Locale (defaultTimeLocale)+#endif++-- | Cookie settings+data CookieSettings+ = CookieSettings+ { cs_EOL :: CookieEOL+ -- ^ cookie expiration setting, see 'CookieEOL'+ , cs_path :: BS.ByteString+ -- ^ a path for the cookie+ , cs_domain :: Maybe BS.ByteString+ -- ^ a domain for the cookie. 'Nothing' means no domain is set+ , cs_HTTPOnly :: Bool+ -- ^ whether the cookie should be set as HttpOnly+ , cs_secure :: Bool+ -- ^ whether the cookie should be marked secure (sent over HTTPS only)+ }++-- | Setting cookie expiration+data CookieEOL+ = CookieValidUntil UTCTime+ -- ^ a point in time in UTC until the cookie is valid+ | CookieValidFor NominalDiffTime+ -- ^ a period (in seconds) for which the cookie is valid+ | CookieValidForSession+ -- ^ the cookie expires with the browser session++-- | Default cookie settings, equals+--+-- > CookieSettings+-- > { cs_EOL = CookieValidForSession+-- > , cs_HTTPOnly = False+-- > , cs_secure = False+-- > , cs_domain = Nothing+-- > , cs_path = "/"+-- > }+--+defaultCookieSettings :: CookieSettings+defaultCookieSettings =+ CookieSettings+ { cs_EOL = CookieValidForSession+ , cs_HTTPOnly = False+ , cs_secure = False+ , cs_domain = Nothing+ , cs_path = "/"+ }++generateCookieHeaderString ::+ T.Text+ -> T.Text+ -> CookieSettings+ -> UTCTime+ -> BS.ByteString+generateCookieHeaderString name value CookieSettings{..} now =+ BS.intercalate "; " $ filter (not . BS.null) fullCookie+ where+ fullCookie =+ [ nv+ , domain+ , path+ , maxAge+ , expires+ , httpOnly+ , secure+ ]+ nv = BS.concat [T.encodeUtf8 name, "=", urlEncode value]+ path = BS.concat ["path=", cs_path]+ domain =+ case cs_domain of+ Nothing -> BS.empty+ Just d -> BS.concat ["domain=", d]+ httpOnly = if cs_HTTPOnly then "HttpOnly" else BS.empty+ secure = if cs_secure then "Secure" else BS.empty++ maxAge =+ case cs_EOL of+ CookieValidForSession -> BS.empty+ CookieValidFor n -> "max-age=" <> maxAgeValue n+ CookieValidUntil t -> "max-age=" <> maxAgeValue (diffUTCTime t now)++ expires =+ case cs_EOL of+ CookieValidForSession -> BS.empty+ CookieValidFor n -> "expires=" <> expiresValue (addUTCTime n now)+ CookieValidUntil t -> "expires=" <> expiresValue t++ maxAgeValue :: NominalDiffTime -> BS.ByteString+ maxAgeValue nrOfSeconds =+ let v = round (max nrOfSeconds 0) :: Integer+ in BS.pack (show v)++ expiresValue :: UTCTime -> BS.ByteString+ expiresValue t =+ BS.pack $ formatTime defaultTimeLocale "%a, %d %b %Y %X %Z" t++ urlEncode :: T.Text -> BS.ByteString+ urlEncode = URI.urlEncode True . T.encodeUtf8++parseCookies :: BS.ByteString -> [(T.Text, T.Text)]+parseCookies =+ map parseCookie . BS.split ';'+ where+ parseCookie :: BS.ByteString -> (T.Text, T.Text)+ parseCookie cstr =+ let (name, urlEncValue) = BS.break (== '=') cstr+ in (T.decodeUtf8 name, T.decodeUtf8 . URI.urlDecode True . BS.drop 1 $ urlEncValue)
src/Web/Spock/Internal/CoreAction.hs view
@@ -2,21 +2,25 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE DoAndIfThenElse #-}+{-# LANGUAGE RecordWildCards #-} module Web.Spock.Internal.CoreAction ( ActionT , UploadedFile (..) , request, header, rawHeader, cookie, body, jsonBody, jsonBody' , reqMethod , files, params, param, param', setStatus, setHeader, redirect+ , setRawMultiHeader+ , CookieSettings(..), CookieEOL(..), defaultCookieSettings+ , setCookie, deleteCookie , jumpNext, middlewarePass, modifyVault, queryVault- , setCookie, setCookie', deleteCookie , bytes, lazyBytes, text, html, file, json, stream, response- , requireBasicAuth+ , requireBasicAuth, withBasicAuthData , getContext, runInContext , preferredFormat, ClientPreferredFormat(..) ) where +import Web.Spock.Internal.Cookies import Web.Spock.Internal.Util import Web.Spock.Internal.Wire @@ -31,16 +35,13 @@ import Control.Monad.RWS.Strict (runRWST) import Control.Monad.State hiding (get, put) import qualified Control.Monad.State as ST+import Data.Maybe import Data.Monoid import Data.Time import Network.HTTP.Types.Header (HeaderName, ResponseHeaders) import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Prelude hiding (head)-#if MIN_VERSION_time(1,5,0)-#else-import System.Locale (defaultTimeLocale)-#endif import Web.PathPieces import Web.Routing.AbstractRouter import qualified Data.Aeson as A@@ -71,15 +72,11 @@ liftM (lookup t . Wai.requestHeaders) request {-# INLINE rawHeader #-} --- | Read a cookie+-- | Read a cookie. The cookie value will already be urldecoded. cookie :: MonadIO m => T.Text -> ActionCtxT ctx m (Maybe T.Text) cookie name = do req <- request- return $ lookup "cookie" (Wai.requestHeaders req) >>= lookup name . parseCookies . T.decodeUtf8- where- parseCookies :: T.Text -> [(T.Text, T.Text)]- parseCookies = map parseCookie . T.splitOn ";" . T.concat . T.words- parseCookie = first T.init . T.breakOnEnd "="+ return $ lookup "cookie" (Wai.requestHeaders req) >>= lookup name . parseCookies {-# INLINE cookie #-} -- | Tries to dected the preferred format of the response using the Accept header@@ -117,13 +114,13 @@ return $ A.decodeStrict b {-# INLINE jsonBody #-} --- | Parse the request body as json and fails with 500 status code on error+-- | Parse the request body as json and fails with 400 status code on error jsonBody' :: (MonadIO m, A.FromJSON a) => ActionCtxT ctx m a jsonBody' = do b <- body case A.eitherDecodeStrict' b of Left err ->- do setStatus status500+ do setStatus status400 text (T.pack $ "Failed to parse json: " ++ err) Right val -> return val@@ -183,36 +180,44 @@ -- be appended. Otherwise the previous value is overwritten. -- See 'setMultiHeader'. setHeader :: MonadIO m => T.Text -> T.Text -> ActionCtxT ctx m ()-setHeader k v =- do let ciVal = CI.mk $ T.encodeUtf8 k- case HM.lookup ciVal multiHeaderMap of+setHeader k v = setRawHeader (CI.mk $ T.encodeUtf8 k) (T.encodeUtf8 v)+{-# INLINE setHeader #-}++setRawHeader :: MonadIO m => CI.CI BS.ByteString -> BS.ByteString -> ActionCtxT ctx m ()+setRawHeader k v =+ do case HM.lookup k multiHeaderMap of Just mhk ->- setMultiHeader mhk v+ setRawMultiHeader mhk v Nothing ->- setHeaderUnsafe k v-{-# INLINE setHeader #-}+ setRawHeaderUnsafe k v -- | INTERNAL: Set a response header that can occur multiple times. (eg: Cache-Control) setMultiHeader :: MonadIO m => MultiHeader -> T.Text -> ActionCtxT ctx m ()-setMultiHeader k v =+setMultiHeader k v = setRawMultiHeader k (T.encodeUtf8 v)+{-# INLINE setMultiHeader #-}++setRawMultiHeader :: MonadIO m => MultiHeader -> BS.ByteString -> ActionCtxT ctx m ()+setRawMultiHeader k v = modify $ \rs -> rs { rs_multiResponseHeaders =- HM.insertWith (++) k [T.encodeUtf8 v] (rs_multiResponseHeaders rs)+ HM.insertWith (++) k [v] (rs_multiResponseHeaders rs) }-{-# INLINE setMultiHeader #-} -- | INTERNAL: Unsafely set a header (no checking if the header can occur multiple times) setHeaderUnsafe :: MonadIO m => T.Text -> T.Text -> ActionCtxT ctx m ()-setHeaderUnsafe k v =+setHeaderUnsafe k v = setRawHeaderUnsafe (CI.mk $ T.encodeUtf8 k) (T.encodeUtf8 v)+{-# INLINE setHeaderUnsafe #-}++-- | INTERNAL: Unsafely set a header (no checking if the header can occur multiple times)+setRawHeaderUnsafe :: MonadIO m => CI.CI BS.ByteString -> BS.ByteString -> ActionCtxT ctx m ()+setRawHeaderUnsafe k v = modify $ \rs -> rs { rs_responseHeaders =- HM.insert (CI.mk $ T.encodeUtf8 k) (T.encodeUtf8 v) (rs_responseHeaders rs)+ HM.insert k v (rs_responseHeaders rs) }-{-# INLINE setHeaderUnsafe #-} - -- | Abort the current action and jump the next one matching the route jumpNext :: MonadIO m => ActionCtxT ctx m a jumpNext = throwError ActionTryNext@@ -245,36 +250,6 @@ liftIO $ vi_lookupKey vaultIf k {-# INLINE queryVault #-} --- | Set a cookie living for a given number of seconds-setCookie :: MonadIO m => T.Text -> T.Text -> NominalDiffTime -> ActionCtxT ctx m ()-setCookie name value validSeconds =- do now <- liftIO getCurrentTime- setCookie' name value (validSeconds `addUTCTime` now)-{-# INLINE setCookie #-}--deleteCookie :: MonadIO m => T.Text -> ActionCtxT ctx m ()-deleteCookie name = setCookie' name T.empty epoch- where- epoch = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)-{-# INLINE deleteCookie #-}---- | Set a cookie living until a specific 'UTCTime'-setCookie' :: MonadIO m => T.Text -> T.Text -> UTCTime -> ActionCtxT ctx m ()-setCookie' name value validUntil =- setMultiHeader MultiHeaderSetCookie rendered- where- rendered =- let formattedTime =- T.pack $ formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" validUntil- in T.concat [ name- , "="- , value- , "; path=/; expires="- , formattedTime- , ";"- ]-{-# INLINE setCookie' #-}- -- | Use a custom 'Wai.Response' generator as response body. response :: MonadIO m => (Status -> ResponseHeaders -> Wai.Response) -> ActionCtxT ctx m a response val =@@ -328,35 +303,47 @@ response $ \status headers -> Wai.responseStream status headers val {-# INLINE stream #-} --- | Basic authentification+-- | Convenience Basic authentification -- provide a title for the prompt and a function to validate -- user and password. Usage example: ----- > get "/my-secret-page" $--- > requireBasicAuth "Secret Page" (\user pass -> return (user == "admin" && pass == "1234")) $--- > do html "This is top secret content. Login using that secret code I provided ;-)"+-- > get ("auth" <//> var <//> var) $ \user pass ->+-- > let checker user' pass' =+-- > unless (user == user' && pass == pass') $+-- > do setStatus status401+-- > text "err"+-- > in requireBasicAuth "Foo" checker $ \() -> text "ok" ---requireBasicAuth :: MonadIO m => T.Text -> (T.Text -> T.Text -> m Bool) -> ActionCtxT ctx m a -> ActionCtxT ctx m a+requireBasicAuth :: MonadIO m => T.Text -> (T.Text -> T.Text -> ActionCtxT ctx m b) -> (b -> ActionCtxT ctx m a) -> ActionCtxT ctx m a requireBasicAuth realmTitle authFun cont =+ withBasicAuthData $ \mAuthHeader ->+ case mAuthHeader of+ Nothing ->+ authFailed Nothing+ Just (user, pass) ->+ authFun user pass >>= cont+ where+ authFailed mMore =+ do setStatus status401+ setMultiHeader MultiHeaderWWWAuth ("Basic realm=\"" <> realmTitle <> "\"")+ text $ "Authentication required. " <> fromMaybe "" mMore++-- | "Lower level" basic authentification handeling. Does not set any headers that will promt+-- browser users, only looks for an "Authorization" header in the request and breaks it into+-- username and passwort component if present+withBasicAuthData :: MonadIO m => (Maybe (T.Text, T.Text) -> ActionCtxT ctx m a) -> ActionCtxT ctx m a+withBasicAuthData handler = do mAuthHeader <- header "Authorization" case mAuthHeader of Nothing ->- authFailed+ handler Nothing Just authHeader -> let (_, rawValue) = T.breakOn " " authHeader (user, rawPass) = (T.breakOn ":" . T.decodeUtf8 . B64.decodeLenient . T.encodeUtf8 . T.strip) rawValue pass = T.drop 1 rawPass- in do isOk <- lift $ authFun user pass- if isOk- then cont- else authFailed- where- authFailed =- do setStatus status401- setMultiHeader MultiHeaderWWWAuth ("Basic realm=\"" <> realmTitle <> "\"")- html "<h1>Authentication required.</h1>"+ in handler (Just (user, pass)) -- | Get the context of the current request getContext :: MonadIO m => ActionCtxT ctx m ctx@@ -381,3 +368,19 @@ throwError interupt Right d -> return d {-# INLINE runInContext #-}++-- | Set a cookie. The cookie value will be urlencoded.+setCookie :: MonadIO m => T.Text -> T.Text -> CookieSettings -> ActionCtxT ctx m ()+setCookie name value cs =+ do now <- liftIO getCurrentTime+ let cookieHeaderString = generateCookieHeaderString name value cs now+ setRawMultiHeader MultiHeaderSetCookie cookieHeaderString+{-# INLINE setCookie #-}++-- | Delete a cookie+deleteCookie :: MonadIO m => T.Text -> ActionCtxT ctx m ()+deleteCookie name = setCookie name T.empty cs+ where+ cs = defaultCookieSettings { cs_EOL = CookieValidUntil epoch }+ epoch = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)+{-# INLINE deleteCookie #-}
src/Web/Spock/Internal/SessionManager.hs view
@@ -8,10 +8,11 @@ import Web.Spock.Internal.Types import Web.Spock.Internal.CoreAction+import Web.Spock.Internal.Wire import Web.Spock.Internal.Util+import Web.Spock.Internal.Cookies import qualified Web.Spock.Internal.SessionVault as SV -import Control.Arrow (first) #if MIN_VERSION_base(4,8,0) #else import Control.Applicative@@ -27,13 +28,11 @@ import System.Locale (defaultTimeLocale) #endif import qualified Crypto.Random as CR+import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64-import qualified Data.ByteString.Lazy as BSL import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Data.Text.Encoding as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Vault.Lazy as V import qualified Network.Wai as Wai @@ -55,6 +54,7 @@ return SessionManager { sm_getSessionId = getSessionIdImpl vaultKey cacheHM+ , sm_regenerateSessionId = regenerateSessionIdImpl vaultKey cacheHM pool cfg , sm_readSession = readSessionImpl vaultKey cacheHM , sm_writeSession = writeSessionImpl vaultKey cacheHM , sm_modifySession = modifySessionImpl vaultKey cacheHM@@ -88,6 +88,20 @@ , sess_safeActions = SafeActionStore HM.empty HM.empty } +regenerateSessionIdImpl ::+ V.Key SessionId+ -> SV.SessionVault (Session conn sess st)+ -> CR.EntropyPool+ -> SessionCfg sess+ -> SpockActionCtx ctx conn sess st ()+regenerateSessionIdImpl vK sessionRef entropyPool cfg =+ do sess <- readSessionBase vK sessionRef+ liftIO $ deleteSessionImpl sessionRef (sess_id sess)+ newSession <- liftIO $ newSessionImpl entropyPool cfg sessionRef (sess_data sess)+ now <- liftIO getCurrentTime+ setRawMultiHeader MultiHeaderSetCookie $ makeSessionIdCookie cfg newSession now+ modifyVault $ V.insert vK (sess_id newSession)+ getSessionIdImpl :: V.Key SessionId -> SV.SessionVault (Session conn sess st) -> SpockActionCtx ctx conn sess st SessionId@@ -100,8 +114,8 @@ -> (Session conn sess st -> (Session conn sess st, a)) -> SpockActionCtx ctx conn sess st a modifySessionBase vK sessionRef modFun =- do req <- request- case V.lookup vK (Wai.vault req) of+ do mValue <- queryVault vK+ case mValue of Nothing -> error "(3) Internal Spock Session Error. Please report this bug!" Just sid ->@@ -119,8 +133,8 @@ -> SV.SessionVault (Session conn sess st) -> SpockActionCtx ctx conn sess st (Session conn sess st) readSessionBase vK sessionRef =- do req <- request- case V.lookup vK (Wai.vault req) of+ do mValue <- queryVault vK+ case mValue of Nothing -> error "(1) Internal Spock Session Error. Please report this bug!" Just sid ->@@ -160,10 +174,11 @@ do base <- readSessionBase vaultKey sessionMapVar return $ HM.lookup hash (sas_forward (sess_safeActions base)) -removeSafeActionImpl :: V.Key SessionId- -> SV.SessionVault (Session conn sess st)- -> PackedSafeAction conn sess st- -> SpockActionCtx ctx conn sess st ()+removeSafeActionImpl ::+ V.Key SessionId+ -> SV.SessionVault (Session conn sess st)+ -> PackedSafeAction conn sess st+ -> SpockActionCtx ctx conn sess st () removeSafeActionImpl vaultKey sessionMapVar action = modifySessionBase vaultKey sessionMapVar (\s -> (s { sess_safeActions = f (sess_safeActions s ) }, ())) where@@ -200,6 +215,18 @@ in (session { sess_data = sessData' }, out) modifySessionBase vK sessionRef modFun +makeSessionIdCookie :: SessionCfg sess -> Session conn sess st -> UTCTime -> BS.ByteString+makeSessionIdCookie cfg sess now =+ generateCookieHeaderString name value settings now+ where+ name = sc_cookieName cfg+ value = sess_id sess+ settings =+ defaultCookieSettings+ { cs_EOL = CookieValidUntil (sess_validUntil sess)+ , cs_HTTPOnly = True+ }+ sessionMiddleware :: CR.EntropyPool -> SessionCfg sess@@ -220,34 +247,20 @@ where getCookieFromReq name = lookup "cookie" (Wai.requestHeaders req) >>=- lookup name . parseCookies . T.decodeUtf8- renderCookie name value validUntil =- let formattedTime =- TL.pack $ formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" validUntil- in TL.concat [ TL.fromStrict name- , "="- , TL.fromStrict value- , "; path=/; expires="- , formattedTime- , ";"- ]- parseCookies :: T.Text -> [(T.Text, T.Text)]- parseCookies = map parseCookie . T.splitOn ";" . T.concat . T.words- parseCookie = first T.init . T.breakOnEnd "="-+ lookup name . parseCookies defVal = sc_emptySession cfg v = Wai.vault req- addCookie sess responseHeaders =- let cookieContent =- renderCookie (sc_cookieName cfg) (sess_id sess) (sess_validUntil sess)- cookieC = ("Set-Cookie", BSL.toStrict $ TL.encodeUtf8 cookieContent)+ addCookie sess now responseHeaders =+ let cookieContent = makeSessionIdCookie cfg sess now+ cookieC = ("Set-Cookie", cookieContent) in (cookieC : responseHeaders) withSess shouldSetCookie sess = app (req { Wai.vault = V.insert vK (sess_id sess) v }) $ \unwrappedResp ->- respond $- if shouldSetCookie- then mapReqHeaders (addCookie sess) unwrappedResp- else unwrappedResp+ do now <- getCurrentTime+ respond $+ if shouldSetCookie+ then mapReqHeaders (addCookie sess now) unwrappedResp+ else unwrappedResp mkNew = do newSess <- newSessionImpl pool cfg sessionRef defVal withSess True newSess@@ -340,4 +353,5 @@ randomHash pool len = do let sys :: CR.SystemRNG sys = CR.cprgCreate pool- return $ T.replace "=" "" $ T.decodeUtf8 $ B64.encode $ fst $ CR.cprgGenerateWithEntropy len sys+ return $ T.replace "=" "" $ T.replace "/" "_" $ T.replace "+" "-" $+ T.decodeUtf8 $ B64.encode $ fst $ CR.cprgGenerateWithEntropy len sys
src/Web/Spock/Internal/Types.hs view
@@ -41,7 +41,7 @@ -- | The 'SpockAction' is a specialisation of 'SpockActionCtx' with a '()' context. type SpockAction conn sess st = SpockActionCtx () conn sess st --- | Spock configuration+-- | Spock configuration, use 'defaultSpockCfg' and change single values if needed data SpockCfg conn sess st = SpockCfg { spc_initialState :: st@@ -218,6 +218,7 @@ data SessionManager conn sess st = SessionManager { sm_getSessionId :: forall ctx. SpockActionCtx ctx conn sess st SessionId+ , sm_regenerateSessionId :: forall ctx. SpockActionCtx ctx conn sess st () , sm_readSession :: forall ctx. SpockActionCtx ctx conn sess st sess , sm_writeSession :: forall ctx. sess -> SpockActionCtx ctx conn sess st () , sm_modifySession :: forall a ctx. (sess -> (sess, a)) -> SpockActionCtx ctx conn sess st a
src/Web/Spock/Internal/Wire.hs view
@@ -206,6 +206,19 @@ ] } +defResponse :: ResponseState+defResponse =+ ResponseState+ { rs_responseHeaders =+ HM.empty+ , rs_multiResponseHeaders =+ HM.empty+ , rs_status = status200+ , rs_responseBody = ResponseBody $ \status headers ->+ Wai.responseLBS status headers $+ BSL.empty+ }+ notFound :: Wai.Response notFound = respStateToResponse $ errorResponse status404 "404 - File not found"@@ -283,9 +296,8 @@ return $ Just $ errorResponse status404 "404 - File not found" applyAction req mkEnv ((captures, selectedAction) : xs) = do let env = mkEnv captures- defResp = errorResponse status200 "" (r, respState, _) <-- runRWST (runErrorT $ runActionCtxT selectedAction) env defResp+ runRWST (runErrorT $ runActionCtxT selectedAction) env defResponse case r of Left (ActionRedirect loc) -> return $ Just $
@@ -7,7 +7,7 @@ {-# LANGUAGE ScopedTypeVariables #-} module Web.Spock.Shared (-- * Helpers for running Spock- runSpock, spockAsApp+ runSpock, runSpockNoBanner, spockAsApp -- * Action types , SpockAction, SpockActionCtx, ActionT, W.ActionCtxT -- * Handling requests@@ -19,7 +19,7 @@ -- * Working with context , getContext, runInContext -- * Sending responses- , setStatus, setHeader, redirect, jumpNext, setCookie, setCookie', deleteCookie, bytes, lazyBytes+ , setStatus, setHeader, redirect, jumpNext, CookieSettings(..), defaultCookieSettings, CookieEOL(..), setCookie, deleteCookie, bytes, lazyBytes , text, html, file, json, stream, response -- * Middleware helpers , middlewarePass, modifyVault, queryVault@@ -30,13 +30,13 @@ -- * Accessing Database and State , HasSpock (runQuery, getState), SpockConn, SpockState, SpockSession -- * Basic HTTP-Auth- , requireBasicAuth+ , requireBasicAuth, withBasicAuthData -- * Sessions , defaultSessionCfg, SessionCfg (..) , defaultSessionHooks, SessionHooks (..) , SessionPersistCfg(..), readShowSessionPersist , SessionId- , getSessionId, readSession, writeSession+ , sessionRegenerateId, getSessionId, readSession, writeSession , modifySession, modifySession', modifyReadSession, mapAllSessions, clearAllSessions -- * Internals for extending Spock , getSpockHeart, runSpockIO, WebStateM, WebState@@ -54,17 +54,29 @@ import qualified Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp --- | Run a Spock application. Basically just a wrapper aroung @Warp.run@.+-- | Run a Spock application. Basically just a wrapper aroung 'Warp.run'. runSpock :: Warp.Port -> IO Wai.Middleware -> IO () runSpock port mw = do putStrLn ("Spock is running on port " ++ show port) app <- spockAsApp mw Warp.run port app +-- | Like 'runSpock', but does not display the banner "Spock is running on port XXX" on stdout.+runSpockNoBanner :: Warp.Port -> IO Wai.Middleware -> IO ()+runSpockNoBanner port mw =+ do app <- spockAsApp mw+ Warp.run port app+ -- | Convert a middleware to an application. All failing requests will -- result in a 404 page spockAsApp :: IO Wai.Middleware -> IO Wai.Application spockAsApp = liftM W.middlewareToApp++-- | Regenerate the users sessionId. This preserves all stored data. Call this prior+-- to logging in a user to prevent session fixation attacks.+sessionRegenerateId :: SpockActionCtx ctx conn sess st ()+sessionRegenerateId =+ getSessMgr >>= sm_regenerateSessionId -- | Get the current users sessionId. Note that this ID should only be -- shown to it's owner as otherwise sessions can be hijacked.
test/Web/Spock/FrameworkSpecHelper.hs view
@@ -6,6 +6,10 @@ import Data.Monoid import Data.Word+import Network.HTTP.Types.Header+import Network.HTTP.Types.Method+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as BSLC import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -80,7 +84,17 @@ actionSpec :: SpecWith Wai.Application actionSpec =- describe "Action Framework" $ return ()+ describe "Action Framework" $+ do it "handles auth correctly" $+ do request methodGet "/auth/user/pass" [mkAuthHeader "user" "pass"] "" `shouldRespondWith` "ok" { matchStatus = 200 }+ request methodGet "/auth/user/pass" [mkAuthHeader "user" ""] "" `shouldRespondWith` "err" { matchStatus = 401 }+ request methodGet "/auth/user/pass" [mkAuthHeader "" ""] "" `shouldRespondWith` "err" { matchStatus = 401 }+ request methodGet "/auth/user/pass" [mkAuthHeader "asd" "asd"] "" `shouldRespondWith` "err" { matchStatus = 401 }+ request methodGet "/auth/user/pass" [] "" `shouldRespondWith` "Authentication required. " { matchStatus = 401 }+ where+ mkAuthHeader :: BS.ByteString -> BS.ByteString -> Header+ mkAuthHeader user pass =+ ("Authorization", "Basic " <> (B64.encode $ user <> ":" <> pass)) cookieTest :: SpecWith Wai.Application cookieTest =
+ test/Web/Spock/Internal/CookiesSpec.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.Spock.Internal.CookiesSpec (spec) where++import Web.Spock.Internal.Cookies++import Data.Time+import Test.Hspec+import qualified Data.ByteString as BS++spec :: Spec+spec =+ do describe "Generating Cookies" $+ do describe "with the default settings" $+ do let generated = g "foo" "bar" def++ it "should generate the name-value pair" $+ generated `shouldContainOnce` "foo=bar"++ it "should not generate a max-age key" $+ generated `shouldNotContain'` "max-age="++ it "should not generate an expires key" $+ generated `shouldNotContain'` "expires="++ it "should generate a root path" $+ generated `shouldContainOnce` "path=/"++ it "should not generate a domain pair" $+ generated `shouldNotContain'` "domain="++ it "should not generate a httponly key" $+ generated `shouldNotContain'` "HttpOnly"++ it "should not generate a secure key" $+ generated `shouldNotContain'` "Secure"++ describe "when setting an expiration time in the future" $+ do let generated = g "foo" "bar" def { cs_EOL = CookieValidUntil (UTCTime (fromGregorian 2016 1 1) 0) }++ it "should set the correct expires key" $+ generated `shouldContainOnce` "expires=Fri, 01 Jan 2016 00:00:00 UTC"++ it "should set the correct max-age key" $+ generated `shouldContainOnce` "max-age=10465200"++ describe "when setting an expiration time in the past" $+ do let generated = g "foo" "bar" def { cs_EOL = CookieValidUntil (UTCTime (fromGregorian 1970 1 1) 0) }++ it "should set the correct expires key" $+ generated `shouldContainOnce` "expires=Thu, 01 Jan 1970 00:00:00 UTC"++ it "should set the max-age key to 0" $+ generated `shouldContainOnce` "max-age=0"++ describe "when setting the path" $+ it "should generate the correct path pair" $+ g "foo" "bar" def { cs_path = "/the-path" } `shouldContainOnce` "path=/the-path"++ describe "when setting the domain" $+ it "should generate the correct domain pair" $+ g "foo" "bar" def { cs_domain = Just "example.org" } `shouldContainOnce` "domain=example.org"++ describe "when setting the httponly option" $+ it "should generate the httponly key" $+ g "foo" "bar" def { cs_HTTPOnly = True } `shouldContainOnce` "HttpOnly"++ describe "when setting the secure option" $+ it "should generate the secure key" $+ g "foo" "bar" def { cs_secure = True } `shouldContainOnce` "Secure"++ describe "cookie value" $+ it "should be urlencoded" $+ g "foo" "most+special chars;%бисквитки" def `shouldContainOnce`+ "foo=most%2Bspecial%20chars%3B%25%D0%B1%D0%B8%D1%81%D0%BA%D0%B2%D0%B8%D1%82%D0%BA%D0%B8"++ describe "Parsing cookies" $+ do it "should parse urlencoded multiple cookies" $+ parseCookies "foo=bar;quux=h&m" `shouldBe` [("foo", "bar"), ("quux", "h&m")]++ it "should parse urlencoded values" $+ parseCookies "foo=most%2Bspecial%20chars%3B%25" `shouldBe` [("foo", "most+special chars;%")]++ it "should parse urlencoded utf-8 content" $+ parseCookies "foo=%D0%B1%D0%B8%D1%81%D0%BA%D0%B2%D0%B8%D1%82%D0%BA%D0%B8" `shouldBe` [("foo", "бисквитки")]+ where+ g n v cs = generateCookieHeaderString n v cs t+ def = defaultCookieSettings+ t = UTCTime (fromGregorian 2015 9 1) (21*60*60)+ shouldContainOnce haystack needle =+ snd (BS.breakSubstring needle haystack) `shouldNotBe` BS.empty+ shouldNotContain' haystack needle =+ snd (BS.breakSubstring needle haystack) `shouldBe` BS.empty
test/Web/Spock/SafeSpec.hs view
@@ -11,6 +11,7 @@ import Data.IORef import Data.List (find) import Data.Monoid+import Network.HTTP.Types.Status import Test.Hspec import qualified Data.HashSet as HS import qualified Data.Text as T@@ -34,11 +35,11 @@ get ("param-test" <//> "static") $ text "static" get ("cookie" <//> "single") $- do setCookie "single" "test" 3600+ do setCookie "single" "test" defaultCookieSettings { cs_EOL = CookieValidFor 3600 } text "set" get ("cookie" <//> "multiple") $- do setCookie "multiple1" "test1" 3600- setCookie "multiple2" "test2" 3600+ do setCookie "multiple1" "test1" defaultCookieSettings { cs_EOL = CookieValidFor 3600 }+ setCookie "multiple2" "test2" defaultCookieSettings { cs_EOL = CookieValidFor 3600 } text "set" get "set-header" $ do setHeader "X-FooBar" "Baz"@@ -56,6 +57,12 @@ case fmt of PrefHTML -> text "html" x -> text (T.pack (show x))+ get ("auth" <//> var <//> var) $ \user pass ->+ let checker user' pass' =+ unless (user == user' && pass == pass') $+ do setStatus status401+ text "err"+ in requireBasicAuth "Foo" checker $ \() -> text "ok" hookAny GET $ text . T.intercalate "/" routeRenderingSpec :: Spec@@ -114,12 +121,28 @@ Just sessCookie -> Test.request "GET" "/check" [("Cookie", T.encodeUtf8 sessCookie)] "" `Test.shouldRespondWith` "5"+ it "should regenerate and preserve all content" $+ do res <- Test.get "/set/5"+ case getSessCookie res of+ Nothing ->+ Test.liftIO $ expectationFailure "Missing spockcookie"+ Just sessCookie ->+ do res2 <- Test.request "GET" "/regenerate" [("Cookie", T.encodeUtf8 sessCookie)] ""+ case getSessCookie res2 of+ Nothing ->+ Test.liftIO $ expectationFailure "Missing new spockcookie"+ Just sessCookie2 ->+ do Test.request "GET" "/check" [("Cookie", T.encodeUtf8 sessCookie2)] ""+ `Test.shouldRespondWith` "5"+ Test.request "GET" "/check" [("Cookie", T.encodeUtf8 sessCookie)] ""+ `Test.shouldRespondWith` "0" where sessionApp = spockAsApp $ spock spockCfg $ do get "test" $ text "text" get ("set" <//> var) $ \number -> writeSession number >> text "done"+ get "regenerate" $ sessionRegenerateId >> text "done" get "check" $ do val <- readSession text (T.pack $ show val)
test/Web/Spock/SimpleSpec.hs view
@@ -5,7 +5,9 @@ import Web.Spock.Simple import Web.Spock.FrameworkSpecHelper +import Control.Monad import Data.Monoid+import Network.HTTP.Types.Status import Test.Hspec import qualified Data.Text as T @@ -35,11 +37,11 @@ PrefHTML -> text "html" x -> text (T.pack (show x)) get "/cookie/single" $- do setCookie "single" "test" 3600+ do setCookie "single" "test" defaultCookieSettings { cs_EOL = CookieValidFor 3600 } text "set" get "/cookie/multiple" $- do setCookie "multiple1" "test1" 3600- setCookie "multiple2" "test2" 3600+ do setCookie "multiple1" "test1" defaultCookieSettings { cs_EOL = CookieValidFor 3600 }+ setCookie "multiple2" "test2" defaultCookieSettings { cs_EOL = CookieValidFor 3600 } text "set" get "set-header" $ do setHeader "X-FooBar" "Baz"@@ -48,6 +50,14 @@ do setHeader "Content-Language" "de" setHeader "Content-Language" "en" text "ok"+ get ("/auth/:user/:pass") $+ do user <- param' "user"+ pass <- param' "pass"+ let checker user' pass' =+ unless (user == user' && pass == pass') $+ do setStatus status401+ text "err"+ requireBasicAuth "Foo" checker $ \() -> text "ok" hookAny GET $ text . T.intercalate "/" spec :: Spec