packages feed

webapp-0.2.0: Web/App/HTTP.hs

{-# LANGUAGE OverloadedStrings, RankNTypes, ScopedTypeVariables #-}

{-|
Module      : Web.App.HTTP
Copyright   : (c) Nathaniel Symer, 2015
License     : MIT
Maintainer  : nate@symer.io
Stability   : experimental
Portability : POSIX

Start an HTTP server.
-}

module Web.App.HTTP
(
  -- * Insecure HTTP
  startHTTP,
  -- * Secure HTTPS
  startHTTPS
)
where

import Web.App.Middleware
import Web.App.Internal.Privileges
import Web.App.RouteT
import Web.App.State
import Web.App.WebApp

import Control.Monad
import Control.Monad.IO.Class

import Network.Wai (Application,Middleware)
import Network.Wai.Handler.Warp (defaultSettings,getPort,getHost,runSettings)
import Network.Wai.Handler.WarpTLS (tlsSettings,runTLSSocket,TLSSettings(..),OnInsecure(..))
import Network.Wai.Handler.Warp.Internal -- (Settings(..))
import Network.Socket (sClose, withSocketsDo)
import Data.Streaming.Network (bindPortTCP)
import Control.Exception (bracket)

import System.Exit
import System.Posix

-- |Start an insecure HTTP server.
startHTTP :: (WebAppState s, MonadIO m)
          => WebApp s m -- ^ Scotty app to serve
          -> (m RouteResult -> IO RouteResult) -- ^ action to eval a monadic computation in @m@ in @IO@
          -> Int -- ^ Port to which to bind
          -> IO ()
startHTTP app runToIO port = serveApp runSettings runToIO app port [gzip 860]

-- |Start a secure HTTPS server. Please note that most HTTP/2-compatible browswers
-- require HTTPS in order to upgrade to HTTP/2.
startHTTPS :: (WebAppState s, MonadIO m)
           => WebApp s m -- ^ Scotty app to serve
           -> (m RouteResult -> IO RouteResult) -- ^ action to eval a monadic computation in @m@ in @IO@
           -> Int -- ^ Port to which to bind
           -> FilePath -- ^ 'FilePath' to an SSL certificate
           -> FilePath -- ^ 'FilePath' to an SSL private key
           -> IO ()
startHTTPS app runToIO port cert key = serveApp (runTLSHandled tlsset) runToIO app port mw
  where mw = [gzip 860, forceSSL]
        tlsset = (tlsSettings cert key) { onInsecure = AllowInsecure }

{- Internal -}

-- |Serves a @'WebAppT' s m ()@ given a serving function, "dropping"
-- function @(m 'RouteResult' -> 'IO' 'RouteResult')@, port, and middlewares.
serveApp :: (WebAppState s, MonadIO m)
         => (Settings -> Application -> IO ()) -- ^ serving function
         -> (m RouteResult -> IO RouteResult) -- ^ "dropping" function
         -> WebApp s m -- ^ app to serve
         -> Int -- ^ port to serve on
         -> [Middleware] -- ^ middleware to add to the app
         -> IO ()
serveApp serve runToIO app port mws = do
  (wai, teardown) <- toApplication runToIO $ mconcat $ app:map middleware mws
  serve (warpSettings teardown) wai
  where
    warpSettings td = defaultSettings {
      settingsPort = port,
      settingsInstallShutdownHandler = installShutdown td,
      settingsBeforeMainLoop = before,
      settingsHTTP2Enabled = True -- explicitly enable HTTP2 support
    }
    before = resignPrivileges "daemon"
    installShutdown teardown killSockets = do
      void $ installHandler sigTERM (handler teardown killSockets) Nothing
      void $ installHandler sigINT (handler teardown killSockets) Nothing
    handler teardown killSockets = Catch $ do
      void $ killSockets
      void $ teardown
      exitImmediately ExitSuccess

-- |Serve an application over TLS, obeying 'settingsInstallShutdownHandler'.
-- This setting is ignored in WarpTLS due to a bug (gasp!). See
-- https://github.com/yesodweb/wai/issues/483
runTLSHandled :: TLSSettings -> Settings -> Application -> IO ()
runTLSHandled tset set wai = withSocketsDo $ bracket acquire destroy f
  where
    acquire = bindPortTCP (getPort set) (getHost set)
    destroy = sClose
    f socket = do
      settingsInstallShutdownHandler set (sClose socket)
      runTLSSocket tset set socket wai