packages feed

http3-0.1.5: test/HTTP3/Server.hs

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

module HTTP3.Server (
    setup,
    server,
    countingServer,
    teardown,
    trailersMaker,
    firstTrailerValue,
    CH.hashInit,
) where

import Control.Concurrent

-- cryptonite

import qualified Control.Exception as E
import Control.Monad
import Crypto.Hash (Context, SHA1)
import qualified Crypto.Hash as CH
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Builder (byteString)
import qualified Data.ByteString.Char8 as C8
import Data.IORef
import Data.IP ()
import Network.HPACK
import Network.HTTP.Types
import Network.HTTP3.Server
import qualified Network.QUIC.Server as QUIC
import Network.Socket ()
import Test.Hspec

import HTTP3.Config

setup :: Server -> Int -> IO ThreadId
setup svr siz = do
    sc <- makeTestServerConfig
    tid <- forkIO $ QUIC.run sc loop
    threadDelay 500000 -- give enough time to the server
    return tid
  where
    loop conn = E.bracket allocSimpleConfig freeSimpleConfig $ \conf0 -> do
        let conf =
                conf0
                    { confQEncoderConfig = defaultQEncoderConfig{ecMaxTableCapacity = siz}
                    , confQDecoderConfig = defaultQDecoderConfig{dcMaxTableCapacity = siz}
                    }

        run conn conf svr

teardown :: ThreadId -> IO ()
teardown tid = killThread tid

-- | 'server', but counting how many times it is handed a request.
--
-- A request the server has already rejected must not reach the application at
-- all, and the only place that shows is here.
countingServer :: IORef Int -> Server
countingServer ref req aux sendResponse = do
    atomicModifyIORef' ref $ \n -> (n + 1, ())
    server req aux sendResponse

server :: Server
server req aux sendResponse = case requestMethod req of
    Just "GET" -> case requestPath req of
        Just "/" -> sendResponse responseHello []
        Just "/sockaddr" -> sendResponse (responseSockAddr aux) []
        _ -> sendResponse response404 []
    Just "POST" -> case requestPath req of
        Just "/echo" -> sendResponse (responseEcho req) []
        -- Reads the body and says nothing about it, so that a test can be
        -- about what reading the body does rather than about trailers.
        Just "/drain" -> do
            let loop = do
                    bs <- getRequestBodyChunk req
                    unless (B.null bs) loop
            loop
            sendResponse responseHello []
        _ -> sendResponse responseHello []
    _ -> sendResponse response405 []

responseHello :: Response
responseHello = responseBuilder ok200 header body
  where
    header = [("Content-Type", "text/plain")]
    body = byteString "Hello, world!\n"

-- | Hands back the two addresses the server was given, so that a test can
-- check they are not the same one twice over.
responseSockAddr :: Aux -> Response
responseSockAddr aux = responseBuilder ok200 header body
  where
    header = [("Content-Type", "text/plain")]
    body =
        byteString $
            C8.pack $
                show (auxMySockAddr aux) ++ " " ++ show (auxPeerSockAddr aux)

response404 :: Response
response404 = responseNoBody notFound404 []

response405 :: Response
response405 = responseNoBody methodNotAllowed405 []

responseEcho :: Request -> Response
responseEcho req = setResponseTrailersMaker h2rsp maker
  where
    h2rsp = responseStreaming ok200 header streamingBody
    header = [("Content-Type", "text/plain")]
    streamingBody write _flush = do
        loop
        mt <- getRequestTrailers req
        firstTrailerValue <$> mt
            `shouldBe` Just "b0870457df2b8cae06a88657a198d9b52f8e2b0a"
      where
        loop = do
            bs <- getRequestBodyChunk req
            unless (B.null bs) $ do
                void $ write $ byteString bs
                loop
    maker = trailersMaker CH.hashInit

-- Strictness is important for Context.
trailersMaker :: Context SHA1 -> Maybe ByteString -> IO NextTrailersMaker
trailersMaker ctx Nothing = return $ Trailers [("X-SHA1", sha1)]
  where
    sha1 = C8.pack $ show $ CH.hashFinalize ctx
trailersMaker ctx (Just bs) = return $ NextTrailersMaker $ trailersMaker ctx'
  where
    ctx' = CH.hashUpdate ctx bs

firstTrailerValue :: TokenHeaderTable -> FieldValue
firstTrailerValue tbl = case fst tbl of
    [] -> error "firstTrailerValue"
    x : _ -> snd x