packages feed

http3 0.1.6 → 0.1.7

raw patch · 8 files changed

+156/−42 lines, 8 filesdep ~quicPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: quic

API changes (from Hackage documentation)

+ Network.QPACK.Internal: getMaxTableCapacity :: DynamicTable -> IO Int

Files

ChangeLog.md view
@@ -1,5 +1,21 @@ # Revision history for http3 +## 0.1.7++* Requiring quic v0.3.10, which hands the application a stream only once+  the frame that opened it has been checked.  A server used to answer on+  a stream whose first frame was past the flow control limit, just before+  the connection was closed over it.+* Closing the connection with H3_CLOSED_CRITICAL_STREAM when a write on+  one of our QPACK streams finds it closed, which the peer's STOP_SENDING+  does.  It used to throw `StreamIsClosed` into whichever thread wrote.+* QPACK: sending Set Dynamic Table Capacity only when the capacity+  changes.  `getMaxTableCapacity` is new.+* h3-server: allowing more unidirectional streams than the three a client+  needs.+* Tests: `h3ErrorSpec` takes how to read the number of requests served,+  so that h3spec can run it against a server elsewhere.+ ## 0.1.6  * Requiring quic v0.3.9 and http2 v5.4.6.  quic v0.3.7 stops opening a
Network/HTTP3/Control.hs view
@@ -7,6 +7,7 @@ ) where  import Control.Concurrent.MVar+import qualified Control.Exception as E import qualified Data.ByteString as BS import Data.IORef import Data.IntSet (IntSet)@@ -52,8 +53,21 @@     -- decoder stream, and an instruction must not be split by another one.     -- quic sends a write in two pieces when flow control stops it partway.     lockD <- newMVar ()-    return (sendStream sE, \bs -> withMVar lockD $ \_ -> sendStream sD bs)+    return+        ( critical $ sendStream sE+        , critical $ \bs -> withMVar lockD $ \_ -> sendStream sD bs+        )   where+    -- The sending part of one of our QPACK streams is closed only when the+    -- peer asked for that with STOP_SENDING, which it must not do (RFC+    -- 9204, section 4.2; RFC 9114, section 6.2.1): the closure of a+    -- critical stream is a connection error of type H3_CLOSED_CRITICAL_STREAM.+    -- The write used to throw StreamIsClosed into whichever thread made it,+    -- and nothing caught it there.+    critical send bs =+        send bs `E.catch` \e -> case e of+            StreamIsClosed -> abortConnection conn H3ClosedCriticalStream ""+            _ -> E.throwIO e     stC = mkType H3ControlStreams     stE = mkType QPACKEncoderStream     stD = mkType QPACKDecoderStream
Network/QPACK.hs view
@@ -174,9 +174,14 @@                     -- MaxEntries.                     setMaxEntries dyntbl n                     let tableSize = min ecMaxTableCapacity n-                    setTableCapacity dyntbl tableSize-                    ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False-                    sendIns dyntbl ins+                    current <- getMaxTableCapacity dyntbl+                    -- The capacity starts at zero (RFC 9204, section 3.2.3),+                    -- so an encoder that will not use the table has nothing+                    -- to tell the decoder.+                    when (tableSize /= current) $ do+                        setTableCapacity dyntbl tableSize+                        ins <- encodeEncoderInstructions [SetDynamicTableCapacity tableSize] False+                        sendIns dyntbl ins                 , setBlockedStreams = setMaxBlockedStreams dyntbl                 , setHeaderSize = setMaxHeaderSize dyntbl                 , getHeaderSize = getMaxHeaderSize dyntbl
Network/QPACK/Table/Dynamic.hs view
@@ -10,6 +10,7 @@     -- * Capacity     isTableReady,     getTableCapacity,+    getMaxTableCapacity,     setTableCapacity,     setDecoderTableCapacity,     getMaxNumOfEntries,@@ -233,6 +234,11 @@  getTableCapacity :: DynamicTable -> IO Int getTableCapacity DynamicTable{..} = readTVarIO tableSize++-- | The capacity, as last set.  'getTableCapacity' is how much of it the+--   entries take, whatever its name says.+getMaxTableCapacity :: DynamicTable -> IO Int+getMaxTableCapacity DynamicTable{..} = readIORef maxTableSize  setTableCapacity :: DynamicTable -> Int -> IO () setTableCapacity dyntbl@DynamicTable{..} maxsiz = do
http3.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               http3-version:            0.1.6+version:            0.1.7 license:            BSD-3-Clause license-file:       LICENSE maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>@@ -89,7 +89,7 @@         network-byte-order,         network-control >= 0.1.7 && <0.2,         psqueues,-        quic >= 0.3.9 && < 0.4,+        quic >= 0.3.10 && < 0.4,         sockaddr,         stm,         time-manager >= 0.2.3 && <0.4,
test/HTTP3/Error.hs view
@@ -16,6 +16,7 @@ import Network.HTTP.Types import qualified Network.HTTP3.Client as H3 import Network.HTTP3.Internal+import Network.QPACK (QDecoderConfig (..), QEncoderConfig (..)) import Network.QPACK.Internal import Network.QUIC import Network.QUIC.Client@@ -71,12 +72,20 @@         threadDelay 100000         return ret +-- | The error cases for an HTTP/3 server.+--+-- These also make h3spec, which runs them against a server somewhere else.+-- The last argument says how, from what the spec is run with, to read how+-- many requests the server has handed its application.  Only a server in the+-- same process can say; given 'Nothing', a test does not check that a+-- request the server refused never got that far. h3ErrorSpec     :: ClientConfig     -> H3.ClientConfig     -> Millisecond-    -> SpecWith (ThreadId, IORef Int)-h3ErrorSpec qcc cconf ms = do+    -> (a -> Maybe (IO Int))+    -> SpecWith a+h3ErrorSpec qcc cconf ms served = do     conf0 <- runIO H3.allocSimpleConfig     describe "HTTP/3 servers" $ do         it@@ -92,16 +101,12 @@                 `shouldThrow` applicationProtocolErrorsIn [H3MessageError]         it             "MUST send H3_MESSAGE_ERROR if mandatory pseudo-header fields are absent [HTTP/3 4.1.3]"-            $ \(_, served) -> do+            $ \x -> do                 let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader0                     qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")-                before' <- readIORef served-                runC qcc' cconf conf ms-                    `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-                -- And it must not have reached the application: the stream was-                -- reset and then the request handed over anyway.-                threadDelay 200000-                readIORef served `shouldReturn` before'+                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]"             $ \_ -> do@@ -132,14 +137,12 @@                     ++ C8.unpack (CI.original name)                     ++ " as malformed [HTTP/3 4.2]"                 )-                $ \(_, served) -> do+                $ \x -> do                     let req = H3.requestNoBody methodGet "/" [field]                         qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")-                    before' <- readIORef served-                    runCReq req qcc' cconf conf0 ms-                        `shouldThrow` applicationProtocolErrorsIn [H3MessageError]-                    threadDelay 200000-                    readIORef served `shouldReturn` before'+                    refusedNotServed (served x) $+                        runCReq req qcc' cconf conf0 ms+                            `shouldThrow` applicationProtocolErrorsIn [H3MessageError]         it             "MUST treat a request whose trailers carry connection as malformed [HTTP/3 4.2]"             $ \_ -> do@@ -245,45 +248,47 @@             "MUST send H3_STREAM_CREATION_ERROR if a second control stream is opened [HTTP/3 6.2.1]"             $ \_ -> do                 -- A stream type of 0x00, then an empty SETTINGS frame.-                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x00\x04\x00"-                runC qcc cconf conf ms-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+                opened <- newIORef False+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x00\x04\x00"+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]         it             "MUST send H3_STREAM_CREATION_ERROR if a client opens a push stream [HTTP/3 6.2.2]"             $ \_ -> do                 -- A stream type of 0x01, then push ID 0.-                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother "\x01\x00"-                runC qcc cconf conf ms-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+                opened <- newIORef False+                let conf = addHook conf0 $ setOnControlStreamCreated $ openAnother opened "\x01\x00"+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]         it             "MUST send H3_STREAM_CREATION_ERROR if a second encoder stream is opened [QPACK 4.2]"             $ \_ -> do-                let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother "\x02"-                runC qcc cconf conf ms-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+                opened <- newIORef False+                let conf = addHook conf0 $ setOnEncoderStreamCreated $ openAnother opened "\x02"+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]         it             "MUST send H3_STREAM_CREATION_ERROR if a second decoder stream is opened [QPACK 4.2]"             $ \_ -> do-                let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother "\x03"-                runC qcc cconf conf ms-                    `shouldThrow` applicationProtocolErrorsIn [H3StreamCreationError]+                opened <- newIORef False+                let conf = addHook conf0 $ setOnDecoderStreamCreated $ openAnother opened "\x03"+                expectIfOpened opened (runC qcc cconf conf ms) [H3StreamCreationError]         it             "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream is closed [QPACK 4.2]"             $ \_ -> do-                let conf = addHook conf0 $ setOnEncoderStreamCreated closeStream+                let conf =+                        noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated closeStream                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]         it             "MUST send H3_CLOSED_CRITICAL_STREAM if a decoder stream is closed [QPACK 4.2]"             $ \_ -> do-                let conf = addHook conf0 $ setOnDecoderStreamCreated closeStream+                let conf =+                        noDecoderTable $ addHook conf0 $ setOnDecoderStreamCreated closeStream                 runC qcc cconf conf ms                     `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]         it             "MUST send H3_CLOSED_CRITICAL_STREAM if an encoder stream ends inside an instruction [QPACK 4.2]"             $ \_ -> do                 -- The first octet of a Set Dynamic Table Capacity that goes on.-                let conf = addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do+                let conf = noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do                         sendStream strm "\x3f"                         closeStream strm                 runC qcc cconf conf ms@@ -297,6 +302,39 @@  ---------------------------------------------------------------- +-- | Making a request the server must refuse and, where we can see what the+-- server hands its application, checking that it handed over nothing: a+-- stream used to be reset and the request handed over anyway.+refusedNotServed :: Maybe (IO Int) -> IO () -> IO ()+refusedNotServed Nothing refused = refused+refusedNotServed (Just count) refused = do+    before' <- count+    refused+    threadDelay 200000+    count `shouldReturn` before'++-- | A client whose encoder will not use the dynamic table, and so has+-- nothing to write on its encoder stream.  For the tests that close that+-- stream: a client that went on writing on it would see the closure itself+-- and close the connection, and the test is about what the server does.+noEncoderTable :: H3.Config -> H3.Config+noEncoderTable conf =+    conf+        { H3.confQEncoderConfig =+            (H3.confQEncoderConfig conf){ecMaxTableCapacity = 0}+        }++-- | A client whose decoder offers no dynamic table, and so has nothing to+-- write on its decoder stream: no acknowledgements, since the server cannot+-- refer to a table, and no Stream Cancellations, which are not sent then.+-- For the test that closes that stream, as 'noEncoderTable'.+noDecoderTable :: H3.Config -> H3.Config+noDecoderTable conf =+    conf+        { H3.confQDecoderConfig =+            (H3.confQDecoderConfig conf){dcMaxTableCapacity = 0}+        }+ addHook :: H3.Config -> (H3.Hooks -> H3.Hooks) -> H3.Config addHook conf modify = conf'   where@@ -477,10 +515,36 @@  -- | Opening a unidirectional stream of our own next to the one given, and -- sending it these octets: a stream type and whatever follows it.-openAnother :: BS.ByteString -> Stream -> IO ()-openAnother bs strm = do-    strm' <- unidirectionalStream $ streamConnection strm-    sendStream strm' bs+--+-- A client needs three unidirectional streams, and a server may let it have+-- no more than that: quic's default does.  Then the stream cannot be opened+-- at all, and this gives up after a tenth of a second rather than waiting+-- for room that will not come, saying so in the 'IORef'.+openAnother :: IORef Bool -> BS.ByteString -> Stream -> IO ()+openAnother opened bs strm = do+    mstrm <- timeout 100000 $ unidirectionalStream $ streamConnection strm+    forM_ mstrm $ \strm' -> do+        sendStream strm' bs+        writeIORef opened True++-- | Expecting one of the errors when 'openAnother' could open its stream,+-- and leaving the test pending when it could not: against a server that+-- lets a client have only the streams it needs, a second control or QPACK+-- stream cannot be opened, and there is nothing to check.+expectIfOpened+    :: IORef Bool -> IO (Maybe ()) -> [ApplicationProtocolError] -> IO ()+expectIfOpened opened action errs = do+    r <- E.try action+    ok <- readIORef opened+    if not ok+        then+            pendingWith+                "the server allows no unidirectional stream beyond the three a client needs"+        else case r of+            Left e+                | applicationProtocolErrorsIn errs e -> return ()+                | otherwise -> expectationFailure $ "unexpected exception: " ++ show e+            Right _ -> expectationFailure "did not get expected exception"  -- A GOAWAY frame announcing 2^30 octets and then sending none of them. --
test/HTTP3/ErrorSpec.hs view
@@ -14,8 +14,9 @@ spec =     beforeAll start $         afterAll (teardown . fst) $-            h3ErrorSpec testClientConfig testH3ClientConfig 2000 -- 2 seconds+            h3ErrorSpec testClientConfig testH3ClientConfig 2000 served -- 2 seconds   where+    served (_, ref) = Just $ readIORef ref     start = do         ref <- newIORef 0         tid <- setup (countingServer ref) 4096
util/h3-server.hs view
@@ -138,6 +138,14 @@                 , scGroups = getGroups (scGroups sc0) optGroups                 , scQLog = optQLogDir                 , scCredentials = Credentials [cred]+                , -- Room for unidirectional streams beyond the three an HTTP/3+                  -- client needs.  quic's default is exactly three, and then+                  -- h3spec cannot open the second control or QPACK stream it+                  -- needs to see this server refuse one; those cases were+                  -- left pending.  RFC 9114, section 6.2, suggests allowing+                  -- more anyway, for extensions and reserved stream types.+                  scParameters =+                    (scParameters sc0){initialMaxStreamsUni = 10}                 }     run sc $ \conn -> do         info <- getConnectionInfo conn