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:
Michael Snoyman 2014-10-20 10:34:38 +03:00
parent 658f31e0d7
commit 972e117005
6 changed files with 55 additions and 20 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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