packages feed

spire-server-0.1.0.0: src/Spire/Server/H2.hs

{-# LANGUAGE OverloadedStrings #-}
-- | HTTP/2 server support via the @http2@ package.
--
-- Bridges the @http2@ library's callback-based server API to spire's
-- @Service IO (Request Body) (Response Body)@ abstraction. The same
-- spire Service that handles HTTP/1.1 requests handles HTTP/2 — no
-- code changes needed.
--
-- @
-- import Spire.Server.H2 (runServerH2, defaultH2Config)
--
-- main = runServerH2 (defaultH2Config 3000) myService
-- @
module Spire.Server.H2
  ( -- * Configuration
    H2Config (..)
  , defaultH2Config
    -- * Running
  , runServerH2
    -- * Bridge (for custom setups)
  , toH2Server
  ) where

import Control.Concurrent (forkIO)
import Control.Exception (bracket, catch, SomeException, finally)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as Builder
import qualified Data.CaseInsensitive as CI
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Network.HTTP.Types (status500, parseQuery, urlDecode)
import Network.Socket

import qualified Network.HTTP2.Server as H2
import Network.HTTP.Semantics (Token(..))

import Spire.Service (Service (..))
import Http.Core
import Http.Core.Body


-- | HTTP/2 server configuration.
data H2Config = H2Config
  { h2Port         :: !Int
  , h2Host         :: !Text
  , h2Workers      :: !Int
  , h2MaxStreams    :: !Int
  } deriving (Show)

-- | Sensible defaults for HTTP/2.
defaultH2Config :: Int -> H2Config
defaultH2Config port = H2Config
  { h2Port      = port
  , h2Host      = "127.0.0.1"
  , h2Workers   = 4
  , h2MaxStreams = 100
  }


-- | Run an HTTP/2 server (h2c — cleartext, no TLS).
--
-- Listens on the configured port and dispatches each HTTP/2 stream
-- to the spire Service.
runServerH2 :: H2Config -> Service IO (Request Body) (Response Body) -> IO ()
runServerH2 config svc = do
  let hints = defaultHints { addrSocketType = Stream }
  addr:_ <- getAddrInfo (Just hints)
    (Just (T.unpack (h2Host config)))
    (Just (show (h2Port config)))
  bracket (openSocket addr) close $ \sock -> do
    setSocketOption sock ReuseAddr 1
    bind sock (addrAddress addr)
    listen sock 128
    acceptLoopH2 config svc sock


acceptLoopH2 :: H2Config -> Service IO (Request Body) (Response Body) -> Socket -> IO ()
acceptLoopH2 config svc sock = do
  (conn, _addr) <- accept sock
  _ <- forkIO $ handleH2Connection config svc conn
  acceptLoopH2 config svc sock


handleH2Connection
  :: H2Config
  -> Service IO (Request Body) (Response Body)
  -> Socket
  -> IO ()
handleH2Connection _config svc conn =
  (do h2Conf <- H2.allocSimpleConfig conn 4096
      H2.run H2.defaultServerConfig h2Conf (toH2Server svc)
      H2.freeSimpleConfig h2Conf)
    `catch` (\(_ :: SomeException) -> pure ())
    `finally` close conn


-- | Bridge a spire Service to the http2 package's Server callback.
--
-- Each HTTP/2 stream (request) is dispatched to the spire Service,
-- and the response is sent back via the http2 callback.
toH2Server :: Service IO (Request Body) (Response Body) -> H2.Server
toH2Server (Service handler) h2Req _aux respond = do
  resp <- (do
    req <- fromH2Request h2Req
    handler req)
    `catch` (\(_ :: SomeException) ->
      pure (Http.Core.Response status500
        [("Content-Type", "text/plain")]
        (fromBytes "Internal Server Error")))
  respond (toH2Response resp) []


-- | Convert an http2 Request to an http-core Request.
fromH2Request :: H2.Request -> IO (Request Body)
fromH2Request h2Req = do
  exts <- emptyExtensions
  let method  = maybe "GET" id (H2.requestMethod h2Req)
      pathRaw = maybe "/" id (H2.requestPath h2Req)
      (pathPart, queryPart) = BS.break (== 0x3F) pathRaw
      pathSegs = filter (/= "") $ map (TE.decodeUtf8Lenient . urlDecode True) $ BS.split 0x2F pathPart
      query = if BS.null queryPart then [] else parseQuery (BS.drop 1 queryPart)
      -- TokenHeaderList = [(Token, FieldValue)]
      -- Token has tokenKey :: CI ByteString, isPseudo :: Bool
      (tokenList, _valueTable) = H2.requestHeaders h2Req
      -- Filter out pseudo-headers (:method, :path, :scheme, :authority)
      headers = [ (tokenKey tok, val)
                | (tok, val) <- tokenList
                , not (isPseudo tok)
                ]
  let bodyStream = streamBody $ do
        chunk <- H2.getRequestBodyChunk h2Req
        if BS.null chunk
          then pure Nothing
          else pure (Just (Chunk chunk))
  pure Http.Core.Request
    { requestMethod     = method
    , requestPathRaw    = pathPart
    , requestPath       = pathSegs
    , requestQuery      = query
    , requestHeaders    = headers
    , requestBody       = bodyStream
    , requestExtensions = exts
    }


-- | Convert an http-core Response to an http2 Response.
toH2Response :: Response Body -> H2.Response
toH2Response resp =
  let status = responseStatus resp
      hdrs   = responseHeaders resp
  in case responseBody resp of
    BodyEmpty ->
      H2.responseNoBody status hdrs
    BodyStrict bs ->
      H2.responseBuilder status hdrs (Builder.byteString bs)
    BodyStream pull ->
      H2.responseStreaming status hdrs $ \write flush -> do
        let go = do
              mChunk <- pull
              case mChunk of
                Nothing          -> flush
                Just (Chunk bs)  -> write (Builder.byteString bs) >> go
                Just ChunkFlush  -> flush >> go
        go
    BodyFile path mRange ->
      case mRange of
        Nothing ->
          H2.responseFile status hdrs (H2.FileSpec path 0 maxBound)
        Just (FileRange off len) ->
          H2.responseFile status hdrs
            (H2.FileSpec path (fromIntegral off) (fromIntegral len))