packages feed

http3-0.1.6: 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.UTF8 as UTF8
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

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'
    E.bracket (newStream ctx) closeStream $ \strm -> do
        forkManagedTimeout ctx "H3 client: sendRequest" $ \th ->
            ( do
                sendHeader ctx strm th hdr'
                sendBody ctx strm th outobj
                QUIC.shutdownStream strm
            )
                -- 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 <- recvHeader ctx sid 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
                    let rsp = Response $ InpObj vt Nothing readB refH
                    processResponse rsp
  where
    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