packages feed

http3-0.1.0: Network/QPACK.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | Thread-safe QPACK encoder/decoder.
module Network.QPACK (
    -- * Encoder
    QEncoderConfig (..),
    defaultQEncoderConfig,
    QEncoder,
    newQEncoder,
    TableOperation (..),

    -- ** Encoder for debugging
    QEncoderS,
    newQEncoderS,

    -- * Decoder
    QDecoderConfig (..),
    defaultQDecoderConfig,
    QDecoder,
    newQDecoder,

    -- ** Decoder for debugging
    QDecoderS,
    newQDecoderS,

    -- * Types
    EncodedEncoderInstruction,
    EncoderInstructionHandler,
    EncoderInstructionHandlerS,
    EncodedDecoderInstruction,
    DecoderInstructionHandler,
    InstructionHandler,
    Size,

    -- * Re-exports
    TokenHeaderTable,
    TokenHeaderList,
    ValueTable,
    Header,
    getFieldValue,
    toTokenHeaderTable,
    original,
    foldedCase,
    mk,
) where

import Control.Concurrent
import Control.Concurrent.STM
import qualified Control.Exception as E
import qualified Data.ByteString as BS
import Data.CaseInsensitive hiding (map)
import qualified Data.CaseInsensitive as CI
import Network.ByteOrder
import Network.HPACK.Internal (
    GCBuffer,
    Size,
    entryToken,
    toEntryToken,
    toTokenHeaderTable,
 )
import Network.HTTP.Types
import Network.QUIC.Internal (StreamId)

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

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

-- | QPACK encoder.
type QEncoder = StreamId -> TokenHeaderList -> IO EncodedFieldSection

-- | QPACK simple encoder.
type QEncoderS = StreamId -> [Header] -> IO EncodedFieldSection

-- | QPACK decoder.
type QDecoder = StreamId -> EncodedFieldSection -> IO TokenHeaderTable

-- | QPACK simple decoder.
type QDecoderS = StreamId -> EncodedFieldSection -> IO (Maybe [Header])

-- | Encoder instruction handler.
type EncoderInstructionHandler = (Int -> IO EncodedEncoderInstruction) -> IO ()

-- | Simple encoder instruction handler.
--   Leftover is returned.
type EncoderInstructionHandlerS =
    EncodedEncoderInstruction -> IO EncodedEncoderInstruction

-- | Encoded decoder instruction.
type EncodedDecoderInstruction = ByteString

-- | Decoder instruction handler.
type DecoderInstructionHandler = (Int -> IO EncodedDecoderInstruction) -> IO ()

-- | A type to integrating handlers.
type InstructionHandler = (Int -> IO ByteString) -> IO ()

data TableOperation = TableOperation
    { setCapacity :: Int -> IO ()
    , setBlockedStreams :: Int -> IO ()
    , setHeaderSize :: Int -> IO ()
    }

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

-- | Configuration for QPACK encoder.
data QEncoderConfig = QEncoderConfig
    { ecMaxTableCapacity :: Size
    , ecHeaderBlockBufferSize :: Size
    , ecInstructionBufferSize :: Size
    }
    deriving (Show)

-- | Default configuration for QPACK encoder.
--
-- >>> defaultQEncoderConfig
-- QEncoderConfig {ecMaxTableCapacity = 4096, ecHeaderBlockBufferSize = 4096, ecInstructionBufferSize = 4096}
defaultQEncoderConfig :: QEncoderConfig
defaultQEncoderConfig =
    QEncoderConfig
        { ecMaxTableCapacity = 4096
        , ecHeaderBlockBufferSize = 4096
        , ecInstructionBufferSize = 4096
        }

-- | Creating a new QPACK encoder.
newQEncoder
    :: QEncoderConfig
    -> (EncodedEncoderInstruction -> IO ())
    -> IO (QEncoder, DecoderInstructionHandler, TableOperation)
newQEncoder QEncoderConfig{..} sendEI = do
    let bufsiz1 = ecHeaderBlockBufferSize
        bufsiz2 = ecInstructionBufferSize
    gcbuf1 <- mallocPlainForeignPtrBytes bufsiz1
    gcbuf2 <- mallocPlainForeignPtrBytes bufsiz2
    dyntbl <- newDynamicTableForEncoding sendEI
    lock <- newMVar ()
    let enc =
            qpackEncoder
                gcbuf1
                bufsiz1
                gcbuf2
                bufsiz2
                dyntbl
                lock
        handler = decoderInstructionHandler dyntbl
        ctl =
            TableOperation
                { setCapacity = \n -> do
                    -- "n" is decoder-proposed size via settings.
                    let tableSize = min ecMaxTableCapacity n
                    setTableCapacity dyntbl tableSize
                    ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False
                    sendIns dyntbl ins
                , setBlockedStreams = setMaxBlockedStreams dyntbl
                , setHeaderSize = setMaxHeaderSize dyntbl
                }
    return (enc, handler, ctl)

