packages feed

http3 0.1.9 → 0.1.10

raw patch · 29 files changed

+1402/−321 lines, 29 filesdep −base16-bytestringdep ~arraydep ~asyncdep ~bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: base16-bytestring

Dependency ranges changed: array, async, bytestring, case-insensitive, containers, http-types, network, network-byte-order, psqueues, quic, sockaddr, stm

API changes (from Hackage documentation)

+ Network.HTTP3.Internal: ISkip :: H3FrameType -> Int -> IFrame
+ Network.HTTP3.Server: runIOWith :: STM () -> Connection -> Config -> (ServerIO Stream -> IO (IO ())) -> IO ()
+ Network.HTTP3.Server: runWith :: STM () -> Connection -> Config -> Server -> IO ()
+ Network.QPACK.Internal: IllegalBase :: DecodeError
+ Network.QPACK.Internal: MalformedFieldLine :: String -> DecodeError
+ Network.QPACK.Internal: updateReported :: DynamicTable -> (Int -> IO Int) -> IO ()
- Network.QPACK.Internal: decodePrefix :: ReadBuffer -> DynamicTable -> IO (RequiredInsertCount, BasePoint, Bool)
+ Network.QPACK.Internal: decodePrefix :: ReadBuffer -> DynamicTable -> IO (RequiredInsertCount, BasePoint)
- Network.QPACK.Internal: decodeTokenHeader :: DynamicTable -> ReadBuffer -> IO (TokenHeaderTable, Bool)
+ Network.QPACK.Internal: decodeTokenHeader :: DynamicTable -> ReadBuffer -> IO (TokenHeaderTable, RequiredInsertCount)
- Network.QPACK.Internal: decodeTokenHeaderS :: DynamicTable -> ReadBuffer -> IO (Maybe ([Header], Bool))
+ Network.QPACK.Internal: decodeTokenHeaderS :: DynamicTable -> ReadBuffer -> IO (Maybe ([Header], RequiredInsertCount))

Files

