diff --git a/Dockerfile b/Dockerfile index 39c5e24..14a96f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use Alpine Linux as base image -FROM alpine:3.6 +FROM alpine:3.8 # Install libpq and gmp dependencies (dynamic libraries required by the project) RUN apk update && apk add libpq gmp libffi diff --git a/Dockerfile.build b/Dockerfile.build index f3bccf1..3e448b7 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,4 +1,4 @@ -FROM alpine:3.6 +FROM alpine:3.8 RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories RUN apk -U add shadow@testing diff --git a/README.md b/README.md index 88b08ed..e1bb2d1 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,17 @@ If you have any problems processing any Postgres related library on a Mac, try i After the build you should be able to run the server using `~/.local/bin/postgres-websockets` (you can add `~/.local/bin` to your PATH variable): -To run the example bellow you will need a PostgreSQL server running on port 5432 of your localhost. You can also change the database connection string editting the `sample.conf` file. +To run the example bellow you will need a PostgreSQL server running on port 5432 of your localhost. ```bash -~/.local/bin/postgres-websockets sample.conf +PGWS_DB_URI="postgres://localhost:5432/postgres" PGWS_JWT_SECRET="auwhfdnskjhewfi34uwehdlaehsfkuaeiskjnfduierhfsiweskjcnzeiluwhskdewishdnpwe" ~/.local/bin/postgres-websockets +postgres-websockets / Connects websockets to PostgreSQL asynchronous notifications. Listening on port 3000 ``` + You can also use the provided [sample-env](./sample-env) file to export the needed variables: +```bash +source sample-env && ~/.local/bin/postgres-websockets +``` After running the above command, open your browser on http://localhost:3000 to see an example of usage. The sample config file provided in the [sample.conf](https://github.com/diogob/postgres-websockets/tree/master/sample.conf) file comes with a jwt secret just for testing and is used in the sample client. diff --git a/app/Config.hs b/app/Config.hs index 6bb76e4..0823b99 100644 --- a/app/Config.hs +++ b/app/Config.hs @@ -20,22 +20,10 @@ module Config ( prettyVersion ) where -import System.IO.Error (IOError) -import Control.Applicative -import qualified Data.Configurator as C -import qualified Data.Configurator.Parser as C -import qualified Data.Configurator.Types as C -import Data.Monoid -import Data.Scientific (floatingOrInteger) -import Data.Text (intercalate, lines) -import Data.Text.Encoding (encodeUtf8) +import Env +import Data.Text (intercalate) import Data.Version (versionBranch) -import Options.Applicative hiding (str) import Paths_postgres_websockets (version) -import System.IO (hPrint) -import Text.Heredoc -import Text.PrettyPrint.ANSI.Leijen hiding ((<>), (<$>)) -import qualified Text.PrettyPrint.ANSI.Leijen as L import Protolude hiding (intercalate, (<>)) -- | Config file settings for the server @@ -54,91 +42,18 @@ data AppConfig = AppConfig { prettyVersion :: Text prettyVersion = intercalate "." $ map show $ versionBranch version --- | Function to read and parse options from the command line +-- | Function to read and parse options from the environment readOptions :: IO AppConfig -readOptions = do - -- First read the config file path from command line - cfgPath <- customExecParser parserPrefs opts - -- Now read the actual config file - conf <- catch - (C.readConfig =<< C.load [C.Required cfgPath]) - configNotfoundHint - - let (mAppConf, errs) = flip C.runParserM conf $ do - -- db ---------------- - cDbUri <- C.key "db-uri" - cPool <- fromMaybe 10 . join . fmap coerceInt <$> C.key "db-pool" - -- server ------------ - cPath <- C.key "server-root" - cHost <- fromMaybe "*4" . mfilter (/= "") <$> C.key "server-host" - cPort <- fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port" - cAuditC <- C.key "audit-channel" - cChannel <- case cAuditC of - Just c -> fromMaybe c . mfilter (/= "") <$> C.key "listen-channel" - Nothing -> C.key "listen-channel" - -- jwt --------------- - cJwtSec <- C.key "jwt-secret" - cJwtB64 <- fromMaybe False <$> C.key "secret-is-base64" - - return $ AppConfig cDbUri cPath cHost cPort cChannel (encodeUtf8 cJwtSec) cJwtB64 cPool - - case mAppConf of - Nothing -> do - forM_ errs $ hPrint stderr - exitFailure - Just appConf -> - return appConf - - where - coerceInt :: (Read i, Integral i) => C.Value -> Maybe i - coerceInt (C.Number x) = rightToMaybe $ floatingOrInteger x - coerceInt (C.String x) = readMaybe $ toS x - coerceInt _ = Nothing - - opts = info (helper <*> pathParser) $ - fullDesc - <> progDesc ( - "postgres-websockets " - <> toS prettyVersion - <> " / Connects websockets to PostgreSQL asynchronous notifications." - ) - <> footerDoc (Just $ - text "Example Config File:" - L.<> nest 2 (hardline L.<> exampleCfg) - ) - - parserPrefs = prefs showHelpOnError - - configNotfoundHint :: IOError -> IO a - configNotfoundHint e = die $ "Cannot open config file:\n\t" <> show e - - missingKeyHint :: C.KeyError -> IO a - missingKeyHint (C.KeyError n) = - die $ - "Required config parameter \"" <> n <> "\" is missing or of wrong type.\n" - - exampleCfg :: Doc - exampleCfg = vsep . map (text . toS) . lines $ - [str|db-uri = "postgres://user:pass@localhost:5432/dbname" - |db-pool = 10 - | - |server-root = "./client-example" - |server-host = "*4" - |server-port = 3000 - |listen-channel = "postgres-websockets-listener" - | - |## choose a secret to enable JWT auth - |## (use "@filename" to load from separate file) - |# jwt-secret = "foo" - |# secret-is-base64 = false - |] - - -pathParser :: Parser FilePath -pathParser = - strArgument $ - metavar "FILENAME" <> - help "Path to configuration file" +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") + <*> var str "PGWS_ROOT_PATH" (def "./" <> helpDef show <> help "Root path to serve static files") + <*> 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") + <*> 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") data PgVersion = PgVersion { pgvNum :: Int32 diff --git a/app/Main.hs b/app/Main.hs index 2d311ad..cad7f04 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -13,7 +13,7 @@ import qualified Data.ByteString.Base64 as B64 import Data.String (IsString (..)) import Data.Text (pack, replace, strip, stripPrefix) import Data.Text.Encoding (encodeUtf8, decodeUtf8) -import qualified Hasql.Query as H +import qualified Hasql.Statement as H import qualified Hasql.Session as H import qualified Hasql.Decoders as HD import qualified Hasql.Encoders as HE @@ -27,12 +27,12 @@ import System.IO (BufferMode (..), isServerVersionSupported :: H.Session Bool isServerVersionSupported = do - ver <- H.query () pgVersion + ver <- H.statement () pgVersion return $ ver >= pgvNum minimumPgVersion where pgVersion = - H.statement "SELECT current_setting('server_version_num')::integer" - HE.unit (HD.singleRow $ HD.value HD.int4) False + H.Statement "SELECT current_setting('server_version_num')::integer" + HE.unit (HD.singleRow $ HD.column HD.int4) False main :: IO () main = do @@ -40,6 +40,10 @@ main = do hSetBuffering stdin LineBuffering hSetBuffering stderr NoBuffering + putStrLn $ ("postgres-websockets " :: Text) + <> prettyVersion + <> " / Connects websockets to PostgreSQL asynchronous notifications." + conf <- loadSecretFile =<< readOptions let host = configHost conf port = configPort conf diff --git a/circle.yml b/circle.yml index e66b7e1..34eed71 100644 --- a/circle.yml +++ b/circle.yml @@ -3,8 +3,8 @@ dependencies: - "~/.stack" - ".stack-work" pre: - - curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp - - sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin + - curl -L https://github.com/commercialhaskell/stack/releases/download/v1.9.1/stack-1.9.1-linux-x86_64.tar.gz | tar zx -C /tmp + - sudo mv /tmp/stack-1.9.1-linux-x86_64/stack /usr/bin override: - stack setup - rm -fr $(stack path --dist-dir) $(stack path --local-install-root) diff --git a/postgres-websockets.cabal b/postgres-websockets.cabal index 654941c..bdc4ac1 100644 --- a/postgres-websockets.cabal +++ b/postgres-websockets.cabal @@ -1,5 +1,5 @@ name: postgres-websockets -version: 0.4.2.1 +version: 0.5.0.0 synopsis: Middleware to map LISTEN/NOTIFY messages to Websockets description: Please see README.md homepage: https://github.com/diogob/postgres-websockets#readme @@ -22,10 +22,10 @@ library , PostgresWebsockets.HasqlBroadcast , PostgresWebsockets.Claims build-depends: base >= 4.7 && < 5 - , hasql-pool >= 0.4 && < 0.5 + , hasql-pool >= 0.4 && < 0.6 , text >= 1.2 && < 2 , wai >= 3.2 && < 4 - , websockets >= 0.9 && < 0.11 + , websockets >= 0.9 && < 0.13 , wai-websockets >= 3.0 && < 4 , http-types >= 0.9 , bytestring >= 0.10 @@ -53,6 +53,7 @@ executable postgres-websockets hs-source-dirs: app main-is: Main.hs other-modules: Config + , Paths_postgres_websockets ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N build-depends: base >= 4.7 && < 5 , transformers >= 0.4 && < 0.6 @@ -63,9 +64,8 @@ executable postgres-websockets , protolude >= 0.2 , base64-bytestring , bytestring - , configurator-ng >= 0.0.0.1 + , configurator , optparse-applicative - , scientific >= 0.3.5.0 , text , time , wai @@ -73,6 +73,7 @@ executable postgres-websockets , wai-app-static , heredoc , ansi-wl-pprint + , envparse default-language: Haskell2010 default-extensions: OverloadedStrings, NoImplicitPrelude, QuasiQuotes diff --git a/sample.conf b/sample-env similarity index 55% rename from sample.conf rename to sample-env index d18df0d..6e3c1e6 100644 --- a/sample.conf +++ b/sample-env @@ -1,20 +1,20 @@ ## PostgreSQL URI where the server will connect to issue NOTIFY and LISTEN commands -db-uri = "postgres://localhost:5432/postgres" +export PGWS_DB_URI="postgres://localhost:5432/postgres" ## Size of connection pool used to issue notify commands (LISTEN commands are always issued on the same connection that is not part of the pool). -db-pool = 10 +export PGWS_POOL_SIZE=10 -## server-root can be used to serve some static files for convenience when testing. -server-root = "./client-example" +## Root path can be used to serve some static files for convenience when testing. +export PGWS_ROOT_PATH="./client-example" ## Sends a copy of every message received from websocket clients to the channel specified bellow as an aditional NOTIFY command. -listen-channel = "postgres-websockets-listener" +export PGWS_LISTEN_CHANNEL="postgres-websockets-listener" ## Host and port on which the websockets server (and the static files server) will be listening. -server-host = "*4" -server-port = 3000 +export PGWS_HOST="*4" +export PGWS_PORT=3000 ## choose a secret to enable JWT auth ## (use "@filename" to load from separate file) -jwt-secret = "auwhfdnskjhewfi34uwehdlaehsfkuaeiskjnfduierhfsiweskjcnzeiluwhskdewishdnpwe" -secret-is-base64 = false +export PGWS_JWT_SECRET="auwhfdnskjhewfi34uwehdlaehsfkuaeiskjnfduierhfsiweskjcnzeiluwhskdewishdnpwe" +export PGWS_JWT_SECRET_BASE64=False diff --git a/src/PostgresWebsockets/Claims.hs b/src/PostgresWebsockets/Claims.hs index 06204b1..dce6d91 100644 --- a/src/PostgresWebsockets/Claims.hs +++ b/src/PostgresWebsockets/Claims.hs @@ -89,8 +89,8 @@ claims2map = val2map . toJSON hs256jwk :: ByteString -> JWK hs256jwk key = fromKeyMaterial km - & jwkUse .~ Just Sig - & jwkAlg .~ (Just $ JWSAlg HS256) + & jwkUse ?~ Sig + & jwkAlg ?~ JWSAlg HS256 where km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key)) diff --git a/src/PostgresWebsockets/Database.hs b/src/PostgresWebsockets/Database.hs index aa6de6a..a732a0e 100644 --- a/src/PostgresWebsockets/Database.hs +++ b/src/PostgresWebsockets/Database.hs @@ -12,9 +12,9 @@ module PostgresWebsockets.Database import Protolude hiding (replace) import Hasql.Pool (Pool, UsageError, use) -import Hasql.Session (sql, run, query) +import Hasql.Session (sql, run, statement) import qualified Hasql.Session as S -import Hasql.Query (statement) +import qualified Hasql.Statement as HST import Hasql.Connection (Connection, withLibPQConnection) import qualified Hasql.Decoders as HD import qualified Hasql.Encoders as HE @@ -45,19 +45,19 @@ toPgIdentifier x = PgIdentifier $ "\"" <> strictlyReplaceQuotes (trimNullChars x -- | Given a Hasql Pool, a channel and a message sends a notify command to the database notifyPool :: Pool -> ByteString -> ByteString -> IO (Either Error ()) notifyPool pool channel mesg = - mapError <$> use pool (query (toS channel, toS mesg) callStatement) + mapError <$> use pool (statement (toS channel, toS mesg) callStatement) where mapError :: Either UsageError () -> Either Error () mapError = mapLeft (NotifyError . show) - callStatement = statement ("SELECT pg_notify" <> "($1, $2)") encoder HD.unit False - encoder = contramap fst (HE.value HE.text) <> contramap snd (HE.value HE.text) + callStatement = HST.Statement ("SELECT pg_notify" <> "($1, $2)") encoder HD.unit False + encoder = contramap fst (HE.param HE.text) <> contramap snd (HE.param HE.text) -- | Given a Hasql Connection, a channel and a message sends a notify command to the database notify :: Connection -> PgIdentifier -> ByteString -> IO (Either Error ()) notify con channel mesg = mapError <$> run (sql ("NOTIFY " <> fromPgIdentifier channel <> ", '" <> mesg <> "'")) con where - mapError :: Either S.Error () -> Either Error () + mapError :: Either S.QueryError () -> Either Error () mapError = mapLeft (NotifyError . show) -- | Given a Hasql Connection and a channel sends a listen command to the database diff --git a/src/PostgresWebsockets/HasqlBroadcast.hs b/src/PostgresWebsockets/HasqlBroadcast.hs index f9d77f2..48dd05b 100644 --- a/src/PostgresWebsockets/HasqlBroadcast.hs +++ b/src/PostgresWebsockets/HasqlBroadcast.hs @@ -11,7 +11,7 @@ module PostgresWebsockets.HasqlBroadcast , relayMessagesForever ) where -import Protolude +import Protolude hiding (putErrLn) import Hasql.Connection import Data.Aeson (decode, Value(..)) diff --git a/stack.yaml b/stack.yaml index f3bc0d1..19cf054 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,7 +1,6 @@ -resolver: lts-9.12 +resolver: lts-12.14 extra-deps: - - protolude-0.2 - - configurator-ng-0.0.0.1 - - critbit-0.2.0.0 +- envparse-0.4.1@sha256:989902e6368532548f61de1fa245ad2b39176cddd8743b20071af519a709ce30 + ghc-options: postgres-websockets: -O2 -Wall -fwarn-identities -fno-warn-redundant-constraints