packages feed

micro-gateway-1.1.0.1: app/Main.hs

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

module Main
  ( main
  ) where

import           Data.Aeson                      (FromJSON, parseJSON,
                                                  withObject, (.!=), (.:),
                                                  (.:?))
import           Data.ByteString                 (ByteString)
import qualified Data.ByteString.Lazy            as LB (ByteString)
import           Data.Maybe                      (fromMaybe)
import           Data.Streaming.Network.Internal (HostPreference (Host))
import           Data.Text.Encoding              (encodeUtf8)
import qualified Data.Text.Lazy                  as LT (Text)
import qualified Data.Yaml                       as Y
import           Micro.Gateway                   (matchAny, optionsHandler,
                                                  proxyDELETEHandler,
                                                  proxyGETHandler,
                                                  proxyPOSTHandler,
                                                  proxyPUTHandler, requireApp,
                                                  verifySignature,
                                                  verifySignature',
                                                  wsProxyHandler)
import qualified Micro.Gateway                   as GW
import qualified Network.HTTP.Client             as HTTP
import           Network.URI                     (URI (..), URIAuth (..),
                                                  parseURI)
import qualified Network.Wai.Handler.Warp        as W (defaultSettings,
                                                       runSettings, setHost,
                                                       setPort)
import           Network.Wai.Handler.WebSockets  (websocketsOr)
import           Network.Wai.Middleware.Cors     (CorsResourcePolicy (..), cors,
                                                  simpleCorsResourcePolicy)
import qualified Network.WebSockets              as WS (defaultConnectionOptions)
import           Options.Applicative
import           Web.Scotty                      (ScottyM, delete, get,
                                                  middleware, options, post,
                                                  put, scottyApp)

newtype Options' = Options' {getConfigFile :: String}

parser :: Parser Options'
parser = Options' <$> strOption (long "config"
                                <> short 'c'
                                <> metavar "FILE"
                                <> help "config file."
                                <> value "config.yaml")

data AppConfig = AppConfig
  { key          :: GW.AppKey
  , secret       :: GW.AppSecret
  , baseUrl      :: String
  , secure       :: Bool
  , proxy        :: Bool -- flag of only proxy
  , wsUrl        :: Maybe String
  , replacePages :: [LT.Text]
  , replaceName  :: ByteString

  -- allow page prefix
  , allowPages   :: [LT.Text]
  -- deny page prefix
  , denyPages    :: [LT.Text]
  }

data Config = Config
  { appList     :: [AppConfig]
  , connTimeout :: Int
  -- default 10 seconds, unit seconds
  , connPool    :: Int
  -- default 10
  , host        :: String
  , port        :: Int
  }

instance FromJSON AppConfig where
  parseJSON = withObject "AppConfig" $ \o -> do
    key          <- o .:  "key"
    secret       <- o .:  "secret"
    baseUrl      <- o .:  "baseUrl"
    secure       <- o .:? "secure"       .!= False
    proxy        <- o .:? "proxy"        .!= False
    wsUrl        <- o .:? "wsUrl"
    allowPages   <- o .:? "allowPages"   .!= []
    denyPages    <- o .:? "denyPages"    .!= []
    replacePages <- o .:? "replacePages" .!= []
    rname        <- o .:? "replaceName"  .!= "__KEY__"
    return AppConfig
      { replaceName = encodeUtf8 rname
      , ..
      }

instance FromJSON Config where
  parseJSON = withObject "Config" $ \o -> do
    appList     <- o .:  "appList"
    connPool    <- o .:? "connPool"    .!= 10
    connTimeout <- o .:? "connTimeout" .!= 10
    host        <- o .:? "host"        .!= "127.0.0.1"
    port        <- o .:? "port"        .!= 3000
    return Config{..}


main :: IO ()
main = execParser opts >>= program
  where
    opts = info (helper <*> parser)
      ( fullDesc
     <> progDesc "Simple Gateway"
     <> header "simple-gateway - Simple Gateway" )

program :: Options' -> IO ()
program Options'{getConfigFile=configPath} = do
  c <- Y.decodeFileEither configPath
  case c of
    Left e     -> print e
    Right Config{..} -> do
      mgr <- HTTP.newManager HTTP.defaultManagerSettings
                { HTTP.managerConnCount = connPool
                , HTTP.managerResponseTimeout = HTTP.responseTimeoutMicro $ connTimeout * 10000000
                }

      let provider = GW.newProvider
            { GW.getAppByKey = getAppAndInitail mgr appList
            }

      sapp <- scottyApp $ application provider
      let app = websocketsOr WS.defaultConnectionOptions (wsProxyHandler provider) sapp
      W.runSettings (W.setPort port . W.setHost (Host host) $ W.defaultSettings) app


findApp :: [AppConfig] -> GW.AppKey -> Maybe AppConfig
findApp [] _ = Nothing
findApp (x:xs) k = if key x == k then Just x
                                 else findApp xs k

getAppAndInitail :: HTTP.Manager -> [AppConfig] -> GW.AppKey -> IO (Maybe GW.App)
getAppAndInitail mgr configs k =
  case findApp configs k of
    Nothing -> return Nothing
    Just AppConfig{..} -> do
      let app = GW.newApp key secret secure proxy
          app' = app
            { GW.doRequest        = processRequest mgr baseUrl
            , GW.prepareWsRequest = processWsRequest $ fromMaybe baseUrl wsUrl
            , GW.allowPages       = allowPages
            , GW.denyPages        = denyPages
            , GW.replaceKeyName   = replaceName
            , GW.replaceKeyPages  = replacePages
            }

      return $ Just app'

processRequest :: HTTP.Manager -> String
               -> (HTTP.Request -> HTTP.Manager -> IO (HTTP.Response LB.ByteString))
               -> String -> IO (HTTP.Response LB.ByteString)
processRequest mgr root req uri = do
  r <- HTTP.parseRequest $ root ++ uri
  req r mgr


processWsRequest :: String -> (String -> Int -> IO ()) -> IO ()
processWsRequest baseUrl next = next (uriRegName auth) (read . drop 1 $ uriPort auth)
  where Just uri = parseURI baseUrl
        Just auth = uriAuthority uri

application :: GW.Provider -> ScottyM ()
application provider = do
  middleware $ cors (const $ Just policy)

  get     matchAny . requireApp provider $ verifySignature' proxyGETHandler
  post    matchAny . requireApp provider $ verifySignature proxyPOSTHandler
  put     matchAny . requireApp provider $ verifySignature proxyPUTHandler
  delete  matchAny . requireApp provider $ verifySignature proxyDELETEHandler
  options matchAny optionsHandler

  where policy = simpleCorsResourcePolicy
                   { corsMethods = [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
                   , corsRequestHeaders =
                     [ "X-REQUEST-KEY"
                     , "X-REQUEST-SIGNATURE"
                     , "X-REQUEST-TIME"
                     , "X-REQUEST-TYPE"
                     , "X-REQUEST-NONCE"
                     , "Content-Type"
                     , "User-Agent"
                     , "X-Real-IP"
                     , "Host"
                     , "X-Forwarded-For"
                     , "X-URI"
                     , "X-Query-String"
                     , "X-Scheme"
                     , "Cookie"
                     , "Authorization"
                     ]
                   , corsMaxAge = Just 86400
                   }