packages feed

warp-3.4.17: Network/Wai/Handler/Warp/Conduit.hs

{-# LANGUAGE CPP #-}

module Network.Wai.Handler.Warp.Conduit where

import Control.Exception (assert, throwIO)
import qualified Data.ByteString as S
import qualified Data.IORef as I
import Data.Word8 (_0, _9, _A, _F, _a, _cr, _f, _lf, _semicolon)

import Network.Wai.Handler.Warp.Imports
import Network.Wai.Handler.Warp.Types

----------------------------------------------------------------

-- | Contains a @Source@ and a byte count that is still to be read in.
data ISource = ISource !Source !(I.IORef Int)

mkISource :: Source -> Int -> IO ISource
mkISource src cnt = do
    ref <- I.newIORef cnt
    return $! ISource src ref

-- | Given an @IsolatedBSSource@ provide a @Source@ that only allows up to the
-- specified number of bytes to be passed downstream. All leftovers should be
-- retained within the @Source@. If there are not enough bytes available,
-- throws a @ConnectionClosedByPeer@ exception.
readISource :: ISource -> IO ByteString
readISource (ISource src ref) = do
    count <- I.readIORef ref
    if count == 0
        then return S.empty
        else do
            bs <- readSource src

            -- If no chunk available, then there aren't enough bytes in the
            -- stream. Throw a ConnectionClosedByPeer
            when (S.null bs) $ throwIO ConnectionClosedByPeer

            let -- How many of the bytes in this chunk to send downstream
                toSend = min count (S.length bs)
                -- How many bytes will still remain to be sent downstream
                count' = count - toSend

            -- [WRITE_IOREF_NOTE]
            -- This doesn't need to be "atomic", since it is only used in
            -- 'recvRequest', which creates the 'Source' and doesn't fork it,
            -- so the 'IORef' is not shared outside of 'recvRequest'.
            I.writeIORef ref count'

            if count' > 0
                then -- The expected count is greater than the size of the
                -- chunk we just read. Send the entire chunk
                -- downstream, and then loop on this function for the
                -- next chunk.
                    return bs
                else do
                    -- Some of the bytes in this chunk should not be sent
                    -- downstream. Split up the chunk into the sent and
                    -- not-sent parts, add the not-sent parts onto the new
                    -- source, and send the rest of the chunk downstream.
                    let (x, y) = S.splitAt toSend bs
                    leftoverSource src y
                    assert (count' == 0) $ return x

----------------------------------------------------------------

data CSource = CSource !Source !(I.IORef ChunkState)

data ChunkState
    = NeedLen
    | NeedLenNewline
    | HaveLen Word
    | DoneChunking
    deriving (Show)

mkCSource :: Source -> IO CSource
mkCSource src = do
    ref <- I.newIORef NeedLen
    return $! CSource src ref

-- | A chunk-size line is a few bytes of hex, and chunk extensions are
--   all but unused; a trailer section is a handful of fields. Neither is
--   a reason to hold on to everything a client cares to send.
maxChunkLineLength :: Int
maxChunkLineLength = 1024

maxTrailerLength :: Int
maxTrailerLength = 8192

readCSource :: CSource -> IO ByteString
readCSource (CSource src ref) = do
    mlen <- I.readIORef ref
    go mlen
  where
    go NeedLen = getLen
    go NeedLenNewline = dropCRLF >> getLen
    go (HaveLen len) = readSource src >>= withLen len
    go DoneChunking = return S.empty

    getLen = do
        len <- chunkSize =<< requireLine
        if len == 0
            then do
                -- Everything up to the empty line which ends the trailer
                -- section belongs to this request. Warp used to drop one
                -- CRLF and leave the trailers in the source, where they
                -- were read as the next request on the connection.
                dropTrailers 0
                yield' S.empty DoneChunking
            else readSource src >>= withLen len

    withLen len bs
        -- The chunk promised more than the client went on to send.
        | S.null bs = throwIO ConnectionClosedByPeer
        | otherwise =
            case S.length bs `compare` fromIntegral len of
                EQ -> yield' bs NeedLenNewline
                LT -> yield' bs $ HaveLen $ len - fromIntegral (S.length bs)
                GT -> do
                    let (x, y) = S.splitAt (fromIntegral len) bs
                    leftoverSource src y
                    yield' x NeedLenNewline

    yield' bs mlen = do
        I.writeIORef ref mlen -- [WRITE_IOREF_NOTE]
        return bs

    -- A chunk size is 1*HEXDIG, with nothing after it but chunk
    -- extensions. Warp used to fold whatever hex digits it could find
    -- and take 0 for the rest, so a line which is no chunk size at all
    -- -- a request line, say -- ended the body and left the rest of it
    -- to be read as the next request.
    chunkSize line
        | S.null digits = throwIO BadChunkedBody
        | not (S.null ext), S.head ext /= _semicolon = throwIO BadChunkedBody
        | otherwise =
            maybe (throwIO BadChunkedBody) (return . fromIntegral) $
                S.foldl' accumulate (Just (0 :: Int)) digits
      where
        (digits, ext) = S.span isHexDigit line
        accumulate Nothing _ = Nothing
        -- A chunk warp cannot measure is a chunk warp cannot read.
        accumulate (Just n) w
            | n > (maxBound - d) `div` 16 = Nothing
            | otherwise = Just $! n * 16 + d
          where
            d = fromIntegral (hexToWord w)

    -- The CRLF which ends chunk-data. Warp used to put anything else
    -- back and read it as the next chunk size, so a chunk that was not
    -- closed off ended the body early.
    dropCRLF = do
        line <- requireLine
        unless (S.null line) $ throwIO BadChunkedBody

    dropTrailers total = do
        mline <- readLine
        case mline of
            -- A client which leaves out the last CRLF and closes has
            -- nothing left to hide behind it.
            Nothing -> return ()
            Just line -> unless (S.null line) $ do
                let total' = total + S.length line
                when (total' > maxTrailerLength) $ throwIO OverLargeHeader
                dropTrailers total'

    requireLine = readLine >>= maybe (throwIO ConnectionClosedByPeer) return

    -- One line of the chunked framing: up to the next LF, without it or
    -- the CR before it, leaving the rest of what was read in the source.
    -- 'Nothing' when the connection ends where a line was to begin.
    -- Warp used to give up one read short of the LF, which mis-read
    -- every size line that arrived in more than two pieces.
    readLine = loop 0 id
      where
        loop total front = do
            bs <- readSource src
            if S.null bs
                then
                    if total == 0
                        then return Nothing
                        else throwIO ConnectionClosedByPeer
                else case S.elemIndex _lf bs of
                    Nothing -> do
                        let total' = total + S.length bs
                        when (total' > maxChunkLineLength) $ throwIO OverLargeHeader
                        loop total' (front . (bs :))
                    Just ix -> do
                        leftoverSource src $ S.drop (ix + 1) bs
                        let line = S.concat $ front [S.take ix bs]
                        when (S.length line > maxChunkLineLength) $
                            throwIO OverLargeHeader
                        return $ Just $ dropCR line
        dropCR bs = case S.unsnoc bs of
            Just (bs', w) | w == _cr -> bs'
            _ -> bs

    hexToWord w
        | w <= _9 = w - _0
        | w <= _F = w - 55
        | otherwise = w - 87

isHexDigit :: Word8 -> Bool
isHexDigit w =
    w >= _0 && w <= _9
        || w >= _A && w <= _F
        || w >= _a && w <= _f