initial commit, functioning grpcurl command-line util

This commit is contained in:
Josh Humphries 2017-11-20 13:15:15 -05:00
commit 9dfaded6cf
33 changed files with 3413 additions and 0 deletions

13
.travis.yml Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

87
README.md Normal file
View File

@ -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.

11
ci.sh Executable file
View File

@ -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 ./...

456
cmd/grpcurl/grpcurl.go Normal file
View File

@ -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))
}
}

View File

@ -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
}

846
grpcurl.go Normal file
View File

@ -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
}

666
grpcurl_test.go Normal file
View File

@ -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
}

51
mk-test-files.sh Executable file
View File

@ -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

BIN
testing/test.protoset Normal file

Binary file not shown.

235
testing/test_server.go Normal file
View File

@ -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{}

16
testing/tls/ca.crl Normal file
View File

@ -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-----

28
testing/tls/ca.crt Normal file
View File

@ -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-----

51
testing/tls/ca.key Normal file
View File

@ -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-----

24
testing/tls/client.crt Normal file
View File

@ -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-----

15
testing/tls/client.csr Normal file
View File

@ -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-----

27
testing/tls/client.key Normal file
View File

@ -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-----

25
testing/tls/expired.crt Normal file
View File

@ -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-----

16
testing/tls/expired.csr Normal file
View File

@ -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-----

27
testing/tls/expired.key Normal file
View File

@ -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-----

25
testing/tls/other.crt Normal file
View File

@ -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-----

16
testing/tls/other.csr Normal file
View File

@ -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-----

27
testing/tls/other.key Normal file
View File

@ -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-----

25
testing/tls/server.crt Normal file
View File

@ -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-----

16
testing/tls/server.csr Normal file
View File

@ -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-----

27
testing/tls/server.key Normal file
View File

@ -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-----

16
testing/tls/wrong-ca.crl Normal file
View File

@ -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-----

29
testing/tls/wrong-ca.crt Normal file
View File

@ -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-----

51
testing/tls/wrong-ca.key Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

331
tls_settings_test.go Normal file
View File

@ -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
}
}