2018-12-13 10:26:15 +03:00
module Hasura.HTTP
( wreqOptions ,
HttpException ( .. ) ,
2019-03-05 15:24:47 +03:00
hdrsToText ,
2023-03-14 21:27:21 +03:00
textToHdrs ,
2019-08-23 11:57:19 +03:00
addDefaultHeaders ,
2022-01-31 09:49:11 +03:00
defaultHeaders ,
2020-04-24 10:55:51 +03:00
HttpResponse ( .. ) ,
2021-08-25 04:52:38 +03:00
addHttpResponseHeaders ,
2021-09-20 16:14:28 +03:00
getHTTPExceptionStatus ,
serializeHTTPExceptionMessage ,
2023-03-21 14:58:16 +03:00
ShowHeadersAndEnvVarInfo ( .. ) ,
serializeHTTPExceptionWithErrorMessage ,
2022-07-11 11:04:30 +03:00
serializeHTTPExceptionMessageForDebugging ,
2023-03-21 14:58:16 +03:00
encodeHTTPRequestJSON ,
ShowErrorInfo ( .. ) ,
getHttpExceptionJson ,
2022-07-11 11:04:30 +03:00
serializeServantClientErrorMessage ,
serializeServantClientErrorMessageForDebugging ,
2018-12-13 10:26:15 +03:00
)
where
2021-09-24 01:56:37 +03:00
2022-07-11 11:04:30 +03:00
import Control.Exception ( Exception ( .. ) , fromException )
2020-01-23 00:55:55 +03:00
import Control.Lens hiding ( ( .= ) )
2018-12-13 10:26:15 +03:00
import Data.Aeson qualified as J
2022-06-08 18:31:28 +03:00
import Data.Aeson.KeyMap qualified as KM
2023-03-14 21:27:21 +03:00
import Data.CaseInsensitive ( mk , original )
2021-09-20 16:14:28 +03:00
import Data.HashMap.Strict qualified as M
import Data.Text qualified as T
2020-01-23 00:55:55 +03:00
import Data.Text.Conversions ( UTF8 ( .. ) , convertText )
2021-09-20 16:14:28 +03:00
import Data.Text.Encoding qualified as TE
2022-07-11 11:04:30 +03:00
import Data.Text.Encoding.Error qualified as TE
2018-11-23 16:02:46 +03:00
import Hasura.Prelude
2021-09-20 16:14:28 +03:00
import Hasura.Server.Utils ( redactSensitiveHeader )
2021-10-13 19:38:56 +03:00
import Hasura.Server.Version ( currentVersion )
2018-11-23 16:02:46 +03:00
import Network.HTTP.Client qualified as HTTP
server: http ip blocklist (closes #2449)
## Description
This PR is in reference to #2449 (support IP blacklisting for multitenant)
*RFC Update: Add support for IPv6 blocking*
### Solution and Design
Using [http-client-restricted](https://hackage.haskell.org/package/http-client-restricted) package, we're creating the HTTP manager with restricting capabilities. The IPs can be supplied from the CLI arguments as `--ipv4BlocklistCidrs cidr1, cidr2...` or `--disableDefaultIPv4Blocklist` for a default IP list. The new manager will block all requests to the provided CIDRs.
We are extracting the error message string to show the end-user that given IP is blocked from being set as a webhook. There are 2 ways to extract the error message "connection to IP address is blocked". Given below are the responses from event trigger to a blocked IP for these implementations:
- 6d74fde316f61e246c861befcca5059d33972fa7 - We return the error message string as a HTTPErr(HOther) from `Hasura/Eventing/HTTP.hs`.
```
{
"data": {
"message": "blocked connection to private IP address "
},
"version": "2",
"type": "client_error"
}
```
- 88e17456345cbb449a5ecd4877c84c9f319dbc25 - We case match on HTTPExceptionContent for InternaException in `Hasura/HTTP.hs` and extract the error message string from it. (this is implemented as it handles all the cases where pro engine makes webhook requests)
```
{
"data": {
"message": {
"type": "http_exception",
"message": "blocked connection to private IP address ",
"request": {
"secure": false,
"path": "/webhook",
"responseTimeout": "ResponseTimeoutMicro 60000000",
"queryString": "",
"method": "POST",
"requestHeaders": {
"Content-Type": "application/json",
"X-B3-ParentSpanId": "5ae6573edb2a6b36",
"X-B3-TraceId": "29ea7bd6de6ebb8f",
"X-B3-SpanId": "303137d9f1d4f341",
"User-Agent": "hasura-graphql-engine/cerebushttp-ip-blacklist-a793a0e41-dirty"
},
"host": "139.59.90.109",
"port": 8000
}
}
},
"version": "2",
"type": "client_error"
}
```
### Steps to test and verify
The restricted IPs can be used as webhooks in event triggers, and hasura will return an error message in reponse.
### Limitations, known bugs & workarounds
- The `http-client-restricted` has a needlessly complex interface, and puts effort into implementing proxy support which we don't want, so we've inlined a stripped down version.
- Performance constraint: As the blocking is checked for each request, if a long list of blocked CIDRs is supplied, iterating through all of them is not what we would prefer. Using trie is suggested to overcome this. (Added to RFC)
- Calls to Lux endpoints are inconsistent: We use either the http manager from the ProServeCtx which is unrestricted, or the http manager from the ServeCtx which is restricted (the latter through the instances for MonadMetadataApiAuthorization and UserAuthentication). (The failure scenario here would be: cloud sets PRO_ENDPOINT to something that resolves to an internal address, and then restricted requests to those endpoints fail, causing auth to fail on user requests. This is about HTTP requests to lux auth endpoints.)
## Changelog
- ✅ `CHANGELOG.md` is updated with user-facing content relevant to this PR.
## Affected components
- ✅ Server
- ✅ Tests
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3186
Co-authored-by: Robert <132113+robx@users.noreply.github.com>
GitOrigin-RevId: 5bd2de2d028bc416b02c99e996c7bebce56fb1e7
2022-02-25 16:29:55 +03:00
import Network.HTTP.Client.Restricted qualified as Restricted
2022-07-11 11:04:30 +03:00
import Network.HTTP.Media qualified as HTTP
2018-11-23 16:02:46 +03:00
import Network.HTTP.Types qualified as HTTP
import Network.Wreq qualified as Wreq
2022-07-11 11:04:30 +03:00
import Servant.Client qualified as Servant
2018-11-23 16:02:46 +03:00
2019-03-05 15:24:47 +03:00
hdrsToText :: [ HTTP . Header ] -> [ ( Text , Text ) ]
hdrsToText hdrs =
[ ( bsToTxt $ original hdrName , bsToTxt hdrVal )
| ( hdrName , hdrVal ) <- hdrs
]
2023-03-14 21:27:21 +03:00
textToHdrs :: [ ( Text , Text ) ] -> [ HTTP . Header ]
textToHdrs hdrs =
[ ( mk ( txtToBs hdrName ) , TE . encodeUtf8 hdrVal )
| ( hdrName , hdrVal ) <- hdrs
]
2021-10-13 19:38:56 +03:00
wreqOptions :: HTTP . Manager -> [ HTTP . Header ] -> Wreq . Options
2018-11-23 16:02:46 +03:00
wreqOptions manager hdrs =
Wreq . defaults
2019-08-23 11:57:19 +03:00
& Wreq . headers .~ addDefaultHeaders hdrs
2018-11-23 16:02:46 +03:00
& Wreq . checkResponse ?~ ( \ _ _ -> return () )
& Wreq . manager .~ Right manager
2019-08-23 11:57:19 +03:00
-- Adds defaults headers overwriting any existing ones
2021-10-13 19:38:56 +03:00
addDefaultHeaders :: [ HTTP . Header ] -> [ HTTP . Header ]
2019-08-23 11:57:19 +03:00
addDefaultHeaders hdrs = defaultHeaders <> rmDefaultHeaders hdrs
where
rmDefaultHeaders = filter ( not . isDefaultHeader )
2021-10-13 19:38:56 +03:00
isDefaultHeader :: HTTP . Header -> Bool
2020-04-24 10:55:51 +03:00
isDefaultHeader ( hdrName , _ ) = hdrName ` elem ` map fst defaultHeaders
2019-08-23 11:57:19 +03:00
2021-10-13 19:38:56 +03:00
defaultHeaders :: [ HTTP . Header ]
2019-08-23 11:57:19 +03:00
defaultHeaders = [ contentType , userAgent ]
2018-11-23 16:02:46 +03:00
where
contentType = ( " Content-Type " , " application/json " )
userAgent =
( " User-Agent " ,
2020-01-23 00:55:55 +03:00
" hasura-graphql-engine/ " <> unUTF8 ( convertText currentVersion )
2018-11-23 16:02:46 +03:00
)
2018-12-13 10:26:15 +03:00
newtype HttpException = HttpException
{ unHttpException :: HTTP . HttpException }
deriving ( Show )
2021-09-20 16:14:28 +03:00
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 "
server: http ip blocklist (closes #2449)
## Description
This PR is in reference to #2449 (support IP blacklisting for multitenant)
*RFC Update: Add support for IPv6 blocking*
### Solution and Design
Using [http-client-restricted](https://hackage.haskell.org/package/http-client-restricted) package, we're creating the HTTP manager with restricting capabilities. The IPs can be supplied from the CLI arguments as `--ipv4BlocklistCidrs cidr1, cidr2...` or `--disableDefaultIPv4Blocklist` for a default IP list. The new manager will block all requests to the provided CIDRs.
We are extracting the error message string to show the end-user that given IP is blocked from being set as a webhook. There are 2 ways to extract the error message "connection to IP address is blocked". Given below are the responses from event trigger to a blocked IP for these implementations:
- 6d74fde316f61e246c861befcca5059d33972fa7 - We return the error message string as a HTTPErr(HOther) from `Hasura/Eventing/HTTP.hs`.
```
{
"data": {
"message": "blocked connection to private IP address "
},
"version": "2",
"type": "client_error"
}
```
- 88e17456345cbb449a5ecd4877c84c9f319dbc25 - We case match on HTTPExceptionContent for InternaException in `Hasura/HTTP.hs` and extract the error message string from it. (this is implemented as it handles all the cases where pro engine makes webhook requests)
```
{
"data": {
"message": {
"type": "http_exception",
"message": "blocked connection to private IP address ",
"request": {
"secure": false,
"path": "/webhook",
"responseTimeout": "ResponseTimeoutMicro 60000000",
"queryString": "",
"method": "POST",
"requestHeaders": {
"Content-Type": "application/json",
"X-B3-ParentSpanId": "5ae6573edb2a6b36",
"X-B3-TraceId": "29ea7bd6de6ebb8f",
"X-B3-SpanId": "303137d9f1d4f341",
"User-Agent": "hasura-graphql-engine/cerebushttp-ip-blacklist-a793a0e41-dirty"
},
"host": "139.59.90.109",
"port": 8000
}
}
},
"version": "2",
"type": "client_error"
}
```
### Steps to test and verify
The restricted IPs can be used as webhooks in event triggers, and hasura will return an error message in reponse.
### Limitations, known bugs & workarounds
- The `http-client-restricted` has a needlessly complex interface, and puts effort into implementing proxy support which we don't want, so we've inlined a stripped down version.
- Performance constraint: As the blocking is checked for each request, if a long list of blocked CIDRs is supplied, iterating through all of them is not what we would prefer. Using trie is suggested to overcome this. (Added to RFC)
- Calls to Lux endpoints are inconsistent: We use either the http manager from the ProServeCtx which is unrestricted, or the http manager from the ServeCtx which is restricted (the latter through the instances for MonadMetadataApiAuthorization and UserAuthentication). (The failure scenario here would be: cloud sets PRO_ENDPOINT to something that resolves to an internal address, and then restricted requests to those endpoints fail, causing auth to fail on user requests. This is about HTTP requests to lux auth endpoints.)
## Changelog
- ✅ `CHANGELOG.md` is updated with user-facing content relevant to this PR.
## Affected components
- ✅ Server
- ✅ Tests
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3186
Co-authored-by: Robert <132113+robx@users.noreply.github.com>
GitOrigin-RevId: 5bd2de2d028bc416b02c99e996c7bebce56fb1e7
2022-02-25 16:29:55 +03:00
HTTP . InternalException err -> case fromException err of
Just ( Restricted . ConnectionRestricted _ _ ) -> " Blocked connection to private IP address "
Nothing -> " Internal Exception "
2022-06-21 14:11:08 +03:00
HTTP . ProxyConnectException { } -> " Proxy connection exception "
2021-09-20 16:14:28 +03:00
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
2023-03-21 14:58:16 +03:00
newtype ShowHeadersAndEnvVarInfo = ShowHeadersAndEnvVarInfo { unShowHeadersAndEnvVarInfo :: Bool }
deriving ( Show , Eq )
serializeHTTPExceptionWithErrorMessage :: ShowHeadersAndEnvVarInfo -> HTTP . HttpException -> Text
serializeHTTPExceptionWithErrorMessage ( ShowHeadersAndEnvVarInfo isShowHeaderAndEnvVarInfo ) = \ case
2022-07-11 11:04:30 +03:00
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 status line: " <> fromUtf8 statusLine
2023-03-21 14:58:16 +03:00
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
2022-07-11 11:04:30 +03:00
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
2023-03-21 14:58:16 +03:00
HTTP . InvalidProxyEnvironmentVariable name value ->
if isShowHeaderAndEnvVarInfo
then " invalid proxy environment variable: " <> name <> " = " <> value
else " invalid proxy environment variable: " <> name
2022-07-11 11:04:30 +03:00
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
2023-03-21 14:58:16 +03:00
serializeHTTPExceptionMessageForDebugging :: HTTP . HttpException -> Text
serializeHTTPExceptionMessageForDebugging = serializeHTTPExceptionWithErrorMessage ( ShowHeadersAndEnvVarInfo True )
2021-09-20 16:14:28 +03:00
encodeHTTPRequestJSON :: HTTP . Request -> J . Value
encodeHTTPRequestJSON request =
J . Object $
2022-06-08 18:31:28 +03:00
KM . fromList
2021-09-20 16:14:28 +03:00
[ ( " host " , J . toJSON $ TE . decodeUtf8 $ HTTP . host request ) ,
( " port " , J . toJSON $ HTTP . port request ) ,
( " secure " , J . toJSON $ HTTP . secure request ) ,
( " requestHeaders " , J . toJSON $ M . 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 )
]
2023-03-21 14:58:16 +03:00
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
2018-12-13 10:26:15 +03:00
instance J . ToJSON HttpException where
2023-03-21 14:58:16 +03:00
toJSON httpException = getHttpExceptionJson ( ShowErrorInfo False ) httpException
2021-09-24 01:56:37 +03:00
2020-04-24 10:55:51 +03:00
data HttpResponse a = HttpResponse
{ _hrBody :: ! a ,
_hrHeaders :: ! HTTP . ResponseHeaders
}
deriving ( Functor , Foldable , Traversable )
2021-08-25 04:52:38 +03:00
addHttpResponseHeaders :: HTTP . ResponseHeaders -> HttpResponse a -> HttpResponse a
addHttpResponseHeaders newHeaders ( HttpResponse b h ) = HttpResponse b ( newHeaders <> h )
2022-07-11 11:04:30 +03:00
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