{-| Module : PostgREST.App Description : PostgREST main application This module is in charge of mapping HTTP requests to PostgreSQL queries. Some of its functionality includes: - Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION. - Producing HTTP Headers according to RFCs. - Content Negotiation -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ViewPatterns #-} module PostgREST.App ( postgrest , run ) where import GHC.IO.Exception (IOErrorType (..)) import System.IO.Error (ioeGetErrorType) import Control.Monad.Except (liftEither) import Control.Monad.Extra (whenJust) import Data.Either.Combinators (mapLeft, whenLeft) import Data.IORef (atomicWriteIORef, newIORef, readIORef) import Data.String (IsString (..), String) import Network.Wai.Handler.Warp (defaultSettings, setHost, setOnException, setPort, setServerName) import qualified Data.Text.Encoding as T import qualified Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Header as WaiHeader import qualified PostgREST.Admin as Admin import qualified PostgREST.ApiRequest as ApiRequest import qualified PostgREST.AppState as AppState import qualified PostgREST.Auth as Auth import qualified PostgREST.Cors as Cors import qualified PostgREST.Error as Error import qualified PostgREST.Listener as Listener import qualified PostgREST.MainTx as MainTx import qualified PostgREST.Plan as Plan import qualified PostgREST.Query as Query import qualified PostgREST.Response as Response import qualified PostgREST.Unix as Unix (installSignalHandlers) import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.AppState (AppState) import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Config (AppConfig (..)) import PostgREST.Error (Error) import PostgREST.Network (resolveSocketToAddress) import PostgREST.Observation (Observation (..)) import PostgREST.Response.Performance (ServerTiming (..), serverTimingHeader) import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.TimeIt (timeItT) import PostgREST.Version (docsVersion, prettyVersion) import Control.Monad.Writer import qualified Data.ByteString.Char8 as BS import qualified Data.List as L import Data.Streaming.Network (bindPortTCP) import qualified Data.Text as T import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types.Header (hVary) import qualified Network.Socket as NS import PostgREST.Unix (createAndBindDomainSocket) import System.Posix.Types (FileMode) import Protolude hiding (Handler) run :: AppState -> IO () run appState = do conf <- AppState.getConfig appState mainSocketRef <- newIORef Nothing adminSocket <- initAdminServerSocket conf let closeSockets = do whenJust adminSocket NS.close readIORef mainSocketRef >>= foldMap NS.close Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState) Admin.runAdmin appState adminSocket (readIORef mainSocketRef) (serverSettings conf) Listener.runListener appState -- Kick off and wait for the initial SchemaCache load before creating the -- main API socket. AppState.schemaCacheLoader appState AppState.waitForSchemaCacheInit appState mainSocket <- initServerSocket conf atomicWriteIORef mainSocketRef $ Just mainSocket let app = postgrest appState (AppState.schemaCacheLoader appState) address <- resolveSocketToAddress mainSocket observer $ AppServerAddressObs address Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app where observer = AppState.getObserver appState onWarpException :: Maybe Wai.Request -> SomeException -> IO () onWarpException _ ex = when (shouldDisplayException ex) $ observer $ WarpServerObs $ show ex -- Similar to wai defaultShouldDisplayException in -- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258 -- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases. -- We want to reuse this to avoid flooding the logs for some transient failure cases. shouldDisplayException :: SomeException -> Bool shouldDisplayException se | Just (_ :: Warp.InvalidRequest) <- fromException se = False | Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False | otherwise = True serverSettings :: AppConfig -> Warp.Settings serverSettings AppConfig{..} = defaultSettings & setHost (fromString $ toS configServerHost) & setPort configServerPort & setServerName ("postgrest/" <> prettyVersion) -- | PostgREST application postgrest :: AppState.AppState -> IO () -> Wai.Application postgrest appState connWorker = traceHeaderMiddleware appState . Cors.middleware appState $ \req respond -> do appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload maybeSchemaCache <- AppState.getSchemaCache appState let handleError = fmap (either (Error.errorResponseFor configClientErrorVerbosity) identity) -- writer to save authRole (uses `tell` for this and `getLast` to obtain it) -- has to be before runExceptT to make sure role is not lost on error (response, authRole) <- runWriterT . handleError . runExceptT $ do (jwtTime, authResult@AuthResult{..}) <- withTiming appConf $ Auth.getAuthResult appState $ ApiRequest.userBearerAuth req tell $ pure authRole postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req AppState.getObserver appState $ genResponseObs (getLast authRole) req response -- Launch the connWorker when the connection is down. The postgrest -- function can respond successfully (with a stale schema cache) before -- the connWorker is done. However, when there's an empty schema cache -- postgrest responds with the error `PGRST002`; this means that the schema -- cache is still loading, so we don't launch the connWorker here because -- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704 -- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker delay <- AppState.getNextDelay appState respond $ addRetryHint delay response where -- TODO WaiHeader.contentLength does a lookup everytime, see: https://hackage.haskell.org/package/wai-extra-3.1.17/docs/src/Network.Wai.Header.html#contentLength -- It might be possible to gain some perf by returning the response length from `postgrestResponse`. We calculate the length manually on Response.hs. genResponseObs :: Maybe ByteString -> Wai.Request -> Wai.Response -> Observation genResponseObs user req resp = ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp) postgrestResponse :: (MonadError Error m, MonadIO m) => AppState.AppState -> AppConfig -> Maybe SchemaCache -> Maybe Double -> AuthResult -> Wai.Request -> m Wai.Response postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do let observer = AppState.getObserver appState sCache <- case maybeSchemaCache of Just sCache -> return sCache Nothing -> do liftIO $ observer SchemaCacheEmptyObs throwError Error.NoSchemaCacheError let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache) body <- liftIO $ Wai.strictRequestBody req (parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body (planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s (txTime, txResult) <- withTiming conf $ do case tx of MainTx.NoDbTx r -> pure r MainTx.DbTx dbSession -> do dbRes <- liftIO $ AppState.usePool appState dbSession let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes -- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message. -- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS. -- This needs refactoring so only the below obsQuery is used. liftIO $ whenLeft eitherResp $ obsQuery . Error.status liftEither eitherResp (respTime, resp) <- withTiming conf $ do let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache status' = either Error.status Response.pgrstStatus response -- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status) liftIO $ obsQuery status' liftEither response return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp where toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod serverTimingHeaders :: ServerTiming -> [HTTP.Header] serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled] varyHeader :: HTTP.Header varyHeader = (hVary, "Accept, Prefer, Range") varyHeaderPresent :: [HTTP.Header] -> Bool varyHeaderPresent = any (\(h, _v) -> h == hVary) withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a) withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled then do (t, r) <- timeItT f pure (Just t, r) else do r <- f pure (Nothing, r) traceHeaderMiddleware :: AppState -> Wai.Middleware traceHeaderMiddleware appState app req respond = do conf <- AppState.getConfig appState case configServerTraceHeader conf of Nothing -> app req respond Just hdr -> let hdrVal = L.lookup hdr $ Wai.requestHeaders req in app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++)) addRetryHint :: Int -> Wai.Response -> Wai.Response addRetryHint delay response = do let h = ("Retry-After", BS.pack $ show delay) Wai.mapResponseHeaders (\hs -> if isServiceUnavailable response then h:hs else hs) response isServiceUnavailable :: Wai.Response -> Bool isServiceUnavailable response = Wai.responseStatus response == HTTP.status503 initSocket :: (Applicative f, Traversable f) => Maybe String -> FileMode -> Text -> f Int -> IO (f NS.Socket) initSocket unixSocket unixSocketMode tcpHost tcpPort = maybe initTCPSocket initDomainSocket unixSocket where initTCPSocket = traverse (`bindPortTCP` (fromString $ T.unpack tcpHost)) tcpPort -- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows, -- but we need to have runtime error if we try to use it in Windows, not compile time error initDomainSocket = fmap pure . (`createAndBindDomainSocket` unixSocketMode) initServerSocket :: AppConfig -> IO NS.Socket initServerSocket AppConfig{..} = runIdentity <$> initSocket configServerUnixSocket configServerUnixSocketMode configServerHost (pure configServerPort) initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket) initAdminServerSocket AppConfig{..} = initSocket configAdminServerUnixSocket configAdminServerUnixSocketMode configAdminServerHost configAdminServerPort