packages feed

periodic-client-exe-1.1.7.1: app/periodic-http-bridge.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

module Main
  ( main
  ) where

import           Control.Monad                   (when)
import           Control.Monad.IO.Class          (liftIO)
import qualified Data.ByteString.Char8           as B (pack)
import qualified Data.ByteString.Lazy            as LB (empty, fromStrict,
                                                        toStrict)
import           Data.Default.Class              (def)
import           Data.List                       (isPrefixOf)
import           Data.Maybe                      (fromMaybe)
import           Data.Streaming.Network.Internal (HostPreference (Host))
import           Data.String                     (fromString)
import           Metro.Class                     (Transport)
import           Metro.Socket                    (getHost, getService)
import           Metro.TP.Socket                 (socket)
import           Metro.TP.TLS                    (makeClientParams', tlsConfig)
import           Metro.TP.WebSockets             (clientConfig)
import           Metro.TP.XOR                    (xorConfig)
import           Network.HTTP.Types              (status204, status500)
import           Network.Wai.Handler.Warp        (setHost, setPort)
import           Periodic.Trans.ClientPool
import           Periodic.Types.Job
import           System.Environment              (getArgs, lookupEnv)
import           System.Exit                     (exitSuccess)
import           Web.Scotty                      (ActionM, ScottyM, body, get,
                                                  param, post, raw, rescue,
                                                  scottyOpts, settings)
import qualified Web.Scotty                      as WS (status)


data Options = Options
    { host     :: String
    , xorFile  :: FilePath
    , useTls   :: Bool
    , useWs    :: Bool
    , hostName :: String
    , certKey  :: FilePath
    , cert     :: FilePath
    , caStore  :: FilePath
    , httpHost :: String
    , httpPort :: Int
    , poolSize :: Int
    , showHelp :: Bool
    }

options :: Maybe String -> Maybe String -> Options
options h f = Options
  { host     = fromMaybe "unix:///tmp/periodic.sock" h
  , xorFile  = fromMaybe "" f
  , useTls   = False
  , useWs    = False
  , hostName = "localhost"
  , certKey  = "client-key.pem"
  , cert     = "client.pem"
  , caStore  = "ca.pem"
  , httpHost = "127.0.0.1"
  , httpPort = 8080
  , poolSize = 10
  , showHelp  = False
  }

parseOptions :: [String] -> Options -> Options
parseOptions []                   opt = opt
parseOptions ("-H":x:xs)          opt = parseOptions xs opt { host = x }
parseOptions ("--host":x:xs)      opt = parseOptions xs opt { host = x }
parseOptions ("--xor":x:xs)       opt = parseOptions xs opt { xorFile = x }
parseOptions ("--tls":xs)         opt = parseOptions xs opt { useTls = True }
parseOptions ("--ws":xs)          opt = parseOptions xs opt { useWs = True }
parseOptions ("--hostname":x:xs)  opt = parseOptions xs opt { hostName = x }
parseOptions ("--cert-key":x:xs)  opt = parseOptions xs opt { certKey = x }
parseOptions ("--cert":x:xs)      opt = parseOptions xs opt { cert = x }
parseOptions ("--ca":x:xs)        opt = parseOptions xs opt { caStore = x }
parseOptions ("--http-host":x:xs) opt = parseOptions xs opt { httpHost = x }
parseOptions ("--http-port":x:xs) opt = parseOptions xs opt { httpPort = read x }
parseOptions ("--pool-size":x:xs) opt = parseOptions xs opt { poolSize = read x }
parseOptions ("--help":xs)        opt = parseOptions xs opt { showHelp = True }
parseOptions ("-h":xs)            opt = parseOptions xs opt { showHelp = True }
parseOptions (_:xs)               opt = parseOptions xs opt

printHelp :: IO ()
printHelp = do
  putStrLn "periodic-http-bridge - Periodic task system client http bridge"
  putStrLn ""
  putStrLn "Usage: periodic [--host|-H HOST] [--http-host HOST] [--http-port PORT] [--pool-size SIZE] [--xor FILE|--ws|--tls [--hostname HOSTNAME] [--cert-key FILE] [--cert FILE] [--ca FILE]]"
  putStrLn ""
  putStrLn "Available options:"
  putStrLn "  -H --host      Socket path [$PERIODIC_PORT]"
  putStrLn "                 eg: tcp://:5000 (optional: unix:///tmp/periodic.sock) "
  putStrLn "     --xor       XOR Transport encode file [$XOR_FILE]"
  putStrLn "     --tls       Use tls transport"
  putStrLn "     --ws        Use websockets transport"
  putStrLn "     --hostname  Host name"
  putStrLn "     --cert-key  Private key associated"
  putStrLn "     --cert      Public certificate (X.509 format)"
  putStrLn "     --ca        trusted certificates"
  putStrLn "     --http-host HTTP host (optional: 127.0.0.1)"
  putStrLn "     --http-port HTTP port (optional: 8080)"
  putStrLn "     --pool-size Connection pool size"
  putStrLn "  -h --help       Display help message"
  putStrLn ""
  putStrLn "Version: v1.1.7.1"
  putStrLn ""
  exitSuccess

main :: IO ()
main = do
  h <- lookupEnv "PERIODIC_PORT"
  f <- lookupEnv "XOR_FILE"

  opts@Options{..} <- flip parseOptions (options h f) <$> getArgs

  when showHelp printHelp

  when (not ("tcp" `isPrefixOf` host) && not ("unix" `isPrefixOf` host)) $ do
    putStrLn $ "Invalid host " ++ host
    printHelp

  run opts def
    { settings = setPort httpPort
               $ setHost (Host httpHost) (settings def)}

run Options {useTls = True, ..} sopts = do
  prms <- makeClientParams' cert [] certKey caStore (hostName, B.pack $ fromMaybe "" $ getService host)
  clientEnv <- openPool (tlsConfig prms (socket host)) poolSize
  scottyOpts sopts $ application clientEnv

run Options {useWs = True, ..} sopts = do
  clientEnv <- openPool (clientConfig (socket host) (fromMaybe "0.0.0.0" $ getHost host) (fromMaybe "" $ getService host)) poolSize
  scottyOpts sopts $ application clientEnv

run Options {xorFile = "", ..} sopts = do
  clientEnv <- openPool (socket host) poolSize
  scottyOpts sopts $ application clientEnv

run Options {..} sopts = do
  clientEnv <- openPool (xorConfig xorFile $ socket host) poolSize
  scottyOpts sopts $ application clientEnv

application :: Transport tp => ClientPoolEnv tp ->  ScottyM ()
application clientEnv = do
  get "/periodic/status/" $ statusHandler clientEnv
  post "/periodic/submit/:func_name/:job_name/" $ submitJobHandler clientEnv
  post "/periodic/run/:func_name/:job_name/" $ runJobHandler clientEnv
  post "/periodic/remove/:func_name/:job_name/" $ removeJobHandler clientEnv
  post "/periodic/drop/:func_name/" $ dropFuncHandler clientEnv

paramJob :: ActionM (FuncName, JobName)
paramJob = do
  fn <- fromString <$> param "func_name"
  jn <- fromString <$> param "job_name"
  return (fn, jn)

paramJob_ :: ActionM Job
paramJob_ = do
  (fn, jn) <- paramJob
  schedAt <- param "sched_at" `rescue` const (return 0)
  timeout <- param "timeout" `rescue` const (return 0)
  wb <- Workload . LB.toStrict <$> body
  pure $ setSchedAt schedAt $ setTimeout timeout $ setWorkload wb $ initJob fn jn

sampleRet :: Bool -> ActionM ()
sampleRet r = do
  WS.status $ if r then status204 else status500
  raw LB.empty

submitJobHandler :: Transport tp => ClientPoolEnv tp -> ActionM ()
submitJobHandler clientEnv = do
  job <- paramJob_
  r <- liftIO $ runClientPoolT clientEnv $ submitJob_ job
  sampleRet r

removeJobHandler :: Transport tp => ClientPoolEnv tp -> ActionM ()
removeJobHandler clientEnv = do
  (fn, jn) <- paramJob
  r <- liftIO $ runClientPoolT clientEnv $ removeJob fn jn
  sampleRet r

runJobHandler :: Transport tp => ClientPoolEnv tp -> ActionM ()
runJobHandler clientEnv = do
  job <- paramJob_
  r <- liftIO $ runClientPoolT clientEnv $ runJob_ job
  case r of
    Nothing -> do
      WS.status status500
      raw "error"
    Just bs -> raw $ LB.fromStrict bs

dropFuncHandler :: Transport tp => ClientPoolEnv tp -> ActionM ()
dropFuncHandler clientEnv = do
  fn <- fromString <$> param "func_name"
  r <- liftIO $ runClientPoolT clientEnv $ dropFunc fn
  sampleRet r

statusHandler :: Transport tp => ClientPoolEnv tp -> ActionM ()
statusHandler clientEnv = do
  r <- liftIO $ runClientPoolT clientEnv status
  raw $ LB.fromStrict r