diff --git a/.circleci/config.yml b/.circleci/config.yml index 50bae41be..7f4ed4bfc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,6 +66,11 @@ jobs: - restore_cache: keys: - v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }} + - run: + name: install ncat + command: | + # utility needed to test socket connection with curl < 7.40 + sudo apt-get install nmap - run: name: install stack & dependencies command: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b5c0947..8a9557980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- #1186, Add support for user defined unix socket via `server-unix-socket` config option - #690, Add `?columns` query parameter for faster bulk inserts, also ignores unspecified json keys in a payload - @steve-chavez - #1239, Add support for resource embedding on materialized views - @vitorbaptista - #1264, Add support for bulk RPC call - @steve-chavez diff --git a/main/Main.hs b/main/Main.hs index 861683224..f697b602c 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -15,13 +15,22 @@ import Control.Retry (RetryStatus, capDelay, import Data.IORef (IORef, atomicWriteIORef, newIORef, readIORef) import Data.String (IsString (..)) -import Data.Text (pack, replace, strip, stripPrefix) +import Data.Text (pack, replace, strip, stripPrefix, + unpack) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.IO (hPutStrLn, readFile) import Data.Time.Clock (getCurrentTime) +import Network.Socket (Family (AF_UNIX), + SockAddr (SockAddrUnix), Socket, + SocketType (Stream), bind, close, + defaultProtocol, listen, + maxListenQueue, socket) import Network.Wai.Handler.Warp (defaultSettings, runSettings, - setHost, setPort, setServerName) + runSettingsSocket, setHost, setPort, + setServerName) +import System.Directory (removeFile) import System.IO (BufferMode (..), hSetBuffering) +import System.IO.Error (isDoesNotExistError) import PostgREST.App (postgrest) import PostgREST.Config (AppConfig (..), configPoolTimeout', @@ -153,6 +162,7 @@ main = do let host = configHost conf port = configPort conf proxy = configProxyUri conf + maybeSocketAddr = configSocket conf pgSettings = toS (configDatabase conf) -- is the db-uri roleClaimKey = configRoleClaimKey conf appSettings = @@ -170,7 +180,6 @@ main = do when (isLeft roleClaimKey) $ panic $ show roleClaimKey - putStrLn $ ("Listening on port " :: Text) <> show (configPort conf) -- -- create connection pool with the provided settings, returns either -- a 'Connection' or a 'ConnectionError'. Does not throw. @@ -222,19 +231,31 @@ main = do -- ask for the OS time at most once per second getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime} - -- run the postgrest application - runSettings appSettings $ - postgrest - conf - refDbStructure - pool - getTime - (connectionWorker - mainTid - pool - (configSchema conf) - refDbStructure - refIsWorkerOn) + let postgrestApplication = + postgrest + conf + refDbStructure + pool + getTime + (connectionWorker + mainTid + pool + (configSchema conf) + refDbStructure + refIsWorkerOn) + in case maybeSocketAddr of + Nothing -> do + -- run the postgrest application + putStrLn $ ("Listening on port " :: Text) <> show (configPort conf) + runSettings appSettings postgrestApplication + Just socketAddr -> do + -- run postgrest application with user defined socket + sock <- createAndBindSocket (unpack socketAddr) + listen sock maxListenQueue + putStrLn $ ("Listening on unix socket " :: Text) <> show socketAddr + runSettingsSocket appSettings sock postgrestApplication + -- clean socket up when done + close sock {-| The purpose of this function is to load the JWT secret from a file if @@ -302,3 +323,15 @@ loadDbUriFile conf = extractDbUri mDbUri Nothing -> return dbUri Just filename -> strip <$> readFile (toS filename) setDbUri dbUri = conf {configDatabase = dbUri} + +createAndBindSocket :: FilePath -> IO Socket +createAndBindSocket filePath = do + deleteSocketFileIfExist filePath + sock <- socket AF_UNIX Stream defaultProtocol + bind sock $ SockAddrUnix filePath + return sock + where + deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist + handleDoesNotExist e + | isDoesNotExistError e = return () + | otherwise = throwIO e diff --git a/postgrest.cabal b/postgrest.cabal index 276413de3..fba09331e 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -33,17 +33,20 @@ executable postgrest default-language: Haskell2010 build-depends: auto-update , base >= 4.8 && < 4.10 - , base64-bytestring - , bytestring , hasql >= 1.3 && < 1.4 , hasql-pool >= 0.5 && < 0.6 , hasql-transaction >= 0.7 && < 0.8 + , network == 2.6.3.2 , postgrest , protolude == 0.2.2 , retry , text , time , warp + , bytestring + , base64-bytestring + , retry + , directory if !os(windows) build-depends: unix diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index fb20dca20..aa6255c3e 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -70,6 +70,7 @@ data AppConfig = AppConfig { , configSchema :: Text , configHost :: Text , configPort :: Int + , configSocket :: Maybe Text , configJwtSecret :: Maybe B.ByteString , configJwtSecretIsBase64 :: Bool @@ -149,6 +150,7 @@ readOptions = do <*> reqString "db-schema" <*> (fromMaybe "!4" . mfilter (/= "") <$> optString "server-host") <*> (fromMaybe 3000 . join . fmap coerceInt <$> optValue "server-port") + <*> optString "server-unix-socket" <*> (fmap encodeUtf8 . mfilter (/= "") <$> optString "jwt-secret") <*> (fromMaybe False . join . fmap coerceBool <$> optValue "secret-is-base64") <*> parseJwtAudience "jwt-aud" @@ -231,6 +233,10 @@ readOptions = do |server-host = "!4" |server-port = 3000 | + |## unix socket location + |## if specified it takes precedence over server-port + |# server-unix-socket = "/tmp/pgrst.sock" + | |## base url for swagger output |# server-proxy-uri = "" | diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index d262e198c..3b9559632 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -64,6 +64,8 @@ getEnvVarWithDefault var def = toS <$> _baseCfg :: AppConfig _baseCfg = -- Connection Settings AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000 + -- No user configured Unix Socket + Nothing -- Jwt settings (Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False Nothing -- Connection Modifiers diff --git a/test/io-tests.sh b/test/io-tests.sh index 3e1d1521e..d27c4ad24 100755 --- a/test/io-tests.sh +++ b/test/io-tests.sh @@ -181,6 +181,30 @@ ensureAppSettings(){ pgrStop } +getSocketStatus() { + curl -sL -w "%{http_code}\\n" -o /dev/null localhost:54321 +} + +socketConnection(){ + # map port 54321 traffic to unix socket as workaround for curl below 7.40 + # not supporting --unix-socket flag + ncat -vlk 54321 -c 'ncat -U /tmp/postgrest.sock' & + pgrStart "./configs/unix-socket.config" + while pgrStarted && test "$( getSocketStatus )" -ne 200 + do + # wait for the server to start + sleep 0.1 \ + || sleep 1 # fallback: subsecond sleep is not standard and may fail + done + if test $( getSocketStatus ) -eq 200 + then + ok "Succesfully connected through unix socket" + else + ko "Failed to connect through unix socket" + fi + pgrStop +} + # PRE: curl must be available test -n "$(command -v curl)" || bailOut 'curl is not available' @@ -191,6 +215,8 @@ setUp echo "Running IO tests.." +socketConnection + readSecretFromFile word.noeol 'simple (no EOL)' readSecretFromFile word.txt 'simple' readSecretFromFile ascii.noeol 'ASCII (no EOL)' @@ -224,6 +250,7 @@ invalidRoleClaimKey 1234 ensureIatClaimWorks ensureAppSettings + cleanUp exit $failedTests diff --git a/test/io-tests/configs/unix-socket.config b/test/io-tests/configs/unix-socket.config new file mode 100644 index 000000000..88c1163f1 --- /dev/null +++ b/test/io-tests/configs/unix-socket.config @@ -0,0 +1,7 @@ +db-uri = "postgres:///postgrest_test" +db-schema = "test" +db-anon-role = "postgrest_test_anonymous" +db-pool = 1 +server-host = "127.0.0.1" +server-unix-socket = "/tmp/postgrest.sock" +jwt-secret = "reallyreallyreallyreallyverysafe"