packages feed

http3-0.1.10: test/QPACK/QIF2Spec.hs

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

module QPACK.QIF2Spec where

import Control.Concurrent
import qualified Control.Exception as E
import Control.Monad
import qualified Data.ByteString as B
import Data.ByteString.Builder (Builder, byteString)
import qualified Data.ByteString.Char8 as C8
import Data.CaseInsensitive hiding (map)
import Data.List
import Data.Maybe
import Network.HTTP.Semantics
import Network.HTTP.Semantics.Client.Internal
import Network.HTTP.Semantics.Server
import Network.HTTP.Types (RequestHeaders, ok200)
import qualified Network.HTTP3.Client as C
import Network.HTTP3.Server
import qualified Network.HTTP3.Server as S
import qualified Network.QUIC.Client as QUIC
import System.IO
import Test.Hspec

import HTTP3.Config
import HTTP3.Server hiding (server)
import QIF

qiffile :: FilePath
qiffile = "qifs/qifs/fb-req-hq.qif"

spec :: Spec
spec = do
    describe "QIF server" $ do
        it "handles dynamic table of 0-bytes" $ do
            let size = 0
            E.bracket (setup server size) teardown $
                \_ -> runClient size
    describe "QIF server" $ do
        it "handles dynamic table of 256-bytes" $ do
            let size = 256
            E.bracket (setup server size) teardown $
                \_ -> runClient size
    describe "QIF server" $ do
        it "handles dynamic table of 512-bytes" $ do
            let size = 512
            E.bracket (setup server size) teardown $
                \_ -> runClient size
    describe "QIF server" $ do
        it "handles dynamic table of 4096-bytes" $ do
            let size = 4096
            E.bracket (setup server size) teardown $
                \_ -> runClient size

runClient :: Int -> IO ()
runClient siz = QUIC.run testClientConfig $ \conn ->
    E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do
        let conf =
                conf0
                    { S.confQEncoderConfig = S.defaultQEncoderConfig{S.ecMaxTableCapacity = siz}
                    , S.confQDecoderConfig = S.defaultQDecoderConfig{S.dcMaxTableCapacity = siz}
                    }

        withFile qiffile ReadMode $ \hdl ->
            C.run conn testH3ClientConfig conf $ client hdl

client :: Handle -> (C.Request -> (C.Response -> IO ()) -> IO a) -> p -> IO ()
client hdl sendRequest _aux = do
    -- delay for set table capacity
    threadDelay 10000
    ms <- forkClient 0 id
    -- A failure in a request's thread is brought back here.  It used to
    -- kill the thread before it said it was done, and the test hung.
    mapM takeMVar ms >>= mapM_ (either E.throwIO return)
  where
    forkClient n build = do
        hs <- headerlist hdl
        if null hs
            then return $ build []
            else do
                var <- newEmptyMVar
                _ <- forkIO $ do
                    let req = requestBuilder2 hs mempty
                    r <- E.try @E.SomeException $ sendRequest req $ checkHeader hs
                    putMVar var $ void r
                when ((n :: Int) `mod` 50 == 0) $ threadDelay 100000
                forkClient (n + 1) (build . (var :))
    checkHeader hs rsp = do
        bs <- getBody
        -- Every field but the pseudo-header ones, which the client may
        -- add to or rewrite.  This compared fields named "cookie:", which
        -- there are none of, and so compared two empty lists.
        -- Cookies are split into crumbs on both sides: the server joins
        -- them with "; " (RFC 9114, section 4.2.1).
        let regular = sort . concatMap crumbs . filter ((/= ":") . B.take 1 . foldedCase . fst)
            crumbs (k, v)
                | k == "cookie" = map ((,) k) $ splitOn "; " v
                | otherwise = [(k, v)]
            hs0 = map keyValue $ split bs
        regular hs0 `shouldBe` regular hs
      where
        getBody = B.concat <$> loop id
        loop build = do
            bs <- C.getResponseBodyChunk rsp
            if bs == ""
                then return $ build []
                else loop (build . (bs :))
        split ls0 = go ls0 id
          where
            go "" build = build []
            go ls build = go ls'' (build . (l :))
              where
                (l, ls') = C8.break (== '\n') ls
                ls'' = C8.drop 1 ls'
        splitOn sep s = case B.breakSubstring sep s of
            (x, "") -> [x]
            (x, rest) -> x : splitOn sep (B.drop (B.length sep) rest)
        keyValue kv = (mk k, v)
          where
            (k, v') = C8.break (== ' ') kv
            v = C8.drop 1 v'

-- | Answering with the fields of the request, whatever its method: the QIF
-- file has POSTs as well as GETs.
server :: Server
server req _aux sendResponse = sendResponse (responseSection req) []

responseSection :: S.Request -> S.Response
responseSection req = responseBuilder ok200 header body
  where
    header =
        [ ("Content-Type", "text/plain")
        , ("Server", "HaskellQuic/0.0.0")
        ]
    (thl, vt) = requestHeaders req
    pseudos = [tokenAuthority, tokenMethod, tokenPath, tokenScheme]
    pthl = map (\t -> (t, fromJust (getFieldValue t vt))) pseudos
    body =
        foldr1 (<>) $
            map (\(k, v) -> byteString (k <> " " <> v <> "\n")) $
                map (\(k, v) -> (tokenFoldedKey k, v)) (pthl ++ thl)

-- | Creating request with builder only from headers
--   where :path and :method are included.
requestBuilder2 :: RequestHeaders -> Builder -> C.Request
requestBuilder2 hdr builder = Request $ OutObj hdr (OutBodyBuilder builder) defaultTrailersMaker