packages feed

seacat-1.0.0.0: src/Web/Seacat/Server.hs

{-# LANGUAGE OverloadedStrings #-}

-- |Here lives everything to do with starting up a Seacat server.
module Web.Seacat.Server ( SeacatSettings(..)
                         , defaultSettings
                         , seacat) where

import Control.Arrow ((***), first, second)
import Control.Monad (when, void)
import Control.Monad.Trans.Reader (runReaderT)
import Data.Either.Utils (forceEither)
import Data.Maybe (fromJust, fromMaybe)
import Data.String (fromString)
import Data.Text (replace)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (getCurrentTime)
import Database.Persist ((<.), deleteWhere)
import Database.Persist.Sql (ConnectionPool, Migration, SqlPersistM, runMigration)
import Network.HTTP.Types.Method (StdMethod(..), parseMethod)
import Network.Wai (Application, requestMethod, queryString)
import Network.Wai.Handler.Warp (runSettings, setHost, setPort)
import Network.Wai.Middleware.Gzip (GzipSettings(..), GzipFiles(GzipCompress), gzip, gzipFiles, def)
import Network.Wai.Middleware.Static (addBase, staticPolicy)
import Network.Wai.Parse (parseRequestBody, lbsBackEnd)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.IO.Error (catchIOError)
import Web.Routes.PathInfo (PathInfo)

import Web.Seacat.Configuration (ConfigParser, applyUserConfig, defaults, get', loadConfigFile, reloadConfigFile)
import Web.Seacat.Database
import Web.Seacat.RequestHandler.Types (Cry(..), MkUrl, Handler, _req, _params, _files)
import Web.Seacat.Router (handleWai)

import qualified Network.Wai.Handler.Warp as W

-- |Optional configuration for Seacat servers
data SeacatSettings = SeacatSettings
    { _config   :: Maybe ConfigParser
      -- ^ Default configuration, overriding the defaults. This should
      -- include all application-required configuration not already
      -- provided by Seacat. Configuration is applied as follows,
      -- where \`merge\` overrides the values in its first argument by
      -- the values in its second,
      --
      --     seacat defaults \`merge\` application config \`merge\` user config
      --
      -- This ensures that all the required configuration values are set.

    , _migrate  :: Maybe (Migration SqlPersistM)
      -- ^ Database migration handler. If a database is used, this must
      -- be provided (or migrations handled manually in runserver).

    , _populate :: Maybe (SqlPersistM ())
      -- ^ Database population handler. If a database is used, this
      -- must be provided (or population handled manually in
      -- runserver).

    , _clean :: Maybe (SqlPersistM ())
      -- ^ Database clean handler. This is optional.

    , _gzip :: GzipSettings
      -- ^ The settings to use for Gzip compression.
    }

-- |Default configuration: no application-specific configuration, no
-- migration handler, no population handler, and gzip if the browser
-- accepts it.
defaultSettings :: SeacatSettings
defaultSettings = SeacatSettings { _config   = Nothing
                                 , _migrate  = Nothing
                                 , _populate = Nothing
                                 , _clean    = Nothing
                                 , _gzip     = def { gzipFiles = GzipCompress }
                                 }

-- |Launch the Seacat web server. Seacat takes two bits of mandatory
-- configuration, a routing function and a 500 handler, and then some
-- optional configuration. By default, the server listens on *:3000.
seacat :: PathInfo r
       => (StdMethod -> r -> Handler r) -- ^ Routing function
       -> (String -> Handler r) -- ^ Top-level error handling function
       -> SeacatSettings -- ^ Optional configuration
       -> IO ()
seacat route on500 settings = do
  args <- getArgs

  when (length args < 1) $
    die "Expected at least one argument"

  let command = head args

  let confFile = case args of
                   (_:conffile:_) -> Just conffile
                   _ -> Nothing

  config <- case confFile of
             Just cfile -> loadConfigFile cfile
             Nothing -> return $ Just defaults

  case config of
    Just conf -> let conf' = applyUserConfig conf (_config settings)
                     backend  = get' conf' "database" "backend"
                     connstr  = get' conf' "database" "connection_string" 
                     poolsize = get' conf' "database" "pool_size"

                     pool = withPool (fromString backend) (fromString connstr) poolsize

                     settings' = settings { _config = Just conf' }
                 in run command route on500 confFile pool settings'
    Nothing   -> die "Failed to read configuration"

-- |Die with a fatal error
die :: String -- ^ The error description
    -> IO ()
die err = putStrLn err >> exitFailure

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

-- |Run one of the applications, depending on the command
run :: PathInfo r
    => String -- ^ Command
    -> (StdMethod -> r -> Handler r) -- ^ Routing function
    -> (String -> Handler r) -- ^ Top-level error handling function
    -> Maybe FilePath -- ^ The config file
    -> ((ConnectionPool -> IO ()) -> IO ()) -- ^ Database connection pool runner
    -> SeacatSettings -- ^ The optional settings
    -> IO ()
run "runserver" = runserver
run "migrate"   = migrate
run "populate"  = populate
run "clean"     = clean
run _           = badcommand

-- |Run the server
runserver :: PathInfo r
          => (StdMethod -> r -> Handler r)
          -> (String -> Handler r)
          -> Maybe FilePath
          -> ((ConnectionPool -> IO ()) -> IO ())
          -> SeacatSettings
          -> IO ()
runserver route on500 cfile pool settings = do
  let conf = fromJust $ _config settings

  let host = get' conf "server" "host"
  let port = get' conf "server" "port"

  void $ clean route on500 cfile pool settings

  let settings' = setHost (fromString host) . setPort port $ W.defaultSettings

  putStrLn $ unlines [ " _ _  _. _ _._|_ "
                     , "_>(/_(_|(_(_| |_ "]
  putStrLn $ "Listening on " ++ host ++ ":" ++ show port
  pool $ runSettings settings' . runner settings route on500 (conf,cfile)

-- |Migrate the database
migrate :: PathInfo r
        => (StdMethod -> r -> Handler r)
        -> (String -> Handler r)
        -> Maybe FilePath
        -> ((ConnectionPool -> IO ()) -> IO ())
        -> SeacatSettings
        -> IO ()
migrate _ _ _ pool settings = do
  runDB pool $ runMigration migrateAll

  case _migrate settings of
    Just migration -> runDB pool $ runMigration migration
    Nothing -> putStrLn "No application migration handler."

-- |Populate the database with test data
populate :: PathInfo r
         => (StdMethod -> r -> Handler r)
         -> (String -> Handler r)
         -> Maybe FilePath
         -> ((ConnectionPool -> IO ()) -> IO ())
         -> SeacatSettings
         -> IO ()
populate _ _ _ pool settings =
    case _populate settings of
      Just pop -> runDB pool pop
      Nothing -> die "No population handler."

-- |Clean out expired bans from the database
clean :: PathInfo r
      => (StdMethod -> r -> Handler r)
      -> (String -> Handler r)
      -> Maybe FilePath
      -> ((ConnectionPool -> IO ()) -> IO ())
      -> SeacatSettings
      -> IO ()
clean _ _ _ pool settings = do
  now <- getCurrentTime

  runDB pool $ do
    deleteWhere [SeacatRateLimitExpires <. now]
    deleteWhere [SeacatIPBanExpires     <. now]
    deleteWhere [SeacatAntiFloodExpires <. now]

  case _clean settings of
    Just cln -> runDB pool cln
    Nothing -> putStrLn "No application clean handler."

-- |Fail with an error
badcommand :: PathInfo r
           => (StdMethod -> r -> Handler r)
           -> (String -> Handler r)
           -> Maybe FilePath
           -> ((ConnectionPool -> IO ()) -> IO ())
           -> SeacatSettings
           -> IO ()
badcommand _ _ _ _ _ = die "Unknown command"

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

-- |runner is the actual WAI application. It takes a request, handles
-- it, and produces a response.
runner :: PathInfo r
       => SeacatSettings -- ^ The settings
       -> (StdMethod -> r -> Handler r)    -- ^ Routing function
       -> (String -> Handler r)           -- ^ Top-level error handling function
       -> (ConfigParser, Maybe FilePath) -- ^ The configuration
       -> ConnectionPool                 -- ^ Database connection reference
       -> Application
runner settings route on500 c@(conf,_) pool = handleWai (fromString webroot) $ \mkurl r ->
  -- This is horrific, come up with a better way of doing it
  let mkurl' r' args = replace "%23" "#" . mkurl r' $ map (\(a,b) -> (a, if b == "" then Nothing else Just b)) args
  in staticPolicy (addBase fileroot) $
     gzip (_gzip settings) $
     process route on500 c pool mkurl' r

  where webroot  = get' conf "server" "web_root"
        fileroot = get' conf "server" "file_root"

-- |Route and process a request
-- Todo: use SqlPersistT?
process :: PathInfo r
        => (StdMethod -> r -> Handler r) -- ^ Routing function
        -> (String -> Handler r)        -- ^ Top-level error handling function
        -> (ConfigParser, Maybe FilePath) -- ^ The configuration
        -> ConnectionPool              -- ^ Database connection reference
        -> MkUrl r                     -- ^ URL building function
        -> r                           -- ^ Requested route
        -> Application
process route on500 (conf,cfile) pool mkurl path req receiver = requestHandler `catchIOError` runError
  where requestHandler = runHandler' $ route method path
        runError err   = runHandler' $ on500 (show err)
        runHandler' h  = runHandler h conf cfile pool mkurl req receiver 
        method         = forceEither . parseMethod . requestMethod $ req

-- |Run a request handler.
runHandler :: PathInfo r
           => Handler r -- ^ The handler to run
           -> ConfigParser
           -> Maybe FilePath
           -> ConnectionPool
           -> MkUrl r
           -> Application
runHandler h conf cfile pool mkurl req receiver = do
  -- Reload the config
  conf' <- case cfile of
            Just cf -> reloadConfigFile conf cf
            Nothing -> return conf

  -- Build the Cry
  (ps, fs) <- parseRequestBody lbsBackEnd req
  let ps' = map (second $ fromMaybe "") $ queryString req
  let cry = Cry { _req    = req
                , _params = map (decodeUtf8 *** decodeUtf8) (ps ++ ps')
                , _files  = map (first decodeUtf8) fs
                , _conf   = conf'
                , _mkurl  = mkurl
                }

  runPool (runReaderT h cry) pool >>= receiver