shomei-server-0.3.0.0: src/Shomei/Server/Middleware/BodyLimit.hs
-- | Refuse oversized request bodies before they reach a handler (EP-8).
--
-- Nothing else in the stack bounds how much a client may send: warp has no request-body size
-- setting (body limiting is a middleware concern), and Shōmei's JSON handlers happily begin
-- consuming whatever arrives. This middleware answers HTTP 413 to any request that declares a
-- @Content-Length@ above the cap, without reading a byte of the body. Bodies without a declared
-- length are metered chunk by chunk as the downstream application consumes them.
module Shomei.Server.Middleware.BodyLimit
( bodyLimitMiddleware,
defaultBodyLimitBytes,
)
where
import Control.Exception (Exception, catch, throwIO)
import Control.Monad (when)
import Data.ByteString qualified as BS
import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
import Data.Word (Word64)
import Network.Wai
( Middleware,
RequestBodyLength (KnownLength),
getRequestBodyChunk,
requestBodyLength,
setRequestBodyChunks,
)
import Shomei.Servant.Error (noProblemOccurrence, pcPayloadTooLarge)
import Shomei.Servant.Middleware (problemResponse)
data BodyOverCap = BodyOverCap Word64
deriving stock (Show)
deriving anyclass (Exception)
-- | 1 MiB. Three orders of magnitude above the largest legitimate Shōmei request body (a
-- WebAuthn attestation), and small enough that a flood of maximal bodies cannot exhaust memory.
defaultBodyLimitBytes :: Word64
defaultBodyLimitBytes = 1024 * 1024
-- | Reject a request once its declared or observed body length is strictly greater than @limit@.
bodyLimitMiddleware :: Word64 -> Middleware
bodyLimitMiddleware limit app req respond
| KnownLength n <- requestBodyLength req, n > limit = respond tooLarge
| otherwise = do
seen <- newIORef 0
responded <- newIORef False
let metered = do
chunk <- getRequestBodyChunk req
total <-
atomicModifyIORef' seen \n ->
let n' = n + fromIntegral (BS.length chunk)
in (n', n')
when (total > limit) (throwIO (BodyOverCap limit))
pure chunk
respond' res = writeIORef responded True >> respond res
app (setRequestBodyChunks metered req) respond' `catch` \e@(BodyOverCap _) -> do
already <- readIORef responded
if already then throwIO e else respond tooLarge
where
tooLarge = problemResponse pcPayloadTooLarge noProblemOccurrence