From dac66e1688695a25e9bb6562220650ec54f06cf7 Mon Sep 17 00:00:00 2001 From: Maksym Ivanov Date: Fri, 10 Nov 2023 10:05:27 +0100 Subject: [PATCH] feat: add config "healthcheck-path" to respond 200 on AWS LB TG healthcheck cannot currently be made to send any custom Host header. It always sends the target's (keter's) IP address in there. Together with vhosting, this makes it impossible to healthcheck individual webapps running under keter. Have a partial remedy with a /keter-health endpoint, that if enabled always responds with status 200. --- etc/keter-config.yaml | 5 +++-- keter.cabal | 1 + src/Keter/Config/V10.hs | 6 +++++- src/Keter/Proxy.hs | 17 +++++++++++++++++ test/Spec.hs | 1 + 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/etc/keter-config.yaml b/etc/keter-config.yaml index 2ff528af..71511e58 100644 --- a/etc/keter-config.yaml +++ b/etc/keter-config.yaml @@ -21,14 +21,15 @@ listeners: session: true # User to run applications as - # setuid: ubuntu # Get the user's IP address from x-forwarded-for. Useful when sitting behind a # load balancer like Amazon ELB. - # ip-from-header: true +# If set, this path will respond 200 OK to requests on any vhost +# healthcheck-path: /keter-health + # Control the port numbers assigned via APPROOT # external-http-port: 8080 # external-https-port: 450 diff --git a/keter.cabal b/keter.cabal index e4d76795..5e9eda06 100644 --- a/keter.cabal +++ b/keter.cabal @@ -114,6 +114,7 @@ library Keter.Yaml.FilePath other-modules: Keter.Aeson.KeyHelper + Paths_keter ghc-options: -Wall c-sources: cbits/process-tracker.c hs-source-dirs: src diff --git a/src/Keter/Config/V10.hs b/src/Keter/Config/V10.hs index b788eac8..2bdff432 100644 --- a/src/Keter/Config/V10.hs +++ b/src/Keter/Config/V10.hs @@ -114,6 +114,7 @@ data KeterConfig = KeterConfig , kconfigProxyException :: !(Maybe F.FilePath) , kconfigRotateLogs :: !Bool + , kconfigHealthcheckPath :: !(Maybe Text) } instance ToCurrent KeterConfig where @@ -134,6 +135,7 @@ instance ToCurrent KeterConfig where , kconfigMissingHostResponse = Nothing , kconfigProxyException = Nothing , kconfigRotateLogs = True + , kconfigHealthcheckPath = Nothing } where getSSL Nothing = V.empty @@ -162,6 +164,7 @@ defaultKeterConfig = KeterConfig , kconfigMissingHostResponse = Nothing , kconfigProxyException = Nothing , kconfigRotateLogs = True + , kconfigHealthcheckPath = Nothing } instance ParseYamlFile KeterConfig where @@ -187,6 +190,7 @@ instance ParseYamlFile KeterConfig where <*> o .:? "missing-host-response-file" <*> o .:? "proxy-exception-response-file" <*> o .:? "rotate-logs" .!= True + <*> o .:? "app-crash-hook" -- | Whether we should force redirect to HTTPS routes. type RequiresSecure = Bool @@ -317,7 +321,7 @@ instance ToCurrent RedirectConfig where instance ParseYamlFile RedirectConfig where parseYamlFile _ = withObject "RedirectConfig" $ \o -> RedirectConfig - <$> (Set.map CI.mk <$> ((o .: "hosts" <|> (Set.singleton <$> (o .: "host"))))) + <$> (Set.map CI.mk <$> (o .: "hosts" <|> Set.singleton <$> o .: "host")) <*> o .:? "status" .!= 303 <*> o .: "actions" <*> o .:? "ssl" .!= SSLFalse diff --git a/src/Keter/Proxy.hs b/src/Keter/Proxy.hs index e8b5bbdb..1750e01c 100644 --- a/src/Keter/Proxy.hs +++ b/src/Keter/Proxy.hs @@ -60,6 +60,7 @@ import System.FilePath (FilePath) import Control.Monad.Logger import Control.Exception (SomeException) import Network.HTTP.Types (mkStatus, + status200, status301, status302, status303, status307, status404, status502) @@ -76,6 +77,9 @@ import qualified Network.TLS as TLS import qualified System.Directory as Dir import Keter.Context +import Data.Version (showVersion) +import qualified Paths_keter as Pkg + #if !MIN_VERSION_http_reverse_proxy(0,6,0) defaultWaiProxySettings = def #endif @@ -91,6 +95,7 @@ data ProxySettings = MkProxySettings , psManager :: !Manager , psIpFromHeader :: Bool , psConnectionTimeBound :: Int + , psHealthcheckPath :: !(Maybe ByteString) , psUnknownHost :: ByteString -> ByteString , psMissingHost :: ByteString , psProxyException :: ByteString @@ -107,6 +112,7 @@ makeSettings hostman = do -- configuration option is in milliseconds let psConnectionTimeBound = kconfigConnectionTimeBound * 1000 let psIpFromHeader = kconfigIpFromHeader + let psHealthcheckPath = encodeUtf8 <$> kconfigHealthcheckPath pure $ MkProxySettings{..} where psHostLookup = HostMan.lookupAction hostman . CI.mk @@ -178,6 +184,10 @@ withClient isSecure = do getDest :: ProxySettings -> Wai.Request -> IO (LocalWaiProxySettings, WaiProxyResponse) + -- respond to healthckecks, regardless of Host header value and presence + getDest MkProxySettings{..} req | psHealthcheckPath == Just (Wai.rawPathInfo req) + = return (defaultLocalWaiProxySettings, WPRResponse healthcheckResponse) + -- inspect Host header to determine which App to proxy to getDest cfg@MkProxySettings{..} req = case Wai.requestHeaderHost req of Nothing -> do @@ -303,6 +313,13 @@ handleProxyException handleException onexceptBody except req respond = do handleException req except respond $ missingHostResponse onexceptBody +healthcheckResponse :: Wai.Response +healthcheckResponse = Wai.responseBuilder + status200 + [("Content-Type", "text/plain; charset=utf-8")] + $ "Keter " <> (copyByteString . S8.pack . showVersion) Pkg.version + <> " is doing okay!\n" + defaultProxyException :: ByteString defaultProxyException = "\nWelcome to Keter

Welcome to Keter

There was a proxy error, check the keter logs for details.

" diff --git a/test/Spec.hs b/test/Spec.hs index 89153ab5..201eb9ae 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -98,4 +98,5 @@ headThenPostNoCrash = do , psProxyException = "" , psIpFromHeader = False , psConnectionTimeBound = 5 * 60 * 1000 + , psHealthcheckPath = Nothing }