h3spec 0.1.14 → 0.1.15
raw patch · 5 files changed
+292/−102 lines, 5 filesdep −networkdep ~http3dep ~quicdep ~tls
Dependencies removed: network
Dependency ranges changed: http3, quic, tls
Files
- ChangeLog.md +17/−0
- HTTP3Error.hs +219/−78
- TransportError.hs +36/−12
- h3spec.cabal +4/−5
- h3spec.hs +16/−7
ChangeLog.md view
@@ -1,5 +1,22 @@ # Revision history for h3spec +## 0.1.15++These changes were made on main without PRs.++* Using `http3` v0.1.10 and `quic` v0.3.20.+* 9 new test cases, 86 in all. HTTP/3 and QPACK: H3_MESSAGE_ERROR for a+ duplicated pseudo-header; H3_FRAME_UNEXPECTED for PUSH_PROMISE;+ H3_ID_ERROR for a CANCEL_PUSH push ID not allowed, a MAX_PUSH_ID going+ down and a GOAWAY going up; H3_FRAME_ERROR for octets after MAX_PUSH_ID's+ push ID; a frame of unknown type ignored however long;+ QPACK_DECOMPRESSION_FAILED for a field line cut short and for a badly+ padded Huffman string.+* Test names and RFC section references corrected.+* `-m` works together with `-s`; an invalid `--timeout` is refused.+* HTTP/3 cases are pending on an HQ connection, and the 0-RTT case is+ pending against a server that does not accept 0-RTT.+ ## 0.1.14 * Using `http3` v0.1.7 and `quic` v0.3.13.
HTTP3Error.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeApplications #-} module HTTP3Error ( 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
TransportError.hs view
@@ -45,7 +45,8 @@ $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated largeOffset runCnoOp cc ms `shouldThrow` transportErrorsIn [FlowControlError]- it "MUST send STREAM_LIMIT_ERROR if a stream ID exceeding the limit" $ \_ -> do+ it+ "MUST send STREAM_LIMIT_ERROR if a stream ID exceeds the limit [Transport 4.6]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated largeStreamId runCnoOp cc ms `shouldThrow` transportErrorsIn [StreamLimitError] it@@ -60,7 +61,7 @@ addHook cc0 $ setOnTransportParametersCreated setOriginalDestinationConnectionId runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError] it- "MUST send TRANSPORT_PARAMETER_ERROR if preferred_address, is received [Transport 18.2]"+ "MUST send TRANSPORT_PARAMETER_ERROR if preferred_address is received [Transport 18.2]" $ \_ -> do let cc = addHook cc0 $ setOnTransportParametersCreated setPreferredAddress runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError]@@ -85,7 +86,7 @@ let cc = addHook cc0 $ setOnTransportParametersCreated setMaxUdpPayloadSize runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError] it- "MUST send TRANSPORT_PARAMETER_ERROR if ack_delay_exponen > 20 [Transport 7.4 and 18.2]"+ "MUST send TRANSPORT_PARAMETER_ERROR if ack_delay_exponent > 20 [Transport 7.4 and 18.2]" $ \_ -> do let cc = addHook cc0 $ setOnTransportParametersCreated setAckDelayExponent runCnoOp cc ms `shouldThrow` transportErrorsIn [TransportParameterError]@@ -119,6 +120,13 @@ $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated unknownFrame runCnoOp cc ms `shouldThrow` transportErrorsIn [FrameEncodingError]+ it+ "MUST send FRAME_ENCODING_ERROR if a frame is cut short [Transport 12.4]"+ $ \_ -> do+ let cc = addHook cc0 $ setOnPlainCreated truncatedFrame+ -- Not the generic codes 'transportErrorsIn' lets through:+ -- INTERNAL_ERROR is what the decoder's exception used to give.+ runCnoOp cc ms `shouldThrow` frameEncodingError it "MUST send PROTOCOL_VIOLATION on no frames [Transport 12.4]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated noFrames runCnoOp cc ms `shouldThrow` transportError@@ -133,7 +141,7 @@ 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]"+ "MUST send PROTOCOL_VIOLATION if PATH_CHALLENGE in Handshake is received [Transport 12.4]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated handshakePathChallenge runCnoOp cc ms `shouldThrow` transportError@@ -153,7 +161,7 @@ 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]"+ "MUST send PROTOCOL_VIOLATION if reserved bits in Short are non-zero [Transport 17.3.1]" $ \_ -> do let cc = addHook cc0 $ setOnPlainCreated $ rrBits RTT1Level runCnoOp cc ms `shouldThrow` transportError@@ -232,15 +240,15 @@ let cc = cc0{ccALPN = \_ -> return $ Just ["dummy"]} runCnoOp cc ms `shouldThrow` cryptoErrorsIn [TLS.NoApplicationProtocol] it- "MUST send missing_extension TLS alert if the quic_transport_parameters extension does not included [TLS 8.2]"+ "MUST send missing_extension TLS alert if the quic_transport_parameters extension is not included [TLS 8.2]" $ \_ -> do let cc = addHook cc0 $ setOnTLSExtensionCreated (const []) runCnoOp cc ms `shouldThrow` cryptoErrorsIn [TLS.MissingExtension] it- "MUST send missing_extension TLS alert if the quic_transport_parameters extension does not included [TLS 8.2]"+ "MUST send missing_extension TLS alert if only the draft quic_transport_parameters extension (0xffa5) is included [TLS 8.2]" $ \_ -> do let f [ExtensionRaw _ v] = [ExtensionRaw (ExtensionID 0xffa5) v]- f _ = error "f"+ f xs = xs cc = addHook cc0 $ setOnTLSExtensionCreated f runCnoOp cc ms `shouldThrow` cryptoErrorsIn [TLS.MissingExtension] it@@ -261,9 +269,8 @@ } runCnoOp cc ms `shouldThrow` transportError _ -> do- putStrLn- "Warning: 0-RTT is not possible. Skipping this test. Use \"h3spec -s 0-RTT\" next time." when (ccDebugLog cc0) $ print mres+ pendingWith "0-RTT is not possible. Use \"h3spec -s 0-RTT\" next time." ---------------------------------------------------------------- @@ -379,14 +386,14 @@ impossibleAckRange :: EncryptionLevel -> Plain -> Plain impossibleAckRange lvl plain | lvl == RTT1Level =- plain{plainFrames = Ack (AckInfo 5 0 [(10, 0)]) 0 : plainFrames plain}+ plain{plainFrames = Ack (AckInfo 5 0 [(10, 0)]) noDelay : 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}+ plain{plainFrames = Ack (AckInfo 1000000 0 []) noDelay : plainFrames plain} | otherwise = plain unknownFrame :: EncryptionLevel -> Plain -> Plain@@ -395,6 +402,19 @@ plain{plainFrames = UnknownFrame 0x20 : plainFrames plain} | otherwise = plain +-- A CRYPTO frame at offset 0 whose length is an eight-octet integer that+-- starts 0x3f: whatever follows, it runs past the end of the packet. Put+-- together an octet at a time, since no frame this library builds is cut+-- short.+truncatedFrame :: EncryptionLevel -> Plain -> Plain+truncatedFrame lvl plain+ | lvl == RTT1Level =+ plain+ { plainFrames =+ plainFrames plain ++ [UnknownFrame 0x06, UnknownFrame 0x00, UnknownFrame 0xff]+ }+ | 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@@ -562,6 +582,10 @@ -- it detects an error condition; a generic error code (such as -- PROTOCOL_VIOLATION or INTERNAL_ERROR) can always be used in place -- of specific error codes.+frameEncodingError :: QUICException -> Bool+frameEncodingError (TransportErrorIsReceived FrameEncodingError _) = True+frameEncodingError _ = False+ transportErrorsIn :: [TransportError] -> QUICException -> Bool transportErrorsIn tes qe@(TransportErrorIsReceived te _) = (te `elem` tes) || transportError qe transportErrorsIn _ _ = False
h3spec.cabal view
@@ -1,5 +1,5 @@ name: h3spec-version: 0.1.14+version: 0.1.15 synopsis: QUIC description: Test tool for error cases of QUIC and HTTP/3 license: BSD3@@ -26,8 +26,7 @@ , hspec , hspec-core , http-types- , http3 >= 0.1.7 && < 0.2- , network- , quic >= 0.3.13 && < 0.4- , tls+ , http3 >= 0.1.10 && < 0.2+ , quic >= 0.3.20 && < 0.4+ , tls >= 2.3 && < 2.6 default-extensions: Strict StrictData
h3spec.hs view
@@ -3,7 +3,8 @@ module Main where import Control.Monad (when)-import Data.List (foldl', intersperse)+import qualified Data.List as L+import Data.Maybe (fromMaybe) import Data.Version (showVersion) import qualified Network.HTTP3.Client as H3 import Network.QUIC.Internal@@ -11,6 +12,7 @@ import System.Environment (getArgs, withArgs) import System.Exit (exitFailure, exitSuccess) import qualified Test.Hspec.Core.Runner as H+import Text.Read (readMaybe) import HTTP3Error import qualified Paths_h3spec as P@@ -38,7 +40,7 @@ , optSkip = [] , optQLogDir = Nothing , optKeyLogFile = Nothing- , optTimeout = 2000 -- 2 milliseconds+ , optTimeout = 2000 -- 2 seconds } options :: [OptDescr (Options -> Options)]@@ -76,7 +78,7 @@ , Option ['t'] ["timeout"]- (ReqArg (\ms o -> o{optTimeout = read ms}) "<milliseconds>")+ (ReqArg setTimeout "<milliseconds>") "timeout for each test case (2000)" , Option ['n']@@ -85,6 +87,10 @@ "no validating server certificates" ] +-- An invalid value is reported by main after getOpt.+setTimeout :: String -> Options -> Options+setTimeout ms o = o{optTimeout = fromMaybe (-1) $ readMaybe ms}+ showUsageAndExit :: String -> IO a showUsageAndExit msg = do putStrLn msg@@ -98,8 +104,10 @@ main = do args0 <- getArgs (opts, args) <- case getOpt Permute options args0 of- (o, n, []) -> return (foldl' (flip id) defaultOptions o, n)+ (o, n, []) -> return (L.foldl' (flip id) defaultOptions o, n) (_, _, errs) -> showUsageAndExit $ concat errs+ when (optTimeout opts <= 0) $+ showUsageAndExit "timeout must be a positive integer\n" when (optVersion opts) $ do putStrLn $ "h3spec " ++ showVersion P.version exitSuccess@@ -119,15 +127,16 @@ qcArgs0 | null (optMatch opts) = [] | otherwise =- "--match" : (intersperse "--match" $ reverse $ optMatch opts)+ "--match" : (L.intersperse "--match" $ reverse $ optMatch opts) qcArgs | null (optSkip opts) = qcArgs0 | otherwise =- "--skip" : (intersperse "--skip" $ reverse $ optSkip opts)+ qcArgs0 ++ "--skip" : (L.intersperse "--skip" $ reverse $ optSkip opts) h3cc = H3.defaultClientConfig{H3.authority = host} ms = optTimeout opts H.readConfig H.defaultConfig qcArgs- >>= withArgs [] . H.runSpec (transportErrorSpec cc ms >> h3ErrorSpec cc h3cc ms (const Nothing))+ >>= withArgs []+ . H.runSpec (transportErrorSpec cc ms >> h3ErrorSpec cc h3cc ms (const Nothing)) >>= H.evaluateSummary getLogger :: Maybe FilePath -> (String -> IO ())