fix: Parse accept header case-insensitively

The Accept header is parsed case-insensitively now, introducing proper
handling of media types specified in upper- and/or mixed-case.

Fixes #3478
This commit is contained in:
Andrei Dziahel
2024-05-09 19:30:42 +02:00
committed by Wolfgang Walther
parent 1fa35cb3b8
commit 3026c1f308
2 changed files with 72 additions and 46 deletions
+1
View File
@@ -44,6 +44,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2985, Fix not adding `application_name` on all connection strings - @steve-chavez - #2985, Fix not adding `application_name` on all connection strings - @steve-chavez
- #3424, Admin `/live` and `/ready` now differentiates a failure as 500 status - @steve-chavez - #3424, Admin `/live` and `/ready` now differentiates a failure as 500 status - @steve-chavez
+ 503 status is still given when postgREST is in a recovering state + 503 status is still given when postgREST is in a recovering state
- #3478, Media Types are parsed case insensitively - @develop7
### Deprecated ### Deprecated
+71 -46
View File
@@ -11,13 +11,17 @@ module PostgREST.MediaType
, decodeMediaType , decodeMediaType
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (c2w)
import Network.HTTP.Types.Header (Header, hContentType) import Network.HTTP.Types.Header (Header, hContentType)
import Protolude import Data.Map (fromList, (!?))
import qualified Data.Text as T (break, drop, dropWhile,
dropWhileEnd, null, splitOn,
toLower)
import Data.Text.Encoding (decodeLatin1)
import Protolude
-- | Enumeration of currently supported media types -- | Enumeration of currently supported media types
data MediaType data MediaType
@@ -114,48 +118,69 @@ toMimePlanFormat PlanText = "text"
-- --
-- >>> decodeMediaType "application/vnd.pgrst.object+json" -- >>> decodeMediaType "application/vnd.pgrst.object+json"
-- MTVndSingularJSON False -- MTVndSingularJSON False
--
-- Test uppercase is parsed correctly (per issue #3478)
-- >>> decodeMediaType "ApplicatIon/vnd.PgRsT.object+json"
-- MTVndSingularJSON False
--
-- >>> decodeMediaType "application/vnd.twkb"
-- MTOther "application/vnd.twkb"
decodeMediaType :: BS.ByteString -> MediaType decodeMediaType :: ByteString -> MediaType
decodeMediaType mt = decodeMediaType mt = decodeMediaType' $ decodeLatin1 mt
case BS.split (BS.c2w ';') mt of
"application/json":_ -> MTApplicationJSON
"application/geo+json":_ -> MTGeoJSON
"text/csv":_ -> MTTextCSV
"text/plain":_ -> MTTextPlain
"text/xml":_ -> MTTextXML
"application/openapi+json":_ -> MTOpenAPI
"application/x-www-form-urlencoded":_ -> MTUrlEncoded
"application/octet-stream":_ -> MTOctetStream
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
"application/vnd.pgrst.object+json":rest -> checkSingularNullStrip rest
"application/vnd.pgrst.object":rest -> checkSingularNullStrip rest
"application/vnd.pgrst.array+json":rest -> checkArrayNullStrip rest
"application/vnd.pgrst.array":rest -> checkArrayNullStrip rest
"*/*":_ -> MTAny
other:_ -> MTOther $ decodeUtf8 other
_ -> MTAny
where where
checkArrayNullStrip ["nulls=stripped"] = MTVndArrayJSONStrip decodeMediaType' :: Text -> MediaType
checkArrayNullStrip _ = MTApplicationJSON decodeMediaType' mt' =
case (T.toLower mainType, T.toLower subType, params) of
("application", "json", _) -> MTApplicationJSON
("application", "geo+json", _) -> MTGeoJSON
("text", "csv", _) -> MTTextCSV
("text", "plain", _) -> MTTextPlain
("text", "xml", _) -> MTTextXML
("application", "openapi+json", _) -> MTOpenAPI
("application", "x-www-form-urlencoded", _) -> MTUrlEncoded
("application", "octet-stream", _) -> MTOctetStream
("application", "vnd.pgrst.plan", _) -> getPlan PlanText
("application", "vnd.pgrst.plan+text", _) -> getPlan PlanText
("application", "vnd.pgrst.plan+json", _) -> getPlan PlanJSON
("application", "vnd.pgrst.object+json", _) -> MTVndSingularJSON strippedNulls
("application", "vnd.pgrst.object", _) -> MTVndSingularJSON strippedNulls
("application", "vnd.pgrst.array+json", _) -> checkArrayNullStrip
("application", "vnd.pgrst.array", _) -> checkArrayNullStrip
("*","*",_) -> MTAny
_ -> MTOther mt'
where
(mainType, subType, params') = tokenizeMediaType mt'
params = fromList $ map (first T.toLower) params' -- normalize parameter names to lowercase, per RFC 7321
getPlan fmt = MTVndPlan mtFor fmt $
[PlanAnalyze | inOpts "analyze" ] ++
[PlanVerbose | inOpts "verbose" ] ++
[PlanSettings | inOpts "settings"] ++
[PlanBuffers | inOpts "buffers" ] ++
[PlanWAL | inOpts "wal" ]
where
mtFor = decodeMediaType' $ fromMaybe "application/json" (params !? "for")
inOpts str = str `elem` opts
opts = T.splitOn "|" $ fromMaybe mempty (params !? "options")
strippedNulls = fromMaybe "false" (params !? "nulls") == "stripped"
checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON
checkSingularNullStrip ["nulls=stripped"] = MTVndSingularJSON True -- | Split a Media Type string into components
checkSingularNullStrip _ = MTVndSingularJSON False -- >>> tokenizeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- ("application","vnd.pgrst.plan+json",[("for","text/csv")])
getPlan fmt rest = -- >>> tokenizeMediaType "*/*"
let -- ("*","*",[])
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest) -- >>> tokenizeMediaType "application/vnd.pgrst.plan;wat=\"application/json;text/csv\""
inOpts str = str `elem` opts -- ("application","vnd.pgrst.plan",[("wat","application/json"),("text/csv\"","")])
dropAround p = BS.dropWhile p . BS.dropWhileEnd p tokenizeMediaType :: Text -> (Text, Text, [(Text, Text)])
mtFor = fromMaybe MTApplicationJSON $ do tokenizeMediaType t = (mainType, subType, params)
foundFor <- find (BS.isPrefixOf "for=") rest where
strippedFor <- BS.stripPrefix "for=" foundFor (mainType, rest) = T.break (== '/') t
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor (subType, restParams) = T.break (== ';') $ T.drop 1 rest
in params =
MTVndPlan mtFor fmt $ let rp = T.drop 1 restParams
[PlanAnalyze | inOpts "analyze" ] ++ in if T.null rp then [] else map param $ T.splitOn ";" rp -- FIXME: breaks if there's a ';' in a quoted value
[PlanVerbose | inOpts "verbose" ] ++ param p =
[PlanSettings | inOpts "settings"] ++ let (k, v) = T.break (== '=') p
[PlanBuffers | inOpts "buffers" ] ++ in (k, dropAround (== '"') $ T.drop 1 v) -- FIXME: doesn't unescape quotes in values
[PlanWAL | inOpts "wal" ] dropAround p = T.dropWhile p . T.dropWhileEnd p