commit 9dfaded6cff9c2d4bbdb904eb8b8db889da16bdb Author: Josh Humphries Date: Mon Nov 20 13:15:15 2017 -0500 initial commit, functioning grpcurl command-line util diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bb84b59 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.6 + - go: 1.7 + - go: 1.8 + - go: 1.9 + - go: tip + +script: + - ./ci.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6b678c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 FullStory, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8b070e --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# GRPCurl +`grpcurl` is a command-line tool that lets you interact with GRPC servers. It's +basically `curl` for GRPC servers. + +The main purpose for this tool is to invoke RPC methods on a GRPC server from the +command-line. GRPC servers use a binary encoding on the wire +([protocol buffers](https://developers.google.com/protocol-buffers/), or "protobufs" +for short). So they are basically impossible to interact with using regular `curl` +(and older versions of `curl` that do not support HTTP/2 are of course non-starters). +This program accepts messages using JSON encoding, which is much more friendly for both +humans and scripts. + +With this tool you can also browse the schema for GRPC services, either by querying +a server that supports [service reflection](https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto) +or by loading in "protoset" files (files that contain encoded file +[descriptor protos](https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto)). +In fact, the way the tool transforms JSON request data into a binary encoded protobuf +is using that very same schema. So, if the server you interact with does not support +reflection, you will need to build "protoset" files that `grpcurl` can use. + +This code for this tool is also a great example of how to use the various packages of +the [protoreflect](https://godoc.org/github.com/jhump/protoreflect) library, and shows +off what they can do. + +## Features +`grpcurl` supports all kinds of RPC methods, including streaming methods. You can even +operate bi-directional streaming methods interactively by running `grpcurl` from an +interactive terminal and using stdin as the request body! + +`grpcurl` supports both plain-text and TLS servers and has numerous options for TLS +configuration. It also supports mutual TLS, where the client is required to present a +client certificate. + +As mentioned above, `grpcurl` works seamlessly if the server supports the reflection +service. If not, you must use `protoc` to build protoset files and provide those to +`grpcurl`. + +## Example Usage +Invoking an RPC on a trusted server (e.g. TLS without self-signed key or custom CA) +that requires no client certs and supports service reflection is the simplest thing to +do with `grpcurl`. This minimal invocation sends an empty request body: +``` +grpcurl grpc.server.com:443 my.custom.server.Service/Method +``` + +To list all services exposed by a server, use the "list" verb. When using protoset files +instead of server reflection, this lists all services defined in the protoset files. +``` +grpcurl localhost:80808 list + +grpcurl -protoset my-protos.bin list +``` + +The "list" verb also lets you see all methods in a particular service: +``` +grpcurl localhost:80808 list my.custom.server.Service +``` + +The "describe" verb will print the type of any symbol that the server knows about +or that is found in a given protoset file and also print the full descriptor for the +symbol, in JSON. +``` +grpcurl localhost:80808 describe my.custom.server.Service.MethodOne + +grpcurl -protoset my-protos.bin describe my.custom.server.Service.MethodOne +``` + +The usage doc for the tool explains the numerous options: +``` +grpcurl -help +``` + +## Protoset Files +To use `grpcurl` on servers that do not support reflection, you need to compile the +`*.proto` files that describe the service into files containing encoded +`FileDescriptorSet` protos, also known as "protoset" files. + +``` +protoc --proto_path=. \ + --descriptor_set_out=myservice.protoset \ + --include_imports \ + my/custom/server/service.proto +``` + +The `--descriptor_set_out` argument is what tells `protoc` to produce a protoset, +and the `--include_imports` arguments is necessary for the protoset to contain +everything that `grpcurl` needs to process and understand the schema. \ No newline at end of file diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..029179a --- /dev/null +++ b/ci.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +fmtdiff="$(gofmt -s -l ./)" +if [[ -n "$fmtdiff" ]]; then + gofmt -s -l ./ >&2 + echo "Run gofmt on the above files!" >&2 + exit 1 +fi + +go test -v -race ./... diff --git a/cmd/grpcurl/grpcurl.go b/cmd/grpcurl/grpcurl.go new file mode 100644 index 0000000..1a9cc67 --- /dev/null +++ b/cmd/grpcurl/grpcurl.go @@ -0,0 +1,456 @@ +// Command grpcurl makes GRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor file or +// service reflection to translate JSON request data into the appropriate protobuf request data and vice +// versa for presenting the response contents. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/grpcreflect" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/metadata" + reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + "google.golang.org/grpc/status" + + "github.com/fullstorydev/grpcurl" +) + +var ( + exit = os.Exit + + help = flag.Bool("help", false, + `Print usage instructions and exit.`) + plaintext = flag.Bool("plaintext", false, + `Use plain-text HTTP/2 when connecting to server (no TLS).`) + insecure = flag.Bool("insecure", false, + `Skip server certificate and domain verification. (NOT SECURE!). Not + valid with -plaintext option.`) + cacert = flag.String("cacert", "", + `File containing trusted root certificates for verifying the server. + Ignored if -insecure is specified.`) + cert = flag.String("cert", "", + `File containing client certificate (public key), to present to the + server. Not valid with -plaintext option. Must also provide -key option.`) + key = flag.String("key", "", + `File containing client private key, to present to the server. Not valid + with -plaintext option. Must also provide -cert option.`) + protoset multiString + addlHeaders multiString + data = flag.String("d", "", + `JSON request contents. If the value is '@' then the request contents are + read from stdin. For calls that accept a stream of requests, the + contents should include all such request messages concatenated together + (optionally separated by whitespace).`) + connectTimeout = flag.String("connect-timeout", "", + `The maximum time, in seconds, to wait for connection to be established. + Defaults to 10 seconds.`) + keepaliveTime = flag.String("keepalive-time", "", + `If present, the maximum idle time in seconds, after which a keepalive + probe is sent. If the connection remains idle and no keepalive response + is received for this same period then the connection is closed and the + operation fails.`) + maxTime = flag.String("max-time", "", + `The maximum total time the operation can take. This is useful for + preventing batch jobs that use grpcurl from hanging due to slow or bad + network links or due to incorrect stream method usage.`) + verbose = flag.Bool("v", false, + `Enable verbose output.`) +) + +func init() { + flag.Var(&addlHeaders, "H", + `Additional request headers in 'name: value' format. May specify more + than one via multiple -H flags.`) + flag.Var(&protoset, "protoset", + `The name of a file containing an encoded FileDescriptorSet. This file's + contents will be used to determine the RPC schema instead of querying + for it from the remote server via the GRPC reflection API. When set: the + 'list' action lists the services found in the given descriptors (vs. + those exposed by the remote server), and the 'describe' action describes + symbols found in the given descriptors. May specify more than one via + multiple -protoset flags.`) +} + +type multiString []string + +func (s *multiString) String() string { + return strings.Join(*s, ",") +} + +func (s *multiString) Set(value string) error { + *s = append(*s, value) + return nil +} + +func main() { + flag.CommandLine.Usage = usage + flag.Parse() + if *help { + usage() + os.Exit(0) + } + + // Do extra validation on arguments and figure out what user asked us to do. + if *plaintext && *insecure { + fail(nil, "The -plaintext and -insecure arguments are mutually exclusive.") + } + if *plaintext && *cert != "" { + fail(nil, "The -plaintext and -cert arguments are mutually exclusive.") + } + if *plaintext && *key != "" { + fail(nil, "The -plaintext and -key arguments are mutually exclusive.") + } + if (*key == "") != (*cert == "") { + fail(nil, "The -cert and -key arguments must be used together and both be present.") + } + + args := flag.Args() + + if len(args) == 0 { + fail(nil, "Too few arguments.") + } + var target string + if args[0] != "list" && args[0] != "describe" { + target = args[0] + args = args[1:] + } + + if len(args) == 0 { + fail(nil, "Too few arguments.") + } + var list, describe, invoke bool + if args[0] == "list" { + list = true + args = args[1:] + } else if args[0] == "describe" { + describe = true + args = args[1:] + } else { + invoke = true + } + + var symbol string + if invoke { + if len(args) == 0 { + fail(nil, "Too few arguments.") + } + symbol = args[0] + args = args[1:] + } else { + if *data != "" { + fail(nil, "The -d argument is not used with 'list' or 'describe' verb.") + } + if len(addlHeaders) > 0 { + fail(nil, "The -H argument is not used with 'list' or 'describe' verb.") + } + if len(args) > 0 { + symbol = args[0] + args = args[1:] + } + } + + if len(args) > 0 { + fail(nil, "Too many arguments.") + } + if invoke && target == "" { + fail(nil, "No host:port specified.") + } + if len(protoset) == 0 && target == "" { + fail(nil, "No host:port specified and no protoset specified.") + } + + ctx := context.Background() + if *maxTime != "" { + t, err := strconv.ParseFloat(*maxTime, 64) + if err != nil { + fail(nil, "The -max-time argument must be a valid number.") + } + timeout := time.Duration(t * float64(time.Second)) + ctx, _ = context.WithTimeout(ctx, timeout) + } + + dial := func() *grpc.ClientConn { + dialTime := 10 * time.Second + if *connectTimeout != "" { + t, err := strconv.ParseFloat(*connectTimeout, 64) + if err != nil { + fail(nil, "The -connect-timeout argument must be a valid number.") + } + dialTime = time.Duration(t * float64(time.Second)) + } + opts := []grpc.DialOption{grpc.WithTimeout(dialTime), grpc.WithBlock()} + if *keepaliveTime != "" { + t, err := strconv.ParseFloat(*keepaliveTime, 64) + if err != nil { + fail(nil, "The -keepalive-time argument must be a valid number.") + } + timeout := time.Duration(t * float64(time.Second)) + opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: timeout, + Timeout: timeout, + })) + } + if *plaintext { + opts = append(opts, grpc.WithInsecure()) + } else { + creds, err := grpcurl.ClientTransportCredentials(*insecure, *cacert, *cert, *key) + if err != nil { + fail(err, "Failed to configure transport credentials") + } + opts = append(opts, grpc.WithTransportCredentials(creds)) + } + cc, err := grpc.Dial(target, opts...) + if err != nil { + fail(err, "Failed to dial target host %q", target) + } + return cc + } + + var cc *grpc.ClientConn + var descSource grpcurl.DescriptorSource + var refClient *grpcreflect.Client + if len(protoset) > 0 { + var err error + descSource, err = grpcurl.DescriptorSourceFromProtoSets(protoset...) + if err != nil { + fail(err, "Failed to process proto descriptor sets") + } + } else { + cc = dial() + refClient = grpcreflect.NewClient(ctx, reflectpb.NewServerReflectionClient(cc)) + descSource = grpcurl.DescriptorSourceFromServer(ctx, refClient) + } + + // arrange for the RPCs to be cleanly shutdown + reset := func() { + if refClient != nil { + refClient.Reset() + refClient = nil + } + if cc != nil { + cc.Close() + cc = nil + } + } + defer reset() + exit = func(code int) { + // since defers aren't run by os.Exit... + reset() + os.Exit(code) + } + + if list { + if symbol == "" { + svcs, err := grpcurl.ListServices(descSource) + if err != nil { + fail(err, "Failed to list services") + } + if len(svcs) == 0 { + fmt.Println("(No services)") + } else { + for _, svc := range svcs { + fmt.Printf("%s\n", svc) + } + } + } else { + methods, err := grpcurl.ListMethods(descSource, symbol) + if err != nil { + fail(err, "Failed to list methods for service %q", symbol) + } + if len(methods) == 0 { + fmt.Println("(No methods)") // probably unlikely + } else { + for _, m := range methods { + fmt.Printf("%s\n", m) + } + } + } + + } else if describe { + var symbols []string + if symbol != "" { + symbols = []string{symbol} + } else { + // if no symbol given, describe all exposed services + svcs, err := descSource.ListServices() + if err != nil { + fail(err, "Failed to list services") + } + if len(svcs) == 0 { + fmt.Println("Server returned an empty list of exposed services") + } + symbols = svcs + } + for _, s := range symbols { + dsc, err := descSource.FindSymbol(s) + if err != nil { + fail(err, "Failed to resolve symbol %q", s) + } + + txt, err := grpcurl.GetDescriptorText(dsc, descSource) + if err != nil { + fail(err, "Failed to describe symbol %q", s) + } + + switch dsc.(type) { + case *desc.MessageDescriptor: + fmt.Printf("%s is a message:\n", dsc.GetFullyQualifiedName()) + case *desc.FieldDescriptor: + fmt.Printf("%s is a field:\n", dsc.GetFullyQualifiedName()) + case *desc.OneOfDescriptor: + fmt.Printf("%s is a one-of:\n", dsc.GetFullyQualifiedName()) + case *desc.EnumDescriptor: + fmt.Printf("%s is an enum:\n", dsc.GetFullyQualifiedName()) + case *desc.EnumValueDescriptor: + fmt.Printf("%s is an enum value:\n", dsc.GetFullyQualifiedName()) + case *desc.ServiceDescriptor: + fmt.Printf("%s is a service:\n", dsc.GetFullyQualifiedName()) + case *desc.MethodDescriptor: + fmt.Printf("%s is a method:\n", dsc.GetFullyQualifiedName()) + default: + err = fmt.Errorf("descriptor has unrecognized type %T", dsc) + fail(err, "Failed to describe symbol %q", s) + } + fmt.Println(txt) + } + + } else { + // Invoke an RPC + if cc == nil { + cc = dial() + } + var dec *json.Decoder + if *data == "@" { + dec = json.NewDecoder(os.Stdin) + } else { + dec = json.NewDecoder(strings.NewReader(*data)) + } + + h := &handler{dec: dec, descSource: descSource} + err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, addlHeaders, h, h.getRequestData) + if err != nil { + fail(err, "Error invoking method %q", symbol) + } + reqSuffix := "" + respSuffix := "" + if h.reqCount != 1 { + reqSuffix = "s" + } + if h.respCount != 1 { + respSuffix = "s" + } + fmt.Printf("Sent %d request%s and received %d response%s\n", h.reqCount, reqSuffix, h.respCount, respSuffix) + if h.stat.Code() != codes.OK { + fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.stat.Code().String(), h.stat.Message()) + exit(1) + } + } +} + +func usage() { + fmt.Fprintf(os.Stderr, `Usage: + %s [flags] [host:port] [list|describe] [symbol] + +The 'host:port' is only optional when used with 'list' or 'describe' and a +protoset flag is provided. + +If 'list' is indicated, the symbol (if present) should be a fully-qualified +service name. If present, all methods of that service are listed. If not +present, all exposed services are listed, or all services defined in protosets. + +If 'describe' is indicated, the descriptor for the given symbol is shown. The +symbol should be a fully-qualified service, enum, or message name. If no symbol +is given then the descriptors for all exposed or known services are shown. + +If neither verb is present, the symbol must be a fully-qualified method name in +'service/method' or 'service.method' format. In this case, the request body will +be used to invoke the named method. If no body is given, an empty instance of +the method's request type will be sent. + +`, os.Args[0]) + flag.PrintDefaults() + +} + +func fail(err error, msg string, args ...interface{}) { + if err != nil { + msg += ": %v" + args = append(args, err) + } + fmt.Fprintf(os.Stderr, msg, args...) + fmt.Fprintln(os.Stderr) + if err != nil { + exit(1) + } else { + // nil error means it was CLI usage issue + fmt.Fprintf(os.Stderr, "Try '%s -help' for more details.\n", os.Args[0]) + exit(2) + } +} + +type handler struct { + dec *json.Decoder + descSource grpcurl.DescriptorSource + reqCount int + respCount int + stat *status.Status +} + +func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) { + if *verbose { + txt, err := grpcurl.GetDescriptorText(md, h.descSource) + if err == nil { + fmt.Printf("\nResolved method descriptor:\n%s\n", txt) + } + } +} + +func (*handler) OnSendHeaders(md metadata.MD) { + if *verbose { + fmt.Printf("\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md)) + } +} + +func (h *handler) getRequestData() (json.RawMessage, error) { + // we don't use a mutex, though this methods will be called from different goroutine + // than other methods for bidi calls, because this method does not share any state + // with the other methods. + var msg json.RawMessage + if err := h.dec.Decode(&msg); err != nil { + return nil, err + } else { + h.reqCount++ + return msg, nil + } +} + +func (*handler) OnReceiveHeaders(md metadata.MD) { + if *verbose { + fmt.Printf("\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md)) + } +} + +func (h *handler) OnReceiveResponse(rsp json.RawMessage) { + h.respCount++ + if *verbose { + fmt.Print("\nResponse contents:\n") + } + fmt.Println(string(rsp)) +} + +func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { + h.stat = stat + if *verbose { + fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md)) + } +} diff --git a/cmd/testserver/testserver.go b/cmd/testserver/testserver.go new file mode 100644 index 0000000..24fc055 --- /dev/null +++ b/cmd/testserver/testserver.go @@ -0,0 +1,168 @@ +// Command testserver spins up a test GRPC server. +package main + +import ( + "flag" + "fmt" + "net" + "os" + "sync/atomic" + "time" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" + + "github.com/fullstorydev/grpcurl" + grpcurl_testing "github.com/fullstorydev/grpcurl/testing" +) + +var ( + help = flag.Bool("help", false, "Print usage instructions and exit.") + cacert = flag.String("cacert", "", + "File containing trusted root certificates for verifying client certs. Ignored if\n"+ + " TLS is not in use (e.g. no -cert or -key specified).") + cert = flag.String("cert", "", + "File containing server certificate (public key). Must also provide -key option.\n"+ + " Server uses plain-text if no -cert and -key options are given.") + key = flag.String("key", "", + "File containing server private key. Must also provide -cert option. Server uses\n"+ + " plain-text if no -cert and -key options are given.") + requirecert = flag.Bool("requirecert", false, + "Require clients to authenticate via client certs. Must be using TLS (e.g. must also\n"+ + " provide -cert and -key options).") + port = flag.Int("p", 0, "Port on which to listen. Ephemeral port used if not specified.") + noreflect = flag.Bool("noreflect", false, "Indicates that server should not support server reflection.") + quiet = flag.Bool("q", false, "Suppresses server request and stream logging.") +) + +func main() { + flag.Parse() + + if *help { + flag.PrintDefaults() + os.Exit(0) + } + + grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stderr)) + + if len(flag.Args()) > 0 { + fmt.Fprintln(os.Stderr, "No arguments expected.") + os.Exit(2) + } + if (*cert == "") != (*key == "") { + fmt.Fprintln(os.Stderr, "The -cert and -key arguments must be used together and both be present.") + os.Exit(2) + } + if *requirecert && *cert == "" { + fmt.Fprintln(os.Stderr, "The -requirecert arg cannot be used without -cert and -key arguments.") + os.Exit(2) + } + + var opts []grpc.ServerOption + if *cert != "" { + creds, err := grpcurl.ServerTransportCredentials(*cacert, *cert, *key, *requirecert) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to configure transport credentials: %v\n", err) + os.Exit(1) + } + opts = []grpc.ServerOption{grpc.Creds(creds)} + } + if !*quiet { + opts = append(opts, grpc.UnaryInterceptor(unaryLogger), grpc.StreamInterceptor(streamLogger)) + } + + l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to listen on socket: %v\n", err) + os.Exit(1) + } + p := l.Addr().(*net.TCPAddr).Port + fmt.Printf("Listening on 127.0.0.1:%d\n", p) + + svr := grpc.NewServer(opts...) + + grpc_testing.RegisterTestServiceServer(svr, grpcurl_testing.TestServer{}) + if !*noreflect { + reflection.Register(svr) + } + + if err := svr.Serve(l); err != nil { + fmt.Fprintf(os.Stderr, "GRPC server returned error: %v\n", err) + os.Exit(1) + } +} + +var id int32 + +func unaryLogger(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + i := atomic.AddInt32(&id, 1) - 1 + grpclog.Printf("start <%d>: %s\n", i, info.FullMethod) + start := time.Now() + rsp, err := handler(ctx, req) + var code codes.Code + if stat, ok := status.FromError(err); ok { + code = stat.Code() + } else { + code = codes.Unknown + } + grpclog.Printf("completed <%d>: %v (%d) %v\n", i, code, code, time.Now().Sub(start)) + return rsp, err +} + +func streamLogger(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + i := atomic.AddInt32(&id, 1) - 1 + start := time.Now() + grpclog.Printf("start <%d>: %s\n", i, info.FullMethod) + err := handler(srv, loggingStream{ss: ss, id: i}) + var code codes.Code + if stat, ok := status.FromError(err); ok { + code = stat.Code() + } else { + code = codes.Unknown + } + grpclog.Printf("completed <%d>: %v(%d) %v\n", i, code, code, time.Now().Sub(start)) + return err +} + +type loggingStream struct { + ss grpc.ServerStream + id int32 +} + +func (l loggingStream) SetHeader(md metadata.MD) error { + return l.ss.SetHeader(md) +} + +func (l loggingStream) SendHeader(md metadata.MD) error { + return l.ss.SendHeader(md) +} + +func (l loggingStream) SetTrailer(md metadata.MD) { + l.ss.SetTrailer(md) +} + +func (l loggingStream) Context() context.Context { + return l.ss.Context() +} + +func (l loggingStream) SendMsg(m interface{}) error { + err := l.ss.SendMsg(m) + if err == nil { + grpclog.Printf("stream <%d>: sent message\n", l.id) + } + return err +} + +func (l loggingStream) RecvMsg(m interface{}) error { + err := l.ss.RecvMsg(m) + if err == nil { + grpclog.Printf("stream <%d>: received message\n", l.id) + } + return err +} diff --git a/grpcurl.go b/grpcurl.go new file mode 100644 index 0000000..330c78f --- /dev/null +++ b/grpcurl.go @@ -0,0 +1,846 @@ +// Package grpcurl provides the core functionality exposed by the grpcurl command, for +// dynamically connecting to a server, using the reflection service to inspect the server, +// and invoking RPCs. The grpcurl command-line tool constructs a DescriptorSource, based +// on the command-line parameters, and supplies an InvocationEventHandler to supply request +// data (which can come from command-line args or the process's stdin) and to log the +// events (to the process's stdout). +package grpcurl + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "sort" + "strings" + "sync" + "sync/atomic" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/protoc-gen-go/descriptor" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/dynamic" + "github.com/jhump/protoreflect/dynamic/grpcdynamic" + "github.com/jhump/protoreflect/grpcreflect" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +var ErrReflectionNotSupported = errors.New("server does not support the reflection API") + +// DescriptorSource is a source of protobuf descriptor information. It can be backed by a FileDescriptorSet +// proto (like a file generated by protoc) or a remote server that supports the reflection API. +type DescriptorSource interface { + // ListServices returns a list of fully-qualified service names. It will be all services in a set of + // descriptor files or the set of all services exposed by a GRPC server. + ListServices() ([]string, error) + // FindSymbol returns a descriptor for the given fully-qualified symbol name. + FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) + // AllExtensionsForType returns all known extension fields that extend the given message type name. + AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) +} + +// DescriptorSourceFromProtoSets creates a DescriptorSource that is backed by the named files, whose contents +// are encoded FileDescriptorSet protos. +func DescriptorSourceFromProtoSets(fileNames ...string) (DescriptorSource, error) { + files := &descriptor.FileDescriptorSet{} + for _, fileName := range fileNames { + b, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, fmt.Errorf("could not load protoset file %q: %v", fileName, err) + } + var fs descriptor.FileDescriptorSet + err = proto.Unmarshal(b, &fs) + if err != nil { + return nil, fmt.Errorf("could not parse contents of protoset file %q: %v", fileName, err) + } + files.File = append(files.File, fs.File...) + } + + unresolved := map[string]*descriptor.FileDescriptorProto{} + for _, fd := range files.File { + unresolved[fd.GetName()] = fd + } + resolved := map[string]*desc.FileDescriptor{} + for _, fd := range files.File { + _, err := resolveFileDescriptor(unresolved, resolved, fd.GetName()) + if err != nil { + return nil, err + } + } + return &fileSource{files: resolved}, nil +} + +func resolveFileDescriptor(unresolved map[string]*descriptor.FileDescriptorProto, resolved map[string]*desc.FileDescriptor, filename string) (*desc.FileDescriptor, error) { + if r, ok := resolved[filename]; ok { + return r, nil + } + fd, ok := unresolved[filename] + if !ok { + return nil, fmt.Errorf("no descriptor found for %q", filename) + } + deps := make([]*desc.FileDescriptor, len(fd.GetDependency())) + for _, dep := range fd.GetDependency() { + depFd, err := resolveFileDescriptor(unresolved, resolved, dep) + if err != nil { + return nil, err + } + deps = append(deps, depFd) + } + result, err := desc.CreateFileDescriptor(fd, deps...) + if err != nil { + return nil, err + } + resolved[filename] = result + return result, nil +} + +type fileSource struct { + files map[string]*desc.FileDescriptor + er *dynamic.ExtensionRegistry + erInit sync.Once +} + +func (fs *fileSource) ListServices() ([]string, error) { + set := map[string]bool{} + for _, fd := range fs.files { + for _, svc := range fd.GetServices() { + set[svc.GetFullyQualifiedName()] = true + } + } + sl := make([]string, 0, len(set)) + for svc := range set { + sl = append(sl, svc) + } + return sl, nil +} + +func (fs *fileSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) { + for _, fd := range fs.files { + if dsc := fd.FindSymbol(fullyQualifiedName); dsc != nil { + return dsc, nil + } + } + return nil, notFound("Symbol", fullyQualifiedName) +} + +func (fs *fileSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) { + fs.erInit.Do(func() { + fs.er = &dynamic.ExtensionRegistry{} + for _, fd := range fs.files { + fs.er.AddExtensionsFromFile(fd) + } + }) + return fs.er.AllExtensionsForType(typeName), nil +} + +// DescriptorSourceFromServer creates a DescriptorSource that uses the given GRPC reflection client +// to interrogate a server for descriptor information. If the server does not support the reflection +// API then the various DescriptorSource methods will return ErrReflectionNotSupported +func DescriptorSourceFromServer(ctx context.Context, refClient *grpcreflect.Client) DescriptorSource { + return serverSource{client: refClient} +} + +type serverSource struct { + client *grpcreflect.Client +} + +func (ss serverSource) ListServices() ([]string, error) { + svcs, err := ss.client.ListServices() + return svcs, reflectionSupport(err) +} + +func (ss serverSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) { + file, err := ss.client.FileContainingSymbol(fullyQualifiedName) + if err != nil { + return nil, reflectionSupport(err) + } + d := file.FindSymbol(fullyQualifiedName) + if d == nil { + return nil, notFound("Symbol", fullyQualifiedName) + } + return d, nil +} + +func (ss serverSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) { + var exts []*desc.FieldDescriptor + nums, err := ss.client.AllExtensionNumbersForType(typeName) + if err != nil { + return nil, reflectionSupport(err) + } + for _, fieldNum := range nums { + ext, err := ss.client.ResolveExtension(typeName, fieldNum) + if err != nil { + return nil, reflectionSupport(err) + } + exts = append(exts, ext) + } + return exts, nil +} + +func reflectionSupport(err error) error { + if err == nil { + return nil + } + if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented { + return ErrReflectionNotSupported + } + return err +} + +// ListServices uses the given descriptor source to return a sorted list of fully-qualified +// service names. +func ListServices(source DescriptorSource) ([]string, error) { + svcs, err := source.ListServices() + if err != nil { + return nil, err + } + sort.Strings(svcs) + return svcs, nil +} + +// ListMethods uses the given descriptor source to return a sorted list of method names +// for the specified fully-qualified service name. +func ListMethods(source DescriptorSource, serviceName string) ([]string, error) { + dsc, err := source.FindSymbol(serviceName) + if err != nil { + return nil, err + } + if sd, ok := dsc.(*desc.ServiceDescriptor); !ok { + return nil, notFound("Service", serviceName) + } else { + methods := make([]string, 0, len(sd.GetMethods())) + for _, method := range sd.GetMethods() { + methods = append(methods, method.GetName()) + } + sort.Strings(methods) + return methods, nil + } +} + +type notFoundError string + +func notFound(kind, name string) error { + return notFoundError(fmt.Sprintf("%s not found: %s", kind, name)) +} + +func (e notFoundError) Error() string { + return string(e) +} + +func isNotFoundError(err error) bool { + if grpcreflect.IsElementNotFoundError(err) { + return true + } + _, ok := err.(notFoundError) + return ok +} + +// InvocationEventHandler is a bag of callbacks for handling events that occur in the course +// of invoking an RPC. The handler also provides request data that is sent. The callbacks are +// generally called in the order they are listed below. +type InvocationEventHandler interface { + // OnResolveMethod is called with a descriptor of the method that is being invoked. + OnResolveMethod(*desc.MethodDescriptor) + // OnSendHeaders is called with the request metadata that is being sent. + OnSendHeaders(metadata.MD) + // OnReceiveHeaders is called when response headers have been received. + OnReceiveHeaders(metadata.MD) + // OnReceiveResponse is called for each response message received. + OnReceiveResponse(json.RawMessage) + // OnReceiveTrailers is called when response trailers and final RPC status have been received. + OnReceiveTrailers(*status.Status, metadata.MD) +} + +type RequestMessageSupplier func() (json.RawMessage, error) + +// InvokeRpc uses te given GRPC connection to invoke the given method. The given descriptor source +// is used to determine the type of method and the type of request and response message. The given +// headers are sent as request metadata. Methods on the given event handler are called as the +// invocation proceeds. +// +// The given requestData function supplies the actual data to send. It should return io.EOF when +// there is no more request data. If it returns a nil error then the returned JSON message should +// not be blank. If the method being invoked is a unary or server-streaming RPC (e.g. exactly one +// request message) and there is no request data (e.g. the first invocation of the function returns +// io.EOF), then a blank request message is sent, as if the request data were an empty object: "{}". +// +// If the requestData function and the given event handler coordinate or share any state, they should +// be thread-safe. This is because the requestData function may be called from a different goroutine +// than the one invoking event callbacks. (This only happens for bi-directional streaming RPCs, where +// one goroutine sends request messages and another consumes the response messages). +func InvokeRpc(ctx context.Context, source DescriptorSource, cc *grpc.ClientConn, methodName string, + headers []string, handler InvocationEventHandler, requestData RequestMessageSupplier) error { + + md := MetadataFromHeaders(headers) + + svc, mth := parseSymbol(methodName) + if svc == "" || mth == "" { + return fmt.Errorf("given method name %q is not in expected format: 'service/method' or 'service.method'", methodName) + } + dsc, err := source.FindSymbol(svc) + if err != nil { + if isNotFoundError(err) { + return fmt.Errorf("target server does not expose service %q", svc) + } else { + return fmt.Errorf("failed to query for service descriptor %q: %v", svc, err) + } + } + sd, ok := dsc.(*desc.ServiceDescriptor) + if !ok { + return fmt.Errorf("target server does not expose service %q", svc) + } + mtd := sd.FindMethodByName(mth) + if mtd == nil { + return fmt.Errorf("service %q does not include a method named %q", svc, mth) + } + + handler.OnResolveMethod(mtd) + + // we also download any applicable extensions so we can provide full support for parsing user-provided data + var ext dynamic.ExtensionRegistry + alreadyFetched := map[string]bool{} + if err = fetchAllExtensions(source, &ext, mtd.GetInputType(), alreadyFetched); err != nil { + return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetInputType().GetFullyQualifiedName(), err) + } + if err = fetchAllExtensions(source, &ext, mtd.GetOutputType(), alreadyFetched); err != nil { + return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetOutputType().GetFullyQualifiedName(), err) + } + + msgFactory := dynamic.NewMessageFactoryWithExtensionRegistry(&ext) + req := msgFactory.NewMessage(mtd.GetInputType()) + + handler.OnSendHeaders(md) + ctx = metadata.NewOutgoingContext(ctx, md) + + stub := grpcdynamic.NewStubWithMessageFactory(cc, msgFactory) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if mtd.IsClientStreaming() && mtd.IsServerStreaming() { + return invokeBidi(ctx, cancel, stub, mtd, handler, requestData, req) + } else if mtd.IsClientStreaming() { + return invokeClientStream(ctx, stub, mtd, handler, requestData, req) + } else if mtd.IsServerStreaming() { + return invokeServerStream(ctx, stub, mtd, handler, requestData, req) + } else { + return invokeUnary(ctx, stub, mtd, handler, requestData, req) + } +} + +func invokeUnary(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, + requestData RequestMessageSupplier, req proto.Message) error { + + data, err := requestData() + if err != nil && err != io.EOF { + return fmt.Errorf("error getting request data: %v", err) + } + if len(data) != 0 { + err = jsonpb.UnmarshalString(string(data), req) + if err != nil { + return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err) + } + } + if err != io.EOF { + // verify there is no second message, which is a usage error + _, err := requestData() + if err == nil { + return fmt.Errorf("method %q is a unary RPC, but request data contained more than 1 message", md.GetFullyQualifiedName()) + } else if err != io.EOF { + return fmt.Errorf("error getting request data: %v", err) + } + } + + // Now we can actually invoke the RPC! + var respHeaders metadata.MD + var respTrailers metadata.MD + resp, err := stub.InvokeRpc(ctx, md, req, grpc.Trailer(&respTrailers), grpc.Header(&respHeaders)) + + stat, ok := status.FromError(err) + if !ok { + // Error codes sent from the server will get printed differently below. + // So just bail for other kinds of errors here. + return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) + } + + handler.OnReceiveHeaders(respHeaders) + + var respStr string + if stat.Code() == codes.OK { + jsm := jsonpb.Marshaler{Indent: " "} + respStr, err = jsm.MarshalToString(resp) + if err != nil { + return fmt.Errorf("failed to generate JSON form of response message: %v", err) + } + handler.OnReceiveResponse(json.RawMessage(respStr)) + } + + handler.OnReceiveTrailers(stat, respTrailers) + + return nil +} + +func invokeClientStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, + requestData RequestMessageSupplier, req proto.Message) error { + + // invoke the RPC! + str, err := stub.InvokeRpcClientStream(ctx, md) + + // Upload each request message in the stream + var resp proto.Message + for err == nil { + var data json.RawMessage + data, err = requestData() + if err == io.EOF { + resp, err = str.CloseAndReceive() + break + } + if err != nil { + return fmt.Errorf("error getting request data: %v", err) + } + if len(data) != 0 { + err = jsonpb.UnmarshalString(string(data), req) + if err != nil { + return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err) + } + } + + err = str.SendMsg(req) + if err == io.EOF { + // We get EOF on send if the server says "go away" + // We have to use CloseAndReceive to get the actual code + resp, err = str.CloseAndReceive() + break + } + + req.Reset() + } + + // finally, process response data + stat, ok := status.FromError(err) + if !ok { + // Error codes sent from the server will get printed differently below. + // So just bail for other kinds of errors here. + return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) + } + + if respHeaders, err := str.Header(); err == nil { + handler.OnReceiveHeaders(respHeaders) + } + + var respStr string + if stat.Code() == codes.OK { + jsm := jsonpb.Marshaler{Indent: " "} + respStr, err = jsm.MarshalToString(resp) + if err != nil { + return fmt.Errorf("failed to generate JSON form of response message: %v", err) + } + handler.OnReceiveResponse(json.RawMessage(respStr)) + } + + handler.OnReceiveTrailers(stat, str.Trailer()) + + return nil +} + +func invokeServerStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, + requestData RequestMessageSupplier, req proto.Message) error { + + data, err := requestData() + if err != nil && err != io.EOF { + return fmt.Errorf("error getting request data: %v", err) + } + if len(data) != 0 { + err = jsonpb.UnmarshalString(string(data), req) + if err != nil { + return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err) + } + } + if err != io.EOF { + // verify there is no second message, which is a usage error + _, err := requestData() + if err == nil { + return fmt.Errorf("method %q is a server-streaming RPC, but request data contained more than 1 message", md.GetFullyQualifiedName()) + } else if err != io.EOF { + return fmt.Errorf("error getting request data: %v", err) + } + } + + // Now we can actually invoke the RPC! + str, err := stub.InvokeRpcServerStream(ctx, md, req) + + if respHeaders, err := str.Header(); err == nil { + handler.OnReceiveHeaders(respHeaders) + } + + // Download each response message + for err == nil { + var resp proto.Message + resp, err = str.RecvMsg() + if err != nil { + if err == io.EOF { + err = nil + } + break + } + jsm := jsonpb.Marshaler{Indent: " "} + respStr, err := jsm.MarshalToString(resp) + if err != nil { + return fmt.Errorf("failed to generate JSON form of response message: %v", err) + } + handler.OnReceiveResponse(json.RawMessage(respStr)) + } + + stat, ok := status.FromError(err) + if !ok { + // Error codes sent from the server will get printed differently below. + // So just bail for other kinds of errors here. + return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) + } + + handler.OnReceiveTrailers(stat, str.Trailer()) + + return nil +} + +func invokeBidi(ctx context.Context, cancel context.CancelFunc, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, + requestData RequestMessageSupplier, req proto.Message) error { + + // invoke the RPC! + str, err := stub.InvokeRpcBidiStream(ctx, md) + + // mutex protects access to handler and sendErr since we'll have two goroutines sharing them + var wg sync.WaitGroup + var sendErr atomic.Value + + defer wg.Wait() + + if err == nil { + wg.Add(1) + go func() { + defer wg.Done() + + // Concurrently upload each request message in the stream + var err error + var data json.RawMessage + for err == nil { + data, err = requestData() + + if err == io.EOF { + err = str.CloseSend() + break + } + if err != nil { + err = fmt.Errorf("error getting request data: %v", err) + break + } + if len(data) != 0 { + err = jsonpb.UnmarshalString(string(data), req) + if err != nil { + err = fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err) + break + } + } + + err = str.SendMsg(req) + + req.Reset() + } + + if err != nil { + sendErr.Store(err) + // signals error to other goroutine + cancel() + } + }() + } + + if respHeaders, err := str.Header(); err == nil { + handler.OnReceiveHeaders(respHeaders) + } + + // Download each response message + for err == nil { + var resp proto.Message + resp, err = str.RecvMsg() + if err != nil { + if err == io.EOF { + err = nil + } + break + } + jsm := jsonpb.Marshaler{Indent: " "} + respStr, err := jsm.MarshalToString(resp) + if err != nil { + return fmt.Errorf("failed to generate JSON form of response message: %v", err) + } + + handler.OnReceiveResponse(json.RawMessage(respStr)) + } + + if se, ok := sendErr.Load().(error); ok && se != io.EOF { + err = se + } + + stat, ok := status.FromError(err) + if !ok { + // Error codes sent from the server will get printed differently below. + // So just bail for other kinds of errors here. + return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) + } + + handler.OnReceiveTrailers(stat, str.Trailer()) + + return nil +} + +func MetadataFromHeaders(headers []string) metadata.MD { + md := make(metadata.MD) + for _, part := range headers { + if part != "" { + pieces := strings.SplitN(part, ":", 2) + if len(pieces) == 1 { + pieces = append(pieces, "") // if no value was specified, just make it "" (maybe the header value doesn't matter) + } + headerName := strings.ToLower(strings.TrimSpace(pieces[0])) + md[headerName] = append(md[headerName], strings.TrimSpace(pieces[1])) + } + } + return md +} + +func parseSymbol(svcAndMethod string) (string, string) { + pos := strings.LastIndex(svcAndMethod, "/") + if pos < 0 { + pos = strings.LastIndex(svcAndMethod, ".") + if pos < 0 { + return "", "" + } + } + return svcAndMethod[:pos], svcAndMethod[pos+1:] +} + +func MetadataToString(md metadata.MD) string { + if len(md) == 0 { + return "(empty)" + } + var b bytes.Buffer + for k, vs := range md { + for _, v := range vs { + b.WriteString(k) + b.WriteString(": ") + b.WriteString(v) + b.WriteString("\n") + } + } + return b.String() +} + +func GetDescriptorText(dsc desc.Descriptor, descSource DescriptorSource) (string, error) { + dscProto := EnsureExtensions(descSource, dsc.AsProto()) + return (&jsonpb.Marshaler{Indent: " "}).MarshalToString(dscProto) +} + +func EnsureExtensions(source DescriptorSource, msg proto.Message) proto.Message { + // load any server extensions so we can properly describe custom options + dsc, err := desc.LoadMessageDescriptorForMessage(msg) + if err != nil { + return msg + } + + var ext dynamic.ExtensionRegistry + if err = fetchAllExtensions(source, &ext, dsc, map[string]bool{}); err != nil { + return msg + } + + // convert message into dynamic message that knows about applicable extensions + // (that way we can show meaningful info for custom options instead of printing as unknown) + msgFactory := dynamic.NewMessageFactoryWithExtensionRegistry(&ext) + dm, err := fullyConvertToDynamic(msgFactory, msg) + if err != nil { + return msg + } + return dm +} + +// fetchAllExtensions recursively fetches from the server extensions for the given message type as well as +// for all message types of nested fields. The extensions are added to the given dynamic registry of extensions +// so that all server-known extensions can be correctly parsed by grpcurl. +func fetchAllExtensions(source DescriptorSource, ext *dynamic.ExtensionRegistry, md *desc.MessageDescriptor, alreadyFetched map[string]bool) error { + msgTypeName := md.GetFullyQualifiedName() + if alreadyFetched[msgTypeName] { + return nil + } + alreadyFetched[msgTypeName] = true + if len(md.GetExtensionRanges()) > 0 { + fds, err := source.AllExtensionsForType(msgTypeName) + for _, fd := range fds { + if err = ext.AddExtension(fd); err != nil { + return fmt.Errorf("could not register extension %d of type %s: %v", fd.GetFullyQualifiedName(), msgTypeName, err) + } + } + } + // recursively fetch extensions for the types of any message fields + for _, fd := range md.GetFields() { + if fd.GetMessageType() != nil { + err := fetchAllExtensions(source, ext, fd.GetMessageType(), alreadyFetched) + if err != nil { + return err + } + } + } + return nil +} + +// fullConvertToDynamic attempts to convert the given message to a dynamic message as well +// as any nested messages it may contain as field values. If the given message factory has +// extensions registered that were not known when the given message was parsed, this effectively +// allows re-parsing to identify those extensions. +func fullyConvertToDynamic(msgFact *dynamic.MessageFactory, msg proto.Message) (proto.Message, error) { + if _, ok := msg.(*dynamic.Message); ok { + return msg, nil // already a dynamic message + } + md, err := desc.LoadMessageDescriptorForMessage(msg) + if err != nil { + return nil, err + } + newMsg := msgFact.NewMessage(md) + dm, ok := newMsg.(*dynamic.Message) + if !ok { + // if message factory didn't produce a dynamic message, then we should leave msg as is + return msg, nil + } + + if err := dm.ConvertFrom(msg); err != nil { + return nil, err + } + + // recursively convert all field values, too + for _, fd := range md.GetFields() { + if fd.IsMap() { + if fd.GetMapValueType().GetMessageType() != nil { + m := dm.GetField(fd).(map[interface{}]interface{}) + for k, v := range m { + // keys can't be nested messages; so we only need to recurse through map values, not keys + newVal, err := fullyConvertToDynamic(msgFact, v.(proto.Message)) + if err != nil { + return nil, err + } else { + dm.PutMapField(fd, k, newVal) + } + } + } + } else if fd.IsRepeated() { + if fd.GetMessageType() != nil { + s := dm.GetField(fd).([]interface{}) + for i, e := range s { + newVal, err := fullyConvertToDynamic(msgFact, e.(proto.Message)) + if err != nil { + return nil, err + } else { + dm.SetRepeatedField(fd, i, newVal) + } + } + } + } else { + if fd.GetMessageType() != nil { + v := dm.GetField(fd) + newVal, err := fullyConvertToDynamic(msgFact, v.(proto.Message)) + if err != nil { + return nil, err + } else { + dm.SetField(fd, newVal) + } + } + } + } + return dm, nil +} + +// ClientTransportCredentials builds transport credentials for a GRPC client using the +// given properties. If cacertFile is blank, only standard trusted certs are used to +// verify the server certs. If clientCertFile is blank, the client will not use a client +// certificate. If clientCertFile is not blank then clientKeyFile must not be blank. +func ClientTransportCredentials(insecureSkipVerify bool, cacertFile, clientCertFile, clientKeyFile string) (credentials.TransportCredentials, error) { + var tlsConf tls.Config + + if clientCertFile != "" { + // Load the client certificates from disk + certificate, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile) + if err != nil { + return nil, fmt.Errorf("could not load client key pair: %v", err) + } + tlsConf.Certificates = []tls.Certificate{certificate} + } + + if insecureSkipVerify { + tlsConf.InsecureSkipVerify = true + } else if cacertFile != "" { + // Create a certificate pool from the certificate authority + certPool := x509.NewCertPool() + ca, err := ioutil.ReadFile(cacertFile) + if err != nil { + return nil, fmt.Errorf("could not read ca certificate: %v", err) + } + + // Append the certificates from the CA + if ok := certPool.AppendCertsFromPEM(ca); !ok { + return nil, errors.New("failed to append ca certs") + } + + tlsConf.RootCAs = certPool + } + + return credentials.NewTLS(&tlsConf), nil +} + +// ServerTransportCredentials builds transport credentials for a GRPC server using the +// given properties. If cacertFile is blank, the server will not request client certs +// unless requireClientCerts is true. When requireClientCerts is false and cacertFile is +// not blank, the server will verify client certs when presented, but will not require +// client certs. The serverCertFile and serverKeyFile must both not be blank. +func ServerTransportCredentials(cacertFile, serverCertFile, serverKeyFile string, requireClientCerts bool) (credentials.TransportCredentials, error) { + var tlsConf tls.Config + + // Load the server certificates from disk + certificate, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile) + if err != nil { + return nil, fmt.Errorf("could not load key pair: %v", err) + } + tlsConf.Certificates = []tls.Certificate{certificate} + + if cacertFile != "" { + // Create a certificate pool from the certificate authority + certPool := x509.NewCertPool() + ca, err := ioutil.ReadFile(cacertFile) + if err != nil { + return nil, fmt.Errorf("could not read ca certificate: %v", err) + } + + // Append the certificates from the CA + if ok := certPool.AppendCertsFromPEM(ca); !ok { + return nil, errors.New("failed to append ca certs") + } + + tlsConf.ClientCAs = certPool + } + + if requireClientCerts { + tlsConf.ClientAuth = tls.RequireAndVerifyClientCert + } else if cacertFile != "" { + tlsConf.ClientAuth = tls.VerifyClientCertIfGiven + } else { + tlsConf.ClientAuth = tls.NoClientCert + } + + return credentials.NewTLS(&tlsConf), nil +} diff --git a/grpcurl_test.go b/grpcurl_test.go new file mode 100644 index 0000000..d06798a --- /dev/null +++ b/grpcurl_test.go @@ -0,0 +1,666 @@ +package grpcurl_test + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + "reflect" + "strings" + "testing" + "time" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/grpcreflect" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/reflection" + reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + "google.golang.org/grpc/status" + + . "github.com/fullstorydev/grpcurl" + grpcurl_testing "github.com/fullstorydev/grpcurl/testing" +) + +var ( + sourceProtoset DescriptorSource + ccProtoset *grpc.ClientConn + + sourceReflect DescriptorSource + ccReflect *grpc.ClientConn +) + +func TestMain(m *testing.M) { + var err error + sourceProtoset, err = DescriptorSourceFromProtoSets("testing/test.protoset") + if err != nil { + panic(err) + } + + // Create a server that includes the reflection service + svrReflect := grpc.NewServer() + grpc_testing.RegisterTestServiceServer(svrReflect, grpcurl_testing.TestServer{}) + reflection.Register(svrReflect) + var portReflect int + if l, err := net.Listen("tcp", "127.0.0.1:0"); err != nil { + panic(err) + } else { + portReflect = l.Addr().(*net.TCPAddr).Port + go svrReflect.Serve(l) + } + defer svrReflect.Stop() + + // And a corresponding client + if ccReflect, err = grpc.Dial(fmt.Sprintf("127.0.0.1:%d", portReflect), + grpc.WithInsecure(), grpc.WithTimeout(10*time.Second), grpc.WithBlock()); err != nil { + panic(err) + } + defer ccReflect.Close() + refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(ccReflect)) + defer refClient.Reset() + + sourceReflect = DescriptorSourceFromServer(context.Background(), refClient) + + // Also create a server that does *not* include the reflection service + svrProtoset := grpc.NewServer() + grpc_testing.RegisterTestServiceServer(svrProtoset, grpcurl_testing.TestServer{}) + var portProtoset int + if l, err := net.Listen("tcp", "127.0.0.1:0"); err != nil { + panic(err) + } else { + portProtoset = l.Addr().(*net.TCPAddr).Port + go svrProtoset.Serve(l) + } + defer svrProtoset.Stop() + + // And a corresponding client + if ccProtoset, err = grpc.Dial(fmt.Sprintf("127.0.0.1:%d", portProtoset), + grpc.WithInsecure(), grpc.WithTimeout(10*time.Second), grpc.WithBlock()); err != nil { + panic(err) + } + defer ccProtoset.Close() + + os.Exit(m.Run()) +} + +func TestServerDoesNotSupportReflection(t *testing.T) { + refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(ccProtoset)) + defer refClient.Reset() + + refSource := DescriptorSourceFromServer(context.Background(), refClient) + + _, err := ListServices(refSource) + if err != ErrReflectionNotSupported { + t.Errorf("ListServices should have returned ErrReflectionNotSupported; instead got %v", err) + } + + _, err = ListMethods(refSource, "SomeService") + if err != ErrReflectionNotSupported { + t.Errorf("ListMethods should have returned ErrReflectionNotSupported; instead got %v", err) + } + + err = InvokeRpc(context.Background(), refSource, ccProtoset, "FooService/Method", nil, nil, nil) + // InvokeRpc wraps the error, so we just verify the returned error includes the right message + if err == nil || !strings.Contains(err.Error(), ErrReflectionNotSupported.Error()) { + t.Errorf("InvokeRpc should have returned ErrReflectionNotSupported; instead got %v", err) + } +} + +func TestListServicesProtoset(t *testing.T) { + doTestListServices(t, sourceProtoset, false) +} + +func TestListServicesReflect(t *testing.T) { + doTestListServices(t, sourceReflect, true) +} + +func doTestListServices(t *testing.T, source DescriptorSource, includeReflection bool) { + names, err := ListServices(source) + if err != nil { + t.Fatalf("failed to list services: %v", err) + } + var expected []string + if includeReflection { + // when using server reflection, we see the TestService as well as the ServerReflection service + expected = []string{"grpc.reflection.v1alpha.ServerReflection", "grpc.testing.TestService"} + } else { + // without reflection, we see all services defined in the same test.proto file, which is the + // TestService as well as UnimplementedService + expected = []string{"grpc.testing.TestService", "grpc.testing.UnimplementedService"} + } + if !reflect.DeepEqual(expected, names) { + t.Errorf("ListServices returned wrong results: wanted %v, got %v", expected, names) + } +} + +func TestListMethodsProtoset(t *testing.T) { + doTestListMethods(t, sourceProtoset, false) +} + +func TestListMethodsReflect(t *testing.T) { + doTestListMethods(t, sourceReflect, true) +} + +func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection bool) { + names, err := ListMethods(source, "grpc.testing.TestService") + if err != nil { + t.Fatalf("failed to list methods for TestService: %v", err) + } + expected := []string{ + "EmptyCall", + "FullDuplexCall", + "HalfDuplexCall", + "StreamingInputCall", + "StreamingOutputCall", + "UnaryCall", + } + if !reflect.DeepEqual(expected, names) { + t.Errorf("ListMethods returned wrong results: wanted %v, got %v", expected, names) + } + + if includeReflection { + // when using server reflection, we see the TestService as well as the ServerReflection service + names, err = ListMethods(source, "grpc.reflection.v1alpha.ServerReflection") + if err != nil { + t.Fatalf("failed to list methods for ServerReflection: %v", err) + } + expected = []string{"ServerReflectionInfo"} + } else { + // without reflection, we see all services defined in the same test.proto file, which is the + // TestService as well as UnimplementedService + names, err = ListMethods(source, "grpc.testing.UnimplementedService") + if err != nil { + t.Fatalf("failed to list methods for ServerReflection: %v", err) + } + expected = []string{"UnimplementedCall"} + } + if !reflect.DeepEqual(expected, names) { + t.Errorf("ListMethods returned wrong results: wanted %v, got %v", expected, names) + } + + // force an error + _, err = ListMethods(source, "FooService") + if err != nil && !strings.Contains(err.Error(), "Symbol not found: FooService") { + t.Errorf("ListMethods should have returned 'not found' error but instead returned %v", err) + } +} + +func TestDescribeProtoset(t *testing.T) { + doTestDescribe(t, sourceProtoset) +} + +func TestDescribeReflect(t *testing.T) { + doTestDescribe(t, sourceReflect) +} + +func doTestDescribe(t *testing.T, source DescriptorSource) { + sym := "grpc.testing.TestService.EmptyCall" + dsc, err := source.FindSymbol(sym) + if err != nil { + t.Fatalf("failed to get descriptor for %q: %v", sym, err) + } + if _, ok := dsc.(*desc.MethodDescriptor); !ok { + t.Fatalf("descriptor for %q was a %T (expecting a MethodDescriptor)", sym, dsc) + } + txt := proto.MarshalTextString(dsc.AsProto()) + expected := + `name: "EmptyCall" +input_type: ".grpc.testing.Empty" +output_type: ".grpc.testing.Empty" +` + if expected != txt { + t.Errorf("descriptor mismatch: expected %s, got %s", expected, txt) + } + + sym = "grpc.testing.StreamingOutputCallResponse" + dsc, err = source.FindSymbol(sym) + if err != nil { + t.Fatalf("failed to get descriptor for %q: %v", sym, err) + } + if _, ok := dsc.(*desc.MessageDescriptor); !ok { + t.Fatalf("descriptor for %q was a %T (expecting a MessageDescriptor)", sym, dsc) + } + txt = proto.MarshalTextString(dsc.AsProto()) + expected = + `name: "StreamingOutputCallResponse" +field: < + name: "payload" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".grpc.testing.Payload" + json_name: "payload" +> +` + if expected != txt { + t.Errorf("descriptor mismatch: expected %s, got %s", expected, txt) + } + + _, err = source.FindSymbol("FooService") + if err != nil && !strings.Contains(err.Error(), "Symbol not found: FooService") { + t.Errorf("FindSymbol should have returned 'not found' error but instead returned %v", err) + } +} + +const ( + // type == COMPRESSABLE, but that is default (since it has + // numeric value == 0) and thus doesn't actually get included + // on the wire + payload1 = `{ + "payload": { + "body": "SXQncyBCdXNpbmVzcyBUaW1l" + } +}` + payload2 = `{ + "payload": { + "type": "RANDOM", + "body": "Rm91eCBkdSBGYUZh" + } +}` + payload3 = `{ + "payload": { + "type": "UNCOMPRESSABLE", + "body": "SGlwaG9wb3BvdGFtdXMgdnMuIFJoeW1lbm9jZXJvcw==" + } +}` +) + +func TestUnaryProtoset(t *testing.T) { + doTestUnary(t, ccProtoset, sourceProtoset) +} + +func TestUnaryReflect(t *testing.T) { + doTestUnary(t, ccReflect, sourceReflect) +} + +func doTestUnary(t *testing.T, cc *grpc.ClientConn, source DescriptorSource) { + // Success + h := &handler{reqMessages: []string{payload1}} + err := InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/UnaryCall", makeHeaders(codes.OK), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + if h.check(t, "grpc.testing.TestService.UnaryCall", codes.OK, 1, 1) { + if h.respMessages[0] != payload1 { + t.Errorf("unexpected response from RPC: expecting %s; got %s", payload1, h.respMessages[0]) + } + } + + // Failure + h = &handler{reqMessages: []string{payload1}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/UnaryCall", makeHeaders(codes.NotFound), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.UnaryCall", codes.NotFound, 1, 0) +} + +func TestClientStreamProtoset(t *testing.T) { + doTestClientStream(t, ccProtoset, sourceProtoset) +} + +func TestClientStreamReflect(t *testing.T) { + doTestClientStream(t, ccReflect, sourceReflect) +} + +func doTestClientStream(t *testing.T, cc *grpc.ClientConn, source DescriptorSource) { + // Success + h := &handler{reqMessages: []string{payload1, payload2, payload3}} + err := InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingInputCall", makeHeaders(codes.OK), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + if h.check(t, "grpc.testing.TestService.StreamingInputCall", codes.OK, 3, 1) { + expected := + `{ + "aggregatedPayloadSize": 61 +}` + if h.respMessages[0] != expected { + t.Errorf("unexpected response from RPC: expecting %s; got %s", expected, h.respMessages[0]) + } + } + + // Fail fast (server rejects as soon as possible) + h = &handler{reqMessages: []string{payload1, payload2, payload3}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingInputCall", makeHeaders(codes.InvalidArgument), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.StreamingInputCall", codes.InvalidArgument, -3, 0) + + // Fail late (server waits until stream is complete to reject) + h = &handler{reqMessages: []string{payload1, payload2, payload3}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingInputCall", makeHeaders(codes.Internal, true), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.StreamingInputCall", codes.Internal, 3, 0) +} + +func TestServerStreamProtoset(t *testing.T) { + doTestServerStream(t, ccProtoset, sourceProtoset) +} + +func TestServerStreamReflect(t *testing.T) { + doTestServerStream(t, ccReflect, sourceReflect) +} + +func doTestServerStream(t *testing.T, cc *grpc.ClientConn, source DescriptorSource) { + req := &grpc_testing.StreamingOutputCallRequest{ + ResponseType: grpc_testing.PayloadType_COMPRESSABLE, + ResponseParameters: []*grpc_testing.ResponseParameters{ + {Size: 10}, {Size: 20}, {Size: 30}, {Size: 40}, {Size: 50}, + }, + } + payload, err := (&jsonpb.Marshaler{}).MarshalToString(req) + if err != nil { + t.Fatalf("failed to construct request: %v", err) + } + + // Success + h := &handler{reqMessages: []string{payload}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingOutputCall", makeHeaders(codes.OK), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + if h.check(t, "grpc.testing.TestService.StreamingOutputCall", codes.OK, 1, 5) { + resp := &grpc_testing.StreamingOutputCallResponse{} + for i, msg := range h.respMessages { + if err := jsonpb.UnmarshalString(msg, resp); err != nil { + t.Errorf("failed to parse response %d: %v", i+1, err) + } + if resp.Payload.GetType() != grpc_testing.PayloadType_COMPRESSABLE { + t.Errorf("response %d has wrong payload type; expecting %v, got %v", i, grpc_testing.PayloadType_COMPRESSABLE, resp.Payload.Type) + } + if len(resp.Payload.Body) != (i+1)*10 { + t.Errorf("response %d has wrong payload size; expecting %d, got %d", i, (i+1)*10, len(resp.Payload.Body)) + } + resp.Reset() + } + } + + // Fail fast (server rejects as soon as possible) + h = &handler{reqMessages: []string{payload}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingOutputCall", makeHeaders(codes.Aborted), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.StreamingOutputCall", codes.Aborted, 1, 0) + + // Fail late (server waits until stream is complete to reject) + h = &handler{reqMessages: []string{payload}} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/StreamingOutputCall", makeHeaders(codes.AlreadyExists, true), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.StreamingOutputCall", codes.AlreadyExists, 1, 5) +} + +func TestHalfDuplexStreamProtoset(t *testing.T) { + doTestHalfDuplexStream(t, ccProtoset, sourceProtoset) +} + +func TestHalfDuplexStreamReflect(t *testing.T) { + doTestHalfDuplexStream(t, ccReflect, sourceReflect) +} + +func doTestHalfDuplexStream(t *testing.T, cc *grpc.ClientConn, source DescriptorSource) { + reqs := []string{payload1, payload2, payload3} + + // Success + h := &handler{reqMessages: reqs} + err := InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/HalfDuplexCall", makeHeaders(codes.OK), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + if h.check(t, "grpc.testing.TestService.HalfDuplexCall", codes.OK, 3, 3) { + for i, resp := range h.respMessages { + if resp != reqs[i] { + t.Errorf("unexpected response %d from RPC:\nexpecting %q\ngot %q", i, reqs[i], resp) + } + } + } + + // Fail fast (server rejects as soon as possible) + h = &handler{reqMessages: reqs} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/HalfDuplexCall", makeHeaders(codes.Canceled), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.HalfDuplexCall", codes.Canceled, -3, 0) + + // Fail late (server waits until stream is complete to reject) + h = &handler{reqMessages: reqs} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/HalfDuplexCall", makeHeaders(codes.DataLoss, true), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.HalfDuplexCall", codes.DataLoss, 3, 3) +} + +func TestFullDuplexStreamProtoset(t *testing.T) { + doTestFullDuplexStream(t, ccProtoset, sourceProtoset) +} + +func TestFullDuplexStreamReflect(t *testing.T) { + doTestFullDuplexStream(t, ccReflect, sourceReflect) +} + +func doTestFullDuplexStream(t *testing.T, cc *grpc.ClientConn, source DescriptorSource) { + reqs := make([]string, 3) + req := &grpc_testing.StreamingOutputCallRequest{ + ResponseType: grpc_testing.PayloadType_RANDOM, + } + for i := range reqs { + req.ResponseParameters = append(req.ResponseParameters, &grpc_testing.ResponseParameters{Size: int32((i + 1) * 10)}) + payload, err := (&jsonpb.Marshaler{}).MarshalToString(req) + if err != nil { + t.Fatalf("failed to construct request %d: %v", i, err) + } + reqs[i] = payload + } + + // Success + h := &handler{reqMessages: reqs} + err := InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/FullDuplexCall", makeHeaders(codes.OK), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + if h.check(t, "grpc.testing.TestService.FullDuplexCall", codes.OK, 3, 6) { + resp := &grpc_testing.StreamingOutputCallResponse{} + i := 0 + for j := 1; j < 3; j++ { + // three requests + for k := 0; k < j; k++ { + // 1 response for first request, 2 for second, etc + msg := h.respMessages[i] + if err := jsonpb.UnmarshalString(msg, resp); err != nil { + t.Errorf("failed to parse response %d: %v", i+1, err) + } + if resp.Payload.GetType() != grpc_testing.PayloadType_RANDOM { + t.Errorf("response %d has wrong payload type; expecting %v, got %v", i, grpc_testing.PayloadType_RANDOM, resp.Payload.Type) + } + if len(resp.Payload.Body) != (k+1)*10 { + t.Errorf("response %d has wrong payload size; expecting %d, got %d", i, (k+1)*10, len(resp.Payload.Body)) + } + resp.Reset() + + i++ + } + } + } + + // Fail fast (server rejects as soon as possible) + h = &handler{reqMessages: reqs} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/FullDuplexCall", makeHeaders(codes.PermissionDenied), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.FullDuplexCall", codes.PermissionDenied, -3, 0) + + // Fail late (server waits until stream is complete to reject) + h = &handler{reqMessages: reqs} + err = InvokeRpc(context.Background(), source, cc, "grpc.testing.TestService/FullDuplexCall", makeHeaders(codes.ResourceExhausted, true), h, h.getRequestData) + if err != nil { + t.Fatalf("unexpected error during RPC: %v", err) + } + + h.check(t, "grpc.testing.TestService.FullDuplexCall", codes.ResourceExhausted, 3, 6) +} + +type handler struct { + method *desc.MethodDescriptor + methodCount int + reqHeaders metadata.MD + reqHeadersCount int + reqMessages []string + reqMessagesCount int + respHeaders metadata.MD + respHeadersCount int + respMessages []string + respTrailers metadata.MD + respStatus *status.Status + respTrailersCount int +} + +func (h *handler) getRequestData() (json.RawMessage, error) { + // we don't use a mutex, though this method will be called from different goroutine + // than other methods for bidi calls, because this method does not share any state + // with the other methods. + h.reqMessagesCount++ + if h.reqMessagesCount > len(h.reqMessages) { + return nil, io.EOF + } + if h.reqMessagesCount > 1 { + // insert delay between messages in request stream + time.Sleep(time.Millisecond * 50) + } + return json.RawMessage(h.reqMessages[h.reqMessagesCount-1]), nil +} + +func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) { + h.methodCount++ + h.method = md +} + +func (h *handler) OnSendHeaders(md metadata.MD) { + h.reqHeadersCount++ + h.reqHeaders = md +} + +func (h *handler) OnReceiveHeaders(md metadata.MD) { + h.respHeadersCount++ + h.respHeaders = md +} + +func (h *handler) OnReceiveResponse(msg json.RawMessage) { + h.respMessages = append(h.respMessages, string(msg)) +} + +func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { + h.respTrailersCount++ + h.respTrailers = md + h.respStatus = stat +} + +func (h *handler) check(t *testing.T, expectedMethod string, expectedCode codes.Code, expectedRequestQueries, expectedResponses int) bool { + // verify a few things were only ever called once + if h.methodCount != 1 { + t.Errorf("expected grpcurl to invoke OnResolveMethod once; was %d", h.methodCount) + } + if h.reqHeadersCount != 1 { + t.Errorf("expected grpcurl to invoke OnSendHeaders once; was %d", h.reqHeadersCount) + } + if h.reqHeadersCount != 1 { + t.Errorf("expected grpcurl to invoke OnSendHeaders once; was %d", h.reqHeadersCount) + } + if h.respHeadersCount != 1 { + t.Errorf("expected grpcurl to invoke OnReceiveHeaders once; was %d", h.respHeadersCount) + } + if h.respTrailersCount != 1 { + t.Errorf("expected grpcurl to invoke OnReceiveTrailers once; was %d", h.respTrailersCount) + } + + // check other stuff against given expectations + if h.method.GetFullyQualifiedName() != expectedMethod { + t.Errorf("wrong method: expecting %v, got %v", expectedMethod, h.method.GetFullyQualifiedName()) + } + if h.respStatus.Code() != expectedCode { + t.Errorf("wrong code: expecting %v, got %v", expectedCode, h.respStatus.Code()) + } + if expectedRequestQueries < 0 { + // negative expectation means "negate and expect up to that number; could be fewer" + if h.reqMessagesCount > -expectedRequestQueries+1 { + // the + 1 is because there will be an extra query that returns EOF + t.Errorf("wrong number of messages queried: expecting no more than %v, got %v", -expectedRequestQueries, h.reqMessagesCount-1) + } + } else { + if h.reqMessagesCount != expectedRequestQueries+1 { + // the + 1 is because there will be an extra query that returns EOF + t.Errorf("wrong number of messages queried: expecting %v, got %v", expectedRequestQueries, h.reqMessagesCount-1) + } + } + if len(h.respMessages) != expectedResponses { + t.Errorf("wrong number of messages received: expecting %v, got %v", expectedResponses, len(h.respMessages)) + } + + // also check headers and trailers came through as expected + v := h.respHeaders["some-fake-header-1"] + if len(v) != 1 || v[0] != "val1" { + t.Errorf("wrong request header for %q: %v", "some-fake-header-1", v) + } + v = h.respHeaders["some-fake-header-2"] + if len(v) != 1 || v[0] != "val2" { + t.Errorf("wrong request header for %q: %v", "some-fake-header-2", v) + } + v = h.respTrailers["some-fake-trailer-1"] + if len(v) != 1 || v[0] != "valA" { + t.Errorf("wrong request header for %q: %v", "some-fake-trailer-1", v) + } + v = h.respTrailers["some-fake-trailer-2"] + if len(v) != 1 || v[0] != "valB" { + t.Errorf("wrong request header for %q: %v", "some-fake-trailer-2", v) + } + + return len(h.respMessages) == expectedResponses +} + +func makeHeaders(code codes.Code, failLate ...bool) []string { + if len(failLate) > 1 { + panic("incorrect use of makeContext; should be at most one failLate flag") + } + + hdrs := append(make([]string, 0, 5), + fmt.Sprintf("%s: %s", grpcurl_testing.MetadataReplyHeaders, "some-fake-header-1: val1"), + fmt.Sprintf("%s: %s", grpcurl_testing.MetadataReplyHeaders, "some-fake-header-2: val2"), + fmt.Sprintf("%s: %s", grpcurl_testing.MetadataReplyTrailers, "some-fake-trailer-1: valA"), + fmt.Sprintf("%s: %s", grpcurl_testing.MetadataReplyTrailers, "some-fake-trailer-2: valB")) + if code != codes.OK { + if len(failLate) > 0 && failLate[0] { + hdrs = append(hdrs, fmt.Sprintf("%s: %d", grpcurl_testing.MetadataFailLate, code)) + } else { + hdrs = append(hdrs, fmt.Sprintf("%s: %d", grpcurl_testing.MetadataFailEarly, code)) + } + } + + return hdrs +} diff --git a/mk-test-files.sh b/mk-test-files.sh new file mode 100755 index 0000000..7237366 --- /dev/null +++ b/mk-test-files.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -e + +cd "$(dirname $0)" + +# Run this script to generate files used by tests. + +echo "Creating protoset..." +protoc ../../../google.golang.org/grpc/interop/grpc_testing/test.proto \ + -I../../../ --include_imports \ + --descriptor_set_out=testing/test.protoset + + +echo "Creating certs for TLS testing..." +if ! hash certstrap 2>/dev/null; then + # certstrap not found: try to install it + go get github.com/square/certstrap + go install github.com/square/certstrap +fi + +function cs() { + certstrap --depot-path testing/tls "$@" --passphrase "" +} + +rm -rf testing/tls + +# Create CA +cs init --years 10 --common-name ca + +# Create client cert +cs request-cert --common-name client +cs sign client --years 10 --CA ca + +# Create server cert +cs request-cert --common-name server --ip 127.0.0.1 --domain localhost +cs sign server --years 10 --CA ca + +# Create another server cert for error testing +cs request-cert --common-name other --ip 1.2.3.4 --domain foobar.com +cs sign other --years 10 --CA ca + +# Create another CA and client cert for more +# error testing +cs init --years 10 --common-name wrong-ca +cs request-cert --common-name wrong-client +cs sign wrong-client --years 10 --CA wrong-ca + +# Create expired cert +cs request-cert --common-name expired --ip 127.0.0.1 --domain localhost +cs sign expired --years 0 --CA ca diff --git a/testing/test.protoset b/testing/test.protoset new file mode 100644 index 0000000..63d7060 Binary files /dev/null and b/testing/test.protoset differ diff --git a/testing/test_server.go b/testing/test_server.go new file mode 100644 index 0000000..37a04d1 --- /dev/null +++ b/testing/test_server.go @@ -0,0 +1,235 @@ +package testing + +import ( + "io" + "strconv" + "time" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/fullstorydev/grpcurl" +) + +type TestServer struct{} + +// One empty request followed by one empty response. +func (TestServer) EmptyCall(ctx context.Context, req *grpc_testing.Empty) (*grpc_testing.Empty, error) { + headers, trailers, failEarly, failLate := processMetadata(ctx) + grpc.SetHeader(ctx, headers) + grpc.SetTrailer(ctx, trailers) + if failEarly != codes.OK { + return nil, status.Error(failEarly, "fail") + } + if failLate != codes.OK { + return nil, status.Error(failLate, "fail") + } + + return req, nil +} + +// One request followed by one response. +// The server returns the client payload as-is. +func (TestServer) UnaryCall(ctx context.Context, req *grpc_testing.SimpleRequest) (*grpc_testing.SimpleResponse, error) { + headers, trailers, failEarly, failLate := processMetadata(ctx) + grpc.SetHeader(ctx, headers) + grpc.SetTrailer(ctx, trailers) + if failEarly != codes.OK { + return nil, status.Error(failEarly, "fail") + } + if failLate != codes.OK { + return nil, status.Error(failLate, "fail") + } + + return &grpc_testing.SimpleResponse{ + Payload: req.Payload, + }, nil +} + +// One request followed by a sequence of responses (streamed download). +// The server returns the payload with client desired type and sizes. +func (TestServer) StreamingOutputCall(req *grpc_testing.StreamingOutputCallRequest, str grpc_testing.TestService_StreamingOutputCallServer) error { + headers, trailers, failEarly, failLate := processMetadata(str.Context()) + str.SetHeader(headers) + str.SetTrailer(trailers) + if failEarly != codes.OK { + return status.Error(failEarly, "fail") + } + + rsp := &grpc_testing.StreamingOutputCallResponse{Payload: &grpc_testing.Payload{}} + for _, param := range req.ResponseParameters { + if str.Context().Err() != nil { + return str.Context().Err() + } + delayMicros := int64(param.GetIntervalUs()) * int64(time.Microsecond) + if delayMicros > 0 { + time.Sleep(time.Duration(delayMicros)) + } + sz := int(param.GetSize()) + buf := make([]byte, sz) + for i := 0; i < sz; i++ { + buf[i] = byte(i) + } + rsp.Payload.Type = req.ResponseType + rsp.Payload.Body = buf + if err := str.Send(rsp); err != nil { + return err + } + } + + if failLate != codes.OK { + return status.Error(failLate, "fail") + } + return nil +} + +// A sequence of requests followed by one response (streamed upload). +// The server returns the aggregated size of client payload as the result. +func (TestServer) StreamingInputCall(str grpc_testing.TestService_StreamingInputCallServer) error { + headers, trailers, failEarly, failLate := processMetadata(str.Context()) + str.SetHeader(headers) + str.SetTrailer(trailers) + if failEarly != codes.OK { + return status.Error(failEarly, "fail") + } + + sz := 0 + for { + if str.Context().Err() != nil { + return str.Context().Err() + } + if req, err := str.Recv(); err != nil { + if err == io.EOF { + break + } + return err + } else { + sz += len(req.Payload.Body) + } + } + if err := str.SendAndClose(&grpc_testing.StreamingInputCallResponse{AggregatedPayloadSize: int32(sz)}); err != nil { + return err + } + + if failLate != codes.OK { + return status.Error(failLate, "fail") + } + return nil +} + +// A sequence of requests with each request served by the server immediately. +// As one request could lead to multiple responses, this interface +// demonstrates the idea of full duplexing. +func (TestServer) FullDuplexCall(str grpc_testing.TestService_FullDuplexCallServer) error { + headers, trailers, failEarly, failLate := processMetadata(str.Context()) + str.SetHeader(headers) + str.SetTrailer(trailers) + if failEarly != codes.OK { + return status.Error(failEarly, "fail") + } + + rsp := &grpc_testing.StreamingOutputCallResponse{Payload: &grpc_testing.Payload{}} + for { + if str.Context().Err() != nil { + return str.Context().Err() + } + req, err := str.Recv() + if err == io.EOF { + break + } else if err != nil { + return err + } + for _, param := range req.ResponseParameters { + sz := int(param.GetSize()) + buf := make([]byte, sz) + for i := 0; i < sz; i++ { + buf[i] = byte(i) + } + rsp.Payload.Type = req.ResponseType + rsp.Payload.Body = buf + if err := str.Send(rsp); err != nil { + return err + } + } + } + + if failLate != codes.OK { + return status.Error(failLate, "fail") + } + return nil +} + +// A sequence of requests followed by a sequence of responses. +// The server buffers all the client requests and then serves them in order. A +// stream of responses are returned to the client when the server starts with +// first request. +func (TestServer) HalfDuplexCall(str grpc_testing.TestService_HalfDuplexCallServer) error { + headers, trailers, failEarly, failLate := processMetadata(str.Context()) + str.SetHeader(headers) + str.SetTrailer(trailers) + if failEarly != codes.OK { + return status.Error(failEarly, "fail") + } + + var reqs []*grpc_testing.StreamingOutputCallRequest + for { + if str.Context().Err() != nil { + return str.Context().Err() + } + if req, err := str.Recv(); err != nil { + if err == io.EOF { + break + } + return err + } else { + reqs = append(reqs, req) + } + } + rsp := &grpc_testing.StreamingOutputCallResponse{} + for _, req := range reqs { + rsp.Payload = req.Payload + if err := str.Send(rsp); err != nil { + return err + } + } + + if failLate != codes.OK { + return status.Error(failLate, "fail") + } + return nil +} + +const ( + MetadataReplyHeaders = "reply-with-headers" + MetadataReplyTrailers = "reply-with-trailers" + MetadataFailEarly = "fail-early" + MetadataFailLate = "fail-late" +) + +func processMetadata(ctx context.Context) (metadata.MD, metadata.MD, codes.Code, codes.Code) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, nil, codes.OK, codes.OK + } + return grpcurl.MetadataFromHeaders(md[MetadataReplyHeaders]), + grpcurl.MetadataFromHeaders(md[MetadataReplyTrailers]), + toCode(md[MetadataFailEarly]), + toCode(md[MetadataFailLate]) +} + +func toCode(vals []string) codes.Code { + if len(vals) == 0 { + return codes.OK + } + i, err := strconv.Atoi(vals[len(vals)-1]) + if err != nil { + return codes.Code(i) + } + return codes.Code(i) +} + +var _ grpc_testing.TestServiceServer = TestServer{} diff --git a/testing/tls/ca.crl b/testing/tls/ca.crl new file mode 100644 index 0000000..ad06dc4 --- /dev/null +++ b/testing/tls/ca.crl @@ -0,0 +1,16 @@ +-----BEGIN X509 CRL----- +MIICfDBmAgEBMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAMTAmNhFw0xNzA4MjUx +NTQ1NTNaFw0yNzA4MjUxNTQ1NTNaMACgIzAhMB8GA1UdIwQYMBaAFM0FLuuYBwuA +J+tocRlu+xUuOw6FMA0GCSqGSIb3DQEBCwUAA4ICAQCcN8WJKbvGrunXgRBjSnsM +j/sejaX3CCZPmrXeditekSNMatO0JDXOjyoEvv7s9aZrAf3eFOU3Vr5N7PlbLRdj +tovuKTeVp3ungqMoT70cFEf/7eMlpWMB2GkfpV9LtF5Tb8dOYT3kllqtMKv4TeZo +2adu+GXdeQsqlz9fDEi0ZV4RBruuO0QyLWXpNrUB6fznUDfE4KVBsAIadjsg+Aew +6jeTkYuUILWMwBM6MzOG/InTKqXpe4ghMufI9fO+phxY10gz4QQ44ZNOa18OuiJw +IH8MoKzhrgUAPLs135hpdGbDePVw5SIKMHUAU2UEKtozAMVfCW45MZHREDdMV3NA +w5QWDoBYl4jol08Orbccmhu4fbauXmB5Id4IPVgGEGFPpiH/QVyJgZIv1AD2dlRg +Td26iz9I25hyrpEfF1gJMtOsDOklDsUiMo8ncQ3CL+pkKnMjhm54k6OFe0qlGsdO +KSavNlEmW/F9h/gs5kaLeFv0v4JxLh12TY28pCE60yoB/UkuB1a+VTHcP3Fa6uUC +uyv0T0f5yHujaM1isGjI3XGgVgLyJiFxtKMPsMEwRrsrEafqp7JCeLpnIWt1J0C/ +Zz3roGcCGj86Oq5zUjdguHS6Ra+uaX+IMJGohWq1cndzVzNfUNkRIyl8IdkCv9o3 +J8fVwBzN6sAu6zWd3BqT4A== +-----END X509 CRL----- diff --git a/testing/tls/ca.crt b/testing/tls/ca.crt new file mode 100644 index 0000000..91af38a --- /dev/null +++ b/testing/tls/ca.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE2jCCAsKgAwIBAgIBATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJjYTAe +Fw0xNzA4MjUxNTQ1NTJaFw0yNzA4MjUxNTQ1NTNaMA0xCzAJBgNVBAMTAmNhMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnjw7iZyn9EtjtK7zT+M59OxS +J43a3kMm11Vnh1Fw8oQ7tH0kQW6COyAwlBhAzWGtfDC6jG7A2n+8mPWoinsxoLA5 +viSZrEkWZ4tGxWWZ5y/xWh5NBrHa3Jsg1rZ5dstAl08gJPwl0v32aYkMdtk16K0k +jtdPpKtO98v1N6ea7fvQKdyrAaUHY/BY+onoqmSBGPX6vVV7FGybWDf72J5vcwyA +07wdaZ7TrUYbD28nhXuc4Dt2KbZbWvkT4OYZ+4c+eiehRFVGtuXXi0cKG0xF516b +DnrSO1/2ZbIB2xmcgKvcay0jLzWqhnsSc+qTqODVLODQAMvrMlY0RUXQPn/WTfAw +aQ+u/j7qIR1KFZcLn7uVq8bCM9g+VeyLjq+6XUvgGL9KhvrR/FA4R4DUka+ZVQqh +s262Qs7pNFdoIIrTsJyPd7/UYWCcQbkCKw0aRoUfBeZgkg4bylcABygZqY9+apRF +NBEhpycAEvWFarr6rosqII9kLm1LpnPNEgSvQ/CIRIHq5z5iKeSvHFYOVxLS44HH +M16Mry5UF/jW7Vg8JY5Jrg5YwyOhdGoOSJ3+c/pbLq1TRkwK9bwEJwGr7FdkrjPZ +uNJ7HQiFn2IaeLlbtzwJ+q3vGSFEjlujOigwUJVzXz16Q93vYIuK3FcyDuOMzpwW +HHgSLH/+rdtd+7hoLwMCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB +/wQIMAYBAf8CAQAwHQYDVR0OBBYEFM0FLuuYBwuAJ+tocRlu+xUuOw6FMA0GCSqG +SIb3DQEBCwUAA4ICAQBLNuP8ovTgFcYf7Ydgc+aDB2v+qMYVIqJMrnp8DuYy20yv +64jYcIxh35IbQWOZxZshJsebRKM9vr6huEo2c/SuHLQ5HZGPxSt++aG+iY4Y1zL5 +KHtG558lK4S5VsXymMkUjGZtm+ZuJida9ZcV+jz/kePMHpErWPeMvH2jDmD4mWgA +YdjipD4cxEn+9O3lBSCkeSjaAd5rQeD9XomV4a2/uL4Y7RDbn9BNt+jdLvfu2pmo +O1zcp0f578oFlUIg0H9fb6YzL3MKOXiuh7KE1/W9el5zsN/kLlyWFbopN34A6PlO +ZHEvZZcQW06bmy2FRWgqkqWMqBwzWk7JKGp+ozv8IBvimhgjNun068FQAZV9nfKU +6U728P6T1USDhgwtpX7/2IaukXcmO2FE9XzKZyYAbmAcOhPLzFO4pdwapU2lPbFE +l2HLkYaHLXzMxB30kQQHW2l8+8xr+MAa+bBcD9Jaxaz/t3ZpLt62/1nxT7SWNwH4 +Sa83BaG3EHBotlBc18hqrFWEKR4KYenqY8xa7kblDI0rXqlXBblUXp0TwIctOmzR +coqR8q6/R4VXhD9FZBIW1/uX2KKEPfTM46aQdaTtdzjd3UzwTP0SRwkvZ4oFftW6 +s1GljfCGsrOpi6O/Uy/IVTE7Xn/oVnlJvGbaP+AHexLytBiBVUBukLBwvpJ8bg== +-----END CERTIFICATE----- diff --git a/testing/tls/ca.key b/testing/tls/ca.key new file mode 100644 index 0000000..b8ab0f8 --- /dev/null +++ b/testing/tls/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +3/WnAoIBAQCTeK3ZRc03A4gPDd7wGbxbyF7o6+KiOUHKtK+OOeWnRI7UPEKulVd2 +KC5CbYDEJykC20MPxka9nNerTYHOKJ+tB1L5AXNelsxSpCw2aVRQbKb1KKvtQOJT +id2Wvc7DsL7+3DssxxWJlcJT1IGAmj7Z+IUIByOwLZLjTJ5xt859uh9Tib9pVQnR +k3Jdo6DVH9tmqM5dh8dNbmcZqz1CnNl08oU5b7PwmMII0MJ60VyJVU25f105p7Kk +EbOdn59Az+rjvSmbKcD+pmhvvaSARpuCubNMmj76wG3OVf9A3eE+IUVNe0cKfNu7 +g+QST2PK/YJoK0lJ+1tQojdATxwNHgO1AoIBAHjpZ9Px6L3Ek6O3ZxRfuDTkmoVB +APmvm4IcvajB0BPd1mdcf4sWYmGhNqf+xajwsKB8jIkd4LYfINjfIZOdnYgq0oQY +cM7K4+b8gkLKsIV2gFI4b95TYcbmanxdTDdbERGTJPsIBajXO5XapAswAJfllSDH +pUvLb2CUgLhMhR9SFZAjyRo0HV++jMqxWJKzhlTOkoPzBY5xAleft+hzVch3WuvP +zZn/NrpzTEpslV7dZ05Wuh8E+vJMoQNCReGlmAwNlrt/vxDuyv6ibNPxBHax82On +yo6EP59d7OE0951FruUIITUgzKG2jIqeR/e5Yb0LJusXnj4RPuvfRULFD00= +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/client.crt b/testing/tls/client.crt new file mode 100644 index 0000000..44b02b3 --- /dev/null +++ b/testing/tls/client.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEGjCCAgKgAwIBAgIRAPt0KCF12GYbCoUj7klj5/AwDQYJKoZIhvcNAQELBQAw +DTELMAkGA1UEAxMCY2EwHhcNMTcwODI1MTU0NTUzWhcNMjcwODI1MTU0NTUyWjAR +MQ8wDQYDVQQDEwZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC1JxEPOsZyf8883tlPBEajotyECtrYMZ48FsYEmQ1XvKPoH3eb7+Ev7tRBVAup +yB87XQ5PU/oNqAtpo/6WD5JGnKSVs+EAMESXmzEF04T9hK8uSd0cVEEkd0tbVNpX +bWMbivHnx5Vp8o2mIx0sVrgGsJW3t+cYbNTp3bOTdmz7LKbiQN2Ix0wH+2/sPXYa +cZsgbI0Ydo9KnqykPm2TqBYCL1kzhGlvaAotjdDIm7OgnaGCFe4CbK4QZB4uFw3e +M+PmLG0TsaH9CT/ZRrE21iBfg0rqgpKZKMcqYQftXdLqlikuV69F+0L84xRfeVqB +1E4j0RwBGWW8EwY4WHK3VE25AgMBAAGjcTBvMA4GA1UdDwEB/wQEAwIDuDAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFMs+/QF/ZJaRu8Wv +vcaMC7jGmPwxMB8GA1UdIwQYMBaAFM0FLuuYBwuAJ+tocRlu+xUuOw6FMA0GCSqG +SIb3DQEBCwUAA4ICAQB0gPDs86Rjy/O2+l8QyaYfwmmyTMPjNVqKgVP1uuikWErN +5hTAlwtDI9FuiMFBqeBdeiT8IQvzEEQPYu69kAX2XYBWBMWDa85co5fJztAzV7Yz +VL1byhxd2jgM14usyx6PbzkhYKBNesujHj7wQ0ur+85Kp66HqKCuNCvbj0zv58PH +RWkojRPgyTpbLdXXCOWJXp62XfddL1Bf7NJCW5QTyHoHoOsOeoPajb4OOmQehzqv +b9FPAHVFBPrU53Xn1CURAzTeBQ2T/OK4nx6EdQgxP9+VVurBQ9N2YBM9VEJmfQK8 +Lf5/+EJHe5ctOy1Xm4A3A52zZ1kGjfvWUtGJUSnJ5ahhMm6Dx63wk7oYNCTXnPup +aVtINWygNlS/dQsWubHaWSFwB9/QwK074+H/4EpDq9HCMMl8yPMktOmv69Hyaju3 +MvGshz/DLNZf9oYpO+lbU8X124Z6XifEztMiBlUPW75KYv9X4CTbKTdE45QaRMiO +ZXcH4HE1/iQ9IOGg7CplMlMcHg+lQ7CpXQjtUUjCEpkj8BAs8YLDodLnjigs56/8 +75+3cVZu0+dY+9eNt/EIqzjaFwEx72hbLyhk2IeS++7QloJDhYqlq+ng54Ul957A +8e7TsSVHlLZVGXw8yqjyxxOwWaFx6mvFzGrcBtvCgK2HwEiYQ9qXJ5VPkdo42w== +-----END CERTIFICATE----- diff --git a/testing/tls/client.csr b/testing/tls/client.csr new file mode 100644 index 0000000..bcb57ea --- /dev/null +++ b/testing/tls/client.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICVjCCAT4CAQAwETEPMA0GA1UEAxMGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtScRDzrGcn/PPN7ZTwRGo6LchAra2DGePBbGBJkNV7yj +6B93m+/hL+7UQVQLqcgfO10OT1P6DagLaaP+lg+SRpyklbPhADBEl5sxBdOE/YSv +LkndHFRBJHdLW1TaV21jG4rx58eVafKNpiMdLFa4BrCVt7fnGGzU6d2zk3Zs+yym +4kDdiMdMB/tv7D12GnGbIGyNGHaPSp6spD5tk6gWAi9ZM4Rpb2gKLY3QyJuzoJ2h +ghXuAmyuEGQeLhcN3jPj5ixtE7Gh/Qk/2UaxNtYgX4NK6oKSmSjHKmEH7V3S6pYp +LlevRftC/OMUX3lagdROI9EcARllvBMGOFhyt1RNuQIDAQABoAAwDQYJKoZIhvcN +AQELBQADggEBAFnxmVCuM3J2bt79JcFOqsXNsvGUUT+4kMl3BcfSWaf1pviuhiXT +fsKkk1WItvaRQvpNdQoFQDjKHGcd6+0vCz+Q6Nni2Vniz3+f3+h/rOzWGA656Xxm +lgByryixnngWZBNLZkLWCz/H1MAlQYu8PTdy0N+JBF/E5SAGfaaXtfTC6tjnnZIm +3rjxC7C3EyELpo3X3erTcHpnFvhl6ZSkViVWfhOjxU0n+TGGohczesbHZc8YC37y +JrkrnRDrNKnca1XkXWUnbV6rH8cVDnJ0Fvs54RI686Tlv+LxW2xa3D2+pV7Koduj +Ru+PguJ3BbaRpieGTxHg7hH/1T5HsZnD2E0= +-----END CERTIFICATE REQUEST----- diff --git a/testing/tls/client.key b/testing/tls/client.key new file mode 100644 index 0000000..2a945fb --- /dev/null +++ b/testing/tls/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtScRDzrGcn/PPN7ZTwRGo6LchAra2DGePBbGBJkNV7yj6B93 +m+/hL+7UQVQLqcgfO10OT1P6DagLaaP+lg+SRpyklbPhADBEl5sxBdOE/YSvLknd +HFRBJHdLW1TaV21jG4rx58eVafKNpiMdLFa4BrCVt7fnGGzU6d2zk3Zs+yym4kDd +iMdMB/tv7D12GnGbIGyNGHaPSp6spD5tk6gWAi9ZM4Rpb2gKLY3QyJuzoJ2hghXu +AmyuEGQeLhcN3jPj5ixtE7Gh/Qk/2UaxNtYgX4NK6oKSmSjHKmEH7V3S6pYpLlev +RftC/OMUX3lagdROI9EcARllvBMGOFhyt1RNuQIDAQABAoIBACE5jRNyADu33VaY +uNqZOiuBD1jYdNL6Jr92ndLyD1RsMNO+Eb3z/SVBdISW2ZzGK5RDuQArss0WaSFz +BpqXOIji6fzbBQV31NzJhfA/n0CwOUEQIxGzEk+R4axan8ExOuAuV7ffDzRjXD+A +aTVcolv3vz326Ne9/j72fp0pN0vJ0b8mk1xmDWNOHhfoWmIGrUZAjqAkA1kh5aLk +Q8MCjVyjT+KYDkFT6NscFVxKslDVhb2OFC7oy+9l/hBru12bsi9eBdYpPT9E1cpR +U9N8+9XS9d7wgVnmVh8CIrFToLsvSrwD8SG0Indot0C6dsy0PkoMUekVxvM5/wXm +YLZnZEECgYEA28JfZxFxO+bjd+zBC+yrusHCVfZK6MZZV0u/V82Bn+gftwgxagI7 +q2h7m56WBtq9MeLlhicZ+em4BtA3yHwVGhqr+d5CaXjkYype1EditqGx8JYRUwlG +9z7W6jCEDJsjrzvGgua1qsyCZFePG78i4rLumK7UVvWEK/OaiZu3Pr0CgYEA0wbT +3STBc4THLXR8nx39b6RP+qH8jO9UcD/V7Hi/SWTCcGB8IIlTV2EJVndKHPregcmI +dN61uH3d+3UtI/WxEPMcfrSlEwVrjF2m5szYjLIAeFynw7pQY95qIhgKi6OH0Yn6 +9OCmieL0x1ez5zOXiv+GVjmn9tDCxXvqfsW9CK0CgYEApOd0Y4kpKUQWyPT135bX +PqsKwyqwB4BfpiwHB0IE1ROASP5y5hOK5xLePmaAOeCGPBsBFOveiDQjjalNUroZ +s570EeoAd9jpuKggxLZUkqs/NUPG+EJr6DhVWSLS1ArOej4mti+dfu87oUQ69R02 +dlrCw/vdBuvxJHIGMuCQXxkCgYEAinSFVygBgQCSCkHObjuoB7LgAsp7QCDa3tcT +TZafstDYPhEf/9z6AG+bR872onL6wF7xF/Tzd7ulhJGJ73kJFtzbSkrNr+AzgyID +GpU2U4GKi24HaIT6r7vDGOF7Mck2mIWWUUqAGiH9hjkFwWD5QeqLQlGL4YVw9U9r +OIgWkfUCgYAoMub8wHJe9zhq7UCBCa4zPXqWVQcN7ANjL6fESvyK+A6TfwL5j782 +CNIVl8ewU9TthEY+AdbJeiAevz+pIazSqi0ln1JKO5YpCOC1Y0UxcEpghplBTlPU +yoQyTJP81iLynwOM4pC322ptJISXIndL7Ig/9AoRZtAJV4Ot6z9b1Q== +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/expired.crt b/testing/tls/expired.crt new file mode 100644 index 0000000..04edc5a --- /dev/null +++ b/testing/tls/expired.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAiCgAwIBAgIQP6vqM4GEKFdwrZvr/s/5KTANBgkqhkiG9w0BAQsFADAN +MQswCQYDVQQDEwJjYTAeFw0xNzA4MjUxNTQ1NThaFw0xNzA4MjUxNTQ1NThaMBIx +EDAOBgNVBAMTB2V4cGlyZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDDlGu08Ye7IGMnh6zyiVbW64btT2BUfj4fZlyTraLNzVPfULOXezgJRAwAXEnY +eYA5j6e7iAO/yGXq+Pxh9rbGlXTVC2BsPIfKzTOcCTxgO4tfrdfSWFRDGgcFtpAA +/4b8fYQ8BMw7khVpOqj9n8TyqsN8DGyZTyNhQuc23zmmY1eVYtDIwVvVBp8/YzDT +3RzO+FyoRpKgdngX6kdqqZ+vaPxP078JM5zAMnysjSkpQCdAdu4EupvA5xRtOrDt +jJoAi4iUf4sG20RiF9XFpiDkyUiOa0ysFC4wrWbXkaT1xHZJJPwwJZ8VyGDRFlFa +POEaM9dtLPKtEV2af3G9Q9s/AgMBAAGjgY4wgYswDgYDVR0PAQH/BAQDAgO4MB0G +A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUNX8nKbJ+G0bv +k88bTHkULUX1U5AwHwYDVR0jBBgwFoAUzQUu65gHC4An62hxGW77FS47DoUwGgYD +VR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQBHZsxC +GGOUhNi1b0xUYmKwFaRP641R69NydzVMma+2rpLZtL8MQq7lWAgWGE8EiKfTewk8 +2Gm+rNwReBRkw56zka8WtLomO4GKKJHvfRsbSempsp9MShc3vcsEtzXGjDQi9c4Y +BJjWbh666jCKPRJ09RewcTyTYPtZ96lVeYRj9HhXF1EGnURG5sM/zCantlZXxInk +1FIOKDuawMbgf3GR6n/bM2oybYCs4Skv3yOp8x3lyhlJ/zmSPGVVkPa7Vki5+sPh +/eIZJ9mzEXsu7IXfg1isZ01iB28+6UgpZt/3017PvgopiYrL0gMKO3EL8UqJDweN +GzJh5VYOqsjTbsrYYsWGqM66vJmfPvqDyYA1jj/EH+q6TBz0s99rzF44bBKKie6T +0KrZT7ohXQ18Vhl2UpqahMqxMeAW/QP5asGuzS5EalUir5mNcOljtq1NnfmYvqok +aDC7rURoANwZ2L1oKN0oB6jn36g791pFdKycw+HdsrGgQGPOMak9P32z5kmDsODH +6aVrfio2WwSGg+1CIBH0QsclHAUgLsAXjyGbRWxPsvMcsLB6OOymuTP2UfriT05X +duabvbEP5IIRehVUfrP5uvoo29xnoPL4UB0C8gwr21IDn7Zew5/ALekN+s6IgsfL +9yKTGSD+6Ir3NqBgL8T+uhOAekyLE5S4CcwCHw== +-----END CERTIFICATE----- diff --git a/testing/tls/expired.csr b/testing/tls/expired.csr new file mode 100644 index 0000000..83d2e9d --- /dev/null +++ b/testing/tls/expired.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIChDCCAWwCAQAwEjEQMA4GA1UEAxMHZXhwaXJlZDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMOUa7Txh7sgYyeHrPKJVtbrhu1PYFR+Ph9mXJOtos3N +U99Qs5d7OAlEDABcSdh5gDmPp7uIA7/IZer4/GH2tsaVdNULYGw8h8rNM5wJPGA7 +i1+t19JYVEMaBwW2kAD/hvx9hDwEzDuSFWk6qP2fxPKqw3wMbJlPI2FC5zbfOaZj +V5Vi0MjBW9UGnz9jMNPdHM74XKhGkqB2eBfqR2qpn69o/E/TvwkznMAyfKyNKSlA +J0B27gS6m8DnFG06sO2MmgCLiJR/iwbbRGIX1cWmIOTJSI5rTKwULjCtZteRpPXE +dkkk/DAlnxXIYNEWUVo84Roz120s8q0RXZp/cb1D2z8CAwEAAaAtMCsGCSqGSIb3 +DQEJDjEeMBwwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEB +CwUAA4IBAQA0GTIB6PxgmHBa234rSYqIew4qRfY9MeUkVQEFRDwodqxa+LWvZx2T +5JmTZYyXBfQwnSye18fDjQuHv1KaI7bnJuMRv9KU8L6ynLkAqrFWRSBjt3eCum01 +IWZFyWu+dUN2c12C79zUQh8uZc15oDNFrD8ivBbGRpWvR1CSG/DH52kJ8nckgEsh +SwxbzSPOXBgLH6ke5z9QGHJMK2rhRFutFOecAId7VBiWqfZJv15+P2ZcyJNQGThs +V2sT974YGFkc0Y1MWlCgi3XhyQzIzqV1tEILGSTDE8biuKlm1nX3H0K8oI3hoGcM +CBjE3HQ1/rs10IY/WvkpIfAU71D/ExMc +-----END CERTIFICATE REQUEST----- diff --git a/testing/tls/expired.key b/testing/tls/expired.key new file mode 100644 index 0000000..b3b8140 --- /dev/null +++ b/testing/tls/expired.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAw5RrtPGHuyBjJ4es8olW1uuG7U9gVH4+H2Zck62izc1T31Cz +l3s4CUQMAFxJ2HmAOY+nu4gDv8hl6vj8Yfa2xpV01QtgbDyHys0znAk8YDuLX63X +0lhUQxoHBbaQAP+G/H2EPATMO5IVaTqo/Z/E8qrDfAxsmU8jYULnNt85pmNXlWLQ +yMFb1QafP2Mw090czvhcqEaSoHZ4F+pHaqmfr2j8T9O/CTOcwDJ8rI0pKUAnQHbu +BLqbwOcUbTqw7YyaAIuIlH+LBttEYhfVxaYg5MlIjmtMrBQuMK1m15Gk9cR2SST8 +MCWfFchg0RZRWjzhGjPXbSzyrRFdmn9xvUPbPwIDAQABAoIBADa1IZuvpCP330SD +cyE0wZHEuC1RcsSvu3jVDThR7aRbtwZUcKgC053j5ueC6TUgZ3mycVzHoyTWTYv4 +scBFXsMVs2SUlhgwpltYIwOWocjZXxcYbbJs+sT6VtSGSKm+0Gd4RLD1NpvDNTIG +MpcfRdwLYDsmzonj1SWzrTFwJ5Qe33cHSR8Oi9OarrxzIHWLDNgp8x+5j2l0T3AG +RPQMXj7jaK6qEdHGD6geg+ButeyjYyxu64l8Ooax11/jfdYHcK+BmmtmP3Bd/6FQ +DCtrXTBv5Txl/T6D/6OsyabSlwGWFDNEAaS47hKCFiYJBR+Az6r7d1QFBIzuqF0+ +T7EwaKECgYEA/tUMcTXnBRquAtQHW4cSvHH2qoZAhGjXCmj/pk4UCOZDT1DP/K5u +m7tOZB4RJBmzg0a1QS89aJ6lhY0Oy1Y9MZTM6ofkBKd8+P0Rgo04UDVeQ8sA9c8x +4bFOrOEqe7NoK7Mwxyn5NOmNi/wxy+tpiMH4Jt3y4TLDEZPg0Tsfu90CgYEAxHnc +fN3gKeY3SV6nHi+johBirSNazhr4n0fx/TCsy02e5Rrn0rksGVfPij5Y2g8HQLel +hdbG7tVyA7UtGgVjwiT+4j0VXmWpCIqfPsibRoy29oPqO4p5pULsz+ueb4biRW2Q +tdLqS4OldM8YUFwkS7k3Pp13SAY+Ir9rHL6wv8sCgYEA/AZaYtCrZMnpFNT7XdLt +fb+78xQJVKqXGi2TwLbxa4fHQ/cpa75bl9scAToXO7vLZOaWNhxxQDm+e6Fw4zqs +FJAURVMV+GBo4ZrvKU1fRzwwuR1ZGsHKlGoV5DZgHKznNmjmseJaG7FsEujdms58 +tgsXz+Cr53qbn5O/wU4W6WUCgYBA16sB9sPtcBIc/8UNvFE3wkqes4VbciFNiBQA +KJlOe26OVCPgMsawEn/nMw5l4QHWxQU2t5xt5Dm9qYSaCt9Sip0oE1rDDbAMpptJ +wDEmxnf3wa+DOP9OoFjBghSG4DA7E57nsxUqGOd5NoPiuZYs+5KU8qkUNyM4mo4C +LZjtowKBgCWuDPr8MBL1S3ym55VTNUWcuMKUHZkMg0HjRpi8ABXeARoM5FAWykbk +hFnI/Waj6EHNGoVVpKttJldP+uRRA+S3wZYKH4gRQcsWI/BVdiBkZgoTG7cwnixs +CqMQ4p6In1Q/6EafPqkVQOY1abNKQ7ZGbhksB/fVbf37XfOnDj3H +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/other.crt b/testing/tls/other.crt new file mode 100644 index 0000000..695af5e --- /dev/null +++ b/testing/tls/other.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAiCgAwIBAgIRAMcIOg3oRf7n7mR1BUqNnlcwDQYJKoZIhvcNAQELBQAw +DTELMAkGA1UEAxMCY2EwHhcNMTcwODI1MTU0NTUzWhcNMjcwODI1MTU0NTUyWjAQ +MQ4wDAYDVQQDEwVvdGhlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AM4lPXHK6YV7GFMeSsHKVq33LDn8Zt0IkY5pGNCuuksPa3vYBFRkwTrrobKQDnmw +jLeaQISyxyelhT4aIU34OQpGjOmCzceYPG0uOcecq7noVvbgw0UeQHkEL0p4NOlb +9LMNVhKTgQUhZC5mYSIpXNxlE/LlGmqrFX/8peSaJ1oAkkB5FYN0gAbYhd8kpJX3 +9Nr2A+f88oicSX5K0L73LUFUrxoTcrFP1pWnFgg28vLvvzrW/VZtq4qJPl3GTLbM +xHOu000LaHK8TgIJMCQUalh2q2nz+Htmtv+g/b27YEMGcDBW1qBX9oMKgoM/Z1Hv +zjVhAm9I649heDUguFXCaoMCAwEAAaOBjzCBjDAOBgNVHQ8BAf8EBAMCA7gwHQYD +VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSUR1DE/STLJwf2 +hW4qXPrOSLqlmDAfBgNVHSMEGDAWgBTNBS7rmAcLgCfraHEZbvsVLjsOhTAbBgNV +HREEFDASggpmb29iYXIuY29thwQBAgMEMA0GCSqGSIb3DQEBCwUAA4ICAQBG87BU +UuDuUCnvcwUNbga8fhe1PR6z2jueKQiI10SxkYG/g6PMQGGYDNO9DZFKu9l/TMTf +LnuEozN+Csig+wC+sc32/MdR35XTmkKtNhL0cVgvKP0Q6zNk97/QJErLtDpYb9VR +Gm2Ky6FGDp+/EEUvQUKRpGBmWIqOtjxqQu8lLoJlt/TPhxJ0lGDd3c8WwaVFYTbS +isBKdHpS2hkn/O1Yd4QtNk06pCpUDQuPumUOBoa+dK3y0jZ+e34h1NoR5EZvfRJy +p3n7CLD2eZSNc4oWKb67X0RDao5LD0b51crjgsFYHhCTS+Mgh0YkgukQZ8uBKpUJ +IBhz2Nr1QXykrJUhal9MrKukjczEikGxzK1VsDgxYY1kLBURhM9/TfvICmcAaQqv +MF9B78lnoJiPZZxD+a5N9MawzN6QBqX8GpvhZoAnj6iAuNwKJVyENpZqravxeq2o +buNjgQ+SmfqxQDfMD3lu95yidqD7bcDipJsXEPQzdBjZ1JOJCGi2eiAm50e6bq94 +CMKmmRjtIbF1hJnHeEFPvXqdPpqcyEvcaDebph/f+54wubTgwFI3VMnhhlv2EPIe +rwcbZV3kNpUZWXAZVzYlQcbK+9US8PocOUzmqzA7ZZRO+rCWNxahHjdgrMK3fG6r +WudSHHXawj3dkPeWrQde6SILSK/myhGLdjg1fg== +-----END CERTIFICATE----- diff --git a/testing/tls/other.csr b/testing/tls/other.csr new file mode 100644 index 0000000..6d80774 --- /dev/null +++ b/testing/tls/other.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICgzCCAWsCAQAwEDEOMAwGA1UEAxMFb3RoZXIwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDOJT1xyumFexhTHkrBylat9yw5/GbdCJGOaRjQrrpLD2t7 +2ARUZME666GykA55sIy3mkCEsscnpYU+GiFN+DkKRozpgs3HmDxtLjnHnKu56Fb2 +4MNFHkB5BC9KeDTpW/SzDVYSk4EFIWQuZmEiKVzcZRPy5RpqqxV//KXkmidaAJJA +eRWDdIAG2IXfJKSV9/Ta9gPn/PKInEl+StC+9y1BVK8aE3KxT9aVpxYINvLy7786 +1v1WbauKiT5dxky2zMRzrtNNC2hyvE4CCTAkFGpYdqtp8/h7Zrb/oP29u2BDBnAw +VtagV/aDCoKDP2dR7841YQJvSOuPYXg1ILhVwmqDAgMBAAGgLjAsBgkqhkiG9w0B +CQ4xHzAdMBsGA1UdEQQUMBKCCmZvb2Jhci5jb22HBAECAwQwDQYJKoZIhvcNAQEL +BQADggEBABuaL2t2Zcv9R72OH93EpIzgExL37odLUiIjTIIykK2TT/gb1LtnE1WK +THdqaLpnPot9IqBofppfkXPMrG7vavJoPlAp0lU2FHYIz64PHou8lj9yiXezDKDn +Jia3TOxCu5VTRnYT7Ypt8kSull/jlyBQgTP+P0YXwoYAXJteQr9O6yD75yWOAx3A +f/oQS77xoe0jdU4RkEMQRQQUIiaNyH8Bx4CeETmPoJDzEiIvnC5xDoySks1VJK7b +w11IANF7zO5UWtYv/i3+Wh5XLMJ0GIIVTpuGkeVaZCjB8goiBJXoaHOKsO5ygJo5 +N+nxwiDTwUIZM+dU88mtCh0dvJeMMfA= +-----END CERTIFICATE REQUEST----- diff --git a/testing/tls/other.key b/testing/tls/other.key new file mode 100644 index 0000000..c392020 --- /dev/null +++ b/testing/tls/other.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAziU9ccrphXsYUx5KwcpWrfcsOfxm3QiRjmkY0K66Sw9re9gE +VGTBOuuhspAOebCMt5pAhLLHJ6WFPhohTfg5CkaM6YLNx5g8bS45x5yruehW9uDD +RR5AeQQvSng06Vv0sw1WEpOBBSFkLmZhIilc3GUT8uUaaqsVf/yl5JonWgCSQHkV +g3SABtiF3ySklff02vYD5/zyiJxJfkrQvvctQVSvGhNysU/WlacWCDby8u+/Otb9 +Vm2riok+XcZMtszEc67TTQtocrxOAgkwJBRqWHarafP4e2a2/6D9vbtgQwZwMFbW +oFf2gwqCgz9nUe/ONWECb0jrj2F4NSC4VcJqgwIDAQABAoIBAQCmygix5gwU/KiM +r6iqrOx+6sq0y9vqIIGsaKo0RfriukIrvHacVbzl0DpPADFGEit4beyfsQpjsI9i +1L93l0uHXderIzMdt7XEXK9RKxjiXPLn4qj7ZmOhxloA9ctRuB3/NN4cP44XOZIV +3K3gdvj0NS/zyZwbC/tkR2Vt1a/bJ8DFfaFrSdk/btpVY2BH/uWjMls1BYIs3tEk +nroJYb+fyliC+n/QXrLQAPTVLfI3jyVjRYpW5b6mZHQk4SGDde1XbtqRCp/f6PLu +H8FQumd+SfrjgTwefphSWCW2H/aMNsZpL9NK/hmglKg3OcGKhwQswdmL9rDVFQmy +AqxXwxk5AoGBAPPmijvwg6bKwqeSCdhoYjQVNXdTbdda3arjC/G/vxhsVntXluOX +wGF2jInAu+sAShBFdhG4JlL+itlpA/aDDIMUxsF+YnhTOX5pofL8Nr4sstY6wjBf +4jVHQKLnaOm/mVpHqWABVv/M085XK7HHZR9e9y32ry1geRSiPF/E5I03AoGBANhf +OPL5WWO7TOchWci6qRcv0kZ47iSE1JxSGnXr+KIIBPIAgrwhYkqgrl6noSe3x1qB +tP7ZvmFWewxmo3mN2OwPTsxAhnjQK4D1PGJcsvf2A3f1uiwG+emqWpcMshTQnjda +Hi2krfMaHwErE+dEbZiilLDRYAMAWlQodXnhllMVAoGAKdDIumYN7DavENO05Glh +DNTmCcM//cASaQ3sKlJZjPJmEVd/Ax4tWYhdp/BnR28RQ6DlETylNW12mLesekMV +jhOtz9a/QynhnY62uVYMfKZlMt14FZsayU+iAUvzbL/wps3KeC9CnzCaz7GaSCyL +Zcl+T18PwZPcrnDyMOks1hkCgYEArrQUE3tpxbER4v12tTCiHuqp6eTyw+HMmXth +ih1B3/KBq7Tl2mlKJ9+daygGYz9sY5OfRLcjlQxyxgyJqjfyEog5o4nmCd5rgfCB +FRqsFrI5Er8B11K6rwSxqIzDrTLUzPSisU/qdAN/TT4vD+icZUXAsRQdZc7/IDya +vhJ7ghECgYEAiZ6v380l2jcEk+tm4UYZ+nXq3c2wq8mZkQcEDVpqvDGL8k/dPEq9 +xOy7PVooQHQJyC2bgTbEKs6JYvzAYQmohiq7L3y9WxDQFJeImtzuNi0s/dwP+mBh +R6htM5JwAO3JnE9V863M6kvtLEIk7XptI5gC0kN3Thi70yT3lnU+emk= +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/server.crt b/testing/tls/server.crt new file mode 100644 index 0000000..0b62372 --- /dev/null +++ b/testing/tls/server.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENzCCAh+gAwIBAgIQSCdvhIY3KZ9w9YRcDH1oRzANBgkqhkiG9w0BAQsFADAN +MQswCQYDVQQDEwJjYTAeFw0xNzA4MjUxNTQ1NTNaFw0yNzA4MjUxNTQ1NTJaMBEx +DzANBgNVBAMTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANlVMhHZHjRWiGqmpOse1KZTmdUaiSgl3T88+Mie17UbiLmsOfnkd3PuEnKlXRXM +sqOg15k9xfnV6SQebpBfkcwqJVH/USjWY4C1e2vDrva+j95L+uzDMZDF9nxpnjHE +uHT9+hnrmB3Xted0tzxRC/77Cht8Kn4gaoljbBoZsRnv0vRRUYKA2OJHJRRCHhzZ +AN6A+lWbWyvyvd3UeOLl3oFhk4lS5fwYY6RY8W9ZTJVxetVycvro42kK2jtpqUeF +NercfjOg8VBWYjqB3Ey1wHDpjS407TW7RWEGA+3mP8ZFsuoYr7Rs+z8LprbYEPpF +ojgvss+vMvjGkrcU8v0MR/sCAwEAAaOBjjCBizAOBgNVHQ8BAf8EBAMCA7gwHQYD +VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRS8x8oOcBJnrrW +jiS0t3qjKee63TAfBgNVHSMEGDAWgBTNBS7rmAcLgCfraHEZbvsVLjsOhTAaBgNV +HREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAI9t4Pvn +o0cW0SrC4EFNJqUaffbqUNd3i+dBn5/EQc8EGYVY3k8E8iUMHzRDyH+/VRp3RUOH +oJDDS2uyeJ2InkC193MOpNJDQV5qf3yOmQCeVmmjcXkg1Nc+3oe8ttWiVW0ArFXS +oud6V7/6qzIz3850ypi0Zz4KLVhSGzaI8dnzPopqNEG+ZbgIwvQLqWhv2bLpqycY +5ANECpjH6QIEp8JNjga9Sp/LspNCqMDmVswBGarySNWZ1+uflg9X30hsdCzVPgX3 +KMy0wVelT/4y893BCTo2KendGh70jaGxm8nBH7OXkeki2TI6boAvsn2Iash0HSZ4 +hPacZ4QWFEYW4jZeu3ZNTJ9tc2u2jgpAGueOcWRY75CraLid1V5t1kpGxAtX4NvX +X56e/IlmEI6qPsaoPouQ1riAWMdRQUT1FLNPanv4vElDYXBNcFl7knuS14JDCC0M +K6ttSb2MxJYfC6J+OJpHQd2GWU5aO2uZcgi9jRMslNwR+R94bxI+q/bjrI31JsDz +1pVRnGRWH7cDejA2f+q7X8/uRuA8bfnqBcu0uI9YR64W0VMMLe41+iR0wt5N3yWr +/DalWIvmUvE8LoaGDwxV9T3xq1I1dWnASX+Xmb1SQ0CnhEEooZfQYfb3ffciG6mU +UvVC0YW1cjOgb193W/N+Dgju7/a/e+XgbsgT +-----END CERTIFICATE----- diff --git a/testing/tls/server.csr b/testing/tls/server.csr new file mode 100644 index 0000000..34d0a11 --- /dev/null +++ b/testing/tls/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICgzCCAWsCAQAwETEPMA0GA1UEAxMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA2VUyEdkeNFaIaqak6x7UplOZ1RqJKCXdPzz4yJ7XtRuI +uaw5+eR3c+4ScqVdFcyyo6DXmT3F+dXpJB5ukF+RzColUf9RKNZjgLV7a8Ou9r6P +3kv67MMxkMX2fGmeMcS4dP36GeuYHde153S3PFEL/vsKG3wqfiBqiWNsGhmxGe/S +9FFRgoDY4kclFEIeHNkA3oD6VZtbK/K93dR44uXegWGTiVLl/BhjpFjxb1lMlXF6 +1XJy+ujjaQraO2mpR4U16tx+M6DxUFZiOoHcTLXAcOmNLjTtNbtFYQYD7eY/xkWy +6hivtGz7PwumttgQ+kWiOC+yz68y+MaStxTy/QxH+wIDAQABoC0wKwYJKoZIhvcN +AQkOMR4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEL +BQADggEBAA76r3SIQUER3XyGp4MOrfKGwBE7RnALcxW1XkrhID2bng2hzovrZZNO +xutL1zqPFCxClIKUxYAXpMeY8lnS6H8I6FoM6ALCZbK7q9rmMK198LPMo3zC6TsO +rEP8HOF9wxaYubg8xaq8iDlaL4e418M0UPOlE75PtkDAjhY++7ZTsjPVr/9WsJpZ +MmEZ5kSS59PZbMbyqXn5MxE0iSD0LfM+lmkIBwSvD8rjq3SQ5NKCg6CJkRRq7BVe +bujA2pPb6ivS5pujjIxkdUoz6S0G+ewZG16kbBoygWuRVFD8xqR9Pa41KSPhpx85 +1qSrqR4zHvS3r+RS9UVIXnbh9ejW6Vg= +-----END CERTIFICATE REQUEST----- diff --git a/testing/tls/server.key b/testing/tls/server.key new file mode 100644 index 0000000..d39bb2f --- /dev/null +++ b/testing/tls/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2VUyEdkeNFaIaqak6x7UplOZ1RqJKCXdPzz4yJ7XtRuIuaw5 ++eR3c+4ScqVdFcyyo6DXmT3F+dXpJB5ukF+RzColUf9RKNZjgLV7a8Ou9r6P3kv6 +7MMxkMX2fGmeMcS4dP36GeuYHde153S3PFEL/vsKG3wqfiBqiWNsGhmxGe/S9FFR +goDY4kclFEIeHNkA3oD6VZtbK/K93dR44uXegWGTiVLl/BhjpFjxb1lMlXF61XJy ++ujjaQraO2mpR4U16tx+M6DxUFZiOoHcTLXAcOmNLjTtNbtFYQYD7eY/xkWy6hiv +tGz7PwumttgQ+kWiOC+yz68y+MaStxTy/QxH+wIDAQABAoIBAEzhDVAw/LVI8wK/ +JlGh21lm82Dl/SS9mDE5kUvunKGNNuVvXibewb65tb7mbjI68epeCEZGCtVg7RMA +zN23YOzW79K8vWnzxMkP6bPqSecw69WYDRBZ0BvFW3cRKYuzagjAmws2Qt4zoz5Y +FEV66gJtrVqhpqptLyKgj+n/sp1YiHMjkcJF5PGnAPudYxpiDHiPFked7vZtigyq +eE2GCVbaOmRGPMe28JzRmOuFqEN9GccRjlq+AuzYe14lWKa6fZLVke78luByF//M +RA+gGoKfHq859wACOEGbqShMWxC++y1HJ2Mu4adUlikzVq8rboinaAxzv56kc1n6 +EDc/qPECgYEA2x+qxHKSJnQuItZp0CpsDk51yr1fJcWtdEbcgmomiwMwl/Nk0OAB +rplrW5gezyVRfMDsoqAnmBS301JEL/7QuEWvedTpptKC503Z+mCINPTZAaJfa7Jb +KUCSQHO3hThfOXFMkb9mcJEVVNqtobnK61tC+JNOSd00Z9TdLQAbcokCgYEA/ehf +Vq3md8bkQFlnMtFib8SIr8IP5j4JecFSeqa/OnBFfHJr0trUhoMO04uaQOl6lTT7 +9Ca36WfJsv3EGkMHEFeceOsPswf65bI/qgxgUS7Qmu+NZTjvXL0gCpeHLPzEaLTV +CDXoo+YAJzzv9yWwVEvIIru5SJPVud6Gap5L1WMCgYAjfO91PXD6FVrbfYpJknVJ +o99j5GOihG9hI5DW9kYjwXJ/SYYMZhsfoe1HOk3TEqIt6Djq5bFD6icTbIFqnIRF +M9QFkTv+Lp3QxEUHTdcBbJ4wq5F0qcAl4DVPhu4z/zs83GKgQDVhCb5AreHtDWAV +2gPwqjrFr7OrFUh0302SsQKBgQCOHKZn9HNfLOIKJj/9kHYhCoZaoSqW+rgA/rQ0 +U+oKQlaR/dTdsn9rPiVpP+S5WjSzGHHAyH79U4rv9Nryu/tTKUY5447o7JmAQJEj +k0PBjItTfKrOMdy/MlehtggBpQQlerkVnF62hYAmdhP1Z5HWzIea8SkWNzBTlPn0 +6N6W8wKBgQCDBPXqGii/ur5IBQ+RASIL78RgIGH49XYcq7IrYFOuztGBbqf4qorR +BTl/mWFFHr+fAcLHlC/qTSBBflGClElqcg+j+92RAI6HbCFemdSbnsv5FCmciL6e +09x+oprKq3/WAVARUBif3RtDq92SFwSBfrx8JFhGIa9kUcsDFSBaDQ== +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/wrong-ca.crl b/testing/tls/wrong-ca.crl new file mode 100644 index 0000000..54286ae --- /dev/null +++ b/testing/tls/wrong-ca.crl @@ -0,0 +1,16 @@ +-----BEGIN X509 CRL----- +MIICgjBsAgEBMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCHdyb25nLWNhFw0x +NzA4MjUxNTQ1NTdaFw0yNzA4MjUxNTQ1NTdaMACgIzAhMB8GA1UdIwQYMBaAFGM6 +559SRpX6VJlKbIObtrLXvc1rMA0GCSqGSIb3DQEBCwUAA4ICAQA+6zsHCq9YRZ+a +fwsZmbGQqDUBVp5TWtDsy+qvKf/084CgTn0sR28HKEONQvX+R1CyzAaCGrkm081k +yDUizdyGrVR8zmCc7O3ztPobfZBmQXbR0pcxwweFiELBO1exEQ5IpM4J0KOPc+CB +AwVA227Q4oKrKyUtNQ9d3qr0/2E2HE8W0aV7Ax38MvXsUfafWk0SNPDusFiYNsTt +v50Gmd2yBlaMzT9Dsze9wuoTvT42lpCby/NSSDYynG9Cra2y0VoIpVxvPqVzn77L +1otkaQRatbfktaa1WVufEK81FXEyeYdM/T4bKCbB5oBKxmwwiS8ukvunc9gdrosx +/7QIlpr3iBEu+X+GeOdGyPA+a+S566Hil38QCyMUX4fI7xjYt3ek2MI1YeNEv576 +CwEihc7NvPh5MI0OQq0nIjTcIeEGQag0eAaGwmJ+AmrZ+4bop5QavuwHVh/7sNel +rNhFucmx+gEPeS/Ae0cp2BeXjEXHmdfwKDCT1n1Rdd5wfLeFuKlBG8NdZKwZ7HH7 +vwi+JwIBx/WJ/f1qcPlWAyF4Y/HUJIRRNSXJOUQCWhCvfGtLQ4xs7uo4ViE8CtyO +RE/3xHrX9UwJUymw50Efj3PpOxWPvJ9B7A+8ED1kJR29HQz5gVZhEcW6nDTntwDQ +5nrvhzi93xzaTQIhEe53uz+rTGs/fw== +-----END X509 CRL----- diff --git a/testing/tls/wrong-ca.crt b/testing/tls/wrong-ca.crt new file mode 100644 index 0000000..1a0fecc --- /dev/null +++ b/testing/tls/wrong-ca.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwh3cm9u +Zy1jYTAeFw0xNzA4MjUxNTQ1NTRaFw0yNzA4MjUxNTQ1NTdaMBMxETAPBgNVBAMT +CHdyb25nLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzs91DHKf +enUfIPigN6n4CsTSHGMkRpWQw9T5vIqUd3ZgMJLCoEnj0OpVlFVcb9+P2/f5RYg8 +H0dmM1iHGi5uCoDnJk7K5zaLMgxGtRy8dwZ1nHri3sZwM6Z9AyvktuSVbuElzp08 +utwB+fstR2METN5Dovp738rdx2q3zYYYcfnEXcYvxBdxVeitFgrXauRDxuqMio+5 +5bHVUpIWlpu8Fd3CqnMUS4N6McijIn6T2wiyALJDf9xE7edAYl4ExYVKaOyR3Ff3 ++BhiS+IEPd9AoctU9JYFpDavfaiZz77AukwwfU+W93NTTFQ+rf/ev8XzsgkFyZPw +CCfKuet+o2/8MIxv4nwKxv6GGMFbQz1gNw3RqG5m19zppqVzp1vgMcNXSeRPFlQI +fXYFN9BY9bvx2L2ZpTn3gsdgzDGNzYU5fGro3YzmtelNBCY3sAz/riG6+wMDHCId +5K5NxrJBW3tTvEZQyKVZA1W22/F/Wz2LxA+4ZLhUoUuXTkJxLS75EWLkK2xK+IXv +h4s8n8CwfhFV/De7u18Pho0XKTm2IPir1nL0WNhjy4jvBYDN/Jy4fE4QALt4oH9t ++GITkDo0Zd/USZSkAOXgEb+Ks5F/fztI9yVp7/nhhj1KnSIrkObBaltlfEOe4vgz +3dNFAG5kH5lyG90uffKR7h1Vo7UkYcpJ3IMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGM6559SRpX6VJlKbIOb +trLXvc1rMA0GCSqGSIb3DQEBCwUAA4ICAQCIm4pRbel8V/W1OKSzJXg3F76ORIjz +zN4mAtCX8YtFQwawBlES25Ju2IQ9XfvqM0CPO5LEe1v8ZTXeke9Vjf/XGReBCCqy +/STzLSBHflQqvybMYH87K5h5e91Ow9T2HjyPtzS3RdyaahU/Y8/EnOTG89uJlpN3 +0k0/KXfwVKAyjrOaoTeGPM9BjDssNq2S07h5C8sCby3MpR26CIMGbFnotwTjmSww +qkDSVd63/ZIB5/dOcOlBd1+rE3LOzYxDiZtKWu0NM+o7N0m4Y+gD4siyxRWuKslz +cTwiwnLmzZG5BUvRT2FmzCwejp45+LjrXmUZ8hCznk68hnkilx9XLdkBL/1qyk40 +I1IUFQtkkcyznwUyKpC0z4VJZAVL8xi6KO60TOYtidxFTJxkPrWcAHvgzItao3XZ +C4hLlNk7RD6BJ8oyMtpXFq7MHAAb8MWSLu/rSAhQHoKqlCEK4Iks9nWLmRP0OdAw +BcXGMuTIn1jFRM0CQvg68GPFOH3FKv+cyUbjPoXvCBYiXKmxA/WX3rYvDo2paZKU +/mDMu+EdAR3Zk/wYXl4738ujqzO88Nw2LBHLKhXytHMaSbfmWf085r0L+fqHLuVM +jlpPEi6vQum25j9tvGnp6GyO8lUDAUqk5gtYIp+D67+NG+9eBocA1ADVpeKZHBQV +xGgCdjnoP+nDVw== +-----END CERTIFICATE----- diff --git a/testing/tls/wrong-ca.key b/testing/tls/wrong-ca.key new file mode 100644 index 0000000..7c0f381 --- /dev/null +++ b/testing/tls/wrong-ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/testing/tls/wrong-client.crt b/testing/tls/wrong-client.crt new file mode 100644 index 0000000..46123ad --- /dev/null +++ b/testing/tls/wrong-client.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEJTCCAg2gAwIBAgIQINVcdjlrWhsKxwcXL4vbUDANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQDEwh3cm9uZy1jYTAeFw0xNzA4MjUxNTQ1NThaFw0yNzA4MjUxNTQ1 +NTZaMBcxFTATBgNVBAMTDHdyb25nLWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANHAXfyT8vuNKc3FeedZH3865bF5PLbRs2R8CaUOdQj/HTM5 +xSsb5Tr3x6IkWK+5SwtnGdaLY7GktSktXyUNf2uZflXHCLAqiqcBpLNO9mcFAACz +pRb7C18ZZ6d9b7UtPA5oK1Vt45iUzI+mdCC0BtRTWeyKdtTF4muD2TtF7RMQwnjf +RGV1EkfQ3sKpX3P7daiA/W116NlESpX3J/VAzoQu+3BrDeXrqgEbNVl+/NN/7uA0 +RY0HxE75RNhL9yuz1VFP4/NaFOdWN3pSMKwcmiIeNC5n0eyW/fodQpOT/dcscfbP +3a0XeoZkfB8nuVcGkmtUkuw0jPy+R64vnQvlugsCAwEAAaNxMG8wDgYDVR0PAQH/ +BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU +0pX6qc7YwiehN5AYh+p4JNkFEXwwHwYDVR0jBBgwFoAUYzrnn1JGlfpUmUpsg5u2 +ste9zWswDQYJKoZIhvcNAQELBQADggIBALH5pFNhbnro2vFE+8RqbRqOZZNoyKqL +INY/e0MNPmhp4CE2BQcrxgFcRgJmyh4lOP8gmIHT8q/9kOvYqMmfU1vbVF/XLFsa +NJxkQSX9uilV1LDykyRbwlI4McjdTW7rLEkW8YrZueMXnDYQHGx9L2qYWgXzA5yA +Mfsgq3pr39sDVDfYg1H+0daA3nIw+OWDsWjORXvzo5TQzjUXLhREp6WuuRKBT1+p +VHGAnUcwDEb6L1bWEloG9ogXJfsXuCUxF+/II1RYSKiAmjge1nDOM2USfIKfD5nz +tLJn0pn0B5dyceJTgOK6dwCXwn0Gc99qVzBSSHtPe+abSuY5dNoIwtL4R4rDE4U+ ++y2vQwzum+GhHn/ZEDuYT/0+IDqkVxeWBiZ5IFEkRpBEFpmEJOdKWaSrIQWMpIjf +FIlxY3VzUD8H5M65kMSRKXbRJ1zSHMcIFKK2R98SPuYnYmgc4kOh49WkEr6dj2B1 +0QNZxPg70HP3qWgCxf8F5Mxg5YOtz7gN6N3AutrlYV0KB/OT0h4lhtvW3inxRgID +iAHw0A4X/1qbFUeSUpINZaVQFtBh6fT/JfYDDTFFBcoqrLOZpFaS7r6FbGP3FDS4 +v9MqsYOSA6LrOHMdRop+eDV718iGDcUVtIItRZZV0s4UTo5q0JpGBtVPo/R8ui19 +eaGxvLJT1Gd+ +-----END CERTIFICATE----- diff --git a/testing/tls/wrong-client.csr b/testing/tls/wrong-client.csr new file mode 100644 index 0000000..abe2343 --- /dev/null +++ b/testing/tls/wrong-client.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICXDCCAUQCAQAwFzEVMBMGA1UEAxMMd3JvbmctY2xpZW50MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cBd/JPy+40pzcV551kffzrlsXk8ttGzZHwJ +pQ51CP8dMznFKxvlOvfHoiRYr7lLC2cZ1otjsaS1KS1fJQ1/a5l+VccIsCqKpwGk +s072ZwUAALOlFvsLXxlnp31vtS08DmgrVW3jmJTMj6Z0ILQG1FNZ7Ip21MXia4PZ +O0XtExDCeN9EZXUSR9Dewqlfc/t1qID9bXXo2URKlfcn9UDOhC77cGsN5euqARs1 +WX7803/u4DRFjQfETvlE2Ev3K7PVUU/j81oU51Y3elIwrByaIh40LmfR7Jb9+h1C +k5P91yxx9s/drRd6hmR8Hye5VwaSa1SS7DSM/L5Hri+dC+W6CwIDAQABoAAwDQYJ +KoZIhvcNAQELBQADggEBAMxscjfVRQ0/0c6f0MWtJJe+vy5Gj26XHVy5EsbH1ofq +eWF00CFlVw5CdznGV0NL6LOE+sz5sBKsN2sZU7xPeV5XRHVXpAuECcOcgWK6FkqA +wSmwVWZ93o+kJXrUZTyZBMkvQMUUr30JIpXIXJmLWKPBq5KRBJLirHZYw4FmbARv +iZdNlQ1rLOZKZl7yUVkAfyfw+ueb0OPFp/fzuCerNB0ySSmYdHzqDFsMLm4Bq/2z +FJOgasAU2RvxFVoRp7P/ZUSvtOKACMUHaYBnKZAvkKv3MIS/qLCSkzxNwsclT8uF +aKRFZnOkZZHvEaXVAwCfJB7tI3TELb+L2KyqU36Q1Oc= +-----END CERTIFICATE REQUEST----- diff --git a/testing/tls/wrong-client.key b/testing/tls/wrong-client.key new file mode 100644 index 0000000..63448ba --- /dev/null +++ b/testing/tls/wrong-client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0cBd/JPy+40pzcV551kffzrlsXk8ttGzZHwJpQ51CP8dMznF +KxvlOvfHoiRYr7lLC2cZ1otjsaS1KS1fJQ1/a5l+VccIsCqKpwGks072ZwUAALOl +FvsLXxlnp31vtS08DmgrVW3jmJTMj6Z0ILQG1FNZ7Ip21MXia4PZO0XtExDCeN9E +ZXUSR9Dewqlfc/t1qID9bXXo2URKlfcn9UDOhC77cGsN5euqARs1WX7803/u4DRF +jQfETvlE2Ev3K7PVUU/j81oU51Y3elIwrByaIh40LmfR7Jb9+h1Ck5P91yxx9s/d +rRd6hmR8Hye5VwaSa1SS7DSM/L5Hri+dC+W6CwIDAQABAoIBAHml4JyRTdX4q+sM +gcPcG3lVtkt0rfK1sh4wFgPlW5kpJE1GTwTOe+b0N5LhE5Jum4h0djbIxrwLc4n7 +J3g82M6VygCDm5VYRuvO9y+LNzrOWo8NoUyvsouoF0a7aCMipfcRETjNr7cZbX5O +ooEpB+Dyqm+Wao7CaavDXySSTInGHG4AD9HM5nQsVIebS1HOkhI7SmNkZTOd3gzp +bR/iZgaYI5eC7Zj7hHNr4gWdRBuefU8wLZZGoqByHRTSrKwICRLIkGyoMRAD9p9r +S48lyUmd3BGHRPLmNl4u0kfsVlcCYBNKVZV6kkSVv4Wht/KVr3tl0+2wrNUe17w7 +vlCsCPECgYEA9AdHq3UjswD6PYITCdrDOGVLMpgL0obA6X2Fi6GsMiRXQgsvxMVY +a4D2vtfZvLa8TSA+b8bK6uSMyD3mOHLxBkUPMQZxiHzq/ldK9vSIvzky0woF6suT +J8fa2F0QfjlKWhFMwf6JVGyZl+vmYqqF55lxRJSWGZHSSxaxYPU7dbkCgYEA3Aqb +YInpCp7zSlWYfXorwnyvsHsTdFbsoGGgMrc5l2B7PcP+Na7lm0dsNCrp7n7TSrE0 +8iIoqhYq5u7RlGf5QXzXcGgZQMHcxLcrqBPviEktuUifXZEwfw9NXrlt4iF/oVTc +++7jqUZ+iH9NIMoxrQPXVdXJSJN9iwc7/yG6j+MCgYA+ehqsWCpSqx5mXwYW0M6I +gs6U3n6wYNXFMeDeFf9rOwioHQsW2tu/cl46EDNr8HEXYfj6TzAmoWs13TszGqKA +02+HQroQksLraVgFEChupOtRQtCvA33ignWSTYlqd6qEksdPJ6brWX6decUbX8M2 +v39TaqNfWok3tlClnUOi6QKBgQCIhfQ9g5OZyWE937nLMH/yHZaMMvCxIDWUlL3m +eZQ7/dq5Sd9xw2AmZbwW6gFWvk2ubCBjkxoT3ckkm0xhfdlC7ohk79GrQh0N2HA3 +ypa1wmGiMhLe5PRoAUCJ4xbwVMRxfsvVbDTIlDpxyjo6e/kyVc3HLevDIe+k0QpC +k9TC7QKBgDyghEPAM5euQHk2o7cMr0YK52vzoM1FGhrRAF5MhTYJEYBNu0Z+0McB +G8kwy4WH5zMuvKj1zZckAzkbpD/iL3XQuzs9pZdNnXzdf25/us0pZ2a/6v5+fpmF +JEuwQ1AztPEv4tLd3+xrmE+j+qd3xDqYt8eaWFswcuxchB6nUdqq +-----END RSA PRIVATE KEY----- diff --git a/tls_settings_test.go b/tls_settings_test.go new file mode 100644 index 0000000..ed0972b --- /dev/null +++ b/tls_settings_test.go @@ -0,0 +1,331 @@ +package grpcurl_test + +import ( + "fmt" + "net" + "strings" + "testing" + "time" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/interop/grpc_testing" + + . "github.com/fullstorydev/grpcurl" + grpcurl_testing "github.com/fullstorydev/grpcurl/testing" +) + +func TestPlainText(t *testing.T) { + e, err := createTestServerAndClient(nil, nil) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + simpleTest(t, e.cc) +} + +func TestBasicTLS(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + simpleTest(t, e.cc) +} + +func TestInsecureClientTLS(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(true, "", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + simpleTest(t, e.cc) +} + +func TestClientCertTLS(t *testing.T) { + serverCreds, err := ServerTransportCredentials("testing/tls/ca.crt", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "testing/tls/client.crt", "testing/tls/client.key") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + simpleTest(t, e.cc) +} + +func TestRequireClientCertTLS(t *testing.T) { + serverCreds, err := ServerTransportCredentials("testing/tls/ca.crt", "testing/tls/server.crt", "testing/tls/server.key", true) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "testing/tls/client.crt", "testing/tls/client.key") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + simpleTest(t, e.cc) +} + +func TestBrokenTLS_ClientPlainText(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + // client connection succeeds since client is not waiting for TLS handshake + e, err := createTestServerAndClient(serverCreds, nil) + if err != nil { + t.Fatalf("failed to setup server and client: %v", err) + } + defer e.Close() + + // but request fails because server closes connection upon seeing request + // bytes that are not a TLS handshake + cl := grpc_testing.NewTestServiceClient(e.cc) + _, err = cl.UnaryCall(context.Background(), &grpc_testing.SimpleRequest{}) + if err == nil { + t.Fatal("expecting failure") + } + // various errors possible when server closes connection + if !strings.Contains(err.Error(), "transport is closing") && + !strings.Contains(err.Error(), "connection is unavailable") && + !strings.Contains(err.Error(), "use of closed network connection") { + + t.Fatalf("expecting transport failure, got: %v", err) + } +} + +func TestBrokenTLS_ServerPlainText(t *testing.T) { + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(nil, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "first record does not look like a TLS handshake") { + t.Fatalf("expecting TLS handshake failure, got: %v", err) + } +} + +func TestBrokenTLS_ServerUsesWrongCert(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/other.crt", "testing/tls/other.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "certificate is valid for") { + t.Fatalf("expecting TLS certificate error, got: %v", err) + } +} + +func TestBrokenTLS_ClientHasExpiredCert(t *testing.T) { + serverCreds, err := ServerTransportCredentials("testing/tls/ca.crt", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "testing/tls/expired.crt", "testing/tls/expired.key") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "bad certificate") { + t.Fatalf("expecting TLS certificate error, got: %v", err) + } +} + +func TestBrokenTLS_ServerHasExpiredCert(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/expired.crt", "testing/tls/expired.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "certificate has expired or is not yet valid") { + t.Fatalf("expecting TLS certificate expired, got: %v", err) + } +} + +func TestBrokenTLS_ClientNotTrusted(t *testing.T) { + serverCreds, err := ServerTransportCredentials("testing/tls/ca.crt", "testing/tls/server.crt", "testing/tls/server.key", true) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "testing/tls/wrong-client.crt", "testing/tls/wrong-client.key") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "bad certificate") { + t.Fatalf("expecting TLS certificate error, got: %v", err) + } +} + +func TestBrokenTLS_ServerNotTrusted(t *testing.T) { + serverCreds, err := ServerTransportCredentials("", "testing/tls/server.crt", "testing/tls/server.key", false) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "", "testing/tls/client.crt", "testing/tls/client.key") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "certificate signed by unknown authority") { + t.Fatalf("expecting TLS certificate error, got: %v", err) + } +} + +func TestBrokenTLS_RequireClientCertButNonePresented(t *testing.T) { + serverCreds, err := ServerTransportCredentials("testing/tls/ca.crt", "testing/tls/server.crt", "testing/tls/server.key", true) + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + clientCreds, err := ClientTransportCredentials(false, "testing/tls/ca.crt", "", "") + if err != nil { + t.Fatalf("failed to create server creds: %v", err) + } + + e, err := createTestServerAndClient(serverCreds, clientCreds) + if err == nil { + t.Fatal("expecting TLS failure setting up server and client") + e.Close() + } + if !strings.Contains(err.Error(), "bad certificate") { + t.Fatalf("expecting TLS certificate error, got: %v", err) + } +} + +func simpleTest(t *testing.T, cc *grpc.ClientConn) { + cl := grpc_testing.NewTestServiceClient(cc) + _, err := cl.UnaryCall(context.Background(), &grpc_testing.SimpleRequest{}) + if err != nil { + t.Errorf("simple RPC failed: %v", err) + } +} + +func createTestServerAndClient(serverCreds, clientCreds credentials.TransportCredentials) (testEnv, error) { + var e testEnv + completed := false + defer func() { + if !completed { + e.Close() + } + }() + + var svrOpts []grpc.ServerOption + if serverCreds != nil { + svrOpts = []grpc.ServerOption{grpc.Creds(serverCreds)} + } + svr := grpc.NewServer(svrOpts...) + grpc_testing.RegisterTestServiceServer(svr, grpcurl_testing.TestServer{}) + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return e, err + } + port := l.Addr().(*net.TCPAddr).Port + go svr.Serve(l) + + cliOpts := []grpc.DialOption{grpc.WithTimeout(2 * time.Second), grpc.WithBlock()} + if clientCreds != nil { + cliOpts = append(cliOpts, grpc.WithTransportCredentials(clientCreds)) + } else { + cliOpts = append(cliOpts, grpc.WithInsecure()) + } + cc, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), cliOpts...) + if err != nil { + return e, err + } + + e.svr = svr + e.cc = cc + completed = true + return e, nil +} + +type testEnv struct { + svr *grpc.Server + cc *grpc.ClientConn +} + +func (e testEnv) Close() { + if e.cc != nil { + e.cc.Close() + e.cc = nil + } + if e.svr != nil { + e.svr.GracefulStop() + e.svr = nil + } +}