mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 01:44:03 +03:00
a8d1002175
In preparation for tightening up the various ways in which we construct and work with session variables, I am trying to move the behavior into the same module(s) as the data types, so that we can avoid exposing the internals of data structures in favor of smart constructors and conversions. The session variable code was split between `Hasura.RQL.Types.Roles`, `Hasura.RQL.Types.Session`, and `Hasura.Session`, with the first two containing most of the data structures (and some logic) and the latter containing the rest of the logic. These files do not interact with the rest of `Hasura.RQL`, though they are depended upon by that namespace. I have refactored these files into a new namespace, `Hasura.Authentication`. It now looks like this: 1. Role types are now in `Hasura.Authentication.Role`. 2. Header constants were moved from `Hasura.Server.Utils` to `Hasura.Authentication.Headers` (plural) to avoid cycles. 3. Header logic was moved from various places into `Hasura.Authentication.Header` (singular) for the same reason. 4. Session variable types and logic live together in `Hasura.Authentication.Session`. 5. User info types and logic live together in `Hasura.Authentication.User`. This new structure is cycle-free and generally avoids importing the rest of the code, which means we should be able to start pruning the list of exports and locking down session variable construction. No behavior was changed in this changeset. The majority of changes are to the imports in a number of files; everything depends on these things. By splitting into multiple files, we also reduce the surface area of an individual import, which was a pleasant side-effect of this work. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10960 GitOrigin-RevId: 7cb962c06483cd9b92b80432aed5cabecb465cda
237 lines
11 KiB
Haskell
237 lines
11 KiB
Haskell
module Hasura.HTTP
|
|
( wreqOptions,
|
|
HttpException (..),
|
|
hdrsToText,
|
|
textToHdrs,
|
|
addDefaultHeaders,
|
|
defaultHeaders,
|
|
HttpResponse (..),
|
|
addHttpResponseHeaders,
|
|
getHTTPExceptionStatus,
|
|
serializeHTTPExceptionMessage,
|
|
ShowHeadersAndEnvVarInfo (..),
|
|
serializeHTTPExceptionWithErrorMessage,
|
|
serializeHTTPExceptionMessageForDebugging,
|
|
encodeHTTPRequestJSON,
|
|
ShowErrorInfo (..),
|
|
getHttpExceptionJson,
|
|
serializeServantClientErrorMessage,
|
|
serializeServantClientErrorMessageForDebugging,
|
|
)
|
|
where
|
|
|
|
import Control.Exception (Exception (..), fromException)
|
|
import Control.Lens hiding ((.=))
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.KeyMap qualified as KM
|
|
import Data.CaseInsensitive (mk, original)
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
import Data.Text qualified as T
|
|
import Data.Text.Conversions (UTF8 (..), convertText)
|
|
import Data.Text.Encoding qualified as TE
|
|
import Data.Text.Encoding.Error qualified as TE
|
|
import Hasura.Authentication.Header (redactSensitiveHeader)
|
|
import Hasura.Prelude
|
|
import Hasura.Server.Version (currentVersion)
|
|
import Network.HTTP.Client qualified as HTTP
|
|
import Network.HTTP.Client.Restricted qualified as Restricted
|
|
import Network.HTTP.Media qualified as HTTP
|
|
import Network.HTTP.Types qualified as HTTP
|
|
import Network.Wreq qualified as Wreq
|
|
import Servant.Client qualified as Servant
|
|
|
|
hdrsToText :: [HTTP.Header] -> [(Text, Text)]
|
|
hdrsToText hdrs =
|
|
[ (bsToTxt $ original hdrName, bsToTxt hdrVal)
|
|
| (hdrName, hdrVal) <- hdrs
|
|
]
|
|
|
|
textToHdrs :: [(Text, Text)] -> [HTTP.Header]
|
|
textToHdrs hdrs =
|
|
[ (mk (txtToBs hdrName), TE.encodeUtf8 hdrVal)
|
|
| (hdrName, hdrVal) <- hdrs
|
|
]
|
|
|
|
wreqOptions :: HTTP.Manager -> [HTTP.Header] -> Wreq.Options
|
|
wreqOptions manager hdrs =
|
|
Wreq.defaults
|
|
& Wreq.headers .~ addDefaultHeaders hdrs
|
|
& Wreq.checkResponse ?~ (\_ _ -> return ())
|
|
& Wreq.manager .~ Right manager
|
|
|
|
-- Adds defaults headers overwriting any existing ones
|
|
addDefaultHeaders :: [HTTP.Header] -> [HTTP.Header]
|
|
addDefaultHeaders hdrs = defaultHeaders <> rmDefaultHeaders hdrs
|
|
where
|
|
rmDefaultHeaders = filter (not . isDefaultHeader)
|
|
|
|
isDefaultHeader :: HTTP.Header -> Bool
|
|
isDefaultHeader (hdrName, _) = hdrName `elem` map fst defaultHeaders
|
|
|
|
defaultHeaders :: [HTTP.Header]
|
|
defaultHeaders = [contentType, userAgent]
|
|
where
|
|
contentType = ("Content-Type", "application/json")
|
|
userAgent =
|
|
( "User-Agent",
|
|
"hasura-graphql-engine/" <> unUTF8 (convertText currentVersion)
|
|
)
|
|
|
|
newtype HttpException = HttpException
|
|
{unHttpException :: HTTP.HttpException}
|
|
deriving (Show)
|
|
|
|
getHTTPExceptionStatus :: HttpException -> Maybe Int
|
|
getHTTPExceptionStatus = \case
|
|
(HttpException (HTTP.HttpExceptionRequest _ httpExceptionContent)) ->
|
|
case httpExceptionContent of
|
|
HTTP.StatusCodeException response _ -> Just $ HTTP.statusCode $ HTTP.responseStatus response
|
|
HTTP.ProxyConnectException _ _ status -> Just $ HTTP.statusCode status
|
|
_ -> Nothing
|
|
(HttpException (HTTP.InvalidUrlException _ _)) -> Nothing
|
|
|
|
serializeHTTPExceptionMessage :: HttpException -> Text
|
|
serializeHTTPExceptionMessage (HttpException (HTTP.HttpExceptionRequest _ httpExceptionContent)) =
|
|
case httpExceptionContent of
|
|
HTTP.StatusCodeException _ _ -> "unexpected"
|
|
HTTP.TooManyRedirects _ -> "Too many redirects"
|
|
HTTP.OverlongHeaders -> "Overlong headers"
|
|
HTTP.ResponseTimeout -> "Response timeout"
|
|
HTTP.ConnectionTimeout -> "Connection timeout"
|
|
HTTP.ConnectionFailure _ -> "Connection failure"
|
|
HTTP.InvalidStatusLine _ -> "Invalid HTTP Status Line"
|
|
HTTP.InternalException err -> case fromException err of
|
|
Just (Restricted.ConnectionRestricted _ _) -> "Blocked connection to private IP address"
|
|
Nothing -> "Internal Exception"
|
|
HTTP.ProxyConnectException {} -> "Proxy connection exception"
|
|
HTTP.NoResponseDataReceived -> "No response data received"
|
|
HTTP.TlsNotSupported -> "TLS not supported"
|
|
HTTP.InvalidDestinationHost _ -> "Invalid destination host"
|
|
HTTP.InvalidHeader _ -> "Invalid Header"
|
|
HTTP.InvalidRequestHeader _ -> "Invalid Request Header"
|
|
HTTP.WrongRequestBodyStreamSize _ _ -> "Wrong request body stream size"
|
|
HTTP.ResponseBodyTooShort _ _ -> "Response body too short"
|
|
HTTP.InvalidChunkHeaders -> "Invalid chunk headers"
|
|
HTTP.IncompleteHeaders -> "Incomplete headers"
|
|
_ -> "unexpected"
|
|
serializeHTTPExceptionMessage (HttpException (HTTP.InvalidUrlException url reason)) = T.pack $ "URL: " <> url <> " is invalid because " <> reason
|
|
|
|
newtype ShowHeadersAndEnvVarInfo = ShowHeadersAndEnvVarInfo {unShowHeadersAndEnvVarInfo :: Bool}
|
|
deriving (Show, Eq)
|
|
|
|
serializeHTTPExceptionWithErrorMessage :: ShowHeadersAndEnvVarInfo -> HTTP.HttpException -> Text
|
|
serializeHTTPExceptionWithErrorMessage (ShowHeadersAndEnvVarInfo isShowHeaderAndEnvVarInfo) = \case
|
|
HTTP.HttpExceptionRequest _ err -> case err of
|
|
HTTP.StatusCodeException response _ -> "Response status code indicated failure" <> (tshow . HTTP.statusCode $ HTTP.responseStatus response)
|
|
HTTP.TooManyRedirects redirects -> "Too many redirects: " <> tshow (length redirects) <> " redirects"
|
|
HTTP.OverlongHeaders -> "Overlong headers"
|
|
HTTP.ResponseTimeout -> "Response timeout"
|
|
HTTP.ConnectionTimeout -> "Connection timeout"
|
|
HTTP.ConnectionFailure exn -> "Connection failure: " <> serializeExceptionForDebugging exn
|
|
HTTP.InvalidStatusLine statusLine -> "Invalid HTTP status line: " <> fromUtf8 statusLine
|
|
HTTP.InvalidHeader header ->
|
|
if isShowHeaderAndEnvVarInfo
|
|
then "Invalid header: " <> fromUtf8 header
|
|
else "Invalid Header"
|
|
HTTP.InvalidRequestHeader requestHeader ->
|
|
if isShowHeaderAndEnvVarInfo
|
|
then "Invalid request header: " <> fromUtf8 requestHeader
|
|
else "Invalid request header"
|
|
HTTP.InternalException exn -> case fromException exn of
|
|
Just (Restricted.ConnectionRestricted _ _) -> "Blocked connection to private IP address: " <> serializeExceptionForDebugging exn
|
|
Nothing -> "Internal error: " <> serializeExceptionForDebugging exn
|
|
HTTP.ProxyConnectException proxyHost port status -> "Proxy connection to " <> fromUtf8 proxyHost <> ":" <> tshow port <> " returned response with status code that indicated failure: " <> tshow (HTTP.statusCode status)
|
|
HTTP.NoResponseDataReceived -> "No response data received"
|
|
HTTP.TlsNotSupported -> "TLS not supported"
|
|
HTTP.WrongRequestBodyStreamSize expected actual -> "Wrong request body stream size. expected: " <> tshow expected <> ", actual: " <> tshow actual
|
|
HTTP.ResponseBodyTooShort expected actual -> "Response body too short. expected: " <> tshow expected <> ", actual: " <> tshow actual
|
|
HTTP.InvalidChunkHeaders -> "Invalid chunk headers"
|
|
HTTP.IncompleteHeaders -> "Incomplete headers"
|
|
HTTP.InvalidDestinationHost host -> "Invalid destination host: " <> fromUtf8 host
|
|
HTTP.HttpZlibException exn -> "HTTP zlib error: " <> serializeExceptionForDebugging exn
|
|
HTTP.InvalidProxyEnvironmentVariable name value ->
|
|
if isShowHeaderAndEnvVarInfo
|
|
then "Invalid proxy environment variable: " <> name <> "=" <> value
|
|
else "Invalid proxy environment variable: " <> name
|
|
HTTP.ConnectionClosed -> "Connection closed"
|
|
HTTP.InvalidProxySettings err' -> "Invalid proxy settings: " <> err'
|
|
HTTP.InvalidUrlException url' reason -> "Invalid url: " <> T.pack url' <> "; reason: " <> T.pack reason
|
|
where
|
|
fromUtf8 = TE.decodeUtf8With TE.lenientDecode
|
|
|
|
serializeHTTPExceptionMessageForDebugging :: HTTP.HttpException -> Text
|
|
serializeHTTPExceptionMessageForDebugging = serializeHTTPExceptionWithErrorMessage (ShowHeadersAndEnvVarInfo True)
|
|
|
|
encodeHTTPRequestJSON :: HTTP.Request -> J.Value
|
|
encodeHTTPRequestJSON request =
|
|
J.Object
|
|
$ KM.fromList
|
|
[ ("host", J.toJSON $ TE.decodeUtf8 $ HTTP.host request),
|
|
("port", J.toJSON $ HTTP.port request),
|
|
("secure", J.toJSON $ HTTP.secure request),
|
|
("requestHeaders", J.toJSON $ HashMap.fromList $ hdrsToText $ map redactSensitiveHeader $ HTTP.requestHeaders request),
|
|
("path", J.toJSON $ TE.decodeUtf8 $ HTTP.path request),
|
|
("queryString", J.toJSON $ TE.decodeUtf8 $ HTTP.queryString request),
|
|
("method", J.toJSON $ TE.decodeUtf8 $ HTTP.method request),
|
|
("responseTimeout", J.String $ tshow $ HTTP.responseTimeout request)
|
|
]
|
|
|
|
newtype ShowErrorInfo = ShowErrorInfo {unShowErrorInfo :: Bool}
|
|
deriving (Show, Eq)
|
|
|
|
-- this function excepts a boolean value (`ShowErrorInfo`) when True, exposes the errors associated with the HTTP
|
|
-- Exceptions using `serializeHTTPExceptionWithErrorMessage` function.
|
|
-- This function is used in event triggers, scheduled triggers and cron triggers where `ShowErrorInfo` is True
|
|
getHttpExceptionJson :: ShowErrorInfo -> HttpException -> J.Value
|
|
getHttpExceptionJson (ShowErrorInfo isShowHTTPErrorInfo) httpException =
|
|
case httpException of
|
|
(HttpException (HTTP.InvalidUrlException _ e)) ->
|
|
J.object
|
|
[ "type" J..= ("invalid_url" :: Text),
|
|
"message" J..= e
|
|
]
|
|
(HttpException (HTTP.HttpExceptionRequest req _)) -> do
|
|
let statusMaybe = getHTTPExceptionStatus httpException
|
|
exceptionContent =
|
|
if isShowHTTPErrorInfo
|
|
then serializeHTTPExceptionWithErrorMessage (ShowHeadersAndEnvVarInfo False) (unHttpException httpException)
|
|
else serializeHTTPExceptionMessage httpException
|
|
reqJSON = encodeHTTPRequestJSON req
|
|
J.object
|
|
$ [ "type" J..= ("http_exception" :: Text),
|
|
"message" J..= exceptionContent,
|
|
"request" J..= reqJSON
|
|
]
|
|
<> maybe mempty (\status -> ["status" J..= status]) statusMaybe
|
|
|
|
-- it will not show HTTP Exception error message info
|
|
instance J.ToJSON HttpException where
|
|
toJSON httpException = getHttpExceptionJson (ShowErrorInfo False) httpException
|
|
|
|
data HttpResponse a = HttpResponse
|
|
{ _hrBody :: !a,
|
|
_hrHeaders :: !HTTP.ResponseHeaders
|
|
}
|
|
deriving (Functor, Foldable, Traversable)
|
|
|
|
addHttpResponseHeaders :: HTTP.ResponseHeaders -> HttpResponse a -> HttpResponse a
|
|
addHttpResponseHeaders newHeaders (HttpResponse b h) = HttpResponse b (newHeaders <> h)
|
|
|
|
serializeServantClientErrorMessage :: Servant.ClientError -> Text
|
|
serializeServantClientErrorMessage = \case
|
|
Servant.FailureResponse _ response -> "response status code indicated failure: " <> (tshow . HTTP.statusCode $ Servant.responseStatusCode response)
|
|
Servant.DecodeFailure decodeErrorText _ -> "unable to decode the response, " <> decodeErrorText
|
|
Servant.UnsupportedContentType mediaType _ -> "unsupported content type in response: " <> TE.decodeUtf8With TE.lenientDecode (HTTP.renderHeader mediaType)
|
|
Servant.InvalidContentTypeHeader _ -> "invalid content type in response"
|
|
Servant.ConnectionError _ -> "connection error"
|
|
|
|
serializeServantClientErrorMessageForDebugging :: Servant.ClientError -> Text
|
|
serializeServantClientErrorMessageForDebugging = \case
|
|
Servant.ConnectionError exn -> case fromException exn of
|
|
Just httpException -> serializeHTTPExceptionMessageForDebugging httpException
|
|
Nothing -> "error in the connection: " <> serializeExceptionForDebugging exn
|
|
other -> serializeServantClientErrorMessage other
|
|
|
|
serializeExceptionForDebugging :: (Exception e) => e -> Text
|
|
serializeExceptionForDebugging = T.pack . displayException
|