packages feed

http3-0.1.12: Network/HTTP3/Send.hs

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

module Network.HTTP3.Send (
    sendHeader,
    checkHeaderSize,
    sendBody,
    closeSentinel,
) where

import qualified Control.Exception as E
import qualified Data.ByteString.Builder.Extra as B
import qualified Data.ByteString.Internal as BS
import Data.IORef
import Foreign.ForeignPtr
import Network.HTTP.Semantics.Client
import Network.HTTP.Semantics.IO
import qualified Network.HTTP.Types as HT
import Network.QUIC
import qualified System.TimeManager as T

import Imports
import Network.HTTP3.Context
import Network.HTTP3.Error
import Network.HTTP3.Frame
import Network.QPACK

-- | Refusing a header section the peer said it would not take, before
--   anything is opened or sent.
--
-- The encoder refuses it too, but on a client that happens in the thread
-- sending the request, which has nobody to tell.
checkHeaderSize :: Context -> HT.RequestHeaders -> IO ()
checkHeaderSize ctx hdrs = do
    (ths, _) <- toTokenHeaderTable hdrs
    lim <- getPeerMaxFieldSectionSize ctx
    let siz = fieldSectionSize ths
    when (siz > lim) $ E.throwIO $ FieldSectionTooLargeForPeer siz lim

sendHeader :: Context -> Stream -> T.Handle -> HT.ResponseHeaders -> IO ()
sendHeader ctx strm th hdrs = do
    -- fixme: fixHeaders
    (ths, _) <- toTokenHeaderTable hdrs
    hdr <- qpackEncode ctx (streamId strm) ths
    let frames = [H3Frame H3FrameHeaders hdr]
        frames' = onHeadersFrameCreated (getHooks ctx) frames
        bss = encodeH3Frames frames'
    sendStreamMany strm bss
    T.tickle th

-- | Sending the body and its trailers.  'False' when the application
--   cancelled the stream ('outBodyCancel'), which has reset it: the caller
--   must not end it with a FIN.
sendBody :: Context -> Stream -> T.Handle -> OutObj -> IO Bool
sendBody ctx strm th outobj = case outObjBody outobj of
    OutBodyNone -> return True
    OutBodyFile (FileSpec path fileoff bytecount) -> do
        (pread, sentinel) <- pReadMaker ctx path
        let next = fillFileBodyGetNext pread fileoff bytecount sentinel
        True <$ sendNext ctx strm th next tlrmkr `E.onException` closeSentinel sentinel
    OutBodyBuilder builder -> do
        let next = fillBuilderBodyGetNext builder
        True <$ sendNext ctx strm th next tlrmkr
    OutBodyStreaming strmbdy ->
        sendStreaming
            ctx
            strm
            th
            tlrmkr
            (\iface -> outBodyUnmask iface $ strmbdy (outBodyPush iface) (outBodyFlush iface))
    OutBodyStreamingIface strmbdy -> sendStreaming ctx strm th tlrmkr strmbdy
  where
    tlrmkr = outObjTrailers outobj

sendNext :: Context -> Stream -> T.Handle -> DynaNext -> TrailersMaker -> IO ()
sendNext ctx strm th curr tlrmkr0 = do
    (bs, mnext, tlrmkr) <- newByteStringWith tlrmkr0 curr
    when (bs /= "") $ encodeH3Frame (H3Frame H3FrameData bs) >>= sendStream strm
    T.tickle th
    case mnext of
        Nothing -> do
            tm <- tlrmkr Nothing
            let trailers = case tm of
                    Trailers t -> t
                    _ -> []
            unless (null trailers) $ sendHeader ctx strm th trailers
        Just next -> sendNext ctx strm th next tlrmkr

newByteStringWith
    :: TrailersMaker -> DynaNext -> IO (ByteString, Maybe DynaNext, TrailersMaker)
