packages feed

http3-0.1.10: Network/QPACK/HeaderBlock/Decode.hs

{-# LANGUAGE BinaryLiterals #-}

module Network.QPACK.HeaderBlock.Decode where

import Control.Concurrent.STM
import qualified Control.Exception as E
import qualified Data.ByteString.Char8 as BS8
import Data.CaseInsensitive
import Data.IORef
import Network.ByteOrder
import qualified Network.HPACK as HPACK
import Network.HPACK.Internal (
    Entry,
    HuffmanDecoder,
    decodeH,
    decodeI,
    decodeS,
    decodeSimple,
    decodeSophisticated,
    entryToken,
    entryTokenHeader,
 )
import Network.HTTP.Types

import Imports
import Network.QPACK.Error
import Network.QPACK.HeaderBlock.Prefix
import Network.QPACK.Table
import Network.QPACK.Types

-- | Errors in the encoding of a field section, as QPACK's 'DecodeError', and
-- so QPACK_DECOMPRESSION_FAILED (RFC 9204, section 2.2).  They came out as
-- HPACK's errors, or 'BufferOverrun' for a section cut short, which nothing
-- took for a decoding error: the request was reset as malformed.
asDecodeError :: IO a -> IO a
asDecodeError io =
    io
        `E.catches` [ E.Handler $ \e -> case e of
                        HPACK.TooLargeHeader -> E.throwIO FieldSectionTooLarge
                        _
                            | encoding e -> E.throwIO $ MalformedFieldLine $ show e
                            -- A field section well encoded but not a valid
                            -- one, such as a pseudo-header after a field:
                            -- the message is malformed.
                            | otherwise -> E.throwIO e
                    , E.Handler $ \BufferOverrun ->
                        E.throwIO $ MalformedFieldLine "cut short"
                    ]

-- | HPACK's errors in the encoding itself, as opposed to the fields it
-- decodes to.
encoding :: HPACK.DecodeError -> Bool
encoding e =
    e
        `elem` [ HPACK.EosInTheMiddle
               , HPACK.IllegalEos
               , HPACK.TooLongEos
               , HPACK.TooLargeInteger
               , HPACK.HeaderBlockTruncated
               ]

decodeTokenHeader
    :: DynamicTable
    -> ReadBuffer
    -> IO (TokenHeaderTable, RequiredInsertCount)
decodeTokenHeader dyntbl rbuf = asDecodeError $ do
    (reqInsertCount, bp) <- decodePrefix rbuf dyntbl
    ready <- checkRequiredInsertCountNB dyntbl reqInsertCount
    -- The count of blocked streams has to come down however the wait ends.
    -- A stream reset while waiting kills the thread, and the count left up
    -- counted a stream that was not there any more: once as many had gone as
    -- SETTINGS_QPACK_BLOCKED_STREAMS allows, every section that had to wait
    -- was refused.
    unless ready $ E.mask $ \restore -> do
        ok <- tryIncreaseStreams dyntbl
        unless ok $ E.throwIO BlockedStreamsOverflow
        restore (checkRequiredInsertCount dyntbl reqInsertCount)
            `E.finally` decreaseStreams dyntbl
    checkRequiredInsertCount dyntbl reqInsertCount
    hufdec <- newHuffmanDecoder rbuf
    dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl reqInsertCount bp hufdec
    -- HPACK's decoder refuses a section of more than 200 fields, which is
    -- the same thing as far as we are concerned: more than we will take.
    -- 'asDecodeError' says so.
    tbl <- decodeSophisticated dec rbuf
    return (tbl, reqInsertCount)

decodeTokenHeaderS
    :: DynamicTable
    -> ReadBuffer
    -> IO (Maybe ([Header], RequiredInsertCount))
decodeTokenHeaderS dyntbl rbuf = asDecodeError $ do
    (reqInsertCount, bp) <- decodePrefix rbuf dyntbl
    ok <- checkRequiredInsertCountNB dyntbl reqInsertCount
    if ok
        then do
            hufdec <- newHuffmanDecoder rbuf
            dec <-
                limitFieldSection dyntbl $ toTokenHeader dyntbl reqInsertCount bp hufdec
            hs <- decodeSimple dec rbuf
            return $ Just (hs, reqInsertCount)
        else return Nothing

-- | A field line decoder that stops once the section comes to more than we
--   said we would take.
--
-- The length of a HEADERS frame is capped, but that bounds the section only
-- as it is encoded.  A field line of a couple of octets can refer to a
-- dynamic table entry as large as the table, so a frame within the cap could
-- decode to many megabytes.  Counting field by field stops that before it is
-- built, and holds one section to the limit plus one field.
limitFieldSection
    :: DynamicTable
    -> (Word8 -> ReadBuffer -> IO TokenHeader)
    -> IO (Word8 -> ReadBuffer -> IO TokenHeader)
limitFieldSection dyntbl dec = do
    lim <- getMaxHeaderSize dyntbl
    ref <- newIORef 0
    return $ \w8 rbuf -> do
        th@(t, v) <- dec w8 rbuf
        let siz = BS8.length (original (tokenKey t)) + BS8.length v + 32
        total <- atomicModifyIORef' ref $ \n -> (n + siz, n + siz)
        when (total > lim) $ E.throwIO FieldSectionTooLarge
        return th

-- | A Huffman decoder with room for anything the rest of this field section
-- can decode to.
--
-- The scratch buffer has to hold one decoded string, and the shortest Huffman
-- code is five bits, so an encoded string of n octets cannot come to more than
-- 8n\/5 symbols -- and the section that contains it is itself at most what is
-- left in the buffer.  Sizing from that means a header field is refused only
-- when the section it is in is, rather than at a fixed 2048 that nothing
-- announced: a 2100-octet value used to fail to decode while the section
-- carrying it was under 1.4K, well inside the SETTINGS_MAX_FIELD_SECTION_SIZE
-- we advertise.
--
-- Allocated per section rather than held on the table, because sections from
-- different streams decode concurrently and this buffer is not shared.
newHuffmanDecoder :: ReadBuffer -> IO HuffmanDecoder
newHuffmanDecoder rbuf = do
    siz <- remainingSize rbuf
    let bufsiz = max 1 ((siz * 8) `div` 5)
    gcbuf <- mallocPlainForeignPtrBytes bufsiz
    return $ decodeH gcbuf bufsiz

{- FOURMOLU_DISABLE -}
toTokenHeader
    :: DynamicTable
    -> RequiredInsertCount
    -> BasePoint
    -> HuffmanDecoder
    -> Word8
    -> ReadBuffer
    -> IO TokenHeader
toTokenHeader dyntbl ric bp hufdec w8 rbuf
    | w8 `testBit` 7 =
        decodeIndexedFieldLine                  rbuf dyntbl ric        bp w8
    | w8 `testBit` 6 =
        decodeLiteralFieldLineWithNameReference rbuf dyntbl ric hufdec bp w8
    | w8 `testBit` 5 =
        decodeLiteralFieldLineWithLiteralName   rbuf dyntbl     hufdec bp w8
    | w8 `testBit` 4 =
        decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl ric        bp w8
    | otherwise =
        decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl ric hufdec bp w8
{- FOURMOLU_ENABLE -}

-- | The entry a field line refers to.
--
-- RFC 9204, section 2.2.3: a reference to an entry at or above the Required
-- Insert Count is QPACK_DECOMPRESSION_FAILED.  It was checked only against
-- what had been inserted, so a section with a Required Insert Count of 0 --
-- which is never acknowledged -- could name any entry in the table.
lookupEntry :: DynamicTable -> RequiredInsertCount -> HIndex -> IO Entry
lookupEntry dyntbl (RequiredInsertCount ric) hidx = do
    case hidx of
        DIndex (AbsoluteIndex ai)
            | ai >= ric -> E.throwIO $ IllegalDynamicIndex ai
        _ -> return ()
    atomically $ toIndexedEntry dyntbl hidx

-- 4.5.2.  Indexed Field Line
decodeIndexedFieldLine
    :: ReadBuffer
    -> DynamicTable
    -> RequiredInsertCount
    -> BasePoint
    -> Word8
    -> IO TokenHeader
decodeIndexedFieldLine rbuf dyntbl ric bp w8 = do
    i <- decodeI 6 (w8 .&. 0b00111111) rbuf
    let static = w8 `testBit` 6
        hidx
            | static = SIndex $ AbsoluteIndex i
            | otherwise = DIndex $ fromPreBaseIndex (PreBaseIndex i) bp
    ret <- entryTokenHeader <$> lookupEntry dyntbl ric hidx
    qpackDebug dyntbl $
        putStrLn $
            "IndexedFieldLine (" ++ show hidx ++ ") " ++ showTokenHeader ret
    return ret

-- 4.5.3.  Indexed Field Line With Post-Base Index
decodeIndexedFieldLineWithPostBaseIndex
    :: ReadBuffer
    -> DynamicTable
    -> RequiredInsertCount
    -> BasePoint
    -> Word8
    -> IO TokenHeader
decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl ric bp w8 = do
    i <- decodeI 4 (w8 .&. 0b00001111) rbuf
    let hidx = DIndex $ fromPostBaseIndex (PostBaseIndex i) bp
    ret <- entryTokenHeader <$> lookupEntry dyntbl ric hidx
    qpackDebug dyntbl $
        putStrLn $
            "IndexedFieldLineWithPostBaseIndex ("
                ++ show hidx
                ++ " "
                ++ show bp
                ++ " after "
                ++ show i
                ++ ") "
                ++ showTokenHeader ret
    return ret

-- 4.5.4.  Literal Field Line With Name Reference
decodeLiteralFieldLineWithNameReference
    :: ReadBuffer
    -> DynamicTable
    -> RequiredInsertCount
    -> HuffmanDecoder
    -> BasePoint
    -> Word8
    -> IO TokenHeader
decodeLiteralFieldLineWithNameReference rbuf dyntbl ric hufdec bp w8 = do
    i <- decodeI 4 (w8 .&. 0b00001111) rbuf
    let static = w8 `testBit` 4
        hidx
            | static = SIndex $ AbsoluteIndex i
            | otherwise = DIndex $ fromPreBaseIndex (PreBaseIndex i) bp
    key <- entryToken <$> lookupEntry dyntbl ric hidx
    val <- decodeS (`clearBit` 7) (`testBit` 7) 7 hufdec rbuf
    let ret = (key, val)
    qpackDebug dyntbl $
        putStrLn $
            "LiteralFieldLineWithNameReference ("
                ++ show hidx
                ++ ") "
                ++ showTokenHeader ret
    return ret

-- 4.5.5.  Literal Field Line With Post-Base Name Reference
decodeLiteralFieldLineWithPostBaseNameReference
    :: ReadBuffer
    -> DynamicTable
    -> RequiredInsertCount
    -> HuffmanDecoder
    -> BasePoint
    -> Word8
    -> IO TokenHeader
decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl ric hufdec bp w8 = do
    i <- decodeI 3 (w8 .&. 0b00000111) rbuf
    let hidx = DIndex $ fromPostBaseIndex (PostBaseIndex i) bp
    key <- entryToken <$> lookupEntry dyntbl ric hidx
    val <- decodeS (`clearBit` 7) (`testBit` 7) 7 hufdec rbuf
    let ret = (key, val)
    qpackDebug dyntbl $
        putStrLn $
            "LiteralFieldLineWithPostBaseNameReference ("
                ++ show hidx
                ++ ") "
                ++ showTokenHeader ret
    return ret

-- 4.5.6.  Literal Field Line With Literal Name
decodeLiteralFieldLineWithLiteralName
    :: ReadBuffer
    -> DynamicTable
    -> HuffmanDecoder
    -> BasePoint
    -> Word8
    -> IO TokenHeader
decodeLiteralFieldLineWithLiteralName rbuf dyntbl hufdec _bp _w8 = do
    ff rbuf (-1)
    key <- toToken <$> decodeS (.&. 0b00000111) (`testBit` 3) 3 hufdec rbuf
    val <- decodeS (`clearBit` 7) (`testBit` 7) 7 hufdec rbuf
    let ret = (key, val)
    qpackDebug dyntbl $
        putStrLn $
            "LiteralFieldLineWithLiteralName " ++ showTokenHeader ret
    return ret

showTokenHeader :: TokenHeader -> String
showTokenHeader (t, val) = "\"" ++ key ++ "\" \"" ++ BS8.unpack val ++ "\""
  where
    key = BS8.unpack $ foldedCase $ tokenKey t