diff --git a/cmd/grpcui/grpcui.go b/cmd/grpcui/grpcui.go index b4d21389..3bf59f64 100644 --- a/cmd/grpcui/grpcui.go +++ b/cmd/grpcui/grpcui.go @@ -307,6 +307,29 @@ type compositeSource struct { file grpcurl.DescriptorSource } +func Union(slice1, slice2 []*desc.FileDescriptor) []*desc.FileDescriptor { + // Create a map to track unique elements + elementMap := make(map[string]*desc.FileDescriptor) + + // Add elements from the first slice to the map + for _, elem := range slice1 { + elementMap[elem.GetName()] = elem + } + + // Add elements from the second slice to the map + for _, elem := range slice2 { + elementMap[elem.GetName()] = elem + } + + // Create a slice to store the union + var union []*desc.FileDescriptor + for elem := range elementMap { + union = append(union, elementMap[elem]) + } + + return union +} + func (cs compositeSource) ListServices() ([]string, error) { return cs.reflection.ListServices() } @@ -599,6 +622,15 @@ func main() { fail(err, "Failed to compute set of methods to expose") } allFiles, err := grpcurl.GetAllFiles(descSource) + // grpcurl doesn't handle the case of composite source, so we need to handle it. + if fileSource != nil { + fileOnlyFiles, err := grpcurl.GetAllFiles(fileSource) + if err != nil { + fail(err, "Failed to enumerate all proto files from fileSource") + } + allFiles = Union(allFiles, fileOnlyFiles) + } + if err != nil { fail(err, "Failed to enumerate all proto files") } @@ -633,7 +665,7 @@ func main() { handlerOpts = append(handlerOpts, configureJSandCSS(extraCSS, standalone.AddCSSFile)...) handlerOpts = append(handlerOpts, configureAssets(otherAssets)...) - handler := standalone.Handler(cc, target, methods, allFiles, handlerOpts...) + handler := standalone.HandlerWithDescriptorSource(cc, target, methods, allFiles, descSource, handlerOpts...) if *maxTime > 0 { timeout := floatSecondsToDuration(*maxTime) // enforce the timeout by wrapping the handler and inserting a diff --git a/handlers.go b/handlers.go index 2bb3b864..69fab085 100644 --- a/handlers.go +++ b/handlers.go @@ -66,6 +66,8 @@ type InvokeOptions struct { // of a bool "verbose" flag, so that additional logs may be added in the // future and the caller control how detailed those logs will be. Verbosity int + + AdditionalDescriptorSource grpcurl.DescriptorSource } // RPCInvokeHandlerWithOptions is the same as RPCInvokeHandler except that it @@ -91,11 +93,18 @@ func RPCInvokeHandlerWithOptions(ch grpc.ClientConnInterface, descs []*desc.Meth for _, md := range descs { if md.GetFullyQualifiedName() == method { - descSource, err := grpcurl.DescriptorSourceFromFileDescriptors(md.GetFile()) - if err != nil { - http.Error(w, "Failed to create descriptor source: "+err.Error(), http.StatusInternalServerError) - return + var descSource grpcurl.DescriptorSource + if options.AdditionalDescriptorSource == nil { + currDescSource, err := grpcurl.DescriptorSourceFromFileDescriptors(md.GetFile()) + if err != nil { + http.Error(w, "Failed to create descriptor source: "+err.Error(), http.StatusInternalServerError) + return + } + descSource = currDescSource + } else { + descSource = options.AdditionalDescriptorSource } + results, err := invokeRPC(r.Context(), method, ch, descSource, r.Header, r.Body, &options) if err != nil { if _, ok := err.(errReadFail); ok { @@ -145,7 +154,7 @@ func RPCMetadataHandler(methods []*desc.MethodDescriptor, files []*desc.FileDesc } else { for _, md := range methods { if md.GetFullyQualifiedName() == method { - r, err := gatherMetadataForMethod(md) + r, err := gatherMetadataForMethod(md, files) if err != nil { http.Error(w, "Failed to gather metadata for RPC Method", http.StatusUnprocessableEntity) return @@ -258,7 +267,8 @@ func gatherAllMessages(msgs []*desc.MessageDescriptor, result *schema) { } } -func gatherMetadataForMethod(md *desc.MethodDescriptor) (*schema, error) { +func gatherMetadataForMethod(md *desc.MethodDescriptor, + files []*desc.FileDescriptor) (*schema, error) { msg := md.GetInputType() result := &schema{ RequestType: msg.GetFullyQualifiedName(), @@ -268,6 +278,9 @@ func gatherMetadataForMethod(md *desc.MethodDescriptor) (*schema, error) { } result.visitMessage(msg) + for _, fd := range files { + gatherAllMessages(fd.GetMessageTypes(), result) + } return result, nil } @@ -435,7 +448,15 @@ func invokeRPC(ctx context.Context, methodName string, ch grpc.ClientConnInterfa reqStats.Sent++ req := input.Data[0] input.Data = input.Data[1:] - if err := jsonpb.Unmarshal(bytes.NewReader(req), m); err != nil { + var jsonUnmarshaler jsonpb.Unmarshaler + if descSource != nil { + anyResolver := grpcurl.AnyResolverFromDescriptorSource(descSource) + jsonUnmarshaler = jsonpb.Unmarshaler{AnyResolver: anyResolver} + } else { + jsonUnmarshaler = jsonpb.Unmarshaler{} + } + + if err := jsonUnmarshaler.Unmarshal(bytes.NewReader(req), m); err != nil { return status.Errorf(codes.InvalidArgument, err.Error()) } return nil diff --git a/standalone/standalone.go b/standalone/standalone.go index 4fa6a024..e6dbb5d7 100644 --- a/standalone/standalone.go +++ b/standalone/standalone.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "github.com/fullstorydev/grpcurl" "html/template" "io" "mime" @@ -42,7 +43,8 @@ const csrfHeaderName = "x-grpcui-csrf-token" // // The returned handler expects to serve resources from "/". If it will instead // be handling a sub-path (e.g. handling "/rpc-ui/") then use http.StripPrefix. -func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, opts ...HandlerOption) http.Handler { +func HandlerWithDescriptorSource(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, + descSource grpcurl.DescriptorSource, opts ...HandlerOption) http.Handler { uiOpts := &handlerOptions{ indexTmpl: defaultIndexTemplate, css: grpcui.WebFormSampleCSS(), @@ -91,10 +93,11 @@ func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescri }) invokeOpts := grpcui.InvokeOptions{ - ExtraMetadata: uiOpts.extraMetadata, - PreserveHeaders: uiOpts.preserveHeaders, - EmitDefaults: uiOpts.emitDefaults, - Verbosity: uiOpts.invokeVerbosity, + ExtraMetadata: uiOpts.extraMetadata, + PreserveHeaders: uiOpts.preserveHeaders, + EmitDefaults: uiOpts.emitDefaults, + Verbosity: uiOpts.invokeVerbosity, + AdditionalDescriptorSource: descSource, } rpcInvokeHandler := http.StripPrefix("/invoke", grpcui.RPCInvokeHandlerWithOptions(ch, methods, invokeOpts)) mux.HandleFunc("/invoke/", func(w http.ResponseWriter, r *http.Request) { @@ -140,6 +143,10 @@ func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescri }) } +func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, opts ...HandlerOption) http.Handler { + return HandlerWithDescriptorSource(ch, target, methods, files, nil, opts...) +} + var defaultIndexTemplate = template.Must(template.New("index.html").Parse(string(standalone.IndexTemplate()))) func getIndexContents(tmpl *template.Template, target string, webFormHTML []byte, addlResources []*resource) []byte {