h3spec 0.1.12 → 0.1.14
raw patch · 5 files changed
+462/−15 lines, 5 filesdep +case-insensitivedep ~http3dep ~quic
Dependencies added: case-insensitive
Dependency ranges changed: http3, quic
Files
- ChangeLog.md +28/−0
- HTTP3Error.hs +311/−9
- TransportError.hs +116/−0
- h3spec.cabal +5/−4
- h3spec.hs +2/−2
+ ChangeLog.md view
@@ -0,0 +1,28 @@+# Revision history for h3spec++## 0.1.14++* Using `http3` v0.1.7 and `quic` v0.3.13.+* 27 new test cases, 77 in all; none of the old ones are removed.+* QUIC: CRYPTO_BUFFER_EXCEEDED for CRYPTO data buffered beyond the limit;+ FRAME_ENCODING_ERROR for an ACK range below zero; PROTOCOL_VIOLATION+ for an ACK of a packet never sent, for STREAM_DATA_BLOCKED and+ DATA_BLOCKED in Handshake packets, and for a RETIRE_CONNECTION_ID never+ issued; TRANSPORT_PARAMETER_ERROR for a malformed parameter value and+ for initial_max_streams_bidi or initial_max_streams_uni over 2^60.+* HTTP/3 and QPACK: H3_STREAM_CREATION_ERROR for a second control,+ encoder or decoder stream and for a client's push stream;+ H3_CLOSED_CRITICAL_STREAM for a closed encoder or decoder stream and+ for an encoder stream ending inside an instruction;+ QPACK_DECOMPRESSION_FAILED for a reference to a dynamic table entry+ that is not there; a request with a connection-specific field treated+ as malformed, one case for each of connection, keep-alive,+ proxy-connection, transfer-encoding, upgrade and TE other than+ trailers, and TE with trailers accepted; a frame longer than+ SETTINGS_MAX_FIELD_SECTION_SIZE not buffered; H3_FRAME_ERROR for a+ SETTINGS frame ending mid-parameter; settings read past a reserved+ identifier, and a repeated identifier treated as an error.+* The cases that open a second control, encoder or decoder stream, or a+ push stream, are pending against a server that lets a client have only+ the three unidirectional streams it needs, since such a stream cannot+ be opened there at all.
HTTP3Error.hs view
@@ -3,16 +3,21 @@ module HTTP3Error ( h3ErrorSpec,+ h3DrainSpec, ) where import Control.Concurrent import qualified Control.Exception as E+import Control.Monad (forM_) import Data.ByteString () import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8+import qualified Data.CaseInsensitive as CI+import Data.IORef import Network.HTTP.Types import qualified Network.HTTP3.Client as H3 import Network.HTTP3.Internal+import Network.QPACK (QDecoderConfig (..), QEncoderConfig (..)) import Network.QPACK.Internal import Network.QUIC import Network.QUIC.Client@@ -46,8 +51,42 @@ threadDelay 100000 return ret -h3ErrorSpec :: ClientConfig -> H3.ClientConfig -> Millisecond -> SpecWith a-h3ErrorSpec qcc cconf ms = do+-- | Like 'runC', but sending a request of our own choosing.+runCReq+ :: H3.Request+ -> ClientConfig+ -> H3.ClientConfig+ -> H3.Config+ -> Millisecond+ -> IO (Maybe ())+runCReq 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 ""+ _ -> H3.run conn cconf conf client+ where+ us = ms * 1000+ client sendRequest _aux = do+ ret <- sendRequest req $ \_rsp -> return ()+ 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+ -> (a -> Maybe (IO Int))+ -> SpecWith a+h3ErrorSpec qcc cconf ms served = do conf0 <- runIO H3.allocSimpleConfig describe "HTTP/3 servers" $ do it@@ -63,11 +102,12 @@ `shouldThrow` applicationProtocolErrorsIn [H3MessageError] it "MUST send H3_MESSAGE_ERROR if mandatory pseudo-header fields are absent [HTTP/3 4.1.3]"- $ \_ -> do+ $ \x -> do let conf = addHook conf0 $ setOnHeadersFrameCreated illegalHeader0 qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")- runC qcc' cconf conf ms- `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+ 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@@ -82,6 +122,21 @@ qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "") runC qcc' cconf conf ms `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+ forM_ connectionSpecific $ \field@(name, _) ->+ it+ ( "MUST treat a request with "+ ++ C8.unpack (CI.original name)+ ++ " as malformed [HTTP/3 4.2]"+ )+ $ \x -> do+ let req = H3.requestNoBody methodGet "/" [field]+ qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+ refusedNotServed (served x) $+ runCReq req qcc' cconf conf0 ms+ `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+ it "MUST accept TE with trailers [HTTP/3 4.2]" $ \_ -> do+ let req = H3.requestNoBody methodGet "/" [("te", "trailers")]+ runCReq req qcc cconf conf0 ms `shouldReturn` Just () it "MUST send H3_MISSING_SETTINGS if the first control frame is not SETTINGS [HTTP/3 6.2.1]" $ \_ -> do@@ -118,6 +173,24 @@ runC qcc cconf conf ms `shouldThrow` applicationProtocolErrorsIn [H3SettingsError] it+ "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+ "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]"+ $ \_ -> 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]" $ \_ -> do let conf = addHook conf0 $ setOnHeadersFrameCreated requestCancelPush@@ -130,6 +203,18 @@ 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]"+ $ \_ -> 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]"+ $ \_ -> 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]" $ \_ -> do let conf = addHook conf0 $ setOnEncoderStreamCreated largeTableCapacity@@ -142,6 +227,55 @@ runC qcc cconf conf ms `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream] it+ "MUST send H3_STREAM_CREATION_ERROR if a second control stream is opened [HTTP/3 6.2.1]"+ $ \_ -> do+ -- A stream type of 0x00, then an empty SETTINGS frame.+ 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.+ 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+ 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+ 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 =+ 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 =+ 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 = noEncoderTable $ addHook conf0 $ setOnEncoderStreamCreated $ \strm -> do+ sendStream strm "\x3f"+ closeStream strm+ runC qcc cconf conf ms+ `shouldThrow` applicationProtocolErrorsIn [H3ClosedCriticalStream]+ it "MUST send QPACK_DECODER_STREAM_ERROR if Insert Count Increment is 0 [QPACK 4.4.3]" $ \_ -> do let conf = addHook conf0 $ setOnDecoderStreamCreated zeroInsertCountIncrement@@ -150,6 +284,75 @@ ---------------------------------------------------------------- +-- | 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}+ }++-- | The error cases that need the server to read a request's body.+--+-- A server checks content-length, and the fields of trailers, only as its+-- application reads the body; an application that answers without reading+-- it never gets to either, and nothing tells from outside which paths read+-- one. These go to /drain, where our test server reads the body, and so+-- are not part of 'h3ErrorSpec': run against a server elsewhere, as h3spec+-- does, they would fail whatever that server does.+h3DrainSpec+ :: ClientConfig+ -> H3.ClientConfig+ -> Millisecond+ -> SpecWith a+h3DrainSpec qcc cconf ms = do+ conf0 <- runIO H3.allocSimpleConfig+ describe "HTTP/3 servers reading a request's body" $ do+ it+ "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 "")+ 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+ -- /drain reads the body, and so the trailers after it.+ let req0 = H3.requestBuilder methodPost "/drain" [] "hello"+ req = H3.setRequestTrailersMaker req0 connectionTrailer+ qcc' = addQUICHook qcc $ setOnResetStreamReceived $ \_strm aerr -> E.throwIO (ApplicationProtocolErrorIsReceived aerr "")+ runCReq req qcc' cconf conf0 ms+ `shouldThrow` applicationProtocolErrorsIn [H3MessageError]+ addHook :: H3.Config -> (H3.Hooks -> H3.Hooks) -> H3.Config addHook conf modify = conf' where@@ -201,7 +404,7 @@ -- [(":method","GET") -- ,(":scheme","https")--- ,(":autority","127.0.0.1")+-- ,(":authority","127.0.0.1") -- ,(":path","/") -- ,(":foo","bar") -- the presence of prohibited fields or pseudo-header fields, -- ]@@ -209,12 +412,12 @@ illegalHeader1 _ = [ H3Frame H3FrameHeaders- "\x00\x00\xd1\xd7\x27\x02\x3a\x61\x75\x74\x6f\x72\x69\x74\x79\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x24\x3a\x66\x6f\x6f\x03\x62\x61\x72"+ "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x24\x3a\x66\x6f\x6f\x03\x62\x61\x72" ] -- [(":method","GET") -- ,(":scheme","https")--- ,(":autority","127.0.0.1")+-- ,(":authority","127.0.0.1") -- ,("foo","bar") -- ,(":path","/") -- pseudo-header fields after fields -- ]@@ -222,7 +425,7 @@ illegalHeader2 _ = [ H3Frame H3FrameHeaders- "\x00\x00\xd1\xd7\x27\x02\x3a\x61\x75\x74\x6f\x72\x69\x74\x79\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x23\x66\x6f\x6f\x03\x62\x61\x72\xc1"+ "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x23\x66\x6f\x6f\x03\x62\x61\x72\xc1" ] -- [(":method","GET")@@ -248,6 +451,21 @@ "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\xff\x24" ] +-- [(":method","GET")+-- ,(":scheme","https")+-- ,(":authority","127.0.0.1")+-- ,(":path","/")] ++ dynamic index 0+--+-- The Required Insert Count in the prefix is 0 and nothing has been inserted,+-- so there is no entry 0 to name. The decoder used to fold the index back+-- into the table and hand over whatever slot it landed on.+illegalHeader5 :: [H3Frame] -> [H3Frame]+illegalHeader5 _ =+ [ H3Frame+ H3FrameHeaders+ "\x00\x00\xd1\xd7\x50\x09\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\xc1\x80"+ ]+ {- -- [(SettingsQpackBlockedStreams,100) -- ,(SettingsQpackMaxTableCapacity,4096)@@ -261,6 +479,32 @@ -- ,(H3SettingsKey 0x2,200) -- HTTP/2 Settings -- ,(SettingsQpackMaxTableCapacity,4096) -- ,(SettingsMaxFieldSectionSize,32768)]+-- An identifier with no value behind it.+--+-- Each parameter is a pair of variable-length integers; this frame stops+-- between them, which section 7.1 makes H3_FRAME_ERROR. Reading off the end+-- used to raise BufferOverrun, which nothing here catches, so the peer was+-- told nothing at all.+truncatedSettings :: [H3Frame] -> [H3Frame]+truncatedSettings _ = [H3Frame H3FrameSettings "\x01"]++-- [(H3SettingsKey 0x21,0) -- reserved, to be ignored+-- ,(H3SettingsKey 0x2,0)] -- HTTP/2 Settings, which must be refused+--+-- 0x21 is the first of the identifiers section 7.2.4.1 reserves and tells+-- endpoints they SHOULD send. Reading used to stop at it, so the HTTP/2+-- setting behind it went unseen -- along with anything else a peer put there.+greaseThenHttp2Setting :: [H3Frame] -> [H3Frame]+greaseThenHttp2Setting _ = [H3Frame H3FrameSettings "\x21\x00\x02\x00"]++-- [(H3SettingsKey 0x40,0)+-- ,(H3SettingsKey 0x40,0)] -- the same identifier twice+--+-- 64 in the two-octet form. The duplicate check was bits in an Int, so+-- identifiers this large repeated unnoticed.+duplicateLargeSetting :: [H3Frame] -> [H3Frame]+duplicateLargeSetting _ = [H3Frame H3FrameSettings "\x40\x40\x00\x40\x40\x00"]+ illegalSettings1 :: [H3Frame] -> [H3Frame] illegalSettings1 _ = [ H3Frame@@ -269,6 +513,64 @@ ] ----------------------------------------------------------------++-- | The fields RFC 9114, section 4.2, names as connection-specific, and TE+-- with a value other than "trailers".+connectionSpecific :: [(HeaderName, BS.ByteString)]+connectionSpecific =+ [ ("connection", "close")+ , ("keep-alive", "timeout=5")+ , ("proxy-connection", "keep-alive")+ , ("transfer-encoding", "chunked")+ , ("upgrade", "websocket")+ , ("te", "gzip")+ ]++-- | Trailers of a single Connection field.+connectionTrailer :: H3.TrailersMaker+connectionTrailer Nothing = return $ H3.Trailers [("connection", "close")]+connectionTrailer (Just _) = return $ H3.NextTrailersMaker connectionTrailer++-- | Opening a unidirectional stream of our own next to the one given, and+-- sending it these octets: a stream type and whatever follows it.+--+-- 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.+--+-- A frame length is a variable-length integer, so a peer can claim up to+-- 2^62-1 and have the other end hold whatever it sends towards that. Nothing+-- has to arrive for the claim to be refused.+overLongFrame :: Stream -> IO ()+overLongFrame strm = sendStream strm "\x07\xc0\x00\x00\x00\x40\x00\x00\x00" -- SetDynamicTableCapacity 10000000000 largeTableCapacity :: Stream -> IO ()
TransportError.hs view
@@ -75,6 +75,11 @@ let cc = addHook cc0 $ setOnTransportParametersCreated setStatelessResetToken runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError] it+ "MUST send TRANSPORT_PARAMETER_ERROR if a parameter value is malformed [Transport 18]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnTLSExtensionCreated danglingParameter+ runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError]+ it "MUST send TRANSPORT_PARAMETER_ERROR if max_udp_payload_size < 1200 [Transport 7.4 and 18.2]" $ \_ -> do let cc = addHook cc0 $ setOnTransportParametersCreated setMaxUdpPayloadSize@@ -90,6 +95,26 @@ let cc = addHook cc0 $ setOnTransportParametersCreated setMaxAckDelay runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError] it+ "MUST send TRANSPORT_PARAMETER_ERROR if initial_max_streams_bidi > 2^60 [Transport 18.2]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnTransportParametersCreated setMaxStreamsBidi+ runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError]+ it+ "MUST send TRANSPORT_PARAMETER_ERROR if initial_max_streams_uni > 2^60 [Transport 18.2]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnTransportParametersCreated setMaxStreamsUni+ runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError]+ it+ "MUST send FRAME_ENCODING_ERROR if an ACK range reaches below zero [Transport 19.3.1]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated impossibleAckRange+ runCnoOp cc ms `shouldThrow` transportErrorsIn [FrameEncodingError]+ it+ "SHOULD send PROTOCOL_VIOLATION on an ACK for a packet never sent [Transport 13.1]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated ackForUnsentPacket+ runCnoOp cc ms `shouldThrow` transportErrorsIn [ProtocolViolation]+ it "MUST send FRAME_ENCODING_ERROR if a frame of unknown type is received [Transport 12.4]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated unknownFrame@@ -103,11 +128,31 @@ let cc = addHook cc0 $ setOnPlainCreated $ rrBits HandshakeLevel runCnoOp cc ms `shouldThrow` transportError it+ "MUST send CRYPTO_BUFFER_EXCEEDED if CRYPTO data is buffered beyond the limit [Transport 7.5]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated cryptoBeyondBuffer+ runCnoOp cc ms `shouldThrow` transportErrorsIn [CryptoBufferExceeded]+ it "MUST send PROTOCOL_VIOLATION if PATH_CHALLENGE in Handshake is received [Transport 17.2.4]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated handshakePathChallenge runCnoOp cc ms `shouldThrow` transportError it+ "MUST send PROTOCOL_VIOLATION if RETIRE_CONNECTION_ID for a sequence number never issued [Transport 19.16]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated retireUnissued+ runCnoOp cc ms `shouldThrow` transportError+ it+ "MUST send PROTOCOL_VIOLATION if STREAM_DATA_BLOCKED in Handshake is received [Transport 12.4]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated handshakeStreamDataBlocked+ runCnoOp cc ms `shouldThrow` transportError+ it+ "MUST send PROTOCOL_VIOLATION if DATA_BLOCKED in Handshake is received [Transport 12.4]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated handshakeDataBlocked+ runCnoOp cc ms `shouldThrow` transportError+ it "MUST send PROTOCOL_VIOLATION if reserved bits in Short are non-zero [Transport 17.2]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated $ rrBits RTT1Level@@ -233,6 +278,14 @@ setOnTransportParametersCreated :: (Parameters -> Parameters) -> Hooks -> Hooks setOnTransportParametersCreated f hooks = hooks{onTransportParametersCreated = f} +-- initial_max_data announcing a zero-length value. Everything the peer+-- really sent is left in front of it, so this is the value alone being wrong+-- rather than the list being cut short. The value of an integer parameter is+-- one variable-length integer, and there is no such thing in no octets.+danglingParameter :: [ExtensionRaw] -> [ExtensionRaw]+danglingParameter [ExtensionRaw eid v] = [ExtensionRaw eid (v <> "\x04\x00")]+danglingParameter xs = xs+ setOnTLSExtensionCreated :: ([ExtensionRaw] -> [ExtensionRaw]) -> Hooks -> Hooks setOnTLSExtensionCreated f params = params{onTLSExtensionCreated = f} @@ -292,8 +345,21 @@ setMaxAckDelay :: Parameters -> Parameters setMaxAckDelay params = params{maxAckDelay = 2 ^ (15 :: Int)} +-- A stream id has 62 bits, two of them saying who opened it and whether it is+-- bidirectional, so a count past 2^60 names no stream.+setMaxStreamsBidi :: Parameters -> Parameters+setMaxStreamsBidi params = params{initialMaxStreamsBidi = 2 ^ (60 :: Int) + 1}++setMaxStreamsUni :: Parameters -> Parameters+setMaxStreamsUni params = params{initialMaxStreamsUni = 2 ^ (60 :: Int) + 1}+ ---------------------------------------------------------------- +-- Stream 0, which the client has not opened. The server must answer the+-- offset with FLOW_CONTROL_ERROR and nothing else: anything it sends back on+-- stream 0 is, to a client that never opened it, a STREAM_STATE_ERROR+-- (RFC 9000 Sec 19.8), and the client closes the connection before the error+-- we are waiting for arrives. largeOffset :: EncryptionLevel -> Plain -> Plain largeOffset lvl plain | lvl == RTT1Level = plain{plainFrames = fake : plainFrames plain}@@ -308,16 +374,66 @@ where fake = StreamF 1000000000 0 ["GET /\r\n"] True +-- Largest acknowledged 5, then a gap of 10: the next range would start at+-- 5 - 10 - 2, which is not a packet number.+impossibleAckRange :: EncryptionLevel -> Plain -> Plain+impossibleAckRange lvl plain+ | lvl == RTT1Level =+ plain{plainFrames = Ack (AckInfo 5 0 [(10, 0)]) 0 : plainFrames plain}+ | otherwise = plain++-- Nobody has sent a million packets down this connection.+ackForUnsentPacket :: EncryptionLevel -> Plain -> Plain+ackForUnsentPacket lvl plain+ | lvl == RTT1Level =+ plain{plainFrames = Ack (AckInfo 1000000 0 []) 0 : plainFrames plain}+ | otherwise = plain+ unknownFrame :: EncryptionLevel -> Plain -> Plain unknownFrame lvl plain | lvl == RTT1Level = plain{plainFrames = UnknownFrame 0x20 : plainFrames plain} | otherwise = plain +-- CRYPTO frames are outside flow control, so nothing but the buffer limit+-- stops a peer from parking a fragment far past where the stream has got to+-- and having it held. One octet at this offset is enough to ask for more+-- than any bound the receiver could sensibly hold.+cryptoBeyondBuffer :: EncryptionLevel -> Plain -> Plain+cryptoBeyondBuffer lvl plain+ | lvl == HandshakeLevel =+ plain{plainFrames = CryptoF 100000000 "x" : plainFrames plain}+ | otherwise = plain+ handshakePathChallenge :: EncryptionLevel -> Plain -> Plain handshakePathChallenge lvl plain | lvl == HandshakeLevel = plain{plainFrames = PathChallenge (PathData "01234567") : plainFrames plain}+ | otherwise = plain++-- RFC 9000 Sec 19.16: "Receipt of a RETIRE_CONNECTION_ID frame containing a+-- sequence number greater than any previously sent to the peer MUST be+-- treated as a connection error of type PROTOCOL_VIOLATION." No endpoint has+-- given out a hundred thousand connection IDs.+retireUnissued :: EncryptionLevel -> Plain -> Plain+retireUnissued lvl plain+ | lvl == RTT1Level =+ plain{plainFrames = RetireConnectionID 100000 : plainFrames plain}+ | otherwise = plain++-- RFC 9000 Table 3 has STREAM_DATA_BLOCKED and DATA_BLOCKED in 0-RTT and+-- 1-RTT packets only, and Sec 12.4 makes a frame in a packet that may not+-- carry it a connection error of type PROTOCOL_VIOLATION.+handshakeStreamDataBlocked :: EncryptionLevel -> Plain -> Plain+handshakeStreamDataBlocked lvl plain+ | lvl == HandshakeLevel =+ plain{plainFrames = StreamDataBlocked 0 0 : plainFrames plain}+ | otherwise = plain++handshakeDataBlocked :: EncryptionLevel -> Plain -> Plain+handshakeDataBlocked lvl plain+ | lvl == HandshakeLevel =+ plain{plainFrames = DataBlocked 0 : plainFrames plain} | otherwise = plain noFrames :: EncryptionLevel -> Plain -> Plain
h3spec.cabal view
@@ -1,5 +1,5 @@ name: h3spec-version: 0.1.12+version: 0.1.14 synopsis: QUIC description: Test tool for error cases of QUIC and HTTP/3 license: BSD3@@ -9,7 +9,7 @@ -- copyright: category: Web build-type: Simple--- extra-source-files: ChangeLog.md+extra-source-files: ChangeLog.md cabal-version: >= 1.10 executable h3spec@@ -22,11 +22,12 @@ ghc-options: -Wall -threaded -rtsopts build-depends: base >= 4.9 && < 5 , bytestring+ , case-insensitive , hspec , hspec-core , http-types- , http3 >= 0.0.10+ , http3 >= 0.1.7 && < 0.2 , network- , quic >= 0.2 && < 0.3+ , quic >= 0.3.13 && < 0.4 , tls default-extensions: Strict StrictData
h3spec.hs view
@@ -124,10 +124,10 @@ | null (optSkip opts) = qcArgs0 | otherwise = "--skip" : (intersperse "--skip" $ reverse $ optSkip opts)- h3cc = H3.ClientConfig "https" host+ h3cc = H3.defaultClientConfig{H3.authority = host} ms = optTimeout opts H.readConfig H.defaultConfig qcArgs- >>= withArgs [] . H.runSpec (transportErrorSpec cc ms >> h3ErrorSpec cc h3cc ms)+ >>= withArgs [] . H.runSpec (transportErrorSpec cc ms >> h3ErrorSpec cc h3cc ms (const Nothing)) >>= H.evaluateSummary getLogger :: Maybe FilePath -> (String -> IO ())