packages feed

http3-0.1.8: Network/HTTP3/Client.hs

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

-- | A client library for HTTP/3.
module Network.HTTP3.Client (
    -- * Runner
    run,

    -- * Client configration
    ClientConfig,
    defaultClientConfig,
    scheme,
    authority,

    -- * Common configuration
    Config (..),
    defaultConfig,
    allocSimpleConfig,
    freeSimpleConfig,
    defaultQEncoderConfig,
    ecMaxTableCapacity,
    ecHeaderBlockBufferSize,
    ecInstructionBufferSize,
    defaultQDecoderConfig,
    dcMaxTableCapacity,
    dcHuffmanBufferSize,
    Hooks (..),
    defaultHooks,

    -- * HTTP semantics
    module Network.HTTP.Semantics.Client,
) where

import Control.Concurrent
import qualified Control.Exception as E
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.UTF8 as UTF8
import Data.IORef
import Data.IP (IPv6)
import Network.HTTP.Semantics.Client
import Network.HTTP.Semantics.Client.Internal
import Network.QUIC (Connection)
import qualified Network.QUIC as QUIC
import qualified Network.QUIC.Internal as QUIC
import Text.Read (readMaybe)

import Imports
import Network.HTTP3.Config
import Network.HTTP3.Context
import Network.HTTP3.Error
import Network.HTTP3.Recv
import Network.HTTP3.Send
import Network.QPACK

-- | Configuration for HTTP\/3 or HQ client. For HQ, 'authority' is
--   not used and an server's IP address is used in 'Request'.
data ClientConfig = ClientConfig
    { scheme :: Scheme
    , authority :: Authority
    }

defaultClientConfig :: ClientConfig
defaultClientConfig =
    ClientConfig
        { scheme = "https"
        , authority = "localhost"
        }

-- | Running an HTTP\/3 client.
run :: Connection -> ClientConfig -> Config -> Client a -> IO a
run conn ClientConfig{..} conf client = withContext conn conf $ \ctx -> do
    forkManaged ctx "H3 client: readerClient" $ readerClient ctx
    client (sendRequest ctx scheme authority) aux
  where
    aux =
        defaultAux
            { auxPossibleClientStreams = QUIC.possibleMyStreams conn
            , auxSendPing = QUIC.sendFrames conn QUIC.RTT1Level [QUIC.Ping]
            }

readerClient :: Context -> IO ()
readerClient ctx = loop
  where
    loop = do
        accept ctx >>= process
        loop
    process strm
        | QUIC.isClientInitiatedUnidirectional sid = return () -- error
        | QUIC.isClientInitiatedBidirectional sid = return ()
        | QUIC.isServerInitiatedUnidirectional sid =
            forkManaged ctx "H3 client: unidirectional handler" $
                unidirectional ctx strm
        -- Server-initiated bidirectional.  RFC 9114, section 6.1: "Clients
        -- MUST treat receipt of a server-initiated bidirectional stream as a
        -- connection error of type H3_STREAM_CREATION_ERROR unless such an
        -- extension has been negotiated."
        | otherwise = abort ctx H3StreamCreationError
      where
        sid = QUIC.streamId strm

-- | The header section of the final response, past any informational (1xx)
-- ones before it.
--
-- RFC 9114, section 4.1: a response may begin with any number of
-- informational responses, each a header section of its own, before the
-- final one.  The first header section was taken for the response: after a
-- 103 (Early Hints) or a 100 (Continue), the client saw that, and the body
-- reader took the final response's HEADERS for trailers and ended the body
-- there, the final response lost.  They are skipped now, as the application
-- has no way here to be handed them.
--
-- RFC 9114 sets no limit on how many there may be, and a server could go on
-- sending them for as long as it liked, the request never answered.  More
-- than 'maxInformational' reset the stream with H3_EXCESSIVE_LOAD: a server
-- has no use for more than a couple -- a 100 and a 103, say.
recvFinalHeader
    :: Context -> QUIC.Stream -> Source -> IO (Maybe TokenHeaderTable)
recvFinalHeader ctx strm src = loop (0 :: Int)
  where
    sid = QUIC.streamId strm
    loop n = do
        mvt <- recvHeader ctx sid src
        case mvt of
            Just (_, vt)
                | Just status <- getFieldValue tokenStatus vt
                , informational status ->
                    if n < maxInformational
                        then loop (n + 1)
                        else do
                            QUIC.resetStream strm H3ExcessiveLoad
                            E.throwIO $
                                QUIC.ApplicationProtocolErrorIsSent
                                    H3ExcessiveLoad
                                    "too many informational responses"
            _ -> return mvt
    informational status = C8.length status == 3 && C8.head status == '1'

