|
| 1 | +{-# LANGUAGE AllowAmbiguousTypes #-} |
| 2 | +{-# LANGUAGE LambdaCase #-} |
| 3 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 4 | +{-# LANGUAGE TypeApplications #-} |
| 5 | + |
| 6 | +-- | Handler for the gRPC Server Reflection API |
| 7 | +-- (<https://github.com/grpc/grpc/blob/master/doc/server-reflection.md>), |
| 8 | +-- @grpc.reflection.v1@ and the older @grpc.reflection.v1alpha@. This lets |
| 9 | +-- generic clients (e.g. @grpcurl@) discover and decode this server's proto |
| 10 | +-- services without a local copy of the @.proto@ files. |
| 11 | +module Cardano.Rpc.Server.Internal.Reflection |
| 12 | + ( serverReflectionInfoMethodV1 |
| 13 | + , serverReflectionInfoMethodV1alpha |
| 14 | + , answerReflectionRequest |
| 15 | + , qualifiedServiceName |
| 16 | + ) |
| 17 | +where |
| 18 | + |
| 19 | +import Cardano.Rpc.Proto.Api.Reflection.V1 qualified as V1 |
| 20 | +import Cardano.Rpc.Proto.Api.Reflection.V1alpha qualified as V1alpha |
| 21 | +import Cardano.Rpc.Server.Internal.Error (throwGrpcErrorWithMessage) |
| 22 | +import Cardano.Rpc.Server.Internal.Reflection.DescriptorTable |
| 23 | + |
| 24 | +import RIO |
| 25 | + |
| 26 | +import Data.ProtoLens (Message, decodeMessage, defMessage, encodeMessage) |
| 27 | +import Data.ProtoLens.Service.Types (Service, ServiceName, ServicePackage) |
| 28 | +import Data.Text qualified as Text |
| 29 | +import GHC.TypeLits (symbolVal) |
| 30 | +import Network.GRPC.Spec |
| 31 | + ( GrpcError (GrpcInternal, GrpcInvalidArgument, GrpcNotFound) |
| 32 | + , NextElem (NextElem, NoNextElem) |
| 33 | + , Proto (Proto) |
| 34 | + , fromGrpcError |
| 35 | + ) |
| 36 | + |
| 37 | +-- | Handle the @ServerReflectionInfo@ bidirectional stream for |
| 38 | +-- @grpc.reflection.v1@: answer every request on the incoming stream in |
| 39 | +-- turn, then forward the client's own terminal marker. A bidi handler that |
| 40 | +-- returns without sending 'NoNextElem' itself has its stream cancelled |
| 41 | +-- instead of closed with trailers, the same requirement as for |
| 42 | +-- server-streaming handlers (both go through grapesy's identical |
| 43 | +-- @sendOutput call . fromNextElem call@ path). |
| 44 | +serverReflectionInfoMethodV1 |
| 45 | + :: MonadIO m |
| 46 | + => [Text] |
| 47 | + -- ^ Fully qualified names of every service registered with this server, |
| 48 | + -- answered verbatim for @list_services@ |
| 49 | + -> IO (NextElem (Proto V1.ServerReflectionRequest)) |
| 50 | + -> (NextElem (Proto V1.ServerReflectionResponse) -> IO ()) |
| 51 | + -> m () |
| 52 | +serverReflectionInfoMethodV1 serviceNames recv send = liftIO loop |
| 53 | + where |
| 54 | + loop = |
| 55 | + recv >>= \case |
| 56 | + NoNextElem -> send NoNextElem |
| 57 | + NextElem request -> do |
| 58 | + send . NextElem $ answerReflectionRequest descriptorTable serviceNames request |
| 59 | + loop |
| 60 | + |
| 61 | +-- | Handle the same stream for the legacy @grpc.reflection.v1alpha@, by |
| 62 | +-- bridging each message to and from @v1@ and answering with the one core |
| 63 | +-- 'answerReflectionRequest'. |
| 64 | +serverReflectionInfoMethodV1alpha |
| 65 | + :: MonadIO m |
| 66 | + => [Text] |
| 67 | + -- ^ Fully qualified names of every service registered with this server, |
| 68 | + -- answered verbatim for @list_services@ |
| 69 | + -> IO (NextElem (Proto V1alpha.ServerReflectionRequest)) |
| 70 | + -> (NextElem (Proto V1alpha.ServerReflectionResponse) -> IO ()) |
| 71 | + -> m () |
| 72 | +serverReflectionInfoMethodV1alpha serviceNames recv send = liftIO loop |
| 73 | + where |
| 74 | + loop = |
| 75 | + recv >>= \case |
| 76 | + NoNextElem -> send NoNextElem |
| 77 | + NextElem request -> do |
| 78 | + v1Request <- bridgeMessage request |
| 79 | + v1alphaResponse <- bridgeMessage (answerReflectionRequest descriptorTable serviceNames v1Request) |
| 80 | + send $ NextElem v1alphaResponse |
| 81 | + loop |
| 82 | + |
| 83 | +-- | Answer one @ServerReflectionRequest@, dispatching on its |
| 84 | +-- @message_request@ oneof. |
| 85 | +-- |
| 86 | +-- Lookup failures ('V1.FileByFilename', 'V1.FileContainingSymbol') are |
| 87 | +-- reported in-stream as an @ErrorResponse@ with @NOT_FOUND@, never as a |
| 88 | +-- gRPC error: the RPC itself stays OK for the life of the stream. |
| 89 | +-- @file_containing_extension@ and @all_extension_numbers_of_type@ also |
| 90 | +-- answer @NOT_FOUND@, since none of the proto files served here declare |
| 91 | +-- proto2 extensions. |
| 92 | +answerReflectionRequest |
| 93 | + :: DescriptorTable |
| 94 | + -> [Text] |
| 95 | + -> Proto V1.ServerReflectionRequest |
| 96 | + -> Proto V1.ServerReflectionResponse |
| 97 | +answerReflectionRequest table serviceNames request = |
| 98 | + defMessage |
| 99 | + & V1.validHost .~ (request ^. V1.host) |
| 100 | + & V1.originalRequest .~ request |
| 101 | + & answer |
| 102 | + where |
| 103 | + answer :: Proto V1.ServerReflectionResponse -> Proto V1.ServerReflectionResponse |
| 104 | + answer = case request ^. V1.maybe'messageRequest of |
| 105 | + -- proto3 leaves message_request entirely unset when malformed by the |
| 106 | + -- client; there is no lookup to fail here, so this is INVALID_ARGUMENT |
| 107 | + -- rather than NOT_FOUND. Answering in-stream here matches grpc's |
| 108 | + -- canonical C++ implementation; Go instead terminates the RPC. |
| 109 | + Nothing -> |
| 110 | + V1.errorResponse .~ mkErrorResponse GrpcInvalidArgument "no message_request set" |
| 111 | + Just (Proto messageRequest) -> case messageRequest of |
| 112 | + V1.ServerReflectionRequest'FileByFilename fileName -> |
| 113 | + fileDescriptorAnswer fileName |
| 114 | + V1.ServerReflectionRequest'FileContainingSymbol symbolName -> |
| 115 | + case lookupSymbol table symbolName of |
| 116 | + Nothing -> V1.errorResponse .~ mkErrorResponse GrpcNotFound ("symbol not found: " <> symbolName) |
| 117 | + Just fileName -> fileDescriptorAnswer fileName |
| 118 | + V1.ServerReflectionRequest'FileContainingExtension extensionRequest -> |
| 119 | + V1.errorResponse |
| 120 | + .~ mkErrorResponse |
| 121 | + GrpcNotFound |
| 122 | + ( "no extensions are declared by this server (requested for type: " |
| 123 | + <> (Proto extensionRequest ^. V1.containingType) |
| 124 | + <> ")" |
| 125 | + ) |
| 126 | + V1.ServerReflectionRequest'AllExtensionNumbersOfType typeName -> |
| 127 | + V1.errorResponse |
| 128 | + .~ mkErrorResponse GrpcNotFound ("no extensions are declared of type: " <> typeName) |
| 129 | + V1.ServerReflectionRequest'ListServices _ -> |
| 130 | + V1.listServicesResponse |
| 131 | + .~ (defMessage & V1.service .~ map (\serviceName -> defMessage & V1.name .~ serviceName) serviceNames) |
| 132 | + |
| 133 | + fileDescriptorAnswer |
| 134 | + :: Text -> Proto V1.ServerReflectionResponse -> Proto V1.ServerReflectionResponse |
| 135 | + fileDescriptorAnswer fileName = case transitiveClosure table fileName of |
| 136 | + Nothing -> V1.errorResponse .~ mkErrorResponse GrpcNotFound ("file not found: " <> fileName) |
| 137 | + Just entries -> |
| 138 | + V1.fileDescriptorResponse .~ (defMessage & V1.fileDescriptorProto .~ map fileEntryBytes entries) |
| 139 | + |
| 140 | + mkErrorResponse :: GrpcError -> Text -> Proto V1.ErrorResponse |
| 141 | + mkErrorResponse grpcError message = |
| 142 | + defMessage |
| 143 | + & V1.errorCode .~ fromIntegral (fromGrpcError grpcError) |
| 144 | + & V1.errorMessage .~ message |
| 145 | + |
| 146 | +-- | Re-encode a message as a wire-compatible message with different |
| 147 | +-- generated Haskell types. Safe between schemas that agree on every field |
| 148 | +-- number and wire type, which @v1@ and @v1alpha@ of the reflection protos |
| 149 | +-- do (@v1alpha@ is @v1@ under its original package name); a future schema |
| 150 | +-- divergence is reported as an @INTERNAL@ gRPC error rather than a panic. |
| 151 | +bridgeMessage :: (Message a, Message b, MonadIO m) => Proto a -> m (Proto b) |
| 152 | +bridgeMessage message = |
| 153 | + either (throwGrpcErrorWithMessage GrpcInternal . ("bridgeMessage: " <>) . Text.pack) pure $ |
| 154 | + decodeMessage (encodeMessage message) |
| 155 | + |
| 156 | +-- | The fully qualified name of a proto service, @\<package\>.\<Service\>@, |
| 157 | +-- read off its own compiled-in descriptor via proto-lens's 'Service' class. |
| 158 | +-- Deriving it this way, rather than writing out the string, means the name |
| 159 | +-- paired with each service's handler in "Cardano.Rpc.Server" and the name |
| 160 | +-- 'answerReflectionRequest' (above) advertises for @list_services@ can |
| 161 | +-- never drift apart. |
| 162 | +qualifiedServiceName :: forall s. Service s => Text |
| 163 | +qualifiedServiceName = |
| 164 | + Text.pack (symbolVal (Proxy @(ServicePackage s))) |
| 165 | + <> "." |
| 166 | + <> Text.pack (symbolVal (Proxy @(ServiceName s))) |
0 commit comments