packages feed

http2-tls-0.5.8: Network/HTTP2/TLS/Server.hs

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

module Network.HTTP2.TLS.Server (
    -- * Runners
    run,
    runWithSocket,
    runH2C,
    runH2CWithSocket,
    Server,
    HostName,
    PortNumber,
    runTLS,
    runTLSWithSocket,

    -- * Settings
    Settings,
    defaultSettings,
    settingsTimeout,
    settingsSendBufferSize,
    settingsSlowlorisSize,
    settingsReadBufferSize,
    settingsReadBufferLowerLimit,
    settingsKeyLogger,
    settingsNumberOfWorkers,
    settingsConcurrentStreams,
    settingsConnectionWindowSize,
    settingsStreamWindowSize,
    settingsSessionManager,
    settingsEarlyDataSize,

    -- ** Rate limits
    settingsPingRateLimit,
    settingsEmptyFrameRateLimit,
    settingsSettingsRateLimit,
    settingsRstRateLimit,
    settingsInstallShutdownHandler,
    settingsUnsafeClose,

    -- * IO backend
    IOBackend,
    send,
    sendWith,
    sendMany,
    recv,
    recvWith,
    requestSock,
    mySockAddr,
    peerSockAddr,

    -- * Internal
    runIO,
    runIOH2C,
    Stream,
    ServerIO (..),
) where

import Control.Concurrent.STM (STM, atomically, check, newTVarIO, readTVar, retry, writeTVar)
import qualified Control.Exception as E
import Data.ByteString (ByteString)
import Network.HTTP2.Client (HTTP2Error (..))
import Network.HTTP2.Server (
    Server,
    connectionWindowSize,
    defaultServerConfig,
    emptyFrameRateLimit,
    initialWindowSize,
    maxConcurrentStreams,
    pingRateLimit,
    rstRateLimit,
    settings,
    settingsRateLimit,
 )
import qualified Network.HTTP2.Server as H2Server
import Network.HTTP2.Server.Internal (ServerIO, Stream)
import qualified Network.HTTP2.Server.Internal as H2I
import Network.Run.TCP.Timeout hiding (ServerSettings (..))
import qualified Network.Run.TCP.Timeout as TCP
import Network.Socket (
    HostName,
    PortNumber,
    Socket,
 )
import Network.TLS hiding (HostName)
import qualified System.TimeManager as T

import Network.HTTP2.TLS.Config
import Network.HTTP2.TLS.IO
import Network.HTTP2.TLS.Server.Settings
import Network.HTTP2.TLS.Supported

-- | Running a TLS client.
--   'IOBackend' provides sending and receiving functions
--   with timeout based on 'Settings'.
runTLS
    :: Settings
    -> Credentials
    -> HostName
    -> PortNumber
    -> ByteString
    -- ^ ALPN
    -> (T.Manager -> IOBackend -> IO ())
    -> IO ()
runTLS settings creds host port alpn action =
    runTCPServerWithSettings
        (tcpSettings settings)
        (settingsTimeout settings)
        (Just host)
        (show port)
        $ \mgr th sock -> do
            giveUpVar <- newTVarIO retry
            recvGiveUpVar <- newTVarIO retry
            backend <- mkBackend settings giveUpVar recvGiveUpVar sock
            ctx <- contextNew backend params
            handshake ctx
            iobackend <- timeoutIOBackend th settings <$> tlsIOBackend giveUpVar recvGiveUpVar ctx sock
            action mgr iobackend
            byeGivingUp ctx
  where
    params = getServerParams settings creds alpn

-- | Running a TLS client.
--   'IOBackend' provides sending and receiving functions
--   with timeout based on 'Settings'.
runTLSWithSocket
    :: Settings
    -> Credentials
    -> Socket
    -> ByteString
    -- ^ ALPN
    -> (T.Manager -> IOBackend -> IO ())
    -> IO ()
runTLSWithSocket settings creds s alpn action =
    runTCPServerWithSocketAndSettings
        (tcpSettings settings)
        (settingsTimeout settings)
        s
        $ \mgr th sock -> do
            giveUpVar <- newTVarIO retry
            recvGiveUpVar <- newTVarIO retry
            backend <- mkBackend settings giveUpVar recvGiveUpVar sock
            ctx <- contextNew backend params
            handshake ctx
            iobackend <- timeoutIOBackend th settings <$> tlsIOBackend giveUpVar recvGiveUpVar ctx sock
            r <- action mgr iobackend
            byeGivingUp ctx
            return r
  where
    params = getServerParams settings creds alpn

-- | Running an HTTP\/2 client over TLS (over TCP).
--   ALPN is "h2".
run :: Settings -> Credentials -> HostName -> PortNumber -> Server -> IO ()
run settings0 creds host port server = do
    (settings, goAway) <- goingAway settings0
    runTLS (byHTTP2 settings) creds host port "h2" $ run' settings goAway server

-- | Running an HTTP\/2 client over TLS (over TCP).
--   ALPN is "h2".
runWithSocket :: Settings -> Credentials -> Socket -> Server -> IO ()
runWithSocket settings0 creds s server = do
    (settings, goAway) <- goingAway settings0
    runTLSWithSocket (byHTTP2 settings) creds s "h2" $ run' settings goAway server

-- | Running an HTTP\/2 client over TCP.
runH2C :: Settings -> HostName -> PortNumber -> Server -> IO ()
runH2C settings0 host port server = do
    (settings, goAway) <- goingAway settings0
    runTCPServerWithSettings
        (tcpSettings $ byHTTP2 settings)
        (settingsTimeout settings)
        (Just host)
        (show port)
        $ \mgr th sock -> do
            iobackend0 <- tcpIOBackend settings sock
            let iobackend = timeoutIOBackend th settings iobackend0
            run' settings goAway server mgr iobackend

