From 8771582cf0d9751400cfa5544ef85d74095ea54e Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 25 Feb 2019 16:44:27 +0530 Subject: [PATCH 01/12] forward response headers from remote servers (fix #1654) --- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 21 +++++----- .../Hasura/GraphQL/Transport/WebSocket.hs | 3 +- server/src-lib/Hasura/Server/App.hs | 24 +++++++----- server/src-lib/Hasura/Server/Auth.hs | 9 +---- server/src-lib/Hasura/Server/Utils.hs | 27 +++++++++++++ server/tests-py/graphql_server.py | 38 +++++++++++++++---- .../remote_schemas/check_resp_headers.yaml | 13 +++++++ server/tests-py/test_schema_stitching.py | 22 +++++++++++ 8 files changed, 122 insertions(+), 35 deletions(-) create mode 100644 server/tests-py/queries/remote_schemas/check_resp_headers.yaml diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 28c4076e270e0..d26c689971661 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -27,6 +27,8 @@ import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.HTTP import Hasura.RQL.DDL.Headers import Hasura.RQL.Types +import Hasura.Server.Utils (filterRequestHeaders, + filterResponseHeaders) import qualified Hasura.GraphQL.Resolve as R import qualified Hasura.GraphQL.Validate as VQ @@ -42,7 +44,7 @@ runGQ -> [N.Header] -> GraphQLRequest -> BL.ByteString -- this can be removed when we have a pretty-printer - -> m BL.ByteString + -> m (BL.ByteString, Maybe [(Text, Text)]) runGQ pool isoL userInfo sc manager reqHdrs req rawReq = do (gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxRoleMap @@ -104,7 +106,7 @@ runHasuraGQ -> UserInfo -> SchemaCache -> VQ.QueryParts - -> m BL.ByteString + -> m (BL.ByteString, Maybe [(Text, Text)]) runHasuraGQ pool isoL userInfo sc queryParts = do (gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxMap (opTy, fields) <- runReaderT (VQ.validateGQ queryParts) gCtx @@ -112,7 +114,7 @@ runHasuraGQ pool isoL userInfo sc queryParts = do "subscriptions are not supported over HTTP, use websockets instead" let tx = R.resolveSelSet userInfo gCtx opTy fields resp <- liftIO (runExceptT $ runTx tx) >>= liftEither - return $ encodeGQResp $ GQSuccess resp + return (encodeGQResp $ GQSuccess resp, Nothing) where gCtxMap = scGCtxMap sc runTx tx = runLazyTx pool isoL $ withUserInfo userInfo tx @@ -126,7 +128,7 @@ runRemoteGQ -- ^ the raw request string -> RemoteSchemaInfo -> G.TypedOperationDefinition - -> m BL.ByteString + -> m (BL.ByteString, Maybe [(Text, Text)]) runRemoteGQ manager userInfo reqHdrs q rsi opDef = do let opTy = G._todType opDef when (opTy == G.OperationTypeSubscription) $ @@ -138,7 +140,9 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res - return $ resp ^. Wreq.responseBody + let respHdrs = map (\(k, v) -> (CS.cs $ CI.original k, CS.cs v)) $ + filterResponseHeaders $ resp ^. Wreq.responseHeaders + return (resp ^. Wreq.responseBody, pure respHdrs) where RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi @@ -147,9 +151,4 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ userInfoToList userInfo - filteredHeaders = flip filter reqHdrs $ \(n, _) -> - n `notElem` [ "Content-Length", "Content-MD5", "User-Agent", "Host" - , "Origin", "Referer" , "Accept", "Accept-Encoding" - , "Accept-Language", "Accept-Datetime" - , "Cache-Control", "Connection", "DNT" - ] + filteredHeaders = filterRequestHeaders reqHdrs diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index 4d3ecb750acfb..ad2265de083d8 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -197,7 +197,8 @@ onStart serverEnv wsConn (StartMsg opId q) msgRaw = catchAndIgnore $ do withComplete $ sendConnErr "subscription to remote server is not supported" resp <- runExceptT $ TH.runRemoteGQ httpMgr userInfo reqHdrs msgRaw rsi opDef - either postExecErr sendSuccResp resp + -- ignore headers when sending response on websocket + either postExecErr sendSuccResp (fst <$> resp) sendCompleted where diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index be3028a59b6eb..eaaf765b816e6 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -146,11 +146,13 @@ logError logError userInfoM req reqBody sc qErr = logResult userInfoM req reqBody sc (Left qErr) Nothing +type Response = (BL.ByteString, Maybe [(Text, Text)]) + mkSpockAction :: (MonadIO m) => (Bool -> QErr -> Value) -> ServerCtx - -> Handler BL.ByteString + -> Handler Response -> ActionT m () mkSpockAction qErrEncoder serverCtx handler = do req <- request @@ -169,7 +171,8 @@ mkSpockAction qErrEncoder serverCtx handler = do t2 <- liftIO getCurrentTime -- for measuring response time purposes -- log result - logResult (Just userInfo) req reqBody serverCtx result $ Just (t1, t2) + logResult (Just userInfo) req reqBody serverCtx (fst <$> result) $ + Just (t1, t2) either (qErrToResp $ userRole userInfo == adminRole) resToResp result where @@ -184,8 +187,9 @@ mkSpockAction qErrEncoder serverCtx handler = do logError Nothing req reqBody serverCtx qErr qErrToResp includeInternal qErr - resToResp resp = do + resToResp (resp, mHdrs) = do uncurry setHeader jsonHeader + onJust mHdrs $ mapM_ (uncurry setHeader) lazyBytes resp withLock :: (MonadIO m, MonadError e m) @@ -200,11 +204,12 @@ withLock lk action = do acquireLock = liftIO $ takeMVar lk releaseLock = liftIO $ putMVar lk () -v1QueryHandler :: RQLQuery -> Handler BL.ByteString +v1QueryHandler :: RQLQuery -> Handler Response v1QueryHandler query = do lk <- scCacheLock . hcServerCtx <$> ask - bool (fst <$> dbAction) (withLock lk dbActionReload) $ + res <- bool (fst <$> dbAction) (withLock lk dbActionReload) $ queryNeedsReload query + return (res, Nothing) where -- Hit postgres dbAction = do @@ -230,7 +235,7 @@ v1QueryHandler query = do liftIO $ writeIORef scRef newSc' return resp -v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler BL.ByteString +v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler Response v1Alpha1GQHandler query = do userInfo <- asks hcUser reqBody <- asks hcReqBody @@ -242,14 +247,15 @@ v1Alpha1GQHandler query = do isoL <- scIsolation . hcServerCtx <$> ask GH.runGQ pool isoL userInfo sc manager reqHeaders query reqBody -gqlExplainHandler :: GE.GQLExplain -> Handler BL.ByteString +gqlExplainHandler :: GE.GQLExplain -> Handler Response gqlExplainHandler query = do onlyAdmin scRef <- scCacheRef . hcServerCtx <$> ask sc <- liftIO $ readIORef scRef pool <- scPGPool . hcServerCtx <$> ask isoL <- scIsolation . hcServerCtx <$> ask - GE.explainGQLQuery pool isoL sc query + res <- GE.explainGQLQuery pool isoL sc query + return (res, Nothing) newtype QueryParser = QueryParser { getQueryParser :: QualifiedTable -> Handler RQLQuery } @@ -271,7 +277,7 @@ queryParsers = q <- decodeValue val return $ f q -legacyQueryHandler :: TableName -> T.Text -> Handler BL.ByteString +legacyQueryHandler :: TableName -> T.Text -> Handler Response legacyQueryHandler tn queryType = case M.lookup queryType queryParsers of Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs index b53d8e2460cd2..83ac7220322f7 100644 --- a/server/src-lib/Hasura/Server/Auth.hs +++ b/server/src-lib/Hasura/Server/Auth.hs @@ -219,12 +219,7 @@ userInfoFromAuthHook logger manager hook reqHeaders = do (Just $ HttpException err) Nothing throw500 "Internal Server Error" - filteredHeaders = flip filter reqHeaders $ \(n, _) -> - n `notElem` [ "Content-Length", "Content-MD5", "User-Agent", "Host" - , "Origin", "Referer" , "Accept", "Accept-Encoding" - , "Accept-Language", "Accept-Datetime" - , "Cache-Control", "Connection", "DNT" - ] + filteredHeaders = filterRequestHeaders reqHeaders getUserInfo @@ -241,7 +236,7 @@ getUserInfo logger manager rawHeaders = \case AMAdminSecret adminScrt unAuthRole -> case adminSecretM of Just givenAdminScrt -> userInfoWhenAdminSecret adminScrt givenAdminScrt - Nothing -> userInfoWhenNoAdminSecret unAuthRole + Nothing -> userInfoWhenNoAdminSecret unAuthRole AMAdminSecretAndHook accKey hook -> whenAdminSecretAbsent accKey (userInfoFromAuthHook logger manager hook rawHeaders) diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 3030dd3f89347..e7074d23c13de 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -14,6 +14,7 @@ import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding.Error as TE import qualified Data.Text.IO as TI import qualified Language.Haskell.TH.Syntax as TH +import qualified Network.HTTP.Types as HTTP import qualified Text.Ginger as TG import qualified Text.Regex.TDFA as TDFA import qualified Text.Regex.TDFA.ByteString as TDFA @@ -134,3 +135,29 @@ matchRegex regex caseSensitive src = fmapL :: (a -> a') -> Either a b -> Either a' b fmapL fn (Left e) = Left (fn e) fmapL _ (Right x) = pure x + + +-- ignore the following request headers from the client +filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header] +filterRequestHeaders = filterHeaders reqHeaders + where + reqHeaders = [ "Content-Length", "Content-MD5", "User-Agent", "Host" + , "Origin", "Referer" , "Accept", "Accept-Encoding" + , "Accept-Language", "Accept-Datetime" + , "Cache-Control", "Connection", "DNT" + ] + + +-- ignore the following response headers from remote +filterResponseHeaders :: [HTTP.Header] -> [HTTP.Header] +filterResponseHeaders = filterHeaders respHeaders + where + respHeaders = [ "Server", "Transfer-Encoding", "Cache-Control" + , "Access-Control-Allow-Credentials" + , "Access-Control-Allow-Methods" + , "Access-Control-Allow-Origin" + , "Content-Type" + ] + +filterHeaders :: [HTTP.HeaderName] -> [HTTP.Header] -> [HTTP.Header] +filterHeaders list = filter (\(n, _) -> n `notElem` list) diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index 7f87bec9e7a71..1336d4540afd9 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -169,7 +169,30 @@ def post(self, req): res = person_schema.execute(req.json['query']) return mkJSONResp(res) -#GraphQL server with interfaces +# GraphQL server that returns Set-Cookie response header +class SampleAuth(graphene.ObjectType): + hello = graphene.String(arg=graphene.String(default_value="world")) + + def resolve_hello(self, info, arg): + return "Hello " + arg + +sample_auth_schema = graphene.Schema(query=SampleAuth, + subscription=SampleAuth) + +class SampleAuthGraphQL(RequestHandler): + def get(self, request): + return Response(HTTPStatus.METHOD_NOT_ALLOWED) + + def post(self, request): + if not request.json: + return Response(HTTPStatus.BAD_REQUEST) + res = hello_schema.execute(request.json['query']) + resp = mkJSONResp(res) + resp.headers['Set-Cookie'] = 'abcd' + return resp + + +# GraphQL server with interfaces class Character(graphene.Interface): id = graphene.ID(required=True) @@ -567,15 +590,15 @@ def post(self, req): if not req.json: return Response(HTTPStatus.BAD_REQUEST) res = echo_schema.execute(req.json['query']) - respDict = res.to_dict() - typesList = respDict.get('data',{}).get('__schema',{}).get('types',None) + resp_dict = res.to_dict() + types_list = resp_dict.get('data',{}).get('__schema',{}).get('types', None) #Hack around enum default_value serialization issue: https://github.com/graphql-python/graphql-core/issues/166 - if typesList is not None: - for t in filter(lambda ty: ty['name'] == 'EchoQuery', typesList): + if types_list is not None: + for t in filter(lambda ty: ty['name'] == 'EchoQuery', types_list): for f in filter(lambda fld: fld['name'] == 'echo', t['fields']): for a in filter(lambda arg: arg['name'] == 'enumInput', f['args']): a['defaultValue'] = 'RED' - return Response(HTTPStatus.OK, respDict, + return Response(HTTPStatus.OK, resp_dict, {'Content-Type': 'application/json'}) handlers = MkHandlers({ @@ -597,7 +620,8 @@ def post(self, req): '/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes, '/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType, '/default-value-echo-graphql' : EchoGraphQL, - '/person-graphql': PersonGraphQL + '/person-graphql': PersonGraphQL, + '/auth-graphql': SampleAuthGraphQL }) diff --git a/server/tests-py/queries/remote_schemas/check_resp_headers.yaml b/server/tests-py/queries/remote_schemas/check_resp_headers.yaml new file mode 100644 index 0000000000000..b19a74a5b0ee8 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/check_resp_headers.yaml @@ -0,0 +1,13 @@ +description: Check response headers from remote +url: /v1alpha1/graphql +status: 200 +response: + data: + hello: Hello me +response_headers: + 'Set-Cookie': abcd +query: + query: | + query { + hello(arg: "me") + } diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index b618b8def8d76..a026025ab535b 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -228,6 +228,28 @@ def test_add_schema_same_type_containing_same_scalar(self, hge_ctx): assert st_code == 200, resp +class TestRemoteSchemaResponseHeaders(): + teardown = {"type": "clear_metadata", "args": {}} + dir = 'queries/remote_schemas' + + @pytest.fixture(autouse=True) + def transact(self, hge_ctx): + q = mk_add_remote_q('sample-auth', 'http://localhost:5000/auth-graphql') + st_code, resp = hge_ctx.v1q(q) + assert st_code == 200, resp + yield + hge_ctx.v1q(self.teardown) + + def test_response_headers_from_remote(self, hge_ctx): + q = {'query': 'query { hello (arg: "me") }'} + resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q) + assert resp.status_code == 200 + assert ('Set-Cookie' in resp.headers and + resp.headers['Set-Cookie'] == 'abcd') + res = resp.json() + assert res['data']['hello'] == "Hello me" + + class TestAddRemoteSchemaCompareRootQueryFields: remote = 'http://localhost:5000/default-value-echo-graphql' From 3e00374f8ec381df8c80077944448583222de639 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 25 Feb 2019 17:08:20 +0530 Subject: [PATCH 02/12] add note in docs about response headers over websocket --- docs/graphql/manual/remote-schemas/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index 56a82131b8bbe..1ab877c741aa1 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -137,6 +137,14 @@ community tooling to write your own client-facing GraphQL gateway that interacts it out of the box** (*by as much as 4x*). If you need any help with remodeling these kind of use cases to use the built-in remote schemas feature, please get in touch with us on `Discord `__. +Response headers from your remote GraphQL servers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Response headers from your remote schema servers are sent back to the client +over HTTP transport. **Over websocket transport, the response headers are not +sent.** If you require the response headers from remote servers, use the HTTP +transport. + + Bypassing Hasura's authorization system for remote schema queries ----------------------------------------------------------------- From 74af57de8bf007e211f6db952bdfc9925b862def Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Tue, 26 Feb 2019 10:50:38 +0530 Subject: [PATCH 03/12] send admin secret headers in tests --- server/src-lib/Hasura/GraphQL/Transport/HTTP.hs | 5 +++-- server/tests-py/test_schema_stitching.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index d26c689971661..c42ff34a1b5e8 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -27,7 +27,8 @@ import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.HTTP import Hasura.RQL.DDL.Headers import Hasura.RQL.Types -import Hasura.Server.Utils (filterRequestHeaders, +import Hasura.Server.Utils (bsToTxt, + filterRequestHeaders, filterResponseHeaders) import qualified Hasura.GraphQL.Resolve as R @@ -140,7 +141,7 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res - let respHdrs = map (\(k, v) -> (CS.cs $ CI.original k, CS.cs v)) $ + let respHdrs = map (\(k, v) -> (bsToTxt $ CI.original k, bsToTxt v)) $ filterResponseHeaders $ resp ^. Wreq.responseHeaders return (resp ^. Wreq.responseBody, pure respHdrs) diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index a026025ab535b..746eac450f1d1 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -241,8 +241,12 @@ def transact(self, hge_ctx): hge_ctx.v1q(self.teardown) def test_response_headers_from_remote(self, hge_ctx): + headers = {} + if hge_ctx.hge_key: + headers = {'x-hasura-admin-secret': hge_ctx.hge_key} q = {'query': 'query { hello (arg: "me") }'} - resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q) + resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q, + headers=headers) assert resp.status_code == 200 assert ('Set-Cookie' in resp.headers and resp.headers['Set-Cookie'] == 'abcd') From c6b6973709496fffd7c81262a4b31c009abb9007 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Tue, 26 Feb 2019 13:52:13 +0530 Subject: [PATCH 04/12] wrap hasura's response and text headers in newtype --- server/graphql-engine.cabal | 1 + .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 13 ++++++----- .../Hasura/GraphQL/Transport/WebSocket.hs | 3 ++- server/src-lib/Hasura/Server/App.hs | 22 +++++++++---------- server/src-lib/Hasura/Server/Context.hs | 22 +++++++++++++++++++ 5 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 server/src-lib/Hasura/Server/Context.hs diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 9cf8c4f08a5f6..6c6705c8e2329 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -142,6 +142,7 @@ library , Hasura.Server.Auth , Hasura.Server.Auth.JWT , Hasura.Server.Init + , Hasura.Server.Context , Hasura.Server.Middleware , Hasura.Server.Logging , Hasura.Server.Query diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index c42ff34a1b5e8..0cbf67fc30af1 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -27,6 +27,7 @@ import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.HTTP import Hasura.RQL.DDL.Headers import Hasura.RQL.Types +import Hasura.Server.Context import Hasura.Server.Utils (bsToTxt, filterRequestHeaders, filterResponseHeaders) @@ -45,7 +46,7 @@ runGQ -> [N.Header] -> GraphQLRequest -> BL.ByteString -- this can be removed when we have a pretty-printer - -> m (BL.ByteString, Maybe [(Text, Text)]) + -> m HResponse runGQ pool isoL userInfo sc manager reqHdrs req rawReq = do (gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxRoleMap @@ -107,7 +108,7 @@ runHasuraGQ -> UserInfo -> SchemaCache -> VQ.QueryParts - -> m (BL.ByteString, Maybe [(Text, Text)]) + -> m HResponse runHasuraGQ pool isoL userInfo sc queryParts = do (gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxMap (opTy, fields) <- runReaderT (VQ.validateGQ queryParts) gCtx @@ -115,7 +116,7 @@ runHasuraGQ pool isoL userInfo sc queryParts = do "subscriptions are not supported over HTTP, use websockets instead" let tx = R.resolveSelSet userInfo gCtx opTy fields resp <- liftIO (runExceptT $ runTx tx) >>= liftEither - return (encodeGQResp $ GQSuccess resp, Nothing) + return $ HResponse (encodeGQResp $ GQSuccess resp) Nothing where gCtxMap = scGCtxMap sc runTx tx = runLazyTx pool isoL $ withUserInfo userInfo tx @@ -129,7 +130,7 @@ runRemoteGQ -- ^ the raw request string -> RemoteSchemaInfo -> G.TypedOperationDefinition - -> m (BL.ByteString, Maybe [(Text, Text)]) + -> m HResponse runRemoteGQ manager userInfo reqHdrs q rsi opDef = do let opTy = G._todType opDef when (opTy == G.OperationTypeSubscription) $ @@ -141,9 +142,9 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res - let respHdrs = map (\(k, v) -> (bsToTxt $ CI.original k, bsToTxt v)) $ + let respHdrs = map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $ filterResponseHeaders $ resp ^. Wreq.responseHeaders - return (resp ^. Wreq.responseBody, pure respHdrs) + return $ HResponse (resp ^. Wreq.responseBody) (Just respHdrs) where RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index ad2265de083d8..63996df7a54a7 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -24,6 +24,7 @@ import qualified STMContainers.Map as STMMap import Control.Concurrent (threadDelay) import qualified Data.IORef as IORef +import Hasura.Server.Context import Hasura.GraphQL.Resolve (resolveSelSet) import Hasura.GraphQL.Resolve.Context (LazyRespTx) @@ -198,7 +199,7 @@ onStart serverEnv wsConn (StartMsg opId q) msgRaw = catchAndIgnore $ do resp <- runExceptT $ TH.runRemoteGQ httpMgr userInfo reqHdrs msgRaw rsi opDef -- ignore headers when sending response on websocket - either postExecErr sendSuccResp (fst <$> resp) + either postExecErr sendSuccResp (_hrBody <$> resp) sendCompleted where diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index eaaf765b816e6..7d7b9e42a5678 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -43,6 +43,7 @@ import Hasura.RQL.DML.QueryTemplate import Hasura.RQL.Types import Hasura.Server.Auth (AuthMode (..), getUserInfo) +import Hasura.Server.Context import Hasura.Server.Cors import Hasura.Server.Init import Hasura.Server.Logging @@ -146,13 +147,12 @@ logError logError userInfoM req reqBody sc qErr = logResult userInfoM req reqBody sc (Left qErr) Nothing -type Response = (BL.ByteString, Maybe [(Text, Text)]) mkSpockAction :: (MonadIO m) => (Bool -> QErr -> Value) -> ServerCtx - -> Handler Response + -> Handler HResponse -> ActionT m () mkSpockAction qErrEncoder serverCtx handler = do req <- request @@ -171,7 +171,7 @@ mkSpockAction qErrEncoder serverCtx handler = do t2 <- liftIO getCurrentTime -- for measuring response time purposes -- log result - logResult (Just userInfo) req reqBody serverCtx (fst <$> result) $ + logResult (Just userInfo) req reqBody serverCtx (_hrBody <$> result) $ Just (t1, t2) either (qErrToResp $ userRole userInfo == adminRole) resToResp result @@ -187,9 +187,9 @@ mkSpockAction qErrEncoder serverCtx handler = do logError Nothing req reqBody serverCtx qErr qErrToResp includeInternal qErr - resToResp (resp, mHdrs) = do + resToResp (HResponse resp mHdrs) = do uncurry setHeader jsonHeader - onJust mHdrs $ mapM_ (uncurry setHeader) + onJust mHdrs $ mapM_ (uncurry setHeader . unHeader) lazyBytes resp withLock :: (MonadIO m, MonadError e m) @@ -204,12 +204,12 @@ withLock lk action = do acquireLock = liftIO $ takeMVar lk releaseLock = liftIO $ putMVar lk () -v1QueryHandler :: RQLQuery -> Handler Response +v1QueryHandler :: RQLQuery -> Handler HResponse v1QueryHandler query = do lk <- scCacheLock . hcServerCtx <$> ask res <- bool (fst <$> dbAction) (withLock lk dbActionReload) $ queryNeedsReload query - return (res, Nothing) + return $ HResponse res Nothing where -- Hit postgres dbAction = do @@ -235,7 +235,7 @@ v1QueryHandler query = do liftIO $ writeIORef scRef newSc' return resp -v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler Response +v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler HResponse v1Alpha1GQHandler query = do userInfo <- asks hcUser reqBody <- asks hcReqBody @@ -247,7 +247,7 @@ v1Alpha1GQHandler query = do isoL <- scIsolation . hcServerCtx <$> ask GH.runGQ pool isoL userInfo sc manager reqHeaders query reqBody -gqlExplainHandler :: GE.GQLExplain -> Handler Response +gqlExplainHandler :: GE.GQLExplain -> Handler HResponse gqlExplainHandler query = do onlyAdmin scRef <- scCacheRef . hcServerCtx <$> ask @@ -255,7 +255,7 @@ gqlExplainHandler query = do pool <- scPGPool . hcServerCtx <$> ask isoL <- scIsolation . hcServerCtx <$> ask res <- GE.explainGQLQuery pool isoL sc query - return (res, Nothing) + return $ HResponse res Nothing newtype QueryParser = QueryParser { getQueryParser :: QualifiedTable -> Handler RQLQuery } @@ -277,7 +277,7 @@ queryParsers = q <- decodeValue val return $ f q -legacyQueryHandler :: TableName -> T.Text -> Handler Response +legacyQueryHandler :: TableName -> T.Text -> Handler HResponse legacyQueryHandler tn queryType = case M.lookup queryType queryParsers of Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler diff --git a/server/src-lib/Hasura/Server/Context.hs b/server/src-lib/Hasura/Server/Context.hs new file mode 100644 index 0000000000000..517426ff1be5a --- /dev/null +++ b/server/src-lib/Hasura/Server/Context.hs @@ -0,0 +1,22 @@ +module Hasura.Server.Context + ( HResponse(..) + , Header (..) + , Headers + ) + where + +import Hasura.Prelude + +import qualified Data.ByteString.Lazy as BL + +newtype Header + = Header { unHeader :: (Text, Text) } + deriving (Show, Eq) + +type Headers = [Header] + +data HResponse + = HResponse + { _hrBody :: !BL.ByteString + , _hrHeaders :: !(Maybe Headers) + } deriving (Show, Eq) From 240bd4d27aab5d67b4950580f22a3bc4f585801f Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Tue, 26 Feb 2019 17:21:28 +0530 Subject: [PATCH 05/12] use hashset in http headers static list --- server/src-lib/Hasura/Server/Utils.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index e7074d23c13de..727ac2a2fc47a 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -9,6 +9,7 @@ import System.Exit import System.Process import qualified Data.ByteString as B +import qualified Data.HashSet as Set import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding.Error as TE @@ -141,7 +142,8 @@ fmapL _ (Right x) = pure x filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header] filterRequestHeaders = filterHeaders reqHeaders where - reqHeaders = [ "Content-Length", "Content-MD5", "User-Agent", "Host" + reqHeaders = Set.fromList + [ "Content-Length", "Content-MD5", "User-Agent", "Host" , "Origin", "Referer" , "Accept", "Accept-Encoding" , "Accept-Language", "Accept-Datetime" , "Cache-Control", "Connection", "DNT" @@ -152,12 +154,13 @@ filterRequestHeaders = filterHeaders reqHeaders filterResponseHeaders :: [HTTP.Header] -> [HTTP.Header] filterResponseHeaders = filterHeaders respHeaders where - respHeaders = [ "Server", "Transfer-Encoding", "Cache-Control" + respHeaders = Set.fromList + [ "Server", "Transfer-Encoding", "Cache-Control" , "Access-Control-Allow-Credentials" , "Access-Control-Allow-Methods" , "Access-Control-Allow-Origin" , "Content-Type" ] -filterHeaders :: [HTTP.HeaderName] -> [HTTP.Header] -> [HTTP.Header] -filterHeaders list = filter (\(n, _) -> n `notElem` list) +filterHeaders :: Set.HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header] +filterHeaders list = filter (\(n, _) -> n `Set.member` list) From a063feb9bfc72b30337cbfea50ccc79085b30ead Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Tue, 26 Feb 2019 17:54:06 +0530 Subject: [PATCH 06/12] dumb mistake of missing a negation --- server/src-lib/Hasura/Server/Utils.hs | 2 +- server/tests-py/test_schema_stitching.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index 727ac2a2fc47a..bccb1729441f1 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -163,4 +163,4 @@ filterResponseHeaders = filterHeaders respHeaders ] filterHeaders :: Set.HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header] -filterHeaders list = filter (\(n, _) -> n `Set.member` list) +filterHeaders list = filter (\(n, _) -> not $ n `Set.member` list) diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index 746eac450f1d1..323f1dbea5b7b 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -248,6 +248,7 @@ def test_response_headers_from_remote(self, hge_ctx): resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q, headers=headers) assert resp.status_code == 200 + print(resp.headers) assert ('Set-Cookie' in resp.headers and resp.headers['Set-Cookie'] == 'abcd') res = resp.json() From d59703623e255d4f6aa280d46804e0b36bbe6273 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Thu, 30 May 2019 19:14:38 +0530 Subject: [PATCH 07/12] forward response headers from remote servers to client --- server/src-lib/Hasura/GraphQL/Execute.hs | 13 ++++++-- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 7 ++--- .../Hasura/GraphQL/Transport/WebSocket.hs | 8 ++--- server/src-lib/Hasura/Server/App.hs | 30 ++++++++++--------- server/src-lib/Hasura/Server/Context.hs | 12 ++++---- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index ee40a44d5c003..f1cb368fd1542 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -41,7 +41,9 @@ import Hasura.HTTP import Hasura.Prelude import Hasura.RQL.DDL.Headers import Hasura.RQL.Types -import Hasura.Server.Utils (bsToTxt, commonClientHeadersIgnored) +import Hasura.Server.Context +import Hasura.Server.Utils (bsToTxt, commonClientHeadersIgnored, + filterResponseHeaders) import qualified Hasura.GraphQL.Execute.LiveQuery as EL import qualified Hasura.GraphQL.Execute.Plan as EP @@ -333,7 +335,7 @@ execRemoteGQ -- ^ the raw request string -> RemoteSchemaInfo -> G.TypedOperationDefinition - -> m EncJSON + -> m (HttpResponse EncJSON) execRemoteGQ manager userInfo reqHdrs q rsi opDef = do let opTy = G._todType opDef when (opTy == G.OperationTypeSubscription) $ @@ -352,7 +354,8 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res - return $ encJFromLBS $ resp ^. Wreq.responseBody + let respHdrs = Just $ mkRespHeaders $ resp ^. Wreq.responseHeaders + return $ HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) respHdrs where RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi @@ -368,3 +371,7 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do let txHdrs = map (\(n, v) -> (bsToTxt $ CI.original n, bsToTxt v)) hdrs in map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ filter (not . isUserVar . fst) txHdrs + + mkRespHeaders hdrs = + map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $ + filterResponseHeaders hdrs diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index b8de4d065a9cd..04999733a084e 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -11,9 +11,6 @@ import Hasura.GraphQL.Transport.HTTP.Protocol import Hasura.Prelude import Hasura.RQL.Types import Hasura.Server.Context -import Hasura.Server.Utils (bsToTxt, - filterRequestHeaders, - filterResponseHeaders) import qualified Hasura.GraphQL.Execute as E @@ -30,14 +27,14 @@ runGQ -> [N.Header] -> GQLReqUnparsed -> BL.ByteString -- this can be removed when we have a pretty-printer - -> m EncJSON + -> m (HttpResponse EncJSON) runGQ pgExecCtx userInfo sqlGenCtx enableAL planCache sc scVer manager reqHdrs req rawReq = do execPlan <- E.getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx enableAL sc scVer req case execPlan of E.GExPHasura resolvedOp -> - runHasuraGQ pgExecCtx userInfo resolvedOp + flip HttpResponse Nothing <$> runHasuraGQ pgExecCtx userInfo resolvedOp E.GExPRemote rsi opDef -> E.execRemoteGQ manager userInfo reqHdrs rawReq rsi opDef diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index f89e77f0a01d2..e29e4cb582529 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -11,6 +11,7 @@ import qualified Control.Concurrent.STM as STM import qualified Data.Aeson as J import qualified Data.Aeson.Casing as J import qualified Data.Aeson.TH as J +import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.CaseInsensitive as CI import qualified Data.HashMap.Strict as Map @@ -26,8 +27,6 @@ import qualified Network.WebSockets as WS import qualified StmContainers.Map as STMMap import Control.Concurrent (threadDelay) -import qualified Data.IORef as IORef -import Hasura.Server.Context import Hasura.EncJSON import qualified Hasura.GraphQL.Execute as E @@ -40,6 +39,7 @@ import Hasura.Prelude import Hasura.RQL.Types import Hasura.RQL.Types.Error (Code (StartFailed)) import Hasura.Server.Auth (AuthMode, getUserInfoWithExpTime) +import Hasura.Server.Context import Hasura.Server.Cors import Hasura.Server.Utils (bsToTxt, diffTimeToMicro) @@ -188,7 +188,7 @@ onConn (L.Logger logger) corsPolicy wsId requestHead = do getOrigin = find ((==) "Origin" . fst) (WS.requestHeaders requestHead) - enforceCors :: ByteString -> [H.Header] -> ExceptT QErr IO [H.Header] + enforceCors :: B.ByteString -> [H.Header] -> ExceptT QErr IO [H.Header] enforceCors origin reqHdrs = case cpConfig corsPolicy of CCAllowAll -> return reqHdrs CCDisabled readCookie -> @@ -286,7 +286,7 @@ onStart serverEnv wsConn (StartMsg opId q) msgRaw = catchAndIgnore $ do let payload = J.encode $ _wpPayload sockPayload resp <- runExceptT $ E.execRemoteGQ httpMgr userInfo reqHdrs payload rsi opDef - either postExecErr sendRemoteResp resp + either postExecErr (sendRemoteResp . _hrBody) resp sendCompleted sendRemoteResp resp = diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index 6b00fc6d5cc0f..565f68fee59a5 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -141,8 +141,9 @@ apiRespToLBS = \case JSONResp _ j -> encJToLBS j RawResp _ b -> b -mkAPIRespHandler :: Handler EncJSON -> Handler APIResp -mkAPIRespHandler = fmap JSONResp +mkAPIRespHandler :: Handler (HttpResponse EncJSON) -> Handler APIResp +mkAPIRespHandler = fmap (\x -> JSONResp (mkHdrs x) (_hrBody x)) + where mkHdrs hdrs = maybe [] (fmap unHeader) $ _hrHeaders hdrs isMetadataEnabled :: ServerCtx -> Bool isMetadataEnabled sc = S.member METADATA $ scEnabledAPIs sc @@ -250,13 +251,13 @@ mkSpockAction qErrEncoder qErrModifier serverCtx handler = do mapM_ (uncurry setHeader) h lazyBytes b -v1QueryHandler :: RQLQuery -> Handler EncJSON +v1QueryHandler :: RQLQuery -> Handler (HttpResponse EncJSON) v1QueryHandler query = do scRef <- scCacheRef . hcServerCtx <$> ask logger <- scLogger . hcServerCtx <$> ask - bool (fst <$> dbAction) (withSCUpdate scRef logger dbActionReload) $ - queryNeedsReload query - return $ HResponse res Nothing + res <- bool (fst <$> dbAction) (withSCUpdate scRef logger dbActionReload) $ + queryNeedsReload query + return $ HttpResponse res Nothing where -- Hit postgres dbAction = do @@ -277,7 +278,7 @@ v1QueryHandler query = do newSc' <- GS.updateSCWithGCtx newSc >>= flip resolveRemoteSchemas httpMgr return (resp, newSc') -v1Alpha1GQHandler :: GH.GQLReqUnparsed -> Handler EncJSON +v1Alpha1GQHandler :: GH.GQLReqUnparsed -> Handler (HttpResponse EncJSON) v1Alpha1GQHandler query = do userInfo <- asks hcUser reqBody <- asks hcReqBody @@ -292,10 +293,10 @@ v1Alpha1GQHandler query = do GH.runGQ pgExecCtx userInfo sqlGenCtx enableAL planCache sc scVer manager reqHeaders query reqBody -v1GQHandler :: GH.GQLReqUnparsed -> Handler EncJSON +v1GQHandler :: GH.GQLReqUnparsed -> Handler (HttpResponse EncJSON) v1GQHandler = v1Alpha1GQHandler -gqlExplainHandler :: GE.GQLExplain -> Handler EncJSON +gqlExplainHandler :: GE.GQLExplain -> Handler (HttpResponse EncJSON) gqlExplainHandler query = do onlyAdmin scRef <- scCacheRef . hcServerCtx <$> ask @@ -303,7 +304,8 @@ gqlExplainHandler query = do pgExecCtx <- scPGExecCtx . hcServerCtx <$> ask sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask enableAL <- scEnableAllowlist . hcServerCtx <$> ask - GE.explainGQLQuery pgExecCtx sc sqlGenCtx enableAL query + res <- GE.explainGQLQuery pgExecCtx sc sqlGenCtx enableAL query + return $ HttpResponse res Nothing v1Alpha1PGDumpHandler :: PGD.PGDumpReqBody -> Handler APIResp v1Alpha1PGDumpHandler b = do @@ -370,7 +372,7 @@ queryParsers = q <- decodeValue val return $ f q -legacyQueryHandler :: TableName -> T.Text -> Handler EncJSON +legacyQueryHandler :: TableName -> T.Text -> Handler (HttpResponse EncJSON) legacyQueryHandler tn queryType = case M.lookup queryType queryParsers of Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler @@ -502,17 +504,17 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do mkAPIRespHandler $ do onlyAdmin respJ <- liftIO $ E.dumpPlanCache $ scPlanCache serverCtx - return $ encJFromJValue respJ + return $ HttpResponse (encJFromJValue respJ) Nothing get "dev/subscriptions" $ mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $ do onlyAdmin respJ <- liftIO $ EL.dumpLiveQueriesState False $ scLQState serverCtx - return $ encJFromJValue respJ + return $ HttpResponse (encJFromJValue respJ) Nothing get "dev/subscriptions/extended" $ mkSpockAction encodeQErr id serverCtx $ mkAPIRespHandler $ do onlyAdmin respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx - return $ encJFromJValue respJ + return $ HttpResponse (encJFromJValue respJ) Nothing forM_ [GET,POST] $ \m -> hookAny m $ \_ -> do let qErr = err404 NotFound "resource does not exist" diff --git a/server/src-lib/Hasura/Server/Context.hs b/server/src-lib/Hasura/Server/Context.hs index 517426ff1be5a..055f47a39ac0a 100644 --- a/server/src-lib/Hasura/Server/Context.hs +++ b/server/src-lib/Hasura/Server/Context.hs @@ -1,5 +1,5 @@ module Hasura.Server.Context - ( HResponse(..) + ( HttpResponse(..) , Header (..) , Headers ) @@ -7,16 +7,14 @@ module Hasura.Server.Context import Hasura.Prelude -import qualified Data.ByteString.Lazy as BL - newtype Header = Header { unHeader :: (Text, Text) } deriving (Show, Eq) type Headers = [Header] -data HResponse - = HResponse - { _hrBody :: !BL.ByteString +data HttpResponse a + = HttpResponse + { _hrBody :: !a , _hrHeaders :: !(Maybe Headers) - } deriving (Show, Eq) + } From 29054350f4f66b25c822c815d677a4d441b7d1b2 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Thu, 30 May 2019 20:54:30 +0530 Subject: [PATCH 08/12] minor refactor --- server/src-lib/Hasura/GraphQL/Execute.hs | 6 +-- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 33 ------------- server/src-lib/Hasura/Server/App.hs | 29 +++++------ server/src-lib/Hasura/Server/Utils.hs | 49 +++++++++---------- 4 files changed, 38 insertions(+), 79 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index f1cb368fd1542..8de40184415e6 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -42,7 +42,8 @@ import Hasura.Prelude import Hasura.RQL.DDL.Headers import Hasura.RQL.Types import Hasura.Server.Context -import Hasura.Server.Utils (bsToTxt, commonClientHeadersIgnored, +import Hasura.Server.Utils (bsToTxt, + filterRequestHeaders, filterResponseHeaders) import qualified Hasura.GraphQL.Execute.LiveQuery as EL @@ -364,8 +365,7 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ userInfoToList userInfo - filteredHeaders = filterUserVars $ flip filter reqHdrs $ \(n, _) -> - n `notElem` commonClientHeadersIgnored + filteredHeaders = filterUserVars $ filterRequestHeaders reqHdrs filterUserVars hdrs = let txHdrs = map (\(n, v) -> (bsToTxt $ CI.original n, bsToTxt v)) hdrs diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 04999733a084e..92a0b185ca0f4 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -55,36 +55,3 @@ runHasuraGQ pgExecCtx userInfo resolvedOp = do "subscriptions are not supported over HTTP, use websockets instead" resp <- liftEither respE return $ encodeGQResp $ GQSuccess $ encJToLBS resp --- runRemoteGQ --- :: (MonadIO m, MonadError QErr m) --- => HTTP.Manager --- -> UserInfo --- -> [N.Header] --- -> BL.ByteString --- -- ^ the raw request string --- -> RemoteSchemaInfo --- -> G.TypedOperationDefinition --- -> m HResponse --- runRemoteGQ manager userInfo reqHdrs q rsi opDef = do --- let opTy = G._todType opDef --- when (opTy == G.OperationTypeSubscription) $ --- throw400 NotSupported "subscription to remote server is not supported" --- hdrs <- getHeadersFromConf hdrConf --- let confHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) hdrs --- clientHdrs = bool [] filteredHeaders fwdClientHdrs --- options = wreqOptions manager (userInfoToHdrs ++ clientHdrs ++ confHdrs) - --- res <- liftIO $ try $ Wreq.postWith options (show url) q --- resp <- either httpThrow return res --- let respHdrs = map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $ --- filterResponseHeaders $ resp ^. Wreq.responseHeaders --- return $ HResponse (resp ^. Wreq.responseBody) (Just respHdrs) - --- where --- RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi --- httpThrow :: (MonadError QErr m) => HTTP.HttpException -> m a --- httpThrow err = throw500 $ T.pack . show $ err - --- userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ --- userInfoToList userInfo --- filteredHeaders = filterRequestHeaders reqHdrs diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index 565f68fee59a5..23112b6ef00c4 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -133,17 +133,16 @@ data HandlerCtx type Handler = ExceptT QErr (ReaderT HandlerCtx IO) data APIResp - = JSONResp ![(Text, Text)] !EncJSON - | RawResp ![(Text,Text)] !BL.ByteString -- headers, body + = JSONResp !(HttpResponse EncJSON) + | RawResp !(HttpResponse BL.ByteString) -- headers, body apiRespToLBS :: APIResp -> BL.ByteString apiRespToLBS = \case - JSONResp _ j -> encJToLBS j - RawResp _ b -> b + JSONResp (HttpResponse j _) -> encJToLBS j + RawResp (HttpResponse b _) -> b mkAPIRespHandler :: Handler (HttpResponse EncJSON) -> Handler APIResp -mkAPIRespHandler = fmap (\x -> JSONResp (mkHdrs x) (_hrBody x)) - where mkHdrs hdrs = maybe [] (fmap unHeader) $ _hrHeaders hdrs +mkAPIRespHandler = fmap JSONResp isMetadataEnabled :: ServerCtx -> Bool isMetadataEnabled sc = S.member METADATA $ scEnabledAPIs sc @@ -222,13 +221,9 @@ mkSpockAction qErrEncoder qErrModifier serverCtx handler = do let modResult = fmapL qErrModifier result -- log result - logResult (Just userInfo) req reqBody logger (apiRespToLBS <$> modResult) $ Just (t1, t2) + logResult (Just userInfo) req reqBody logger (apiRespToLBS <$> modResult) $ + Just (t1, t2) either (qErrToResp $ userRole userInfo == adminRole) resToResp modResult --- ======= --- logResult (Just userInfo) req reqBody serverCtx (_hrBody <$> result) $ --- Just (t1, t2) --- either (qErrToResp $ userRole userInfo == adminRole) resToResp result --- >>>>>>> fix-1654-fwd-resp-hdrs where logger = scLogger serverCtx @@ -243,12 +238,12 @@ mkSpockAction qErrEncoder qErrModifier serverCtx handler = do qErrToResp includeInternal qErr resToResp = \case - JSONResp h j -> do + JSONResp (HttpResponse j h) -> do uncurry setHeader jsonHeader - mapM_ (uncurry setHeader) h + mapM_ (mapM_ (uncurry setHeader . unHeader)) h lazyBytes $ encJToLBS j - RawResp h b -> do - mapM_ (uncurry setHeader) h + RawResp (HttpResponse b h) -> do + mapM_ (mapM_ (uncurry setHeader . unHeader)) h lazyBytes b v1QueryHandler :: RQLQuery -> Handler (HttpResponse EncJSON) @@ -312,7 +307,7 @@ v1Alpha1PGDumpHandler b = do onlyAdmin ci <- scConnInfo . hcServerCtx <$> ask output <- PGD.execPGDump b ci - return $ RawResp [sqlHeader] output + return $ RawResp $ HttpResponse output (Just [Header sqlHeader]) consoleAssetsHandler :: L.Logger -> Text -> FilePath -> ActionT IO () consoleAssetsHandler logger dir path = do diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index b898b8eb23e14..b5c47e0121b1d 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -51,14 +51,6 @@ userIdHeader = "x-hasura-user-id" bsToTxt :: B.ByteString -> T.Text bsToTxt = TE.decodeUtf8With TE.lenientDecode -commonClientHeadersIgnored :: (IsString a) => [a] -commonClientHeadersIgnored = - [ "Content-Length", "Content-MD5", "User-Agent", "Host" - , "Origin", "Referer" , "Accept", "Accept-Encoding" - , "Accept-Language", "Accept-Datetime" - , "Cache-Control", "Connection", "DNT", "Content-Type" - ] - txtToBs :: T.Text -> B.ByteString txtToBs = TE.encodeUtf8 @@ -175,28 +167,33 @@ diffTimeToMicro diff = aSecond = 1000 * 1000 -- ignore the following request headers from the client -filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header] -filterRequestHeaders = filterHeaders reqHeaders - where - reqHeaders = Set.fromList - [ "Content-Length", "Content-MD5", "User-Agent", "Host" - , "Origin", "Referer" , "Accept", "Accept-Encoding" - , "Accept-Language", "Accept-Datetime" - , "Cache-Control", "Connection", "DNT" - ] +commonClientHeadersIgnored :: (IsString a) => [a] +commonClientHeadersIgnored = + [ "Content-Length", "Content-MD5", "User-Agent", "Host" + , "Origin", "Referer" , "Accept", "Accept-Encoding" + , "Accept-Language", "Accept-Datetime" + , "Cache-Control", "Connection", "DNT", "Content-Type" + ] + +commonResponseHeadersIgnored :: (IsString a) => [a] +commonResponseHeadersIgnored = + [ "Server", "Transfer-Encoding", "Cache-Control" + , "Access-Control-Allow-Credentials" + , "Access-Control-Allow-Methods" + , "Access-Control-Allow-Origin" + , "Content-Type" + ] + + +filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header] +filterRequestHeaders = + filterHeaders $ Set.fromList commonClientHeadersIgnored -- ignore the following response headers from remote filterResponseHeaders :: [HTTP.Header] -> [HTTP.Header] -filterResponseHeaders = filterHeaders respHeaders - where - respHeaders = Set.fromList - [ "Server", "Transfer-Encoding", "Cache-Control" - , "Access-Control-Allow-Credentials" - , "Access-Control-Allow-Methods" - , "Access-Control-Allow-Origin" - , "Content-Type" - ] +filterResponseHeaders = + filterHeaders $ Set.fromList commonResponseHeadersIgnored filterHeaders :: Set.HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header] filterHeaders list = filter (\(n, _) -> not $ n `Set.member` list) From ade31a26821fe58146f7c0c6534beea062a4dcf5 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Fri, 31 May 2019 10:49:20 +0530 Subject: [PATCH 09/12] forward only set-cookie header from remote server --- server/src-lib/Hasura/GraphQL/Execute.hs | 5 ++++- server/src-lib/Hasura/Server/Utils.hs | 2 +- server/tests-py/graphql_server.py | 1 + server/tests-py/test_schema_stitching.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 8de40184415e6..098f7fac37c2e 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -355,7 +355,8 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res - let respHdrs = Just $ mkRespHeaders $ resp ^. Wreq.responseHeaders + let cookieHdr = getCookieHdr (resp ^? Wreq.responseHeader "Set-Cookie") + respHdrs = Just $ mkRespHeaders cookieHdr return $ HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) respHdrs where @@ -372,6 +373,8 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do in map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ filter (not . isUserVar . fst) txHdrs + getCookieHdr = maybe [] (\h -> [("Set-Cookie", h)]) + mkRespHeaders hdrs = map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $ filterResponseHeaders hdrs diff --git a/server/src-lib/Hasura/Server/Utils.hs b/server/src-lib/Hasura/Server/Utils.hs index b5c47e0121b1d..bf52d2125683d 100644 --- a/server/src-lib/Hasura/Server/Utils.hs +++ b/server/src-lib/Hasura/Server/Utils.hs @@ -182,7 +182,7 @@ commonResponseHeadersIgnored = , "Access-Control-Allow-Credentials" , "Access-Control-Allow-Methods" , "Access-Control-Allow-Origin" - , "Content-Type" + , "Content-Type", "Content-Length" ] diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index ff8f4bfb40300..c0cbd4cc3e7ac 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -189,6 +189,7 @@ def post(self, request): res = hello_schema.execute(request.json['query']) resp = mkJSONResp(res) resp.headers['Set-Cookie'] = 'abcd' + resp.headers['Custom-Header'] = 'custom-value' return resp diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index 898e53fdf4b0e..b3ef7c22e3021 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -372,7 +372,7 @@ def test_response_headers_from_remote(self, hge_ctx): if hge_ctx.hge_key: headers = {'x-hasura-admin-secret': hge_ctx.hge_key} q = {'query': 'query { hello (arg: "me") }'} - resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q, + resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1/graphql', json=q, headers=headers) assert resp.status_code == 200 print(resp.headers) From 626c222ce9b2751e55c5f40347c50611a83ff555 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Fri, 31 May 2019 10:52:32 +0530 Subject: [PATCH 10/12] update docs with cookie header --- docs/graphql/manual/remote-schemas/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index c316fcd23038b..448370c6e1d18 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -169,12 +169,12 @@ will selected. ``x-hasura-admin-secret`` is sent, then all ``x-hasura-*`` values from the client are respected, otherwise they are ignored. -Response headers from your remote GraphQL servers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Response headers from your remote schema servers are sent back to the client -over HTTP transport. **Over websocket transport, the response headers are not -sent.** If you require the response headers from remote servers, use the HTTP -transport. +Cookie header from your remote GraphQL servers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Only ``Set-Cookie`` header from your remote schema servers are sent back to the +client over HTTP transport. **Over websocket transport, the response headers are +not sent.** If you require the response headers from remote servers, use the +HTTP transport. Bypassing Hasura's authorization system for remote schema queries From 098410afe3ef06ea6c33ff9015cffd7ec15ae1c4 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 3 Jun 2019 14:10:23 +0530 Subject: [PATCH 11/12] fix review comment --- server/src-lib/Hasura/GraphQL/Execute.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 098f7fac37c2e..69ca1fc1d8b0c 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -43,8 +43,7 @@ import Hasura.RQL.DDL.Headers import Hasura.RQL.Types import Hasura.Server.Context import Hasura.Server.Utils (bsToTxt, - filterRequestHeaders, - filterResponseHeaders) + filterRequestHeaders) import qualified Hasura.GraphQL.Execute.LiveQuery as EL import qualified Hasura.GraphQL.Execute.Plan as EP @@ -376,5 +375,4 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do getCookieHdr = maybe [] (\h -> [("Set-Cookie", h)]) mkRespHeaders hdrs = - map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $ - filterResponseHeaders hdrs + map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) hdrs From 40c62f8b95354e6f0f999c017b54bc873869c1fa Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi <0x777@users.noreply.github.com> Date: Tue, 4 Jun 2019 15:01:06 +0530 Subject: [PATCH 12/12] simplify docs --- docs/graphql/manual/remote-schemas/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index 448370c6e1d18..9ff5d3b12d36b 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -171,10 +171,10 @@ will selected. Cookie header from your remote GraphQL servers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Only ``Set-Cookie`` header from your remote schema servers are sent back to the -client over HTTP transport. **Over websocket transport, the response headers are -not sent.** If you require the response headers from remote servers, use the -HTTP transport. +``Set-Cookie`` headers from your remote schema servers are sent back to the +client over HTTP transport. **Over websocket transport there exists no means +to send headers after a query/mutation and hence ``Set-Cookie`` headers are +not sent to the client.** Use HTTP transport if your remote servers set cookies. Bypassing Hasura's authorization system for remote schema queries