packages feed

http3 0.1.5 → 0.1.6

raw patch · 21 files changed

+1397/−122 lines, 21 filesdep ~http2dep ~quicPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: http2, quic

API changes (from Hackage documentation)

+ Network.HTTP3.Internal: ConnectionSpecificField :: ByteString -> ConnectionSpecificField
+ Network.HTTP3.Internal: newtype ConnectionSpecificField
+ Network.QPACK: FieldSectionTooLarge :: FieldSectionTooLarge
+ Network.QPACK: FieldSectionTooLargeForPeer :: Int -> Int -> FieldSectionTooLargeForPeer
+ Network.QPACK: [getHeaderSize] :: TableOperation -> IO Int
+ Network.QPACK: [peerLimit] :: FieldSectionTooLargeForPeer -> Int
+ Network.QPACK: [sectionSize] :: FieldSectionTooLargeForPeer -> Int
+ Network.QPACK: data FieldSectionTooLarge
+ Network.QPACK: data FieldSectionTooLargeForPeer
+ Network.QPACK: fieldSectionSize :: TokenHeaderList -> Int
+ Network.QPACK.Internal: FieldSectionTooLarge :: FieldSectionTooLarge
+ Network.QPACK.Internal: FieldSectionTooLargeForPeer :: Int -> Int -> FieldSectionTooLargeForPeer
+ Network.QPACK.Internal: [peerLimit] :: FieldSectionTooLargeForPeer -> Int
+ Network.QPACK.Internal: [sectionSize] :: FieldSectionTooLargeForPeer -> Int
+ Network.QPACK.Internal: data FieldSectionTooLarge
+ Network.QPACK.Internal: data FieldSectionTooLargeForPeer
+ Network.QPACK.Internal: getAndDelSections :: DynamicTable -> StreamId -> IO [Section]
+ Network.QPACK.Internal: getMaxEntries :: DynamicTable -> IO Int
+ Network.QPACK.Internal: setDecoderTableCapacity :: DynamicTable -> Int -> IO ()
+ Network.QPACK.Internal: setMaxEntries :: DynamicTable -> Int -> IO ()
+ Network.QPACK.Internal: unblockStreamsE :: DynamicTable -> IO ()
- Network.QPACK: TableOperation :: (Int -> IO ()) -> (Int -> IO ()) -> (Int -> IO ()) -> TableOperation
+ Network.QPACK: TableOperation :: (Int -> IO ()) -> (Int -> IO ()) -> (Int -> IO ()) -> IO Int -> TableOperation

Files

