More efficient token generation
Old code would create a new System.Random generator each time, which requires going to the system entropy store. New code caches an mwc-random gen at startup.
This commit is contained in:
parent
658f31e0d7
commit
972e117005
@ -68,6 +68,7 @@ import Control.Monad.Logger
|
|||||||
import Control.Monad (when)
|
import Control.Monad (when)
|
||||||
import qualified Paths_yesod_core
|
import qualified Paths_yesod_core
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
import qualified System.Random.MWC as MWC
|
||||||
|
|
||||||
-- | Convert the given argument into a WAI application, executable with any WAI
|
-- | Convert the given argument into a WAI application, executable with any WAI
|
||||||
-- handler. This function will provide no middlewares; if you want commonly
|
-- handler. This function will provide no middlewares; if you want commonly
|
||||||
@ -76,10 +77,12 @@ toWaiAppPlain :: YesodDispatch site => site -> IO W.Application
|
|||||||
toWaiAppPlain site = do
|
toWaiAppPlain site = do
|
||||||
logger <- makeLogger site
|
logger <- makeLogger site
|
||||||
sb <- makeSessionBackend site
|
sb <- makeSessionBackend site
|
||||||
|
gen <- MWC.createSystemRandom
|
||||||
return $ toWaiAppYre $ YesodRunnerEnv
|
return $ toWaiAppYre $ YesodRunnerEnv
|
||||||
{ yreLogger = logger
|
{ yreLogger = logger
|
||||||
, yreSite = site
|
, yreSite = site
|
||||||
, yreSessionBackend = sb
|
, yreSessionBackend = sb
|
||||||
|
, yreGen = gen
|
||||||
}
|
}
|
||||||
|
|
||||||
toWaiAppYre :: YesodDispatch site => YesodRunnerEnv site -> W.Application
|
toWaiAppYre :: YesodDispatch site => YesodRunnerEnv site -> W.Application
|
||||||
@ -125,10 +128,12 @@ toWaiApp site = do
|
|||||||
toWaiAppLogger :: YesodDispatch site => Logger -> site -> IO W.Application
|
toWaiAppLogger :: YesodDispatch site => Logger -> site -> IO W.Application
|
||||||
toWaiAppLogger logger site = do
|
toWaiAppLogger logger site = do
|
||||||
sb <- makeSessionBackend site
|
sb <- makeSessionBackend site
|
||||||
|
gen <- MWC.createSystemRandom
|
||||||
let yre = YesodRunnerEnv
|
let yre = YesodRunnerEnv
|
||||||
{ yreLogger = logger
|
{ yreLogger = logger
|
||||||
, yreSite = site
|
, yreSite = site
|
||||||
, yreSessionBackend = sb
|
, yreSessionBackend = sb
|
||||||
|
, yreGen = gen
|
||||||
}
|
}
|
||||||
messageLoggerSource
|
messageLoggerSource
|
||||||
site
|
site
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import Data.String (IsString)
|
|||||||
import Control.Arrow (second)
|
import Control.Arrow (second)
|
||||||
import qualified Network.Wai.Parse as NWP
|
import qualified Network.Wai.Parse as NWP
|
||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import System.Random (RandomGen, randomRs)
|
|
||||||
import Web.Cookie (parseCookiesText)
|
import Web.Cookie (parseCookiesText)
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
@ -33,7 +32,7 @@ import Data.Maybe (fromMaybe, catMaybes)
|
|||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeUtf8With)
|
import Data.Text.Encoding (decodeUtf8With, decodeUtf8)
|
||||||
import Data.Text.Encoding.Error (lenientDecode)
|
import Data.Text.Encoding.Error (lenientDecode)
|
||||||
import Data.Conduit
|
import Data.Conduit
|
||||||
import Data.Conduit.List (sourceList)
|
import Data.Conduit.List (sourceList)
|
||||||
@ -41,9 +40,16 @@ import Data.Conduit.Binary (sourceFile, sinkFile)
|
|||||||
import Data.Word (Word64)
|
import Data.Word (Word64)
|
||||||
import Control.Monad.Trans.Resource (runResourceT, ResourceT)
|
import Control.Monad.Trans.Resource (runResourceT, ResourceT)
|
||||||
import Control.Exception (throwIO)
|
import Control.Exception (throwIO)
|
||||||
|
import Control.Monad ((<=<), liftM)
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
|
import qualified System.Random.MWC as MWC
|
||||||
|
import Control.Monad.Primitive (PrimMonad, PrimState)
|
||||||
|
import qualified Data.Vector.Storable as V
|
||||||
|
import Data.Word (Word8)
|
||||||
|
import Data.ByteString.Internal (ByteString (PS))
|
||||||
|
import qualified Data.Word8 as Word8
|
||||||
|
|
||||||
-- | Impose a limit on the size of the request body.
|
-- | Impose a limit on the size of the request body.
|
||||||
limitRequestBody :: Word64 -> W.Request -> IO W.Request
|
limitRequestBody :: Word64 -> W.Request -> IO W.Request
|
||||||
@ -68,12 +74,11 @@ tooLargeResponse = W.responseLBS
|
|||||||
[("Content-Type", "text/plain")]
|
[("Content-Type", "text/plain")]
|
||||||
"Request body too large to be processed."
|
"Request body too large to be processed."
|
||||||
|
|
||||||
parseWaiRequest :: RandomGen g
|
parseWaiRequest :: W.Request
|
||||||
=> W.Request
|
|
||||||
-> SessionMap
|
-> SessionMap
|
||||||
-> Bool
|
-> Bool
|
||||||
-> Maybe Word64 -- ^ max body size
|
-> Maybe Word64 -- ^ max body size
|
||||||
-> (Either (IO YesodRequest) (g -> IO YesodRequest))
|
-> (Either (IO YesodRequest) (MWC.GenIO -> IO YesodRequest))
|
||||||
parseWaiRequest env session useToken mmaxBodySize =
|
parseWaiRequest env session useToken mmaxBodySize =
|
||||||
-- In most cases, we won't need to generate any random values. Therefore,
|
-- In most cases, we won't need to generate any random values. Therefore,
|
||||||
-- we split our results: if we need a random generator, return a Right
|
-- we split our results: if we need a random generator, return a Right
|
||||||
@ -81,7 +86,7 @@ parseWaiRequest env session useToken mmaxBodySize =
|
|||||||
-- acquisition.
|
-- acquisition.
|
||||||
case etoken of
|
case etoken of
|
||||||
Left token -> Left $ mkRequest token
|
Left token -> Left $ mkRequest token
|
||||||
Right mkToken -> Right $ mkRequest . mkToken
|
Right mkToken -> Right $ mkRequest <=< mkToken
|
||||||
where
|
where
|
||||||
mkRequest token' = do
|
mkRequest token' = do
|
||||||
envLimited <- maybe return limitRequestBody mmaxBodySize env
|
envLimited <- maybe return limitRequestBody mmaxBodySize env
|
||||||
@ -124,7 +129,7 @@ parseWaiRequest env session useToken mmaxBodySize =
|
|||||||
-- Already have a token, use it.
|
-- Already have a token, use it.
|
||||||
Just bs -> Left $ Just $ decodeUtf8With lenientDecode bs
|
Just bs -> Left $ Just $ decodeUtf8With lenientDecode bs
|
||||||
-- Don't have a token, get a random generator and make a new one.
|
-- Don't have a token, get a random generator and make a new one.
|
||||||
Nothing -> Right $ Just . pack . randomString 10
|
Nothing -> Right $ fmap Just . randomString 10
|
||||||
| otherwise = Left Nothing
|
| otherwise = Left Nothing
|
||||||
|
|
||||||
textQueryString :: W.Request -> [(Text, Text)]
|
textQueryString :: W.Request -> [(Text, Text)]
|
||||||
@ -153,13 +158,23 @@ addTwoLetters (toAdd, exist) (l:ls) =
|
|||||||
-- | Generate a random String of alphanumerical characters
|
-- | Generate a random String of alphanumerical characters
|
||||||
-- (a-z, A-Z, and 0-9) of the given length using the given
|
-- (a-z, A-Z, and 0-9) of the given length using the given
|
||||||
-- random number generator.
|
-- random number generator.
|
||||||
randomString :: RandomGen g => Int -> g -> String
|
randomString :: PrimMonad m => Int -> MWC.Gen (PrimState m) -> m Text
|
||||||
randomString len = take len . map toChar . randomRs (0, 61)
|
randomString len gen =
|
||||||
|
liftM (decodeUtf8 . fromByteVector) $ V.replicateM len asciiChar
|
||||||
where
|
where
|
||||||
toChar i
|
asciiChar = liftM toAscii $ MWC.uniformR (0, 61) gen
|
||||||
| i < 26 = toEnum $ i + fromEnum 'A'
|
|
||||||
| i < 52 = toEnum $ i + fromEnum 'a' - 26
|
toAscii i
|
||||||
| otherwise = toEnum $ i + fromEnum '0' - 52
|
| i < 26 = i + Word8._A
|
||||||
|
| i < 52 = i + Word8._a - 26
|
||||||
|
| otherwise = i + Word8._0 - 52
|
||||||
|
|
||||||
|
fromByteVector :: V.Vector Word8 -> ByteString
|
||||||
|
fromByteVector v =
|
||||||
|
PS fptr offset idx
|
||||||
|
where
|
||||||
|
(fptr, offset, idx) = V.unsafeToForeignPtr v
|
||||||
|
{-# INLINE fromByteVector #-}
|
||||||
|
|
||||||
mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo
|
mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo
|
||||||
mkFileInfoLBS name ct lbs = FileInfo name ct (sourceList $ L.toChunks lbs) (\fp -> L.writeFile fp lbs)
|
mkFileInfoLBS name ct lbs = FileInfo name ct (sourceList $ L.toChunks lbs) (\fp -> L.writeFile fp lbs)
|
||||||
|
|||||||
@ -256,10 +256,10 @@ yesodRunner handler' YesodRunnerEnv {..} route req sendResponse
|
|||||||
(session, saveSession) <- liftIO $ do
|
(session, saveSession) <- liftIO $ do
|
||||||
maybe (return (Map.empty, dontSaveSession)) (\sb -> sbLoadSession sb req) yreSessionBackend
|
maybe (return (Map.empty, dontSaveSession)) (\sb -> sbLoadSession sb req) yreSessionBackend
|
||||||
let mkYesodReq = parseWaiRequest req session (isJust yreSessionBackend) mmaxLen
|
let mkYesodReq = parseWaiRequest req session (isJust yreSessionBackend) mmaxLen
|
||||||
yreq <-
|
let yreq =
|
||||||
case mkYesodReq of
|
case mkYesodReq of
|
||||||
Left yreq -> return yreq
|
Left yreq -> yreq
|
||||||
Right needGen -> liftIO $ needGen <$> newStdGen
|
Right needGen -> needGen yreGen
|
||||||
let ra = resolveApproot yreSite req
|
let ra = resolveApproot yreSite req
|
||||||
let log' = messageLoggerSource yreSite yreLogger
|
let log' = messageLoggerSource yreSite yreLogger
|
||||||
-- We set up two environments: the first one has a "safe" error handler
|
-- We set up two environments: the first one has a "safe" error handler
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import Network.Wai (FilePart,
|
|||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import qualified Network.Wai.Parse as NWP
|
import qualified Network.Wai.Parse as NWP
|
||||||
import System.Log.FastLogger (LogStr, LoggerSet, toLogStr, pushLogStr)
|
import System.Log.FastLogger (LogStr, LoggerSet, toLogStr, pushLogStr)
|
||||||
|
import qualified System.Random.MWC as MWC
|
||||||
import Network.Wai.Logger (DateCacheGetter)
|
import Network.Wai.Logger (DateCacheGetter)
|
||||||
import Text.Blaze.Html (Html)
|
import Text.Blaze.Html (Html)
|
||||||
import Text.Hamlet (HtmlUrl)
|
import Text.Hamlet (HtmlUrl)
|
||||||
@ -193,6 +194,7 @@ data YesodRunnerEnv site = YesodRunnerEnv
|
|||||||
{ yreLogger :: !Logger
|
{ yreLogger :: !Logger
|
||||||
, yreSite :: !site
|
, yreSite :: !site
|
||||||
, yreSessionBackend :: !(Maybe SessionBackend)
|
, yreSessionBackend :: !(Maybe SessionBackend)
|
||||||
|
, yreGen :: !MWC.GenIO
|
||||||
}
|
}
|
||||||
|
|
||||||
data YesodSubRunnerEnv sub parent parentMonad = YesodSubRunnerEnv
|
data YesodSubRunnerEnv sub parent parentMonad = YesodSubRunnerEnv
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import Data.Map (singleton)
|
|||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Data.Word (Word64)
|
import Data.Word (Word64)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
import qualified System.Random.MWC as MWC
|
||||||
|
import Control.Monad.ST
|
||||||
|
import Control.Monad (replicateM)
|
||||||
|
|
||||||
randomStringSpecs :: Spec
|
randomStringSpecs :: Spec
|
||||||
randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do
|
randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do
|
||||||
@ -22,14 +25,20 @@ randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do
|
|||||||
-- NOTE: this testcase may break on other systems/architectures if
|
-- NOTE: this testcase may break on other systems/architectures if
|
||||||
-- mkStdGen is not identical everywhere (is it?).
|
-- mkStdGen is not identical everywhere (is it?).
|
||||||
looksRandom :: Bool
|
looksRandom :: Bool
|
||||||
looksRandom = randomString 20 (mkStdGen 0) == "VH9SkhtptqPs6GqtofVg"
|
looksRandom = runST $ do
|
||||||
|
gen <- MWC.create
|
||||||
|
s <- randomString 20 gen
|
||||||
|
return $ s == "VH9SkhtptqPs6GqtofVg"
|
||||||
|
|
||||||
noRepeat :: Int -> Int -> Bool
|
noRepeat :: Int -> Int -> Bool
|
||||||
noRepeat len n = length (nub $ map (randomString len . mkStdGen) [1..n]) == n
|
noRepeat len n = runST $ do
|
||||||
|
gen <- MWC.create
|
||||||
|
ss <- replicateM n $ randomString len gen
|
||||||
|
return $ length (nub ss) == n
|
||||||
|
|
||||||
|
|
||||||
-- For convenience instead of "(undefined :: StdGen)".
|
-- For convenience instead of "(undefined :: StdGen)".
|
||||||
g :: StdGen
|
g :: MWC.GenIO
|
||||||
g = error "test/YesodCoreTest/InternalRequest.g"
|
g = error "test/YesodCoreTest/InternalRequest.g"
|
||||||
|
|
||||||
parseWaiRequest' :: Request
|
parseWaiRequest' :: Request
|
||||||
|
|||||||
@ -65,6 +65,9 @@ library
|
|||||||
, conduit-extra
|
, conduit-extra
|
||||||
, exceptions >= 0.6
|
, exceptions >= 0.6
|
||||||
, deepseq
|
, deepseq
|
||||||
|
, mwc-random
|
||||||
|
, primitive
|
||||||
|
, word8
|
||||||
|
|
||||||
exposed-modules: Yesod.Core
|
exposed-modules: Yesod.Core
|
||||||
Yesod.Core.Content
|
Yesod.Core.Content
|
||||||
@ -154,6 +157,7 @@ test-suite tests
|
|||||||
, shakespeare
|
, shakespeare
|
||||||
, streaming-commons
|
, streaming-commons
|
||||||
, wai-extra
|
, wai-extra
|
||||||
|
, mwc-random
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
extensions: TemplateHaskell
|
extensions: TemplateHaskell
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user