packages feed

hprox-0.7.1: src/Network/HProx/Quic.hs

-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2026 Bin Jin. All Rights Reserved.

{-# LANGUAGE CPP #-}

module Network.HProx.Quic
  ( quicAddressPlan
  , quicAltSvc
#ifdef QUIC_ENABLED
  , buildQuicServerConfig
  , quicSessionManagerConfig
#endif
  , quicUse0RTT
  , runQuicAndTls
  ) where

import Data.ByteString.Char8       qualified as BS8
import Network.TLS                 qualified as TLS
import Network.Wai                 (Application)
import Network.Wai.Handler.Warp    (Settings)
import Network.Wai.Handler.WarpTLS (TLSSettings, runTLS)

import Network.HProx.Log

#ifdef QUIC_ENABLED
import Control.Concurrent.Async     (mapConcurrently_)
import Data.Default.Class           (def)
import Data.List                    (find)
import Data.Maybe                   (fromMaybe)
import Data.String                  (fromString)
import Network.QUIC.Internal        qualified as Q
import Network.TLS.SessionManager   qualified as SM
import Network.Wai.Handler.Warp     (setAltSvc)
import Network.Wai.Handler.WarpQUIC (runQUIC)
#endif

quicAltSvc :: Int -> BS8.ByteString
quicAltSvc port = BS8.concat ["h3=\":", BS8.pack $ show port, "\""]

quicAddressPlan :: Maybe String -> Int -> [(String, Int)]
-- Keep both wildcard sockets explicit: relying on an IPv6 wildcard socket
-- to receive IPv4 traffic depends on the host IPV6_V6ONLY default, while
-- QUIC creates one UDP socket per planned address and does not force that
-- socket option.
quicAddressPlan Nothing port     = [("0.0.0.0", port), ("::", port)]
quicAddressPlan (Just bind) port = [(bind, port)]

quicUse0RTT :: Bool
quicUse0RTT = False

runQuicAndTls
    :: Logger
    -> Maybe String
    -> Settings
    -> TLSSettings
    -> (Maybe String -> IO TLS.Credentials)
    -> Int
    -> Application
    -> IO ()
#ifdef QUIC_ENABLED
runQuicAndTls logger bind settings tlsSettings onSNI qport app = do
    logger INFO $ "bind to UDP port " <> toLogStr (fromMaybe "all interfaces" bind) <> ":" <> toLogStr qport
    sessionManager <- SM.newSessionManager quicSessionManagerConfig
    mapConcurrently_ ($ app)
        [ runQUIC (buildQuicServerConfig bind onSNI sessionManager qport) settings
        , runTLS tlsSettings (setAltSvc (quicAltSvc qport) settings)
        ]

alpn :: Q.Version -> [BS8.ByteString] -> IO BS8.ByteString
alpn _ protocols = return $ fromMaybe "" $ find (== "h3") protocols

buildQuicServerConfig
    :: Maybe String
    -> (Maybe String -> IO TLS.Credentials)
    -> TLS.SessionManager
    -> Int
    -> Q.ServerConfig
buildQuicServerConfig bind onSNI sessionManager port = Q.defaultServerConfig
    { Q.scAddresses      = [(fromString host, fromIntegral bindPort) | (host, bindPort) <- quicAddressPlan bind port]
    , Q.scVersions       = [Q.Version1, Q.Version2]
    , Q.scCredentials    = TLS.Credentials []
    , Q.scALPN           = Just alpn
    , Q.scTlsHooks       = def { TLS.onServerNameIndication = onSNI }
    , Q.scUse0RTT        = quicUse0RTT
    , Q.scSessionManager = sessionManager
    }

quicSessionManagerConfig :: SM.Config
quicSessionManagerConfig = SM.defaultConfig { SM.dbMaxSize = 0 }
#else
runQuicAndTls _ _ settings tlsSettings _ _ app = runTLS tlsSettings settings app
#endif