packages feed

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

-- | TLS support for spire-server.
--
-- Accepts a pre-configured @TLS.ServerParams@ so users have full
-- control over cipher suites, certificate loading, and TLS versions.
-- See the @tls@ package documentation for configuration options.
--
-- @
-- import qualified Network.TLS as TLS
-- import qualified Network.TLS.Extra.Cipher as TLS
-- import Spire.Server.TLS
--
-- main = do
--   params <- loadTLSParams "cert.pem" "key.pem"
--   runServerTLS 443 "0.0.0.0" params myService
-- @
module Spire.Server.TLS
  ( -- * Running with TLS
    runServerTLS
    -- * Helper for loading credentials
  , loadTLSParams
  ) where

import Control.Concurrent (forkIO)
import Control.Exception (bracket, catch, finally, SomeException)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Network.HTTP.Types (status400, status500, parseQuery, urlDecode)
import Network.Socket
import qualified Network.TLS as TLS

import Spire.Service (Service (..))
import Http.Core
import Http.Core.Body
import Spire.Server.Parse (parseRequestHead, RequestHead (..))
import Spire.Server.Render (renderFull)


-- | Run the server with TLS on the given port.
runServerTLS
  :: Int                                    -- ^ Port
  -> HostName                               -- ^ Host
  -> TLS.ServerParams                       -- ^ TLS configuration
  -> Service IO (Request Body) (Response Body)
  -> IO ()
runServerTLS port host params svc = do
  let hints = defaultHints { addrFlags = [AI_PASSIVE], addrSocketType = Stream }
  addr:_ <- getAddrInfo (Just hints) (Just host) (Just (show port))
  bracket (openSocket addr) close $ \sock -> do
    setSocketOption sock ReuseAddr 1
    bind sock (addrAddress addr)
    listen sock 128
    let loop = do
          (conn, _) <- accept sock
          _ <- forkIO $ handleConn params svc conn
          loop
    loop


-- | Load TLS credentials and build ServerParams.
--
-- Uses strong cipher suites and TLS 1.2+. Returns the error string
-- if credential loading fails.
loadTLSParams :: FilePath -> FilePath -> IO (Either String TLS.ServerParams)
loadTLSParams certFile keyFile = do
  result <- TLS.credentialLoadX509 certFile keyFile
  pure $ case result of
    Left err   -> Left err
    Right cred -> Right $ TLS.defaultParamsServer
      { TLS.serverShared = (TLS.serverShared TLS.defaultParamsServer)
          { TLS.sharedCredentials = TLS.Credentials [cred]
          }
      }


handleConn :: TLS.ServerParams -> Service IO (Request Body) (Response Body) -> Socket -> IO ()
handleConn params svc sock = (do
  ctx <- TLS.contextNew sock params
  TLS.handshake ctx

  input <- TLS.recvData ctx
  case parseRequestHead input of
    Nothing -> do
      TLS.sendData ctx (LBS.fromStrict $ renderFull status400 [] "Bad Request")
      TLS.bye ctx
    Just (reqHead, remainder) -> do
      let bodyBytes = case lookup "content-length" (rhHeaders reqHead) of
            Just cl -> case reads (BS8.unpack cl) of
              [(n, "")] -> BS.take n remainder
              _         -> BS.empty
            Nothing -> BS.empty

      exts <- emptyExtensions
      let pathRaw = rhPath reqHead
          (pathPart, queryPart) = BS8.break (== '?') pathRaw
          path = filter (/= "") $ map (TE.decodeUtf8Lenient . urlDecode True) $ BS.split 0x2F pathPart
          query = if BS.null queryPart then [] else parseQuery (BS.drop 1 queryPart)
          req = Request
            { requestMethod     = rhMethod reqHead
            , requestPathRaw    = pathPart
            , requestPath       = path
            , requestQuery      = query
            , requestHeaders    = rhHeaders reqHead
            , requestBody       = fromBytes bodyBytes
            , requestExtensions = exts
            }

      resp <- catch
        (runService svc req)
        (\(_ :: SomeException) ->
          pure (Response status500 [("Content-Type", "text/plain")] (fromBytes "Internal Server Error")))

      respBody <- bodyToStrict (responseBody resp)
      TLS.sendData ctx (LBS.fromStrict $ renderFull (responseStatus resp) (responseHeaders resp) respBody)
      TLS.bye ctx
  ) `finally` close sock