-- | How many informational responses 'recvFinalHeader' takes before the
--   final one.
maxInformational :: Int
maxInformational = 10

sendRequest
    :: Context -> Scheme -> Authority -> Request -> (Response -> IO a) -> IO a
sendRequest ctx scm auth (Request outobj) processResponse = do
    -- RFC 9114, section 4.2.2: an endpoint "SHOULD NOT send an HTTP message
    -- header that exceeds the indicated size".  Here, so that the caller is
    -- the one told.
    checkHeaderSize ctx hdr'
    sent <- newIORef False
    E.bracket (newStream ctx) (done sent) $ \strm -> do
        forkManagedTimeout ctx "H3 client: sendRequest" $ \th ->
            ( do
                sendHeader ctx strm th hdr'
                sendBody ctx strm th outobj
                QUIC.shutdownStream strm
                writeIORef sent True
            )
                -- What goes wrong here is thrown away with the thread, and
                -- the request would be left half sent, with the response
                -- awaited for good.  Trailers too large for the peer are one
                -- such thing.
                `E.catch` \se -> case E.fromException se of
                    _ | isAsyncException se -> E.throwIO se
                    -- The sending part is closed: the server asked us with
                    -- STOP_SENDING to stop, and it has had its RESET_STREAM
                    -- for that already, or we are done with the stream.
                    -- The response may be on its way yet, and resetting
                    -- here takes the stream out of the table, after which
                    -- whatever arrives for it is thrown away: the response
                    -- would be awaited for good.
                    Just QUIC.StreamIsClosed -> return ()
                    _ -> QUIC.resetStream strm H3RequestCancelled
        src <- newSource strm
        let sid = QUIC.streamId strm
        (`E.finally` cancelUnlessReadToEnd ctx sid src) $ do
            mvt <- recvFinalHeader ctx strm src
            case mvt of
                Nothing -> do
                    QUIC.resetStream strm H3MessageError
                    threadDelay 100000
                    -- just for type inference
                    E.throwIO $ QUIC.ApplicationProtocolErrorIsSent H3MessageError ""
                Just vt
                    -- Malformed (RFC 9114, section 4.2).
                    | Just name <- connectionSpecificField vt -> do
                        QUIC.resetStream strm H3MessageError
                        E.throwIO $ ConnectionSpecificField name
                Just vt@(_, valtbl) -> do
                    (readB, refH) <-
                        newBodyReader ctx sid src valtbl $ noContent valtbl
                    let rsp = Response $ InpObj vt Nothing readB refH
                    processResponse rsp
  where
    -- A response defined to have no content: to HEAD, a 204 or a 304, or a
    -- 2xx to CONNECT (RFC 9110, section 6.4.1).  Its content-length, which it
    -- may carry, describes content it does not have; it was checked against
    -- the empty body all the same, and the client refused such a response
    -- with ContentLengthMismatch.
    noContent valtbl =
        method == Just "HEAD"
            || status `elem` [Just "204", Just "304"]
            || (method == Just "CONNECT" && maybe False ("2" `C8.isPrefixOf`) status)
      where
        status = getFieldValue tokenStatus valtbl
    method = snd <$> find ((== ":method") . fst) hdr
    -- Done with the stream: the response has been processed, or waiting for
    -- it was given up -- 'timeout', say.  A request sent in full has had its
    -- FIN, and closing the stream only tidies up.  One that has not is cut
    -- short, and is reset (H3_REQUEST_CANCELLED).  'closeStream' used to be
    -- called either way, and it ends the sending part with a FIN: the server
    -- took a request body cut short for a whole one.  The sender, if it is
    -- still at it, finds the stream closed and stops.
    done sent strm = do
        complete <- readIORef sent
        if complete
            then closeStream strm
            else QUIC.resetStream strm H3RequestCancelled
    hdr = outObjHeaders outobj
    isIPv6 = isJust (readMaybe auth :: Maybe IPv6)
    auth'
        | isIPv6 = "[" <> UTF8.fromString auth <> "]"
        | otherwise = UTF8.fromString auth
    hdr' = addIfNotExist (":scheme", scm) $ addIfNotExist (":authority", auth') hdr
    addIfNotExist keyVal@(key, _) hs = case find (\(k, _) -> k == key) hs of
        Nothing -> keyVal : hs
        Just _ -> hs