-- | Running an HTTP\/2 client over TCP.
runH2CWithSocket :: Settings -> Socket -> Server -> IO ()
runH2CWithSocket settings0 s server = do
    (settings, goAway) <- goingAway settings0
    runTCPServerWithSocketAndSettings
        (tcpSettings $ byHTTP2 settings)
        (settingsTimeout settings)
        s
        $ \mgr th sock -> do
            iobackend0 <- tcpIOBackend settings sock
            let iobackend = timeoutIOBackend th settings iobackend0
            run' settings goAway server mgr iobackend

run' :: Settings -> STM () -> Server -> T.Manager -> IOBackend -> IO ()
run' settings0@Settings{..} goAway server mgr IOBackend{..} =
    E.bracket
        (allocConfigForServer settings0 mgr send sendWith recv recvWith mySockAddr peerSockAddr)
        freeConfigForServer
        (\conf -> endedNormally $ H2Server.runWith goAway sconf conf server)
  where
    sconf =
        defaultServerConfig
            { connectionWindowSize = settingsConnectionWindowSize
            , settings =
                (settings defaultServerConfig)
                    { initialWindowSize = settingsStreamWindowSize
                    , maxConcurrentStreams = Just settingsConcurrentStreams
                    , pingRateLimit = settingsPingRateLimit
                    , emptyFrameRateLimit = settingsEmptyFrameRateLimit
                    , settingsRateLimit = settingsSettingsRateLimit
                    , rstRateLimit = settingsRstRateLimit
                    }
            }

runIO
    :: Settings
    -> Credentials
    -> Socket
    -> (ServerIO Stream -> IO (IO ()))
    -> IO ()
runIO settings0 creds s action = do
    (settings, goAway) <- goingAway settings0
    runTLSWithSocket (byHTTP2 settings) creds s "h2" $ \mgr iobackend ->
        runIO' settings goAway action mgr iobackend

runIO'
    :: Settings
    -> STM ()
    -> (ServerIO Stream -> IO (IO ()))
    -> T.Manager
    -> IOBackend
    -> IO ()
runIO' settings0@Settings{..} goAway action mgr IOBackend{..} =
    E.bracket
        (allocConfigForServer settings0 mgr send sendWith recv recvWith mySockAddr peerSockAddr)
        freeConfigForServer
        (\conf -> endedNormally $ H2I.runIOWith goAway sconf conf action)
  where
    sconf =
        defaultServerConfig
            { connectionWindowSize = settingsConnectionWindowSize
            , settings =
                (settings defaultServerConfig)
                    { initialWindowSize = settingsStreamWindowSize
                    , maxConcurrentStreams = Just settingsConcurrentStreams
                    , pingRateLimit = settingsPingRateLimit
                    , emptyFrameRateLimit = settingsEmptyFrameRateLimit
                    , settingsRateLimit = settingsSettingsRateLimit
                    , rstRateLimit = settingsRstRateLimit
                    }
            }

runIOH2C
    :: Settings -> Socket -> (ServerIO Stream -> IO (IO ())) -> IO ()
runIOH2C settings0 s action = do
    (settings, goAway) <- goingAway settings0
    runTCPServerWithSocketAndSettings
        (tcpSettings $ byHTTP2 settings)
        (settingsTimeout settings)
        s
        $ \mgr th sock -> do
            iobackend0 <- tcpIOBackend settings sock
            let iobackend = timeoutIOBackend th settings iobackend0
            runIO' settings goAway action mgr iobackend

----------------------------------------------------------------

tcpSettings :: Settings -> TCP.ServerSettings
tcpSettings settings =
    TCP.defaultServerSettings
        { TCP.settingsInstallShutdownHandler = settingsInstallShutdownHandler settings
        , TCP.settingsUnsafeClose = settingsUnsafeClose settings
        }

-- | For the entry points which run http2, which waits for its threads.
byHTTP2 :: Settings -> Settings
byHTTP2 settings = settings{settingsUnsafeClose = True}

-- | Stopping the server also tells each HTTP\/2 connection to go away:
--   GOAWAY, and closing once the streams it has taken are done.
goingAway :: Settings -> IO (Settings, STM ())
goingAway settings = do
    var <- newTVarIO False
    let install stop =
            settingsInstallShutdownHandler settings $
                stop >> atomically (writeTVar var True)
    return
        (settings{settingsInstallShutdownHandler = install}, readTVar var >>= check)

-- | http2 ends a connection which has run its course with
--   'ConnectionIsClosed'.  Taken as returning, so that close_notify is
--   sent after it, as for any other action.
endedNormally :: IO () -> IO ()
endedNormally = E.handle $ \e -> case e of
    ConnectionIsClosed -> return ()
    _ -> E.throwIO e

----------------------------------------------------------------

getServerParams
    :: Settings
    -> Credentials
    -> ByteString
    -> ServerParams
getServerParams Settings{..} creds alpn =
    defaultParamsServer
        { serverSupported = supported
        , serverShared = shared
        , serverHooks = hooks
        , serverDebug = debug
        , serverEarlyDataSize = settingsEarlyDataSize
        }
  where
    shared =
        defaultShared
            { sharedCredentials = creds
            , sharedSessionManager = settingsSessionManager
            }
    supported = strongSupported
    hooks =
        defaultServerHooks
            { onALPNClientSuggest = Just $ selectALPN alpn
            }
    debug =
        defaultDebugParams
            { debugKeyLogger = settingsKeyLogger
            }

selectALPN :: ByteString -> [ByteString] -> IO ByteString
selectALPN key xs
    | key `elem` xs = return key
    | otherwise = return ""