ChangeLog.md view
@@ -1,5 +1,45 @@ # Revision history for http3 +## 0.1.6++* Requiring quic v0.3.9 and http2 v5.4.6.  quic v0.3.7 stops opening a+  closed stream again for a late copy of its data, which closed the+  connection with FLOW_CONTROL_ERROR now and then (#15); v0.3.8 tells a+  stream that was reset from one that ended; v0.3.9 keeps the peer's open+  streams within initial_max_streams, closes the sending part on+  STOP_SENDING, and opens a stream for a RESET_STREAM that comes before+  its data.+* QPACK: fixing the dynamic table where the two ends disagreed: the+  maximum number of entries, when a section is blocked, the blocked+  streams, more than one outstanding section on a stream, and a change of+  capacity.  Encoding a field larger than the encoder's buffers.+* QPACK: stopping counting a blocked stream however its wait ends.+* QPACK: evicting an entry nothing refers to once its insertion is+  acknowledged.+* QPACK: sending Stream Cancellation for a stream not read to its end or+  reset by the peer, and acting on one received.+* QPACK: inserting with a reference to a name not yet acknowledged.+* Holding a field section to our SETTINGS_MAX_FIELD_SECTION_SIZE as it+  decodes, not only the frame carrying it; a server answers a request+  over it with 431.  The new `FieldSectionTooLarge` is thrown otherwise.+* Keeping to the peer's SETTINGS_MAX_FIELD_SECTION_SIZE when sending.  The+  new `FieldSectionTooLargeForPeer` is thrown by `sendRequest` and+  `sendResponse` for a header section over it.+* Reading on past a DATA frame that is empty.+* Treating a message with a connection-specific field as malformed.  The+  new `ConnectionSpecificField` is thrown for a response or trailers+  carrying one.+* Refusing a second control or QPACK stream, and noticing a QPACK stream+  closing.  Refusing a push stream.+* Stopping reading a unidirectional stream of an unknown type, and+  refusing a server-initiated bidirectional stream.+* A client whose request the server stops with STOP_SENDING no longer+  resets the stream, so that a response sent after it is still read.+* Closing a unidirectional stream we stop reading, so that it is given+  back to the peer's limit.+* This is a patch release, but `TableOperation` in `Network.QPACK` has a+  new field, `getHeaderSize`.+ ## 0.1.5  * Security fixes.  Requiring http2 v5.4.5, whose HPACK integer decoder is
Network/HTTP3/Client.hs view
@@ -35,7 +35,6 @@ import Control.Concurrent import qualified Control.Exception as E import qualified Data.ByteString.UTF8 as UTF8-import Data.IORef import Data.IP (IPv6) import Network.HTTP.Semantics.Client import Network.HTTP.Semantics.Client.Internal@@ -90,31 +89,62 @@         | QUIC.isServerInitiatedUnidirectional sid =             forkManaged ctx "H3 client: unidirectional handler" $                 unidirectional ctx strm-        | otherwise = return () -- push?+        -- Server-initiated bidirectional.  RFC 9114, section 6.1: "Clients+        -- MUST treat receipt of a server-initiated bidirectional stream as a+        -- connection error of type H3_STREAM_CREATION_ERROR unless such an+        -- extension has been negotiated."+        | otherwise = abort ctx H3StreamCreationError       where         sid = QUIC.streamId strm  sendRequest     :: Context -> Scheme -> Authority -> Request -> (Response -> IO a) -> IO a-sendRequest ctx scm auth (Request outobj) processResponse =+sendRequest ctx scm auth (Request outobj) processResponse = do+    -- RFC 9114, section 4.2.2: an endpoint "SHOULD NOT send an HTTP message+    -- header that exceeds the indicated size".  Here, so that the caller is+    -- the one told.+    checkHeaderSize ctx hdr'     E.bracket (newStream ctx) closeStream $ \strm -> do-        forkManagedTimeout ctx "H3 client: sendRequest" $ \th -> do-            sendHeader ctx strm th hdr'-            sendBody ctx strm th outobj-            QUIC.shutdownStream strm+        forkManagedTimeout ctx "H3 client: sendRequest" $ \th ->+            ( do+                sendHeader ctx strm th hdr'+                sendBody ctx strm th outobj+                QUIC.shutdownStream strm+            )+                -- What goes wrong here is thrown away with the thread, and+                -- the request would be left half sent, with the response+                -- awaited for good.  Trailers too large for the peer are one+                -- such thing.+                `E.catch` \se -> case E.fromException se of+                    _ | isAsyncException se -> E.throwIO se+                    -- The sending part is closed: the server asked us with+                    -- STOP_SENDING to stop, and it has had its RESET_STREAM+                    -- for that already, or we are done with the stream.+                    -- The response may be on its way yet, and resetting+                    -- here takes the stream out of the table, after which+                    -- whatever arrives for it is thrown away: the response+                    -- would be awaited for good.+                    Just QUIC.StreamIsClosed -> return ()+                    _ -> QUIC.resetStream strm H3RequestCancelled         src <- newSource strm         let sid = QUIC.streamId strm-        mvt <- recvHeader ctx sid src-        case mvt of-            Nothing -> do-                QUIC.resetStream strm H3MessageError-                threadDelay 100000-                -- just for type inference-                E.throwIO $ QUIC.ApplicationProtocolErrorIsSent H3MessageError ""-            Just vt@(_, valtbl) -> do-                (readB, refH) <- newBodyReader ctx sid src valtbl-                let rsp = Response $ InpObj vt Nothing readB refH-                processResponse rsp+        (`E.finally` cancelUnlessReadToEnd ctx sid src) $ do+            mvt <- recvHeader ctx sid src+            case mvt of+                Nothing -> do+                    QUIC.resetStream strm H3MessageError+                    threadDelay 100000+                    -- just for type inference+                    E.throwIO $ QUIC.ApplicationProtocolErrorIsSent H3MessageError ""+                Just vt+                    -- Malformed (RFC 9114, section 4.2).+                    | Just name <- connectionSpecificField vt -> do+                        QUIC.resetStream strm H3MessageError+                        E.throwIO $ ConnectionSpecificField name+                Just vt@(_, valtbl) -> do+                    (readB, refH) <- newBodyReader ctx sid src valtbl+                    let rsp = Response $ InpObj vt Nothing readB refH+                    processResponse rsp   where     hdr = outObjHeaders outobj     isIPv6 = isJust (readMaybe auth :: Maybe IPv6)
Network/HTTP3/Context.hs view
@@ -5,6 +5,7 @@     Context,     withContext,     unidirectional,+    cancelStream,     isH3Server,     isH3Client,     accept,@@ -20,6 +21,7 @@     getMySockAddr,     getPeerSockAddr,     getMaxFieldSectionSize,+    getPeerMaxFieldSectionSize,     forkManaged,     forkManagedTimeout,     forkManagedTimeoutFinally,@@ -31,12 +33,13 @@ import Data.IORef import Network.HTTP.Semantics.Client import Network.QUIC-import Network.QUIC.Internal (connDebugLog, isClient, isServer)+import Network.QUIC.Internal (isClient, isServer) import Network.Socket (SockAddr) import qualified System.ThreadManager as T  import Network.HTTP3.Config import Network.HTTP3.Control+import Network.HTTP3.Error import Network.HTTP3.Frame import Network.HTTP3.Stream import Network.QPACK@@ -55,6 +58,11 @@     , ctxMaxFieldSectionSize :: Int     -- ^ What we told the peer we would accept, and so the most of any one     -- frame we are willing to hold in memory while it arrives.+    , ctxCancelStream :: StreamId -> IO ()+    , ctxPeerMaxFieldSectionSize :: IO Int+    -- ^ The peer's SETTINGS_MAX_FIELD_SECTION_SIZE, or 'maxBound' until its+    -- SETTINGS have arrived+    -- ^ Sending a Stream Cancellation on our QPACK decoder stream     }  withContext :: Connection -> Config -> (Context -> IO a) -> IO a@@ -70,18 +78,34 @@     (ctxQDecoder, handleEI) <- newQDecoder (confQDecoderConfig conf) sendDI     let ctxMaxFieldSectionSize = dcMaxFieldSectionSize $ confQDecoderConfig conf     ctl <- controlStream conn ctxMaxFieldSectionSize dyntblE <$> newIORef IInit+    let ctxPeerMaxFieldSectionSize = getHeaderSize dyntblE+    seen <- newIORef []     info <- getConnectionInfo conn     let handleDI' recv = handleDI recv `E.catch` abortWith QpackDecoderStreamError         handleEI' recv = handleEI recv `E.catch` abortWith QpackEncoderStreamError-        ctxUniSwitch = switch conn ctl handleEI' handleDI'+        ctxUniSwitch = switch conn seen ctl handleEI' handleDI'         ctxPReadMaker = confPositionReadMaker conf         ctxHooks = confHooks conf         ctxMySockAddr = localSockAddr info         ctxPeerSockAddr = remoteSockAddr info+    let ctxCancelStream sid+            -- RFC 9204, section 4.4.2: "A decoder with a maximum dynamic table+            -- capacity equal to zero MAY omit sending Stream Cancellations",+            -- since the encoder then has nothing to release.+            | dcMaxTableCapacity (confQDecoderConfig conf) == 0 = return ()+            | otherwise =+                -- The stream is being given up on, and so, quite possibly, is+                -- the connection; there is nobody to report a failure to.+                (encodeDecoderInstructions [StreamCancellation sid] >>= sendDI)+                    `E.catch` ignoreSync     ctxThreadManager <- T.newThreadManager $ confTimeoutManager conf     let ctxConnection = conn     return Context{..}   where+    ignoreSync :: E.SomeException -> IO ()+    ignoreSync se+        | isAsyncException se = E.throwIO se+        | otherwise = return ()     abortWith :: ApplicationProtocolError -> E.SomeException -> IO ()     abortWith aerr se         | isAsyncException se = E.throwIO se@@ -93,18 +117,47 @@         Just (E.SomeAsyncException _) -> True         Nothing -> False +-- | The handler for a unidirectional stream the peer opened, by its type.+--+-- The control stream and the two QPACK streams are critical: a peer may open+-- each of them only once (RFC 9114, section 6.2.1; RFC 9204, section 4.2),+-- and may not close any of them.  The control stream handler sees to the+-- closing itself; the QPACK ones are library code that simply returns when+-- the stream ends. switch     :: Connection+    -> IORef [H3StreamType]+    -- ^ The critical streams the peer has opened so far     -> InstructionHandler     -> InstructionHandler     -> InstructionHandler     -> H3StreamType     -> InstructionHandler-switch conn ctl handleEI handleDI styp-    | styp == H3ControlStreams = ctl-    | styp == QPACKEncoderStream = handleEI-    | styp == QPACKDecoderStream = handleDI-    | otherwise = \_ -> connDebugLog conn "switch unknown stream type"+switch conn seen ctl handleEI handleDI styp = case styp of+    H3ControlStreams -> once ctl+    QPACKEncoderStream -> once $ closing handleEI+    QPACKDecoderStream -> once $ closing handleDI+    -- RFC 9114, section 6.2.2: "Only servers can push; if a server receives+    -- a client-initiated push stream, this MUST be treated as a connection+    -- error of type H3_STREAM_CREATION_ERROR."  And section 4.6: a client+    -- "MUST treat receipt of a push stream as a connection error of type+    -- H3_ID_ERROR when no MAX_PUSH_ID frame has been sent", which this client+    -- never does.+    H3PushStreams+        | isServer conn -> \_ -> abortConnection conn H3StreamCreationError ""+        | otherwise -> \_ -> abortConnection conn H3IdError ""+    -- 'unidirectional' does not get here with an unknown type.+    H3StreamTypeUnknown _ -> \_ -> return ()+  where+    once, closing :: InstructionHandler -> InstructionHandler+    once handler recv = do+        dup <- atomicModifyIORef' seen $ \ts -> (styp : ts, styp `elem` ts)+        if dup+            then abortConnection conn H3StreamCreationError ""+            else handler recv+    closing handler recv = do+        handler recv+        abortConnection conn H3ClosedCriticalStream ""  isH3Server :: Context -> Bool isH3Server Context{..} = isServer ctxConnection@@ -121,6 +174,9 @@ qpackDecode :: Context -> QDecoder qpackDecode Context{..} = ctxQDecoder +cancelStream :: Context -> StreamId -> IO ()+cancelStream Context{..} = ctxCancelStream+ unidirectional :: Context -> Stream -> IO () unidirectional Context{..} strm = do     -- The type is a variable-length integer (RFC 9114, section 6.2), so one,@@ -132,8 +188,23 @@         -- The peer opened a unidirectional stream and closed it without         -- saying what it was for.  Nothing to dispatch to; this used to be a         -- pattern match failure.-        Nothing -> return ()-        Just i -> ctxUniSwitch (toH3StreamType i) (recvStream strm)+        --+        -- Closed here, as below, because only a stream we close is counted+        -- as done with, and so given back to the peer's limit on the+        -- unidirectional streams it may open.  One left unclosed is lost to+        -- it for good.+        Nothing -> closeStream strm+        Just i -> case toH3StreamType i of+            -- RFC 9114, section 6.2: "Recipients of unknown stream types MUST+            -- either abort reading of the stream or discard incoming data+            -- without further processing.  If reading is aborted, the+            -- recipient SHOULD use the H3_STREAM_CREATION_ERROR error code".+            -- Neither was done: whatever the peer sent was left where it+            -- arrived, for as long as the connection lasted.+            H3StreamTypeUnknown _ -> do+                stopStream strm H3StreamCreationError+                closeStream strm+            styp -> ctxUniSwitch styp (recvStream strm)  withHandle :: Context -> (T.Handle -> IO ()) -> IO () withHandle Context{..} action = void $ T.withHandle ctxThreadManager (return ()) action@@ -170,3 +241,6 @@  getMaxFieldSectionSize :: Context -> Int getMaxFieldSectionSize = ctxMaxFieldSectionSize++getPeerMaxFieldSectionSize :: Context -> IO Int+getPeerMaxFieldSectionSize = ctxPeerMaxFieldSectionSize
Network/HTTP3/Control.hs view
@@ -6,6 +6,7 @@     controlStream, ) where +import Control.Concurrent.MVar import qualified Data.ByteString as BS import Data.IORef import Data.IntSet (IntSet)@@ -47,7 +48,11 @@     H3.onControlStreamCreated hooks sC     H3.onEncoderStreamCreated hooks sE     H3.onDecoderStreamCreated hooks sD-    return (sendStream sE, sendStream sD)+    -- Every request stream writes acknowledgements and cancellations to the+    -- decoder stream, and an instruction must not be split by another one.+    -- quic sends a write in two pieces when flow control stops it partway.+    lockD <- newMVar ()+    return (sendStream sE, \bs -> withMVar lockD $ \_ -> sendStream sD bs)   where     stC = mkType H3ControlStreams     stE = mkType QPACKEncoderStream
Network/HTTP3/Error.hs view
@@ -2,6 +2,7 @@  module Network.HTTP3.Error (     ContentLengthMismatch (..),+    ConnectionSpecificField (..),     ApplicationProtocolError (         H3NoError,         H3GeneralProtocolError,@@ -24,6 +25,7 @@ ) where  import qualified Control.Exception as E+import Data.ByteString (ByteString) import Network.QUIC  -- | A message whose content does not match the content-length it declared.@@ -40,6 +42,14 @@     deriving (Eq, Show)  instance E.Exception ContentLengthMismatch++-- | A message carrying a connection-specific field, named here, which makes+--   it malformed (RFC 9114, section 4.2).  Thrown where 'ContentLengthMismatch'+--   is, and handled the same way.+newtype ConnectionSpecificField = ConnectionSpecificField ByteString+    deriving (Eq, Show)++instance E.Exception ConnectionSpecificField  {- FOURMOLU_DISABLE -} pattern H3NoError                :: ApplicationProtocolError
Network/HTTP3/Recv.hs view
@@ -8,6 +8,8 @@     readSource',     recvHeader,     newBodyReader,+    cancelUnlessReadToEnd,+    connectionSpecificField, ) where  import qualified Control.Exception as E@@ -22,13 +24,32 @@ import Network.HTTP3.Frame  data Source = Source-    { sourceRead :: IO ByteString+    { sourceStream :: Stream+    , sourceRead :: IO ByteString     , sourcePending :: IORef (Maybe ByteString)+    , sourceReadToEnd :: IORef Bool+    -- ^ Whether the body reader has come to the end of the stream between two+    -- frames, and so has processed every field section on it.     }  newSource :: Stream -> IO Source-newSource strm = Source (recvStream strm 1024) <$> newIORef Nothing+newSource strm =+    Source strm (recvStream strm 1024) <$> newIORef Nothing <*> newIORef False +-- | Telling the peer's QPACK encoder, unless the stream has been read to its+--   end, that the field sections left on it will never be processed (RFC 9204,+--   section 4.4.2): until it is told, it keeps the entries they refer to.+--+-- A stream the peer reset has not been read to its end, even if it looks so:+-- the reset reads as an end, and one that lands between two frames used to+-- be taken for the real thing.  Nothing is lost by telling an encoder about a+-- stream it has nothing outstanding on.+cancelUnlessReadToEnd :: Context -> StreamId -> Source -> IO ()+cancelUnlessReadToEnd ctx sid Source{..} = do+    done <- readIORef sourceReadToEnd+    reset <- isJust <$> resetReceived sourceStream+    when (not done || reset) $ cancelStream ctx sid+ readSource :: Source -> IO ByteString readSource Source{..} = do     mx <- readIORef sourcePending@@ -79,6 +100,25 @@                         loop IInit -- dummy                 st' -> loop st' +-- | The first connection-specific field in a field section, if any.+--+-- RFC 9114, section 4.2: "An endpoint MUST NOT generate an HTTP/3 field+-- section containing connection-specific fields; any message containing+-- connection-specific fields MUST be treated as malformed."  It names+-- Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding and Upgrade,+-- and lets TE through only with the value "trailers".  Everything else a+-- Connection field could name is a connection-specific field too, but then+-- the Connection field is there to be refused.+connectionSpecificField :: TokenHeaderTable -> Maybe ByteString+connectionSpecificField (ths, vt)+    | isJust (getFieldValue tokenConnection vt) = Just "connection"+    | isJust (getFieldValue tokenTransferEncoding vt) = Just "transfer-encoding"+    | maybe False (/= "trailers") (getFieldValue tokenTE vt) = Just "te"+    | otherwise = find (`elem` byName) $ map (foldedCase . tokenKey . fst) ths+  where+    -- No tokens of their own.+    byName = ["keep-alive", "proxy-connection", "upgrade"]+ -- | A body reader for one message, and the place its trailers will appear. -- -- The reader counts what it hands out and checks the total against@@ -126,7 +166,11 @@     loop st = do         bs <- readSource src         if bs == ""-            then endOfBody+            then do+                -- Not in the middle of a frame, which would mean it was cut+                -- off.+                when (st == IInit) $ writeIORef (sourceReadToEnd src) True+                endOfBody             else case parseH3Frame lim st bs of                 ITooLong _ _ -> do                     abort ctx H3ExcessiveLoad@@ -143,12 +187,17 @@                         writeIORef refI IInit                         -- pushbackSource src leftover -- fixme                         hdr <- qpackDecode ctx sid payload+                        forM_ (connectionSpecificField hdr) $+                            E.throwIO . ConnectionSpecificField                         writeIORef refH $ Just hdr                         endOfBody                     | typ == H3FrameData -> do                         writeIORef refI IInit                         pushbackSource src leftover-                        chunk payload+                        -- A DATA frame may be empty (RFC 9114, section+                        -- 7.2.1), and "" is how the end of the body is told+                        -- to the reader.  Go on to the next frame instead.+                        if BS.null payload then loop IInit else chunk payload                     | permittedInRequestStream typ -> do                         pushbackSource src leftover                         loop IInit
Network/HTTP3/Send.hs view
@@ -3,6 +3,7 @@  module Network.HTTP3.Send (     sendHeader,+    checkHeaderSize,     sendBody, ) where @@ -11,7 +12,6 @@ import qualified Data.ByteString.Internal as BS import Data.IORef import Foreign.ForeignPtr-import Network.HPACK.Internal (toTokenHeaderTable) import Network.HTTP.Semantics.Client import Network.HTTP.Semantics.IO import qualified Network.HTTP.Types as HT@@ -21,6 +21,19 @@ import Imports import Network.HTTP3.Context import Network.HTTP3.Frame+import Network.QPACK++-- | Refusing a header section the peer said it would not take, before+--   anything is opened or sent.+--+-- The encoder refuses it too, but on a client that happens in the thread+-- sending the request, which has nobody to tell.+checkHeaderSize :: Context -> HT.RequestHeaders -> IO ()+checkHeaderSize ctx hdrs = do+    (ths, _) <- toTokenHeaderTable hdrs+    lim <- getPeerMaxFieldSectionSize ctx+    let siz = fieldSectionSize ths+    when (siz > lim) $ E.throwIO $ FieldSectionTooLargeForPeer siz lim  sendHeader :: Context -> Stream -> T.Handle -> HT.ResponseHeaders -> IO () sendHeader ctx strm th hdrs = do
Network/HTTP3/Server.hs view
@@ -33,7 +33,6 @@ import Control.Concurrent.Async import Control.Concurrent.STM import qualified Control.Exception as E-import Data.IORef import GHC.Conc.Sync import Network.HTTP.Semantics import Network.HTTP.Semantics.Server@@ -48,7 +47,6 @@ import Network.HTTP3.Config import Network.HTTP3.Context import Network.HTTP3.Error-import Network.HTTP3.Frame import Network.HTTP3.Recv import Network.HTTP3.Send import Network.QPACK@@ -105,37 +103,45 @@     -> IO () processRequest ctx server strm th = E.handle reset $ do     src <- newSource strm-    mvt <- recvHeader ctx sid src-    case mvt of-        Nothing -> QUIC.resetStream strm H3MessageError-        Just ht -> do-            mreq <- mkRequest ctx strm src ht-            case mreq of-                -- Malformed; 'mkRequest' has reset the stream.-                Nothing -> return ()-                Just req -> do-                    let aux =-                            defaultAux-                                { auxTimeHandle = th-                                , auxMySockAddr = getMySockAddr ctx-                                , auxPeerSockAddr = getPeerSockAddr ctx-                                }-                    server req aux $ sendResponse ctx strm th+    (`E.finally` cancelUnlessReadToEnd ctx sid src) $ do+        emvt <- E.try $ recvHeader ctx sid src+        case emvt of+            Left FieldSectionTooLarge -> refuseTooLarge ctx strm th+            Right Nothing -> QUIC.resetStream strm H3MessageError+            Right (Just ht) -> do+                mreq <- mkRequest ctx strm src ht+                case mreq of+                    -- Malformed; 'mkRequest' has reset the stream.+                    Nothing -> return ()+                    Just req -> do+                        let aux =+                                defaultAux+                                    { auxTimeHandle = th+                                    , auxMySockAddr = getMySockAddr ctx+                                    , auxPeerSockAddr = getPeerSockAddr ctx+                                    }+                        server req aux $ sendResponse ctx strm th   where     sid = QUIC.streamId strm     reset se         | isAsyncException se = E.throwIO se         | Just (_ :: DecodeError) <- E.fromException se =             abort ctx QpackDecompressionFailed+        -- The response was more than the client will take, and the+        -- application did nothing about it.  Our failure, not the client's.+        | Just (_ :: FieldSectionTooLargeForPeer) <- E.fromException se =+            QUIC.resetStream strm H3InternalError         | otherwise = QUIC.resetStream strm H3MessageError  processRequestIO :: Context -> ((Stream, Request) -> IO ()) -> Stream -> IO () processRequestIO ctx put strm = E.handle reset $ do     src <- newSource strm-    mvt <- recvHeader ctx sid src-    case mvt of-        Nothing -> QUIC.resetStream strm H3MessageError-        Just ht -> do+    emvt <- E.try $ recvHeader ctx sid src+    case emvt of+        Left FieldSectionTooLarge ->+            void $ withHandle ctx $ refuseTooLarge ctx strm+        Right Nothing -> QUIC.resetStream strm H3MessageError+        Right (Just ht) -> do             mreq <- mkRequest ctx strm src ht             case mreq of                 Nothing -> return ()@@ -148,6 +154,19 @@             abort ctx QpackDecompressionFailed         | otherwise = QUIC.resetStream strm H3MessageError +-- | Answering a request whose header section is more than we said we would+--   take.+--+-- RFC 9114, section 4.2.2: "A server that receives a larger field section+-- than it is willing to handle can send an HTTP 431 (Request Header Fields+-- Too Large) status code".  What is left of the request is not wanted, and+-- section 4.1.1 lets a server say so with STOP_SENDING and H3_NO_ERROR.+refuseTooLarge :: Context -> Stream -> T.Handle -> IO ()+refuseTooLarge ctx strm th = do+    QUIC.stopStream strm H3NoError+    sendHeader ctx strm th [(":status", "431")]+    QUIC.shutdownStream strm+ -- | Build the 'Request', or reset the stream and answer 'Nothing' when the -- message is malformed. --@@ -168,12 +187,14 @@         mScheme = getFieldValue tokenScheme vt         mAuthority = getFieldValue tokenAuthority vt         mPath = getFieldValue tokenPath vt+        malformed = do+            QUIC.resetStream strm H3MessageError+            return Nothing     case (mMethod, mScheme, mAuthority, mPath) of+        _ | isJust (connectionSpecificField ht) -> malformed         (Just "CONNECT", _, Just _, _) -> Just <$> build         (Just _, Just _, Just _, Just _) -> Just <$> build-        _ -> do-            QUIC.resetStream strm H3MessageError-            return Nothing+        _ -> malformed   where     build = do         let sid = QUIC.streamId strm
Network/QPACK.hs view
@@ -9,6 +9,7 @@     QEncoder,     newQEncoder,     TableOperation (..),+    fieldSectionSize,      -- ** Encoder for debugging     QEncoderS,@@ -19,6 +20,8 @@     defaultQDecoderConfig,     QDecoder,     newQDecoder,+    FieldSectionTooLarge (..),+    FieldSectionTooLargeForPeer (..),      -- ** Decoder for debugging     QDecoderS,@@ -104,8 +107,19 @@     { setCapacity :: Int -> IO ()     , setBlockedStreams :: Int -> IO ()     , setHeaderSize :: Int -> IO ()+    , getHeaderSize :: IO Int+    -- ^ The peer's SETTINGS_MAX_FIELD_SECTION_SIZE, or 'maxBound' until it+    -- is known     } +-- | The size of a field section as SETTINGS_MAX_FIELD_SECTION_SIZE counts+--   it: the lengths of each name and value plus 32 for every field (RFC+--   9114, section 4.2.2).+fieldSectionSize :: TokenHeaderList -> Int+fieldSectionSize = foldl' (\n (t, v) -> n + keyLength t + BS.length v + 32) 0+  where+    keyLength = BS.length . CI.original . tokenKey+ ----------------------------------------------------------------  -- | Configuration for QPACK encoder.@@ -151,36 +165,45 @@                 ecUseHuffman                 dyntbl                 lock-        handler = decoderInstructionHandler dyntbl+        handler = decoderInstructionHandler dyntbl lock         ctl =             TableOperation-                { setCapacity = \n -> do+                { setCapacity = \n -> withMVar lock $ \_ -> do                     -- "n" is decoder-proposed size via settings.+                    -- It, not the capacity chosen below, determines+                    -- MaxEntries.+                    setMaxEntries dyntbl n                     let tableSize = min ecMaxTableCapacity n                     setTableCapacity dyntbl tableSize                     ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False                     sendIns dyntbl ins                 , setBlockedStreams = setMaxBlockedStreams dyntbl                 , setHeaderSize = setMaxHeaderSize dyntbl+                , getHeaderSize = getMaxHeaderSize dyntbl                 }     return (enc, handler, ctl)  tokenHeaderSize :: TokenHeader -> Int tokenHeaderSize (t, v) = BS.length (CI.original (tokenKey t)) + BS.length v + 8 -- adhoc overhead +-- | Taking as many fields as fit in @lim@, but always at least one.+--+-- A field that does not fit on its own makes a chunk by itself, which+-- 'encodeChunk' gives buffers of its own.  It used to be refused with+-- 'BufferOverrun' -- so a value of 4K or so could not be sent at all -- and+-- one that came to exactly @lim@ produced an empty chunk and left the rest+-- as it was, so 'splitThrough' went round forever. 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 =+        | s /= 0 && s' > lim = ([], xxs)+        | otherwise =             let (ys, zs) = split' s' xs              in (x : ys, zs)-        | otherwise = ([], xxs)       where-        siz = tokenHeaderSize x-        s' = s + siz+        s' = s + tokenHeaderSize x  splitThrough :: Int -> TokenHeaderList -> [TokenHeaderList] splitThrough lim ts0 = loop ts0 id@@ -190,6 +213,33 @@       where         (ts1, ts2) = split lim ts +-- | Encoding a chunk from 'splitThrough', in the shared buffers when it fits.+--+-- One that does not is a single large field, and gets buffers of its own.+-- Their size allows for Huffman coding being tried in place before it is known+-- to be the shorter: a code is at most 30 bits, so under four octets per octet.+encodeChunk+    :: Buffer+    -> BufferSize+    -> Buffer+    -> BufferSize+    -> Bool+    -> DynamicTable+    -> TokenHeaderList+    -> IO (ByteString, [AbsoluteIndex])+encodeChunk buf1 bufsiz1 buf2 bufsiz2 huff dyntbl ts+    | siz <= min bufsiz1 bufsiz2 =+        qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 huff dyntbl ts+    | otherwise = do+        let bufsiz = siz * 4 + 64+        gcbuf1 <- mallocPlainForeignPtrBytes bufsiz+        gcbuf2 <- mallocPlainForeignPtrBytes bufsiz+        withForeignPtr gcbuf1 $ \b1 ->+            withForeignPtr gcbuf2 $ \b2 ->+                qpackEncodeHeader b1 bufsiz b2 bufsiz huff dyntbl ts+  where+    siz = sum $ map tokenHeaderSize ts+ qpackEncoder     :: GCBuffer     -> Int@@ -199,7 +249,11 @@     -> DynamicTable     -> MVar ()     -> QEncoder-qpackEncoder gcbuf1 bufsiz1 gcbuf2 bufsiz2 huff dyntbl lock sid ts =+qpackEncoder gcbuf1 bufsiz1 gcbuf2 bufsiz2 huff dyntbl lock sid ts = do+    -- Before anything is inserted or counted.+    lim <- getMaxHeaderSize dyntbl+    let siz0 = fieldSectionSize ts+    when (siz0 > lim) $ E.throwIO $ FieldSectionTooLargeForPeer siz0 lim     withMVar lock $ \_ ->         withForeignPtr gcbuf1 $ \buf1 ->             withForeignPtr gcbuf2 $ \buf2 -> do@@ -209,15 +263,20 @@                         "---- Stream " ++ show sid ++ " " ++ "tblsiz: " ++ show siz                 setBasePointToInsersionPoint dyntbl                 clearRequiredInsertCount dyntbl-                let tss = splitThrough bufsiz1 ts-                his <- mapM (qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 huff dyntbl) tss+                let tss = splitThrough (min bufsiz1 bufsiz2) ts+                his <- mapM (encodeChunk buf1 bufsiz1 buf2 bufsiz2 huff 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) $+                when (reqInsCnt /= 0) $ do+                    -- Counted against the decoder's+                    -- SETTINGS_QPACK_BLOCKED_STREAMS, which+                    -- 'checkBlockedStreams' consults while encoding.+                    blocked <- wouldSectionBeBlocked dyntbl reqInsCnt+                    when blocked $ insertBlockedStreamE dyntbl sid                     insertSection dyntbl sid $                         Section reqInsCnt $                             concat daiss@@ -242,8 +301,8 @@                         "---- Stream " ++ show sid ++ " " ++ "tblsiz: " ++ show siz                 setBasePointToInsersionPoint dyntbl                 clearRequiredInsertCount dyntbl-                let tss = splitThrough bufsiz1 ts-                his <- mapM (qpackEncodeHeader buf1 bufsiz1 buf2 bufsiz2 huff dyntbl) tss+                let tss = splitThrough (min bufsiz1 bufsiz2) ts+                his <- mapM (encodeChunk buf1 bufsiz1 buf2 bufsiz2 huff dyntbl) tss                 let (hbs, daiss) = unzip his                 prefix <- qpackEncodePrefix buf1 bufsiz1 dyntbl                 let section = BS.concat (prefix : hbs)@@ -260,6 +319,7 @@                         -- The same logic of SectionAcknowledgement.                         updateKnownReceivedCount dyntbl reqInsCnt                         mapM_ (decreaseReference dyntbl) dais+                        _ <- getAndDelSection dyntbl sid                         deleteBlockedStreamE dyntbl sid                 -- Need to emulate InsertCountIncrement since                 -- SectionAcknowledgement is not returned if@@ -297,18 +357,28 @@     toByteString wbuf1  -- Note: dyntbl for encoder-decoderInstructionHandler :: DynamicTable -> DecoderInstructionHandler-decoderInstructionHandler dyntbl recv = loop ""+--+-- The lock is the encoder's.  Acknowledgements change the same reference+-- counts, sections and blocked streams that encoding reads and writes, with+-- plain reads and writes rather than atomic ones; run beside an encoding in+-- progress, an update could be lost and an entry either kept forever or+-- evicted while a field section still referred to it.+decoderInstructionHandler+    :: DynamicTable -> MVar () -> DecoderInstructionHandler+decoderInstructionHandler dyntbl lock recv = loop ""   where+    -- Returns when the stream ends, with an instruction cut short or not.+    -- Going on with what was left over used to spin: the same incomplete+    -- instruction, decoded again after every empty read.     loop bs0 = do         bs1 <- recv 1024         let bs                 | bs0 == "" = bs1                 | otherwise = bs0 <> bs1-        when (bs /= "") $ do+        when (bs1 /= "") $ do             (ins, leftover) <- decodeDecoderInstructions bs             qpackDebug dyntbl $ mapM_ print ins-            mapM_ handle ins+            withMVar lock $ \_ -> mapM_ handle ins             loop leftover     handle (SectionAcknowledgement sid) = do         msec <- getAndDelSection dyntbl sid@@ -317,11 +387,25 @@             Just (Section reqInsCnt ais) -> do                 updateKnownReceivedCount dyntbl reqInsCnt                 mapM_ (decreaseReference dyntbl) ais-                deleteBlockedStreamE dyntbl sid-    handle (StreamCancellation _n) = return () -- fixme+                -- Not simply deleted: a later section on the same stream+                -- may still be blocked.+                unblockStreamsE dyntbl+    -- The decoder will not process the rest of the stream (RFC 9204, section+    -- 4.4.2), so its outstanding field sections will never be acknowledged.+    -- Their references are released as an acknowledgement would release+    -- them, but the Known Received Count stays where it is: nothing about+    -- which insertions arrived is learnt from this.  Ignoring it used to keep+    -- the entries those sections referred to from ever being evicted, and the+    -- stream counted as blocked for good.+    handle (StreamCancellation sid) = do+        secs <- getAndDelSections dyntbl sid+        forM_ secs $ \(Section _ ais) -> mapM_ (decreaseReference dyntbl) ais+        unblockStreamsE dyntbl     handle (InsertCountIncrement n)         | n == 0 = E.throwIO DecoderInstructionError-        | otherwise = incrementKnownReceivedCount dyntbl n+        | otherwise = do+            incrementKnownReceivedCount dyntbl n+            unblockStreamsE dyntbl  ---------------------------------------------------------------- @@ -338,6 +422,7 @@     gcbuf1 <- mallocPlainForeignPtrBytes bufsiz1     gcbuf2 <- mallocPlainForeignPtrBytes bufsiz2     dyntbl <- newDynamicTableForEncoding saveEI+    setMaxEntries dyntbl ecMaxTableCapacity     setTableCapacity dyntbl ecMaxTableCapacity     setMaxBlockedStreams dyntbl blocked     setImmediateAck dyntbl immediateAck@@ -386,7 +471,10 @@ newQDecoder QDecoderConfig{..} sendDI = do     dyntbl <-         newDynamicTableForDecoding dcHuffmanBufferSize sendDI+    setMaxEntries dyntbl dcMaxTableCapacity     setMaxBlockedStreams dyntbl dcBlockedSterams+    -- What we announce as SETTINGS_MAX_FIELD_SECTION_SIZE.+    setMaxHeaderSize dyntbl dcMaxFieldSectionSize     let dec = qpackDecoder dyntbl         handler = encoderInstructionHandler dcMaxTableCapacity dyntbl     return (dec, handler)@@ -400,7 +488,10 @@ newQDecoderS QDecoderConfig{..} sendDI debug = do     dyntbl <-         newDynamicTableForDecoding dcHuffmanBufferSize sendDI+    setMaxEntries dyntbl dcMaxTableCapacity     setMaxBlockedStreams dyntbl dcBlockedSterams+    -- What we announce as SETTINGS_MAX_FIELD_SECTION_SIZE.+    setMaxHeaderSize dyntbl dcMaxFieldSectionSize     setDebugQPACK dyntbl debug     let dec = qpackDecoderS dyntbl         handler = encoderInstructionHandlerS dcMaxTableCapacity dyntbl@@ -430,12 +521,13 @@ encoderInstructionHandler :: Int -> DynamicTable -> EncoderInstructionHandler encoderInstructionHandler decCapLim dyntbl recv = loop ""   where+    -- Returns when the stream ends; see 'decoderInstructionHandler'.     loop bs0 = do         bs1 <- recv 1024         let bs                 | bs0 == "" = bs1                 | otherwise = bs0 <> bs1-        when (bs /= "") $ do+        when (bs1 /= "") $ do             leftover <- encoderInstructionHandlerS decCapLim dyntbl bs             loop leftover @@ -453,7 +545,7 @@     handle ins@(SetDynamicTableCapacity n)         | n > decCapLim = E.throwIO EncoderInstructionError         | otherwise = do-            setTableCapacity dyntbl n+            setDecoderTableCapacity dyntbl n             qpackDebug dyntbl $ print ins             return 0     handle ins@(InsertWithNameReference ii val) = do
Network/QPACK/Error.hs view
@@ -8,6 +8,8 @@         QpackDecoderStreamError     ),     DecodeError (..),+    FieldSectionTooLarge (..),+    FieldSectionTooLargeForPeer (..),     EncoderInstructionError (..),     DecoderInstructionError (..), ) where@@ -35,11 +37,32 @@     | BlockedStreamsOverflow     deriving (Eq, Show) +-- | A field section that decodes to more than the+--   SETTINGS_MAX_FIELD_SECTION_SIZE we announced (RFC 9114, section 4.2.2).+--+-- Not a 'DecodeError': nothing is wrong with the encoding, and the+-- connection can go on.  The size is counted as the RFC counts it, the+-- lengths of each name and value plus 32 for every field.+data FieldSectionTooLarge = FieldSectionTooLarge+    deriving (Eq, Show)++-- | A field section more than the peer's SETTINGS_MAX_FIELD_SECTION_SIZE,+--   which "SHOULD NOT" be sent (RFC 9114, section 4.2.2).  Thrown by the+--   encoder before it touches anything, so nothing has been sent or changed.+data FieldSectionTooLargeForPeer = FieldSectionTooLargeForPeer+    { sectionSize :: Int+    -- ^ As the limit counts it+    , peerLimit :: Int+    }+    deriving (Eq, Show)+ data EncoderInstructionError = EncoderInstructionError     deriving (Eq, Show) data DecoderInstructionError = DecoderInstructionError     deriving (Eq, Show)  instance E.Exception DecodeError+instance E.Exception FieldSectionTooLarge+instance E.Exception FieldSectionTooLargeForPeer instance E.Exception EncoderInstructionError instance E.Exception DecoderInstructionError
Network/QPACK/HeaderBlock/Decode.hs view
@@ -6,7 +6,9 @@ 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 (     HuffmanDecoder,     decodeH,@@ -32,14 +34,25 @@ decodeTokenHeader dyntbl rbuf = do     (reqInsertCount, bp, needAck) <- decodePrefix rbuf dyntbl     ready <- checkRequiredInsertCountNB dyntbl reqInsertCount-    unless ready $ do+    -- 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-        checkRequiredInsertCount dyntbl reqInsertCount-        decreaseStreams dyntbl+        restore (checkRequiredInsertCount dyntbl reqInsertCount)+            `E.finally` decreaseStreams dyntbl     checkRequiredInsertCount dyntbl reqInsertCount     hufdec <- newHuffmanDecoder rbuf-    tbl <- decodeSophisticated (toTokenHeader dyntbl bp hufdec) rbuf+    dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl 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.+    tbl <-+        decodeSophisticated dec rbuf `E.catch` \e -> case e of+            HPACK.TooLargeHeader -> E.throwIO FieldSectionTooLarge+            _ -> E.throwIO e     return (tbl, needAck)  decodeTokenHeaderS@@ -52,9 +65,32 @@     if ok         then do             hufdec <- newHuffmanDecoder rbuf-            hs <- decodeSimple (toTokenHeader dyntbl bp hufdec) rbuf+            dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl bp hufdec+            hs <- decodeSimple dec rbuf             return $ Just (hs, needAck)         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.
Network/QPACK/HeaderBlock/Encode.hs view
@@ -152,11 +152,17 @@                 encodeIndexed wbuf1 dyntbl hi                 increaseReference dyntbl ai                 return $ Just ai-        K hi@(SIndex i) -> tryInsertVal hi $ do+        K hi@(SIndex i) -> tryInsertVal hi id $ do             insertWithNameReference val ent Nothing $ Left i         K hi@(DIndex ai) -> do             qpackDebug dyntbl $ checkAbsoluteIndex dyntbl ai "K (1)"-            withDIndex ai $ tryInsertVal hi $ do+            -- Only a field line referring to the entry can block the+            -- stream.  The insertion refers to it on the encoder stream,+            -- which never blocks anything (RFC 9204, section 2.1.1), so it+            -- is only the fallback that has to be guarded.  Guarding the+            -- whole of it gave up inserting whenever the name was in an+            -- entry not yet acknowledged and this stream could not block.+            tryInsertVal hi (withDIndex ai) $ do                 ridx <- toInsRelativeIndex ai <$> getInsertionPoint dyntbl                 insertWithNameReference val ent (Just ai) $ Right ridx         N -> tryInsertKeyVal $ insertWithLiteralName val ent@@ -203,7 +209,9 @@                 return $ Just ai             else encodeLiteralFieldLineStatic -    tryInsertVal hi action = do+    -- 'guardRef' wraps the fallback, a field line with a reference to the+    -- name, which is where a reference to the dynamic table can block.+    tryInsertVal hi guardRef action = do         -- Field representation MUST not refer to a dropped entry         -- on insertion.         let possiblelyDropMySelf = case hi of@@ -212,7 +220,7 @@         ok <- checkExistenceAndSpace ent key val possiblelyDropMySelf "Val"         if ok             then action-            else do+            else guardRef $ do                 -- 4.5.4/4.5.5                 encodeWithNameReference wbuf1 dyntbl hi val huff                 case hi of
Network/QPACK/HeaderBlock/Prefix.hs view
@@ -40,7 +40,9 @@ decodeRequiredInsertCount     :: Int -> InsertionPoint -> Int -> RequiredInsertCount decodeRequiredInsertCount _ _ 0 = 0-decodeRequiredInsertCount 0 _ n = RequiredInsertCount (n - 1)+-- With no dynamic table, nothing but 0 can be encoded (RFC 9204, section+-- 4.5.1.1).+decodeRequiredInsertCount 0 _ _ = E.throw IllegalInsertCount decodeRequiredInsertCount maxEntries (InsertionPoint totalNumberOfInserts) encodedInsertCount     | encodedInsertCount > fullRange = E.throw IllegalInsertCount     | reqInsertCount > maxValue && reqInsertCount <= fullRange =@@ -83,7 +85,7 @@ encodePrefix :: WriteBuffer -> DynamicTable -> IO () encodePrefix wbuf dyntbl = do     clearWriteBuffer wbuf-    maxEntries <- getMaxNumOfEntries dyntbl+    maxEntries <- getMaxEntries dyntbl     baseIndex <- getBasePoint dyntbl     reqInsCnt <- getRequiredInsertCount dyntbl     qpackDebug dyntbl $ print reqInsCnt@@ -101,7 +103,7 @@ decodePrefix     :: ReadBuffer -> DynamicTable -> IO (RequiredInsertCount, BasePoint, Bool) decodePrefix rbuf dyntbl = do-    maxEntries <- getMaxNumOfEntries dyntbl+    maxEntries <- getMaxEntries dyntbl     totalNumberOfInserts <- getInsertionPoint dyntbl     w8 <- read8 rbuf     ric <- decodeI 8 w8 rbuf
Network/QPACK/Table/Dynamic.hs view
@@ -11,7 +11,10 @@     isTableReady,     getTableCapacity,     setTableCapacity,+    setDecoderTableCapacity,     getMaxNumOfEntries,+    getMaxEntries,+    setMaxEntries,      -- * Entry     insertEntryToDecoder,@@ -22,6 +25,7 @@     Section (..),     insertSection,     getAndDelSection,+    getAndDelSections,     increaseReference,     decreaseReference, @@ -34,6 +38,7 @@     -- * Blocked streams     insertBlockedStreamE,     deleteBlockedStreamE,+    unblockStreamsE,     checkBlockedStreams,      -- * Required insert count@@ -97,6 +102,8 @@ import Data.IORef import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IntMap+import Data.Sequence (Seq, ViewL (..), viewl, (|>))+import qualified Data.Sequence as Seq import Data.Set (Set) import qualified Data.Set as Set -- Set.size is O(1), IntSet.size is O(n) import Imports@@ -135,7 +142,7 @@         , drainingPoint       :: IORef AbsoluteIndex         , knownReceivedCount  :: TVar Int         , referenceCounters   :: IORef (IOArray Index Reference)-        , sections            :: IORef (IntMap Section)+        , sections            :: IORef (IntMap (Seq Section)) -- oldest first         , lruCache            :: LRUCacheRef (FieldName, FieldValue) ()         , immediateAck        :: IORef Bool -- for QIF         , blockedStreamsE     :: IORef (Set Int)@@ -152,6 +159,7 @@     { codeInfo          :: CodeInfo     , insertionPoint    :: TVar InsertionPoint     , maxNumOfEntries   :: TVar Int+    , maxEntries        :: IORef Int -- MaxEntries of RFC 9204 section 4.5.1.1     , circularTable     :: TVar Table     , basePoint         :: IORef BasePoint     , debugQPACK        :: IORef Bool@@ -206,6 +214,7 @@     let codeInfo = info     insertionPoint <- newTVarIO 0     maxNumOfEntries <- newTVarIO 0+    maxEntries <- newIORef 0     circularTable <- newTVarIO tbl     basePoint <- newIORef 0     debugQPACK <- newIORef False@@ -245,9 +254,47 @@     maxN = maxNumbers maxsiz     end = maxN - 1 +-- | Setting the capacity of the decoder's table, as the encoder instructs.+--+-- The encoder may change the capacity whenever it likes, within the maximum+-- the decoder announced (RFC 9204, section 3.2.3), and the entries in the+-- table stay where they are.  So the ring is allocated once, for as many+-- entries as the maximum allows ('setMaxEntries'), and a later change only+-- moves the capacity.  Reallocating it, as 'setTableCapacity' does, emptied+-- the table and changed what the absolute indices map onto: references to+-- entries inserted before the change came back as a dummy entry.+setDecoderTableCapacity :: DynamicTable -> Int -> IO ()+setDecoderTableCapacity dyntbl@DynamicTable{..} cap = do+    qpackDebug dyntbl $ putStrLn $ "setDecoderTableCapacity " ++ show cap+    maxN <- readIORef maxEntries+    allocated <- (>= 1) <$> readTVarIO maxNumOfEntries+    when (not allocated && maxN >= 1) $ do+        tbl <- atomically $ newArray (0, maxN - 1) dummyEntry+        atomically $ do+            writeTVar maxNumOfEntries maxN+            writeTVar circularTable tbl+    writeIORef maxTableSize cap+    -- No entry is smaller than 32, so below that nothing can be inserted.+    writeIORef capaReady (maxN >= 1 && maxNumbers cap >= 1)+ getMaxNumOfEntries :: DynamicTable -> IO Int getMaxNumOfEntries DynamicTable{..} = readTVarIO maxNumOfEntries +-- | MaxEntries, which the Required Insert Count of a field section is+-- encoded and decoded against (RFC 9204, section 4.5.1.1).+--+-- It comes from the decoder's SETTINGS_QPACK_MAX_TABLE_CAPACITY, not from the+-- capacity the encoder actually set.  The two ends agree on the former; the+-- latter is only the encoder's choice within it, and using it here made the+-- two ends disagree once more than 2*MaxEntries entries had been inserted.+getMaxEntries :: DynamicTable -> IO Int+getMaxEntries DynamicTable{..} = readIORef maxEntries++-- | Setting MaxEntries from the decoder's maximum table capacity.+setMaxEntries :: DynamicTable -> Int -> IO ()+setMaxEntries DynamicTable{..} maxCapacity =+    writeIORef maxEntries $ maxNumbers maxCapacity+ ----------------------------------------------------------------  insertEntryToEncoder :: Entry -> DynamicTable -> IO AbsoluteIndex@@ -308,23 +355,44 @@  ---------------------------------------------------------------- +-- | Registering a field section which awaits acknowledgement.+--+-- A stream can have more than one outstanding -- headers and trailers, say --+-- and a Section Acknowledgement is for the oldest of them (RFC 9204, section+-- 4.4.1), so they are queued rather than keyed by the stream alone.  Keyed+-- alone, the second replaced the first, whose references were then never+-- released, and the second acknowledgement found nothing and was taken for a+-- decoder stream error. insertSection :: DynamicTable -> StreamId -> Section -> IO () insertSection DynamicTable{..} sid section = atomicModifyIORef' sections ins   where     ins m =-        let m' = IntMap.insert sid section m+        let m' = IntMap.insertWith (\_ old -> old |> section) sid (Seq.singleton section) m          in (m', ())     EncodeInfo{..} = codeInfo +-- | Taking out the oldest outstanding field section of a stream. getAndDelSection :: DynamicTable -> StreamId -> IO (Maybe Section) getAndDelSection DynamicTable{..} sid = atomicModifyIORef' sections getAndDel   where-    getAndDel m =-        let (msec, m') = IntMap.updateLookupWithKey f sid m-         in (m', msec)-    f _ _ = Nothing -- delete the entry if found+    getAndDel m = case IntMap.lookup sid m of+        Nothing -> (m, Nothing)+        Just q -> case viewl q of+            EmptyL -> (IntMap.delete sid m, Nothing)+            sec :< rest+                | Seq.null rest -> (IntMap.delete sid m, Just sec)+                | otherwise -> (IntMap.insert sid rest m, Just sec)     EncodeInfo{..} = codeInfo +-- | Taking out every outstanding field section of a stream.+getAndDelSections :: DynamicTable -> StreamId -> IO [Section]+getAndDelSections DynamicTable{..} sid = atomicModifyIORef' sections getAndDel+  where+    getAndDel m = case IntMap.lookup sid m of+        Nothing -> (m, [])+        Just q -> (IntMap.delete sid m, toList q)+    EncodeInfo{..} = codeInfo+ increaseReference :: DynamicTable -> AbsoluteIndex -> IO () increaseReference = modifyReference $ \(Reference c t) -> Reference (c + 1) (t + 1) @@ -366,8 +434,9 @@ tryIncreaseStreams :: DynamicTable -> IO Bool tryIncreaseStreams DynamicTable{..} = do     lim <- readIORef maxBlockedStreams-    curr <- atomicModifyIORef' blockedStreamsD (\n -> (n + 1, n + 1))-    return (curr <= lim)+    -- Counted only if it is let in, since nothing takes a refused one off.+    atomicModifyIORef' blockedStreamsD $ \n ->+        if n < lim then (n + 1, True) else (n, False)   where     DecodeInfo{..} = codeInfo @@ -386,16 +455,34 @@  insertBlockedStreamE :: DynamicTable -> StreamId -> IO () insertBlockedStreamE DynamicTable{..} sid =-    modifyIORef' blockedStreamsE (Set.insert sid)+    atomicModifyIORef' blockedStreamsE (\set -> (Set.insert sid set, ()))   where     EncodeInfo{..} = codeInfo  deleteBlockedStreamE :: DynamicTable -> StreamId -> IO () deleteBlockedStreamE DynamicTable{..} sid =-    modifyIORef' blockedStreamsE (Set.delete sid)+    atomicModifyIORef' blockedStreamsE (\set -> (Set.delete sid set, ()))   where     EncodeInfo{..} = codeInfo +-- | Forgetting the blocked streams that the known received count has caught+-- up with.+--+-- A stream stops being blocked once the decoder holds every entry its+-- section refers to (RFC 9204, section 2.1.2), which an Insert Count+-- Increment can say as well as a Section Acknowledgement.  Waiting for the+-- acknowledgement alone would be safe but would keep a stream counted for as+-- long as it is never acknowledged -- forever, for one that was reset.+unblockStreamsE :: DynamicTable -> IO ()+unblockStreamsE DynamicTable{..} = do+    krc <- readTVarIO knownReceivedCount+    secs <- readIORef sections+    let blocking (Section (RequiredInsertCount ric) _) = ric > krc+        stillBlocked sid = maybe False (any blocking) $ IntMap.lookup sid secs+    atomicModifyIORef' blockedStreamsE (\set -> (Set.filter stillBlocked set, ()))+  where+    EncodeInfo{..} = codeInfo+ checkBlockedStreams :: DynamicTable -> IO Bool checkBlockedStreams dyntbl = do     maxBlocked <- getMaxBlockedStreams dyntbl@@ -464,7 +551,10 @@ wouldInstructionBeBlocked :: DynamicTable -> AbsoluteIndex -> IO Bool wouldInstructionBeBlocked DynamicTable{..} (AbsoluteIndex ai) = atomically $ do     krc <- readTVar knownReceivedCount-    return (ai > krc)+    -- The decoder is known to hold entries 0 .. krc-1, so a reference to+    -- krc itself is still one it may not have.  This is the same test as+    -- 'wouldSectionBeBlocked', whose Required Insert Count is ai + 1.+    return (ai >= krc)   where     EncodeInfo{..} = codeInfo @@ -532,8 +622,10 @@     maxN <- readTVarIO maxNumOfEntries     let i = ai `mod` maxN     -- modifyArray' is not provided by GHC 9.4 or earlier, sigh.-    Reference current total <- unsafeRead arr i-    if current == 0 && total >= tailDuplicationThreshold+    ref@(Reference _ total) <- unsafeRead arr i+    krc <- readTVarIO knownReceivedCount+    -- Duplicating it evicts it, so it has to be evictable.+    if evictable krc ai ref && total >= tailDuplicationThreshold         then return $ Just dai         else return Nothing   where@@ -558,6 +650,19 @@  ---------------------------------------------------------------- +-- | Whether the entry at an absolute index may be evicted, given the Known+--   Received Count (RFC 9204, section 2.1.1): once its insertion has been+--   acknowledged and no unacknowledged field section refers to it.+--+-- Whether it was ever referred to has nothing to do with it.  This used to+-- ask for that instead of the acknowledgement, which kept an entry nothing+-- referred to for good -- and, since entries go oldest first, every entry+-- after it too, so that the table filled up and took nothing new for the+-- rest of the connection.  An encoder inserts such an entry whenever it may+-- not block another stream and so writes the field out literally.+evictable :: Int -> Int -> Reference -> Bool+evictable krc ai (Reference current _) = current == 0 && ai < krc+ canInsertEntry :: DynamicTable -> Entry -> Maybe AbsoluteIndex -> IO Bool canInsertEntry DynamicTable{..} ent mai = do     let siz = entrySize ent@@ -583,8 +688,9 @@                     maxN <- readTVarIO maxNumOfEntries                     let i = ai `mod` maxN                     refs <- readIORef referenceCounters-                    Reference current total <- unsafeRead refs i-                    if current == 0 && total >= 1+                    ref <- unsafeRead refs i+                    krc <- readTVarIO knownReceivedCount+                    if evictable krc ai ref                         then do                             table <- readTVarIO circularTable                             dent <- atomically $ unsafeRead table i@@ -613,8 +719,9 @@         then do             let i = ai `mod` maxN             refs <- readIORef referenceCounters-            Reference current total <- unsafeRead refs i-            if current == 0 && total >= 1+            ref <- unsafeRead refs i+            krc <- readTVarIO knownReceivedCount+            if evictable krc ai ref                 then do                     table <- readTVarIO circularTable                     ent <- atomically $ do
http3.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               http3-version:            0.1.5+version:            0.1.6 license:            BSD-3-Clause license-file:       LICENSE maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>@@ -83,13 +83,13 @@         containers,         http-semantics >= 0.4 && <0.5,         http-types,-        http2 >=5.4.5 && <5.5,+        http2 >=5.4.6 && <5.5,         iproute >= 1.7 && < 1.8,         network,         network-byte-order,         network-control >= 0.1.7 && <0.2,         psqueues,-        quic >= 0.3.0 && < 0.4,+        quic >= 0.3.9 && < 0.4,         sockaddr,         stm,         time-manager >= 0.2.3 && <0.4,@@ -208,6 +208,7 @@         HTTP3.Error         HTTP3.ErrorSpec         HTTP3.FrameSpec+        HTTP3.LimitSpec         HTTP3.Server         HTTP3.ServerSpec         QPACK.HeaderBlockSpec
test/HTTP3/Config.hs view
@@ -4,8 +4,10 @@     makeTestServerConfig,     testClientConfig,     testH3ClientConfig,+    waitForSettings, ) where +import Control.Concurrent (threadDelay) import Data.ByteString (ByteString) import qualified Data.List as L import qualified Network.HTTP3.Client as H3@@ -30,6 +32,11 @@ testServerConfig =     defaultServerConfig         { scAddresses = [("127.0.0.1", 8003)]+        , -- Room for more unidirectional streams than the three a client+          -- needs, so that a test can open a second control or QPACK stream+          -- and see it refused.  The default of three leaves no room for one.+          scParameters =+            (scParameters defaultServerConfig){initialMaxStreamsUni = 10}         }  testClientConfig :: ClientConfig@@ -53,3 +60,13 @@  testH3ClientConfig :: H3.ClientConfig testH3ClientConfig = H3.defaultClientConfig{H3.authority = "127.0.0.1"}++-- | Giving the peer's SETTINGS time to take effect, after a round trip.+--+-- A response is no sign that they have: they come on the control stream,+-- and that is read by a thread of its own, so the response on a request+-- stream can get to the application first.  A test that depends on them --+-- a limit the peer announced, or a dynamic table it allowed -- used to fail+-- a time or two in a hundred.  There is nothing to wait on, so this sleeps.+waitForSettings :: IO ()+waitForSettings = threadDelay 100000
test/HTTP3/Error.hs view
@@ -7,9 +7,11 @@  import Control.Concurrent import qualified Control.Exception as E+import Control.Monad (forM_) import Data.ByteString () import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8+import qualified Data.CaseInsensitive as CI import Data.IORef import Network.HTTP.Types import qualified Network.HTTP3.Client as H3@@ -124,7 +126,33 @@                     qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")                 runCReq req qcc' cconf conf0 ms                     `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+        forM_ connectionSpecific $ \field@(name, _) ->+            it+                ( "MUST treat a request with "+                    ++ C8.unpack (CI.original name)+                    ++ " as malformed [HTTP/3 4.2]"+                )+                $ \(_, served) -> do+                    let req = H3.requestNoBody methodGet "/" [field]+                        qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                    before' <- readIORef served+                    runCReq req qcc' cconf conf0 ms+                        `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+                    threadDelay 200000+                    readIORef served `shouldReturn` before'         it+            "MUST treat a request whose trailers carry connection as malformed [HTTP/3 4.2]"+            $ \_ -> do+                -- /drain reads the body, and so the trailers after it.+                let req0 = H3.requestBuilder methodPost "/drain" [] "hello"+                    req = H3.setRequestTrailersMaker req0 connectionTrailer+                    qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                runCReq req qcc' cconf conf0 ms+                    `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+        it "MUST accept TE with trailers [HTTP/3 4.2]" $ \_ -> do+            let req = H3.requestNoBody methodGet "/" [("te", "trailers")]+            runCReq req qcc cconf conf0 ms `shouldReturn` Just ()+        it             "MUST send H3_MISSING_SETTINGS if the first control frame is not SETTINGS [HTTP/3 6.2.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated startWithNonSettings@@ -214,6 +242,53 @@                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]         it+            "MUST send H3_STREAM_CREATION_ERROR if a second control stream is opened [HTTP/3 6.2.1]"+            $ \_ -> do+                -- A stream type of 0x00, then an empty SETTINGS frame.+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x00\x04\x00"+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+        it+            "MUST send H3_STREAM_CREATION_ERROR if a client opens a push stream [HTTP/3 6.2.2]"+            $ \_ -> do+                -- A stream type of 0x01, then push ID 0.+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x01\x00"+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+        it+            "MUST send H3_STREAM_CREATION_ERROR if a second encoder stream is opened [QPACK 4.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother "\x02"+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+        it+            "MUST send H3_STREAM_CREATION_ERROR if a second decoder stream is opened [QPACK 4.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother "\x03"+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+        it+            "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream is closed [QPACK 4.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnEncoderStreamCreated closeStream+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]+        it+            "MUST send H3_CLOSED_CRITICAL_STREAM if a decoder stream is closed [QPACK 4.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnDecoderStreamCreated closeStream+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]+        it+            "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream ends inside an instruction [QPACK 4.2]"+            $ \_ -> do+                -- The first octet of a Set Dynamic Table Capacity that goes on.+                let conf = addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do+                        sendStream strm "\x3f"+                        closeStream strm+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]+        it             "MUST send QPACK_DECODER_STREAM_ERROR if Insert Count Increment is 0 [QPACK 4.4.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnDecoderStreamCreated zeroInsertCountIncrement@@ -382,6 +457,30 @@     ]  ----------------------------------------------------------------++-- | The fields RFC 9114, section 4.2, names as connection-specific, and TE+-- with a value other than "trailers".+connectionSpecific :: [(HeaderName, BS.ByteString)]+connectionSpecific =+    [ ("connection", "close")+    , ("keep-alive", "timeout=5")+    , ("proxy-connection", "keep-alive")+    , ("transfer-encoding", "chunked")+    , ("upgrade", "websocket")+    , ("te", "gzip")+    ]++-- | Trailers of a single Connection field.+connectionTrailer :: H3.TrailersMaker+connectionTrailer Nothing = return $ H3.Trailers [("connection", "close")]+connectionTrailer (Just _) = return $ H3.NextTrailersMaker connectionTrailer++-- | Opening a unidirectional stream of our own next to the one given, and+-- sending it these octets: a stream type and whatever follows it.+openAnother :: BS.ByteString -> Stream -> IO ()+openAnother bs strm = do+    strm' <- unidirectionalStream $ streamConnection strm+    sendStream strm' bs  -- A GOAWAY frame announcing 2^30 octets and then sending none of them. --
+ test/HTTP3/LimitSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module HTTP3.LimitSpec where++import qualified Control.Exception as E+import qualified Data.ByteString as B+import Network.HTTP.Types+import qualified Network.HTTP3.Client as C+import Network.HTTP3.Internal (H3Frame (..), H3FrameType (..))+import Network.HTTP3.Server+import qualified Network.QUIC.Client as QUIC+import Test.Hspec++import HTTP3.Config+import HTTP3.Server++-- | A server that keeps to its SETTINGS_MAX_FIELD_SECTION_SIZE without+-- announcing it.+--+-- Our client keeps to the limit a server announces and sends nothing over+-- it, so a server that announces its limit never sees a section over it from+-- us.  Such a section has to be sent all the same to see what the server+-- does with it, and a client that has not been told of a limit sends it.+spec :: Spec+spec =+    describe "H3 server keeping to a limit it does not announce" $+        it "answers 431 to a request header section over its limit" $+            E.bracket (setupWith hideLimit server 4096) teardown $+                \_ -> runTooLargeClient++-- | SETTINGS with the dynamic table as the test server has it, and without+-- SETTINGS_MAX_FIELD_SECTION_SIZE: QPACK_MAX_TABLE_CAPACITY 4096 and+-- QPACK_BLOCKED_STREAMS 100.+hideLimit :: Config -> Config+hideLimit conf = conf{confHooks = (confHooks conf){onControlFrameCreated = map replace}}+  where+    replace (H3Frame H3FrameSettings _) =+        H3Frame H3FrameSettings "\x01\x50\x00\x07\x40\x64"+    replace frame = frame++-- | 150 copies of one field with a 300-octet value.  After the first two,+-- each is a reference to the dynamic table of an octet or so, so the HEADERS+-- frame comes to around 600 octets, well within the cap on its length.  As+-- the limit counts them, though, they are 335 octets each, 50250 in all+-- against the server's 32768.+runTooLargeClient :: IO ()+runTooLargeClient = QUIC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+            -- One round trip first, so that the server's SETTINGS are in and+            -- the encoder may use the dynamic table.  Without it every copy+            -- goes as a literal and the frame is over the cap.+            sendRequest (C.requestNoBody methodGet "/" []) $ \_ -> return ()+            waitForSettings+            let hdr = replicate 150 ("x-a", B.replicate 300 0x62)+                req = C.requestNoBody methodGet "/" hdr+            sendRequest req $ \rsp ->+                C.responseStatus rsp `shouldBe` Just requestHeaderFieldsTooLarge431
test/HTTP3/Server.hs view
@@ -3,6 +3,7 @@  module HTTP3.Server (     setup,+    setupWith,     server,     countingServer,     teardown,@@ -21,7 +22,7 @@ import qualified Crypto.Hash as CH import Data.ByteString (ByteString) import qualified Data.ByteString as B-import Data.ByteString.Builder (byteString)+import Data.ByteString.Builder (Builder, byteString) import qualified Data.ByteString.Char8 as C8 import Data.IORef import Data.IP ()@@ -35,7 +36,11 @@ import HTTP3.Config  setup :: Server -> Int -> IO ThreadId-setup svr siz = do+setup = setupWith id++-- | 'setup', with the HTTP/3 configuration changed as given.+setupWith :: (Config -> Config) -> Server -> Int -> IO ThreadId+setupWith modify svr siz = do     sc <- makeTestServerConfig     tid <- forkIO $ QUIC.run sc loop     threadDelay 500000 -- give enough time to the server@@ -48,7 +53,7 @@                     , confQDecoderConfig = defaultQDecoderConfig{dcMaxTableCapacity = siz}                     } -        run conn conf svr+        run conn (modify conf) svr  teardown :: ThreadId -> IO () teardown tid = killThread tid@@ -67,6 +72,13 @@     Just "GET" -> case requestPath req of         Just "/" -> sendResponse responseHello []         Just "/sockaddr" -> sendResponse (responseSockAddr aux) []+        -- A response that is reset after one DATA frame, between two+        -- frames: the body throws, and the stream is reset.+        Just "/reset" ->+            sendResponse (responseStreaming ok200 [] resetAfterOne) []+        -- A response whose header section is some 2K.+        Just "/bigheader" ->+            sendResponse (responseNoBody ok200 [("x-big", B.replicate 2000 0x61)]) []         _ -> sendResponse response404 []     Just "POST" -> case requestPath req of         Just "/echo" -> sendResponse (responseEcho req) []@@ -78,8 +90,30 @@                     unless (B.null bs) loop             loop             sendResponse responseHello []+        -- Answers with the length of the body it read.+        Just "/length" -> do+            let loop n = do+                    bs <- getRequestBodyChunk req+                    if B.null bs then return n else loop (n + B.length bs)+            n <- loop (0 :: Int)+            sendResponse (responseLength n) []         _ -> sendResponse responseHello []     _ -> sendResponse response405 []++responseLength :: Int -> Response+responseLength n = responseBuilder ok200 header body+  where+    header = [("Content-Type", "text/plain")]+    body = byteString $ C8.pack $ show n++resetAfterOne :: (Builder -> IO ()) -> IO () -> IO ()+resetAfterOne write flush = do+    write $ byteString "partial"+    flush+    -- A RESET_STREAM can go out ahead of stream data queued before it, and+    -- then the client sees neither HEADERS nor DATA.  Let them arrive first.+    threadDelay 200000+    E.throwIO $ userError "reset after one DATA frame"  responseHello :: Response responseHello = responseBuilder ok200 header body
test/HTTP3/ServerSpec.hs view
@@ -3,14 +3,36 @@  module HTTP3.ServerSpec where +import Control.Concurrent (threadDelay) import Control.Concurrent.Async import qualified Control.Exception as E import Control.Monad import qualified Data.ByteString as B+import Data.IORef import Network.HTTP.Types import qualified Network.HTTP3.Client as C+import Network.HTTP3.Internal (+    ApplicationProtocolError (..),+    H3Frame (..),+    H3FrameType (..),+ ) import Network.HTTP3.Server+import Network.QPACK (+    FieldSectionTooLargeForPeer (..),+    QDecoderConfig (..),+ )+import qualified Network.QUIC as Q import qualified Network.QUIC.Client as QUIC+import Network.QUIC.Internal (+    ClientConfig (..),+    EncryptionLevel,+    Frame (..),+    Hooks (..),+    Plain (..),+    StreamId,+ )+import System.IO.Unsafe (unsafePerformIO)+import System.Timeout (timeout) import Test.Hspec  import HTTP3.Config@@ -25,6 +47,20 @@         it "handles normal cases" $ \_ -> runClient         it "tells the application the peer's address, not its own" $ \_ ->             runSockAddrClient+        it "reads a body past a DATA frame that is empty" $ \_ ->+            runEmptyDataClient+        it "cancels a stream whose response it does not read to the end" $ \_ ->+            runCancelClient+        it "cancels a stream reset between two frames" $ \_ ->+            runResetCancelClient+        it "stops reading a unidirectional stream of an unknown type" $ \_ ->+            runUnknownStreamClient+        it "gives back what a stream of an unknown type took of the limit" $ \_ ->+            runManyUnknownStreamsClient+        it "does not send a request header section over the server's limit" $ \_ ->+            runTooLargeForServerClient+        it "does not send a response header section over the client's limit" $ \_ ->+            runTooLargeForClientClient  runClient :: IO () runClient = QUIC.run testClientConfig $ \conn ->@@ -65,6 +101,211 @@         go build = do             bs <- C.getResponseBodyChunk rsp             if B.null bs then return (B.concat (build [])) else go (build . (bs :))++-- | A DATA frame may carry nothing (RFC 9114, section 7.2.1).  One sent right+-- after HEADERS used to be taken for the end of the body, and the server read+-- nothing of what followed.+runEmptyDataClient :: IO ()+runEmptyDataClient = QUIC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do+        let hooks = (confHooks conf0){C.onHeadersFrameCreated = (++ [emptyData])}+            conf = conf0{confHooks = hooks}+            req = C.requestBuilder methodPost "/length" [] "hello"+        C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+            sendRequest req $ \rsp -> do+                C.responseStatus rsp `shouldBe` Just ok200+                C.getResponseBodyChunk rsp `shouldReturn` "5"+  where+    emptyData = H3Frame H3FrameData ""++-- | RFC 9204, section 4.4.2: a decoder that gives up on a stream sends a+-- Stream Cancellation, so that the encoder stops keeping the entries the+-- field sections it will never hear about refer to.  None was ever sent.+--+-- The first response is read to the end and the second is not; only the+-- second is cancelled.  What the client writes on its decoder stream is taken+-- from the QUIC packets it sends.+runCancelClient :: IO ()+runCancelClient = do+    writeIORef sentOnDecoderStream []+    let qcc =+            testClientConfig+                { ccHooks =+                    (ccHooks testClientConfig){onPlainCreated = recordDecoderStream}+                }+    QUIC.run qcc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+            C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+                let req = C.requestNoBody methodGet "/" []+                -- Stream 0, read to the end.+                sendRequest req $ \rsp -> do+                    let drain = do+                            bs <- C.getResponseBodyChunk rsp+                            unless (B.null bs) drain+                    drain+                -- Stream 4, not.+                sendRequest req $ \_rsp -> return ()+                -- Time for the cancellation to go out.+                threadDelay 100000+    bs <- B.concat <$> readIORef sentOnDecoderStream+    -- Stream Cancellation is 01 and then the stream ID in six bits.+    B.elem 0x40 bs `shouldBe` False+    B.elem 0x44 bs `shouldBe` True++-- | RFC 9114, section 6.2: the receiver of a stream of an unknown type must+-- stop reading it or throw away what arrives on it.  The server did neither.+--+-- STOP_SENDING is answered with a RESET_STREAM carrying the same code, so the+-- client sending one for its stream of a reserved type shows the server asked+-- it to stop.  The connection goes on.+runUnknownStreamClient :: IO ()+runUnknownStreamClient = do+    writeIORef sentResets []+    let qcc =+            testClientConfig+                { ccHooks =+                    (ccHooks testClientConfig){onPlainCreated = recordResets}+                }+    sid <- QUIC.run qcc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+            C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+                -- 0x21 is the first of the reserved types, 0x1f * N + 0x21.+                strm <- Q.unidirectionalStream conn+                Q.sendStream strm "\x21hello"+                let req = C.requestNoBody methodGet "/" []+                sendRequest req $ \rsp ->+                    C.responseStatus rsp `shouldBe` Just ok200+                threadDelay 200000+                return $ Q.streamId strm+    lookup sid <$> readIORef sentResets+        `shouldReturn` Just H3StreamCreationError++-- | The test server lets a client have 10 unidirectional streams, and the+-- control and QPACK streams take 3.  Opening 15 more, one after another,+-- needs the server to give each back once it is done with it: quic counts a+-- stream as done with only when it is closed, and the server used to stop+-- reading one of an unknown type and never close it.  The eighth then waited+-- for room that never came.+runManyUnknownStreamsClient :: IO ()+runManyUnknownStreamsClient = QUIC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \_sendRequest _aux -> do+            opened <- newIORef (0 :: Int)+            _ <- timeout 3000000 $ forM_ [1 .. 15 :: Int] $ \_ -> do+                strm <- Q.unidirectionalStream conn+                Q.sendStream strm "\x21hello"+                modifyIORef' opened (+ 1)+            readIORef opened `shouldReturn` 15++{-# NOINLINE sentResets #-}+sentResets :: IORef [(StreamId, ApplicationProtocolError)]+sentResets = unsafePerformIO $ newIORef []++{-# NOINLINE recordResets #-}+recordResets :: EncryptionLevel -> Plain -> Plain+recordResets _ plain = unsafePerformIO $ do+    forM_ (plainFrames plain) $ \frame -> case frame of+        ResetStream sid aerr _ ->+            atomicModifyIORef' sentResets $ \xs -> ((sid, aerr) : xs, ())+        _ -> return ()+    return plain++-- | RFC 9114, section 4.2.2: an endpoint "SHOULD NOT send an HTTP message+-- header that exceeds the indicated size".  The peer's limit used to be kept+-- and never consulted.+--+-- 40000 octets in one field is over the server's 32768; the caller is told,+-- and the connection goes on.+runTooLargeForServerClient :: IO ()+runTooLargeForServerClient = QUIC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+            let hello = C.requestNoBody methodGet "/" []+                ok rsp = C.responseStatus rsp `shouldBe` Just ok200+            -- The server's SETTINGS in first, so that its limit is known.+            sendRequest hello ok+            waitForSettings+            let big = C.requestNoBody methodGet "/" [("x-a", B.replicate 40000 0x62)]+                tooLarge FieldSectionTooLargeForPeer{} = True+            sendRequest big (\_ -> return ()) `shouldThrow` tooLarge+            sendRequest hello ok++-- | The client says it takes 1000 octets, and /bigheader answers with some+-- 2000.  The server does not send it, and resets the stream with+-- H3_INTERNAL_ERROR: the failure is its own.+runTooLargeForClientClient :: IO ()+runTooLargeForClientClient = do+    let qcc =+            testClientConfig+                { ccHooks =+                    (ccHooks testClientConfig)+                        { onResetStreamReceived = \_ aerr ->+                            E.throwIO $ Q.ApplicationProtocolErrorIsReceived aerr ""+                        }+                }+        isInternalError (Q.ApplicationProtocolErrorIsReceived aerr _) =+            aerr == H3InternalError+        isInternalError _ = False+        client = QUIC.run qcc $ \conn ->+            E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do+                let conf =+                        conf0+                            { confQDecoderConfig =+                                defaultQDecoderConfig{dcMaxFieldSectionSize = 1000}+                            }+                C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+                    -- Our SETTINGS in at the server first.+                    sendRequest (C.requestNoBody methodGet "/" []) $ \rsp ->+                        C.responseStatus rsp `shouldBe` Just ok200+                    waitForSettings+                    sendRequest (C.requestNoBody methodGet "/bigheader" []) (\_ -> return ())+    client `shouldThrow` isInternalError++-- | A response reset right after a DATA frame reads, to the body reader,+-- just like one that ended there.  It is not one, and the client has to send+-- a Stream Cancellation for it all the same; it used to take the reset for+-- the end and send none.+runResetCancelClient :: IO ()+runResetCancelClient = do+    writeIORef sentOnDecoderStream []+    let qcc =+            testClientConfig+                { ccHooks =+                    (ccHooks testClientConfig){onPlainCreated = recordDecoderStream}+                }+    QUIC.run qcc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+            C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+                let req = C.requestNoBody methodGet "/reset" []+                -- Stream 0, read to what looks like its end.+                sendRequest req $ \rsp -> do+                    let drain = do+                            bs <- C.getResponseBodyChunk rsp+                            unless (B.null bs) drain+                    drain+                threadDelay 100000+    bs <- B.concat <$> readIORef sentOnDecoderStream+    B.elem 0x40 bs `shouldBe` True++-- | The client's QPACK decoder stream: its third unidirectional stream, after+-- the control stream and the encoder stream.+clientDecoderStream :: StreamId+clientDecoderStream = 10++{-# NOINLINE sentOnDecoderStream #-}+sentOnDecoderStream :: IORef [B.ByteString]+sentOnDecoderStream = unsafePerformIO $ newIORef []++-- | The hook is pure, so this is the only way to see what goes out.+{-# NOINLINE recordDecoderStream #-}+recordDecoderStream :: EncryptionLevel -> Plain -> Plain+recordDecoderStream _ plain = unsafePerformIO $ do+    forM_ (plainFrames plain) $ \frame -> case frame of+        StreamF sid _ dats _+            | sid == clientDecoderStream ->+                atomicModifyIORef' sentOnDecoderStream $ \xs -> (xs ++ dats, ())+        _ -> return ()+    return plain  client0 :: C.Client () client0 sendRequest _aux = do
test/QPACK/HeaderBlockSpec.hs view
@@ -2,10 +2,23 @@  module QPACK.HeaderBlockSpec where +import Control.Concurrent (threadDelay)+import Control.Concurrent.Async+import Control.Concurrent.STM+import Control.Monad import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8+import Data.IORef+import Data.Maybe (isNothing) import Network.HPACK.Token (toToken) import Network.QPACK+import Network.QPACK.Internal (+    DecoderInstruction (..),+    EncoderInstruction (..),+    encodeDecoderInstructions,+    encodeEncoderInstructions,+ )+import System.Timeout (timeout) import Test.Hspec  spec :: Spec@@ -19,17 +32,319 @@             -- advertise.             mapM_ roundTrip [1000, 2100, 8000, 20000] +        it "encodes a field larger than the encoder's buffers" $ do+            -- With the default 4096-octet buffers.  4083 is the value that+            -- makes the field come to exactly the buffer by the encoder's+            -- estimate, which used to loop forever; anything larger was+            -- refused with BufferOverrun.  100000 is also long enough for+            -- its Huffman code to need a four-octet length.+            forM_ [4083, 4084, 10000, 100000] $ \n -> do+                r <- timeout 5000000 $ roundTripWith defaultQEncoderConfig n+                void r `shouldBe` Just ()++        it "agrees with the decoder on MaxEntries when the encoder uses less" $ do+            -- The decoder offers 64K, the encoder settles for 4K.  Required+            -- Insert Count is encoded against the 64K both ends know about;+            -- encoding it against the 4K made the two ends disagree once+            -- 2*128 entries had been inserted.+            roundTripMany 4096 65536 600++        it "does not refer to an unacknowledged entry when blocking is not allowed" $ do+            -- SETTINGS_QPACK_BLOCKED_STREAMS is 0, and no acknowledgement+            -- ever arrives: every section has to be decodable with what the+            -- decoder is known to hold, i.e. Required Insert Count 0.  The+            -- third one used to refer to the entry the second one inserted.+            (enc, _, tblop) <- newQEncoder defaultQEncoderConfig (\_ -> return ())+            setCapacity tblop 4096+            setBlockedStreams tblop 0+            forM_ [0 .. 4] $ \i -> do+                blk <- enc (i * 4) [(toToken "x-foo", "bar")]+                encodedInsertCount blk `shouldBe` 0++        it "keeps the number of blocked streams within the decoder's limit" $ do+            -- One blocked stream allowed, and nothing is ever acknowledged, so+            -- every section that refers to the dynamic table is blocked: at+            -- most one of them may do so.+            (enc, _, tblop) <- newQEncoder defaultQEncoderConfig (\_ -> return ())+            setCapacity tblop 4096+            setBlockedStreams tblop 1+            blks <- forM [0 .. 29] $ \i -> do+                let hdr = [(toToken "x-foo", C8.pack (show (i `div` 3 :: Int)))]+                enc (i * 4) hdr+            length (filter ((/= 0) . encodedInsertCount) blks) `shouldSatisfy` (<= 1)++        it "takes two outstanding sections on one stream one at a time" $ do+            -- Headers and trailers on the same stream, both referring to the+            -- dynamic table and so both acknowledged.  The second used to+            -- replace the first, and the second acknowledgement then found+            -- nothing and was taken for a decoder stream error.+            eiRef <- newIORef []+            diRef <- newIORef []+            let save ref bs = modifyIORef' ref (++ [bs])+            (enc, handleDI, tblop) <- newQEncoder defaultQEncoderConfig (save eiRef)+            (dec, handleEI) <- newQDecoder defaultQDecoderConfig (save diRef)+            setCapacity tblop 4096+            setBlockedStreams tblop 100+            let hdr = [(toToken "x-foo", "bar")]+                exchange sid = do+                    blk <- enc sid hdr+                    drain eiRef handleEI+                    (ths, _) <- dec sid blk+                    ths `shouldBe` hdr+                    return blk+            -- Get the field into the table and acknowledged.+            forM_ [0, 4, 8] $ \sid -> exchange sid >> drain diRef handleDI+            blk1 <- exchange 12+            blk2 <- exchange 12+            map encodedInsertCount [blk1, blk2] `shouldSatisfy` all (/= 0)+            drain diRef handleDI++        it "keeps the decoder's entries across a change of capacity" $ do+            -- The encoder may change the capacity at any time.  The decoder+            -- used to reallocate its table when told, so the entry inserted+            -- before the change was gone when referred to after it.+            eiRef <- newIORef []+            (dec, handleEI) <- newQDecoder defaultQDecoderConfig (\_ -> return ())+            ins <-+                encodeEncoderInstructions+                    [ SetDynamicTableCapacity 4096+                    , InsertWithLiteralName (toToken "x-foo") "bar"+                    , SetDynamicTableCapacity 2048+                    ]+                    False+            writeIORef eiRef [ins]+            drain eiRef handleEI+            -- Required Insert Count 1 (encoded as 2 against 128 entries),+            -- Delta Base 0, then an indexed field line for relative index 0.+            (ths, _) <- dec 0 $ BS.pack [0x02, 0x00, 0x80]+            ths `shouldBe` [(toToken "x-foo", "bar")]++        it "stops counting a blocked stream whose wait is given up" $ do+            -- One blocked stream allowed.  A section waiting for an entry+            -- that has not arrived is abandoned, as when its stream is reset.+            -- The stream it was counted as used to stay counted, and the next+            -- section that had to wait was refused as one too many.+            eiRef <- newIORef []+            (dec, handleEI) <-+                newQDecoder defaultQDecoderConfig{dcBlockedSterams = 1} (\_ -> return ())+            -- Required Insert Count 1, Delta Base 0, then an indexed field+            -- line for relative index 0: the first entry, not inserted yet.+            let blk = BS.pack [0x02, 0x00, 0x80]+            withAsync (dec 0 blk) $ \a -> do+                threadDelay 100000+                poll a >>= (`shouldSatisfy` isNothing)+            withAsync (dec 4 blk) $ \a -> do+                threadDelay 100000+                poll a >>= (`shouldSatisfy` isNothing)+                ins <-+                    encodeEncoderInstructions+                        [ SetDynamicTableCapacity 4096+                        , InsertWithLiteralName (toToken "x-foo") "bar"+                        ]+                        False+                writeIORef eiRef [ins]+                drain eiRef handleEI+                (ths, _) <- wait a+                ths `shouldBe` [(toToken "x-foo", "bar")]++        it "evicts an entry nothing refers to once its insertion is acknowledged" $ do+            -- No stream may block, and nothing is acknowledged at first, so+            -- every entry is inserted without being referred to.  Such an+            -- entry used to be kept for good, and once the table was full+            -- nothing new went in.+            eiRef <- newIORef []+            diRef <- newIORef []+            (enc, handleDI, tblop) <-+                newQEncoder defaultQEncoderConfig (\bs -> modifyIORef' eiRef (++ [bs]))+            setCapacity tblop 256+            setBlockedStreams tblop 0+            let inserts i = do+                    writeIORef eiRef []+                    -- Twice, since a field is only inserted the second time+                    -- it is seen.+                    forM_ [0, 1] $ \j ->+                        enc ((i * 2 + j) * 4) [(toToken (C8.pack ("x-foo-" ++ show i)), "a")]+                    any (not . BS.null) <$> readIORef eiRef+            -- 40 octets each: six fit in 256.+            mapM inserts [0 .. 5] `shouldReturn` replicate 6 True+            -- Full, and none of them acknowledged, so none is evictable.+            inserts 6 `shouldReturn` False+            di <- encodeDecoderInstructions [InsertCountIncrement 6]+            writeIORef diRef [di]+            drain diRef handleDI+            inserts 7 `shouldReturn` True++        it "releases the sections of a stream the decoder cancels" $ do+            -- One blocked stream allowed, and nothing acknowledged.  Stream 4+            -- takes it, so stream 8 may not refer to the entry.  Once the+            -- decoder cancels stream 4, stream 12 may.  The cancellation used+            -- to be ignored, and stream 4 stayed blocked for good.+            diRef <- newIORef []+            (enc, handleDI, tblop) <- newQEncoder defaultQEncoderConfig (\_ -> return ())+            setCapacity tblop 4096+            setBlockedStreams tblop 1+            let hdr = [(toToken "x-foo", "bar")]+            -- Inserted the second time it is seen.+            _ <- enc 0 hdr+            enc 4 hdr >>= (`shouldSatisfy` (/= 0)) . encodedInsertCount+            enc 8 hdr >>= (`shouldBe` 0) . encodedInsertCount+            di <- encodeDecoderInstructions [StreamCancellation 4]+            writeIORef diRef [di]+            drain diRef handleDI+            enc 12 hdr >>= (`shouldSatisfy` (/= 0)) . encodedInsertCount++        it "refuses a section that decodes to more than the announced limit" $ do+            -- A field line of one octet refers to a 1037-octet entry, so a+            -- section of a few octets decodes to far more than its length.+            -- Only the length of the frame used to be capped.+            eiRef <- newIORef []+            (dec, handleEI) <-+                newQDecoder+                    defaultQDecoderConfig{dcMaxFieldSectionSize = 2000}+                    (\_ -> return ())+            ins <-+                encodeEncoderInstructions+                    [ SetDynamicTableCapacity 4096+                    , InsertWithLiteralName (toToken "x-foo") (C8.replicate 1000 'a')+                    ]+                    False+            writeIORef eiRef [ins]+            drain eiRef handleEI+            -- Required Insert Count 1, Delta Base 0, then indexed field+            -- lines for relative index 0: once, and then twice.+            (ths, _) <- dec 0 $ BS.pack [0x02, 0x00, 0x80]+            map fst ths `shouldBe` [toToken "x-foo"]+            dec 4 (BS.pack [0x02, 0x00, 0x80, 0x80])+                `shouldThrow` (== FieldSectionTooLarge)++        it "inserts with a reference to a name not yet acknowledged" $ do+            -- No stream may block, and nothing is acknowledged.  x-foo is in+            -- the table, unacknowledged, so a field line may not refer to it;+            -- but an insertion may, on the encoder stream, and x-foo: b is+            -- inserted so.  The encoder used to give that up and insert+            -- nothing, since it guarded the insertion as though it were a+            -- field line.+            eiRef <- newIORef []+            (enc, _, tblop) <-+                newQEncoder defaultQEncoderConfig (\bs -> modifyIORef' eiRef (++ [bs]))+            (dec, handleEI) <- newQDecoder defaultQDecoderConfig (\_ -> return ())+            setCapacity tblop 4096+            setBlockedStreams tblop 0+            let hdr v = [(toToken "x-foo", v)]+            -- Twice each, since a field is only inserted the second time it+            -- is seen.+            blks1 <- forM [0, 4] $ \sid -> enc sid (hdr "a")+            eis1 <- readIORef eiRef+            writeIORef eiRef []+            blks2 <- forM [8, 12] $ \sid -> enc sid (hdr "b")+            eis2 <- readIORef eiRef+            BS.concat eis2 `shouldSatisfy` (not . BS.null)+            -- And what went out still decodes.+            eiQ <- newIORef [BS.concat (eis1 ++ eis2)]+            drain eiQ handleEI+            forM_ (zip [0, 4, 8, 12] (blks1 ++ blks2)) $ \(sid, blk) -> do+                encodedInsertCount blk `shouldBe` 0+                (ths, _) <- dec sid blk+                ths `shouldSatisfy` (`elem` [hdr "a", hdr "b"])++        it "refuses a section more than the peer will take" $ do+            -- RFC 9114, section 4.2.2: "SHOULD NOT send".  The peer's limit+            -- used to be kept and never looked at.  Refused before anything+            -- is inserted, so the encoder stream carries nothing.+            eiRef <- newIORef []+            (enc, _, tblop) <-+                newQEncoder defaultQEncoderConfig (\bs -> modifyIORef' eiRef (++ [bs]))+            setCapacity tblop 4096+            setHeaderSize tblop 100+            writeIORef eiRef []+            -- 5 + 100 + 32 octets as the limit counts them.+            enc 0 [(toToken "x-foo", C8.replicate 100 'a')]+                `shouldThrow` (== FieldSectionTooLargeForPeer 137 100)+            BS.concat <$> readIORef eiRef `shouldReturn` ""+            void $ enc 4 [(toToken "x-foo", "bar")]++-- | Feeding an instruction handler what has been queued for it, then an end+-- of stream, so that it returns -- or throws -- here.+drain :: IORef [BS.ByteString] -> ((Int -> IO BS.ByteString) -> IO ()) -> IO ()+drain ref handler = do+    bss <- readIORef ref+    writeIORef ref []+    queue <- newIORef [BS.concat bss]+    handler $ \_ -> atomicModifyIORef' queue $ \xs -> case xs of+        [] -> ([], "")+        y : ys -> (ys, y)+ roundTrip :: Int -> IO () roundTrip n = do-    (enc, _, _) <--        newQEncoder-            defaultQEncoderConfig{ecHeaderBlockBufferSize = 262144}+    blk <- roundTripWith defaultQEncoderConfig{ecHeaderBlockBufferSize = 262144} n+    -- The encoded section stays well under the announced limit; it is only+    -- the decoded value that is large.+    BS.length blk `shouldSatisfy` (< dcMaxFieldSectionSize defaultQDecoderConfig)++-- | Encoding and decoding a field with an n-octet value; the encoded field+-- section is returned.+roundTripWith :: QEncoderConfig -> Int -> IO BS.ByteString+roundTripWith conf n = do+    (enc, _, _) <- newQEncoder conf (\_ -> return ())+    -- Room for the largest field here, which the default limit on a field+    -- section is not.+    (dec, _) <-+        newQDecoder+            defaultQDecoderConfig{dcMaxFieldSectionSize = 1000000}             (\_ -> return ())-    (dec, _) <- newQDecoder defaultQDecoderConfig (\_ -> return ())     let val = C8.replicate n 'a'     blk <- enc 0 [(toToken ":status", "200"), (toToken "x-big", val)]     (ths, _) <- dec 0 blk-    -- The encoded section stays well under the announced limit; it is only-    -- the decoded value that is large.-    BS.length blk `shouldSatisfy` (< dcMaxFieldSectionSize defaultQDecoderConfig)     lookup (toToken "x-big") ths `shouldBe` Just val+    return blk++roundTripMany :: Int -> Int -> Int -> IO ()+roundTripMany encCap decCap n = do+    eiQ <- newTQueueIO+    diQ <- newTQueueIO+    (enc, handleDI, tblop) <-+        newQEncoder+            defaultQEncoderConfig{ecMaxTableCapacity = encCap}+            (atomically . writeTQueue eiQ)+    (dec, handleEI) <-+        newQDecoder+            defaultQDecoderConfig{dcMaxTableCapacity = decCap}+            (atomically . writeTQueue diQ)+    -- What the decoder's SETTINGS would carry.+    setCapacity tblop decCap+    setBlockedStreams tblop 100+    let recvFrom q _ = atomically $ readTQueue q+    ricMax <- newIORef 0+    withAsync (handleEI $ recvFrom eiQ) $ \_ ->+        withAsync (handleDI $ recvFrom diQ) $ \_ ->+            forM_ [0 .. n - 1] $ \i -> do+                -- Twice each, since a field is only inserted the second+                -- time it is seen.+                forM_ [0, 1] $ \j -> do+                    let sid = (i * 2 + j) * 4+                        hdr = [(toToken "x-count", C8.pack (show i))]+                    blk <- enc sid hdr+                    modifyIORef' ricMax (max (encodedInsertCount blk))+                    (ths, _) <- dec sid blk+                    ths `shouldBe` hdr+                    -- Let acknowledgements reach the encoder, so that+                    -- entries can be evicted and inserting goes on.+                    atomically $ isEmptyTQueue diQ >>= check+    -- Our own decoder would accept either reading of MaxEntries as long as+    -- the encoder used the same one, so look at the prefix itself: against+    -- the decoder's 2048 entries nothing here wraps, and the encoded count+    -- goes past what 2*128 would have allowed.+    readIORef ricMax >>= (`shouldSatisfy` (> 2 * (encCap `div` 32)))++-- | The encoded Required Insert Count: an integer with an 8-bit prefix.+encodedInsertCount :: BS.ByteString -> Int+encodedInsertCount blk = case BS.unpack blk of+    w : ws+        | w < 255 -> fromIntegral w+        | otherwise -> 255 + go 0 0 ws+    [] -> 0+  where+    go acc sh (x : xs)+        | x < 128 = acc + fromIntegral x * 2 ^ (sh :: Int)+        | otherwise = go (acc + fromIntegral (x - 128) * 2 ^ sh) (sh + 7) xs+    go acc _ [] = acc