tokenHeaderSize :: TokenHeader -> Int
tokenHeaderSize (t, v) = BS.length (CI.original (tokenKey t)) + BS.length v + 8 -- adhoc overhead

split :: Int -> TokenHeaderList -> (TokenHeaderList, TokenHeaderList)
split lim ts = split' 0 ts
  where
    split' _ [] = ([], [])
    split' s xxs@(x : xs)
        | siz > lim = E.throw BufferOverrun
        | s' < lim =
            let (ys, zs) = split' s' xs
             in (x : ys, zs)
        | otherwise = ([], xxs)
      where
        siz = tokenHeaderSize x
        s' = s + siz

splitThrough :: Int -> TokenHeaderList -> [TokenHeaderList]
splitThrough lim ts0 = loop ts0 id
  where
    loop [] builder = builder []
    loop ts builder = loop ts2 (builder . (ts1 :))
      where
        (ts1, ts2) = split lim ts

qpackEncoder
    :: GCBuffer
    -> Int
    -> GCBuffer
    -> Int
    -> DynamicTable
    -> MVar ()
    -> QEncoder
qpackEncoder gcbuf1 bufsiz1 gcbuf2 bufsiz2 dyntbl lock sid ts =
    withMVar lock $ \_ ->
        withForeignPtr gcbuf1 $ \buf1 ->
            withForeignPtr gcbuf2 $ \buf2 -> do
                siz <- getTableCapacity dyntbl
                qpackDebug dyntbl $
                    putStrLn $
                        "---- Stream " ++ show sid ++ " " ++ "tblsiz: " ++ show siz
                setBasePointToInsersionPoint dyntbl
                clearRequiredInsertCount dyntbl
                let tss = splitThrough bufsiz1 ts
                his <- mapM (qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 dyntbl) tss
                let (hbs, daiss) = unzip his
                prefix <- qpackEncodePrefix buf1 bufsiz1 dyntbl
                let section = BS.concat (prefix : hbs)
                reqInsCnt <- getRequiredInsertCount dyntbl
                -- To count only blocked sections,
                -- dont' register this section if reqInsCnt == 0.
                when (reqInsCnt /= 0) $
                    insertSection dyntbl sid $
                        Section reqInsCnt $
                            concat daiss
                return section

qpackEncoderS
    :: GCBuffer
    -> Int
    -> GCBuffer
    -> Int
    -> DynamicTable
    -> MVar ()
    -> QEncoderS
qpackEncoderS gcbuf1 bufsiz1 gcbuf2 bufsiz2 dyntbl lock sid hs =
    withMVar lock $ \_ ->
        withForeignPtr gcbuf1 $ \buf1 ->
            withForeignPtr gcbuf2 $ \buf2 -> do
                siz <- getTableCapacity dyntbl
                qpackDebug dyntbl $
                    putStrLn $
                        "---- Stream " ++ show sid ++ " " ++ "tblsiz: " ++ show siz
                setBasePointToInsersionPoint dyntbl
                clearRequiredInsertCount dyntbl
                let tss = splitThrough bufsiz1 ts
                his <- mapM (qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 dyntbl) tss
                let (hbs, daiss) = unzip his
                prefix <- qpackEncodePrefix buf1 bufsiz1 dyntbl
                let section = BS.concat (prefix : hbs)
                reqInsCnt <- getRequiredInsertCount dyntbl
                -- To count only blocked sections,
                -- dont' register this section if reqInsCnt == 0.
                immAck <- getImmediateAck dyntbl
                when (reqInsCnt /= 0) $ do
                    blocked <- wouldSectionBeBlocked dyntbl reqInsCnt
                    when blocked $ insertBlockedStreamE dyntbl sid
                    let dais = concat daiss
                    insertSection dyntbl sid $ Section reqInsCnt dais
                    when immAck $ do
                        -- The same logic of SectionAcknowledgement.
                        updateKnownReceivedCount dyntbl reqInsCnt
                        mapM_ (decreaseReference dyntbl) dais
                        deleteBlockedStreamE dyntbl sid
                -- Need to emulate InsertCountIncrement since
                -- SectionAcknowledgement is not returned if
                -- RequiredInsertCount is 0.
                when immAck $ setInsersionPointToKnownReceivedCount dyntbl
                return section
  where
    mk' (k, v) = (t, v)
      where
        t = toToken $ foldedCase k
    ts = map mk' hs