newByteStringWith tlrmkr0 action = do
    fp <- BS.mallocByteString fileChunkSize
    Next len _reqflush mnext1 <- withForeignPtr fp $ \buf -> action buf fileChunkSize
    if len == 0
        -- Nothing this time, but not necessarily the end: there may be a
        -- next.  This used to end the body here.
        then return ("", mnext1, tlrmkr0)
        else do
            let bs = PS fp 0 len
            tlrmkr1 <- nextMaker <$> tlrmkr0 (Just bs)
            return (bs, mnext1, tlrmkr1)

-- | Closing a file whose body was not sent to its end.  Only the end closed
--   it, so a failure or a reset partway left the descriptor open.  'hClose',
--   which the default closer is, may be called again.
closeSentinel :: Sentinel -> IO ()
closeSentinel (Closer close) = close
closeSentinel (Refresher _) = return ()

-- | The most of a file or builder body put in one DATA frame.  It was 2048,
--   so a large body went out in many small frames, one 'sendStream' each.
fileChunkSize :: Int
fileChunkSize = 16384

-- | The trailers maker to go on with after a chunk of the body.
--
-- One that answers with the trailers before the end keeps them for the end.
-- That was taken to be impossible, and failed with a pattern match; or the
-- trailers were dropped for none.
nextMaker :: NextTrailersMaker -> TrailersMaker
nextMaker (NextTrailersMaker t) = t
nextMaker tm@(Trailers _) = \_ -> return tm

newByteStringAndSend
    :: Stream
    -> T.Handle
    -> TrailersMaker
    -> B.BufferWriter
    -> IO (B.Next, TrailersMaker)
newByteStringAndSend strm th tlrmkr0 action = do
    fp <- BS.mallocByteString 2048
    (len, signal) <- withForeignPtr fp $ \buf -> action buf 2048
    if len == 0
        then return (signal, tlrmkr0)
        else do
            let bs = PS fp 0 len
            tlrmkr1 <- nextMaker <$> tlrmkr0 (Just bs)
            encodeH3Frame (H3Frame H3FrameData bs) >>= sendStream strm
            T.tickle th
            return (signal, tlrmkr1)

sendStreaming
    :: Context
    -> Stream
    -> T.Handle
    -> TrailersMaker
    -> (OutBodyIface -> IO ())
    -> IO Bool
sendStreaming ctx strm th tlrmkr0 strmbdy = do
    ref <- newIORef tlrmkr0
    cancelled <- newIORef False
    E.mask $ \unmask -> do
        let iface =
                OutBodyIface
                    { outBodyUnmask = unmask
                    , outBodyPush = write ref
                    , outBodyPushFinal = write ref -- fixme
                    , outBodyFlush = flush
                    , outBodyCancel = cancel cancelled
                    }
        strmbdy iface
    done <- not <$> readIORef cancelled
    when done $ do
        tlrmkr <- readIORef ref
        tm <- tlrmkr Nothing
        let trailers = case tm of
                Trailers t -> t
                _ -> []
        unless (null trailers) $ sendHeader ctx strm th trailers
    return done
  where
    -- fixme: how to implement flush?
    flush = return ()
    -- The stream is reset, with H3_INTERNAL_ERROR for an exception and
    -- H3_REQUEST_CANCELLED otherwise, as HTTP/2 does with RST_STREAM.
    -- It did nothing, and the body cut short was ended with a FIN.
    cancel cancelled merr = do
        writeIORef cancelled True
        resetStream strm $ maybe H3RequestCancelled (const H3InternalError) merr
    write ref builder = do
        tlrmkr1 <- readIORef ref
        tlrmkr2 <- newByteStringAndSend strm th tlrmkr1 (B.runBuilder builder) >>= loop
        writeIORef ref tlrmkr2
      where
        loop (B.Done, tlrmkr1) = return tlrmkr1
        loop (B.More _ writer, tlrmkr1) =
            newByteStringAndSend strm th tlrmkr1 writer >>= loop
        loop (B.Chunk bs writer, tlrmkr1) = do
            encodeH3Frame (H3Frame H3FrameData bs) >>= sendStream strm
            tlrmkr2 <- nextMaker <$> tlrmkr1 (Just bs)
            T.tickle th
            newByteStringAndSend strm th tlrmkr2 writer >>= loop