packages feed

http2 5.4.12 → 5.4.13

raw patch · 13 files changed

+646/−77 lines, 13 filesdep ~networkdep ~stmPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: network, stm

API changes (from Hackage documentation)

+ Network.HTTP2.Client: confReadNWith :: Config -> Maybe (STM () -> Int -> IO ByteString)
+ Network.HTTP2.Client: confSendAllWith :: Config -> Maybe (STM () -> ByteString -> IO (Either () ()))
+ Network.HTTP2.Client.Internal: [confReadNWith] :: Config -> Maybe (STM () -> Int -> IO ByteString)
+ Network.HTTP2.Client.Internal: [confSendAllWith] :: Config -> Maybe (STM () -> ByteString -> IO (Either () ()))
+ Network.HTTP2.Server: confReadNWith :: Config -> Maybe (STM () -> Int -> IO ByteString)
+ Network.HTTP2.Server: confSendAllWith :: Config -> Maybe (STM () -> ByteString -> IO (Either () ()))
+ Network.HTTP2.Server: runWith :: STM () -> ServerConfig -> Config -> Server -> IO ()
+ Network.HTTP2.Server.Internal: [confReadNWith] :: Config -> Maybe (STM () -> Int -> IO ByteString)
+ Network.HTTP2.Server.Internal: [confSendAllWith] :: Config -> Maybe (STM () -> ByteString -> IO (Either () ()))
+ Network.HTTP2.Server.Internal: runIOWith :: STM () -> ServerConfig -> Config -> (ServerIO Stream -> IO (IO ())) -> IO ()
- Network.HTTP2.Client.Internal: Config :: Buffer -> BufferSize -> (ByteString -> IO ()) -> (Int -> IO ByteString) -> PositionReadMaker -> Manager -> SockAddr -> SockAddr -> Bool -> (StreamId -> TokenHeaderTable -> IO ()) -> Config
+ Network.HTTP2.Client.Internal: Config :: Buffer -> BufferSize -> (ByteString -> IO ()) -> Maybe (STM () -> ByteString -> IO (Either () ())) -> (Int -> IO ByteString) -> Maybe (STM () -> Int -> IO ByteString) -> PositionReadMaker -> Manager -> SockAddr -> SockAddr -> Bool -> (StreamId -> TokenHeaderTable -> IO ()) -> Config
- Network.HTTP2.Server.Internal: Config :: Buffer -> BufferSize -> (ByteString -> IO ()) -> (Int -> IO ByteString) -> PositionReadMaker -> Manager -> SockAddr -> SockAddr -> Bool -> (StreamId -> TokenHeaderTable -> IO ()) -> Config
+ Network.HTTP2.Server.Internal: Config :: Buffer -> BufferSize -> (ByteString -> IO ()) -> Maybe (STM () -> ByteString -> IO (Either () ())) -> (Int -> IO ByteString) -> Maybe (STM () -> Int -> IO ByteString) -> PositionReadMaker -> Manager -> SockAddr -> SockAddr -> Bool -> (StreamId -> TokenHeaderTable -> IO ()) -> Config

Files

