Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36eb72c2a0 | ||
|
|
38c596800a | ||
|
|
a7f9181462 | ||
|
|
f68d5944e6 |
@@ -5,6 +5,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [12.2.11] - 2025-04-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
|
||||
|
||||
## [12.2.10] - 2025-04-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3889, Fix: JWT cache purging on every request decreases performance - @mkleczek
|
||||
|
||||
## [12.2.9] - 2025-04-16
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 12.2.9
|
||||
version: 12.2.11
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
, AuthResult(..)
|
||||
, JwtCacheState(..)
|
||||
, destroy
|
||||
, getConfig
|
||||
, getSchemaCache
|
||||
@@ -13,7 +14,7 @@ module PostgREST.AppState
|
||||
, getNextDelay
|
||||
, getNextListenerDelay
|
||||
, getTime
|
||||
, getJwtCache
|
||||
, getJwtCacheState
|
||||
, getSocketREST
|
||||
, getSocketAdmin
|
||||
, init
|
||||
@@ -83,6 +84,12 @@ data AuthResult = AuthResult
|
||||
, authRole :: BS.ByteString
|
||||
}
|
||||
|
||||
-- | JWT Cache and IO action that triggers purging old entries from the cache
|
||||
data JwtCacheState = JwtCacheState
|
||||
{ jwtCache :: C.Cache ByteString AuthResult
|
||||
, purgeCache :: IO ()
|
||||
}
|
||||
|
||||
data AppState = AppState
|
||||
-- | Database connection pool
|
||||
{ statePool :: SQL.Pool
|
||||
@@ -107,7 +114,7 @@ data AppState = AppState
|
||||
-- | Keeps track of the next delay for the listener
|
||||
, stateNextListenerDelay :: IORef Int
|
||||
-- | JWT Cache
|
||||
, jwtCache :: C.Cache ByteString AuthResult
|
||||
, jwtCacheState :: JwtCacheState
|
||||
-- | Network socket for REST API
|
||||
, stateSocketREST :: NS.Socket
|
||||
-- | Network socket for the admin UI
|
||||
@@ -139,6 +146,16 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
|
||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||
cache <- C.newCache Nothing
|
||||
-- purgeExpired has O(n^2) complexity
|
||||
-- so we wrap it in debounce to make sure it:
|
||||
-- 1) is executed asynchronously
|
||||
-- 2) only a single purge operation is running at a time
|
||||
debounce <- mkDebounce defaultDebounceSettings
|
||||
-- debounceFreq is set to default 1 second
|
||||
{ debounceAction = C.purgeExpired cache
|
||||
, debounceEdge = leadingEdge
|
||||
}
|
||||
|
||||
appState <- AppState pool
|
||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||
@@ -151,7 +168,7 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||
<*> myThreadId
|
||||
<*> newIORef 0
|
||||
<*> newIORef 1
|
||||
<*> C.newCache Nothing
|
||||
<*> pure (JwtCacheState cache debounce)
|
||||
<*> pure sock
|
||||
<*> pure adminSock
|
||||
<*> pure observer
|
||||
@@ -314,8 +331,8 @@ putConfig = atomicWriteIORef . stateConf
|
||||
getTime :: AppState -> IO UTCTime
|
||||
getTime = stateGetTime
|
||||
|
||||
getJwtCache :: AppState -> C.Cache ByteString AuthResult
|
||||
getJwtCache = jwtCache
|
||||
getJwtCacheState :: AppState -> JwtCacheState
|
||||
getJwtCacheState = jwtCacheState
|
||||
|
||||
getSocketREST :: AppState -> NS.Socket
|
||||
getSocketREST = stateSocketREST
|
||||
@@ -439,7 +456,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
-- | Reads the in-db config and reads the config file again
|
||||
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
||||
readInDbConfig :: Bool -> AppState -> IO ()
|
||||
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
|
||||
conf <- getConfig appState
|
||||
pgVer <- getPgVersion appState
|
||||
dbSettings <-
|
||||
@@ -476,7 +493,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
if configJwtSecret conf == configJwtSecret newConf then
|
||||
pass
|
||||
else
|
||||
C.purge (getJwtCache appState) -- atomic O(1) operation
|
||||
C.purge jwtCache -- atomic O(1) operation
|
||||
|
||||
if startingUp then
|
||||
pass
|
||||
|
||||
+10
-8
@@ -44,8 +44,9 @@ import System.Clock (TimeSpec (..))
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
|
||||
getJwtCache, getTime)
|
||||
import PostgREST.AppState (AppState, AuthResult (..),
|
||||
JwtCacheState (..), getConfig,
|
||||
getJwtCacheState, getTime)
|
||||
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
|
||||
@@ -131,7 +132,8 @@ middleware appState app req respond = do
|
||||
-- | Used to retrieve and insert JWT to JWT Cache
|
||||
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
||||
getJWTFromCache appState token maxLifetime parseJwt utc = do
|
||||
checkCache <- C.lookup (getJwtCache appState) token
|
||||
let JwtCacheState{..} = getJwtCacheState appState
|
||||
checkCache <- C.lookup jwtCache token
|
||||
authResult <- maybe parseJwt (pure . Right) checkCache
|
||||
|
||||
case (authResult,checkCache) of
|
||||
@@ -151,17 +153,17 @@ getJWTFromCache appState token maxLifetime parseJwt utc = do
|
||||
|
||||
let timeSpec = getTimeSpec res maxLifetime utc
|
||||
|
||||
-- purge expired cache entries
|
||||
C.purgeExpired jwtCache
|
||||
|
||||
-- insert new cache entry
|
||||
C.insert' jwtCache timeSpec token res
|
||||
|
||||
-- Execute IO action to purge the cache
|
||||
-- It is assumed this action returns immidiately
|
||||
-- so that request processing is not blocked.
|
||||
purgeCache
|
||||
|
||||
_ -> pure ()
|
||||
|
||||
return authResult
|
||||
where
|
||||
jwtCache = getJwtCache appState
|
||||
|
||||
-- Used to extract JWT exp claim and add to JWT Cache
|
||||
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
|
||||
|
||||
@@ -183,14 +183,14 @@ decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
|
||||
-- >>> P.parse tokenizeMediaType "" "application/vnd.pgrst.plan+text; for=\"text/xml\"; options=analyze|verbose|settings|buffers|wal"
|
||||
-- Right ("application","vnd.pgrst.plan+text",[("for","text/xml"),("options","analyze|verbose|settings|buffers|wal")])
|
||||
|
||||
-- TODO: Improve mediatype parser as per RFC 2045 https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
tokenizeMediaType :: P.Parser (Text, Text, [(Text, Text)])
|
||||
tokenizeMediaType = do
|
||||
mainType <- P.many1 (P.alphaNum <|> P.oneOf ".*")
|
||||
P.char '/'
|
||||
subType <- P.many1 (P.alphaNum <|> P.oneOf ".*+-")
|
||||
params <- P.many pSemicolonSeparatedKeyVals
|
||||
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon
|
||||
P.eof
|
||||
P.optional $ P.try $ P.spaces *> P.char ';' -- ending semicolon, discard input after that because it has already failed or we have hit EOF
|
||||
return (T.pack mainType, T.pack subType, params)
|
||||
where
|
||||
pSemicolonSeparatedKeyVals :: P.Parser (Text, Text)
|
||||
@@ -198,12 +198,12 @@ tokenizeMediaType = do
|
||||
where
|
||||
pKeyVal :: P.Parser (Text, Text)
|
||||
pKeyVal = do
|
||||
key <- P.many1 P.alphaNum
|
||||
key <- P.many1 (P.alphaNum <|> P.oneOf "-")
|
||||
P.spaces
|
||||
P.char '='
|
||||
P.spaces
|
||||
val <- P.try pQuoted <|> P.try pUnQuoted
|
||||
return (T.pack key, T.pack val)
|
||||
where
|
||||
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|")
|
||||
pUnQuoted = P.many1 (P.alphaNum <|> P.oneOf "|-")
|
||||
pQuoted = P.char '\"' *> P.manyTill P.anyChar (P.char '\"')
|
||||
|
||||
@@ -382,3 +382,24 @@ spec = describe "custom media types" $ do
|
||||
`shouldRespondWith`
|
||||
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: undefined"} |]
|
||||
{ matchStatus = 406 }
|
||||
|
||||
context "media type parser allowed characters" $ do
|
||||
it "regression test allowing charset=utf-8" $
|
||||
request methodPost "/rpc/overloaded_default"
|
||||
[("Content-Type", "application/json; charset=utf-8")]
|
||||
[json|{"must_param":1}|]
|
||||
`shouldRespondWith`
|
||||
[json|{"val":1}|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
it "handle unrecognized parameters leniently" $ do
|
||||
request methodPost "/rpc/overloaded_default"
|
||||
[("Content-Type", "application/json; $$ unrecognized-chars=ignored $$")]
|
||||
[json|{"must_param":1}|]
|
||||
`shouldRespondWith`
|
||||
[json|{"val":1}|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user