ChangeLog.md view
@@ -1,5 +1,29 @@ # Revision history for http3 +## 0.1.10++These changes were made on main without PRs.++* New `runWith` and `runIOWith`: the connection goes away with two+  GOAWAYs and ends once the requests taken are answered.+* `runIO` processes each request stream in a thread of its own and+  returns once `sioDone` is called and every request is answered.+* Request streams are closed with H3_NO_ERROR (server) and+  H3_REQUEST_CANCELLED (client).+* A request that the server never started is rejected with+  H3_REQUEST_REJECTED, and the client gets the reset code as+  `StreamResetIsReceived`.+* A connection error stops where it is sent.+* Validate PUSH_PROMISE, CANCEL_PUSH, GOAWAY and MAX_PUSH_ID, and skip+  unknown frames.+* Answer 431 to a HEADERS frame longer than the limit, and check what+  follows the trailers.+* QPACK: validate the prefix, references and Insert Count Increment, and+  make a badly encoded field section QPACK_DECOMPRESSION_FAILED.+* HQ server: reset unfinished responses, and parse and bound the request+  line.+* Requires `quic` 0.3.20. The library's dependencies are bounded.+ ## 0.1.9  * A connection's threads are waited for, up to five seconds, before `run`
Network/HQ/Server.hs view
@@ -50,6 +50,7 @@  import Imports import Network.HTTP3.Config+import Network.HTTP3.Error (ApplicationProtocolError (H3InternalError)) import Network.HTTP3.Recv (newSource, readSource')  -- | Running an HQ server.@@ -61,57 +62,93 @@     forever $ do         strm <- QUIC.acceptStream conn         forkFinally-            (labelMe "H3 processRequest" >> processRequest conf mysa peersa server strm)-            (\_ -> QUIC.closeStream strm)+            (labelMe "HQ processRequest" >> processRequest conf mysa peersa server strm)+            (finish strm)+  where+    -- Only a response sent in full ends with a FIN.  One whose handler+    -- failed or timed out used to end so too, and the client took what it+    -- had for the whole response.+    finish strm (Right True) = QUIC.closeStream strm+    finish strm _ = do+        QUIC.resetStream strm H3InternalError+        QUIC.closeStream strm  labelMe :: String -> IO () labelMe label = do     tid <- myThreadId     labelThread tid label -processRequest :: Config -> SockAddr -> SockAddr -> Server -> Stream -> IO ()+-- | Whether the response went out in full.+--+-- The handler is killed when the timeout manager says so; it used to be+-- given a timeout that did nothing, so a client that never finished its+-- request line kept the thread for as long as the connection lasted.+processRequest :: Config -> SockAddr -> SockAddr -> Server -> Stream -> IO Bool processRequest conf mysa peersa server strm-    | QUIC.isClientInitiatedBidirectional sid =-        void $ T.withHandle (confTimeoutManager conf) (return ()) $ \th -> do-            vt <- recvHeader strm mysa-            src <- newSource strm-            refH <- newIORef Nothing-            let readB = readSource' src-                req = Request $ InpObj vt Nothing readB refH-                aux =-                    defaultAux-                        { auxTimeHandle = th-                        , auxMySockAddr = mysa-                        , auxPeerSockAddr = peersa-                        }-            server req aux $ sendResponse conf strm-    | otherwise = return () -- fixme: should consume the data?+    | QUIC.isClientInitiatedBidirectional sid = do+        done <- newIORef False+        T.withHandleKillThread (confTimeoutManager conf) (return ()) $ \th -> do+            mvt <- recvHeader strm mysa+            forM_ mvt $ \vt -> do+                src <- newSource strm+                refH <- newIORef Nothing+                let readB = readSource' src+                    req = Request $ InpObj vt Nothing readB refH+                    aux =+                        defaultAux+                            { auxTimeHandle = th+                            , auxMySockAddr = mysa+                            , auxPeerSockAddr = peersa+                            }+                server req aux $ sendResponse conf strm+                writeIORef done True+        readIORef done+    | otherwise = return True -- fixme: should consume the data?   where     sid = QUIC.streamId strm -recvHeader :: Stream -> SockAddr -> IO TokenHeaderTable+-- | The request line, as a header table, or 'Nothing' when it is not one.+recvHeader :: Stream -> SockAddr -> IO (Maybe TokenHeaderTable) recvHeader strm myaddr = do-    (method, path) <- recvRequestLine id-    let auth = showSockAddrBS myaddr-        vt =-            (":path", path)-                : (":method", method)-                : (":scheme", "https")-                : (":authority", auth)-                : []-    toTokenHeaderTable vt+    mpath <- recvRequestLine strm+    forM mpath $ \path -> do+        let auth = showSockAddrBS myaddr+            vt =+                (":path", path)+                    : (":method", "GET")+                    : (":scheme", "https")+                    : (":authority", auth)+                    : []+        toTokenHeaderTable vt++-- | The path of an HTTP/0.9 request line, "GET" and the path, ending in CRLF+-- or LF, or at the end of the stream.+--+-- It was taken to be the octets after the first four, less the last two,+-- of everything up to the end of the stream: no check that the method was+-- GET, a version taken as part of the path, the last octet of the path lost+-- to a bare LF, and no limit on what was held.+recvRequestLine :: Stream -> IO (Maybe ByteString)+recvRequestLine strm = loop ""   where-    recvRequestLine builder = do+    maxLength = 8192+    loop acc = do         bs <- QUIC.recvStream strm 1024-        if bs == ""-            then do-                return $ parseRequestLine $ BS.concat $ builder []-            else recvRequestLine (builder . (bs :))-    parseRequestLine bs = (method, path)+        let acc' = acc <> bs+        case BS.elemIndex 0x0a acc' of+            Just i -> return $ parse $ BS.take i acc'+            Nothing+                | BS.null bs -> return $ parse acc'+                | BS.length acc' > maxLength -> return Nothing+                | otherwise -> loop acc'+    parse line0 = case BS.stripPrefix "GET " line of+        Just rest+            | path <- BS.takeWhile (/= 0x20) rest+            , "/" `BS.isPrefixOf` path ->+                Just path+        _ -> Nothing       where-        method = "GET"-        path0 = BS.drop 4 bs-        path = BS.take (BS.length path0 - 2) path0+        line = fromMaybe line0 $ BS.stripSuffix "\r" line0  sendResponse :: Config -> Stream -> Response -> [PushPromise] -> IO () sendResponse conf strm (Response outobj) _ = case outObjBody outobj of
Network/HTTP3/Client.hs view
@@ -178,6 +178,18 @@             mvt <- recvFinalHeader ctx strm src             case mvt of                 Nothing -> do+                    -- The server reset the stream before answering: its code+                    -- goes to the application, which may retry a request+                    -- rejected with H3_REQUEST_REJECTED (RFC 9114, section+                    -- 4.1.1).  It used to be dropped for H3_MESSAGE_ERROR.+                    --+                    -- Not at once: a server closing the connection may reset+                    -- its streams first, and the connection error, which says+                    -- more, comes to this thread in the meantime.+                    mreset <- QUIC.resetReceived strm+                    forM_ mreset $ \aerr -> do+                        threadDelay 100000+                        E.throwIO $ StreamResetIsReceived aerr                     QUIC.resetStream strm H3MessageError                     threadDelay 100000                     -- just for type inference@@ -215,7 +227,7 @@     done sent strm = do         complete <- readIORef sent         if complete-            then closeStream strm+            then QUIC.closeStreamWith strm H3RequestCancelled             else QUIC.resetStream strm H3RequestCancelled     hdr = outObjHeaders outobj     isIPv6 = isJust (readMaybe auth :: Maybe IPv6)
Network/HTTP3/Context.hs view
@@ -26,14 +26,26 @@     forkManagedTimeout,     forkManagedTimeoutFinally,     isAsyncException,+    admitRequest,+    requestDone,+    goAway,+    goAwayAll,+    waitPTO,+    drained, ) where +import Control.Concurrent (threadDelay)+import Control.Concurrent.STM import qualified Control.Exception as E-import Control.Monad (void)+import Control.Monad (forM_, void) import Data.IORef+import Data.Maybe (isJust)+import Data.IntSet (IntSet)+import qualified Data.IntSet as IntSet+import Network.ByteOrder (withWriteBuffer) import Network.HTTP.Semantics.Client import Network.QUIC-import Network.QUIC.Internal (isClient, isServer)+import Network.QUIC.Internal (Microseconds (..), connLDCC, encodeInt', getPTO, isClient, isServer) import Network.Socket (SockAddr) import qualified System.ThreadManager as T import qualified System.TimeManager as TM@@ -68,6 +80,13 @@     -- ^ The peer's SETTINGS_MAX_FIELD_SECTION_SIZE, or 'maxBound' until its     -- SETTINGS have arrived     -- ^ Sending a Stream Cancellation on our QPACK decoder stream+    , ctxSendControl :: [H3Frame] -> IO ()+    , ctxGoAway :: TVar (Maybe StreamId)+    -- ^ Server only: the ID of our GOAWAY, from which no request is taken.+    , ctxLastRequest :: TVar (Maybe StreamId)+    -- ^ Server only: the largest request stream taken.+    , ctxInFlight :: TVar IntSet+    -- ^ Server only: the request streams taken and not yet done with.     }  -- | Running an action with a context, and seeing this connection's own@@ -93,12 +112,13 @@  newContext :: Connection -> Config -> IO Context newContext conn conf = do-    (sendEI, sendDI) <- setupUnidirectional conn conf+    (sendEI, sendDI, ctxSendControl) <- setupUnidirectional conn conf     (ctxQEncoder, handleDI, dyntblE) <- newQEncoder (confQEncoderConfig conf) sendEI     -- newQDecoder passes dyntbl for decoder to handleEI internally     (ctxQDecoder, handleEI) <- newQDecoder (confQDecoderConfig conf) sendDI     let ctxMaxFieldSectionSize = dcMaxFieldSectionSize $ confQDecoderConfig conf-    ctl <- controlStream conn ctxMaxFieldSectionSize dyntblE <$> newIORef IInit+    cst <- newControlState+    ctl <- controlStream conn ctxMaxFieldSectionSize dyntblE cst <$> newIORef IInit     let ctxPeerMaxFieldSectionSize = getHeaderSize dyntblE     seen <- newIORef []     info <- getConnectionInfo conn@@ -120,6 +140,9 @@                 (encodeDecoderInstructions [StreamCancellation sid] >>= sendDI)                     `E.catch` ignoreSync     ctxThreadManager <- T.newThreadManager+    ctxGoAway <- newTVarIO Nothing+    ctxLastRequest <- newTVarIO Nothing+    ctxInFlight <- newTVarIO IntSet.empty     let ctxTimeoutManager = confTimeoutManager conf     let ctxConnection = conn     return Context{..}@@ -131,7 +154,7 @@     abortWith :: ApplicationProtocolError -> E.SomeException -> IO ()     abortWith aerr se         | isAsyncException se = E.throwIO se-        | otherwise = abortConnection conn aerr ""+        | otherwise = connectionError conn aerr  isAsyncException :: E.Exception e => e -> Bool isAsyncException e =@@ -166,8 +189,8 @@     -- 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 ""+        | isServer conn -> \_ -> connectionError conn H3StreamCreationError+        | otherwise -> \_ -> connectionError conn H3IdError     -- 'unidirectional' does not get here with an unknown type.     H3StreamTypeUnknown _ -> \_ -> return ()   where@@ -175,11 +198,11 @@     once handler recv = do         dup <- atomicModifyIORef' seen $ \ts -> (styp : ts, styp `elem` ts)         if dup-            then abortConnection conn H3StreamCreationError ""+            then connectionError conn H3StreamCreationError             else handler recv     closing handler recv = do         handler recv-        abortConnection conn H3ClosedCriticalStream ""+        connectionError conn H3ClosedCriticalStream  isH3Server :: Context -> Bool isH3Server Context{..} = isServer ctxConnection@@ -238,20 +261,38 @@ pReadMaker = ctxPReadMaker  forkManaged :: Context -> String -> IO () -> IO ()-forkManaged Context{..} = T.forkManaged ctxThreadManager+forkManaged Context{..} label =+    T.forkManaged ctxThreadManager label . endingOnConnectionError  forkManagedTimeout :: Context -> String -> (TM.Handle -> IO ()) -> IO ()-forkManagedTimeout Context{..} =-    T.forkManagedTimeout ctxThreadManager ctxTimeoutManager+forkManagedTimeout Context{..} label body =+    T.forkManagedTimeout ctxThreadManager ctxTimeoutManager label $+        endingOnConnectionError . body  forkManagedTimeoutFinally     :: Context -> String -> (TM.Handle -> IO ()) -> IO () -> IO ()-forkManagedTimeoutFinally Context{..} =-    T.forkManagedTimeoutFinally ctxThreadManager ctxTimeoutManager+forkManagedTimeoutFinally Context{..} label body =+    T.forkManagedTimeoutFinally ctxThreadManager ctxTimeoutManager label $+        endingOnConnectionError . body -abort :: Context -> ApplicationProtocolError -> IO ()-abort ctx aerr = abortConnection (ctxConnection ctx) aerr ""+-- | A thread that has sent a connection error ('connectionError') ends there+-- quietly: the connection is going, and the error has been told to the peer.+--+-- So does one waiting on a connection the peer has closed with no error:+-- quic tells the wait with 'ConnectionIsClosed'.  The thread reading the+-- peer's control stream used to die of it, reported as uncaught.+endingOnConnectionError :: IO () -> IO ()+endingOnConnectionError io =+    io `E.catch` \e -> case e of+        ApplicationProtocolErrorIsSent _ _ -> return ()+        ConnectionIsClosed _ -> return ()+        _ -> E.throwIO e +-- | Closing the connection with this error, and stopping here: see+-- 'connectionError'.+abort :: Context -> ApplicationProtocolError -> IO a+abort ctx = connectionError (ctxConnection ctx)+ getHooks :: Context -> Hooks getHooks = ctxHooks @@ -266,3 +307,59 @@  getPeerMaxFieldSectionSize :: Context -> IO Int getPeerMaxFieldSectionSize = ctxPeerMaxFieldSectionSize++----------------------------------------------------------------+-- GOAWAY of ours (RFC 9114, section 5.2)++-- | Taking a request stream, unless our GOAWAY has gone out at or below+--   it.  One that is not taken must be rejected, not processed.+admitRequest :: Context -> StreamId -> IO Bool+admitRequest Context{..} sid = atomically $ do+    lim <- readTVar ctxGoAway+    if maybe False (sid >=) lim+        then return False+        else do+            modifyTVar' ctxLastRequest $ Just . maybe sid (max sid)+            modifyTVar' ctxInFlight $ IntSet.insert sid+            return True++-- | Done with a request stream taken by 'admitRequest'.+requestDone :: Context -> StreamId -> IO ()+requestDone Context{..} sid = atomically $ modifyTVar' ctxInFlight $ IntSet.delete sid++-- | Sending GOAWAY once, with the first request stream not taken: the+--   streams taken so far are still answered.+goAway :: Context -> IO ()+goAway ctx@Context{..} = do+    mlim <- atomically $ do+        old <- readTVar ctxGoAway+        case old of+            Just _ -> return Nothing+            Nothing -> do+                lim <- maybe 0 (+ 4) <$> readTVar ctxLastRequest+                writeTVar ctxGoAway $ Just lim+                return $ Just lim+    forM_ mlim $ sendGoAway ctx++-- | The first of two GOAWAYs: the largest identifier, which stops the+--   client sending requests and takes every one already on its way.+goAwayAll :: Context -> IO ()+goAwayAll ctx = sendGoAway ctx $ 2 ^ (62 :: Int) - 4++-- | Waiting for the connection's probe timeout, which is at least a round+--   trip.+waitPTO :: Context -> IO ()+waitPTO Context{..} = do+    Microseconds pto <- getPTO $ connLDCC ctxConnection+    threadDelay pto++sendGoAway :: Context -> StreamId -> IO ()+sendGoAway Context{..} lim = do+    payload <- withWriteBuffer 8 $ \wbuf -> encodeInt' wbuf $ fromIntegral lim+    ctxSendControl [H3Frame H3FrameGoaway payload]++-- | Waiting until our GOAWAY is out and no request taken is left.+drained :: Context -> STM ()+drained Context{..} = do+    readTVar ctxGoAway >>= check . isJust+    readTVar ctxInFlight >>= check . IntSet.null
Network/HTTP3/Control.hs view
@@ -4,6 +4,9 @@ module Network.HTTP3.Control (     setupUnidirectional,     controlStream,+    ControlState,+    newControlState,+    connectionError, ) where  import Control.Concurrent.MVar@@ -13,6 +16,7 @@ import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet import Network.QUIC+import Network.QUIC.Internal (isServer)  import Imports import qualified Network.HTTP3.Config as H3@@ -28,7 +32,11 @@ setupUnidirectional     :: Connection     -> H3.Config-    -> IO (EncodedEncoderInstruction -> IO (), EncodedDecoderInstruction -> IO ())+    -> IO+        ( EncodedEncoderInstruction -> IO ()+        , EncodedDecoderInstruction -> IO ()+        , [H3Frame] -> IO ()+        ) setupUnidirectional conn conf@H3.Config{..} = do     let QDecoderConfig{..} = confQDecoderConfig     settings <-@@ -56,6 +64,7 @@     return         ( critical $ sendStream sE         , critical $ \bs -> withMVar lockD $ \_ -> sendStream sD bs+        , critical $ sendStreamMany sC . encodeH3Frames         )   where     -- The sending part of one of our QPACK streams is closed only when the@@ -66,22 +75,45 @@     -- and nothing caught it there.     critical send bs =         send bs `E.catch` \e -> case e of-            StreamIsClosed -> abortConnection conn H3ClosedCriticalStream ""+            StreamIsClosed -> connectionError conn H3ClosedCriticalStream             _ -> E.throwIO e     stC = mkType H3ControlStreams     stE = mkType QPACKEncoderStream     stD = mkType QPACKDecoderStream     hooks = H3.confHooks conf +-- | Closing the connection with this error, and stopping here.+--+-- 'abortConnection' only throws to the connection's main thread, so code+-- after it went on running in this one: a frame refused was then handled.+connectionError :: Connection -> ApplicationProtocolError -> IO a+connectionError conn aerr = do+    abortConnection conn aerr ""+    E.throwIO $ ApplicationProtocolErrorIsSent aerr ""++-- | The last GOAWAY and MAX_PUSH_ID the peer sent on its control stream.+data ControlState = ControlState+    { lastGoaway :: IORef (Maybe Int64)+    , lastMaxPushId :: IORef (Maybe Int64)+    }++newControlState :: IO ControlState+newControlState = ControlState <$> newIORef Nothing <*> newIORef Nothing+ -- DynamicTable for Encoder controlStream-    :: Connection -> Int -> TableOperation -> IORef IFrame -> InstructionHandler-controlStream conn lim tblop ref recv = loop0+    :: Connection+    -> Int+    -> TableOperation+    -> ControlState+    -> IORef IFrame+    -> InstructionHandler+controlStream conn lim tblop cst ref recv = loop0   where     loop0 = do         bs <- recv 1024         if bs == ""-            then abortConnection conn H3ClosedCriticalStream ""+            then connectionError conn H3ClosedCriticalStream             else do                 (done, st1) <- readIORef ref >>= parse0 bs                 writeIORef ref st1@@ -89,7 +121,7 @@     loop = do         bs <- recv 1024         if bs == ""-            then abortConnection conn H3ClosedCriticalStream ""+            then connectionError conn H3ClosedCriticalStream             else do                 readIORef ref >>= parse bs >>= writeIORef ref                 loop@@ -100,15 +132,13 @@                 -- business on a control stream, so refuse it before any of it                 -- is buffered rather than after.                 | Just H3FrameData <- frameTypeOf st1' -> do-                    abortConnection conn H3FrameUnexpected ""-                    return (False, IInit)+                    connectionError conn H3FrameUnexpected             ITooLong _ _ -> do-                abortConnection conn H3ExcessiveLoad ""-                return (False, IInit)+                connectionError conn H3ExcessiveLoad             IDone typ payload leftover -> do                 case typ of                     H3FrameSettings -> checkSettings conn tblop payload-                    _ -> abortConnection conn H3MissingSettings ""+                    _ -> connectionError conn H3MissingSettings                 st1 <- parse leftover IInit                 return (True, st1)             st1 -> return (False, st1)@@ -117,23 +147,51 @@         case parseH3Frame lim st0 bs of             st1'                 | Just H3FrameData <- frameTypeOf st1' -> do-                    abortConnection conn H3FrameUnexpected ""-                    return IInit+                    connectionError conn H3FrameUnexpected             ITooLong _ _ -> do-                abortConnection conn H3ExcessiveLoad ""-                return IInit-            IDone typ _payload leftover -> do+                connectionError conn H3ExcessiveLoad+            IDone typ payload leftover -> do                 case typ of-                    H3FrameCancelPush -> return ()-                    H3FrameSettings -> abortConnection conn H3FrameUnexpected ""-                    H3FrameGoaway -> return ()-                    H3FrameMaxPushId -> return ()+                    H3FrameCancelPush -> singleInt payload >>= cancelPush+                    H3FrameSettings -> connectionError conn H3FrameUnexpected+                    H3FrameGoaway -> singleInt payload >>= goaway+                    H3FrameMaxPushId -> singleInt payload >>= maxPushId                     _                         | permittedInControlStream typ -> return ()-                        | otherwise -> abortConnection conn H3FrameUnexpected ""+                        | otherwise -> connectionError conn H3FrameUnexpected                 parse leftover IInit             st1 -> return st1 +    -- RFC 9114, section 7.1: the payload is exactly the fields the frame+    -- has, here one variable-length integer, or H3_FRAME_ERROR.+    singleInt payload = case parseQInt QInit payload of+        QDone i "" -> return i+        _ -> connectionError conn H3FrameError+    server = isServer conn+    -- Section 7.2.3: a push ID beyond what is allowed is H3_ID_ERROR.  A+    -- client allows none, since ours sends no MAX_PUSH_ID; a server, those+    -- up to the client's MAX_PUSH_ID.+    cancelPush pid = do+        allowed <-+            if server+                then readIORef (lastMaxPushId cst)+                else return Nothing+        unless (maybe False (pid <=) allowed) $ connectionError conn H3IdError+    -- Section 5.2: from a server, the ID of a client-initiated bidirectional+    -- stream; and in either direction, no larger than one sent before.+    -- What to do on GOAWAY is left to the caller of the connection.+    goaway i = do+        when (not server && i `mod` 4 /= 0) $ connectionError conn H3IdError+        prev <- readIORef (lastGoaway cst)+        when (maybe False (i >) prev) $ connectionError conn H3IdError+        writeIORef (lastGoaway cst) (Just i)+    -- Section 7.2.7: only a server takes MAX_PUSH_ID, and it may not go down.+    maxPushId i = do+        unless server $ connectionError conn H3FrameUnexpected+        prev <- readIORef (lastMaxPushId cst)+        when (maybe False (i <) prev) $ connectionError conn H3IdError+        writeIORef (lastMaxPushId cst) (Just i)+ checkSettings :: Connection -> TableOperation -> ByteString -> IO () checkSettings conn tblop payload = do     mh3settings <- decodeH3Settings payload@@ -141,7 +199,7 @@         -- RFC 9114 section 7.1: a payload that "terminates before the end of         -- the identified fields MUST be treated as a connection error of type         -- H3_FRAME_ERROR".-        Nothing -> abortConnection conn H3FrameError ""+        Nothing -> connectionError conn H3FrameError         Just h3settings -> loop IntSet.empty h3settings   where     loop :: IntSet -> H3Settings -> IO ()@@ -151,7 +209,7 @@         -- identifier, and this does.  A set rather than bits in an Int:         -- identifiers are variable-length integers, so everything from 64 up         -- fell off the end of the word and could repeat unnoticed.-        | i `IntSet.member` seen = abortConnection conn H3SettingsError ""+        | i `IntSet.member` seen = connectionError conn H3SettingsError         | otherwise = do             let seen' = IntSet.insert i seen             case k of@@ -171,7 +229,7 @@                     loop seen' ss                 _                     -- HTTP/2 settings-                    | i <= 0x6 -> abortConnection conn H3SettingsError ""+                    | i <= 0x6 -> connectionError conn H3SettingsError                     -- Unknown, so ignored -- but the rest of the frame is                     -- not.  This used to stop here, which meant a peer                     -- following section 7.2.4.1's advice to send a reserved
Network/HTTP3/Frame.hs view
@@ -205,6 +205,10 @@       ITooLong         H3FrameType         Int -- The length it claimed+    | -- | Skipping the payload of a frame of unknown type+      ISkip+        H3FrameType+        Int -- Bytes still to skip     deriving (Eq, Show)  -- | The frame type, once the parse has got far enough to know it.@@ -215,6 +219,7 @@ frameTypeOf (IPay typ _ _ _) = Just typ frameTypeOf (IDone typ _ _) = Just typ frameTypeOf (ITooLong typ _) = Just typ+frameTypeOf (ISkip typ _) = Just typ  -- | Feed bytes to a frame parse. --@@ -240,6 +245,10 @@ parseH3Frame lim (ILen typ ist) bs = case parseQInt ist bs of     QDone i bs'         | reqLen == 0 -> IDone typ "" bs'+        -- RFC 9114, section 7.2.8: frames of unknown types "MUST be+        -- ignored".  Their payload is skipped rather than held, so the cap+        -- does not apply: one over it used to close the connection.+        | ignorable typ -> parseH3Frame lim (ISkip typ reqLen) bs'         | typ /= H3FrameData && reqLen > lim -> ITooLong typ reqLen         | otherwise -> parseH3Frame lim (IPay typ reqLen 0 []) bs'       where@@ -253,7 +262,16 @@          in IDone typ (compose bs2 bss0) leftover   where     len1 = len0 + BS.length bs0+parseH3Frame _ (ISkip typ n) bs+    | BS.length bs < n = ISkip typ (n - BS.length bs)+    | otherwise = IDone typ "" (BS.drop n bs) parseH3Frame _ st _ = st++-- | A frame type not defined by RFC 9114, nor reserved there for one of+-- HTTP/2's (section 11.2.1).+ignorable :: H3FrameType -> Bool+ignorable (H3FrameUnknown i) = i > 0x09+ignorable _ = False  compose :: ByteString -> [ByteString] -> ByteString compose bs bss = BS.concat $ reverse (bs : bss)
Network/HTTP3/Recv.hs view
@@ -22,6 +22,7 @@ import Network.HTTP3.Context import Network.HTTP3.Error import Network.HTTP3.Frame+import Network.QPACK.Error (FieldSectionTooLarge (..))  data Source = Source     { sourceStream :: Stream@@ -68,6 +69,14 @@ pushbackSource _ "" = return () pushbackSource Source{..} bs = writeIORef sourcePending $ Just bs +-- | A PUSH_PROMISE on a request stream.  A server cannot receive one (RFC+-- 9114, section 7.2.5), and a client that has sent no MAX_PUSH_ID, as ours+-- never does, has allowed none (section 4.6).  It used to be ignored.+pushPromise :: Context -> IO a+pushPromise ctx+    | isH3Server ctx = abort ctx H3FrameUnexpected+    | otherwise = abort ctx H3IdError+ recvHeader :: Context -> StreamId -> Source -> IO (Maybe TokenHeaderTable) recvHeader ctx sid src = loop IInit   where@@ -77,9 +86,13 @@         if bs == ""             then return Nothing             else case parseH3Frame lim st bs of+                -- A field section longer than we said we would take, before+                -- it is decoded: the request is refused with 431 (RFC 9114,+                -- section 4.2.2), as one that decodes to too much is.  It+                -- used to close the connection with H3_EXCESSIVE_LOAD.+                ITooLong H3FrameHeaders _ -> E.throwIO FieldSectionTooLarge                 ITooLong _ _ -> do                     abort ctx H3ExcessiveLoad-                    loop IInit -- dummy                 st0                     -- Nothing here drains DATA, so it is not covered by the                     -- length cap; and it is not allowed before HEADERS@@ -87,17 +100,16 @@                     -- than after buffering whatever length it claimed.                     | Just H3FrameData <- frameTypeOf st0 -> do                         abort ctx H3FrameUnexpected-                        loop IInit -- dummy                 IDone typ payload leftover                     | typ == H3FrameHeaders -> do                         pushbackSource src leftover                         Just <$> qpackDecode ctx sid payload+                    | typ == H3FramePushPromise -> pushPromise ctx                     | permittedInRequestStream typ -> do                         pushbackSource src leftover                         loop IInit                     | otherwise -> do                         abort ctx H3FrameUnexpected-                        loop IInit -- dummy                 st' -> loop st'  -- | The first connection-specific field in a field section, if any.@@ -172,6 +184,28 @@     chunk bs = do         modifyIORef' refL (+ BS.length bs)         return (bs, False)+    -- The rest of the stream after the trailers, which may hold no HEADERS+    -- or DATA (RFC 9114, section 4.1).  It used to be dropped unread: the+    -- frames in the same read as the trailers, and whatever came after them.+    afterTrailers st = do+        bs <- readSource src+        if bs == ""+            then do+                when (st /= IInit) $ abort ctx H3FrameError+                writeIORef (sourceReadToEnd src) True+            else case parseH3Frame lim st bs of+                ITooLong _ _ -> abort ctx H3ExcessiveLoad+                st0+                    | Just typ <- frameTypeOf st0+                    , typ == H3FrameData || typ == H3FrameHeaders ->+                        abort ctx H3FrameUnexpected+                IDone typ _ leftover+                    | typ == H3FramePushPromise -> pushPromise ctx+                    | permittedInRequestStream typ -> do+                        pushbackSource src leftover+                        afterTrailers IInit+                    | otherwise -> abort ctx H3FrameUnexpected+                st' -> afterTrailers st'     loop st = do         bs <- readSource src         if bs == ""@@ -189,17 +223,12 @@                     Nothing                         | st /= IInit -> do                             abort ctx H3FrameError-                            E.throwIO $-                                ApplicationProtocolErrorIsSent-                                    H3FrameError-                                    "a frame cut off by the end of the stream"                         | otherwise -> do                             writeIORef (sourceReadToEnd src) True                             endOfBody             else case parseH3Frame lim st bs of                 ITooLong _ _ -> do                     abort ctx H3ExcessiveLoad-                    return ("", True) -- dummy                 IPay H3FrameData siz received bss -> do                     let st' = IPay H3FrameData siz received []                     if null bss@@ -210,11 +239,12 @@                 IDone typ payload leftover                     | typ == H3FrameHeaders -> do                         writeIORef refI IInit-                        -- pushbackSource src leftover -- fixme                         hdr <- qpackDecode ctx sid payload                         forM_ (connectionSpecificField hdr) $                             E.throwIO . ConnectionSpecificField                         writeIORef refH $ Just hdr+                        pushbackSource src leftover+                        afterTrailers IInit                         endOfBody                     | typ == H3FrameData -> do                         writeIORef refI IInit@@ -223,10 +253,10 @@                         -- 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+                    | typ == H3FramePushPromise -> pushPromise ctx                     | permittedInRequestStream typ -> do                         pushbackSource src leftover                         loop IInit                     | otherwise -> do                         abort ctx H3FrameUnexpected-                        return (payload, False) -- dummy                 st' -> loop st'
Network/HTTP3/Send.hs view
@@ -87,12 +87,23 @@     fp <- BS.mallocByteString 2048     Next len _reqflush mnext1 <- withForeignPtr fp $ \buf -> action buf 2048     if len == 0-        then return ("", Nothing, tlrmkr0)+        -- Nothing this time, but not necessarily the end: there may be a+        -- next.  This used to end the body here.+        then return ("", mnext1, tlrmkr0)         else do             let bs = PS fp 0 len-            NextTrailersMaker tlrmkr1 <- tlrmkr0 $ Just bs+            tlrmkr1 <- nextMaker <$> tlrmkr0 (Just bs)             return (bs, mnext1, tlrmkr1) +-- | The trailers maker to go on with after a chunk of the body.+--+-- One that answers with the trailers before the end keeps them for the end.+-- That was taken to be impossible, and failed with a pattern match; or the+-- trailers were dropped for none.+nextMaker :: NextTrailersMaker -> TrailersMaker+nextMaker (NextTrailersMaker t) = t+nextMaker tm@(Trailers _) = \_ -> return tm+ newByteStringAndSend     :: Stream     -> T.Handle@@ -106,10 +117,7 @@         then return (signal, tlrmkr0)         else do             let bs = PS fp 0 len-            tm <- tlrmkr0 $ Just bs-            let tlrmkr1 = case tm of-                    NextTrailersMaker t -> t-                    _ -> defaultTrailersMaker+            tlrmkr1 <- nextMaker <$> tlrmkr0 (Just bs)             encodeH3Frame (H3Frame H3FrameData bs) >>= sendStream strm             T.tickle th             return (signal, tlrmkr1)@@ -134,7 +142,10 @@                     }         strmbdy iface     tlrmkr <- readIORef ref-    Trailers trailers <- tlrmkr Nothing+    tm <- tlrmkr Nothing+    let trailers = case tm of+            Trailers t -> t+            _ -> []     unless (null trailers) $ sendHeader ctx strm th trailers   where     -- fixme: how to implement flush and cancel?@@ -150,9 +161,6 @@             newByteStringAndSend strm th tlrmkr1 writer >>= loop         loop (B.Chunk bs writer, tlrmkr1) = do             encodeH3Frame (H3Frame H3FrameData bs) >>= sendStream strm-            tm2 <- tlrmkr1 $ Just bs-            let tlrmkr2 = case tm2 of-                    NextTrailersMaker t -> t-                    _ -> defaultTrailersMaker+            tlrmkr2 <- nextMaker <$> tlrmkr1 (Just bs)             T.tickle th             newByteStringAndSend strm th tlrmkr2 writer >>= loop
Network/HTTP3/Server.hs view
@@ -5,6 +5,7 @@ module Network.HTTP3.Server (     -- * Runner     run,+    runWith,      -- * Common configration     Config (..),@@ -26,6 +27,7 @@      -- * Internal     runIO,+    runIOWith,     ServerIO (..), ) where @@ -33,6 +35,8 @@ import Control.Concurrent.Async import Control.Concurrent.STM import qualified Control.Exception as E+import qualified Control.Monad.STM as STM+import Data.IORef import GHC.Conc.Sync import Network.HTTP.Semantics import Network.HTTP.Semantics.Server@@ -40,7 +44,6 @@ import Network.HTTP2.Server.Internal (ServerIO (..)) import Network.QUIC (Connection, ConnectionInfo (..), Stream, getConnectionInfo) import qualified Network.QUIC as QUIC-import Network.QUIC.Internal (mainDone) import qualified System.TimeManager as T  import Imports@@ -54,32 +57,109 @@  -- | Running an HTTP\/3 server. run :: Connection -> Config -> Server -> IO ()-run conn conf server = withContext conn conf $ \ctx -> do+run = runServer Nothing++-- | Like 'run', but the connection goes away once the 'STM' action fires:+--   two GOAWAYs a PTO apart (RFC 9114, section 5.2), request streams from+--   the second one's ID on rejected, and the connection ended when the+--   requests taken before it are answered.+--+--   @since 0.1.10+runWith :: STM () -> Connection -> Config -> Server -> IO ()+runWith = runServer . Just++runServer :: Maybe (STM ()) -> Connection -> Config -> Server -> IO ()+runServer mstop conn conf server = withContext conn conf $ \ctx -> do     myThreadId >>= \t -> labelThread t "H3 server: run"-    readerServer ctx $ \strm ->-        -- 'processRequest' handles its own exceptions; only asynchronous ones-        -- leave it, and the thread manager takes those that it threw.-        forkManagedTimeoutFinally-            ctx-            "H3 server: processRequest"-            (processRequest ctx server strm)-            (closeStream strm)+    let serve = readerServer ctx $ \strm -> do+            started <- newIORef False+            -- 'processRequest' handles its own exceptions; only asynchronous ones+            -- leave it, and the thread manager takes those that it threw.+            forkManagedTimeoutFinally+                ctx+                "H3 server: processRequest"+                (\th -> writeIORef started True >> processRequest ctx server strm th)+                (finish ctx strm started)+    maybe serve (\stop -> race_ (goingAway ctx (True <$ stop)) serve) mstop+  where+    -- A thread stopped before it began, as the connection goes, has not+    -- looked at the request: it is rejected, which tells the client it may+    -- retry (RFC 9114, section 4.1.1).  The stream used to end with a FIN+    -- and nothing on it, which the client could only take for malformed.+    --+    -- The request is done with however that goes: a stream or connection+    -- already gone must not leave it in flight for a GOAWAY to wait on.+    finish ctx strm started =+        ( do+            s <- readIORef started+            unless s $ QUIC.resetStream strm H3RequestRejected+            QUIC.closeStreamWith strm H3NoError+        )+            `E.finally` requestDone ctx (QUIC.streamId strm) +-- | Once told: GOAWAY, and the requests taken before it answered.+--+-- Told to stop, in two steps (RFC 9114, section 5.2): the largest+-- identifier first, and the last request taken once a PTO has passed, so+-- that requests already on their way are taken.  An application that is+-- done answers nothing more, so its GOAWAY is the last one at once.+goingAway :: Context -> STM Bool -> IO ()+goingAway ctx told = do+    twoSteps <- atomically told+    when twoSteps $ do+        goAwayAll ctx+        waitPTO ctx+    goAway ctx+    atomically $ drained ctx+ runIO :: Connection -> Config -> (ServerIO Stream -> IO (IO ())) -> IO ()-runIO conn conf action = withContext conn conf $ \ctx -> do+runIO = runIOServer Nothing++-- | Like 'runIO', but the connection goes away once the 'STM' action+--   fires, as with 'runWith'.+--+--   @since 0.1.10+runIOWith+    :: STM () -> Connection -> Config -> (ServerIO Stream -> IO (IO ())) -> IO ()+runIOWith = runIOServer . Just++runIOServer+    :: Maybe (STM ())+    -> Connection+    -> Config+    -> (ServerIO Stream -> IO (IO ()))+    -> IO ()+runIOServer mstop conn conf action = withContext conn conf $ \ctx -> do     info <- getConnectionInfo conn     reqq <- newTQueueIO+    done <- newTVarIO False     let sio =             ServerIO                 { sioMySockAddr = localSockAddr info                 , sioPeerSockAddr = remoteSockAddr info                 , sioReadRequest = atomically $ readTQueue reqq-                , sioWriteResponse = sendResponseIO ctx-                , sioDone = mainDone conn+                , sioWriteResponse = \strm rsp ->+                    sendResponseIO ctx strm rsp+                        `E.finally` requestDone ctx (QUIC.streamId strm)+                , -- Not quic's 'mainDone': its sender ends once that is set+                  -- and nothing is queued, before the responses still to come.+                  sioDone = atomically $ writeTVar done True                 }-        put strmreq = atomically $ writeTQueue reqq strmreq+        put = atomically . writeTQueue reqq+        -- The application being done ends the connection as being told to+        -- does.  This used to wait for streams for good, and so the+        -- connection was left open until the peer or the idle timeout ended it.+        appDone = readTVar done >>= check     io <- action sio-    concurrently_ io (readerServer ctx $ processRequestIO ctx put)+    -- A thread per stream, as in 'run'.  Reading HEADERS on the accept loop+    -- let one stream that never sent them stop every stream after it.+    race_ (goingAway ctx $ (False <$ appDone) <|> maybe STM.retry (True <$) mstop) $+        concurrently_ io $+            readerServer ctx $ \strm ->+                forkManagedTimeout ctx "H3 server: processRequestIO" $ \_th -> do+                    let sid = QUIC.streamId strm+                    handed <- processRequestIO ctx put strm `E.onException` requestDone ctx sid+                    unless handed $ requestDone ctx sid  readerServer :: Context -> (Stream -> IO ()) -> IO () readerServer ctx action = loop@@ -91,7 +171,15 @@         | QUIC.isClientInitiatedUnidirectional sid =             forkManaged ctx "H3 server: unidirectional handler" $                 unidirectional ctx strm-        | QUIC.isClientInitiatedBidirectional sid = action strm+        | QUIC.isClientInitiatedBidirectional sid = do+            admitted <- admitRequest ctx sid+            -- At or above our GOAWAY: not processed, so the client may+            -- retry it elsewhere (RFC 9114, section 5.2).+            if admitted+                then action strm+                else do+                    QUIC.stopStream strm H3RequestRejected+                    QUIC.resetStream strm H3RequestRejected         | QUIC.isServerInitiatedUnidirectional sid = return () -- error         | otherwise = return ()       where@@ -137,6 +225,9 @@             E.throwIO se         | Just (_ :: DecodeError) <- E.fromException se =             abort ctx QpackDecompressionFailed+        -- A connection error has been sent: the stream goes with it.+        | Just (QUIC.ApplicationProtocolErrorIsSent _ _) <- E.fromException se =+            return ()         -- 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 =@@ -156,26 +247,33 @@         | Just FieldSectionTooLarge <- E.fromException se = True         | otherwise = False -processRequestIO :: Context -> ((Stream, Request) -> IO ()) -> Stream -> IO ()+-- | Answers whether the request was handed to the application.+processRequestIO :: Context -> ((Stream, Request) -> IO ()) -> Stream -> IO Bool processRequestIO ctx put strm = E.handle reset $ do     src <- newSource strm     emvt <- E.try $ recvHeader ctx sid src     case emvt of         Left FieldSectionTooLarge ->-            void $ withHandle ctx $ refuseTooLarge ctx strm-        Right Nothing -> QUIC.resetStream strm H3MessageError+            False <$ withHandle ctx (refuseTooLarge ctx strm)+        Right Nothing -> False <$ QUIC.resetStream strm H3MessageError         Right (Just ht) -> do             mreq <- mkRequest ctx strm src ht             case mreq of-                Nothing -> return ()-                Just req -> put (strm, req)+                Nothing -> return False+                Just req -> True <$ put (strm, req)   where     sid = QUIC.streamId strm     reset se-        | isAsyncException se = E.throwIO se+        -- Killed by the timeout, or with the connection, before the request+        -- was handed over.+        | isAsyncException se = do+            QUIC.resetStream strm H3RequestIncomplete+            E.throwIO se         | Just (_ :: DecodeError) <- E.fromException se =             abort ctx QpackDecompressionFailed-        | otherwise = QUIC.resetStream strm H3MessageError+        | Just (QUIC.ApplicationProtocolErrorIsSent _ _) <- E.fromException se =+            return False+        | otherwise = False <$ QUIC.resetStream strm H3MessageError  -- | Answering a request whose header section is more than we said we would --   take.@@ -237,4 +335,4 @@     void $ withHandle ctx $ \th -> do         sendHeader ctx strm th $ outObjHeaders outobj         sendBody ctx strm th outobj-        QUIC.closeStream strm+        QUIC.closeStreamWith strm H3NoError
Network/QPACK.hs view
@@ -515,9 +515,8 @@ qpackDecoder     :: DynamicTable -> StreamId -> EncodedFieldSection -> IO TokenHeaderTable qpackDecoder dyntbl sid bs = do-    (tbl, needAck) <- withReadBuffer bs $ \rbuf -> decodeTokenHeader dyntbl rbuf-    when needAck $-        encodeDecoderInstructions [SectionAcknowledgement sid] >>= sendIns dyntbl+    (tbl, ric) <- withReadBuffer bs $ \rbuf -> decodeTokenHeader dyntbl rbuf+    acknowledgeSection dyntbl sid ric     return tbl  qpackDecoderS@@ -527,11 +526,30 @@     mhs <- withReadBuffer bs $ \rbuf -> decodeTokenHeaderS dyntbl rbuf     case mhs of         Nothing -> return Nothing-        Just (hs, needAck) -> do-            when needAck $-                encodeDecoderInstructions [SectionAcknowledgement sid] >>= sendIns dyntbl+        Just (hs, ric) -> do+            acknowledgeSection dyntbl sid ric             return $ Just hs +-- | A Section Acknowledgement, for a section with a Required Insert Count+-- other than 0 (RFC 9204, section 4.4.1).  It tells the encoder that the+-- insertions up to that count have arrived.+acknowledgeSection :: DynamicTable -> StreamId -> RequiredInsertCount -> IO ()+acknowledgeSection _ _ 0 = return ()+acknowledgeSection dyntbl sid (RequiredInsertCount ric) =+    updateReported dyntbl $ \reported -> do+        encodeDecoderInstructions [SectionAcknowledgement sid] >>= sendIns dyntbl+        return $ max reported ric++-- | An Insert Count Increment for the insertions the encoder has not been+-- told of, by this or by a Section Acknowledgement (RFC 9204, section 4.4.3).+reportInsertions :: DynamicTable -> IO ()+reportInsertions dyntbl = updateReported dyntbl $ \reported -> do+    InsertionPoint ip <- getInsertionPoint dyntbl+    when (ip > reported) $+        encodeDecoderInstructions [InsertCountIncrement (ip - reported)]+            >>= sendIns dyntbl+    return $ max reported ip+ -- Note: dyntbl for decoder encoderInstructionHandler :: Int -> DynamicTable -> EncoderInstructionHandler encoderInstructionHandler decCapLim dyntbl recv = loop ""@@ -568,9 +586,8 @@ encoderInstructionHandlerS _ _dyntbl "" = return "" encoderInstructionHandlerS decCapLim dyntbl bs = do     (ins, leftover) <- decodeEncoderInstructions hufdec bs-    cnt <- sum <$> mapM handle ins-    when (cnt /= 0) $-        encodeDecoderInstructions [InsertCountIncrement cnt] >>= sendIns dyntbl+    mapM_ handle ins+    reportInsertions dyntbl     return leftover   where     hufdec = getHuffmanDecoder dyntbl -- only for encoder instruction handler@@ -579,7 +596,7 @@         | otherwise = do             setDecoderTableCapacity dyntbl n             qpackDebug dyntbl $ print ins-            return 0+            return ()     handle ins@(InsertWithNameReference ii val) = do         ready <- isTableReady dyntbl         unless ready $ E.throwIO EncoderInstructionError@@ -594,7 +611,7 @@             _ <- insertEntryToDecoder ent dyntbl             return idx         qpackDebug dyntbl $ putStrLn $ show ins ++ ": " ++ show dai-        return 1+        return ()     handle ins@(InsertWithLiteralName t val) = do         ready <- isTableReady dyntbl         unless ready $ E.throwIO EncoderInstructionError@@ -602,7 +619,7 @@             let ent = toEntryToken t val             insertEntryToDecoder ent dyntbl         qpackDebug dyntbl $ putStrLn $ show ins ++ ": " ++ show dai-        return 1+        return ()     handle ins@(Duplicate ri) = do         ready <- isTableReady dyntbl         unless ready $ E.throwIO EncoderInstructionError@@ -616,4 +633,4 @@         qpackDebug dyntbl $             putStrLn $                 show ins ++ ": " ++ show dai ++ " -> " ++ show dai'-        return 1+        return ()
Network/QPACK/Error.hs view
@@ -34,6 +34,11 @@     | -- | An absolute index outside the dynamic table's live window       IllegalDynamicIndex Int     | IllegalInsertCount+    | -- | A Base below zero (RFC 9204, section 4.5.1.2)+      IllegalBase+    | -- | A field line that is cut short, or whose Huffman string or+      -- integer is not one+      MalformedFieldLine String     | BlockedStreamsOverflow     deriving (Eq, Show) 
Network/QPACK/HeaderBlock/Decode.hs view
@@ -10,6 +10,7 @@ import Network.ByteOrder import qualified Network.HPACK as HPACK import Network.HPACK.Internal (+    Entry,     HuffmanDecoder,     decodeH,     decodeI,@@ -27,12 +28,43 @@ import Network.QPACK.Table import Network.QPACK.Types +-- | Errors in the encoding of a field section, as QPACK's 'DecodeError', and+-- so QPACK_DECOMPRESSION_FAILED (RFC 9204, section 2.2).  They came out as+-- HPACK's errors, or 'BufferOverrun' for a section cut short, which nothing+-- took for a decoding error: the request was reset as malformed.+asDecodeError :: IO a -> IO a+asDecodeError io =+    io+        `E.catches` [ E.Handler $ \e -> case e of+                        HPACK.TooLargeHeader -> E.throwIO FieldSectionTooLarge+                        _+                            | encoding e -> E.throwIO $ MalformedFieldLine $ show e+                            -- A field section well encoded but not a valid+                            -- one, such as a pseudo-header after a field:+                            -- the message is malformed.+                            | otherwise -> E.throwIO e+                    , E.Handler $ \BufferOverrun ->+                        E.throwIO $ MalformedFieldLine "cut short"+                    ]++-- | HPACK's errors in the encoding itself, as opposed to the fields it+-- decodes to.+encoding :: HPACK.DecodeError -> Bool+encoding e =+    e+        `elem` [ HPACK.EosInTheMiddle+               , HPACK.IllegalEos+               , HPACK.TooLongEos+               , HPACK.TooLargeInteger+               , HPACK.HeaderBlockTruncated+               ]+ decodeTokenHeader     :: DynamicTable     -> ReadBuffer-    -> IO (TokenHeaderTable, Bool)-decodeTokenHeader dyntbl rbuf = do-    (reqInsertCount, bp, needAck) <- decodePrefix rbuf dyntbl+    -> IO (TokenHeaderTable, RequiredInsertCount)+decodeTokenHeader dyntbl rbuf = asDecodeError $ do+    (reqInsertCount, bp) <- decodePrefix rbuf dyntbl     ready <- checkRequiredInsertCountNB dyntbl reqInsertCount     -- The count of blocked streams has to come down however the wait ends.     -- A stream reset while waiting kills the thread, and the count left up@@ -46,28 +78,27 @@             `E.finally` decreaseStreams dyntbl     checkRequiredInsertCount dyntbl reqInsertCount     hufdec <- newHuffmanDecoder rbuf-    dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl bp hufdec+    dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl reqInsertCount bp hufdec     -- HPACK's decoder refuses a section of more than 200 fields, which is     -- the same thing as far as we are concerned: more than we will take.-    tbl <--        decodeSophisticated dec rbuf `E.catch` \e -> case e of-            HPACK.TooLargeHeader -> E.throwIO FieldSectionTooLarge-            _ -> E.throwIO e-    return (tbl, needAck)+    -- 'asDecodeError' says so.+    tbl <- decodeSophisticated dec rbuf+    return (tbl, reqInsertCount)  decodeTokenHeaderS     :: DynamicTable     -> ReadBuffer-    -> IO (Maybe ([Header], Bool))-decodeTokenHeaderS dyntbl rbuf = do-    (reqInsertCount, bp, needAck) <- decodePrefix rbuf dyntbl+    -> IO (Maybe ([Header], RequiredInsertCount))+decodeTokenHeaderS dyntbl rbuf = asDecodeError $ do+    (reqInsertCount, bp) <- decodePrefix rbuf dyntbl     ok <- checkRequiredInsertCountNB dyntbl reqInsertCount     if ok         then do             hufdec <- newHuffmanDecoder rbuf-            dec <- limitFieldSection dyntbl $ toTokenHeader dyntbl bp hufdec+            dec <-+                limitFieldSection dyntbl $ toTokenHeader dyntbl reqInsertCount bp hufdec             hs <- decodeSimple dec rbuf-            return $ Just (hs, needAck)+            return $ Just (hs, reqInsertCount)         else return Nothing  -- | A field line decoder that stops once the section comes to more than we@@ -116,34 +147,54 @@ {- FOURMOLU_DISABLE -} toTokenHeader     :: DynamicTable+    -> RequiredInsertCount     -> BasePoint     -> HuffmanDecoder     -> Word8     -> ReadBuffer     -> IO TokenHeader-toTokenHeader dyntbl bp hufdec w8 rbuf+toTokenHeader dyntbl ric bp hufdec w8 rbuf     | w8 `testBit` 7 =-        decodeIndexedFieldLine                  rbuf dyntbl        bp w8+        decodeIndexedFieldLine                  rbuf dyntbl ric        bp w8     | w8 `testBit` 6 =-        decodeLiteralFieldLineWithNameReference rbuf dyntbl hufdec bp w8+        decodeLiteralFieldLineWithNameReference rbuf dyntbl ric hufdec bp w8     | w8 `testBit` 5 =-        decodeLiteralFieldLineWithLiteralName   rbuf dyntbl hufdec bp w8+        decodeLiteralFieldLineWithLiteralName   rbuf dyntbl     hufdec bp w8     | w8 `testBit` 4 =-        decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl        bp w8+        decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl ric        bp w8     | otherwise =-        decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl hufdec bp w8+        decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl ric hufdec bp w8 {- FOURMOLU_ENABLE -} +-- | The entry a field line refers to.+--+-- RFC 9204, section 2.2.3: a reference to an entry at or above the Required+-- Insert Count is QPACK_DECOMPRESSION_FAILED.  It was checked only against+-- what had been inserted, so a section with a Required Insert Count of 0 --+-- which is never acknowledged -- could name any entry in the table.+lookupEntry :: DynamicTable -> RequiredInsertCount -> HIndex -> IO Entry+lookupEntry dyntbl (RequiredInsertCount ric) hidx = do+    case hidx of+        DIndex (AbsoluteIndex ai)+            | ai >= ric -> E.throwIO $ IllegalDynamicIndex ai+        _ -> return ()+    atomically $ toIndexedEntry dyntbl hidx+ -- 4.5.2.  Indexed Field Line decodeIndexedFieldLine-    :: ReadBuffer -> DynamicTable -> BasePoint -> Word8 -> IO TokenHeader-decodeIndexedFieldLine rbuf dyntbl bp w8 = do+    :: ReadBuffer+    -> DynamicTable+    -> RequiredInsertCount+    -> BasePoint+    -> Word8+    -> IO TokenHeader+decodeIndexedFieldLine rbuf dyntbl ric bp w8 = do     i <- decodeI 6 (w8 .&. 0b00111111) rbuf     let static = w8 `testBit` 6         hidx             | static = SIndex $ AbsoluteIndex i             | otherwise = DIndex $ fromPreBaseIndex (PreBaseIndex i) bp-    ret <- atomically (entryTokenHeader <$> toIndexedEntry dyntbl hidx)+    ret <- entryTokenHeader <$> lookupEntry dyntbl ric hidx     qpackDebug dyntbl $         putStrLn $             "IndexedFieldLine (" ++ show hidx ++ ") " ++ showTokenHeader ret@@ -151,11 +202,16 @@  -- 4.5.3.  Indexed Field Line With Post-Base Index decodeIndexedFieldLineWithPostBaseIndex-    :: ReadBuffer -> DynamicTable -> BasePoint -> Word8 -> IO TokenHeader-decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl bp w8 = do+    :: ReadBuffer+    -> DynamicTable+    -> RequiredInsertCount+    -> BasePoint+    -> Word8+    -> IO TokenHeader+decodeIndexedFieldLineWithPostBaseIndex rbuf dyntbl ric bp w8 = do     i <- decodeI 4 (w8 .&. 0b00001111) rbuf     let hidx = DIndex $ fromPostBaseIndex (PostBaseIndex i) bp-    ret <- atomically (entryTokenHeader <$> toIndexedEntry dyntbl hidx)+    ret <- entryTokenHeader <$> lookupEntry dyntbl ric hidx     qpackDebug dyntbl $         putStrLn $             "IndexedFieldLineWithPostBaseIndex ("@@ -172,17 +228,18 @@ decodeLiteralFieldLineWithNameReference     :: ReadBuffer     -> DynamicTable+    -> RequiredInsertCount     -> HuffmanDecoder     -> BasePoint     -> Word8     -> IO TokenHeader-decodeLiteralFieldLineWithNameReference rbuf dyntbl hufdec bp w8 = do+decodeLiteralFieldLineWithNameReference rbuf dyntbl ric hufdec bp w8 = do     i <- decodeI 4 (w8 .&. 0b00001111) rbuf     let static = w8 `testBit` 4         hidx             | static = SIndex $ AbsoluteIndex i             | otherwise = DIndex $ fromPreBaseIndex (PreBaseIndex i) bp-    key <- atomically (entryToken <$> toIndexedEntry dyntbl hidx)+    key <- entryToken <$> lookupEntry dyntbl ric hidx     val <- decodeS (`clearBit` 7) (`testBit` 7) 7 hufdec rbuf     let ret = (key, val)     qpackDebug dyntbl $@@ -197,14 +254,15 @@ decodeLiteralFieldLineWithPostBaseNameReference     :: ReadBuffer     -> DynamicTable+    -> RequiredInsertCount     -> HuffmanDecoder     -> BasePoint     -> Word8     -> IO TokenHeader-decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl hufdec bp w8 = do+decodeLiteralFieldLineWithPostBaseNameReference rbuf dyntbl ric hufdec bp w8 = do     i <- decodeI 3 (w8 .&. 0b00000111) rbuf     let hidx = DIndex $ fromPostBaseIndex (PostBaseIndex i) bp-    key <- atomically (entryToken <$> toIndexedEntry dyntbl hidx)+    key <- entryToken <$> lookupEntry dyntbl ric hidx     val <- decodeS (`clearBit` 7) (`testBit` 7) 7 hufdec rbuf     let ret = (key, val)     qpackDebug dyntbl $
Network/QPACK/HeaderBlock/Prefix.hs view
@@ -48,6 +48,9 @@     | reqInsertCount > maxValue && reqInsertCount <= fullRange =         E.throw IllegalInsertCount     | reqInsertCount > maxValue = RequiredInsertCount (reqInsertCount - fullRange)+    -- RFC 9204, section 4.5.1.1: "Value of 0 must be encoded as 0."  An+    -- encoded 1 with nothing inserted yet came out as 0, and was taken.+    | reqInsertCount == 0 = E.throw IllegalInsertCount     | otherwise = RequiredInsertCount reqInsertCount   where     fullRange = 2 * maxEntries@@ -76,7 +79,11 @@ -- BasePoint 6 decodeBase :: RequiredInsertCount -> Bool -> Int -> BasePoint decodeBase (RequiredInsertCount reqInsCnt) False deltaBase = BasePoint (reqInsCnt + deltaBase)-decodeBase (RequiredInsertCount reqInsCnt) True deltaBase = BasePoint (reqInsCnt - deltaBase - 1)+-- RFC 9204, section 4.5.1.2: with the sign bit set, "the value of Required+-- Insert Count MUST be greater than the value of Delta Base".+decodeBase (RequiredInsertCount reqInsCnt) True deltaBase+    | reqInsCnt <= deltaBase = E.throw IllegalBase+    | otherwise = BasePoint (reqInsCnt - deltaBase - 1)  ---------------------------------------------------------------- @@ -101,7 +108,7 @@  -- | Decoding the prefix part of header block. decodePrefix-    :: ReadBuffer -> DynamicTable -> IO (RequiredInsertCount, BasePoint, Bool)+    :: ReadBuffer -> DynamicTable -> IO (RequiredInsertCount, BasePoint) decodePrefix rbuf dyntbl = do     maxEntries <- getMaxEntries dyntbl     totalNumberOfInserts <- getInsertionPoint dyntbl@@ -122,5 +129,4 @@                 ++ show totalNumberOfInserts                 ++ ", maxN "                 ++ show maxEntries-    let needAck = reqInsCnt /= 0-    return (reqInsCnt, baseIndex, needAck)+    return (reqInsCnt, baseIndex)
Network/QPACK/Table/Dynamic.hs view
@@ -55,6 +55,7 @@     wouldSectionBeBlocked,     wouldInstructionBeBlocked,     setInsersionPointToKnownReceivedCount,+    updateReported,      -- * Points     getBasePoint,@@ -152,6 +153,7 @@         { huffmanDecoder  :: HuffmanDecoder  -- only for encoder instruction handler         , blockedStreamsD :: IORef Int         , evictedD        :: TVar AbsoluteIndex -- the oldest entry not evicted+        , reportedD       :: MVar Int -- the Known Received Count told to the encoder         } {- FOURMOLU_ENABLE -} @@ -209,6 +211,7 @@     let huffmanDecoder = decodeH gcbuf huftmpsiz     blockedStreamsD <- newIORef 0     evictedD <- newTVarIO 0+    reportedD <- newMVar 0     newDynamicTable DecodeInfo{..} sendDI  newDynamicTable :: CodeInfo -> (ByteString -> IO ()) -> IO DynamicTable@@ -584,9 +587,15 @@  ---------------------------------------------------------------- +-- | RFC 9204, section 4.4.3: an increment that takes the Known Received+-- Count beyond what has been inserted is a QPACK_DECODER_STREAM_ERROR.  It+-- was added unchecked, and entries never acknowledged could be evicted. incrementKnownReceivedCount :: DynamicTable -> Int -> IO ()-incrementKnownReceivedCount DynamicTable{..} n =-    atomically $ modifyTVar' knownReceivedCount (+ n)+incrementKnownReceivedCount DynamicTable{..} n = atomically $ do+    krc <- readTVar knownReceivedCount+    InsertionPoint ip <- readTVar insertionPoint+    when (krc + n > ip) $ throwSTM DecoderInstructionError+    writeTVar knownReceivedCount (krc + n)   where     EncodeInfo{..} = codeInfo @@ -612,6 +621,19 @@     return (ai >= krc)   where     EncodeInfo{..} = codeInfo++-- | Telling the encoder of insertions, under a lock and with what it has+-- been told so far (decoder only).+--+-- A Section Acknowledgement tells it of insertions as well as an Insert+-- Count Increment does (RFC 9204, section 2.1.4).  The increment used to be+-- the number of entries just inserted, so a section acknowledged first had+-- them counted twice, and the encoder's Known Received Count went past what+-- had been inserted.+updateReported :: DynamicTable -> (Int -> IO Int) -> IO ()+updateReported DynamicTable{..} = modifyMVar_ reportedD+  where+    DecodeInfo{..} = codeInfo  setInsersionPointToKnownReceivedCount :: DynamicTable -> IO () setInsersionPointToKnownReceivedCount dyntbl@DynamicTable{..} = do
http3.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               http3-version:            0.1.9+version:            0.1.10 license:            BSD-3-Clause license-file:       LICENSE maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>@@ -76,22 +76,22 @@     ghc-options:        -Wall     build-depends:         base >=4.9 && <5,-        array,-        async,-        bytestring,-        case-insensitive,-        containers,+        array >=0.5 && <0.6,+        async >=2.2 && <2.3,+        bytestring >=0.10,+        case-insensitive >=1.2 && <1.3,+        containers >=0.6,         http-semantics >= 0.4 && <0.5,-        http-types,+        http-types >=0.12 && <0.13,         http2 >=5.4.10 && <5.5,         iproute >= 1.7 && < 1.8,-        network,-        network-byte-order,+        network >=3.1,+        network-byte-order >=0.1.7 && <0.2,         network-control >= 0.1.7 && <0.2,-        psqueues,-        quic >= 0.3.18 && < 0.4,-        sockaddr,-        stm,+        psqueues >=0.2 && <0.3,+        quic >= 0.3.20 && < 0.4,+        sockaddr >=0.0.1 && <0.1,+        stm >=2.5 && <2.6,         time-manager >= 0.5 && <0.6,         utf8-string >=1.0 && <1.1 @@ -107,15 +107,13 @@     ghc-options:        -Wall -threaded -rtsopts     build-depends:         base >=4.9 && <5,-        base16-bytestring,         bytestring,         crypton,-        filepath,         http-types,         http2,         http3,         quic,-        tls >=2.3.0 && <2.5,+        tls >=2.3.0 && <2.6,         tls-session-manager      if flag(devel)@@ -136,13 +134,11 @@     build-depends:         base >=4.9 && <5,         async,-        base16-bytestring,         bytestring,-        filepath,         http-types,         http3,         quic,-        tls >=2.3.0 && <2.5,+        tls >=2.3.0 && <2.6,         unix-time      if flag(devel)@@ -164,11 +160,8 @@         cereal,         conduit,         conduit-extra,-        containers,         filepath,-        http3,-        quic,-        stm+        http3      if flag(devel) @@ -190,8 +183,7 @@         conduit-extra,         containers,         http3,-        quic,-        stm+        quic      if flag(devel) @@ -204,13 +196,17 @@     build-tool-depends: hspec-discover:hspec-discover     hs-source-dirs:     test util     other-modules:+        HQ.ServerSpec+        HTTP3.ClientSpec         HTTP3.Config         HTTP3.Error         HTTP3.ErrorSpec         HTTP3.FrameSpec+        HTTP3.GoAwaySpec         HTTP3.HandlerSpec         HTTP3.InformationalSpec         HTTP3.LimitSpec+        HTTP3.RunIOSpec         HTTP3.Server         HTTP3.ServerSpec         QPACK.HeaderBlockSpec@@ -228,7 +224,6 @@         QuickCheck,         attoparsec,         async,-        base16-bytestring,         bytestring,         case-insensitive,         conduit,
+ test/HQ/ServerSpec.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}++module HQ.ServerSpec where++import Control.Concurrent+import qualified Control.Exception as E+import qualified Data.ByteString as BS+import Data.ByteString.Builder (Builder, byteString)+import qualified Network.HQ.Server as HQ+import Network.HTTP.Types+import qualified Network.QUIC as QUIC+import qualified Network.QUIC.Client as QUICC+import qualified Network.QUIC.Server as QUICS+import System.Timeout (timeout)+import Test.Hspec++import HTTP3.Config+import HTTP3.Server (teardown)++spec :: Spec+spec = beforeAll setupHQ $+    afterAll teardown $+        describe "HQ server" $ do+            it "answers a request" $ \_ ->+                request "GET /\r\n" `shouldReturn` Just ("hello", Nothing)+            it "takes the path alone from a request line with a version" $ \_ ->+                request "GET /path HTTP/1.0\r\n" `shouldReturn` Just ("/path", Nothing)+            it "takes a request line ending in LF alone" $ \_ ->+                request "GET /path\n" `shouldReturn` Just ("/path", Nothing)+            it "resets the stream of a request that is not GET" $ \_ ->+                fmap snd <$> request "POST /path\r\n" `shouldReturn` Just (Just resetCode)+            it "resets the stream of a response its handler failed to finish" $ \_ ->+                fmap snd <$> request "GET /fail\r\n" `shouldReturn` Just (Just resetCode)+            it "resets the stream of a request line that goes on and on" $ \_ ->+                fmap snd <$> request (BS.replicate 100000 0x61)+                    `shouldReturn` Just (Just resetCode)++resetCode :: QUIC.ApplicationProtocolError+resetCode = QUIC.ApplicationProtocolError 0x102++setupHQ :: IO ThreadId+setupHQ = do+    sc <- makeTestServerConfig+    tid <- forkIO $ QUICS.run sc $ \conn ->+        E.bracket HQ.allocSimpleConfig HQ.freeSimpleConfig $ \conf ->+            HQ.run conn conf hqServer+    threadDelay 500000 -- give enough time to the server+    return tid++-- | "/" answers hello, "/fail" fails after part of its response, and+-- anything else answers with its path.+hqServer :: HQ.Server+hqServer req _aux sendResponse = case HQ.requestPath req of+    Just "/" -> sendResponse (HQ.responseBuilder ok200 [] "hello") []+    Just "/fail" -> sendResponse (HQ.responseStreaming ok200 [] failing) []+    Just path -> sendResponse (HQ.responseBuilder ok200 [] (byteString path)) []+    Nothing -> sendResponse (HQ.responseNoBody badRequest400 []) []+  where+    failing :: (Builder -> IO ()) -> IO () -> IO ()+    failing write flush = do+        write "partial"+        flush+        E.throwIO $ userError "failing"++-- | Sending the octets as a request, and reading the response: what came,+-- and the code of a reset if there was one.+request+    :: BS.ByteString+    -> IO (Maybe (BS.ByteString, Maybe QUIC.ApplicationProtocolError))+request bs = timeout 3000000 $ QUICC.run testClientConfig $ \conn -> do+    strm <- QUIC.stream conn+    QUIC.sendStream strm bs+    QUIC.shutdownStream strm+    let loop acc = do+            x <- QUIC.recvStream strm 1024+            if BS.null x then return acc else loop (acc <> x)+    body <- loop "" `E.catch` \(E.SomeException _) -> return ""+    reset <- QUIC.resetReceived strm+    return (body, reset)
+ test/HTTP3/ClientSpec.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}++module HTTP3.ClientSpec where++import Control.Concurrent+import qualified Control.Exception as E+import Control.Monad+import Network.HTTP.Types+import qualified Network.HTTP3.Client as C+import Network.HTTP3.Internal (+    ApplicationProtocolError (..),+    StreamResetIsReceived (..),+ )+import qualified Network.QUIC as QUIC+import qualified Network.QUIC.Client as QUICC+import qualified Network.QUIC.Server as QUICS+import System.Timeout (timeout)+import Test.Hspec++import HTTP3.Config+import HTTP3.Server (teardown)++spec :: Spec+spec =+    describe "H3 client" $+        it "tells the application of a request the server rejected" $+            E.bracket setupRejecting teardown $ \_ ->+                timeout 3000000 rejectedRequest+                    `shouldReturn` Just (Left (StreamResetIsReceived H3RequestRejected))++-- | A server that resets every request stream with H3_REQUEST_REJECTED,+-- which says the request may be retried (RFC 9114, section 4.1.1).+setupRejecting :: IO ThreadId+setupRejecting = do+    sc <- makeTestServerConfig+    tid <- forkIO $ QUICS.run sc $ \conn -> forever $ do+        strm <- QUIC.acceptStream conn+        when (QUIC.isClientInitiatedBidirectional $ QUIC.streamId strm) $+            QUIC.resetStream strm H3RequestRejected+    threadDelay 500000 -- give enough time to the server+    return tid++-- | The client used to reset the stream back with H3_MESSAGE_ERROR and throw+-- a generic error, so the application could not tell it might retry.+rejectedRequest :: IO (Either StreamResetIsReceived ())+rejectedRequest = QUICC.run testClientConfig $ \conn ->+    E.bracket C.allocSimpleConfig C.freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+            E.try $ sendRequest (C.requestNoBody methodGet "/" []) $ \_ -> return ()
test/HTTP3/Config.hs view
@@ -8,10 +8,13 @@ ) where  import Control.Concurrent (threadDelay)+import qualified Control.Exception as E import Data.ByteString (ByteString) import qualified Data.List as L import qualified Network.HTTP3.Client as H3+import qualified Network.Socket as NS import Network.TLS (Credentials (..), credentialLoadX509)+import System.IO.Unsafe (unsafePerformIO)  import Network.QUIC.Client import Network.QUIC.Internal@@ -28,10 +31,22 @@             , scALPN = Just chooseALPN             } +-- | The UDP port of this test process, one the OS had free.+--+-- It used to be 8003 for every run, and two runs at once -- from two+-- working trees, say -- reached each other's servers: a test failed, or+-- passed, against another run's server.+testPort :: NS.PortNumber+testPort = unsafePerformIO $+    E.bracket (NS.socket NS.AF_INET NS.Datagram NS.defaultProtocol) NS.close $ \s -> do+        NS.bind s $ NS.SockAddrInet 0 $ NS.tupleToHostAddress (127, 0, 0, 1)+        NS.socketPort s+{-# NOINLINE testPort #-}+ testServerConfig :: ServerConfig testServerConfig =     defaultServerConfig-        { scAddresses = [("127.0.0.1", 8003)]+        { scAddresses = [("127.0.0.1", testPort)]         , -- 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.@@ -42,7 +57,7 @@ testClientConfig :: ClientConfig testClientConfig =     defaultClientConfig-        { ccPortName = "8003"+        { ccPortName = show testPort         , ccValidate = False         } 
test/HTTP3/Error.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeApplications #-}  module HTTP3.Error (     h3ErrorSpec,@@ -29,27 +30,16 @@  type Millisecond = Int +-- | Making a request, and then waiting for the connection error a test+-- expects, for as long as the timeout allows.+--+-- It used to wait a tenth of a second after the response.  A server+-- answering the request before it got round to the control stream, as one+-- under load could, then had the connection closed under it, and the test+-- failed for want of an error it was about to send. runC     :: ClientConfig -> H3.ClientConfig -> H3.Config -> Millisecond -> IO (Maybe ())-runC qcc cconf conf ms = timeout us $ run qcc $ \conn -> do-    info <- getConnectionInfo conn-    case alpn info of-        Just proto | "hq" `BS.isPrefixOf` proto -> do-            waitEstablished conn-            putStrLn $-                "Warning: "-                    ++ C8.unpack proto-                    ++ " is negotiated. Skipping this test. Use \"h3spec -s HTTP/3\" next time."-            E.throwIO $ ApplicationProtocolErrorIsReceived H3InternalError ""-        _ -> H3.run conn cconf conf client-  where-    us = ms * 1000-    client :: H3.Client ()-    client sendRequest _aux = do-        let req = H3.requestNoBody methodGet "/" []-        ret <- sendRequest req $ \_rsp -> return ()-        threadDelay 100000-        return ret+runC = runCReq $ H3.requestNoBody methodGet "/" []  -- | Like 'runC', but sending a request of our own choosing. runCReq@@ -59,20 +49,62 @@     -> H3.Config     -> Millisecond     -> IO (Maybe ())-runCReq req qcc cconf conf ms = timeout us $ run qcc $ \conn -> do+runCReq = runCWith True++-- | Like 'runCReq', for a test that expects no error: done a tenth of a+-- second after the response.+runCReqOk+    :: H3.Request+    -> ClientConfig+    -> H3.ClientConfig+    -> H3.Config+    -> Millisecond+    -> IO (Maybe ())+runCReqOk = runCWith False++-- | With 'True', a test that expects a connection error: a reset of the+-- request does not end it, since a server closing the connection may reset+-- its streams first.+runCWith+    :: Bool+    -> H3.Request+    -> ClientConfig+    -> H3.ClientConfig+    -> H3.Config+    -> Millisecond+    -> IO (Maybe ())+runCWith expectError req qcc cconf conf ms = timeout us $ run qcc $ \conn -> do     info <- getConnectionInfo conn     case alpn info of         Just proto | "hq" `BS.isPrefixOf` proto -> do             waitEstablished conn-            E.throwIO $ ApplicationProtocolErrorIsReceived H3InternalError ""+            E.throwIO $ HQNegotiated proto         _ -> H3.run conn cconf conf client   where     us = ms * 1000-    client sendRequest _aux = do-        ret <- sendRequest req $ \_rsp -> return ()-        threadDelay 100000-        return ret+    client :: H3.Client ()+    client sendRequest _aux+        | expectError = do+            _ <- E.try @StreamResetIsReceived $ sendRequest req $ \_rsp -> return ()+            threadDelay us+        | otherwise = do+            ret <- sendRequest req $ \_rsp -> return ()+            threadDelay 100000+            return ret +-- | Thrown when hq is negotiated instead of h3, so that the test is+-- reported as pending rather than passing on a generic error code.+newtype HQNegotiated = HQNegotiated BS.ByteString deriving (Show)++instance E.Exception HQNegotiated++-- | 'it' for the tests here, leaving a test pending against an hq server.+itH3 :: HasCallStack => String -> (a -> Expectation) -> SpecWith a+itH3 name body = it name $ \x ->+    body x `E.catch` \(HQNegotiated proto) ->+        pendingWith $+            C8.unpack proto ++ " is negotiated. Use \"h3spec -s HTTP/3\" next time."+ -- | The error cases for an HTTP/3 server. -- -- These also make h3spec, which runs them against a server somewhere else.@@ -89,184 +121,234 @@ h3ErrorSpec qcc cconf ms served = do     conf0 <- runIO H3.allocSimpleConfig     describe "HTTP/3 servers" $ do-        it+        itH3             "MUST send H3_FRAME_UNEXPECTED if DATA is received before HEADERS [HTTP/3 4.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated requestIllegalData                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]-        it "MUST send H3_MESSAGE_ERROR if a pseudo-header is duplicated [HTTP/3 4.1.1]" $ \_ -> do-            let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader3-                qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")-            runC qcc' cconf conf ms-                `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-        it-            "MUST send H3_MESSAGE_ERROR if mandatory pseudo-header fields are absent [HTTP/3 4.1.3]"+        itH3+            "MUST send H3_MESSAGE_ERROR if a pseudo-header is duplicated [HTTP/3 4.3.1]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader3+                    qcc' = throwOnReset qcc+                runC qcc' cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+        itH3+            "MUST send H3_MESSAGE_ERROR if mandatory pseudo-header fields are absent [HTTP/3 4.3.1]"             $ \x -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader0-                    qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                    qcc' = throwOnReset qcc                 refusedNotServed (served x) $                     runC qcc' cconf conf ms                         `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-        it-            "MUST send H3_MESSAGE_ERROR if prohibited pseudo-header fields are present[HTTP/3 4.1.3]"+        itH3+            "MUST send H3_MESSAGE_ERROR if prohibited pseudo-header fields are present [HTTP/3 4.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader1-                    qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                    qcc' = throwOnReset qcc                 runC qcc' cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-        it-            "MUST send H3_MESSAGE_ERROR if pseudo-header fields exist after fields [HTTP/3 4.1.3]"+        itH3+            "MUST send H3_MESSAGE_ERROR if pseudo-header fields exist after fields [HTTP/3 4.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader2-                    qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                    qcc' = throwOnReset qcc                 runC qcc' cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3MessageError]         forM_ connectionSpecific $ \field@(name, _) ->-            it+            itH3                 ( "MUST treat a request with "                     ++ C8.unpack (CI.original name)                     ++ " as malformed [HTTP/3 4.2]"                 )                 $ \x -> do                     let req = H3.requestNoBody methodGet "/" [field]-                        qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                        qcc' = throwOnReset qcc                     refusedNotServed (served x) $                         runCReq req qcc' cconf conf0 ms                             `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-        it "MUST accept TE with trailers [HTTP/3 4.2]" $ \_ -> do+        itH3 "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+            runCReqOk req qcc cconf conf0 ms `shouldReturn` Just ()+        itH3             "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                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3MissingSettings]-        it+        itH3             "MUST send H3_FRAME_UNEXPECTED if a DATA frame is received on a control stream [HTTP/3 7.2.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated controlData                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]-        it+        itH3             "MUST send H3_FRAME_UNEXPECTED if a HEADERS frame is received on a control stream [HTTP/3 7.2.2]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated controlHeaders                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]-        it+        itH3             "MUST send H3_FRAME_UNEXPECTED if a second SETTINGS frame is received [HTTP/3 7.2.4]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated doubleSettings                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]         {- this is MAY-                it "MUST send H3_SETTINGS_ERROR if duplicate setting identifiers exist [HTTP/3 7.2.4]" $ \_ -> do+                itH3 "MUST send H3_SETTINGS_ERROR if duplicate setting identifiers exist [HTTP/3 7.2.4]" $ \_ -> do                     let conf = addHook conf0 $ setOnControlFrameCreated illegalSettings0                     runC qcc cconf conf ms `shouldThrow` applicationProtocolErrorsIn [H3SettingsError]         -}-        it+        itH3             "MUST send H3_SETTINGS_ERROR if HTTP/2 settings are included [HTTP/3 7.2.4.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated illegalSettings1                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3SettingsError]-        it+        itH3             "MUST send H3_FRAME_ERROR if a SETTINGS frame stops mid-parameter [HTTP/3 7.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated truncatedSettings                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameError]-        it+        itH3             "MUST NOT stop reading settings at a reserved identifier [HTTP/3 7.2.4.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated greaseThenHttp2Setting                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3SettingsError]-        it-            "treats a repeated setting identifier as an error whatever its value [HTTP/3 7.2.4.1]"+        itH3+            "treats a repeated setting identifier as an error whatever its value [HTTP/3 7.2.4]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlFrameCreated duplicateLargeSetting                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3SettingsError]-        it-            "MUST send H3_FRAME_UNEXPECTED if CANCEL_PUSH is received in a request stream [HTTP/3 7.2.5]"+        itH3+            "MUST send H3_FRAME_UNEXPECTED if CANCEL_PUSH is received in a request stream [HTTP/3 7.2.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated requestCancelPush                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]-        it-            "MUST send QPACK_DECOMPRESSION_FAILED if an invalid static table index exits in a field line representation [QPACK 3.1]"+        itH3+            "MUST send H3_FRAME_UNEXPECTED if PUSH_PROMISE is received [HTTP/3 7.2.5]"             $ \_ -> do+                let conf = addHook conf0 $ setOnHeadersFrameCreated requestPushPromise+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3FrameUnexpected]+        itH3+            "MUST send H3_ID_ERROR if CANCEL_PUSH names a push ID not allowed [HTTP/3 7.2.3]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnControlFrameCreated cancelPushNotAllowed+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3IdError]+        itH3+            "MUST send H3_ID_ERROR if MAX_PUSH_ID goes down [HTTP/3 7.2.7]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnControlFrameCreated maxPushIdDown+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3IdError]+        itH3+            "MUST send H3_ID_ERROR if GOAWAY goes up [HTTP/3 5.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnControlFrameCreated goawayUp+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3IdError]+        itH3+            "MUST send H3_FRAME_ERROR if MAX_PUSH_ID has octets after its push ID [HTTP/3 7.1]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnControlFrameCreated maxPushIdTrailing+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [H3FrameError]+        itH3+            "MUST ignore a frame of unknown type, however long [HTTP/3 7.2.8]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnControlFrameCreated longUnknownFrame+                runCReqOk (H3.requestNoBody methodGet "/" []) qcc cconf conf ms+                    `shouldReturn` Just ()+        itH3+            "MUST send QPACK_DECOMPRESSION_FAILED if an invalid static table index exists in a field line representation [QPACK 3.1]"+            $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader4                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [QpackDecompressionFailed]-        it-            "MUST send QPACK_DECOMPRESSION_FAILED if a field line references a dynamic table entry that is not there [QPACK 2.1.2]"+        itH3+            "MUST send QPACK_DECOMPRESSION_FAILED if a field line references a dynamic table entry that is not there [QPACK 2.2.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader5                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [QpackDecompressionFailed]-        it-            "MUST NOT buffer a frame longer than SETTINGS_MAX_FIELD_SECTION_SIZE [HTTP/3 7.1]"+        itH3+            "MUST send QPACK_DECOMPRESSION_FAILED if a field line is cut short [QPACK 2.2.3]"             $ \_ -> do+                let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader6+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [QpackDecompressionFailed]+        itH3+            "MUST send QPACK_DECOMPRESSION_FAILED if a Huffman string is badly padded [QPACK 4.1.2]"+            $ \_ -> do+                let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader7+                runC qcc cconf conf ms+                    `shouldThrow` applicationProtocolErrorsIn [QpackDecompressionFailed]+        itH3+            "MUST NOT buffer an over-long frame on a control stream [HTTP/3 7.1]"+            $ \_ -> do                 let conf = addHook conf0 $ setOnControlStreamCreated overLongFrame                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ExcessiveLoad]-        it-            "MUST send QPACK_ENCODER_STREAM_ERROR if a new dynamic table capacity value exceeds the limit [QPACK 4.1.3]"+        itH3+            "MUST send QPACK_ENCODER_STREAM_ERROR if a new dynamic table capacity value exceeds the limit [QPACK 4.3.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnEncoderStreamCreated largeTableCapacity                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [QpackEncoderStreamError]-        it-            "MUST send H3_CLOSED_CRITICAL_STREAM if a control stream is closed [QPACK 4.2]"+        itH3+            "MUST send H3_CLOSED_CRITICAL_STREAM if a control stream is closed [HTTP/3 6.2.1]"             $ \_ -> do                 let conf = addHook conf0 $ setOnControlStreamCreated closeStream                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]-        it+        itH3             "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.                 opened <- newIORef False                 let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x00\x04\x00"                 expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]-        it+        itH3             "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.                 opened <- newIORef False                 let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x01\x00"                 expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]-        it+        itH3             "MUST send H3_STREAM_CREATION_ERROR if a second encoder stream is opened [QPACK 4.2]"             $ \_ -> do                 opened <- newIORef False                 let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother opened "\x02"                 expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]-        it+        itH3             "MUST send H3_STREAM_CREATION_ERROR if a second decoder stream is opened [QPACK 4.2]"             $ \_ -> do                 opened <- newIORef False                 let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother opened "\x03"                 expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]-        it+        itH3             "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream is closed [QPACK 4.2]"             $ \_ -> do                 let conf =                         noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated closeStream                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]-        it+        itH3             "MUST send H3_CLOSED_CRITICAL_STREAM if a decoder stream is closed [QPACK 4.2]"             $ \_ -> do                 let conf =                         noDecoderTable $ addHook conf0 $ setOnDecoderStreamCreated closeStream                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]-        it+        itH3             "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.@@ -275,7 +357,7 @@                         closeStream strm                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]-        it+        itH3             "MUST send QPACK_DECODER_STREAM_ERROR if Insert Count Increment is 0 [QPACK 4.4.3]"             $ \_ -> do                 let conf = addHook conf0 $ setOnDecoderStreamCreated zeroInsertCountIncrement@@ -333,23 +415,23 @@ h3DrainSpec qcc cconf ms = do     conf0 <- runIO H3.allocSimpleConfig     describe "HTTP/3 servers reading a request's body" $ do-        it+        itH3             "MUST treat content that does not match content-length as malformed [HTTP/3 4.1.2]"             $ \_ -> do                 -- content-length says five octets and the request carries                 -- none.  /drain reads the body, which is where the count is                 -- checked; a body nobody reads is never counted.                 let req = H3.requestNoBody methodPost "/drain" [("content-length", "5")]-                    qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+                    qcc' = throwOnReset qcc                 runCReq req qcc' cconf conf0 ms                     `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-        it+        itH3             "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 "")+                    qcc' = throwOnReset qcc                 runCReq req qcc' cconf conf0 ms                     `shouldThrow` applicationProtocolErrorsIn [H3MessageError] @@ -392,6 +474,32 @@ requestCancelPush :: [H3Frame] -> [H3Frame] requestCancelPush fs = H3Frame H3FrameCancelPush "" : fs +-- PUSH_PROMISE, push ID 0, and no field section to speak of.  Before+-- HEADERS, since a server need not read what comes after them.+requestPushPromise :: [H3Frame] -> [H3Frame]+requestPushPromise fs = H3Frame H3FramePushPromise "\x00" : fs++-- CANCEL_PUSH for push ID 0, with no MAX_PUSH_ID sent: no push is allowed.+cancelPushNotAllowed :: [H3Frame] -> [H3Frame]+cancelPushNotAllowed fs = fs ++ [H3Frame H3FrameCancelPush "\x00"]++-- MAX_PUSH_ID 5, then 3.+maxPushIdDown :: [H3Frame] -> [H3Frame]+maxPushIdDown fs = fs ++ [H3Frame H3FrameMaxPushId "\x05", H3Frame H3FrameMaxPushId "\x03"]++-- GOAWAY 0, then 4.+goawayUp :: [H3Frame] -> [H3Frame]+goawayUp fs = fs ++ [H3Frame H3FrameGoaway "\x00", H3Frame H3FrameGoaway "\x04"]++-- MAX_PUSH_ID 5, and an octet more.+maxPushIdTrailing :: [H3Frame] -> [H3Frame]+maxPushIdTrailing fs = fs ++ [H3Frame H3FrameMaxPushId "\x05\x00"]++-- A reserved type (0x21), with a payload longer than+-- SETTINGS_MAX_FIELD_SECTION_SIZE.+longUnknownFrame :: [H3Frame] -> [H3Frame]+longUnknownFrame fs = fs ++ [H3Frame (H3FrameUnknown 0x21) (BS.replicate 40000 0)]+ requestIllegalData :: [H3Frame] -> [H3Frame] requestIllegalData fs = H3Frame H3FrameData "" : fs @@ -466,6 +574,34 @@         "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x80"     ] +-- [(":method","GET")+-- ,(":scheme","https")+-- ,(":authority","127.0.0.1")+-- ,(":path","/")] ++ age with a value of five octets, of which two are there+--+-- Running off the end of the section raised BufferOverrun, which the server+-- answered with a stream reset of H3_MESSAGE_ERROR.+illegalHeader6 :: [H3Frame] -> [H3Frame]+illegalHeader6 _ =+    [ H3Frame+        H3FrameHeaders+        "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x52\x05ab"+    ]++-- [(":method","GET")+-- ,(":scheme","https")+-- ,(":authority","127.0.0.1")+-- ,(":path","/")] ++ age with a one-octet Huffman value of 0x00+--+-- The three bits left over are padding, which must be the start of EOS:+-- ones (RFC 7541, section 5.2).+illegalHeader7 :: [H3Frame] -> [H3Frame]+illegalHeader7 _ =+    [ H3Frame+        H3FrameHeaders+        "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x52\x81\x00"+    ]+ {- -- [(SettingsQpackBlockedStreams,100) -- ,(SettingsQpackMaxTableCapacity,4096)@@ -586,6 +722,11 @@ addQUICHook cc modify = cc'   where     cc' = cc{ccHooks = modify $ ccHooks cc}++-- | Turning a RESET_STREAM from the server into the exception the tests expect.+throwOnReset :: ClientConfig -> ClientConfig+throwOnReset qcc = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr ->+    E.throwIO (ApplicationProtocolErrorIsReceived aerr "")  setOnResetStreamReceived     :: (Stream -> ApplicationProtocolError -> IO ()) -> Hooks -> Hooks
+ test/HTTP3/GoAwaySpec.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module HTTP3.GoAwaySpec where++import Control.Concurrent+import Control.Concurrent.Async+import Control.Concurrent.STM+import qualified Control.Exception as E+import Network.HTTP.Types+import qualified Network.HTTP3.Client as C+import Network.HTTP3.Server+import qualified Network.QUIC.Client as QUICC+import qualified Network.QUIC.Server as QUICS+import System.Timeout (timeout)+import Test.Hspec++import HTTP3.Config+import HTTP3.Server (teardown)++spec :: Spec+spec =+    describe "H3 server going away" $+        -- RFC 9114, section 5.2: the request taken before GOAWAY is answered,+        -- one opened after it is rejected, and the connection then ends.+        do+            it "answers the request it took and rejects a later one" $ do+                stop <- newTVarIO False+                returned <- newEmptyMVar+                E.bracket (setupGoAway stop returned) teardown $ \_ -> do+                    -- Long after the second GOAWAY.+                    timeout 5000000 (slowThenFast 500000 stop) `shouldReturn` Just (Just ok200, False)+                    timeout 3000000 (takeMVar returned) `shouldReturn` Just ()+            -- The first GOAWAY has the largest identifier, and the second+            -- comes a PTO later: a request sent in between is taken.+            it "answers a request sent between its two GOAWAYs" $ do+                stop <- newTVarIO False+                returned <- newEmptyMVar+                E.bracket (setupGoAway stop returned) teardown $ \_ -> do+                    timeout 5000000 (slowThenFast 0 stop) `shouldReturn` Just (Just ok200, True)+                    timeout 3000000 (takeMVar returned) `shouldReturn` Just ()++-- | A server going away once the variable is set; the 'MVar' is filled when+-- 'runWith' returns.  "/slow" is answered after a second.+setupGoAway :: TVar Bool -> MVar () -> IO ThreadId+setupGoAway stop returned = do+    sc <- makeTestServerConfig+    tid <- forkIO $ QUICS.run sc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf -> do+            runWith (readTVar stop >>= check) conn conf svr+            putMVar returned ()+    threadDelay 500000 -- give enough time to the server+    return tid+  where+    svr req _aux sendResponse = do+        case requestPath req of+            Just "/slow" -> threadDelay 1000000+            _ -> return ()+        sendResponse (responseBuilder ok200 [] "hello") []++-- | A request for "/slow"; the server told to go away; then, after the+-- delay, a request for "/".  The status of the first, and whether the+-- second succeeded.+slowThenFast :: Int -> TVar Bool -> IO (Maybe Status, Bool)+slowThenFast delay stop = QUICC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+            let get path = sendRequest (C.requestNoBody methodGet path []) $ return . C.responseStatus+            slow <- async $ get "/slow"+            threadDelay 100000+            atomically $ writeTVar stop True+            threadDelay delay+            fast <- E.try $ get "/"+            s <- wait slow+            return (s, either (\(_ :: E.SomeException) -> False) (const True) fast)
test/HTTP3/LimitSpec.hs view
@@ -23,10 +23,13 @@ -- 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" $+    describe "H3 server keeping to a limit it does not announce" $ do         it "answers 431 to a request header section over its limit" $             E.bracket (setupWith hideLimit server 4096) teardown $                 \_ -> runTooLargeClient+        it "answers 431 to a HEADERS frame longer than its limit" $+            E.bracket (setupWith hideLimit server 4096) teardown $+                \_ -> runTooLongFrameClient  -- | SETTINGS with the dynamic table as the test server has it, and without -- SETTINGS_MAX_FIELD_SECTION_SIZE: QPACK_MAX_TABLE_CAPACITY 4096 and@@ -54,5 +57,17 @@             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++-- | One field with a 40000-octet value of "~", which Huffman makes no shorter: the+-- HEADERS frame itself is longer than the server's 32768.  The section was+-- refused with the 431 above only once decoded, and a frame this long closed+-- the whole connection with H3_EXCESSIVE_LOAD instead.+runTooLongFrameClient :: IO ()+runTooLongFrameClient = QUIC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+            let req = C.requestNoBody methodGet "/" [("x-a", B.replicate 40000 0x7e)]             sendRequest req $ \rsp ->                 C.responseStatus rsp `shouldBe` Just requestHeaderFieldsTooLarge431
+ test/HTTP3/RunIOSpec.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE OverloadedStrings #-}++module HTTP3.RunIOSpec where++import Control.Concurrent+import qualified Control.Exception as E+import Control.Monad+import Network.HTTP.Types+import qualified Network.HTTP3.Client as C+import Network.HTTP3.Server+import qualified Network.QUIC as QUIC+import qualified Network.QUIC.Client as QUICC+import qualified Network.QUIC.Server as QUICS+import System.Timeout (timeout)+import Test.Hspec hiding (runIO)++import HTTP3.Config+import HTTP3.Server (teardown)++spec :: Spec+spec =+    describe "H3 server with runIO" $ do+        it "serves a request while an earlier stream has sent no HEADERS" $+            E.bracket setupIO teardown $ \_ ->+                timeout 3000000 stalledThenRequest `shouldReturn` Just ()+        it "returns once sioDone is called and the requests are answered" $ do+            returned <- newEmptyMVar+            E.bracket (setupDone returned) teardown $ \_ -> do+                -- sioDone comes before the response: the request is still+                -- answered, and only then does runIO return.+                timeout 3000000 oneRequest `shouldReturn` Just ()+                timeout 3000000 (takeMVar returned) `shouldReturn` Just ()++setupIO :: IO ThreadId+setupIO = do+    sc <- makeTestServerConfig+    tid <- forkIO $ QUICS.run sc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+            runIO conn conf $ \sio -> return $ forever $ do+                (strm, _req) <- sioReadRequest sio+                sioWriteResponse sio strm $ responseBuilder ok200 [] "hello"+    threadDelay 500000 -- give enough time to the server+    return tid++-- | A server whose application takes one request, calls 'sioDone', and+-- then answers it.  The 'MVar' is filled when 'runIO' returns.+setupDone :: MVar () -> IO ThreadId+setupDone returned = do+    sc <- makeTestServerConfig+    tid <- forkIO $ QUICS.run sc $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf -> do+            runIO conn conf $ \sio -> return $ do+                (strm, _req) <- sioReadRequest sio+                sioDone sio+                sioWriteResponse sio strm $ responseBuilder ok200 [] "hello"+                forever $ sioReadRequest sio+            putMVar returned ()+    threadDelay 500000 -- give enough time to the server+    return tid++oneRequest :: IO ()+oneRequest = QUICC.run testClientConfig $ \conn ->+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+            sendRequest (C.requestNoBody methodGet "/" []) $ \rsp ->+                C.responseStatus rsp `shouldBe` Just ok200++-- | A bidirectional stream carrying only the type of a HEADERS frame, and+-- then an ordinary request on the same connection.+stalledThenRequest :: IO ()+stalledThenRequest = QUICC.run testClientConfig $ \conn -> do+    strm <- QUIC.stream conn+    QUIC.sendStream strm "\x01"+    E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+        C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+            sendRequest (C.requestNoBody methodGet "/" []) $ \rsp ->+                C.responseStatus rsp `shouldBe` Just ok200
test/HTTP3/ServerSpec.hs view
@@ -61,6 +61,12 @@             runResetRequestClient         it "resets a request interrupted before its body is sent" $ \_ ->             runInterruptedRequestClient+        it "does not hand the handler a CANCEL_PUSH it refused as body" $ \_ ->+            runCancelPushInBodyClient+        it "refuses DATA after trailers" $ \_ ->+            runDataAfterTrailersClient+        it "sends a request whose trailers maker answers early" $ \_ ->+            runEarlyTrailersClient         it "accepts a content-length on a response with no content" $ \_ ->             runNoContentClient         it "closes the connection on a stream ending in the middle of a frame" $ \_ ->@@ -344,6 +350,63 @@                 r                     `shouldBe` Just                         (Left (show (StreamResetIsReceived H3RequestCancelled)))++-- | A CANCEL_PUSH frame where the body should be.  It is a connection error+-- (RFC 9114, section 7.2.3), and its payload used to go on to the handler+-- as body all the same, since 'abort' did not stop the reader.+runCancelPushInBodyClient :: IO ()+runCancelPushInBodyClient = do+    _ <- tryTakeMVar recordedBody+    _ <- E.try @E.SomeException $ QUIC.run testClientConfig $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do+            let cancelPush = H3Frame H3FrameCancelPush "abc"+                hooks = (confHooks conf0){C.onHeadersFrameCreated = (++ [cancelPush])}+                conf = conf0{confHooks = hooks}+            C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+                void $ timeout 1000000 $ sendRequest (C.requestNoBody methodPost "/record" []) $ \_ ->+                    return ()+    r <- timeout 1000000 $ takeMVar recordedBody+    r `shouldSatisfy` \x -> case x of+        Just (Right _) -> False+        _ -> True++-- | A trailers maker that answers with the trailers at the first chunk of+-- the body, rather than asking to be called again.  The sender took that+-- for granted and failed with a pattern match.+runEarlyTrailersClient :: IO ()+runEarlyTrailersClient = do+    _ <- tryTakeMVar recordedBody+    QUIC.run testClientConfig $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf ->+            C.run conn testH3ClientConfig conf $ \sendRequest _aux -> do+                let req0 = C.requestBuilder methodPost "/record" [] "hello"+                    req = C.setRequestTrailersMaker req0 $ \_ ->+                        return $ C.Trailers [("x-t", "1")]+                sendRequest req $ \rsp -> C.responseStatus rsp `shouldBe` Just ok200+    timeout 1000000 (takeMVar recordedBody) `shouldReturn` Just (Right 5)++-- | A DATA frame after the trailers, which RFC 9114, section 4.1, makes+-- H3_FRAME_UNEXPECTED.  Whatever followed the trailers used to be dropped+-- unread, and the handler was told of a whole body.+--+-- The hook puts the DATA after every HEADERS frame: after the request's, where+-- it is body, and after the trailers.+runDataAfterTrailersClient :: IO ()+runDataAfterTrailersClient = do+    _ <- tryTakeMVar recordedBody+    _ <- E.try @E.SomeException $ QUIC.run testClientConfig $ \conn ->+        E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do+            let extra = H3Frame H3FrameData "zz"+                hooks = (confHooks conf0){C.onHeadersFrameCreated = (++ [extra])}+                conf = conf0{confHooks = hooks}+                req0 = C.requestBuilder methodPost "/record" [] "hello"+                req = C.setRequestTrailersMaker req0 (trailersMaker hashInit)+            C.run conn testH3ClientConfig conf $ \sendRequest _aux ->+                void $ timeout 1000000 $ sendRequest req $ \_ -> return ()+    r <- timeout 1000000 $ takeMVar recordedBody+    r `shouldSatisfy` \x -> case x of+        Just (Right _) -> False+        _ -> True  -- | Responses with no content and a content-length of 1000: to HEAD, and a -- 304.  The client checked the empty body against it and refused them with
test/QPACK/HeaderBlockSpec.hs view
@@ -14,7 +14,9 @@ import Network.HPACK.Token (toToken) import Network.QPACK import Network.QPACK.Internal (+    DecodeError (..),     DecoderInstruction (..),+    DecoderInstructionError (..),     EncoderInstruction (..),     EncoderInstructionError (..),     encodeDecoderInstructions,@@ -246,6 +248,41 @@             drain diRef handleDI             enc 12 hdr >>= (`shouldSatisfy` (/= 0)) . encodedInsertCount +        it "refuses an Insert Count Increment beyond what was inserted" $ do+            -- One entry inserted, five acknowledged.  It used to be added to+            -- the Known Received Count unchecked (RFC 9204, section 4.4.3).+            diRef <- newIORef []+            (enc, handleDI, tblop) <- newQEncoder defaultQEncoderConfig (\_ -> return ())+            setCapacity tblop 4096+            setBlockedStreams tblop 100+            let hdr = [(toToken "x-foo", "bar")]+            -- Inserted the second time it is seen.+            mapM_ (`enc` hdr) [0, 4]+            di <- encodeDecoderInstructions [InsertCountIncrement 5]+            writeIORef diRef [di]+            E.try (drain diRef handleDI) `shouldReturn` Left DecoderInstructionError++        it "refuses a reference at or above the Required Insert Count" $ do+            -- Entry 0 is in the table, but a section whose Required Insert+            -- Count is 0 may not name it (RFC 9204, section 2.2.3).+            -- Prefix 0x00 0x01: Required Insert Count 0, Base 1; then an+            -- Indexed Field Line, pre-base index 0, which is entry 0.+            dec <- decoderWithOneEntry+            decodeError (dec 0 "\x00\x01\x80")+                `shouldReturn` Left (IllegalDynamicIndex 0)++        it "refuses a negative Base" $ do+            -- Required Insert Count 1 (encoded 2), the sign bit and Delta+            -- Base 5: a Base of -5 (RFC 9204, section 4.5.1.2).+            dec <- decoderWithOneEntry+            decodeError (dec 0 "\x02\x85\x10") `shouldReturn` Left IllegalBase++        it "refuses an encoded Required Insert Count that comes to 0" $ do+            -- Encoded 1 with nothing inserted is 0, which has to be encoded+            -- as 0 (RFC 9204, section 4.5.1.1).  Then :method GET.+            (dec, _) <- newQDecoder defaultQDecoderConfig (\_ -> return ())+            decodeError (dec 0 "\x01\x00\xd1") `shouldReturn` Left IllegalInsertCount+         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.@@ -326,6 +363,22 @@     handler $ \_ -> atomicModifyIORef' queue $ \xs -> case xs of         [] -> ([], "")         y : ys -> (ys, y)++-- | The 'DecodeError' an action throws, if any.+decodeError :: IO a -> IO (Either DecodeError ())+decodeError io = E.try (void io)++-- | A decoder that has been sent one entry, x-foo: bar.+decoderWithOneEntry :: IO QDecoder+decoderWithOneEntry = do+    (dec, handleEI) <- newQDecoder defaultQDecoderConfig (\_ -> return ())+    ei <-+        encodeEncoderInstructions+            [SetDynamicTableCapacity 4096, InsertWithLiteralName (toToken "x-foo") "bar"]+            False+    ref <- newIORef [ei]+    drain ref handleEI+    return dec  roundTrip :: Int -> IO () roundTrip n = do
test/QPACK/QIF2Spec.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeApplications #-}  module QPACK.QIF2Spec where @@ -15,7 +16,7 @@ import Network.HTTP.Semantics import Network.HTTP.Semantics.Client.Internal import Network.HTTP.Semantics.Server-import Network.HTTP.Types (RequestHeaders, notFound404, ok200)+import Network.HTTP.Types (RequestHeaders, ok200) import qualified Network.HTTP3.Client as C import Network.HTTP3.Server import qualified Network.HTTP3.Server as S@@ -70,7 +71,9 @@     -- delay for set table capacity     threadDelay 10000     ms <- forkClient 0 id-    mapM_ takeMVar ms+    -- A failure in a request's thread is brought back here.  It used to+    -- kill the thread before it said it was done, and the test hung.+    mapM takeMVar ms >>= mapM_ (either E.throwIO return)   where     forkClient n build = do         hs <- headerlist hdl@@ -80,16 +83,23 @@                 var <- newEmptyMVar                 _ <- forkIO $ do                     let req = requestBuilder2 hs mempty-                    _ <- sendRequest req $ checkHeader hs-                    putMVar var ()+                    r <- E.try @E.SomeException $ sendRequest req $ checkHeader hs+                    putMVar var $ void r                 when ((n :: Int) `mod` 50 == 0) $ threadDelay 100000                 forkClient (n + 1) (build . (var :))     checkHeader hs rsp = do         bs <- getBody-        let hs0 = map keyValue $ split bs-            hs0' = sort $ filter (\(k, _) -> k == "cookie:") hs0-            hs' = sort $ filter (\(k, _) -> k == "cookie:") hs-        hs0' `shouldBe` hs'+        -- Every field but the pseudo-header ones, which the client may+        -- add to or rewrite.  This compared fields named "cookie:", which+        -- there are none of, and so compared two empty lists.+        -- Cookies are split into crumbs on both sides: the server joins+        -- them with "; " (RFC 9114, section 4.2.1).+        let regular = sort . concatMap crumbs . filter ((/= ":") . B.take 1 . foldedCase . fst)+            crumbs (k, v)+                | k == "cookie" = map ((,) k) $ splitOn "; " v+                | otherwise = [(k, v)]+            hs0 = map keyValue $ split bs+        regular hs0 `shouldBe` regular hs       where         getBody = B.concat <$> loop id         loop build = do@@ -104,15 +114,18 @@               where                 (l, ls') = C8.break (== '\n') ls                 ls'' = C8.drop 1 ls'+        splitOn sep s = case B.breakSubstring sep s of+            (x, "") -> [x]+            (x, rest) -> x : splitOn sep (B.drop (B.length sep) rest)         keyValue kv = (mk k, v)           where             (k, v') = C8.break (== ' ') kv             v = C8.drop 1 v' +-- | Answering with the fields of the request, whatever its method: the QIF+-- file has POSTs as well as GETs. server :: Server-server req _aux sendResponse = case requestMethod req of-    Just "GET" -> sendResponse (responseSection req) []-    _ -> sendResponse (responseNoBody notFound404 []) []+server req _aux sendResponse = sendResponse (responseSection req) []  responseSection :: S.Request -> S.Response responseSection req = responseBuilder ok200 header body
util/h3-client.hs view
@@ -8,8 +8,8 @@ import Control.Monad import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8+import Data.Maybe (fromMaybe) import Data.UnixTime-import Data.Word import Foreign.C.Types import qualified Network.HTTP3.Client as H3 import Network.TLS.QUIC@@ -18,6 +18,7 @@ import System.Exit import qualified System.Timeout as T import Text.Printf+import Text.Read (readMaybe)  import ClientX import Common@@ -41,7 +42,6 @@     , optInteractive :: Bool     , optMigration :: Maybe ConnectionControl     , optPacketSize :: Maybe Int-    , optPerformance :: Word64     , optNumOfReqs :: Int     }     deriving (Show)@@ -64,7 +64,6 @@         , optInteractive = False         , optMigration = Nothing         , optPacketSize = Nothing-        , optPerformance = 0         , optNumOfReqs = 1         } @@ -111,7 +110,7 @@     , Option         ['s']         ["packet-size"]-        (ReqArg (\n o -> o{optPacketSize = Just (read n)}) "<size>")+        (ReqArg (\n o -> o{optPacketSize = Just (fromMaybe 0 (readMaybe n))}) "<size>")         "specify QUIC packet size (UDP payload size)"     , Option         ['i']@@ -164,14 +163,9 @@         (NoArg (\o -> o{optMigration = Just ActiveMigration}))         "use a new address and a new server CID"     , Option-        ['t']-        ["performance"]-        (ReqArg (\n o -> o{optPerformance = read n}) "<size>")-        "measure performance"-    , Option         ['n']         ["number-of-requests"]-        (ReqArg (\n o -> o{optNumOfReqs = read n}) "<n>")+        (ReqArg (\n o -> o{optNumOfReqs = fromMaybe 0 (readMaybe n)}) "<n>")         "specify the number of requests"     ] @@ -197,14 +191,19 @@         h : p : [] -> return (h, p, ["/"])         h : p : ps -> return (h, p, C8.pack <$> ps)     cmvar <- newEmptyMVar-    let ccalpn ver-            | optPerformance /= 0 = return $ Just ["perf"]-            | otherwise =-                let (h3X, hqX) = makeProtos ver-                    protos-                        | optHQ = [hqX, h3X]-                        | otherwise = [h3X, hqX]-                 in return $ Just protos+    -- Bad numbers are left as 0 by the options, and refused here.  They+    -- used to stop the program with "Prelude.read: no parse", and a+    -- negative number of requests never ended.+    when (maybe False (<= 0) optPacketSize) $+        showUsageAndExit "packet size must be a positive integer"+    when (optNumOfReqs <= 0) $+        showUsageAndExit "number of requests must be a positive integer"+    let ccalpn ver =+            let (h3X, hqX) = makeProtos ver+                protos+                    | optHQ = [hqX, h3X]+                    | otherwise = [h3X, hqX]+             in return $ Just protos         gvers vers             | optVerNego = GreasingVersion : vers             | otherwise = vers
util/h3-server.hs view
@@ -5,6 +5,7 @@  module Main where +import qualified Control.Exception as E import Control.Monad import Data.ByteString (ByteString) import qualified Data.ByteString as BS@@ -15,6 +16,7 @@ import System.Environment (getArgs) import System.Exit import System.IO+import Text.Read (readMaybe)  import Common import Network.QUIC@@ -100,8 +102,6 @@         (_, _, errs) -> showUsageAndExit $ concat errs  chooseALPN :: Version -> [ByteString] -> IO ByteString-chooseALPN _ protos-    | "perf" `elem` protos = return "perf" chooseALPN ver protos = return $ case mh3idx of     Nothing -> case mhqidx of         Nothing -> ""@@ -120,11 +120,15 @@     args <- getArgs     (Options{..}, ips) <- serverOpts args     when (length ips < 2) $ showUsageAndExit "cannot recognize <addr> and <port>\n"-    let port = read (last ips)-        addrs = read <$> init ips-        aps = (,port) <$> addrs+    aps <- case (,) <$> mapM readMaybe (init ips) <*> readMaybe (last ips) of+        Nothing -> showUsageAndExit "cannot parse <addr> or <port>\n"+        Just (addrs, port) -> return $ (,port) <$> addrs     smgr <- SM.newSessionManager SM.defaultConfig-    Right cred@(!_cc, !_priv) <- credentialLoadX509 optCertFile optKeyFile+    let loadCred =+            credentialLoadX509 optCertFile optKeyFile+                `E.catch` \e -> return $ Left $ show (e :: E.IOException)+    cred@(!_cc, !_priv) <-+        loadCred >>= either (showUsageAndExit . ("cannot load credentials: " ++)) return     let sc0 = defaultServerConfig         sc =             sc0
util/qif-dec.hs view
@@ -4,6 +4,7 @@  module Main where +import Control.Monad (unless) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import Data.Conduit.Attoparsec@@ -123,9 +124,16 @@     ratio <- newIORef $ Ratio{ratioInst = 0, ratioHeader = 0}     withFile qfile ReadMode $ \h -> do         processBlock efile $ testSwitch dec insthdr ref h ratio+        waiting <- readIORef ref+        unless (Seq.null waiting) $ do+            putStrLn $ show (Seq.length waiting) ++ " blocks never decoded"+            exitFailure         r <- readIORef ratio-        let (x, y) = ((ratioInst r * 1000) `div` ratioHeader r) `divMod` 10-        putStrLn $ show x ++ "." ++ show y+        if ratioHeader r == 0+            then putStrLn "no blocks"+            else do+                let (x, y) = ((ratioInst r * 1000) `div` ratioHeader r) `divMod` 10+                putStrLn $ show x ++ "." ++ show y  testSwitch     :: (StreamId -> ByteString -> IO (Maybe [Header]))@@ -146,7 +154,7 @@         mhdr <- dec n bs         case mhdr of             Nothing -> modifyIORef' ref (blk <|)-            Just hdr -> compareHeaders hdr+            Just hdr -> compareHeaders n bs hdr   where     loop fifo = do         case viewr fifo of@@ -156,18 +164,20 @@                 case mhdr of                     Nothing -> writeIORef ref fifo                     Just hdr -> do-                        compareHeaders hdr+                        compareHeaders n1 bs1 hdr                         loop fifo'-    compareHeaders hdr = do+    -- The block's own number and octets.  The ones of the encoder stream+    -- block that let it be decoded used to be counted in their place.+    compareHeaders n0 bs0 hdr = do         hdr' <- fromCaseSensitive <$> headerlist h         if fromCaseSensitive hdr == hdr'             then modifyIORef' ratio $ \r ->                 r-                    { ratioInst = ratioInst r + BS.length bs+                    { ratioInst = ratioInst r + BS.length bs0                     , ratioHeader = ratioHeader r + sum (map headerSize hdr)                     }             else do-                putStrLn $ "---- Stream " ++ show n+                putStrLn $ "---- Stream " ++ show n0                 let hdrt = zip hdr hdr'                 mapM_ printDiff hdrt                 exitFailure
util/qif-enc.hs view
@@ -69,6 +69,9 @@                     putWord64be $ fromIntegral (sid :: Int)                     putWord32be $ fromIntegral $ BS.length bs             BS.appendFile efile $ header <> bs+    -- Started afresh: 'save' appends, and a second run used to follow the+    -- first in the same file.+    BS.writeFile efile BS.empty     enc <- newQEncoderS encConf (save 0) blocked immack debug     withFile qfile ReadMode $ qencode enc save