qpackEncodeHeader
    :: Buffer
    -> BufferSize
    -> Buffer
    -> BufferSize
    -> DynamicTable
    -> TokenHeaderList
    -> IO (ByteString, [AbsoluteIndex])
qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 dyntbl ts = do
    wbuf1 <- newWriteBuffer buf1 bufsiz1
    wbuf2 <- newWriteBuffer buf2 bufsiz2
    dais <- encodeTokenHeader wbuf1 wbuf2 dyntbl ts
    hb <- toByteString wbuf1
    ins <- toByteString wbuf2
    when (ins /= "") $ sendIns dyntbl ins
    return (hb, dais)

qpackEncodePrefix :: Buffer -> BufferSize -> DynamicTable -> IO ByteString
qpackEncodePrefix buf1 bufsiz1 dyntbl = do
    wbuf1 <- newWriteBuffer buf1 bufsiz1
    encodePrefix wbuf1 dyntbl
    toByteString wbuf1

-- Note: dyntbl for encoder
decoderInstructionHandler :: DynamicTable -> DecoderInstructionHandler
decoderInstructionHandler dyntbl recv = loop ""
  where
    loop bs0 = do
        bs1 <- recv 1024
        let bs
                | bs0 == "" = bs1
                | otherwise = bs0 <> bs1
        when (bs /= "") $ do
            (ins, leftover) <- decodeDecoderInstructions bs
            qpackDebug dyntbl $ mapM_ print ins
            mapM_ handle ins
            loop leftover
    handle (SectionAcknowledgement sid) = do
        msec <- getAndDelSection dyntbl sid
        case msec of
            Nothing -> E.throwIO DecoderInstructionError
            Just (Section reqInsCnt ais) -> do
                updateKnownReceivedCount dyntbl reqInsCnt
                mapM_ (decreaseReference dyntbl) ais
                deleteBlockedStreamE dyntbl sid
    handle (StreamCancellation _n) = return () -- fixme
    handle (InsertCountIncrement n)
        | n == 0 = E.throwIO DecoderInstructionError
        | otherwise = incrementKnownReceivedCount dyntbl n

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

newQEncoderS
    :: QEncoderConfig -- capacity
    -> (EncodedEncoderInstruction -> IO ())
    -> Int -- blocked stream
    -> Bool -- immediate Acks
    -> Bool -- debug
    -> IO QEncoderS
newQEncoderS QEncoderConfig{..} saveEI blocked immediateAck debug = do
    let bufsiz1 = ecHeaderBlockBufferSize
        bufsiz2 = ecInstructionBufferSize
    gcbuf1 <- mallocPlainForeignPtrBytes bufsiz1
    gcbuf2 <- mallocPlainForeignPtrBytes bufsiz2
    dyntbl <- newDynamicTableForEncoding saveEI
    setTableCapacity dyntbl ecMaxTableCapacity
    setMaxBlockedStreams dyntbl blocked
    setImmediateAck dyntbl immediateAck
    setDebugQPACK dyntbl debug
    lock <- newMVar ()
    let enc =
            qpackEncoderS
                gcbuf1
                bufsiz1
                gcbuf2
                bufsiz2
                dyntbl
                lock
    return enc

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

-- | Configuration for QPACK decoder.
data QDecoderConfig = QDecoderConfig
    { dcMaxTableCapacity :: Size
    , dcHuffmanBufferSize :: Size -- for encoder insteruction handler
    , dcBlockedSterams :: Int
    , dcMaxFieldSectionSize :: Int
    }
    deriving (Show)

-- | Default configuration for QPACK decoder.
--
-- >>> defaultQDecoderConfig
-- QDecoderConfig {dcMaxTableCapacity = 4096, dcHuffmanBufferSize = 2048, dcBlockedSterams = 100, dcMaxFieldSectionSize = 32768}
defaultQDecoderConfig :: QDecoderConfig
defaultQDecoderConfig =
    QDecoderConfig
        { dcMaxTableCapacity = 4096
        , dcHuffmanBufferSize = 2048 -- no global locking
        , dcBlockedSterams = 100
        , dcMaxFieldSectionSize = 32768
        }

-- | Creating a new QPACK decoder.
newQDecoder
    :: QDecoderConfig
    -> (EncodedDecoderInstruction -> IO ())
    -> IO (QDecoder, EncoderInstructionHandler)
newQDecoder QDecoderConfig{..} sendDI = do
    dyntbl <-
        newDynamicTableForDecoding dcHuffmanBufferSize sendDI
    setMaxBlockedStreams dyntbl dcBlockedSterams
    let dec = qpackDecoder dyntbl
        handler = encoderInstructionHandler dcMaxTableCapacity dyntbl
    return (dec, handler)

