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 +16/−0
- Network/HTTP3/Control.hs +15/−1
- Network/QPACK.hs +8/−3
- Network/QPACK/Table/Dynamic.hs +6/−0
- http3.cabal +2/−2
- test/HTTP3/Error.hs +99/−35
- test/HTTP3/ErrorSpec.hs +2/−1
- util/h3-server.hs +8/−0
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