ChangeLog.md view
@@ -1,5 +1,32 @@ # ChangeLog for http2 +## 5.4.13++* `Config` gains `confSendAllWith` and `confReadNWith`, a send and a read+  which take an `STM` action to give up on.  `allocSimpleConfigWith` fills+  them with network's `sendAllWith` and `recvWith`.  With them, a write the+  peer does not read is given up on once it has taken the whole timeout,+  and the receiver is stopped after a GOAWAY without being interrupted.+* `runWith` and `runIOWith` take an `STM` action.  When it fires, the server+  sends GOAWAY(NO_ERROR) with the largest stream identifier and a PING, then,+  once the PING is answered or a second has passed, GOAWAY with the last+  stream taken.  Streams above it are refused with REFUSED_STREAM, and the+  connection is closed when those below it are done (RFC 9113, section 6.8).+* A stream is half-closed in the transaction which queues the end of its+  body.  A client which had read the whole response could find the stream+  still open and reset it; enough such resets made the peer close the+  connection with "too many rst_stream".+* Give up on a write which takes the whole timeout.+  [#233](https://github.com/kazu-yamamoto/http2/pull/233)+* Stop the receiver in STM when its reads can give up.+  [#234](https://github.com/kazu-yamamoto/http2/pull/234)+* Start no GOAWAY thread for run and runIO.+  [#235](https://github.com/kazu-yamamoto/http2/pull/235)+* Stop the receiver rather than cancel it when the sender fails first.+  [#238](https://github.com/kazu-yamamoto/http2/pull/238)+* Requires network 3.2.12.+* Allows iproute 1.8 and unix-time 0.6.+ ## 5.4.12  * Close a connection's streams before waiting for its threads.
Network/HTTP2/Client.hs view
@@ -76,7 +76,9 @@     confWriteBuffer,     confBufferSize,     confSendAll,+    confSendAllWith,     confReadN,+    confReadNWith,     confPositionReadMaker,     confTimeoutManager,     confMySockAddr,
Network/HTTP2/H2/Config.hs view
@@ -3,11 +3,12 @@ module Network.HTTP2.H2.Config where  import Control.Concurrent.STM+import Control.Monad (void) import Data.IORef import Foreign.Marshal.Alloc (free, mallocBytes) import Network.HTTP.Semantics.Client import Network.Socket-import Network.Socket.ByteString (sendAll)+import Network.Socket.ByteString (sendAll, sendAllWith) import qualified System.TimeManager as T  import Network.HPACK@@ -31,13 +32,19 @@ --   reader ends as it does at end of input.  Nothing is closed and no --   thread is interrupted to make it return. --+--   It also fills 'confSendAllWith' and 'confReadNWith' (since 5.4.13), so+--   that the library can give up on a send or a read in the same way.+-- --   @since 5.4.11 allocSimpleConfigWith :: Socket -> BufferSize -> Int -> STM a -> IO Config allocSimpleConfigWith s bufsiz usec giveUp = do     confWriteBuffer <- mallocBytes bufsiz     let confBufferSize = bufsiz     let confSendAll = sendAll s-    confReadN <- (\ref -> defaultReadNWith s ref giveUp) <$> newIORef Nothing+        confSendAllWith = Just $ \giveUp' bs -> sendAllWith s bs giveUp'+    ref <- newIORef Nothing+    let confReadN = defaultReadNWith s ref giveUp+        confReadNWith = Just $ \stop -> defaultReadNWith s ref (void giveUp `orElse` stop)     let confPositionReadMaker = defaultPositionReadMaker     confTimeoutManager <- T.initialize usec     confMySockAddr <- getSocketName s
Network/HTTP2/H2/Context.hs view
@@ -4,6 +4,7 @@  module Network.HTTP2.H2.Context where +import Control.Concurrent (ThreadId) import Control.Concurrent.STM import qualified Control.Exception as E import Data.IORef@@ -95,7 +96,7 @@     , continued          :: IORef (Maybe HeaderContinuation)     , myStreamId         :: TVar StreamId     , peerStreamId       :: IORef StreamId-    , peerLastStreamId   :: IORef StreamId+    , peerLastStreamId   :: TVar StreamId     , outputBufferLimit  :: IORef Int     , outputQ            :: TQueue Output     -- ^ Invariant: Each stream will only ever have at most one 'Output'@@ -118,6 +119,10 @@     -- ^ For the threads that are timed out: owning a thread and     --   timing it out are separate, so the manager carries no timer.     , receiverDone       :: TVar (Maybe E.SomeException)+    , stopReceiving      :: TVar Bool+    -- ^ Set to end the receiver at the next frame: see 'stopReceiver'.+    , receiverStoppable  :: Bool+    -- ^ Whether the receiver's reads give up on 'stopReceiving'.     , appDone            :: STM Bool     -- ^ Server only: set by 'sioDone' once a 'runIO' application has     --   nothing more to send.  Never for 'run' and clients.@@ -128,6 +133,11 @@     , peerGoAway         :: TVar (Maybe StreamId)     -- ^ The last stream identifier of the peer's GOAWAY(NO_ERROR), once     --   one has come: see 'goingAway'.+    , myGoAway           :: TVar (Maybe StreamId)+    -- ^ Server only: the last stream identifier of our GOAWAY(NO_ERROR),+    --   once we have sent one: see 'admitPeerStream'.+    , pingAcked          :: TVar ByteString+    -- ^ The payload of the last PING the peer acknowledged.     , activeRequests     :: TVar Int     -- ^ Client only: requests whose 'processResponse' has not returned.     --   A response can be complete, its stream gone from the table, and@@ -179,7 +189,7 @@     continued         <- newIORef Nothing     myStreamId        <- newTVarIO sid0     peerStreamId      <- newIORef 0-    peerLastStreamId  <- newIORef 0+    peerLastStreamId  <- newTVarIO 0     outputBufferLimit <- newIORef buflim     outputQ           <- newTQueueIO     outputQStreamID   <- newTVarIO sid0@@ -206,9 +216,13 @@     threadManager   <- T.newThreadManager     let timeoutManager = timmgr     receiverDone    <- newTVarIO Nothing+    stopReceiving   <- newTVarIO False+    let receiverStoppable = isJust confReadNWith     let informationalCallback = confOnInformational     let appDone = fromMaybe (return False) mdone     peerGoAway      <- newTVarIO Nothing+    myGoAway        <- newTVarIO Nothing+    pingAcked       <- newTVarIO ""     activeRequests  <- newTVarIO 0     resetByMe       <- newIORef IntSet.empty     return Context{..}@@ -227,6 +241,33 @@  ---------------------------------------------------------------- +-- | Ending the receiver as at end of input, once the streams it reads for+--   are done: in 'STM' when its reads can give up, by an exception to it+--   otherwise.+stopReceiver :: Context -> ThreadId -> IO ()+stopReceiver Context{stopReceiving, receiverStoppable} receiver+    | receiverStoppable = atomically $ writeTVar stopReceiving True+    | otherwise = E.throwTo receiver ConnectionIsClosed++----------------------------------------------------------------++-- | Fires once the receiver has been told to stop.+receiverStopped :: Context -> STM ()+receiverStopped Context{stopReceiving} = readTVar stopReceiving >>= check++----------------------------------------------------------------++-- | Fires once the receiver has ended with 'ConnectionIsTimeout': a+--   write still blocked then is given up on.+timedOut :: Context -> STM ()+timedOut Context{receiverDone} = do+    done <- readTVar receiverDone+    case done >>= E.fromException of+        Just ConnectionIsTimeout -> return ()+        _ -> retry++----------------------------------------------------------------+ isClient :: Context -> Bool isClient ctx = role ctx == Client @@ -251,10 +292,19 @@ ----------------------------------------------------------------  getPeerLastStreamId :: Context -> IO StreamId-getPeerLastStreamId ctx = readIORef $ peerLastStreamId ctx+getPeerLastStreamId ctx = readTVarIO $ peerLastStreamId ctx -modifyPeerLastStreamId :: Context -> StreamId -> IO ()-modifyPeerLastStreamId ctx sid = atomicModifyIORef' (peerLastStreamId ctx) $ \n -> if sid > n then (sid, ()) else (n, ())+-- | Counting a stream of the peer's towards the last stream identifier,+--   unless our GOAWAY has gone out below it.  Answers whether it is+--   admitted; one that is not must be refused, not processed.+admitPeerStream :: Context -> StreamId -> IO Bool+admitPeerStream Context{peerLastStreamId, myGoAway} sid = atomically $ do+    mlast <- readTVar myGoAway+    case mlast of+        Just lastSid | sid > lastSid -> return False+        _ -> do+            modifyTVar' peerLastStreamId $ max sid+            return True  ---------------------------------------------------------------- @@ -323,9 +373,19 @@     open st = st  halfClosedRemote :: Context -> Stream -> IO ()-halfClosedRemote ctx stream@Stream{streamState} = do-    closingCode <- atomically $ stateTVar streamState closeHalf+halfClosedRemote ctx stream = halfClosedRemoteWith ctx stream $ return ()++-- | 'halfClosedRemote' in the transaction which hands the end of the body+--   to its reader, so that a reader which has seen the end never finds the+--   stream open, and resets it as one it did not read to the end.+halfClosedRemoteWith :: Context -> Stream -> STM a -> IO a+halfClosedRemoteWith ctx stream@Stream{streamState} deliver = do+    (a, closingCode) <- atomically $ do+        a <- deliver+        closingCode <- stateTVar streamState closeHalf+        return (a, closingCode)     traverse_ (closed ctx stream) closingCode+    return a   where     closeHalf :: StreamState -> (Maybe ClosedCode, StreamState)     closeHalf x@(Closed _) = (Nothing, x)@@ -588,3 +648,26 @@                     | otherwise = (evens, odds)                 noneOfMine = maybe True ((> lastSid) . fst) $ IntMap.lookupMin mine             check $ IntMap.null theirs && noneOfMine && active == 0++----------------------------------------------------------------+-- GOAWAY of ours++-- | Fixing the last stream identifier of our GOAWAY(NO_ERROR) at the+--   streams admitted so far.  Answers it the first time only.+goingAwayMine :: Context -> STM (Maybe StreamId)+goingAwayMine Context{peerLastStreamId, myGoAway} = do+    old <- readTVar myGoAway+    case old of+        Just _ -> return Nothing+        Nothing -> do+            lastSid <- readTVar peerLastStreamId+            writeTVar myGoAway $ Just lastSid+            return $ Just lastSid++-- | Waiting until the streams below our GOAWAY are done.  Those above it+--   are refused, so none of the peer's is left.+drainedMine :: Context -> STM ()+drainedMine Context{myGoAway, oddStreamTable} = do+    readTVar myGoAway >>= check . isJust+    odds <- oddTable <$> readTVar oddStreamTable+    check $ IntMap.null odds
Network/HTTP2/H2/Receiver.hs view
@@ -23,7 +23,6 @@ import Data.Void import Network.Control import Network.HTTP.Semantics-import qualified System.IO.Error as E import qualified System.ThreadManager as T import qualified System.TimeManager as TM @@ -72,10 +71,19 @@             else                 TM.withHandle (timeoutManager ctx) (E.throwTo tid ConnectionIsTimeout) loop2 +    -- Told to stop, the receiver ends between frames as at end of input.+    stopped = receiverStopped ctx+    waitOrStop = do+        stop <-+            atomically $+                (True <$ stopped) `orElse` (False <$ waitControlQ (controlQ ctx))+        when stop $ E.throwIO ConnectionIsClosed+    readHeader = readNGivingUp conf stopped frameHeaderLength+     loop1 :: IO Void     loop1 = do-        atomically $ waitControlQ $ controlQ ctx-        hd <- confReadN frameHeaderLength -- throwing an exception on timeout+        waitOrStop+        hd <- readHeader -- throwing an exception on timeout         when (BS.null hd) $ E.throwIO ConnectionIsClosed         processFrame ctx conf $ decodeFrameHeader hd         loop1@@ -85,8 +93,8 @@         -- If 'confReadN' is timeouted, 'ConnectionIsTimeout' is thrown         -- to destroy the thread trees.  So it is if the peer does not         -- read what we send, and we wait here for ever.-        atomically $ waitControlQ $ controlQ ctx-        hd <- confReadN frameHeaderLength+        waitOrStop+        hd <- readHeader         TM.tickle th         when (BS.null hd) $ E.throwIO ConnectionIsClosed         processFrame ctx conf $ decodeFrameHeader hd@@ -105,13 +113,13 @@     | isServer ctx =         E.throwIO $             ConnectionErrorIsSent ProtocolError streamId "push promise is not allowed"-processFrame Context{..} conf (ftyp, FrameHeader{payloadLength, streamId})+processFrame ctx@Context{..} conf (ftyp, FrameHeader{payloadLength, streamId})     | ftyp > maxFrameType = do         mx <- readIORef continued         case mx of             Nothing -> do                 -- ignoring unknown frame-                void $ readPayload conf payloadLength+                void $ readPayload ctx conf payloadLength             Just _ -> E.throwIO $ ConnectionErrorIsSent ProtocolError streamId "unknown frame" processFrame ctx@Context{..} conf typhdr@(ftyp, header) = do     -- My SETTINGS_MAX_FRAME_SIZE@@ -136,20 +144,20 @@ -- 'confReadN' answers with an empty string at end of input, so a payload that -- comes back short means the peer hung up in the middle of the frame.  Saying -- so here keeps every decoder below from being handed fewer bytes than the--- frame header promised it.-readPayload :: Config -> Int -> IO ByteString-readPayload Config{..} len = do-    bs <- confReadN len+-- frame header promised it.  A receiver told to stop gives it up the same.+readPayload :: Context -> Config -> Int -> IO ByteString+readPayload ctx conf len = do+    bs <- readNGivingUp conf (receiverStopped ctx) len     when (BS.length bs /= len) $ E.throwIO ConnectionIsClosed     return bs  controlOrStream :: Context -> Config -> FrameType -> FrameHeader -> IO () controlOrStream ctx@Context{..} conf ftyp header@FrameHeader{flags, streamId, payloadLength}     | isControl streamId = do-        bs <- readPayload conf payloadLength+        bs <- readPayload ctx conf payloadLength         control ftyp header bs ctx     | ftyp == FramePushPromise = do-        bs <- readPayload conf payloadLength+        bs <- readPayload ctx conf payloadLength         -- A promised stream can be refused over concurrency too, and by the         -- time 'push' gets that far it has decoded the field block, so the         -- same reasoning as 'resettable' applies: reset the promised stream@@ -159,7 +167,7 @@     | otherwise = do         mcont <- checkContinued         mstrm <- getStream ctx ftyp streamId-        bs <- readPayload conf payloadLength+        bs <- readPayload ctx conf payloadLength         case mcont of             Just hc -> continuation hc bs mstrm             Nothing ->@@ -262,7 +270,7 @@                         state <- onResponseHeaders ctx streamId hcl (hcEndOfStream hc) tbl                         processState state ctx strm streamId                     Open _ (Body q mcl bodyLength tlr) -> do-                        state <- onTrailers ctx streamId blk q mcl bodyLength tlr+                        state <- onTrailers ctx strm streamId blk q mcl bodyLength tlr                         processState state ctx strm streamId                     _otherwise ->                         -- The block began on a stream that was open for@@ -370,9 +378,13 @@ -- that had been acted on. launchHandler :: Context -> Stream -> InpObj -> IO () launchHandler ctx@Context{roleInfo} strm inpObj = do-    modifyPeerLastStreamId ctx $ streamNumber strm+    -- One above our GOAWAY is refused: the peer retries it elsewhere.+    admitted <- admitPeerStream ctx sid+    unless admitted $ E.throwIO $ StreamErrorIsSent RefusedStream sid "going away"     let ServerInfo{..} = toServerInfo roleInfo     launch ctx strm inpObj+  where+    sid = streamNumber strm  ---------------------------------------------------------------- @@ -496,8 +508,9 @@                         setframe = CFrames (Just peerAlist) (frames ++ [ack])                     writeIORef myFirstSettings True                     enqueueControl controlQ setframe-control FramePing FrameHeader{flags, streamId} bs ctx@Context{mySettings, pingRate} =-    unless (testAck flags) $ do+control FramePing FrameHeader{flags, streamId} bs ctx@Context{mySettings, pingRate, pingAcked}+    | testAck flags = atomically $ writeTVar pingAcked bs+    | otherwise = do         rate <- getRate pingRate         if rate > pingRateLimit mySettings             then E.throwIO $ ConnectionErrorIsSent EnhanceYourCalm streamId "too many ping"@@ -514,7 +527,7 @@                 receiver <- myThreadId                 T.forkManaged (threadManager ctx) "H2 draining after GOAWAY" $ do                     atomically $ drained ctx-                    E.throwTo receiver ConnectionIsClosed+                    stopReceiver ctx receiver         else E.throwIO $ ConnectionErrorIsReceived err sid $ Short.toShort msg control FrameWindowUpdate header bs ctx = do     WindowUpdateFrame n <- guardIt $ decodeWindowUpdateFrame header bs@@ -635,14 +648,14 @@                     return s  -- Transition (stream2)-stream FrameHeaders header@FrameHeader{flags, streamId} bs ctx s@(Open _ (Body q mcl bodyLength tlr)) _ = do+stream FrameHeaders header@FrameHeader{flags, streamId} bs ctx s@(Open _ (Body q mcl bodyLength tlr)) strm = do     HeadersFrame _ frag <- guardIt $ decodeHeadersFrame header bs     let endOfStream = testEndStream flags     -- checking frag == "" is not necessary     if endOfStream         then do             if testEndHeader flags-                then onTrailers ctx streamId frag q mcl bodyLength tlr+                then onTrailers ctx strm streamId frag q mcl bodyLength tlr                 else do                     startHeaderBlock ctx streamId endOfStream frag                     return s@@ -714,12 +727,20 @@                     ProtocolError                     streamId                     "actual body length is not the same as content-length"+        -- The end goes in with the half-close: see 'halfClosedRemoteWith'.+        let deliver+                | endOfStream =+                    halfClosedRemoteWith ctx strm . (<* writeTQueue q (Right (mempty, True)))+                | otherwise = atomically         -- Empty Frame Flooding - CVE-2019-9518         if body == ""-            then unless endOfStream $ do-                rate <- getRate emptyFrameRate-                when (rate > emptyFrameRateLimit mySettings) $ do-                    E.throwIO $ ConnectionErrorIsSent EnhanceYourCalm streamId "too many empty data"+            then+                if endOfStream+                    then deliver $ return ()+                    else do+                        rate <- getRate emptyFrameRate+                        when (rate > emptyFrameRateLimit mySettings) $ do+                            E.throwIO $ ConnectionErrorIsSent EnhanceYourCalm streamId "too many empty data"             else do                 writeIORef bodyLength len                 -- Not for a stream closed since its state was read, by@@ -732,7 +753,7 @@                 -- the stream.  Checked in the same transaction, so that                 -- a closer that has emptied the queue finds nothing put                 -- in after it.-                outcome <- atomically $ do+                outcome <- deliver $ do                     st <- readTVar streamState                     discard <- readTVar streamDiscardBody                     if@@ -748,12 +769,7 @@                         unless (connectionCreditedOnArrival streamId) $                             giveBackConnectionWindow ctx $                                 BS.length body-        if endOfStream-            then do-                -- no trailers-                atomically $ writeTQueue q $ Right (mempty, True)-                return HalfClosedRemote-            else return s+        return $ if endOfStream then HalfClosedRemote else s  -- (No state transition) stream FrameWindowUpdate header bs _ s strm = do@@ -938,9 +954,6 @@     sid <- getPeerLastStreamId ctx     return $ goawayFrame sid err msg -sendGoaway :: Config -> ByteString -> IO ()-sendGoaway Config{..} frame = confSendAll frame `E.catchIOError` \_ -> return ()- ----------------------------------------------------------------  sendPing :: Context -> Bool -> ByteString -> IO ()@@ -958,6 +971,7 @@ -- the block is decoded, for the dynamic table. onTrailers     :: Context+    -> Stream     -> StreamId     -> HeaderBlockFragment     -> TQueue (Either E.SomeException (ByteString, Bool))@@ -967,7 +981,7 @@     -- ^ The length of the body received     -> IORef (Maybe TokenHeaderTable)     -> IO StreamState-onTrailers ctx streamId blk q mcl bodyLength tlr = do+onTrailers ctx strm streamId blk q mcl bodyLength tlr = do     tbl <- hpackDecodeTrailer blk streamId ctx     len <- readIORef bodyLength     when (just mcl (/= len)) $@@ -977,7 +991,7 @@                 streamId                 "actual body length is not the same as content-length"     writeIORef tlr (Just tbl)-    atomically $ writeTQueue q $ Right (mempty, True)+    halfClosedRemoteWith ctx strm $ writeTQueue q $ Right (mempty, True)     return HalfClosedRemote  -- | Start accumulating a header block that does not fit in a single frame
Network/HTTP2/H2/Sender.hs view
@@ -19,6 +19,7 @@ import Network.HTTP.Semantics.Client import Network.HTTP.Semantics.IO import qualified System.ThreadManager as T+import qualified System.TimeManager as TM  import Imports import Network.HPACK (setLimitForEncoding, toTokenHeaderTable)@@ -107,10 +108,27 @@ checkDone _ _ _ = return Nothing  frameSender :: Context -> Config -> IO E.SomeException-frameSender+frameSender ctx conf@Config{confSendAllWith} = do+    labelMe "H2 sender"+    case confSendAllWith of+        Nothing -> frameSenderWith ctx conf $ sendAllGivingUp conf retry+        -- A write is given up on once it has taken the whole timeout: the+        -- receiver's timeout no longer covers it once the receiver has ended.+        Just _ -> do+            stalled <- newTVarIO False+            TM.withHandle (timeoutManager ctx) (atomically $ writeTVar stalled True) $ \th -> do+                TM.pause th+                frameSenderWith ctx conf $ \bs -> do+                    TM.resume th+                    let giveUp = timedOut ctx `orElse` (readTVar stalled >>= check)+                    sendAllGivingUp conf giveUp bs <* TM.pause th++frameSenderWith+    :: Context -> Config -> (ByteString -> IO (Either () ())) -> IO E.SomeException+frameSenderWith     ctx@Context{outputQ, controlQ, encodeDynamicTable, outputBufferLimit}-    Config{..} = do-        labelMe "H2 sender"+    Config{..}+    send = do         -- DATA that has to wait for the connection window, in the order it         -- came: see 'dequeue'.         parked <- newTVarIO []@@ -139,7 +157,10 @@         -- the buffer are filled.         flushN :: Offset -> IO ()         flushN 0 = return ()-        flushN n = bufferIO confWriteBuffer n confSendAll+        flushN n = bufferIO confWriteBuffer n $ \bs ->+            -- Given up on once the connection has timed out: closed with+            -- GOAWAY like any other timeout.+            send bs >>= either (\_ -> E.throwIO ConnectionIsTimeout) return          flushIfNecessary :: Offset -> IO Offset         flushIfNecessary off = do
Network/HTTP2/H2/Types.hs view
@@ -15,6 +15,7 @@ import Network.HTTP.Semantics.Client import Network.HTTP.Semantics.IO import Network.Socket hiding (Stream)+import System.IO.Error (catchIOError) import System.IO.Unsafe import qualified System.TimeManager as T @@ -266,7 +267,28 @@     --   So, this value is announced via SETTINGS_MAX_FRAME_SIZE     --   to the peer.     , confSendAll :: ByteString -> IO ()+    -- ^ Not used when 'confSendAllWith' is 'Just'.  When replacing it in+    --   a config which has one, e.g. from 'allocSimpleConfig', set+    --   'confSendAllWith' to 'Nothing' or to a send of the same kind.+    , confSendAllWith :: Maybe (STM () -> ByteString -> IO (Either () ()))+    -- ^ 'confSendAll' which the library can give up on: 'Left' when the+    --   'STM' action fires first.  With it, a write which the peer does+    --   not read is ended by the timeout rather than by killing a thread.+    --   Part of the data may have gone, so the connection is closed after.+    --+    --   @since 5.4.13     , confReadN :: Int -> IO ByteString+    -- ^ When 'confReadNWith' is 'Just', frames are read with it and only+    --   the connection preface with this.  When replacing it in a config+    --   which has one, e.g. from 'allocSimpleConfig', set 'confReadNWith'+    --   to 'Nothing' or to a read of the same kind sharing its buffer.+    , confReadNWith :: Maybe (STM () -> Int -> IO ByteString)+    -- ^ 'confReadN' which the library can give up on: it answers with an+    --   empty string, as at end of input, when the 'STM' action fires+    --   first.  With it, the reader is stopped without being interrupted.+    --   It must share its buffer with 'confReadN'.+    --+    --   @since 5.4.13     , confPositionReadMaker :: PositionReadMaker     , confTimeoutManager :: TM.Manager     , confMySockAddr :: SockAddr@@ -290,7 +312,9 @@         { confWriteBuffer = nullPtr         , confBufferSize = 0         , confSendAll = \_ -> return ()+        , confSendAllWith = Nothing         , confReadN = \_ -> return ""+        , confReadNWith = Nothing         , confPositionReadMaker = defaultPositionReadMaker         , confTimeoutManager = T.defaultManager         , confMySockAddr = SockAddrInet 0 0@@ -298,6 +322,31 @@         , confReadNTimeout = False         , confOnInformational = \_ _ -> return ()         }++-- | Sending with 'confSendAllWith' if there is one, giving up when the+--   'STM' action fires, and with 'confSendAll' otherwise.+sendAllGivingUp :: Config -> STM () -> ByteString -> IO (Either () ())+sendAllGivingUp Config{..} giveUp bs = case confSendAllWith of+    Just sendWith -> sendWith giveUp bs+    Nothing -> Right <$> confSendAll bs++-- | Reading with 'confReadNWith' if there is one, giving up when the+--   'STM' action fires, and with 'confReadN' otherwise.+readNGivingUp :: Config -> STM () -> Int -> IO ByteString+readNGivingUp Config{..} giveUp n = case confReadNWith of+    Just readWith -> readWith giveUp n+    Nothing -> confReadN n++-- | Sending GOAWAY, as a last word: it waits one second at most, since a+--   peer which does not read would hold the connection open for good.+sendGoaway :: Config -> ByteString -> IO ()+sendGoaway conf frame = send `catchIOError` \_ -> return ()+  where+    send = case confSendAllWith conf of+        Nothing -> confSendAll conf frame+        Just _ -> do+            late <- registerDelay 1000000+            void $ sendAllGivingUp conf (readTVar late >>= check) frame  isAsyncException :: E.Exception e => e -> Bool isAsyncException e =
Network/HTTP2/Server.hs view
@@ -26,6 +26,7 @@ module Network.HTTP2.Server (     -- * Runner     run,+    runWith,      -- * Server configuration     ServerConfig,@@ -56,7 +57,9 @@     confWriteBuffer,     confBufferSize,     confSendAll,+    confSendAllWith,     confReadN,+    confReadNWith,     confPositionReadMaker,     confTimeoutManager,     confMySockAddr,@@ -77,4 +80,5 @@     ServerConfig (..),     defaultServerConfig,     run,+    runWith,  )
Network/HTTP2/Server/Internal.hs view
@@ -9,6 +9,7 @@     Stream,     ServerIO (..),     runIO,+    runIOWith, ) where  import Network.HTTP.Semantics.Server
Network/HTTP2/Server/Run.hs view
@@ -45,12 +45,24 @@  -- | Running HTTP/2 server. run :: ServerConfig -> Config -> Server -> IO ()-run sconf conf server = do+run = runMaybe Nothing++-- | Like 'run', but the connection goes away once the 'STM' action fires:+--   GOAWAY(NO_ERROR) at once, streams above it refused, and the+--   connection closed when those below it are done.+--+--   @since 5.4.13+runWith :: STM () -> ServerConfig -> Config -> Server -> IO ()+runWith = runMaybe . Just++-- Nothing for 'run': no thread waits on an action which never fires.+runMaybe :: Maybe (STM ()) -> ServerConfig -> Config -> Server -> IO ()+runMaybe mGoAway sconf conf server = do     ok <- checkPreface conf     when ok $ do         let lnch = runServer conf server         ctx <- setup sconf conf lnch Nothing-        runH2 conf ctx+        runH2 conf ctx mGoAway  ---------------------------------------------------------------- @@ -71,7 +83,27 @@     -> Config     -> (ServerIO Stream -> IO (IO ()))     -> IO ()-runIO sconf conf@Config{..} action = do+runIO = runIOMaybe Nothing++-- | Like 'runIO', but the connection goes away once the 'STM' action+--   fires, as with 'runWith'.+--+--   @since 5.4.13+runIOWith+    :: STM ()+    -> ServerConfig+    -> Config+    -> (ServerIO Stream -> IO (IO ()))+    -> IO ()+runIOWith = runIOMaybe . Just++runIOMaybe+    :: Maybe (STM ())+    -> ServerConfig+    -> Config+    -> (ServerIO Stream -> IO (IO ()))+    -> IO ()+runIOMaybe mGoAway sconf conf@Config{..} action = do     ok <- checkPreface conf     when ok $ do         inpQ <- newTQueueIO@@ -97,7 +129,7 @@                     , sioDone = atomically $ writeTVar done True                     }         io <- action serverIO-        concurrently_ io $ runH2 conf ctx+        concurrently_ io $ runH2 conf ctx mGoAway  checkPreface :: Config -> IO Bool checkPreface conf@Config{..} = do@@ -120,32 +152,75 @@         confTimeoutManager         mIsDone -runH2 :: Config -> Context -> IO ()-runH2 conf ctx = do+runH2 :: Config -> Context -> Maybe (STM ()) -> IO ()+runH2 conf ctx mGoAway = do     let mgr = threadManager ctx         runReceiver = frameReceiver ctx conf         runSender = frameSender ctx conf+        -- Two GOAWAYs, as RFC 9113 section 6.8 has it: the first, with+        -- the largest identifier, stops the client opening streams; the+        -- second fixes the last one taken once a PING has gone round, so+        -- that the streams it opened meanwhile are not refused.  The+        -- receiver is stopped as it is after the peer's GOAWAY, once the+        -- streams below ours are done; it reads until then.+        sendGoAway :: STM () -> Async a -> IO ()+        sendGoAway goAway ar = do+            atomically goAway+            enqueueControl (controlQ ctx) $+                CFrames+                    Nothing+                    [ goawayFrame maxStreamId NoError "going away"+                    , pingFrame False goAwayPing+                    ]+            roundTrip <- registerDelay 1000000+            atomically $+                (readTVar (pingAcked ctx) >>= check . (== goAwayPing))+                    `orElse` (readTVar roundTrip >>= check)+            mlast <- atomically $ goingAwayMine ctx+            forM_ mlast $ \lastSid ->+                enqueueControl (controlQ ctx) $+                    CFrames Nothing [goawayFrame lastSid NoError "going away"]+            atomically $ drainedMine ctx+            stopReceiver ctx $ asyncThreadId ar+        withGoAway ar body = case mGoAway of+            Nothing -> body+            Just goAway -> withAsync (sendGoAway goAway ar) $ \_ -> body         runBackgroundThreads =             withAsync runReceiver $ \ar ->-                withAsync runSender $ \as -> do-                    r <- waitEither ar as-                    e <- case r of-                        -- The receiver is done; the sender finishes once it-                        -- has flushed what is queued.-                        Left _ -> wait as-                        -- The sender finished first.  Either the receiver is-                        -- done too and not yet seen to be, and this is its-                        -- error, or the sender failed: nothing more would go-                        -- out, and leaving the receiver to run on left the-                        -- connection open and silent, with no GOAWAY.  Both-                        -- are closed with it.-                        Right e -> return e-                    closureServer conf ctx e+                withGoAway ar $+                    withAsync runSender $ \as -> do+                        r <- waitEither ar as+                        e <- case r of+                            -- The receiver is done; the sender finishes once it+                            -- has flushed what is queued.+                            Left _ -> wait as+                            -- The sender finished first.  Either the receiver is+                            -- done too and not yet seen to be, and this is its+                            -- error, or the sender failed: nothing more would go+                            -- out, and leaving the receiver to run on left the+                            -- connection open and silent, with no GOAWAY.  Both+                            -- are closed with it: the receiver is stopped+                            -- rather than cancelled when its reads can give up.+                            Right e -> do+                                stopReceiver ctx $ asyncThreadId ar+                                late <- registerDelay 1000000+                                atomically $+                                    void (waitCatchSTM ar) `orElse` (readTVar late >>= check)+                                return e+                        closureServer conf ctx e     stopAfterAndWait mgr runBackgroundThreads $ \res ->         closeAllStreams (oddStreamTable ctx) (evenStreamTable ctx) res +-- | The payload of the PING sent with the first GOAWAY.+goAwayPing :: ByteString+goAwayPing = "goaway\0\0"++-- | The largest stream identifier.+maxStreamId :: StreamId+maxStreamId = 2147483647+ -- connClose must not be called here since Run:fork calls it goaway :: Config -> ErrorCode -> ByteString -> IO ()-goaway Config{..} etype debugmsg = confSendAll bytestream+goaway conf etype debugmsg = sendGoaway conf bytestream   where     bytestream = goawayFrame 0 etype debugmsg
http2.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               http2-version:            5.4.12+version:            5.4.13 license:            BSD3 license-file:       LICENSE maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>@@ -117,13 +117,13 @@         containers >=0.6,         http-semantics >= 0.4.2 && <0.5,         http-types >=0.12 && <0.13,-        iproute >= 1.7 && < 1.8,-        network >=3.1,+        iproute >= 1.7 && < 1.9,+        network >=3.2.12,         network-byte-order >=0.1.7 && <0.2,         network-control >=0.1 && <0.2,         stm >=2.5 && <2.6,         time-manager >=0.5 && <0.6,-        unix-time >=0.4.11 && <0.6,+        unix-time >=0.4.11 && <0.7,         utf8-string >=1.0 && <1.1  executable h2c-client@@ -293,6 +293,7 @@         HPACK.IntegerSpec         HTTP2.ClientSpec         HTTP2.FrameSpec+        HTTP2.GiveUpSpec         HTTP2.ServerSpec      default-language:   Haskell2010@@ -312,6 +313,7 @@         network-byte-order,         network-run >= 0.7 && <0.8,         random,+        stm,         time-manager >=0.5 && <0.6,         typed-process 
+ test/HTTP2/GiveUpSpec.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE OverloadedStrings #-}++module HTTP2.GiveUpSpec (spec) where++import Control.Concurrent+import Control.Concurrent.STM+import qualified Control.Exception as E+import Control.Monad (forever)+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.ByteString.Builder (byteString)+import Data.Word (Word8)+import Network.HTTP.Types+import Network.Run.TCP+import Network.Socket (+    ShutdownCmd (..),+    SocketOption (..),+    setSocketOption,+    shutdown,+ )+import Network.Socket.ByteString+import System.IO.Unsafe+import System.Random+import System.Timeout (timeout)+import Test.Hspec++import Network.HTTP2.Frame (connectionPreface)+import Network.HTTP2.Server++port :: String+port = show $ unsafePerformIO (randomPort <$> getStdGen)+  where+    randomPort = fst . randomR (45321 :: Int, 46320)++host :: String+host = "127.0.0.1"++-- | The timeout of the server, in microseconds.+serverTimeout :: Int+serverTimeout = 1000000++spec :: Spec+spec = describe "give-up" $ do+    it "gives up on a write the peer does not read" $ do+        ended <- newEmptyMVar+        let server _req _aux sendResponse =+                sendResponse (responseStreaming ok200 [] endless) []+            endless write flush = forever $ write (byteString chunk) >> flush+            chunk = B.replicate 65536 0x41+            runServer = runTCPServer (Just host) port $ \s ->+                E.bracket (allocSimpleConfig' s 32768 serverTimeout) freeSimpleConfig $ \conf ->+                    (run defaultServerConfig conf server `E.catch` ignore)+                        `E.finally` putMVar ended ()+        E.bracket (forkIO runServer) killThread $ \_ -> do+            threadDelay 10000+            -- The client opens the windows wide, asks, and never reads,+            -- so the server's writes stop in the socket, not on a window.+            r <- runTCPClient host port $ \s -> do+                -- A fixed small buffer: one that grows would take the data in.+                setSocketOption s RecvBuffer 4096+                sendAll s connectionPreface+                sendAll s $ frame 4 0 0 $ B.pack [0, 4, 0x7f, 0xff, 0xff, 0xff]+                sendAll s $ frame 8 0 0 $ B.pack [0x7f, 0xff, 0, 0]+                sendAll s $ frame 1 5 1 getHeaderBlock+                timeout 6000000 $ takeMVar ended+            r `shouldBe` Just ()+    it "gives up on a write the peer does not read after the receiver ends" $ do+        ended <- newEmptyMVar+        let server _req _aux sendResponse =+                sendResponse (responseStreaming ok200 [] endless) []+            endless write flush = forever $ write (byteString chunk) >> flush+            chunk = B.replicate 65536 0x41+            runServer = runTCPServer (Just host) port $ \s ->+                E.bracket (allocSimpleConfig' s 32768 serverTimeout) freeSimpleConfig $ \conf ->+                    (run defaultServerConfig conf server `E.catch` ignore)+                        `E.finally` putMVar ended ()+        E.bracket (forkIO runServer) killThread $ \_ -> do+            threadDelay 10000+            -- As above, but the client ends its side once it has asked, so+            -- the receiver ends on EOF rather than by its timeout.+            r <- runTCPClient host port $ \s -> do+                setSocketOption s RecvBuffer 4096+                sendAll s connectionPreface+                sendAll s $ frame 4 0 0 $ B.pack [0, 4, 0x7f, 0xff, 0xff, 0xff]+                sendAll s $ frame 8 0 0 $ B.pack [0x7f, 0xff, 0, 0]+                sendAll s $ frame 1 5 1 getHeaderBlock+                threadDelay 100000+                shutdown s ShutdownSend+                timeout 6000000 $ takeMVar ended+            r `shouldBe` Just ()+    it "stops a receiver which cannot be interrupted once it has gone away" $ do+        ended <- newEmptyMVar+        goVar <- newTVarIO False+        let server _req _aux sendResponse = sendResponse (responseNoBody ok200 []) []+            -- A read no exception reaches, as on a platform where a blocked+            -- recv cannot be interrupted; the one which gives up still can.+            uninterruptible conf = conf{confReadN = E.uninterruptibleMask_ . confReadN conf}+            runServer = runTCPServer (Just host) port $ \s ->+                E.bracket (allocSimpleConfig' s 32768 30000000) freeSimpleConfig $ \conf ->+                    (go (uninterruptible conf) `E.catch` ignore) `E.finally` putMVar ended ()+            go conf = runWith (readTVar goVar >>= check) defaultServerConfig conf server+        E.bracket (forkIO runServer) killThread $ \_ -> do+            threadDelay 10000+            -- The client says nothing after its SETTINGS and does not answer+            -- the PING, so the receiver waits in a read when it is stopped.+            r <- runTCPClient host port $ \s -> do+                sendAll s connectionPreface+                sendAll s $ frame 4 0 0 ""+                threadDelay 100000+                atomically $ writeTVar goVar True+                timeout 5000000 $ takeMVar ended+            r `shouldBe` Just ()+    it "stops a receiver which cannot be interrupted once the sender has failed" $ do+        ended <- newEmptyMVar+        let server _req _aux sendResponse = sendResponse (responseNoBody ok200 []) []+            -- No exception reaches the reads, and every write fails: the+            -- receiver used to be cancelled, which waited for good.+            broken conf =+                conf+                    { confReadN = E.uninterruptibleMask_ . confReadN conf+                    , confReadNWith = fmap (\r stop -> E.uninterruptibleMask_ . r stop) (confReadNWith conf)+                    , confSendAllWith = Just $ \_ _ -> E.throwIO $ userError "broken"+                    }+            runServer = runTCPServer (Just host) port $ \s ->+                E.bracket (allocSimpleConfig' s 32768 30000000) freeSimpleConfig $ \conf ->+                    (run defaultServerConfig (broken conf) server `E.catch` ignore)+                        `E.finally` putMVar ended ()+        E.bracket (forkIO runServer) killThread $ \_ -> do+            threadDelay 10000+            r <- runTCPClient host port $ \s -> do+                sendAll s connectionPreface+                sendAll s $ frame 4 0 0 ""+                timeout 5000000 $ takeMVar ended+            r `shouldBe` Just ()+  where+    ignore :: E.SomeException -> IO ()+    ignore _ = return ()++-- | A frame of this type, flags and stream with this payload.+frame :: Word8 -> Word8 -> Word8 -> ByteString -> ByteString+frame typ flags sid payload =+    B.pack [0, fromIntegral (len `div` 256), fromIntegral (len `mod` 256), typ, flags, 0, 0, 0, sid]+        <> payload+  where+    len = B.length payload++-- | GET / for 127.0.0.1, from the static table and one literal.+getHeaderBlock :: ByteString+getHeaderBlock = B.pack [0x82, 0x86, 0x84, 0x01, 0x09] <> "127.0.0.1"
test/HTTP2/ServerSpec.hs view
@@ -18,6 +18,7 @@  import Control.Concurrent import Control.Concurrent.Async+import Control.Concurrent.STM (TVar, atomically, check, newTVarIO, readTVar, writeTVar) import qualified Control.Exception as E import Control.Monad import Crypto.Hash (Context, SHA1)@@ -27,6 +28,7 @@ import Data.ByteString.Builder (Builder, byteString) import qualified Data.ByteString.Char8 as C8 import Data.IORef+import Data.List (sort) import Data.Maybe (isJust, isNothing) import Network.HTTP.Semantics import Network.HTTP.Types@@ -445,6 +447,56 @@                     Just (Left e) -> expectationFailure $ show (e :: C.HTTP2Error)                     Nothing -> expectationFailure "timed out" +        it "answers the streams it has taken and refuses the rest once it goes away" $ do+            -- RFC 9113, section 6.8: GOAWAY with the largest identifier, a+            -- PING, and once the PING is answered GOAWAY(NO_ERROR) with the+            -- last stream taken; one opened after it is refused, and the+            -- connection is closed once stream 1 is answered.+            goVar <- newTVarIO False+            withServer (runServerGoingAway id goVar) $ do+                threadDelay 10000+                timeout 5000000 (goAwayFromServer False goVar)+                    `shouldReturn` Just+                        [ "GOAWAY 2147483647 NoError"+                        , "GOAWAY 1 NoError"+                        , "RST_STREAM 3 RefusedStream"+                        , "DATA 1 END_STREAM"+                        , "GOAWAY 1 NoError"+                        ]++        it "goes away the same with reads which cannot give up" $ do+            -- Without 'confReadNWith', as for warp and http2-tls, the+            -- receiver is stopped by an exception instead.+            goVar <- newTVarIO False+            withServer (runServerGoingAway noReadNWith goVar) $ do+                threadDelay 10000+                timeout 5000000 (goAwayFromServer False goVar)+                    `shouldReturn` Just+                        [ "GOAWAY 2147483647 NoError"+                        , "GOAWAY 1 NoError"+                        , "RST_STREAM 3 RefusedStream"+                        , "DATA 1 END_STREAM"+                        , "GOAWAY 1 NoError"+                        ]++        it "answers a stream opened between its two GOAWAYs" $ do+            -- The second GOAWAY waits for the PING, so a stream already on+            -- its way when the first came is taken, not refused.+            goVar <- newTVarIO False+            withServer (runServerGoingAway id goVar) $ do+                threadDelay 10000+                r <- timeout 5000000 (goAwayFromServer True goVar)+                sort <$> r+                    `shouldBe` Just+                        ( sort+                            [ "GOAWAY 2147483647 NoError"+                            , "GOAWAY 3 NoError"+                            , "DATA 3 END_STREAM"+                            , "DATA 1 END_STREAM"+                            , "GOAWAY 3 NoError"+                            ]+                        )+         it "closes the connection once a runIO application is done" $             -- 'sioDone' set a flag that nothing read, so the connection             -- stayed open until the client closed it.@@ -478,6 +530,12 @@                 timeout 5000000 goAwayFromClient                     `shouldReturn` Just ["HEADERS 1", "DATA 1 END_STREAM", "GOAWAY NoError"] +        it "answers them the same with reads which cannot give up" $+            withServer (runServerWithConfig noReadNWith) $ do+                threadDelay 10000+                timeout 5000000 goAwayFromClient+                    `shouldReturn` Just ["HEADERS 1", "DATA 1 END_STREAM", "GOAWAY NoError"]+         it "resets a malformed request and goes on serving the connection" $             -- An upper-case field name makes the request malformed: a             -- stream error (RFC 9113, section 8.1.1).  It used to close the@@ -670,14 +728,22 @@ ignoreHTTP2Error _ = pure ()  runServer :: ServerSettings -> IO ()-runServer set = runTCPServerWithSettings set (Just host) port runHTTP2Server+runServer = runServerWithConfig id++-- | Like 'runServer', with the config changed.+runServerWithConfig :: (Config -> Config) -> ServerSettings -> IO ()+runServerWithConfig modify set = runTCPServerWithSettings set (Just host) port runHTTP2Server   where     runHTTP2Server s =         E.bracket             (allocSimpleConfig s 32768)             freeSimpleConfig-            (\conf -> run defaultServerConfig conf server)+            (\conf -> run defaultServerConfig (modify conf) server) +-- | A config whose reads the library cannot give up on.+noReadNWith :: Config -> Config+noReadNWith conf = conf{confReadNWith = Nothing}+ -- | Like 'runServer', but announcing room for a single concurrent stream. -- | Uploading 100000 octets of a file through 'C.runIO', and what the server -- says it received.@@ -929,6 +995,75 @@                     ]     _ <- E.try (action s) :: IO (Either E.IOException ())     collectFrames s++-- | Like 'runServerWithConfig', going away once the variable is set.+runServerGoingAway :: (Config -> Config) -> TVar Bool -> ServerSettings -> IO ()+runServerGoingAway modify goVar set = runTCPServerWithSettings set (Just host) port runHTTP2Server+  where+    runHTTP2Server s =+        E.bracket+            (allocSimpleConfig s 32768)+            freeSimpleConfig+            (\conf -> runWith (readTVar goVar >>= check) defaultServerConfig (modify conf) server)++-- | A request for /slow, then the server told to go away, and a request for+-- /: before the PING that came with the first GOAWAY is answered when+-- early, after the second GOAWAY otherwise.  What the server sends from its+-- first GOAWAY on.+goAwayFromServer :: Bool -> TVar Bool -> IO [String]+goAwayFromServer early goVar = runTCPClient host port $ \s -> do+    sendAll s connectionPreface+    sendAll s $ encodeFrame (EncodeInfo defaultFlags 0 Nothing) $ SettingsFrame []+    request s 1 "/slow"+    threadDelay 50000+    atomically $ writeTVar goVar True+    first <- readUntil s isGoAway+    when early $ request s 3 "/"+    ping <- readUntil s isPing+    second <-+        if early+            then return []+            else readUntil s isGoAway <* request s 3 "/"+    rest <- collect s+    return $ first ++ ping ++ second ++ rest+  where+    request s sid path =+        sendAll s $+            encodeFrame (EncodeInfo (setEndStream $ setEndHeader defaultFlags) sid Nothing) $+                HeadersFrame Nothing $+                    hpackEncode+                        [ (":scheme", "http")+                        , (":authority", "127.0.0.1")+                        , (":path", path)+                        , (":method", "GET")+                        ]+    isGoAway (ftyp, _, _) = ftyp == FrameGoAway+    isPing (ftyp, fh, _) = ftyp == FramePing && not (testAck (flags fh))+    -- Up to and including the first frame which satisfies the predicate;+    -- a PING is answered as it comes.+    readUntil s done = do+        mf <- recvFrame s+        case mf of+            Nothing -> return []+            Just f -> do+                answerPing s f+                if done f+                    then return $ maybe [] pure $ frameText f+                    else maybe id (:) (frameText f) <$> readUntil s done+    collect s = readUntil s (const False)+    answerPing s (FramePing, fh, p)+        | not (testAck (flags fh)) =+            sendAll s $ encodeFrame (EncodeInfo (setAck defaultFlags) 0 Nothing) $ PingFrame p+    answerPing _ _ = return ()+    frameText (FrameGoAway, fh, p)+        | Right (GoAwayFrame sid err _) <- decodeGoAwayFrame fh p =+            Just $ "GOAWAY " ++ show sid ++ " " ++ show err+    frameText (FrameRSTStream, fh, p)+        | Right (RSTStreamFrame err) <- decodeRSTStreamFrame fh p =+            Just $ "RST_STREAM " ++ show (streamId fh) ++ " " ++ show err+    frameText (FrameData, fh, _)+        | testEndStream (flags fh) = Just $ "DATA " ++ show (streamId fh) ++ " END_STREAM"+    frameText _ = Nothing  -- | A request for /slow, then GOAWAY(NO_ERROR) at once.  What the server -- sends from then on until it closes the connection.