module PostgresWebsockets.Config
( prettyVersion
, loadConfig
, warpSettings
, AppConfig (..)
) where
import Env
import Data.Text (intercalate, pack, replace, strip, stripPrefix)
import Data.Version (versionBranch)
import Paths_postgres_websockets (version)
import Protolude hiding (intercalate, (<>), optional, replace)
import Data.String (IsString(..))
import Network.Wai.Handler.Warp
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
data AppConfig = AppConfig {
configDatabase :: Text
, configPath :: Maybe Text
, configHost :: Text
, configPort :: Int
, configListenChannel :: Text
, configMetaChannel :: Maybe Text
, configJwtSecret :: ByteString
, configJwtSecretIsBase64 :: Bool
, configPool :: Int
, configRetries :: Int
}
prettyVersion :: Text
prettyVersion = intercalate "." $ map show $ versionBranch version
loadConfig :: IO AppConfig
loadConfig = readOptions >>= loadSecretFile >>= loadDatabaseURIFile
warpSettings :: (IO () -> IO ()) -> AppConfig -> Settings
warpSettings waitForShutdown AppConfig{..} =
setHost (fromString $ toS configHost)
. setPort configPort
. setServerName (toS $ "postgres-websockets/" <> prettyVersion)
. setTimeout 3600
. setInstallShutdownHandler waitForShutdown
. setGracefulShutdownTimeout (Just 5)
$ defaultSettings
readOptions :: IO AppConfig
readOptions =
Env.parse (header "You need to configure some environment variables to start the service.") $
AppConfig <$> var (str <=< nonempty) "PGWS_DB_URI" (help "String to connect to PostgreSQL")
<*> optional (var str "PGWS_ROOT_PATH" (help "Root path to serve static files, unset to disable."))
<*> var str "PGWS_HOST" (def "*4" <> helpDef show <> help "Address the server will listen for websocket connections")
<*> var auto "PGWS_PORT" (def 3000 <> helpDef show <> help "Port the server will listen for websocket connections")
<*> var str "PGWS_LISTEN_CHANNEL" (def "postgres-websockets-listener" <> helpDef show <> help "Master channel used in the database to send or read messages in any notification channel")
<*> optional (var str "PGWS_META_CHANNEL" (help "Websockets channel used to send events about the server state changes."))
<*> var str "PGWS_JWT_SECRET" (help "Secret used to sign JWT tokens used to open communications channels")
<*> var auto "PGWS_JWT_SECRET_BASE64" (def False <> helpDef show <> help "Indicate whether the JWT secret should be decoded from a base64 encoded string")
<*> var auto "PGWS_POOL_SIZE" (def 10 <> helpDef show <> help "How many connection to the database should be used by the connection pool")
<*> var auto "PGWS_RETRIES" (def 5 <> helpDef show <> help "How many times it should try to connect to the database on startup before exiting with an error")
loadDatabaseURIFile :: AppConfig -> IO AppConfig
loadDatabaseURIFile conf@AppConfig{..} =
case stripPrefix "@" configDatabase of
Nothing -> pure conf
Just filename -> setDatabase . strip <$> readFile (toS filename)
where
setDatabase uri = conf {configDatabase = uri}
loadSecretFile :: AppConfig -> IO AppConfig
loadSecretFile conf = extractAndTransform secret
where
secret = decodeUtf8 $ configJwtSecret conf
isB64 = configJwtSecretIsBase64 conf
extractAndTransform :: Text -> IO AppConfig
extractAndTransform s =
fmap setSecret $ transformString isB64 =<<
case stripPrefix "@" s of
Nothing -> return . encodeUtf8 $ s
Just filename -> chomp <$> BS.readFile (toS filename)
where
chomp bs = fromMaybe bs (BS.stripSuffix "\n" bs)
transformString :: Bool -> ByteString -> IO ByteString
transformString False t = return t
transformString True t =
case B64.decode $ encodeUtf8 $ strip $ replaceUrlChars $ decodeUtf8 t of
Left errMsg -> panic $ pack errMsg
Right bs -> return bs
setSecret bs = conf {configJwtSecret = bs}
replaceUrlChars =
replace "_" "/" . replace "-" "+" . replace "." "="