-- | Creating a new simple QPACK decoder.
newQDecoderS
    :: QDecoderConfig
    -> (EncodedDecoderInstruction -> IO ())
    -> Bool
    -> IO (QDecoderS, EncoderInstructionHandlerS)
newQDecoderS QDecoderConfig{..} sendDI debug = do
    dyntbl <-
        newDynamicTableForDecoding dcHuffmanBufferSize sendDI
    setMaxBlockedStreams dyntbl dcBlockedSterams
    setDebugQPACK dyntbl debug
    let dec = qpackDecoderS dyntbl
        handler = encoderInstructionHandlerS dcMaxTableCapacity dyntbl
    return (dec, handler)

qpackDecoder
    :: DynamicTable -> StreamId -> EncodedFieldSection -> IO TokenHeaderTable
qpackDecoder dyntbl sid bs = do
    (tbl, needAck) <- withReadBuffer bs $ \rbuf -> decodeTokenHeader dyntbl rbuf
    when needAck $
        encodeDecoderInstructions [SectionAcknowledgement sid] >>= sendIns dyntbl
    return tbl

qpackDecoderS
    :: DynamicTable -> StreamId -> EncodedFieldSection -> IO (Maybe [Header])
qpackDecoderS dyntbl sid bs = do
    qpackDebug dyntbl $ putStrLn $ "---- Stream " ++ show sid
    mhs <- withReadBuffer bs $ \rbuf -> decodeTokenHeaderS dyntbl rbuf
    case mhs of
        Nothing -> return Nothing
        Just (hs, needAck) -> do
            when needAck $
                encodeDecoderInstructions [SectionAcknowledgement sid] >>= sendIns dyntbl
            return $ Just hs

-- Note: dyntbl for decoder
encoderInstructionHandler :: Int -> DynamicTable -> EncoderInstructionHandler
encoderInstructionHandler decCapLim dyntbl recv = loop ""
  where
    loop bs0 = do
        bs1 <- recv 1024
        let bs
                | bs0 == "" = bs1
                | otherwise = bs0 <> bs1
        when (bs /= "") $ do
            leftover <- encoderInstructionHandlerS decCapLim dyntbl bs
            loop leftover

-- Note: dyntbl for decoder
encoderInstructionHandlerS :: Int -> DynamicTable -> EncoderInstructionHandlerS
encoderInstructionHandlerS _ _dyntbl "" = return ""
encoderInstructionHandlerS decCapLim dyntbl bs = do
    (ins, leftover) <- decodeEncoderInstructions hufdec bs
    cnt <- sum <$> mapM handle ins
    when (cnt /= 0) $
        encodeDecoderInstructions [InsertCountIncrement cnt] >>= sendIns dyntbl
    return leftover
  where
    hufdec = getHuffmanDecoder dyntbl -- only for encoder instruction handler
    handle ins@(SetDynamicTableCapacity n)
        | n > decCapLim = E.throwIO EncoderInstructionError
        | otherwise = do
            setTableCapacity dyntbl n
            qpackDebug dyntbl $ print ins
            return 0
    handle ins@(InsertWithNameReference ii val) = do
        ready <- isTableReady dyntbl
        unless ready $ E.throwIO EncoderInstructionError
        dai <- atomically $ do
            idx <- case ii of
                Left ai -> return $ SIndex ai
                Right ri -> do
                    ip <- getInsertionPointSTM dyntbl
                    return $ DIndex $ fromInsRelativeIndex ri ip
            ent0 <- toIndexedEntry dyntbl idx
            let ent = toEntryToken (entryToken ent0) val
            _ <- insertEntryToDecoder ent dyntbl
            return idx
        qpackDebug dyntbl $ putStrLn $ show ins ++ ": " ++ show dai
        return 1
    handle ins@(InsertWithLiteralName t val) = do
        ready <- isTableReady dyntbl
        unless ready $ E.throwIO EncoderInstructionError
        dai <- atomically $ do
            let ent = toEntryToken t val
            insertEntryToDecoder ent dyntbl
        qpackDebug dyntbl $ putStrLn $ show ins ++ ": " ++ show dai
        return 1
    handle ins@(Duplicate ri) = do
        ready <- isTableReady dyntbl
        unless ready $ E.throwIO EncoderInstructionError
        (dai, dai') <- atomically $ do
            ip <- getInsertionPointSTM dyntbl
            let ai = fromInsRelativeIndex ri ip
                idx = DIndex ai
            ent <- toIndexedEntry dyntbl idx
            ai' <- insertEntryToDecoder ent dyntbl
            return (ai, ai')
        qpackDebug dyntbl $
            putStrLn $
                show ins ++ ": " ++ show dai ++ " -> " ++ show dai'
        return 1