packages feed

http3-0.1.10: Network/HTTP3/Context.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Network.HTTP3.Context (
    Context,
    withContext,
    unidirectional,
    cancelStream,
    isH3Server,
    isH3Client,
    accept,
    qpackEncode,
    qpackDecode,
    withHandle,
    newStream,
    closeStream,
    pReadMaker,
    abort,
    getHooks,
    Hooks (..), -- re-export
    getMySockAddr,
    getPeerSockAddr,
    getMaxFieldSectionSize,
    getPeerMaxFieldSectionSize,
    forkManaged,
    forkManagedTimeout,
    forkManagedTimeoutFinally,
    isAsyncException,
    admitRequest,
    requestDone,
    goAway,
    goAwayAll,
    waitPTO,
    drained,
) where

import Control.Concurrent (threadDelay)
import Control.Concurrent.STM
import qualified Control.Exception as E
import Control.Monad (forM_, void)
import Data.IORef
import Data.Maybe (isJust)
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Network.ByteOrder (withWriteBuffer)
import Network.HTTP.Semantics.Client
import Network.QUIC
import Network.QUIC.Internal (Microseconds (..), connLDCC, encodeInt', getPTO, isClient, isServer)
import Network.Socket (SockAddr)
import qualified System.ThreadManager as T
import qualified System.TimeManager as TM
import System.Timeout (timeout)

import Network.HTTP3.Config
import Network.HTTP3.Control
import Network.HTTP3.Error
import Network.HTTP3.Frame
import Network.HTTP3.Stream
import Network.QPACK
import Network.QPACK.Internal

data Context = Context
    { ctxConnection :: Connection
    , ctxQEncoder :: QEncoder
    , ctxQDecoder :: QDecoder
    , ctxUniSwitch :: H3StreamType -> InstructionHandler
    , ctxPReadMaker :: PositionReadMaker
    , ctxThreadManager :: T.ThreadManager
    , ctxTimeoutManager :: TM.Manager
    -- ^ Owning a thread and timing it out are separate, so the
    --   thread manager carries no timer and this is kept here.
    , ctxHooks :: Hooks
    , ctxMySockAddr :: SockAddr
    , ctxPeerSockAddr :: SockAddr
    , ctxMaxFieldSectionSize :: Int
    -- ^ What we told the peer we would accept, and so the most of any one
    -- frame we are willing to hold in memory while it arrives.
    , ctxCancelStream :: StreamId -> IO ()
    , ctxPeerMaxFieldSectionSize :: IO Int
    -- ^ The peer's SETTINGS_MAX_FIELD_SECTION_SIZE, or 'maxBound' until its
    -- SETTINGS have arrived
    -- ^ Sending a Stream Cancellation on our QPACK decoder stream
    , ctxSendControl :: [H3Frame] -> IO ()
    , ctxGoAway :: TVar (Maybe StreamId)
    -- ^ Server only: the ID of our GOAWAY, from which no request is taken.
    , ctxLastRequest :: TVar (Maybe StreamId)
    -- ^ Server only: the largest request stream taken.
    , ctxInFlight :: TVar IntSet
    -- ^ Server only: the request streams taken and not yet done with.
    }

-- | Running an action with a context, and seeing this connection's own
--   threads out before returning: they are using the connection, which
--   the caller tears down once this returns.
--
-- The wait is capped because one of the threads waited for runs the
-- application's handler: an application which swallows the asynchronous
-- exception would otherwise keep this from ever returning.  quic waits
-- for its own threads with no cap, because those are all its own and are
-- known to end.
withContext :: Connection -> Config -> (Context -> IO a) -> IO a
withContext conn conf action = do
    ctx <- newContext conn conf
    let mgr = ctxThreadManager ctx
    T.stopAfter mgr (action ctx) $ \_ ->
        void $ timeout stopTimeout $ T.waitUntilAllGone mgr

-- | How long the threads are given to end after they have been told to.
--   Unwinding takes milliseconds; this is for one that does not stop.
stopTimeout :: Int
stopTimeout = 5000000

newContext :: Connection -> Config -> IO Context
newContext conn conf = do
    (sendEI, sendDI, ctxSendControl) <- setupUnidirectional conn conf
    (ctxQEncoder, handleDI, dyntblE) <- newQEncoder (confQEncoderConfig conf) sendEI
    -- newQDecoder passes dyntbl for decoder to handleEI internally
    (ctxQDecoder, handleEI) <- newQDecoder (confQDecoderConfig conf) sendDI
    let ctxMaxFieldSectionSize = dcMaxFieldSectionSize $ confQDecoderConfig conf
    cst <- newControlState
    ctl <- controlStream conn ctxMaxFieldSectionSize dyntblE cst <$> newIORef IInit
    let ctxPeerMaxFieldSectionSize = getHeaderSize dyntblE
    seen <- newIORef []
    info <- getConnectionInfo conn
    let handleDI' recv = handleDI recv `E.catch` abortWith QpackDecoderStreamError
        handleEI' recv = handleEI recv `E.catch` abortWith QpackEncoderStreamError
        ctxUniSwitch = switch conn seen ctl handleEI' handleDI'
        ctxPReadMaker = confPositionReadMaker conf
        ctxHooks = confHooks conf
        ctxMySockAddr = localSockAddr info
        ctxPeerSockAddr = remoteSockAddr info
    let ctxCancelStream sid
            -- RFC 9204, section 4.4.2: "A decoder with a maximum dynamic table
            -- capacity equal to zero MAY omit sending Stream Cancellations",
            -- since the encoder then has nothing to release.
            | dcMaxTableCapacity (confQDecoderConfig conf) == 0 = return ()
            | otherwise =
                -- The stream is being given up on, and so, quite possibly, is
                -- the connection; there is nobody to report a failure to.
                (encodeDecoderInstructions [StreamCancellation sid] >>= sendDI)
                    `E.catch` ignoreSync
    ctxThreadManager <- T.newThreadManager
    ctxGoAway <- newTVarIO Nothing
    ctxLastRequest <- newTVarIO Nothing
    ctxInFlight <- newTVarIO IntSet.empty
    let ctxTimeoutManager = confTimeoutManager conf
    let ctxConnection = conn
    return Context{..}
  where
    ignoreSync :: E.SomeException -> IO ()
    ignoreSync se
        | isAsyncException se = E.throwIO se
        | otherwise = return ()
    abortWith :: ApplicationProtocolError -> E.SomeException -> IO ()
    abortWith aerr se
        | isAsyncException se = E.throwIO se
        | otherwise = connectionError conn aerr

isAsyncException :: E.Exception e => e -> Bool
isAsyncException e =
    case E.fromException (E.toException e) of
        Just (E.SomeAsyncException _) -> True
        Nothing -> False

-- | The handler for a unidirectional stream the peer opened, by its type.
--
-- The control stream and the two QPACK streams are critical: a peer may open
-- each of them only once (RFC 9114, section 6.2.1; RFC 9204, section 4.2),
-- and may not close any of them.  The control stream handler sees to the
-- closing itself; the QPACK ones are library code that simply returns when
-- the stream ends.
switch
    :: Connection
    -> IORef [H3StreamType]
    -- ^ The critical streams the peer has opened so far
    -> InstructionHandler
    -> InstructionHandler
    -> InstructionHandler
    -> H3StreamType
    -> InstructionHandler
switch conn seen ctl handleEI handleDI styp = case styp of
    H3ControlStreams -> once ctl
    QPACKEncoderStream -> once $ closing handleEI
    QPACKDecoderStream -> once $ closing handleDI
    -- RFC 9114, section 6.2.2: "Only servers can push; if a server receives
    -- a client-initiated push stream, this MUST be treated as a connection
    -- error of type H3_STREAM_CREATION_ERROR."  And section 4.6: a client
    -- "MUST treat receipt of a push stream as a connection error of type
    -- H3_ID_ERROR when no MAX_PUSH_ID frame has been sent", which this client
    -- never does.
    H3PushStreams
        | isServer conn -> \_ -> connectionError conn H3StreamCreationError
        | otherwise -> \_ -> connectionError conn H3IdError
    -- 'unidirectional' does not get here with an unknown type.
    H3StreamTypeUnknown _ -> \_ -> return ()
  where
    once, closing :: InstructionHandler -> InstructionHandler
    once handler recv = do
        dup <- atomicModifyIORef' seen $ \ts -> (styp : ts, styp `elem` ts)
        if dup
            then connectionError conn H3StreamCreationError
            else handler recv
    closing handler recv = do
        handler recv
        connectionError conn H3ClosedCriticalStream

isH3Server :: Context -> Bool
isH3Server Context{..} = isServer ctxConnection

isH3Client :: Context -> Bool
isH3Client Context{..} = isClient ctxConnection

accept :: Context -> IO Stream
accept Context{..} = acceptStream ctxConnection

qpackEncode :: Context -> QEncoder
qpackEncode Context{..} = ctxQEncoder

qpackDecode :: Context -> QDecoder
qpackDecode Context{..} = ctxQDecoder

cancelStream :: Context -> StreamId -> IO ()
cancelStream Context{..} = ctxCancelStream

unidirectional :: Context -> Stream -> IO ()
unidirectional Context{..} strm = do
    -- The type is a variable-length integer (RFC 9114, section 6.2), so one,
    -- two, four or eight octets -- not the single one it used to be read as,
    -- which cut anything from 0x40 up in half and handed the tail of the type
    -- to a handler as though it were the stream's contents.
    mtyp <- recvQInt (recvStream strm)
    case mtyp of
        -- The peer opened a unidirectional stream and closed it without
        -- saying what it was for.  Nothing to dispatch to; this used to be a
        -- pattern match failure.
        --
        -- Closed here, as below, because only a stream we close is counted
        -- as done with, and so given back to the peer's limit on the
        -- unidirectional streams it may open.  One left unclosed is lost to
        -- it for good.
        Nothing -> closeStream strm
        Just i -> case toH3StreamType i of
            -- RFC 9114, section 6.2: "Recipients of unknown stream types MUST
            -- either abort reading of the stream or discard incoming data
            -- without further processing.  If reading is aborted, the
            -- recipient SHOULD use the H3_STREAM_CREATION_ERROR error code".
            -- Neither was done: whatever the peer sent was left where it
            -- arrived, for as long as the connection lasted.
            H3StreamTypeUnknown _ -> do
                stopStream strm H3StreamCreationError
                closeStream strm
            styp -> ctxUniSwitch styp (recvStream strm)

withHandle :: Context -> (TM.Handle -> IO ()) -> IO ()
withHandle Context{..} action = void $ TM.withHandle ctxTimeoutManager (return ()) action

newStream :: Context -> IO Stream
newStream Context{..} = stream ctxConnection

pReadMaker :: Context -> PositionReadMaker
pReadMaker = ctxPReadMaker

forkManaged :: Context -> String -> IO () -> IO ()
forkManaged Context{..} label =
    T.forkManaged ctxThreadManager label . endingOnConnectionError

forkManagedTimeout :: Context -> String -> (TM.Handle -> IO ()) -> IO ()
forkManagedTimeout Context{..} label body =
    T.forkManagedTimeout ctxThreadManager ctxTimeoutManager label $
        endingOnConnectionError . body

forkManagedTimeoutFinally
    :: Context -> String -> (TM.Handle -> IO ()) -> IO () -> IO ()
forkManagedTimeoutFinally Context{..} label body =
    T.forkManagedTimeoutFinally ctxThreadManager ctxTimeoutManager label $
        endingOnConnectionError . body

-- | A thread that has sent a connection error ('connectionError') ends there
-- quietly: the connection is going, and the error has been told to the peer.
--
-- So does one waiting on a connection the peer has closed with no error:
-- quic tells the wait with 'ConnectionIsClosed'.  The thread reading the
-- peer's control stream used to die of it, reported as uncaught.
endingOnConnectionError :: IO () -> IO ()
endingOnConnectionError io =
    io `E.catch` \e -> case e of
        ApplicationProtocolErrorIsSent _ _ -> return ()
        ConnectionIsClosed _ -> return ()
        _ -> E.throwIO e

-- | Closing the connection with this error, and stopping here: see
-- 'connectionError'.
abort :: Context -> ApplicationProtocolError -> IO a
abort ctx = connectionError (ctxConnection ctx)

getHooks :: Context -> Hooks
getHooks = ctxHooks

getMySockAddr :: Context -> SockAddr
getMySockAddr = ctxMySockAddr

getPeerSockAddr :: Context -> SockAddr
getPeerSockAddr = ctxPeerSockAddr

getMaxFieldSectionSize :: Context -> Int
getMaxFieldSectionSize = ctxMaxFieldSectionSize

getPeerMaxFieldSectionSize :: Context -> IO Int
getPeerMaxFieldSectionSize = ctxPeerMaxFieldSectionSize

----------------------------------------------------------------
-- GOAWAY of ours (RFC 9114, section 5.2)

-- | Taking a request stream, unless our GOAWAY has gone out at or below
--   it.  One that is not taken must be rejected, not processed.
admitRequest :: Context -> StreamId -> IO Bool
admitRequest Context{..} sid = atomically $ do
    lim <- readTVar ctxGoAway
    if maybe False (sid >=) lim
        then return False
        else do
            modifyTVar' ctxLastRequest $ Just . maybe sid (max sid)
            modifyTVar' ctxInFlight $ IntSet.insert sid
            return True

-- | Done with a request stream taken by 'admitRequest'.
requestDone :: Context -> StreamId -> IO ()
requestDone Context{..} sid = atomically $ modifyTVar' ctxInFlight $ IntSet.delete sid

-- | Sending GOAWAY once, with the first request stream not taken: the
--   streams taken so far are still answered.
goAway :: Context -> IO ()
goAway ctx@Context{..} = do
    mlim <- atomically $ do
        old <- readTVar ctxGoAway
        case old of
            Just _ -> return Nothing
            Nothing -> do
                lim <- maybe 0 (+ 4) <$> readTVar ctxLastRequest
                writeTVar ctxGoAway $ Just lim
                return $ Just lim
    forM_ mlim $ sendGoAway ctx

-- | The first of two GOAWAYs: the largest identifier, which stops the
--   client sending requests and takes every one already on its way.
goAwayAll :: Context -> IO ()
goAwayAll ctx = sendGoAway ctx $ 2 ^ (62 :: Int) - 4

-- | Waiting for the connection's probe timeout, which is at least a round
--   trip.
waitPTO :: Context -> IO ()
waitPTO Context{..} = do
    Microseconds pto <- getPTO $ connLDCC ctxConnection
    threadDelay pto

sendGoAway :: Context -> StreamId -> IO ()
sendGoAway Context{..} lim = do
    payload <- withWriteBuffer 8 $ \wbuf -> encodeInt' wbuf $ fromIntegral lim
    ctxSendControl [H3Frame H3FrameGoaway payload]

-- | Waiting until our GOAWAY is out and no request taken is left.
drained :: Context -> STM ()
drained Context{..} = do
    readTVar ctxGoAway >>= check . isJust
    readTVar ctxInFlight >>= check . IntSet.null