├── .circleci └── config.yml ├── .github ├── dependabot.yml └── workflows │ └── codesee-arch-diagram.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── grpcurl │ ├── grpcurl.go │ ├── indent_test.go │ └── unix.go ├── desc_source.go ├── desc_source_test.go ├── download_protoc.sh ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── grpcurl.go ├── grpcurl_test.go ├── internal └── testing │ ├── cmd │ ├── bankdemo │ │ ├── README.md │ │ ├── auth.go │ │ ├── bank.go │ │ ├── bank.pb.go │ │ ├── bank.proto │ │ ├── bank_grpc.pb.go │ │ ├── chat.go │ │ ├── db.go │ │ ├── main.go │ │ ├── support.pb.go │ │ ├── support.proto │ │ └── support_grpc.pb.go │ └── testserver │ │ ├── README.md │ │ ├── testserver.go │ │ └── unix.go │ ├── example.proto │ ├── example.protoset │ ├── example2.proto │ ├── jsonpb_test_proto │ ├── test_objects.pb.go │ └── test_objects.proto │ ├── test.pb.go │ ├── test.proto │ ├── test.protoset │ ├── test_grpc.pb.go │ ├── test_server.go │ └── tls │ ├── ca.crl │ ├── ca.crt │ ├── ca.key │ ├── client.crt │ ├── client.csr │ ├── client.key │ ├── expired.crt │ ├── expired.csr │ ├── expired.key │ ├── other.crt │ ├── other.csr │ ├── other.key │ ├── server.crt │ ├── server.csr │ ├── server.key │ ├── wrong-ca.crl │ ├── wrong-ca.crt │ ├── wrong-ca.key │ ├── wrong-client.crt │ ├── wrong-client.csr │ └── wrong-client.key ├── invoke.go ├── mk-test-files.sh ├── releasing ├── README.md ├── RELEASE_NOTES.md └── do-release.sh ├── snap ├── README.md └── snapcraft.yaml └── tls_settings_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | shared_configs: 2 | simple_job_steps: &simple_job_steps 3 | - checkout 4 | - run: 5 | name: Run tests 6 | command: | 7 | make test 8 | 9 | # Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference 10 | version: 2.1 11 | jobs: 12 | build-1-22: 13 | working_directory: ~/repo 14 | docker: 15 | - image: cimg/go:1.22 16 | steps: *simple_job_steps 17 | 18 | build-1-23: 19 | working_directory: ~/repo 20 | docker: 21 | - image: cimg/go:1.23 22 | steps: *simple_job_steps 23 | 24 | build-1-24: 25 | working_directory: ~/repo 26 | docker: 27 | - image: cimg/go:1.24 28 | steps: 29 | - checkout 30 | - run: 31 | name: Run tests and linters 32 | command: | 33 | make ci 34 | 35 | workflows: 36 | pr-build-test: 37 | jobs: 38 | - build-1-22 39 | - build-1-23 40 | - build-1-24 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | # Check for updates once a week 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/ 2 | # This is v2.0 of this workflow file 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request_target: 8 | types: [opened, synchronize, reopened] 9 | 10 | name: CodeSee 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | codesee: 16 | runs-on: ubuntu-latest 17 | continue-on-error: true 18 | name: Analyze the repo with CodeSee 19 | steps: 20 | - uses: Codesee-io/codesee-action@v2 21 | with: 22 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .idea/ 3 | VERSION 4 | .tmp/ 5 | *.snap 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: grpcurl 3 | main: ./cmd/grpcurl 4 | goos: 5 | - linux 6 | - darwin 7 | - windows 8 | goarch: 9 | - amd64 10 | - 386 11 | - arm 12 | - arm64 13 | - s390x 14 | - ppc64le 15 | goarm: 16 | - 5 17 | - 6 18 | - 7 19 | ignore: 20 | - goos: darwin 21 | goarch: 386 22 | - goos: windows 23 | goarch: arm64 24 | - goos: darwin 25 | goarch: arm 26 | - goos: windows 27 | goarch: arm 28 | - goos: darwin 29 | goarch: s390x 30 | - goos: windows 31 | goarch: s390x 32 | - goos: darwin 33 | goarch: ppc64le 34 | - goos: windows 35 | goarch: ppc64le 36 | ldflags: 37 | - -s -w -X main.version=v{{.Version}} 38 | 39 | archives: 40 | - format: tar.gz 41 | name_template: >- 42 | {{ .Binary }}_{{ .Version }}_ 43 | {{- if eq .Os "darwin" }}osx{{ else }}{{ .Os }}{{ end }}_ 44 | {{- if eq .Arch "amd64" }}x86_64 45 | {{- else if eq .Arch "386" }}x86_32 46 | {{- else }}{{ .Arch }}{{ end }} 47 | {{- with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }} 48 | format_overrides: 49 | - goos: windows 50 | format: zip 51 | files: 52 | - LICENSE 53 | 54 | nfpms: 55 | - vendor: Fullstory 56 | homepage: https://github.com/fullstorydev/grpcurl/ 57 | maintainer: Engineering at Fullstory 58 | description: 'Like cURL, but for gRPC: Command-line tool for interacting with gRPC servers' 59 | license: MIT 60 | id: nfpms 61 | formats: 62 | - deb 63 | - rpm 64 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine as builder 2 | MAINTAINER Fullstory Engineering 3 | 4 | # create non-privileged group and user 5 | RUN addgroup -S grpcurl && adduser -S grpcurl -G grpcurl 6 | 7 | WORKDIR /tmp/fullstorydev/grpcurl 8 | # copy just the files/sources we need to build grpcurl 9 | COPY VERSION *.go go.* /tmp/fullstorydev/grpcurl/ 10 | COPY cmd /tmp/fullstorydev/grpcurl/cmd 11 | # and build a completely static binary (so we can use 12 | # scratch as basis for the final image) 13 | ENV CGO_ENABLED=0 14 | ENV GO111MODULE=on 15 | RUN go build -o /grpcurl \ 16 | -ldflags "-w -extldflags \"-static\" -X \"main.version=$(cat VERSION)\"" \ 17 | ./cmd/grpcurl 18 | 19 | FROM alpine:3 as alpine 20 | WORKDIR / 21 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 22 | COPY --from=builder /etc/passwd /etc/passwd 23 | COPY --from=builder /grpcurl /bin/grpcurl 24 | USER grpcurl 25 | 26 | ENTRYPOINT ["/bin/grpcurl"] 27 | 28 | # New FROM so we have a nice'n'tiny image 29 | FROM scratch 30 | WORKDIR / 31 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 32 | COPY --from=builder /etc/passwd /etc/passwd 33 | COPY --from=builder /grpcurl /bin/grpcurl 34 | USER grpcurl 35 | 36 | ENTRYPOINT ["/bin/grpcurl"] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Fullstory, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev_build_version=$(shell git describe --tags --always --dirty) 2 | 3 | export PATH := $(shell pwd)/.tmp/protoc/bin:$(PATH) 4 | export PROTOC_VERSION := 22.0 5 | # Disable CGO for improved compatibility across distros 6 | export CGO_ENABLED=0 7 | export GOFLAGS=-trimpath 8 | export GOWORK=off 9 | 10 | # TODO: run golint and errcheck, but only to catch *new* violations and 11 | # decide whether to change code or not (e.g. we need to be able to whitelist 12 | # violations already in the code). They can be useful to catch errors, but 13 | # they are just too noisy to be a requirement for a CI -- we don't even *want* 14 | # to fix some of the things they consider to be violations. 15 | .PHONY: ci 16 | ci: deps checkgofmt checkgenerate vet staticcheck ineffassign predeclared test 17 | 18 | .PHONY: deps 19 | deps: 20 | go get -d -v -t ./... 21 | go mod tidy 22 | 23 | .PHONY: updatedeps 24 | updatedeps: 25 | go get -d -v -t -u -f ./... 26 | go mod tidy 27 | 28 | .PHONY: install 29 | install: 30 | go install -ldflags '-X "main.version=dev build $(dev_build_version)"' ./... 31 | 32 | .PHONY: release 33 | release: 34 | @go install github.com/goreleaser/goreleaser@v1.21.0 35 | goreleaser release --clean 36 | 37 | .PHONY: docker 38 | docker: 39 | @echo $(dev_build_version) > VERSION 40 | docker build -t fullstorydev/grpcurl:$(dev_build_version) . 41 | @rm VERSION 42 | 43 | .PHONY: generate 44 | generate: .tmp/protoc/bin/protoc 45 | @go install google.golang.org/protobuf/cmd/protoc-gen-go@a709e31e5d12 46 | @go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0 47 | @go install github.com/jhump/protoreflect/desc/sourceinfo/cmd/protoc-gen-gosrcinfo@v1.14.1 48 | go generate ./... 49 | go mod tidy 50 | 51 | .PHONY: checkgenerate 52 | checkgenerate: generate 53 | git status --porcelain -- '**/*.go' 54 | @if [ -n "$$(git status --porcelain -- '**/*.go')" ]; then \ 55 | git diff -- '**/*.go'; \ 56 | exit 1; \ 57 | fi 58 | 59 | .PHONY: checkgofmt 60 | checkgofmt: 61 | gofmt -s -l . 62 | @if [ -n "$$(gofmt -s -l .)" ]; then \ 63 | exit 1; \ 64 | fi 65 | 66 | .PHONY: vet 67 | vet: 68 | go vet ./... 69 | 70 | .PHONY: staticcheck 71 | staticcheck: 72 | @go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 73 | staticcheck -checks "inherit,-SA1019" ./... 74 | 75 | .PHONY: ineffassign 76 | ineffassign: 77 | @go install github.com/gordonklaus/ineffassign@7953dde2c7bf 78 | ineffassign . 79 | 80 | .PHONY: predeclared 81 | predeclared: 82 | @go install github.com/nishanths/predeclared@51e8c974458a0f93dc03fe356f91ae1a6d791e6f 83 | predeclared ./... 84 | 85 | # Intentionally omitted from CI, but target here for ad-hoc reports. 86 | .PHONY: golint 87 | golint: 88 | @go install golang.org/x/lint/golint@v0.0.0-20210508222113-6edffad5e616 89 | golint -min_confidence 0.9 -set_exit_status ./... 90 | 91 | # Intentionally omitted from CI, but target here for ad-hoc reports. 92 | .PHONY: errcheck 93 | errcheck: 94 | @go install github.com/kisielk/errcheck@v1.2.0 95 | errcheck ./... 96 | 97 | .PHONY: test 98 | test: deps 99 | CGO_ENABLED=1 go test -race ./... 100 | 101 | .tmp/protoc/bin/protoc: ./Makefile ./download_protoc.sh 102 | ./download_protoc.sh 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPCurl 2 | [![Build Status](https://circleci.com/gh/fullstorydev/grpcurl/tree/master.svg?style=svg)](https://circleci.com/gh/fullstorydev/grpcurl/tree/master) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/fullstorydev/grpcurl)](https://goreportcard.com/report/github.com/fullstorydev/grpcurl) 4 | [![Snap Release Status](https://snapcraft.io/grpcurl/badge.svg)](https://snapcraft.io/grpcurl) 5 | 6 | `grpcurl` is a command-line tool that lets you interact with gRPC servers. It's 7 | basically `curl` for gRPC servers. 8 | 9 | The main purpose for this tool is to invoke RPC methods on a gRPC server from the 10 | command-line. gRPC servers use a binary encoding on the wire 11 | ([protocol buffers](https://developers.google.com/protocol-buffers/), or "protobufs" 12 | for short). So they are basically impossible to interact with using regular `curl` 13 | (and older versions of `curl` that do not support HTTP/2 are of course non-starters). 14 | This program accepts messages using JSON encoding, which is much more friendly for both 15 | humans and scripts. 16 | 17 | With this tool you can also browse the schema for gRPC services, either by querying 18 | a server that supports [server reflection](https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1/reflection.proto), 19 | by reading proto source files, or by loading in compiled "protoset" files (files that contain 20 | encoded file [descriptor protos](https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto)). 21 | In fact, the way the tool transforms JSON request data into a binary encoded protobuf 22 | is using that very same schema. So, if the server you interact with does not support 23 | reflection, you will either need the proto source files that define the service or need 24 | protoset files that `grpcurl` can use. 25 | 26 | This repo also provides a library package, `github.com/fullstorydev/grpcurl`, that has 27 | functions for simplifying the construction of other command-line tools that dynamically 28 | invoke gRPC endpoints. This code is a great example of how to use the various packages of 29 | the [protoreflect](https://godoc.org/github.com/jhump/protoreflect) library, and shows 30 | off what they can do. 31 | 32 | See also the [`grpcurl` talk at GopherCon 2018](https://www.youtube.com/watch?v=dDr-8kbMnaw). 33 | 34 | ## Features 35 | `grpcurl` supports all kinds of RPC methods, including streaming methods. You can even 36 | operate bi-directional streaming methods interactively by running `grpcurl` from an 37 | interactive terminal and using stdin as the request body! 38 | 39 | `grpcurl` supports both secure/TLS servers _and_ plain-text servers (i.e. no TLS) and has 40 | numerous options for TLS configuration. It also supports mutual TLS, where the client is 41 | required to present a client certificate. 42 | 43 | As mentioned above, `grpcurl` works seamlessly if the server supports the reflection 44 | service. If not, you can supply the `.proto` source files or you can supply protoset 45 | files (containing compiled descriptors, produced by `protoc`) to `grpcurl`. 46 | 47 | ## Installation 48 | 49 | ### Binaries 50 | 51 | Download the binary from the [releases](https://github.com/fullstorydev/grpcurl/releases) page. 52 | 53 | ### Homebrew (macOS) 54 | 55 | On macOS, `grpcurl` is available via Homebrew: 56 | ```shell 57 | brew install grpcurl 58 | ``` 59 | 60 | ### Docker 61 | 62 | For platforms that support Docker, you can download an image that lets you run `grpcurl`: 63 | ```shell 64 | # Download image 65 | docker pull fullstorydev/grpcurl:latest 66 | # Run the tool 67 | docker run fullstorydev/grpcurl api.grpc.me:443 list 68 | ``` 69 | Note that there are some pitfalls when using docker: 70 | - If you need to interact with a server listening on the host's loopback network, you must specify the host as `host.docker.internal` instead of `localhost` (for Mac or Windows) _OR_ have the container use the host network with `-network="host"` (Linux only). 71 | - If you need to provide proto source files or descriptor sets, you must mount the folder containing the files as a volume (`-v $(pwd):/protos`) and adjust the import paths to container paths accordingly. 72 | - If you want to provide the request message via stdin, using the `-d @` option, you need to use the `-i` flag on the docker command. 73 | 74 | ### Other Packages 75 | 76 | There are numerous other ways to install `grpcurl`, thanks to support from third parties that 77 | have created recipes/packages for it. These include other ways to install `grpcurl` on a variety 78 | of environments, including Windows and myriad Linux distributions. 79 | 80 | You can see more details and the full list of other packages for `grpcurl` at _repology.org_: 81 | https://repology.org/project/grpcurl/information 82 | 83 | ### Snap 84 | 85 | You can install `grpcurl` using the snap package: 86 | 87 | `snap install grpcurl` 88 | 89 | ### From Source 90 | If you already have the [Go SDK](https://golang.org/doc/install) installed, you can use the `go` 91 | tool to install `grpcurl`: 92 | ```shell 93 | go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest 94 | ``` 95 | 96 | This installs the command into the `bin` sub-folder of wherever your `$GOPATH` 97 | environment variable points. (If you have no `GOPATH` environment variable set, 98 | the default install location is `$HOME/go/bin`). If this directory is already in 99 | your `$PATH`, then you should be good to go. 100 | 101 | If you have already pulled down this repo to a location that is not in your 102 | `$GOPATH` and want to build from the sources, you can `cd` into the repo and then 103 | run `make install`. 104 | 105 | If you encounter compile errors and are using a version of the Go SDK older than 1.13, 106 | you could have out-dated versions of `grpcurl`'s dependencies. You can update the 107 | dependencies by running `make updatedeps`. Or, if you are using Go 1.11 or 1.12, you 108 | can add `GO111MODULE=on` as a prefix to the commands above, which will also build using 109 | the right versions of dependencies (vs. whatever you may already have in your `GOPATH`). 110 | 111 | ## Usage 112 | The usage doc for the tool explains the numerous options: 113 | ```shell 114 | grpcurl -help 115 | ``` 116 | 117 | In the sections below, you will find numerous examples demonstrating how to use 118 | `grpcurl`. 119 | 120 | ### Invoking RPCs 121 | Invoking an RPC on a trusted server (e.g. TLS without self-signed key or custom CA) 122 | that requires no client certs and supports server reflection is the simplest thing to 123 | do with `grpcurl`. This minimal invocation sends an empty request body: 124 | ```shell 125 | grpcurl grpc.server.com:443 my.custom.server.Service/Method 126 | 127 | # no TLS 128 | grpcurl -plaintext grpc.server.com:80 my.custom.server.Service/Method 129 | ``` 130 | 131 | To send a non-empty request, use the `-d` argument. Note that all arguments must come 132 | *before* the server address and method name: 133 | ```shell 134 | grpcurl -d '{"id": 1234, "tags": ["foo","bar"]}' \ 135 | grpc.server.com:443 my.custom.server.Service/Method 136 | ``` 137 | 138 | As can be seen in the example, the supplied body must be in JSON format. The body will 139 | be parsed and then transmitted to the server in the protobuf binary format. 140 | 141 | If you want to include `grpcurl` in a command pipeline, such as when using `jq` to 142 | create a request body, you can use `-d @`, which tells `grpcurl` to read the actual 143 | request body from stdin: 144 | ```shell 145 | grpcurl -d @ grpc.server.com:443 my.custom.server.Service/Method <&2 9 | exit 1 10 | fi 11 | PROTOC_OS="$(uname -s)" 12 | PROTOC_ARCH="$(uname -m)" 13 | case "${PROTOC_OS}" in 14 | Darwin) PROTOC_OS="osx" ;; 15 | Linux) PROTOC_OS="linux" ;; 16 | *) 17 | echo "Invalid value for uname -s: ${PROTOC_OS}" >&2 18 | exit 1 19 | esac 20 | 21 | # This is for macs with M1 chips. Precompiled binaries for osx/amd64 are not available for download, so for that case 22 | # we download the x86_64 version instead. This will work as long as rosetta2 is installed. 23 | if [ "$PROTOC_OS" = "osx" ] && [ "$PROTOC_ARCH" = "arm64" ]; then 24 | PROTOC_ARCH="x86_64" 25 | fi 26 | 27 | PROTOC="${PWD}/.tmp/protoc/bin/protoc" 28 | 29 | if [[ "$(${PROTOC} --version 2>/dev/null)" != "libprotoc 3.${PROTOC_VERSION}" ]]; then 30 | rm -rf ./.tmp/protoc 31 | mkdir -p .tmp/protoc 32 | curl -L "https://github.com/google/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-${PROTOC_OS}-${PROTOC_ARCH}.zip" > .tmp/protoc/protoc.zip 33 | pushd ./.tmp/protoc && unzip protoc.zip && popd 34 | fi 35 | 36 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package grpcurl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/golang/protobuf/jsonpb" //lint:ignore SA1019 we have to import these because some of their types appear in exported API 11 | "github.com/golang/protobuf/proto" //lint:ignore SA1019 same as above 12 | "github.com/jhump/protoreflect/desc" //lint:ignore SA1019 same as above 13 | "google.golang.org/grpc/metadata" 14 | "google.golang.org/protobuf/types/known/structpb" 15 | ) 16 | 17 | func TestRequestParser(t *testing.T) { 18 | source, err := DescriptorSourceFromProtoSets("internal/testing/example.protoset") 19 | if err != nil { 20 | t.Fatalf("failed to create descriptor source: %v", err) 21 | } 22 | 23 | msg, err := makeProto() 24 | if err != nil { 25 | t.Fatalf("failed to create message: %v", err) 26 | } 27 | 28 | testCases := []struct { 29 | format Format 30 | input string 31 | expectedOutput []proto.Message 32 | }{ 33 | { 34 | format: FormatJSON, 35 | input: "", 36 | }, 37 | { 38 | format: FormatJSON, 39 | input: messageAsJSON, 40 | expectedOutput: []proto.Message{msg}, 41 | }, 42 | { 43 | format: FormatJSON, 44 | input: messageAsJSON + messageAsJSON + messageAsJSON, 45 | expectedOutput: []proto.Message{msg, msg, msg}, 46 | }, 47 | { 48 | // unlike JSON, empty input yields one empty message (vs. zero messages) 49 | format: FormatText, 50 | input: "", 51 | expectedOutput: []proto.Message{&structpb.Value{}}, 52 | }, 53 | { 54 | format: FormatText, 55 | input: messageAsText, 56 | expectedOutput: []proto.Message{msg}, 57 | }, 58 | { 59 | format: FormatText, 60 | input: messageAsText + string(textSeparatorChar), 61 | expectedOutput: []proto.Message{msg, &structpb.Value{}}, 62 | }, 63 | { 64 | format: FormatText, 65 | input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText, 66 | expectedOutput: []proto.Message{msg, msg, msg}, 67 | }, 68 | } 69 | 70 | for i, tc := range testCases { 71 | name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput)) 72 | rf, _, err := RequestParserAndFormatter(tc.format, source, strings.NewReader(tc.input), FormatOptions{}) 73 | if err != nil { 74 | t.Errorf("Failed to create parser and formatter: %v", err) 75 | continue 76 | } 77 | numReqs := 0 78 | for { 79 | var req structpb.Value 80 | err := rf.Next(&req) 81 | if err == io.EOF { 82 | break 83 | } else if err != nil { 84 | t.Errorf("%s, msg %d: unexpected error: %v", name, numReqs, err) 85 | } 86 | if !proto.Equal(&req, tc.expectedOutput[numReqs]) { 87 | t.Errorf("%s, msg %d: incorrect message;\nexpecting:\n%v\ngot:\n%v", name, numReqs, tc.expectedOutput[numReqs], &req) 88 | } 89 | numReqs++ 90 | } 91 | if rf.NumRequests() != numReqs { 92 | t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.NumRequests()) 93 | } 94 | } 95 | } 96 | 97 | // Handler prints response data (and headers/trailers in verbose mode). 98 | // This verifies that we get the right output in both JSON and proto text modes. 99 | func TestHandler(t *testing.T) { 100 | source, err := DescriptorSourceFromProtoSets("internal/testing/example.protoset") 101 | if err != nil { 102 | t.Fatalf("failed to create descriptor source: %v", err) 103 | } 104 | d, err := source.FindSymbol("TestService.GetFiles") 105 | if err != nil { 106 | t.Fatalf("failed to find method 'TestService.GetFiles': %v", err) 107 | } 108 | md, ok := d.(*desc.MethodDescriptor) 109 | if !ok { 110 | t.Fatalf("wrong kind of descriptor found: %T", d) 111 | } 112 | 113 | reqHeaders := metadata.Pairs("foo", "123", "bar", "456") 114 | respHeaders := metadata.Pairs("foo", "abc", "bar", "def", "baz", "xyz") 115 | respTrailers := metadata.Pairs("a", "1", "b", "2", "c", "3") 116 | rsp, err := makeProto() 117 | if err != nil { 118 | t.Fatalf("failed to create response message: %v", err) 119 | } 120 | 121 | for _, format := range []Format{FormatJSON, FormatText} { 122 | for _, numMessages := range []int{1, 3} { 123 | for verbosityLevel := 0; verbosityLevel <= 2; verbosityLevel++ { 124 | name := fmt.Sprintf("%s, %d message(s)", format, numMessages) 125 | if verbosityLevel > 0 { 126 | name += fmt.Sprintf(", verbosityLevel=%d", verbosityLevel) 127 | } 128 | 129 | verbose := verbosityLevel > 0 130 | 131 | _, formatter, err := RequestParserAndFormatter(format, source, nil, FormatOptions{IncludeTextSeparator: !verbose}) 132 | if err != nil { 133 | t.Errorf("Failed to create parser and formatter: %v", err) 134 | continue 135 | } 136 | 137 | var buf bytes.Buffer 138 | h := &DefaultEventHandler{ 139 | Out: &buf, 140 | Formatter: formatter, 141 | VerbosityLevel: verbosityLevel, 142 | } 143 | 144 | h.OnResolveMethod(md) 145 | h.OnSendHeaders(reqHeaders) 146 | h.OnReceiveHeaders(respHeaders) 147 | for i := 0; i < numMessages; i++ { 148 | h.OnReceiveResponse(rsp) 149 | } 150 | h.OnReceiveTrailers(nil, respTrailers) 151 | 152 | expectedOutput := "" 153 | if verbose { 154 | expectedOutput += verbosePrefix 155 | } 156 | for i := 0; i < numMessages; i++ { 157 | if verbosityLevel > 1 { 158 | expectedOutput += verboseResponseSize 159 | } 160 | if verbose { 161 | expectedOutput += verboseResponseHeader 162 | } 163 | if format == "json" { 164 | expectedOutput += messageAsJSON 165 | } else { 166 | if i > 0 && !verbose { 167 | expectedOutput += string(textSeparatorChar) 168 | } 169 | expectedOutput += messageAsText 170 | } 171 | } 172 | if verbose { 173 | expectedOutput += verboseSuffix 174 | } 175 | 176 | out := buf.String() 177 | if !compare(out, expectedOutput) { 178 | t.Errorf("%s: Incorrect output. Expected:\n%s\nGot:\n%s", name, expectedOutput, out) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | // compare checks that actual and expected are equal, returning true if so. 186 | // A simple equality check (==) does not suffice because jsonpb formats 187 | // structpb.Value strangely. So if that formatting gets fixed, we don't 188 | // want this test in grpcurl to suddenly start failing. So we check each 189 | // line and compare the lines after stripping whitespace (which removes 190 | // the jsonpb format anomalies). 191 | func compare(actual, expected string) bool { 192 | actualLines := strings.Split(actual, "\n") 193 | expectedLines := strings.Split(expected, "\n") 194 | if len(actualLines) != len(expectedLines) { 195 | return false 196 | } 197 | for i := 0; i < len(actualLines); i++ { 198 | if strings.TrimSpace(actualLines[i]) != strings.TrimSpace(expectedLines[i]) { 199 | return false 200 | } 201 | } 202 | return true 203 | } 204 | 205 | func makeProto() (proto.Message, error) { 206 | var rsp structpb.Value 207 | err := jsonpb.UnmarshalString(`{ 208 | "foo": ["abc", "def", "ghi"], 209 | "bar": { "a": 1, "b": 2 }, 210 | "baz": true, 211 | "null": null 212 | }`, &rsp) 213 | if err != nil { 214 | return nil, err 215 | } 216 | return &rsp, nil 217 | } 218 | 219 | var ( 220 | verbosePrefix = ` 221 | Resolved method descriptor: 222 | rpc GetFiles ( .TestRequest ) returns ( .TestResponse ); 223 | 224 | Request metadata to send: 225 | bar: 456 226 | foo: 123 227 | 228 | Response headers received: 229 | bar: def 230 | baz: xyz 231 | foo: abc 232 | ` 233 | verboseSuffix = ` 234 | Response trailers received: 235 | a: 1 236 | b: 2 237 | c: 3 238 | ` 239 | verboseResponseSize = ` 240 | Estimated response size: 100 bytes 241 | ` 242 | verboseResponseHeader = ` 243 | Response contents: 244 | ` 245 | messageAsJSON = `{ 246 | "bar": { 247 | "a": 1, 248 | "b": 2 249 | }, 250 | "baz": true, 251 | "foo": [ 252 | "abc", 253 | "def", 254 | "ghi" 255 | ], 256 | "null": null 257 | } 258 | ` 259 | messageAsText = `struct_value: < 260 | fields: < 261 | key: "bar" 262 | value: < 263 | struct_value: < 264 | fields: < 265 | key: "a" 266 | value: < 267 | number_value: 1 268 | > 269 | > 270 | fields: < 271 | key: "b" 272 | value: < 273 | number_value: 2 274 | > 275 | > 276 | > 277 | > 278 | > 279 | fields: < 280 | key: "baz" 281 | value: < 282 | bool_value: true 283 | > 284 | > 285 | fields: < 286 | key: "foo" 287 | value: < 288 | list_value: < 289 | values: < 290 | string_value: "abc" 291 | > 292 | values: < 293 | string_value: "def" 294 | > 295 | values: < 296 | string_value: "ghi" 297 | > 298 | > 299 | > 300 | > 301 | fields: < 302 | key: "null" 303 | value: < 304 | null_value: NULL_VALUE 305 | > 306 | > 307 | > 308 | ` 309 | ) 310 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fullstorydev/grpcurl 2 | 3 | go 1.21 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/golang/protobuf v1.5.4 8 | github.com/jhump/protoreflect v1.17.0 9 | google.golang.org/grpc v1.61.0 10 | google.golang.org/protobuf v1.36.6 11 | ) 12 | 13 | require ( 14 | cloud.google.com/go/compute v1.23.3 // indirect 15 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 16 | github.com/bufbuild/protocompile v0.14.1 // indirect 17 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect 20 | github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 // indirect 21 | github.com/envoyproxy/go-control-plane v0.11.1 // indirect 22 | github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect 23 | golang.org/x/net v0.38.0 // indirect 24 | golang.org/x/oauth2 v0.14.0 // indirect 25 | golang.org/x/sync v0.12.0 // indirect 26 | golang.org/x/sys v0.31.0 // indirect 27 | golang.org/x/text v0.23.0 // indirect 28 | google.golang.org/appengine v1.6.8 // indirect 29 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect 30 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect 31 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= 3 | cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= 4 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 5 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 8 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 9 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 10 | github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= 11 | github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= 12 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 13 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 14 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 15 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= 16 | github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 17 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 18 | github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= 19 | github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 23 | github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= 24 | github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= 25 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 26 | github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= 27 | github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= 28 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 29 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 30 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 33 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 34 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 35 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 37 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 39 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 41 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 43 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 47 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 48 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 49 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 52 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 53 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 54 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 55 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 56 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 57 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 58 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 59 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 60 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 61 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 62 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 63 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 64 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 65 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 66 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 67 | golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= 68 | golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= 69 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 74 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 75 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 77 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 82 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 83 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 84 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 87 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 88 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 89 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 90 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 91 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 92 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 93 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 94 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 95 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 96 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 97 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 98 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 101 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 102 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 103 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 104 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 105 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 106 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= 107 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= 108 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= 109 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= 110 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= 111 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= 112 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 113 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 114 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 115 | google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= 116 | google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= 117 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 118 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 119 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 120 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 121 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 122 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 123 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 124 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 125 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/README.md: -------------------------------------------------------------------------------- 1 | # bankdemo 2 | 3 | The `bankdemo` program is an example gRPC server that was used to demo `grpcurl` at Gophercon 2018. 4 | 5 | It demonstrates interesting concepts for building a gRPC server, including chat functionality (that relies on full-duplex bidirectional streams). This code was written specifically to provide an interesting concrete demonstration and, as such, should not be considered in any way production-worthy. 6 | 7 | The demo app tracks user accounts, transactions, and balances completely in memory. Every few seconds, as well as on graceful shutdown (like when the server receives a SIGTERM or SIGINT signal), this state is saved to a file named `accounts.json`, so that the data can be restored if the process restarts. 8 | 9 | In addition to bank account data, the server also tracks "chat sessions", for demonstrating bidirectional streams in the form of an application where customers can chat with support agents. 10 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "google.golang.org/grpc/metadata" 8 | ) 9 | 10 | func getCustomer(ctx context.Context) string { 11 | // we'll just treat the "auth token" as if it is a 12 | // customer ID, but reject tokens that begin with "agent" 13 | // (those are auth tokens for support agents, not customers) 14 | cust := getAuthCode(ctx) 15 | if strings.HasPrefix(cust, "agent") { 16 | return "" 17 | } 18 | return cust 19 | } 20 | 21 | func getAgent(ctx context.Context) string { 22 | // we'll just treat the "auth token" as if it is an agent's 23 | // user ID, but reject tokens that don't begin with "agent" 24 | // (those are auth tokens for customers, not support agents) 25 | agent := getAuthCode(ctx) 26 | if !strings.HasPrefix(agent, "agent") { 27 | return "" 28 | } 29 | return agent 30 | } 31 | 32 | func getAuthCode(ctx context.Context) string { 33 | md, ok := metadata.FromIncomingContext(ctx) 34 | if !ok { 35 | return "" 36 | } 37 | vals := md.Get("authorization") 38 | if len(vals) != 1 { 39 | return "" 40 | } 41 | pieces := strings.SplitN(strings.ToLower(vals[0]), " ", 2) 42 | if len(pieces) != 2 { 43 | return "" 44 | } 45 | if pieces[0] != "token" { 46 | return "" 47 | } 48 | return pieces[1] 49 | } 50 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/bank.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/protobuf/ptypes/empty" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | ) 12 | 13 | // bankServer implements the Bank gRPC service. 14 | type bankServer struct { 15 | UnimplementedBankServer 16 | allAccounts *accounts 17 | } 18 | 19 | func (s *bankServer) OpenAccount(ctx context.Context, req *OpenAccountRequest) (*Account, error) { 20 | cust := getCustomer(ctx) 21 | if cust == "" { 22 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 23 | } 24 | switch req.Type { 25 | case Account_CHECKING, Account_SAVING, Account_MONEY_MARKET: 26 | if req.InitialDepositCents < 0 { 27 | return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount cannot be negative: %s", dollars(req.InitialDepositCents)) 28 | } 29 | case Account_LINE_OF_CREDIT, Account_LOAN, Account_EQUITIES: 30 | if req.InitialDepositCents != 0 { 31 | return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount must be zero for account type %v: %s", req.Type, dollars(req.InitialDepositCents)) 32 | } 33 | default: 34 | return nil, status.Errorf(codes.InvalidArgument, "invalid account type: %v", req.Type) 35 | } 36 | 37 | return s.allAccounts.openAccount(cust, req.Type, req.InitialDepositCents), nil 38 | } 39 | 40 | func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest) (*empty.Empty, error) { 41 | cust := getCustomer(ctx) 42 | if cust == "" { 43 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 44 | } 45 | 46 | if err := s.allAccounts.closeAccount(cust, req.AccountNumber); err != nil { 47 | return nil, err 48 | } 49 | return &empty.Empty{}, nil 50 | } 51 | 52 | func (s *bankServer) GetAccounts(ctx context.Context, _ *empty.Empty) (*GetAccountsResponse, error) { 53 | cust := getCustomer(ctx) 54 | if cust == "" { 55 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 56 | } 57 | 58 | accounts := s.allAccounts.getAllAccounts(cust) 59 | return &GetAccountsResponse{Accounts: accounts}, nil 60 | } 61 | 62 | func (s *bankServer) GetTransactions(req *GetTransactionsRequest, stream Bank_GetTransactionsServer) error { 63 | cust := getCustomer(stream.Context()) 64 | if cust == "" { 65 | return status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 66 | } 67 | 68 | acct, err := s.allAccounts.getAccount(cust, req.AccountNumber) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | var start, end time.Time 74 | if req.Start != nil { 75 | err := req.Start.CheckValid() 76 | if err != nil { 77 | return err 78 | } 79 | start = req.Start.AsTime() 80 | } 81 | if req.End != nil { 82 | err := req.End.CheckValid() 83 | if err != nil { 84 | return err 85 | } 86 | end = req.End.AsTime() 87 | } else { 88 | end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.Local) 89 | } 90 | 91 | txns := acct.getTransactions() 92 | for _, txn := range txns { 93 | err := txn.Date.CheckValid() 94 | if err != nil { 95 | return err 96 | } 97 | t := txn.Date.AsTime() 98 | if (t.After(start) || t.Equal(start)) && 99 | (t.Before(end) || t.Equal(end)) { 100 | 101 | if err := stream.Send(txn); err != nil { 102 | return err 103 | } 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | func (s *bankServer) Deposit(ctx context.Context, req *DepositRequest) (*BalanceResponse, error) { 110 | cust := getCustomer(ctx) 111 | if cust == "" { 112 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 113 | } 114 | 115 | switch req.Source { 116 | case DepositRequest_ACH, DepositRequest_CASH, DepositRequest_CHECK, DepositRequest_WIRE: 117 | // ok 118 | default: 119 | return nil, status.Errorf(codes.InvalidArgument, "unknown deposit source: %v", req.Source) 120 | } 121 | 122 | if req.AmountCents <= 0 { 123 | return nil, status.Errorf(codes.InvalidArgument, "deposit amount cannot be non-positive: %s", dollars(req.AmountCents)) 124 | } 125 | 126 | desc := fmt.Sprintf("%v deposit", req.Source) 127 | if req.Desc != "" { 128 | desc = fmt.Sprintf("%s: %s", desc, req.Desc) 129 | } 130 | acct, err := s.allAccounts.getAccount(cust, req.AccountNumber) 131 | if err != nil { 132 | return nil, err 133 | } 134 | newBalance, err := acct.newTransaction(req.AmountCents, desc) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &BalanceResponse{ 139 | AccountNumber: req.AccountNumber, 140 | BalanceCents: newBalance, 141 | }, nil 142 | } 143 | 144 | func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*BalanceResponse, error) { 145 | cust := getCustomer(ctx) 146 | if cust == "" { 147 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 148 | } 149 | 150 | if req.AmountCents >= 0 { 151 | return nil, status.Errorf(codes.InvalidArgument, "withdrawal amount cannot be non-negative: %s", dollars(req.AmountCents)) 152 | } 153 | 154 | acct, err := s.allAccounts.getAccount(cust, req.AccountNumber) 155 | if err != nil { 156 | return nil, err 157 | } 158 | newBalance, err := acct.newTransaction(req.AmountCents, req.Desc) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return &BalanceResponse{ 163 | AccountNumber: req.AccountNumber, 164 | BalanceCents: newBalance, 165 | }, nil 166 | } 167 | 168 | func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) { 169 | cust := getCustomer(ctx) 170 | if cust == "" { 171 | return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 172 | } 173 | 174 | if req.AmountCents <= 0 { 175 | return nil, status.Errorf(codes.InvalidArgument, "transfer amount cannot be non-positive: %s", dollars(req.AmountCents)) 176 | } 177 | 178 | var srcAcct *account 179 | var srcDesc string 180 | switch src := req.Source.(type) { 181 | case *TransferRequest_ExternalSource: 182 | srcDesc = fmt.Sprintf("ACH %09d:%06d", src.ExternalSource.AchRoutingNumber, src.ExternalSource.AchAccountNumber) 183 | if src.ExternalSource.AchAccountNumber == 0 || src.ExternalSource.AchRoutingNumber == 0 { 184 | return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", srcDesc) 185 | } 186 | case *TransferRequest_SourceAccountNumber: 187 | srcDesc = fmt.Sprintf("account %06d", src.SourceAccountNumber) 188 | var err error 189 | if srcAcct, err = s.allAccounts.getAccount(cust, src.SourceAccountNumber); err != nil { 190 | return nil, err 191 | } 192 | } 193 | 194 | var destAcct *account 195 | var destDesc string 196 | switch dest := req.Dest.(type) { 197 | case *TransferRequest_ExternalDest: 198 | destDesc = fmt.Sprintf("ACH %09d:%06d", dest.ExternalDest.AchRoutingNumber, dest.ExternalDest.AchAccountNumber) 199 | if dest.ExternalDest.AchAccountNumber == 0 || dest.ExternalDest.AchRoutingNumber == 0 { 200 | return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", destDesc) 201 | } 202 | case *TransferRequest_DestAccountNumber: 203 | destDesc = fmt.Sprintf("account %06d", dest.DestAccountNumber) 204 | var err error 205 | if destAcct, err = s.allAccounts.getAccount(cust, dest.DestAccountNumber); err != nil { 206 | return nil, err 207 | } 208 | } 209 | 210 | var srcBalance int32 211 | if srcAcct != nil { 212 | desc := fmt.Sprintf("transfer to %s", destDesc) 213 | if req.Desc != "" { 214 | desc = fmt.Sprintf("%s: %s", desc, req.Desc) 215 | } 216 | var err error 217 | if srcBalance, err = srcAcct.newTransaction(-req.AmountCents, desc); err != nil { 218 | return nil, err 219 | } 220 | } 221 | 222 | var destBalance int32 223 | if destAcct != nil { 224 | desc := fmt.Sprintf("transfer from %s", srcDesc) 225 | if req.Desc != "" { 226 | desc = fmt.Sprintf("%s: %s", desc, req.Desc) 227 | } 228 | var err error 229 | if destBalance, err = destAcct.newTransaction(req.AmountCents, desc); err != nil { 230 | return nil, err 231 | } 232 | } 233 | 234 | return &TransferResponse{ 235 | SrcAccountNumber: req.GetSourceAccountNumber(), 236 | SrcBalanceCents: srcBalance, 237 | DestAccountNumber: req.GetDestAccountNumber(), 238 | DestBalanceCents: destBalance, 239 | }, nil 240 | } 241 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/bank.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = ".;main"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | // Bank provides operations for interacting with bank accounts. All 9 | // operations operate for the authenticated user (identified via an 10 | // "authorization" request header, where the type is "token" and the 11 | // credential is the customer's ID). 12 | service Bank { 13 | // OpenAccount creates an account with the type and given initial deposit 14 | // as its balance. 15 | rpc OpenAccount(OpenAccountRequest) returns (Account); 16 | // CloseAccount closes the indicated account. An account can only be 17 | // closed if its balance is zero. 18 | rpc CloseAccount(CloseAccountRequest) returns (google.protobuf.Empty); 19 | // GetAccounts lists all accounts for the current customer. 20 | rpc GetAccounts(google.protobuf.Empty) returns (GetAccountsResponse); 21 | // GetTransactions streams all transactions that match the given criteria. 22 | // If the given start date is not specified, transactions since beginning 23 | // of time are included. Similarly, if the given end date is not specified, 24 | // transactions all the way to the presnet are included. 25 | rpc GetTransactions(GetTransactionsRequest) returns (stream Transaction); 26 | // Deposit increases the balance of an account by depositing funds into it. 27 | rpc Deposit(DepositRequest) returns (BalanceResponse); 28 | // Withdraw decreases the balance of an account by withdrawing funds from it. 29 | rpc Withdraw(WithdrawRequest) returns (BalanceResponse); 30 | // Transfer moves money from one account to another. The source and destination 31 | // accounts can be with this bank (e.g. "local" account numbers) or can be 32 | // external accounts, identified by their ACH routing and account numbers. 33 | rpc Transfer(TransferRequest) returns (TransferResponse); 34 | } 35 | 36 | message OpenAccountRequest { 37 | int32 initial_deposit_cents = 1; 38 | Account.Type type = 2; 39 | } 40 | 41 | message CloseAccountRequest { 42 | uint64 account_number = 1; 43 | } 44 | 45 | message GetAccountsResponse { 46 | repeated Account accounts = 1; 47 | } 48 | 49 | message Account { 50 | uint64 account_number = 1; 51 | enum Type { 52 | UNKNOWN = 0; 53 | CHECKING = 1; 54 | SAVING = 2; 55 | MONEY_MARKET = 3; 56 | LINE_OF_CREDIT = 4; 57 | LOAN = 5; 58 | EQUITIES = 6; 59 | } 60 | Type type = 2; 61 | int32 balance_cents = 3; 62 | } 63 | 64 | message GetTransactionsRequest { 65 | uint64 account_number = 1; 66 | google.protobuf.Timestamp start = 2; 67 | google.protobuf.Timestamp end = 3; 68 | } 69 | 70 | message Transaction { 71 | uint64 account_number = 1; 72 | uint64 seq_number = 2; 73 | google.protobuf.Timestamp date = 3; 74 | int32 amount_cents = 4; 75 | string desc = 5; 76 | } 77 | 78 | message DepositRequest { 79 | uint64 account_number = 1; 80 | int32 amount_cents = 2; 81 | enum Source { 82 | UNKNOWN = 0; 83 | CASH = 1; 84 | CHECK = 2; 85 | ACH = 3; 86 | WIRE = 4; 87 | } 88 | Source source = 3; 89 | string desc = 4; 90 | } 91 | 92 | message BalanceResponse { 93 | uint64 account_number = 1; 94 | int32 balance_cents = 2; 95 | } 96 | 97 | message WithdrawRequest { 98 | uint64 account_number = 1; 99 | int32 amount_cents = 2; 100 | string desc = 3; 101 | } 102 | 103 | message TransferRequest { 104 | message ExternalAccount { 105 | uint64 ach_routing_number = 1; 106 | uint64 ach_account_number = 2; 107 | } 108 | oneof source { 109 | uint64 source_account_number = 1; 110 | ExternalAccount external_source = 2; 111 | } 112 | oneof dest { 113 | uint64 dest_account_number = 3; 114 | ExternalAccount external_dest = 4; 115 | } 116 | int32 amount_cents = 5; 117 | string desc = 6; 118 | } 119 | 120 | message TransferResponse { 121 | uint64 src_account_number = 1; 122 | int32 src_balance_cents = 2; 123 | uint64 dest_account_number = 3; 124 | int32 dest_balance_cents = 4; 125 | } 126 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | "google.golang.org/protobuf/types/known/timestamppb" 12 | ) 13 | 14 | // chatServer implements the Support gRPC service, for providing 15 | // a capability to connect customers and support agents in real-time 16 | // chat. 17 | type chatServer struct { 18 | UnimplementedSupportServer 19 | chatsBySession map[string]*session 20 | chatsAwaitingAgent []string 21 | lastSession int32 22 | mu sync.RWMutex 23 | } 24 | 25 | type session struct { 26 | Session 27 | active bool 28 | cust *listener 29 | agents map[string]*listener 30 | mu sync.RWMutex 31 | } 32 | 33 | type listener struct { 34 | ch chan<- *ChatEntry 35 | ctx context.Context 36 | } 37 | 38 | func (l *listener) send(e *ChatEntry) { 39 | select { 40 | case l.ch <- e: 41 | case <-l.ctx.Done(): 42 | } 43 | } 44 | 45 | func (s *session) copySession() *Session { 46 | s.mu.RLock() 47 | defer s.mu.RUnlock() 48 | return &Session{ 49 | SessionId: s.SessionId, 50 | CustomerName: s.Session.CustomerName, 51 | History: s.Session.History, 52 | } 53 | } 54 | 55 | func (s *chatServer) ChatCustomer(stream Support_ChatCustomerServer) error { 56 | ctx, cancel := context.WithCancel(stream.Context()) 57 | defer cancel() 58 | 59 | cust := getCustomer(ctx) 60 | if cust == "" { 61 | return status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 62 | } 63 | 64 | var sess *session 65 | var ch chan *ChatEntry 66 | var chCancel context.CancelFunc 67 | cleanup := func() { 68 | if sess != nil { 69 | sess.mu.Lock() 70 | sess.cust = nil 71 | sess.mu.Unlock() 72 | chCancel() 73 | close(ch) 74 | go func() { 75 | // drain channel to prevent deadlock 76 | for range ch { 77 | } 78 | }() 79 | } 80 | } 81 | defer cleanup() 82 | for { 83 | req, err := stream.Recv() 84 | if err != nil { 85 | if err == io.EOF { 86 | return nil 87 | } 88 | return err 89 | } 90 | 91 | switch req := req.Req.(type) { 92 | case *ChatCustomerRequest_Init: 93 | if sess != nil { 94 | return status.Errorf(codes.FailedPrecondition, "already called init, currently in chat session %q", sess.SessionId) 95 | } 96 | sessionID := req.Init.ResumeSessionId 97 | if sessionID == "" { 98 | sess, ch, chCancel = s.newSession(ctx, cust) 99 | } else if sess, ch, chCancel = s.resumeSession(ctx, cust, sessionID); sess == nil { 100 | return status.Errorf(codes.FailedPrecondition, "cannot resume session %q; it is not an open session", sessionID) 101 | } 102 | err := stream.Send(&ChatCustomerResponse{ 103 | Resp: &ChatCustomerResponse_Session{ 104 | Session: sess.copySession(), 105 | }, 106 | }) 107 | 108 | if err != nil { 109 | return err 110 | } 111 | // monitor the returned channel, sending incoming agent messages down the pipe 112 | go func() { 113 | for { 114 | select { 115 | case entry, ok := <-ch: 116 | if !ok { 117 | return 118 | } 119 | if e, ok := entry.Entry.(*ChatEntry_AgentMsg); ok { 120 | stream.Send(&ChatCustomerResponse{ 121 | Resp: &ChatCustomerResponse_Msg{ 122 | Msg: e.AgentMsg, 123 | }, 124 | }) 125 | } 126 | case <-ctx.Done(): 127 | return 128 | } 129 | } 130 | }() 131 | 132 | case *ChatCustomerRequest_Msg: 133 | if sess == nil { 134 | return status.Errorf(codes.FailedPrecondition, "never called init, no chat session for message") 135 | } 136 | 137 | entry := &ChatEntry{ 138 | Date: timestamppb.Now(), 139 | Entry: &ChatEntry_CustomerMsg{ 140 | CustomerMsg: req.Msg, 141 | }, 142 | } 143 | func() { 144 | sess.mu.Lock() 145 | sess.Session.History = append(sess.Session.History, entry) 146 | sess.mu.Unlock() 147 | 148 | sess.mu.RLock() 149 | defer sess.mu.RUnlock() 150 | for _, l := range sess.agents { 151 | l.send(entry) 152 | } 153 | }() 154 | 155 | case *ChatCustomerRequest_HangUp: 156 | if sess == nil { 157 | return status.Errorf(codes.FailedPrecondition, "never called init, no chat session to hang up") 158 | } 159 | s.closeSession(sess) 160 | cleanup() 161 | sess = nil 162 | 163 | default: 164 | return status.Error(codes.InvalidArgument, "unknown request type") 165 | } 166 | } 167 | } 168 | 169 | func (s *chatServer) ChatAgent(stream Support_ChatAgentServer) error { 170 | ctx, cancel := context.WithCancel(stream.Context()) 171 | defer cancel() 172 | 173 | agent := getAgent(ctx) 174 | if agent == "" { 175 | return status.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 176 | } 177 | 178 | var sess *session 179 | var ch chan *ChatEntry 180 | var chCancel context.CancelFunc 181 | cleanup := func() { 182 | if sess != nil { 183 | sess.mu.Lock() 184 | delete(sess.agents, agent) 185 | if len(sess.agents) == 0 { 186 | s.mu.Lock() 187 | s.chatsAwaitingAgent = append(s.chatsAwaitingAgent, sess.SessionId) 188 | s.mu.Unlock() 189 | } 190 | sess.mu.Unlock() 191 | chCancel() 192 | close(ch) 193 | go func() { 194 | // drain channel to prevent deadlock 195 | for range ch { 196 | } 197 | }() 198 | } 199 | } 200 | defer cleanup() 201 | 202 | checkSession := func() { 203 | // see if session was concurrently closed 204 | if sess != nil { 205 | sess.mu.RLock() 206 | active := sess.active 207 | sess.mu.RUnlock() 208 | if !active { 209 | cleanup() 210 | sess = nil 211 | } 212 | } 213 | } 214 | 215 | for { 216 | req, err := stream.Recv() 217 | if err != nil { 218 | if err == io.EOF { 219 | return nil 220 | } 221 | return err 222 | } 223 | 224 | checkSession() 225 | 226 | switch req := req.Req.(type) { 227 | case *ChatAgentRequest_Accept: 228 | if sess != nil { 229 | return status.Errorf(codes.FailedPrecondition, "already called accept, currently in chat session %q", sess.SessionId) 230 | } 231 | sess, ch, chCancel = s.acceptSession(ctx, agent, req.Accept.SessionId) 232 | if sess == nil { 233 | return status.Errorf(codes.FailedPrecondition, "no session to accept") 234 | } 235 | err := stream.Send(&ChatAgentResponse{ 236 | Resp: &ChatAgentResponse_AcceptedSession{ 237 | AcceptedSession: sess.copySession(), 238 | }, 239 | }) 240 | if err != nil { 241 | return err 242 | } 243 | // monitor the returned channel, sending incoming agent messages down the pipe 244 | go func() { 245 | for { 246 | select { 247 | case entry, ok := <-ch: 248 | if !ok { 249 | return 250 | } 251 | 252 | if entry == nil { 253 | stream.Send(&ChatAgentResponse{ 254 | Resp: &ChatAgentResponse_SessionEnded{ 255 | SessionEnded: Void_VOID, 256 | }, 257 | }) 258 | continue 259 | } 260 | 261 | if agentMsg, ok := entry.Entry.(*ChatEntry_AgentMsg); ok { 262 | if agentMsg.AgentMsg.AgentName == agent { 263 | continue 264 | } 265 | } 266 | stream.Send(&ChatAgentResponse{ 267 | Resp: &ChatAgentResponse_Msg{ 268 | Msg: entry, 269 | }, 270 | }) 271 | case <-ctx.Done(): 272 | return 273 | } 274 | } 275 | }() 276 | 277 | case *ChatAgentRequest_Msg: 278 | if sess == nil { 279 | return status.Errorf(codes.FailedPrecondition, "never called accept, no chat session for message") 280 | } 281 | 282 | entry := &ChatEntry{ 283 | Date: timestamppb.Now(), 284 | Entry: &ChatEntry_AgentMsg{ 285 | AgentMsg: &AgentMessage{ 286 | AgentName: agent, 287 | Msg: req.Msg, 288 | }, 289 | }, 290 | } 291 | active := true 292 | func() { 293 | sess.mu.Lock() 294 | active = sess.active 295 | if active { 296 | sess.Session.History = append(sess.Session.History, entry) 297 | } 298 | sess.mu.Unlock() 299 | 300 | if !active { 301 | return 302 | } 303 | 304 | sess.mu.RLock() 305 | defer sess.mu.RUnlock() 306 | if sess.cust != nil { 307 | sess.cust.send(entry) 308 | } 309 | for otherAgent, l := range sess.agents { 310 | if otherAgent == agent { 311 | continue 312 | } 313 | l.send(entry) 314 | } 315 | }() 316 | if !active { 317 | return status.Errorf(codes.FailedPrecondition, "customer hung up on chat session %s", sess.SessionId) 318 | } 319 | 320 | case *ChatAgentRequest_LeaveSession: 321 | if sess == nil { 322 | return status.Errorf(codes.FailedPrecondition, "never called init, no chat session to hang up") 323 | } 324 | s.closeSession(sess) 325 | cleanup() 326 | sess = nil 327 | 328 | default: 329 | return status.Error(codes.InvalidArgument, "unknown request type") 330 | } 331 | } 332 | } 333 | 334 | func (s *chatServer) newSession(ctx context.Context, cust string) (*session, chan *ChatEntry, context.CancelFunc) { 335 | s.mu.Lock() 336 | defer s.mu.Unlock() 337 | s.lastSession++ 338 | id := fmt.Sprintf("%06d", s.lastSession) 339 | s.chatsAwaitingAgent = append(s.chatsAwaitingAgent, id) 340 | 341 | ch := make(chan *ChatEntry, 1) 342 | ctx, cancel := context.WithCancel(ctx) 343 | l := &listener{ 344 | ch: ch, 345 | ctx: ctx, 346 | } 347 | sess := session{ 348 | active: true, 349 | Session: Session{ 350 | SessionId: id, 351 | CustomerName: cust, 352 | }, 353 | cust: l, 354 | } 355 | s.chatsBySession[id] = &sess 356 | 357 | return &sess, ch, cancel 358 | } 359 | 360 | func (s *chatServer) resumeSession(ctx context.Context, cust, sessionID string) (*session, chan *ChatEntry, context.CancelFunc) { 361 | s.mu.Lock() 362 | defer s.mu.Unlock() 363 | sess := s.chatsBySession[sessionID] 364 | if sess.CustomerName != cust { 365 | // customer cannot join chat that they did not start 366 | return nil, nil, nil 367 | } 368 | if !sess.active { 369 | // chat has been closed 370 | return nil, nil, nil 371 | } 372 | if sess.cust != nil { 373 | // customer is active in the chat in another stream! 374 | return nil, nil, nil 375 | } 376 | 377 | ch := make(chan *ChatEntry, 1) 378 | ctx, cancel := context.WithCancel(ctx) 379 | l := &listener{ 380 | ch: ch, 381 | ctx: ctx, 382 | } 383 | sess.cust = l 384 | return sess, ch, cancel 385 | } 386 | 387 | func (s *chatServer) closeSession(sess *session) { 388 | active := true 389 | func() { 390 | sess.mu.Lock() 391 | active = sess.active 392 | sess.active = false 393 | sess.mu.Unlock() 394 | 395 | if !active { 396 | // already closed 397 | return 398 | } 399 | 400 | sess.mu.RLock() 401 | defer sess.mu.RUnlock() 402 | for _, l := range sess.agents { 403 | l.send(nil) 404 | } 405 | }() 406 | 407 | if !active { 408 | // already closed 409 | return 410 | } 411 | 412 | s.mu.Lock() 413 | defer s.mu.Unlock() 414 | delete(s.chatsBySession, sess.SessionId) 415 | for i, id := range s.chatsAwaitingAgent { 416 | if id == sess.SessionId { 417 | s.chatsAwaitingAgent = append(s.chatsAwaitingAgent[:i], s.chatsAwaitingAgent[i+1:]...) 418 | break 419 | } 420 | } 421 | } 422 | 423 | func (s *chatServer) acceptSession(ctx context.Context, agent, sessionID string) (*session, chan *ChatEntry, context.CancelFunc) { 424 | var sess *session 425 | func() { 426 | s.mu.Lock() 427 | defer s.mu.Unlock() 428 | 429 | if len(s.chatsAwaitingAgent) == 0 { 430 | return 431 | } 432 | if sessionID == "" { 433 | sessionID = s.chatsAwaitingAgent[0] 434 | s.chatsAwaitingAgent = s.chatsAwaitingAgent[1:] 435 | } else { 436 | found := false 437 | for i, id := range s.chatsAwaitingAgent { 438 | if id == sessionID { 439 | found = true 440 | s.chatsAwaitingAgent = append(s.chatsAwaitingAgent[:i], s.chatsAwaitingAgent[i+1:]...) 441 | break 442 | } 443 | } 444 | if !found { 445 | return 446 | } 447 | } 448 | sess = s.chatsBySession[sessionID] 449 | }() 450 | if sess == nil { 451 | return nil, nil, nil 452 | } 453 | ch := make(chan *ChatEntry, 1) 454 | ctx, cancel := context.WithCancel(ctx) 455 | l := &listener{ 456 | ch: ch, 457 | ctx: ctx, 458 | } 459 | sess.mu.Lock() 460 | if sess.agents == nil { 461 | sess.agents = map[string]*listener{} 462 | } 463 | sess.agents[agent] = l 464 | sess.mu.Unlock() 465 | return sess, ch, cancel 466 | } 467 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/types/known/timestamppb" 11 | ) 12 | 13 | // In-memory database that is periodically saved to a JSON file. 14 | 15 | type accounts struct { 16 | AccountNumbersByCustomer map[string][]uint64 17 | AccountsByNumber map[uint64]*account 18 | AccountNumbers []uint64 19 | Customers []string 20 | LastAccountNum uint64 21 | mu sync.RWMutex 22 | } 23 | 24 | func (a *accounts) openAccount(customer string, accountType Account_Type, initialBalanceCents int32) *Account { 25 | a.mu.Lock() 26 | defer a.mu.Unlock() 27 | 28 | accountNums, ok := a.AccountNumbersByCustomer[customer] 29 | if !ok { 30 | // no accounts for this customer? it's a new customer 31 | a.Customers = append(a.Customers, customer) 32 | } 33 | num := a.LastAccountNum + 1 34 | a.LastAccountNum = num 35 | a.AccountNumbers = append(a.AccountNumbers, num) 36 | accountNums = append(accountNums, num) 37 | a.AccountNumbersByCustomer[customer] = accountNums 38 | var acct account 39 | acct.AccountNumber = num 40 | acct.Type = accountType 41 | acct.BalanceCents = initialBalanceCents 42 | acct.Transactions = append(acct.Transactions, &Transaction{ 43 | AccountNumber: num, 44 | SeqNumber: 1, 45 | Date: timestamppb.Now(), 46 | AmountCents: initialBalanceCents, 47 | Desc: "initial deposit", 48 | }) 49 | a.AccountsByNumber[num] = &acct 50 | return &acct.Account 51 | } 52 | 53 | func (a *accounts) closeAccount(customer string, accountNumber uint64) error { 54 | a.mu.Lock() 55 | defer a.mu.Unlock() 56 | 57 | acctNums := a.AccountNumbersByCustomer[customer] 58 | found := -1 59 | for i, num := range acctNums { 60 | if num == accountNumber { 61 | found = i 62 | break 63 | } 64 | } 65 | if found == -1 { 66 | return status.Errorf(codes.NotFound, "you have no account numbered %d", accountNumber) 67 | } 68 | 69 | acct := a.AccountsByNumber[accountNumber] 70 | if acct.BalanceCents != 0 { 71 | return status.Errorf(codes.FailedPrecondition, "account %d cannot be closed because it has a non-zero balance: %s", accountNumber, dollars(acct.BalanceCents)) 72 | } 73 | 74 | for i, num := range a.AccountNumbers { 75 | if num == accountNumber { 76 | a.AccountNumbers = append(a.AccountNumbers[:i], a.AccountNumbers[i+1:]...) 77 | break 78 | } 79 | } 80 | 81 | a.AccountNumbersByCustomer[customer] = append(acctNums[:found], acctNums[found+1:]...) 82 | delete(a.AccountsByNumber, accountNumber) 83 | return nil 84 | } 85 | 86 | func (a *accounts) getAccount(customer string, accountNumber uint64) (*account, error) { 87 | a.mu.RLock() 88 | defer a.mu.RUnlock() 89 | acctNums := a.AccountNumbersByCustomer[customer] 90 | for _, num := range acctNums { 91 | if num == accountNumber { 92 | return a.AccountsByNumber[num], nil 93 | } 94 | } 95 | return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", accountNumber) 96 | } 97 | 98 | func (a *accounts) getAllAccounts(customer string) []*Account { 99 | a.mu.RLock() 100 | defer a.mu.RUnlock() 101 | 102 | accountNums := a.AccountNumbersByCustomer[customer] 103 | var accounts []*Account 104 | for _, num := range accountNums { 105 | accounts = append(accounts, &a.AccountsByNumber[num].Account) 106 | } 107 | return accounts 108 | } 109 | 110 | type account struct { 111 | Account 112 | Transactions []*Transaction 113 | mu sync.RWMutex 114 | } 115 | 116 | func (a *account) getTransactions() []*Transaction { 117 | a.mu.RLock() 118 | defer a.mu.RUnlock() 119 | return a.Transactions 120 | } 121 | 122 | func (a *account) newTransaction(amountCents int32, desc string) (newBalance int32, err error) { 123 | a.mu.Lock() 124 | defer a.mu.Unlock() 125 | bal := a.BalanceCents + amountCents 126 | if bal < 0 { 127 | return 0, status.Errorf(codes.FailedPrecondition, "insufficient funds: cannot withdraw %s when balance is %s", dollars(amountCents), dollars(a.BalanceCents)) 128 | } 129 | a.BalanceCents = bal 130 | a.Transactions = append(a.Transactions, &Transaction{ 131 | AccountNumber: a.AccountNumber, 132 | Date: timestamppb.Now(), 133 | AmountCents: amountCents, 134 | SeqNumber: uint64(len(a.Transactions) + 1), 135 | Desc: desc, 136 | }) 137 | return bal, nil 138 | } 139 | 140 | func (t *Transaction) MarshalJSON() ([]byte, error) { 141 | return protojson.Marshal(t) 142 | } 143 | 144 | func (t *Transaction) UnmarshalJSON(b []byte) error { 145 | return protojson.Unmarshal(b, t) 146 | } 147 | 148 | func (a *accounts) clone() *accounts { 149 | var clone accounts 150 | clone.AccountNumbersByCustomer = map[string][]uint64{} 151 | clone.AccountsByNumber = map[uint64]*account{} 152 | 153 | a.mu.RLock() 154 | clone.Customers = a.Customers 155 | a.mu.RUnlock() 156 | 157 | for _, cust := range clone.Customers { 158 | var acctNums []uint64 159 | a.mu.RLock() 160 | acctNums = a.AccountNumbersByCustomer[cust] 161 | a.mu.RUnlock() 162 | 163 | clone.AccountNumbersByCustomer[cust] = acctNums 164 | clone.AccountNumbers = append(clone.AccountNumbers, acctNums...) 165 | 166 | for _, acctNum := range acctNums { 167 | a.mu.RLock() 168 | acct := a.AccountsByNumber[acctNum] 169 | a.mu.RUnlock() 170 | 171 | acct.mu.RLock() 172 | txns := acct.Transactions 173 | acct.mu.RUnlock() 174 | 175 | clone.AccountsByNumber[acctNum] = &account{Transactions: txns} 176 | } 177 | } 178 | 179 | return &clone 180 | } 181 | 182 | func dollars(amountCents int32) string { 183 | return fmt.Sprintf("$%02f", float64(amountCents)/100) 184 | } 185 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate protoc --go_out=. --go-grpc_out=. bank.proto support.proto 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "sync/atomic" 14 | "syscall" 15 | "time" 16 | 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/grpclog" 19 | "google.golang.org/grpc/peer" 20 | "google.golang.org/grpc/reflection" 21 | "google.golang.org/grpc/status" 22 | ) 23 | 24 | func main() { 25 | grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stderr)) 26 | 27 | port := flag.Int("port", 12345, "The port on which bankdemo gRPC server will listen.") 28 | datafile := flag.String("datafile", "accounts.json", "The path and filename to which bank account data is saved and from which data will be loaded.") 29 | flag.Parse() 30 | 31 | // create the server and load initial dataset 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | s := &svr{ 34 | datafile: *datafile, 35 | ctx: ctx, 36 | cancel: cancel, 37 | } 38 | if err := s.load(); err != nil { 39 | panic(err) 40 | } 41 | 42 | l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | grpcSvr := gRPCServer() 48 | 49 | // Register gRPC service implementations 50 | bankSvc := bankServer{ 51 | allAccounts: &s.allAccounts, 52 | } 53 | RegisterBankServer(grpcSvr, &bankSvc) 54 | 55 | chatSvc := chatServer{ 56 | chatsBySession: map[string]*session{}, 57 | } 58 | RegisterSupportServer(grpcSvr, &chatSvc) 59 | 60 | go s.bgSaver() 61 | 62 | // don't forget to include server reflection support! 63 | reflection.Register(grpcSvr) 64 | 65 | defer func() { 66 | cancel() 67 | s.flush() 68 | }() 69 | 70 | // trap SIGINT / SIGTERM to exit cleanly 71 | c := make(chan os.Signal, 1) 72 | signal.Notify(c, syscall.SIGINT) 73 | signal.Notify(c, syscall.SIGTERM) 74 | go func() { 75 | <-c 76 | fmt.Println("Shutting down...") 77 | grpcSvr.GracefulStop() 78 | }() 79 | 80 | grpclog.Infof("server starting, listening on %v", l.Addr()) 81 | if err := grpcSvr.Serve(l); err != nil { 82 | panic(err) 83 | } 84 | } 85 | 86 | func gRPCServer() *grpc.Server { 87 | var reqCounter uint64 88 | return grpc.NewServer( 89 | grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 90 | reqID := atomic.AddUint64(&reqCounter, 1) 91 | var client string 92 | if p, ok := peer.FromContext(ctx); ok { 93 | client = p.Addr.String() 94 | } else { 95 | client = "?" 96 | } 97 | grpclog.Infof("request %d started for %s from %s", reqID, info.FullMethod, client) 98 | 99 | rsp, err := handler(ctx, req) 100 | 101 | stat, _ := status.FromError(err) 102 | grpclog.Infof("request %d completed for %s from %s: %v %s", reqID, info.FullMethod, client, stat.Code(), stat.Message()) 103 | return rsp, err 104 | 105 | }), 106 | grpc.StreamInterceptor(func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 107 | reqID := atomic.AddUint64(&reqCounter, 1) 108 | var client string 109 | if p, ok := peer.FromContext(ss.Context()); ok { 110 | client = p.Addr.String() 111 | } else { 112 | client = "?" 113 | } 114 | grpclog.Infof("request %d started for %s from %s", reqID, info.FullMethod, client) 115 | 116 | err := handler(srv, ss) 117 | 118 | stat, _ := status.FromError(err) 119 | grpclog.Infof("request %d completed for %s from %s: %v %s", reqID, info.FullMethod, client, stat.Code(), stat.Message()) 120 | return err 121 | })) 122 | } 123 | 124 | type svr struct { 125 | datafile string 126 | ctx context.Context 127 | cancel context.CancelFunc 128 | allAccounts accounts 129 | } 130 | 131 | func (s *svr) load() error { 132 | accts, err := os.ReadFile(s.datafile) 133 | if err != nil && !os.IsNotExist(err) { 134 | return err 135 | } 136 | if len(accts) == 0 { 137 | s.allAccounts.AccountNumbersByCustomer = map[string][]uint64{} 138 | s.allAccounts.AccountsByNumber = map[uint64]*account{} 139 | } else if err := json.Unmarshal(accts, &s.allAccounts); err != nil { 140 | return err 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (s *svr) bgSaver() { 147 | ticker := time.NewTicker(5 * time.Second) 148 | for { 149 | select { 150 | case <-ticker.C: 151 | s.flush() 152 | case <-s.ctx.Done(): 153 | ticker.Stop() 154 | return 155 | } 156 | } 157 | } 158 | 159 | func (s *svr) flush() { 160 | accounts := s.allAccounts.clone() 161 | 162 | if b, err := json.Marshal(accounts); err != nil { 163 | grpclog.Errorf("failed to save data to %q", s.datafile) 164 | } else if err := os.WriteFile(s.datafile, b, 0666); err != nil { 165 | grpclog.Errorf("failed to save data to %q", s.datafile) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/support.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = ".;main"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | // Support provides an interactive chat service, for customers to interact with 8 | // the bank's support agents. A single stream, for either of the two methods, is 9 | // a stateful connection to a single "chat session". Streams are initially disconnected 10 | // (not part of any session). A stream must be disconnected from a session (via customer 11 | // hang up or via agent leaving a session) before it can be connected to a new one. 12 | service Support { 13 | // ChatCustomer is used by a customer-facing app to send the customer's messages 14 | // to a chat session. The customer is how initiates and terminates (via "hangup") 15 | // a chat session. Only customers may invoke this method (e.g. requests must 16 | // include customer auth credentials). 17 | rpc ChatCustomer(stream ChatCustomerRequest) returns (stream ChatCustomerResponse); 18 | // ChatAgent is used by an agent-facing app to allow an agent to reply to a 19 | // customer's messages in a chat session. The agent may accept a chat session, 20 | // which defaults to the session awaiting an agent for the longest period of time 21 | // (FIFO queue). 22 | rpc ChatAgent(stream ChatAgentRequest) returns (stream ChatAgentResponse); 23 | } 24 | 25 | enum Void { 26 | VOID = 0; 27 | } 28 | 29 | message ChatCustomerRequest { 30 | oneof req { 31 | // init is used when a chat stream is not part of a 32 | // chat session. This is a stream's initial state, as well as 33 | // the state after a "hang_up" request is sent. This creates 34 | // a new state session or resumes an existing one. 35 | InitiateChat init = 1; 36 | // msg is used to send the customer's messages to support 37 | // agents. 38 | string msg = 2; 39 | // hang_up is used to terminate a chat session. If a stream 40 | // is broken, but the session was not terminated, the client 41 | // may initiate a new stream and use init to resume that 42 | // session. Sessions are not terminated unless done so 43 | // explicitly via sending this kind of request on the stream. 44 | Void hang_up = 3; 45 | } 46 | } 47 | 48 | message InitiateChat { 49 | string resume_session_id = 1; 50 | } 51 | 52 | message AgentMessage { 53 | string agent_name = 1; 54 | string msg = 2; 55 | } 56 | 57 | message ChatCustomerResponse { 58 | oneof resp { 59 | // session is sent from the server when the stream is connected 60 | // to a chat session. This happens after an init request is sent 61 | // and the stream is connected to either a new or resumed session. 62 | Session session = 1; 63 | // msg is sent from the server to convey agents' messages to the 64 | // customer. 65 | AgentMessage msg = 2; 66 | } 67 | } 68 | 69 | message ChatAgentRequest { 70 | oneof req { 71 | // accept is used when an agent wants to join a customer chat 72 | // session. It can be used to connect to a specific session (by 73 | // ID), or to just accept the session for which the customer has 74 | // been waiting the longest (e.g. poll a FIFO queue of sessions 75 | // awaiting a support agent). It is possible for multiple agents 76 | // to be connected to the same chat session. 77 | AcceptChat accept = 1; 78 | // msg is used to send a message to the customer. It will also be 79 | // delivered to any other connected support agents. 80 | string msg = 2; 81 | // leave_session allows an agent to exit a chat session. They can 82 | // always re-enter later by sending an accept message for that 83 | // session ID. 84 | Void leave_session = 3; 85 | } 86 | } 87 | 88 | message AcceptChat { 89 | string session_id = 1; 90 | } 91 | 92 | message ChatEntry { 93 | google.protobuf.Timestamp date = 1; 94 | oneof entry { 95 | string customer_msg = 2; 96 | AgentMessage agent_msg = 3; 97 | } 98 | } 99 | 100 | message ChatAgentResponse { 101 | oneof resp { 102 | // accepted_session provides the detail of a chat session. The server 103 | // sends this message after the agent has accepted a chat session. 104 | Session accepted_session = 1; 105 | // msg is sent by the server when the customer, or another support 106 | // agent, sends a message in stream's current session. 107 | ChatEntry msg = 2; 108 | // session_ended notifies the support agent that their currently 109 | // connected chat session has been terminated by the customer. 110 | Void session_ended = 3; 111 | } 112 | } 113 | 114 | message Session { 115 | string session_id = 1; 116 | string customer_name = 2; 117 | repeated ChatEntry history = 3; 118 | } 119 | -------------------------------------------------------------------------------- /internal/testing/cmd/bankdemo/support_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // SupportClient is the client API for Support service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type SupportClient interface { 21 | // ChatCustomer is used by a customer-facing app to send the customer's messages 22 | // to a chat session. The customer is how initiates and terminates (via "hangup") 23 | // a chat session. Only customers may invoke this method (e.g. requests must 24 | // include customer auth credentials). 25 | ChatCustomer(ctx context.Context, opts ...grpc.CallOption) (Support_ChatCustomerClient, error) 26 | // ChatAgent is used by an agent-facing app to allow an agent to reply to a 27 | // customer's messages in a chat session. The agent may accept a chat session, 28 | // which defaults to the session awaiting an agent for the longest period of time 29 | // (FIFO queue). 30 | ChatAgent(ctx context.Context, opts ...grpc.CallOption) (Support_ChatAgentClient, error) 31 | } 32 | 33 | type supportClient struct { 34 | cc grpc.ClientConnInterface 35 | } 36 | 37 | func NewSupportClient(cc grpc.ClientConnInterface) SupportClient { 38 | return &supportClient{cc} 39 | } 40 | 41 | func (c *supportClient) ChatCustomer(ctx context.Context, opts ...grpc.CallOption) (Support_ChatCustomerClient, error) { 42 | stream, err := c.cc.NewStream(ctx, &Support_ServiceDesc.Streams[0], "/Support/ChatCustomer", opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | x := &supportChatCustomerClient{stream} 47 | return x, nil 48 | } 49 | 50 | type Support_ChatCustomerClient interface { 51 | Send(*ChatCustomerRequest) error 52 | Recv() (*ChatCustomerResponse, error) 53 | grpc.ClientStream 54 | } 55 | 56 | type supportChatCustomerClient struct { 57 | grpc.ClientStream 58 | } 59 | 60 | func (x *supportChatCustomerClient) Send(m *ChatCustomerRequest) error { 61 | return x.ClientStream.SendMsg(m) 62 | } 63 | 64 | func (x *supportChatCustomerClient) Recv() (*ChatCustomerResponse, error) { 65 | m := new(ChatCustomerResponse) 66 | if err := x.ClientStream.RecvMsg(m); err != nil { 67 | return nil, err 68 | } 69 | return m, nil 70 | } 71 | 72 | func (c *supportClient) ChatAgent(ctx context.Context, opts ...grpc.CallOption) (Support_ChatAgentClient, error) { 73 | stream, err := c.cc.NewStream(ctx, &Support_ServiceDesc.Streams[1], "/Support/ChatAgent", opts...) 74 | if err != nil { 75 | return nil, err 76 | } 77 | x := &supportChatAgentClient{stream} 78 | return x, nil 79 | } 80 | 81 | type Support_ChatAgentClient interface { 82 | Send(*ChatAgentRequest) error 83 | Recv() (*ChatAgentResponse, error) 84 | grpc.ClientStream 85 | } 86 | 87 | type supportChatAgentClient struct { 88 | grpc.ClientStream 89 | } 90 | 91 | func (x *supportChatAgentClient) Send(m *ChatAgentRequest) error { 92 | return x.ClientStream.SendMsg(m) 93 | } 94 | 95 | func (x *supportChatAgentClient) Recv() (*ChatAgentResponse, error) { 96 | m := new(ChatAgentResponse) 97 | if err := x.ClientStream.RecvMsg(m); err != nil { 98 | return nil, err 99 | } 100 | return m, nil 101 | } 102 | 103 | // SupportServer is the server API for Support service. 104 | // All implementations must embed UnimplementedSupportServer 105 | // for forward compatibility 106 | type SupportServer interface { 107 | // ChatCustomer is used by a customer-facing app to send the customer's messages 108 | // to a chat session. The customer is how initiates and terminates (via "hangup") 109 | // a chat session. Only customers may invoke this method (e.g. requests must 110 | // include customer auth credentials). 111 | ChatCustomer(Support_ChatCustomerServer) error 112 | // ChatAgent is used by an agent-facing app to allow an agent to reply to a 113 | // customer's messages in a chat session. The agent may accept a chat session, 114 | // which defaults to the session awaiting an agent for the longest period of time 115 | // (FIFO queue). 116 | ChatAgent(Support_ChatAgentServer) error 117 | mustEmbedUnimplementedSupportServer() 118 | } 119 | 120 | // UnimplementedSupportServer must be embedded to have forward compatible implementations. 121 | type UnimplementedSupportServer struct { 122 | } 123 | 124 | func (UnimplementedSupportServer) ChatCustomer(Support_ChatCustomerServer) error { 125 | return status.Errorf(codes.Unimplemented, "method ChatCustomer not implemented") 126 | } 127 | func (UnimplementedSupportServer) ChatAgent(Support_ChatAgentServer) error { 128 | return status.Errorf(codes.Unimplemented, "method ChatAgent not implemented") 129 | } 130 | func (UnimplementedSupportServer) mustEmbedUnimplementedSupportServer() {} 131 | 132 | // UnsafeSupportServer may be embedded to opt out of forward compatibility for this service. 133 | // Use of this interface is not recommended, as added methods to SupportServer will 134 | // result in compilation errors. 135 | type UnsafeSupportServer interface { 136 | mustEmbedUnimplementedSupportServer() 137 | } 138 | 139 | func RegisterSupportServer(s grpc.ServiceRegistrar, srv SupportServer) { 140 | s.RegisterService(&Support_ServiceDesc, srv) 141 | } 142 | 143 | func _Support_ChatCustomer_Handler(srv interface{}, stream grpc.ServerStream) error { 144 | return srv.(SupportServer).ChatCustomer(&supportChatCustomerServer{stream}) 145 | } 146 | 147 | type Support_ChatCustomerServer interface { 148 | Send(*ChatCustomerResponse) error 149 | Recv() (*ChatCustomerRequest, error) 150 | grpc.ServerStream 151 | } 152 | 153 | type supportChatCustomerServer struct { 154 | grpc.ServerStream 155 | } 156 | 157 | func (x *supportChatCustomerServer) Send(m *ChatCustomerResponse) error { 158 | return x.ServerStream.SendMsg(m) 159 | } 160 | 161 | func (x *supportChatCustomerServer) Recv() (*ChatCustomerRequest, error) { 162 | m := new(ChatCustomerRequest) 163 | if err := x.ServerStream.RecvMsg(m); err != nil { 164 | return nil, err 165 | } 166 | return m, nil 167 | } 168 | 169 | func _Support_ChatAgent_Handler(srv interface{}, stream grpc.ServerStream) error { 170 | return srv.(SupportServer).ChatAgent(&supportChatAgentServer{stream}) 171 | } 172 | 173 | type Support_ChatAgentServer interface { 174 | Send(*ChatAgentResponse) error 175 | Recv() (*ChatAgentRequest, error) 176 | grpc.ServerStream 177 | } 178 | 179 | type supportChatAgentServer struct { 180 | grpc.ServerStream 181 | } 182 | 183 | func (x *supportChatAgentServer) Send(m *ChatAgentResponse) error { 184 | return x.ServerStream.SendMsg(m) 185 | } 186 | 187 | func (x *supportChatAgentServer) Recv() (*ChatAgentRequest, error) { 188 | m := new(ChatAgentRequest) 189 | if err := x.ServerStream.RecvMsg(m); err != nil { 190 | return nil, err 191 | } 192 | return m, nil 193 | } 194 | 195 | // Support_ServiceDesc is the grpc.ServiceDesc for Support service. 196 | // It's only intended for direct use with grpc.RegisterService, 197 | // and not to be introspected or modified (even as a copy) 198 | var Support_ServiceDesc = grpc.ServiceDesc{ 199 | ServiceName: "Support", 200 | HandlerType: (*SupportServer)(nil), 201 | Methods: []grpc.MethodDesc{}, 202 | Streams: []grpc.StreamDesc{ 203 | { 204 | StreamName: "ChatCustomer", 205 | Handler: _Support_ChatCustomer_Handler, 206 | ServerStreams: true, 207 | ClientStreams: true, 208 | }, 209 | { 210 | StreamName: "ChatAgent", 211 | Handler: _Support_ChatAgent_Handler, 212 | ServerStreams: true, 213 | ClientStreams: true, 214 | }, 215 | }, 216 | Metadata: "support.proto", 217 | } 218 | -------------------------------------------------------------------------------- /internal/testing/cmd/testserver/README.md: -------------------------------------------------------------------------------- 1 | # testserver 2 | 3 | The `testserver` program is a simple server that can be used for testing RPC clients such 4 | as `grpcurl`. It implements an RPC interface that is defined in `grpcurl`'s [testing package](https://github.com/fullstorydev/grpcurl/blob/master/testing/example.proto) and also exposes [the implementation](https://godoc.org/github.com/fullstorydev/grpcurl/testing#TestServer) that is defined in that same package. This is the same test interface and implementation that is used in unit tests for `grpcurl`. 5 | 6 | For a possibly more interesting test server, take a look at `bankdemo`, which is a demo gRPC app that provides a more concrete RPC interface, including full-duplex bidirectional streaming methods, plus an example implementation. -------------------------------------------------------------------------------- /internal/testing/cmd/testserver/testserver.go: -------------------------------------------------------------------------------- 1 | // Command testserver spins up a test GRPC server. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | "sync/atomic" 11 | "time" 12 | 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/grpclog" 16 | "google.golang.org/grpc/metadata" 17 | "google.golang.org/grpc/reflection" 18 | "google.golang.org/grpc/status" 19 | 20 | "github.com/fullstorydev/grpcurl" 21 | grpcurl_testing "github.com/fullstorydev/grpcurl/internal/testing" 22 | ) 23 | 24 | var ( 25 | getUnixSocket func() string // nil when run on non-unix platforms 26 | 27 | help = flag.Bool("help", false, "Print usage instructions and exit.") 28 | cacert = flag.String("cacert", "", 29 | `File containing trusted root certificates for verifying client certs. Ignored 30 | if TLS is not in use (e.g. no -cert or -key specified).`) 31 | cert = flag.String("cert", "", 32 | `File containing server certificate (public key). Must also provide -key option. 33 | Server uses plain-text if no -cert and -key options are given.`) 34 | key = flag.String("key", "", 35 | `File containing server private key. Must also provide -cert option. Server uses 36 | plain-text if no -cert and -key options are given.`) 37 | requirecert = flag.Bool("requirecert", false, 38 | `Require clients to authenticate via client certs. Must be using TLS (e.g. must 39 | also provide -cert and -key options).`) 40 | port = flag.Int("p", 0, "Port on which to listen. Ephemeral port used if not specified.") 41 | noreflect = flag.Bool("noreflect", false, "Indicates that server should not support server reflection.") 42 | quiet = flag.Bool("q", false, "Suppresses server request and stream logging.") 43 | ) 44 | 45 | func main() { 46 | flag.Parse() 47 | 48 | if *help { 49 | flag.PrintDefaults() 50 | os.Exit(0) 51 | } 52 | 53 | grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stderr)) 54 | 55 | if len(flag.Args()) > 0 { 56 | fmt.Fprintln(os.Stderr, "No arguments expected.") 57 | os.Exit(2) 58 | } 59 | if (*cert == "") != (*key == "") { 60 | fmt.Fprintln(os.Stderr, "The -cert and -key arguments must be used together and both be present.") 61 | os.Exit(2) 62 | } 63 | if *requirecert && *cert == "" { 64 | fmt.Fprintln(os.Stderr, "The -requirecert arg cannot be used without -cert and -key arguments.") 65 | os.Exit(2) 66 | } 67 | 68 | var opts []grpc.ServerOption 69 | if *cert != "" { 70 | creds, err := grpcurl.ServerTransportCredentials(*cacert, *cert, *key, *requirecert) 71 | if err != nil { 72 | fmt.Fprintf(os.Stderr, "Failed to configure transport credentials: %v\n", err) 73 | os.Exit(1) 74 | } 75 | opts = []grpc.ServerOption{grpc.Creds(creds)} 76 | } 77 | if !*quiet { 78 | opts = append(opts, grpc.UnaryInterceptor(unaryLogger), grpc.StreamInterceptor(streamLogger)) 79 | } 80 | 81 | var network, addr string 82 | if getUnixSocket != nil && getUnixSocket() != "" { 83 | network = "unix" 84 | addr = getUnixSocket() 85 | } else { 86 | network = "tcp" 87 | addr = fmt.Sprintf("127.0.0.1:%d", *port) 88 | } 89 | l, err := net.Listen(network, addr) 90 | if err != nil { 91 | fmt.Fprintf(os.Stderr, "Failed to listen on socket: %v\n", err) 92 | os.Exit(1) 93 | } 94 | fmt.Printf("Listening on %v\n", l.Addr()) 95 | 96 | svr := grpc.NewServer(opts...) 97 | 98 | grpcurl_testing.RegisterTestServiceServer(svr, grpcurl_testing.TestServer{}) 99 | if !*noreflect { 100 | reflection.Register(svr) 101 | } 102 | 103 | if err := svr.Serve(l); err != nil { 104 | fmt.Fprintf(os.Stderr, "GRPC server returned error: %v\n", err) 105 | os.Exit(1) 106 | } 107 | } 108 | 109 | var id int32 110 | 111 | func unaryLogger(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 112 | i := atomic.AddInt32(&id, 1) - 1 113 | grpclog.Infof("start <%d>: %s\n", i, info.FullMethod) 114 | start := time.Now() 115 | rsp, err := handler(ctx, req) 116 | var code codes.Code 117 | if stat, ok := status.FromError(err); ok { 118 | code = stat.Code() 119 | } else { 120 | code = codes.Unknown 121 | } 122 | grpclog.Infof("completed <%d>: %v (%d) %v\n", i, code, code, time.Since(start)) 123 | return rsp, err 124 | } 125 | 126 | func streamLogger(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 127 | i := atomic.AddInt32(&id, 1) - 1 128 | start := time.Now() 129 | grpclog.Infof("start <%d>: %s\n", i, info.FullMethod) 130 | err := handler(srv, loggingStream{ss: ss, id: i}) 131 | var code codes.Code 132 | if stat, ok := status.FromError(err); ok { 133 | code = stat.Code() 134 | } else { 135 | code = codes.Unknown 136 | } 137 | grpclog.Infof("completed <%d>: %v(%d) %v\n", i, code, code, time.Since(start)) 138 | return err 139 | } 140 | 141 | type loggingStream struct { 142 | ss grpc.ServerStream 143 | id int32 144 | } 145 | 146 | func (l loggingStream) SetHeader(md metadata.MD) error { 147 | return l.ss.SetHeader(md) 148 | } 149 | 150 | func (l loggingStream) SendHeader(md metadata.MD) error { 151 | return l.ss.SendHeader(md) 152 | } 153 | 154 | func (l loggingStream) SetTrailer(md metadata.MD) { 155 | l.ss.SetTrailer(md) 156 | } 157 | 158 | func (l loggingStream) Context() context.Context { 159 | return l.ss.Context() 160 | } 161 | 162 | func (l loggingStream) SendMsg(m interface{}) error { 163 | err := l.ss.SendMsg(m) 164 | if err == nil { 165 | grpclog.Infof("stream <%d>: sent message\n", l.id) 166 | } 167 | return err 168 | } 169 | 170 | func (l loggingStream) RecvMsg(m interface{}) error { 171 | err := l.ss.RecvMsg(m) 172 | if err == nil { 173 | grpclog.Infof("stream <%d>: received message\n", l.id) 174 | } 175 | return err 176 | } 177 | -------------------------------------------------------------------------------- /internal/testing/cmd/testserver/unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package main 5 | 6 | import "flag" 7 | 8 | var ( 9 | unix = flag.String("unix", "", 10 | `Use instead of -p to indicate listening on a Unix domain socket instead of a 11 | TCP port. If present, must be the path to a domain socket.`) 12 | ) 13 | 14 | func init() { 15 | getUnixSocket = func() string { 16 | return *unix 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/testing/example.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/descriptor.proto"; 4 | import "google/protobuf/empty.proto"; 5 | import "google/protobuf/timestamp.proto"; 6 | import "example2.proto"; 7 | 8 | message TestRequest { 9 | repeated string file_names = 1; 10 | repeated Extension extensions = 2; 11 | } 12 | 13 | message TestResponse { 14 | map file_protos = 1; 15 | google.protobuf.Timestamp last_update_date = 2; 16 | } 17 | 18 | service TestService { 19 | rpc GetFiles (TestRequest) returns (TestResponse); 20 | rpc Ping (google.protobuf.Empty) returns (google.protobuf.Empty); 21 | } -------------------------------------------------------------------------------- /internal/testing/example.protoset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstorydev/grpcurl/f28d506cea820a059b45cd92fb732f13d3b220ff/internal/testing/example.protoset -------------------------------------------------------------------------------- /internal/testing/example2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | 5 | message Extension { 6 | uint64 id = 1; 7 | google.protobuf.Any data = 2; 8 | } 9 | -------------------------------------------------------------------------------- /internal/testing/jsonpb_test_proto/test_objects.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "google/protobuf/any.proto"; 4 | import "google/protobuf/duration.proto"; 5 | import "google/protobuf/struct.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | import "google/protobuf/wrappers.proto"; 8 | 9 | package jsonpb; 10 | 11 | option go_package=".;jsonpb"; 12 | 13 | message KnownTypes { 14 | optional google.protobuf.Any an = 14; 15 | optional google.protobuf.Duration dur = 1; 16 | optional google.protobuf.Struct st = 12; 17 | optional google.protobuf.Timestamp ts = 2; 18 | optional google.protobuf.ListValue lv = 15; 19 | optional google.protobuf.Value val = 16; 20 | 21 | optional google.protobuf.DoubleValue dbl = 3; 22 | optional google.protobuf.FloatValue flt = 4; 23 | optional google.protobuf.Int64Value i64 = 5; 24 | optional google.protobuf.UInt64Value u64 = 6; 25 | optional google.protobuf.Int32Value i32 = 7; 26 | optional google.protobuf.UInt32Value u32 = 8; 27 | optional google.protobuf.BoolValue bool = 9; 28 | optional google.protobuf.StringValue str = 10; 29 | optional google.protobuf.BytesValue bytes = 11; 30 | } 31 | -------------------------------------------------------------------------------- /internal/testing/test.proto: -------------------------------------------------------------------------------- 1 | // NB: Copied from the gRPC Go repo: google.golang.org/grpc/interop/grpc_testing/test.proto 2 | 3 | // Copyright 2017 gRPC authors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // An integration test service that covers all the method signature permutations 18 | // of unary/streaming requests/responses. 19 | syntax = "proto3"; 20 | 21 | package testing; 22 | 23 | option go_package = ".;testing"; 24 | 25 | message Empty {} 26 | 27 | // The type of payload that should be returned. 28 | enum PayloadType { 29 | // Compressable text format. 30 | COMPRESSABLE = 0; 31 | 32 | // Uncompressable binary format. 33 | UNCOMPRESSABLE = 1; 34 | 35 | // Randomly chosen from all other formats defined in this enum. 36 | RANDOM = 2; 37 | } 38 | 39 | // A block of data, to simply increase gRPC message size. 40 | message Payload { 41 | // The type of data in body. 42 | PayloadType type = 1; 43 | // Primary contents of payload. 44 | bytes body = 2; 45 | } 46 | 47 | // A protobuf representation for grpc status. This is used by test 48 | // clients to specify a status that the server should attempt to return. 49 | message EchoStatus { 50 | int32 code = 1; 51 | string message = 2; 52 | } 53 | 54 | // Unary request. 55 | message SimpleRequest { 56 | // Desired payload type in the response from the server. 57 | // If response_type is RANDOM, server randomly chooses one from other formats. 58 | PayloadType response_type = 1; 59 | 60 | // Desired payload size in the response from the server. 61 | // If response_type is COMPRESSABLE, this denotes the size before compression. 62 | int32 response_size = 2; 63 | 64 | // Optional input payload sent along with the request. 65 | Payload payload = 3; 66 | 67 | // Whether SimpleResponse should include username. 68 | bool fill_username = 4; 69 | 70 | // Whether SimpleResponse should include OAuth scope. 71 | bool fill_oauth_scope = 5; 72 | 73 | // Whether server should return a given status 74 | EchoStatus response_status = 7; 75 | } 76 | 77 | // Unary response, as configured by the request. 78 | message SimpleResponse { 79 | // Payload to increase message size. 80 | Payload payload = 1; 81 | 82 | // The user the request came from, for verifying authentication was 83 | // successful when the client expected it. 84 | string username = 2; 85 | 86 | // OAuth scope. 87 | string oauth_scope = 3; 88 | } 89 | 90 | // Client-streaming request. 91 | message StreamingInputCallRequest { 92 | // Optional input payload sent along with the request. 93 | Payload payload = 1; 94 | 95 | // Not expecting any payload from the response. 96 | } 97 | 98 | // Client-streaming response. 99 | message StreamingInputCallResponse { 100 | // Aggregated size of payloads received from the client. 101 | int32 aggregated_payload_size = 1; 102 | } 103 | 104 | // Configuration for a particular response. 105 | message ResponseParameters { 106 | // Desired payload sizes in responses from the server. 107 | // If response_type is COMPRESSABLE, this denotes the size before compression. 108 | int32 size = 1; 109 | 110 | // Desired interval between consecutive responses in the response stream in 111 | // microseconds. 112 | int32 interval_us = 2; 113 | } 114 | 115 | // Server-streaming request. 116 | message StreamingOutputCallRequest { 117 | // Desired payload type in the response from the server. 118 | // If response_type is RANDOM, the payload from each response in the stream 119 | // might be of different types. This is to simulate a mixed type of payload 120 | // stream. 121 | PayloadType response_type = 1; 122 | 123 | // Configuration for each expected response message. 124 | repeated ResponseParameters response_parameters = 2; 125 | 126 | // Optional input payload sent along with the request. 127 | Payload payload = 3; 128 | 129 | // Whether server should return a given status 130 | EchoStatus response_status = 7; 131 | } 132 | 133 | // Server-streaming response, as configured by the request and parameters. 134 | message StreamingOutputCallResponse { 135 | // Payload to increase response size. 136 | Payload payload = 1; 137 | } 138 | 139 | // A simple service to test the various types of RPCs and experiment with 140 | // performance with various types of payload. 141 | service TestService { 142 | // One empty request followed by one empty response. 143 | rpc EmptyCall(Empty) returns (Empty); 144 | 145 | // One request followed by one response. 146 | // The server returns the client payload as-is. 147 | rpc UnaryCall(SimpleRequest) returns (SimpleResponse); 148 | 149 | // One request followed by a sequence of responses (streamed download). 150 | // The server returns the payload with client desired type and sizes. 151 | rpc StreamingOutputCall(StreamingOutputCallRequest) 152 | returns (stream StreamingOutputCallResponse); 153 | 154 | // A sequence of requests followed by one response (streamed upload). 155 | // The server returns the aggregated size of client payload as the result. 156 | rpc StreamingInputCall(stream StreamingInputCallRequest) 157 | returns (StreamingInputCallResponse); 158 | 159 | // A sequence of requests with each request served by the server immediately. 160 | // As one request could lead to multiple responses, this interface 161 | // demonstrates the idea of full duplexing. 162 | rpc FullDuplexCall(stream StreamingOutputCallRequest) 163 | returns (stream StreamingOutputCallResponse); 164 | 165 | // A sequence of requests followed by a sequence of responses. 166 | // The server buffers all the client requests and then serves them in order. A 167 | // stream of responses are returned to the client when the server starts with 168 | // first request. 169 | rpc HalfDuplexCall(stream StreamingOutputCallRequest) 170 | returns (stream StreamingOutputCallResponse); 171 | } 172 | 173 | // A simple service NOT implemented at servers so clients can test for 174 | // that case. 175 | service UnimplementedService { 176 | // A call that no server should implement 177 | rpc UnimplementedCall(Empty) returns (Empty); 178 | } 179 | -------------------------------------------------------------------------------- /internal/testing/test.protoset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstorydev/grpcurl/f28d506cea820a059b45cd92fb732f13d3b220ff/internal/testing/test.protoset -------------------------------------------------------------------------------- /internal/testing/test_server.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | //go:generate protoc --go_out=. --go-grpc_out=. test.proto 4 | //go:generate protoc --descriptor_set_out=./test.protoset test.proto 5 | //go:generate protoc --descriptor_set_out=./example.protoset --include_imports example.proto 6 | 7 | import ( 8 | "context" 9 | "io" 10 | "strconv" 11 | "time" 12 | 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/metadata" 16 | "google.golang.org/grpc/status" 17 | 18 | "github.com/fullstorydev/grpcurl" 19 | ) 20 | 21 | // TestServer implements the TestService interface defined in example.proto. 22 | type TestServer struct { 23 | UnimplementedTestServiceServer 24 | } 25 | 26 | // EmptyCall accepts one empty request and issues one empty response. 27 | func (TestServer) EmptyCall(ctx context.Context, req *Empty) (*Empty, error) { 28 | headers, trailers, failEarly, failLate := processMetadata(ctx) 29 | grpc.SetHeader(ctx, headers) 30 | grpc.SetTrailer(ctx, trailers) 31 | if failEarly != codes.OK { 32 | return nil, status.Error(failEarly, "fail") 33 | } 34 | if failLate != codes.OK { 35 | return nil, status.Error(failLate, "fail") 36 | } 37 | 38 | return req, nil 39 | } 40 | 41 | // UnaryCall accepts one request and issues one response. The response includes 42 | // the client's payload as-is. 43 | func (TestServer) UnaryCall(ctx context.Context, req *SimpleRequest) (*SimpleResponse, error) { 44 | headers, trailers, failEarly, failLate := processMetadata(ctx) 45 | grpc.SetHeader(ctx, headers) 46 | grpc.SetTrailer(ctx, trailers) 47 | if failEarly != codes.OK { 48 | return nil, status.Error(failEarly, "fail") 49 | } 50 | if failLate != codes.OK { 51 | return nil, status.Error(failLate, "fail") 52 | } 53 | 54 | return &SimpleResponse{ 55 | Payload: req.Payload, 56 | }, nil 57 | } 58 | 59 | // StreamingOutputCall accepts one request and issues a sequence of responses 60 | // (streamed download). The server returns the payload with client desired type 61 | // and sizes as specified in the request's ResponseParameters. 62 | func (TestServer) StreamingOutputCall(req *StreamingOutputCallRequest, str TestService_StreamingOutputCallServer) error { 63 | headers, trailers, failEarly, failLate := processMetadata(str.Context()) 64 | str.SetHeader(headers) 65 | str.SetTrailer(trailers) 66 | if failEarly != codes.OK { 67 | return status.Error(failEarly, "fail") 68 | } 69 | 70 | rsp := &StreamingOutputCallResponse{Payload: &Payload{}} 71 | for _, param := range req.ResponseParameters { 72 | if str.Context().Err() != nil { 73 | return str.Context().Err() 74 | } 75 | delayMicros := int64(param.GetIntervalUs()) * int64(time.Microsecond) 76 | if delayMicros > 0 { 77 | time.Sleep(time.Duration(delayMicros)) 78 | } 79 | sz := int(param.GetSize()) 80 | buf := make([]byte, sz) 81 | for i := 0; i < sz; i++ { 82 | buf[i] = byte(i) 83 | } 84 | rsp.Payload.Type = req.ResponseType 85 | rsp.Payload.Body = buf 86 | if err := str.Send(rsp); err != nil { 87 | return err 88 | } 89 | } 90 | 91 | if failLate != codes.OK { 92 | return status.Error(failLate, "fail") 93 | } 94 | return nil 95 | } 96 | 97 | // StreamingInputCall accepts a sequence of requests and issues one response 98 | // (streamed upload). The server returns the aggregated size of client payloads 99 | // as the result. 100 | func (TestServer) StreamingInputCall(str TestService_StreamingInputCallServer) error { 101 | headers, trailers, failEarly, failLate := processMetadata(str.Context()) 102 | str.SetHeader(headers) 103 | str.SetTrailer(trailers) 104 | if failEarly != codes.OK { 105 | return status.Error(failEarly, "fail") 106 | } 107 | 108 | sz := 0 109 | for { 110 | if str.Context().Err() != nil { 111 | return str.Context().Err() 112 | } 113 | if req, err := str.Recv(); err != nil { 114 | if err == io.EOF { 115 | break 116 | } 117 | return err 118 | } else { 119 | sz += len(req.Payload.Body) 120 | } 121 | } 122 | if err := str.SendAndClose(&StreamingInputCallResponse{AggregatedPayloadSize: int32(sz)}); err != nil { 123 | return err 124 | } 125 | 126 | if failLate != codes.OK { 127 | return status.Error(failLate, "fail") 128 | } 129 | return nil 130 | } 131 | 132 | // FullDuplexCall accepts a sequence of requests with each request served by the 133 | // server immediately. As one request could lead to multiple responses, this 134 | // interface demonstrates the idea of full duplexing. 135 | func (TestServer) FullDuplexCall(str TestService_FullDuplexCallServer) error { 136 | headers, trailers, failEarly, failLate := processMetadata(str.Context()) 137 | str.SetHeader(headers) 138 | str.SetTrailer(trailers) 139 | if failEarly != codes.OK { 140 | return status.Error(failEarly, "fail") 141 | } 142 | 143 | rsp := &StreamingOutputCallResponse{Payload: &Payload{}} 144 | for { 145 | if str.Context().Err() != nil { 146 | return str.Context().Err() 147 | } 148 | req, err := str.Recv() 149 | if err == io.EOF { 150 | break 151 | } else if err != nil { 152 | return err 153 | } 154 | for _, param := range req.ResponseParameters { 155 | sz := int(param.GetSize()) 156 | buf := make([]byte, sz) 157 | for i := 0; i < sz; i++ { 158 | buf[i] = byte(i) 159 | } 160 | rsp.Payload.Type = req.ResponseType 161 | rsp.Payload.Body = buf 162 | if err := str.Send(rsp); err != nil { 163 | return err 164 | } 165 | } 166 | } 167 | 168 | if failLate != codes.OK { 169 | return status.Error(failLate, "fail") 170 | } 171 | return nil 172 | } 173 | 174 | // HalfDuplexCall accepts a sequence of requests and issues a sequence of 175 | // responses. The server buffers all the client requests and then serves them 176 | // in order. A stream of responses is returned to the client once the client 177 | // half-closes the stream. 178 | func (TestServer) HalfDuplexCall(str TestService_HalfDuplexCallServer) error { 179 | headers, trailers, failEarly, failLate := processMetadata(str.Context()) 180 | str.SetHeader(headers) 181 | str.SetTrailer(trailers) 182 | if failEarly != codes.OK { 183 | return status.Error(failEarly, "fail") 184 | } 185 | 186 | var reqs []*StreamingOutputCallRequest 187 | for { 188 | if str.Context().Err() != nil { 189 | return str.Context().Err() 190 | } 191 | if req, err := str.Recv(); err != nil { 192 | if err == io.EOF { 193 | break 194 | } 195 | return err 196 | } else { 197 | reqs = append(reqs, req) 198 | } 199 | } 200 | rsp := &StreamingOutputCallResponse{} 201 | for _, req := range reqs { 202 | rsp.Payload = req.Payload 203 | if err := str.Send(rsp); err != nil { 204 | return err 205 | } 206 | } 207 | 208 | if failLate != codes.OK { 209 | return status.Error(failLate, "fail") 210 | } 211 | return nil 212 | } 213 | 214 | const ( 215 | // MetadataReplyHeaders is a request header that contains values that will 216 | // be echoed back to the client as response headers. The format of the value 217 | // is "key: val". To have the server reply with more than one response 218 | // header, supply multiple values in request metadata. 219 | MetadataReplyHeaders = "reply-with-headers" 220 | // MetadataReplyTrailers is a request header that contains values that will 221 | // be echoed back to the client as response trailers. Its format its the 222 | // same as MetadataReplyHeaders. 223 | MetadataReplyTrailers = "reply-with-trailers" 224 | // MetadataFailEarly is a request header that, if present and not zero, 225 | // indicates that the RPC should fail immediately with that code. 226 | MetadataFailEarly = "fail-early" 227 | // MetadataFailLate is a request header that, if present and not zero, 228 | // indicates that the RPC should fail at the end with that code. This is 229 | // different from MetadataFailEarly only for streaming calls. An early 230 | // failure means the call to fail before any request stream is read or any 231 | // response stream is generated. A late failure means the entire request and 232 | // response streams will be consumed/processed and only then will the error 233 | // code be sent. 234 | MetadataFailLate = "fail-late" 235 | ) 236 | 237 | func processMetadata(ctx context.Context) (metadata.MD, metadata.MD, codes.Code, codes.Code) { 238 | md, ok := metadata.FromIncomingContext(ctx) 239 | if !ok { 240 | return nil, nil, codes.OK, codes.OK 241 | } 242 | return grpcurl.MetadataFromHeaders(md[MetadataReplyHeaders]), 243 | grpcurl.MetadataFromHeaders(md[MetadataReplyTrailers]), 244 | toCode(md[MetadataFailEarly]), 245 | toCode(md[MetadataFailLate]) 246 | } 247 | 248 | func toCode(vals []string) codes.Code { 249 | if len(vals) == 0 { 250 | return codes.OK 251 | } 252 | i, err := strconv.Atoi(vals[len(vals)-1]) 253 | if err != nil { 254 | return codes.Code(i) 255 | } 256 | return codes.Code(i) 257 | } 258 | 259 | var _ TestServiceServer = TestServer{} 260 | -------------------------------------------------------------------------------- /internal/testing/tls/ca.crl: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIICfDBmAgEBMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAMTAmNhFw0xNzA4MjUx 3 | NTQ1NTNaFw0yNzA4MjUxNTQ1NTNaMACgIzAhMB8GA1UdIwQYMBaAFM0FLuuYBwuA 4 | J+tocRlu+xUuOw6FMA0GCSqGSIb3DQEBCwUAA4ICAQCcN8WJKbvGrunXgRBjSnsM 5 | j/sejaX3CCZPmrXeditekSNMatO0JDXOjyoEvv7s9aZrAf3eFOU3Vr5N7PlbLRdj 6 | tovuKTeVp3ungqMoT70cFEf/7eMlpWMB2GkfpV9LtF5Tb8dOYT3kllqtMKv4TeZo 7 | 2adu+GXdeQsqlz9fDEi0ZV4RBruuO0QyLWXpNrUB6fznUDfE4KVBsAIadjsg+Aew 8 | 6jeTkYuUILWMwBM6MzOG/InTKqXpe4ghMufI9fO+phxY10gz4QQ44ZNOa18OuiJw 9 | IH8MoKzhrgUAPLs135hpdGbDePVw5SIKMHUAU2UEKtozAMVfCW45MZHREDdMV3NA 10 | w5QWDoBYl4jol08Orbccmhu4fbauXmB5Id4IPVgGEGFPpiH/QVyJgZIv1AD2dlRg 11 | Td26iz9I25hyrpEfF1gJMtOsDOklDsUiMo8ncQ3CL+pkKnMjhm54k6OFe0qlGsdO 12 | KSavNlEmW/F9h/gs5kaLeFv0v4JxLh12TY28pCE60yoB/UkuB1a+VTHcP3Fa6uUC 13 | uyv0T0f5yHujaM1isGjI3XGgVgLyJiFxtKMPsMEwRrsrEafqp7JCeLpnIWt1J0C/ 14 | Zz3roGcCGj86Oq5zUjdguHS6Ra+uaX+IMJGohWq1cndzVzNfUNkRIyl8IdkCv9o3 15 | J8fVwBzN6sAu6zWd3BqT4A== 16 | -----END X509 CRL----- 17 | -------------------------------------------------------------------------------- /internal/testing/tls/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2jCCAsKgAwIBAgIBATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJjYTAe 3 | Fw0xNzA4MjUxNTQ1NTJaFw0yNzA4MjUxNTQ1NTNaMA0xCzAJBgNVBAMTAmNhMIIC 4 | IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnjw7iZyn9EtjtK7zT+M59OxS 5 | J43a3kMm11Vnh1Fw8oQ7tH0kQW6COyAwlBhAzWGtfDC6jG7A2n+8mPWoinsxoLA5 6 | viSZrEkWZ4tGxWWZ5y/xWh5NBrHa3Jsg1rZ5dstAl08gJPwl0v32aYkMdtk16K0k 7 | jtdPpKtO98v1N6ea7fvQKdyrAaUHY/BY+onoqmSBGPX6vVV7FGybWDf72J5vcwyA 8 | 07wdaZ7TrUYbD28nhXuc4Dt2KbZbWvkT4OYZ+4c+eiehRFVGtuXXi0cKG0xF516b 9 | DnrSO1/2ZbIB2xmcgKvcay0jLzWqhnsSc+qTqODVLODQAMvrMlY0RUXQPn/WTfAw 10 | aQ+u/j7qIR1KFZcLn7uVq8bCM9g+VeyLjq+6XUvgGL9KhvrR/FA4R4DUka+ZVQqh 11 | s262Qs7pNFdoIIrTsJyPd7/UYWCcQbkCKw0aRoUfBeZgkg4bylcABygZqY9+apRF 12 | NBEhpycAEvWFarr6rosqII9kLm1LpnPNEgSvQ/CIRIHq5z5iKeSvHFYOVxLS44HH 13 | M16Mry5UF/jW7Vg8JY5Jrg5YwyOhdGoOSJ3+c/pbLq1TRkwK9bwEJwGr7FdkrjPZ 14 | uNJ7HQiFn2IaeLlbtzwJ+q3vGSFEjlujOigwUJVzXz16Q93vYIuK3FcyDuOMzpwW 15 | HHgSLH/+rdtd+7hoLwMCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB 16 | /wQIMAYBAf8CAQAwHQYDVR0OBBYEFM0FLuuYBwuAJ+tocRlu+xUuOw6FMA0GCSqG 17 | SIb3DQEBCwUAA4ICAQBLNuP8ovTgFcYf7Ydgc+aDB2v+qMYVIqJMrnp8DuYy20yv 18 | 64jYcIxh35IbQWOZxZshJsebRKM9vr6huEo2c/SuHLQ5HZGPxSt++aG+iY4Y1zL5 19 | KHtG558lK4S5VsXymMkUjGZtm+ZuJida9ZcV+jz/kePMHpErWPeMvH2jDmD4mWgA 20 | YdjipD4cxEn+9O3lBSCkeSjaAd5rQeD9XomV4a2/uL4Y7RDbn9BNt+jdLvfu2pmo 21 | O1zcp0f578oFlUIg0H9fb6YzL3MKOXiuh7KE1/W9el5zsN/kLlyWFbopN34A6PlO 22 | ZHEvZZcQW06bmy2FRWgqkqWMqBwzWk7JKGp+ozv8IBvimhgjNun068FQAZV9nfKU 23 | 6U728P6T1USDhgwtpX7/2IaukXcmO2FE9XzKZyYAbmAcOhPLzFO4pdwapU2lPbFE 24 | l2HLkYaHLXzMxB30kQQHW2l8+8xr+MAa+bBcD9Jaxaz/t3ZpLt62/1nxT7SWNwH4 25 | Sa83BaG3EHBotlBc18hqrFWEKR4KYenqY8xa7kblDI0rXqlXBblUXp0TwIctOmzR 26 | coqR8q6/R4VXhD9FZBIW1/uX2KKEPfTM46aQdaTtdzjd3UzwTP0SRwkvZ4oFftW6 27 | s1GljfCGsrOpi6O/Uy/IVTE7Xn/oVnlJvGbaP+AHexLytBiBVUBukLBwvpJ8bg== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /internal/testing/tls/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAnjw7iZyn9EtjtK7zT+M59OxSJ43a3kMm11Vnh1Fw8oQ7tH0k 3 | QW6COyAwlBhAzWGtfDC6jG7A2n+8mPWoinsxoLA5viSZrEkWZ4tGxWWZ5y/xWh5N 4 | BrHa3Jsg1rZ5dstAl08gJPwl0v32aYkMdtk16K0kjtdPpKtO98v1N6ea7fvQKdyr 5 | AaUHY/BY+onoqmSBGPX6vVV7FGybWDf72J5vcwyA07wdaZ7TrUYbD28nhXuc4Dt2 6 | KbZbWvkT4OYZ+4c+eiehRFVGtuXXi0cKG0xF516bDnrSO1/2ZbIB2xmcgKvcay0j 7 | LzWqhnsSc+qTqODVLODQAMvrMlY0RUXQPn/WTfAwaQ+u/j7qIR1KFZcLn7uVq8bC 8 | M9g+VeyLjq+6XUvgGL9KhvrR/FA4R4DUka+ZVQqhs262Qs7pNFdoIIrTsJyPd7/U 9 | YWCcQbkCKw0aRoUfBeZgkg4bylcABygZqY9+apRFNBEhpycAEvWFarr6rosqII9k 10 | Lm1LpnPNEgSvQ/CIRIHq5z5iKeSvHFYOVxLS44HHM16Mry5UF/jW7Vg8JY5Jrg5Y 11 | wyOhdGoOSJ3+c/pbLq1TRkwK9bwEJwGr7FdkrjPZuNJ7HQiFn2IaeLlbtzwJ+q3v 12 | GSFEjlujOigwUJVzXz16Q93vYIuK3FcyDuOMzpwWHHgSLH/+rdtd+7hoLwMCAwEA 13 | AQKCAgBvitoVWX7zsKkqVyFhMTZLtsL66v5cK04YATYnp3tNGXXU91o1Xacj8r8L 14 | xkT4AmD+6IK4N+JupBjYYmNaqxkCwvcRWE+TqTnH5+ANil+BHsSt2CpIC9vSIvB1 15 | KtBYs1Jm1vo72Br5rtii8F7+8IMV7+eTYafc1n2mI/pKLzYBiL7mo41QbXrWMjkm 16 | 80w1wP9YDx2flcBbV2vyNhSsUJMTsL6ngzXgnHtu67prmNltOQQO9RuIr+maKXaf 17 | 1NSAAIhEJ+eAefSNPVxB6+Pt9khYntIC1QWZoT3Z1i+EuXsfIQcR7hGdV+FLRzps 18 | x/Eq3MKpDhjSVu0G4MmcA2iWhhsUXbOihpXKWnAdLv0cUP0tbbxcUsEa4igAruBW 19 | n6+hYrVkbD5sZuGoMdzKvnGkqRTf/ragdcFlfty7KaAUBFr5V0fzsaW53+/5zG5o 20 | eSRoyCNLQSbBh2TVVpnNIbXELuYmwoDvnPNup4G7ITFsUQZEW2Tm3LHQt7EAi/wn 21 | hJU/21rI46ubB3r5wJZ+6JOK4PiqPeSIokyVfFyPk2ny4LT7ne5MtPHF+wAVAOYj 22 | 0wcLEyh2s1b0VSlko6GnPjbLi8eAtOAk2ggVK5GofnkhsPohA+yoNvcDDLbcdQ8v 23 | 9Q/nbL2dENce6HZEBSi5RlElU9+BbOJc4qFM2o9mzrWuOpHRsQKCAQEA0j+OhUag 24 | qbu7tNcWvE8w0I9vt1CXuZrS2ypKpuaaMYknb2Lo4YtuV9+bHx1isGHnAc2RAbUB 25 | 23mLANhquRIOo7u3NTvtRsvyzrRuuFviQZ5p1b8MHfqpg+/mA+yve8J3zyMMNC5f 26 | c7m/13J+dsQNf/WWhqbnWU3wOoRa0NQBtOw7UhVFl+1JeBfSiVmYQXH1n5VnOWs8 27 | Vab5kGkeYpUssgMFJG07qgdX5Ux3KzAQm7Onvsn4tT5UMtHt67f/wWzlP/xcoBJW 28 | 67clhCuO2Jiojo4jSNko0PGgTmFPmF3EOW+zd+iEYP3LF+S4uTR7yz4+foUNa9e2 29 | xf6XwMp3ymjbfwKCAQEAwKsnMt0KErZ1/iRbN6U5Rcgxcq6FxRivXyzQX/3QrWc0 30 | r6H4WWk5+gwy/Fb1CpQyiJkXG7PpVysdWaWF5S3NRVL3Ixuyp1R10DmPYch/4Pn6 31 | 4BD9UgKUhS2nxBVcMyyM/mN0W1Img22tCaJhaI+/raYf61JxgWmUJmUq8k8Xzgfv 32 | ndEYQGgf62jG35aopkqfwiC8+rApgbiLoN1mGiusyJUcZmYLLYp2ao/xHc7UtjMP 33 | N5tQeE0aZgSaBBwDAMQxMdWovo5qThvpJdy8q8EVq6sCO1G1MzLzIMxd/4asVzLc 34 | wUHSG/8c9qdgxBGGhYAbTSVegWTaqznDrlFB8RP+fQKCAQBOx+vyeqWHFEZgm9v0 35 | EcRb0fNtgDBqJt5tqyov4ebTOu5g6XIT2XguSyZIAW3SY8z4uvtj5VxdzexNE8rh 36 | sCd2KMeclejyB0fjNm7qe9uK9P35Ts4OibdtLb5FqDGVMShNoHdZMisoJOkCpO9I 37 | N2xLj02pBO9ZYj/q3V9eMqK1FXOg7UGXjR1jd6G3P7Ayja4Y7xWvyUPhYGDRQOJW 38 | 1EjcJw+NN7UMoBXKYN2ifC8s+KOZdPrRhxprtIfvNJIL+27ni/t1K4oQZx8SqHOt 39 | K361dAM6r8yAhpmn5QS7Nh9p2jYobyLzaQXp3RVuqIDehmNKaza9OyZMiHp6jiNW 40 | 3/WnAoIBAQCTeK3ZRc03A4gPDd7wGbxbyF7o6+KiOUHKtK+OOeWnRI7UPEKulVd2 41 | KC5CbYDEJykC20MPxka9nNerTYHOKJ+tB1L5AXNelsxSpCw2aVRQbKb1KKvtQOJT 42 | id2Wvc7DsL7+3DssxxWJlcJT1IGAmj7Z+IUIByOwLZLjTJ5xt859uh9Tib9pVQnR 43 | k3Jdo6DVH9tmqM5dh8dNbmcZqz1CnNl08oU5b7PwmMII0MJ60VyJVU25f105p7Kk 44 | EbOdn59Az+rjvSmbKcD+pmhvvaSARpuCubNMmj76wG3OVf9A3eE+IUVNe0cKfNu7 45 | g+QST2PK/YJoK0lJ+1tQojdATxwNHgO1AoIBAHjpZ9Px6L3Ek6O3ZxRfuDTkmoVB 46 | APmvm4IcvajB0BPd1mdcf4sWYmGhNqf+xajwsKB8jIkd4LYfINjfIZOdnYgq0oQY 47 | cM7K4+b8gkLKsIV2gFI4b95TYcbmanxdTDdbERGTJPsIBajXO5XapAswAJfllSDH 48 | pUvLb2CUgLhMhR9SFZAjyRo0HV++jMqxWJKzhlTOkoPzBY5xAleft+hzVch3WuvP 49 | zZn/NrpzTEpslV7dZ05Wuh8E+vJMoQNCReGlmAwNlrt/vxDuyv6ibNPxBHax82On 50 | yo6EP59d7OE0951FruUIITUgzKG2jIqeR/e5Yb0LJusXnj4RPuvfRULFD00= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /internal/testing/tls/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEGjCCAgKgAwIBAgIRAPt0KCF12GYbCoUj7klj5/AwDQYJKoZIhvcNAQELBQAw 3 | DTELMAkGA1UEAxMCY2EwHhcNMTcwODI1MTU0NTUzWhcNMjcwODI1MTU0NTUyWjAR 4 | MQ8wDQYDVQQDEwZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 5 | AQC1JxEPOsZyf8883tlPBEajotyECtrYMZ48FsYEmQ1XvKPoH3eb7+Ev7tRBVAup 6 | yB87XQ5PU/oNqAtpo/6WD5JGnKSVs+EAMESXmzEF04T9hK8uSd0cVEEkd0tbVNpX 7 | bWMbivHnx5Vp8o2mIx0sVrgGsJW3t+cYbNTp3bOTdmz7LKbiQN2Ix0wH+2/sPXYa 8 | cZsgbI0Ydo9KnqykPm2TqBYCL1kzhGlvaAotjdDIm7OgnaGCFe4CbK4QZB4uFw3e 9 | M+PmLG0TsaH9CT/ZRrE21iBfg0rqgpKZKMcqYQftXdLqlikuV69F+0L84xRfeVqB 10 | 1E4j0RwBGWW8EwY4WHK3VE25AgMBAAGjcTBvMA4GA1UdDwEB/wQEAwIDuDAdBgNV 11 | HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFMs+/QF/ZJaRu8Wv 12 | vcaMC7jGmPwxMB8GA1UdIwQYMBaAFM0FLuuYBwuAJ+tocRlu+xUuOw6FMA0GCSqG 13 | SIb3DQEBCwUAA4ICAQB0gPDs86Rjy/O2+l8QyaYfwmmyTMPjNVqKgVP1uuikWErN 14 | 5hTAlwtDI9FuiMFBqeBdeiT8IQvzEEQPYu69kAX2XYBWBMWDa85co5fJztAzV7Yz 15 | VL1byhxd2jgM14usyx6PbzkhYKBNesujHj7wQ0ur+85Kp66HqKCuNCvbj0zv58PH 16 | RWkojRPgyTpbLdXXCOWJXp62XfddL1Bf7NJCW5QTyHoHoOsOeoPajb4OOmQehzqv 17 | b9FPAHVFBPrU53Xn1CURAzTeBQ2T/OK4nx6EdQgxP9+VVurBQ9N2YBM9VEJmfQK8 18 | Lf5/+EJHe5ctOy1Xm4A3A52zZ1kGjfvWUtGJUSnJ5ahhMm6Dx63wk7oYNCTXnPup 19 | aVtINWygNlS/dQsWubHaWSFwB9/QwK074+H/4EpDq9HCMMl8yPMktOmv69Hyaju3 20 | MvGshz/DLNZf9oYpO+lbU8X124Z6XifEztMiBlUPW75KYv9X4CTbKTdE45QaRMiO 21 | ZXcH4HE1/iQ9IOGg7CplMlMcHg+lQ7CpXQjtUUjCEpkj8BAs8YLDodLnjigs56/8 22 | 75+3cVZu0+dY+9eNt/EIqzjaFwEx72hbLyhk2IeS++7QloJDhYqlq+ng54Ul957A 23 | 8e7TsSVHlLZVGXw8yqjyxxOwWaFx6mvFzGrcBtvCgK2HwEiYQ9qXJ5VPkdo42w== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /internal/testing/tls/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICVjCCAT4CAQAwETEPMA0GA1UEAxMGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEF 3 | AAOCAQ8AMIIBCgKCAQEAtScRDzrGcn/PPN7ZTwRGo6LchAra2DGePBbGBJkNV7yj 4 | 6B93m+/hL+7UQVQLqcgfO10OT1P6DagLaaP+lg+SRpyklbPhADBEl5sxBdOE/YSv 5 | LkndHFRBJHdLW1TaV21jG4rx58eVafKNpiMdLFa4BrCVt7fnGGzU6d2zk3Zs+yym 6 | 4kDdiMdMB/tv7D12GnGbIGyNGHaPSp6spD5tk6gWAi9ZM4Rpb2gKLY3QyJuzoJ2h 7 | ghXuAmyuEGQeLhcN3jPj5ixtE7Gh/Qk/2UaxNtYgX4NK6oKSmSjHKmEH7V3S6pYp 8 | LlevRftC/OMUX3lagdROI9EcARllvBMGOFhyt1RNuQIDAQABoAAwDQYJKoZIhvcN 9 | AQELBQADggEBAFnxmVCuM3J2bt79JcFOqsXNsvGUUT+4kMl3BcfSWaf1pviuhiXT 10 | fsKkk1WItvaRQvpNdQoFQDjKHGcd6+0vCz+Q6Nni2Vniz3+f3+h/rOzWGA656Xxm 11 | lgByryixnngWZBNLZkLWCz/H1MAlQYu8PTdy0N+JBF/E5SAGfaaXtfTC6tjnnZIm 12 | 3rjxC7C3EyELpo3X3erTcHpnFvhl6ZSkViVWfhOjxU0n+TGGohczesbHZc8YC37y 13 | JrkrnRDrNKnca1XkXWUnbV6rH8cVDnJ0Fvs54RI686Tlv+LxW2xa3D2+pV7Koduj 14 | Ru+PguJ3BbaRpieGTxHg7hH/1T5HsZnD2E0= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /internal/testing/tls/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAtScRDzrGcn/PPN7ZTwRGo6LchAra2DGePBbGBJkNV7yj6B93 3 | m+/hL+7UQVQLqcgfO10OT1P6DagLaaP+lg+SRpyklbPhADBEl5sxBdOE/YSvLknd 4 | HFRBJHdLW1TaV21jG4rx58eVafKNpiMdLFa4BrCVt7fnGGzU6d2zk3Zs+yym4kDd 5 | iMdMB/tv7D12GnGbIGyNGHaPSp6spD5tk6gWAi9ZM4Rpb2gKLY3QyJuzoJ2hghXu 6 | AmyuEGQeLhcN3jPj5ixtE7Gh/Qk/2UaxNtYgX4NK6oKSmSjHKmEH7V3S6pYpLlev 7 | RftC/OMUX3lagdROI9EcARllvBMGOFhyt1RNuQIDAQABAoIBACE5jRNyADu33VaY 8 | uNqZOiuBD1jYdNL6Jr92ndLyD1RsMNO+Eb3z/SVBdISW2ZzGK5RDuQArss0WaSFz 9 | BpqXOIji6fzbBQV31NzJhfA/n0CwOUEQIxGzEk+R4axan8ExOuAuV7ffDzRjXD+A 10 | aTVcolv3vz326Ne9/j72fp0pN0vJ0b8mk1xmDWNOHhfoWmIGrUZAjqAkA1kh5aLk 11 | Q8MCjVyjT+KYDkFT6NscFVxKslDVhb2OFC7oy+9l/hBru12bsi9eBdYpPT9E1cpR 12 | U9N8+9XS9d7wgVnmVh8CIrFToLsvSrwD8SG0Indot0C6dsy0PkoMUekVxvM5/wXm 13 | YLZnZEECgYEA28JfZxFxO+bjd+zBC+yrusHCVfZK6MZZV0u/V82Bn+gftwgxagI7 14 | q2h7m56WBtq9MeLlhicZ+em4BtA3yHwVGhqr+d5CaXjkYype1EditqGx8JYRUwlG 15 | 9z7W6jCEDJsjrzvGgua1qsyCZFePG78i4rLumK7UVvWEK/OaiZu3Pr0CgYEA0wbT 16 | 3STBc4THLXR8nx39b6RP+qH8jO9UcD/V7Hi/SWTCcGB8IIlTV2EJVndKHPregcmI 17 | dN61uH3d+3UtI/WxEPMcfrSlEwVrjF2m5szYjLIAeFynw7pQY95qIhgKi6OH0Yn6 18 | 9OCmieL0x1ez5zOXiv+GVjmn9tDCxXvqfsW9CK0CgYEApOd0Y4kpKUQWyPT135bX 19 | PqsKwyqwB4BfpiwHB0IE1ROASP5y5hOK5xLePmaAOeCGPBsBFOveiDQjjalNUroZ 20 | s570EeoAd9jpuKggxLZUkqs/NUPG+EJr6DhVWSLS1ArOej4mti+dfu87oUQ69R02 21 | dlrCw/vdBuvxJHIGMuCQXxkCgYEAinSFVygBgQCSCkHObjuoB7LgAsp7QCDa3tcT 22 | TZafstDYPhEf/9z6AG+bR872onL6wF7xF/Tzd7ulhJGJ73kJFtzbSkrNr+AzgyID 23 | GpU2U4GKi24HaIT6r7vDGOF7Mck2mIWWUUqAGiH9hjkFwWD5QeqLQlGL4YVw9U9r 24 | OIgWkfUCgYAoMub8wHJe9zhq7UCBCa4zPXqWVQcN7ANjL6fESvyK+A6TfwL5j782 25 | CNIVl8ewU9TthEY+AdbJeiAevz+pIazSqi0ln1JKO5YpCOC1Y0UxcEpghplBTlPU 26 | yoQyTJP81iLynwOM4pC322ptJISXIndL7Ig/9AoRZtAJV4Ot6z9b1Q== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /internal/testing/tls/expired.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEODCCAiCgAwIBAgIQP6vqM4GEKFdwrZvr/s/5KTANBgkqhkiG9w0BAQsFADAN 3 | MQswCQYDVQQDEwJjYTAeFw0xNzA4MjUxNTQ1NThaFw0xNzA4MjUxNTQ1NThaMBIx 4 | EDAOBgNVBAMTB2V4cGlyZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 5 | AQDDlGu08Ye7IGMnh6zyiVbW64btT2BUfj4fZlyTraLNzVPfULOXezgJRAwAXEnY 6 | eYA5j6e7iAO/yGXq+Pxh9rbGlXTVC2BsPIfKzTOcCTxgO4tfrdfSWFRDGgcFtpAA 7 | /4b8fYQ8BMw7khVpOqj9n8TyqsN8DGyZTyNhQuc23zmmY1eVYtDIwVvVBp8/YzDT 8 | 3RzO+FyoRpKgdngX6kdqqZ+vaPxP078JM5zAMnysjSkpQCdAdu4EupvA5xRtOrDt 9 | jJoAi4iUf4sG20RiF9XFpiDkyUiOa0ysFC4wrWbXkaT1xHZJJPwwJZ8VyGDRFlFa 10 | POEaM9dtLPKtEV2af3G9Q9s/AgMBAAGjgY4wgYswDgYDVR0PAQH/BAQDAgO4MB0G 11 | A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUNX8nKbJ+G0bv 12 | k88bTHkULUX1U5AwHwYDVR0jBBgwFoAUzQUu65gHC4An62hxGW77FS47DoUwGgYD 13 | VR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQBHZsxC 14 | GGOUhNi1b0xUYmKwFaRP641R69NydzVMma+2rpLZtL8MQq7lWAgWGE8EiKfTewk8 15 | 2Gm+rNwReBRkw56zka8WtLomO4GKKJHvfRsbSempsp9MShc3vcsEtzXGjDQi9c4Y 16 | BJjWbh666jCKPRJ09RewcTyTYPtZ96lVeYRj9HhXF1EGnURG5sM/zCantlZXxInk 17 | 1FIOKDuawMbgf3GR6n/bM2oybYCs4Skv3yOp8x3lyhlJ/zmSPGVVkPa7Vki5+sPh 18 | /eIZJ9mzEXsu7IXfg1isZ01iB28+6UgpZt/3017PvgopiYrL0gMKO3EL8UqJDweN 19 | GzJh5VYOqsjTbsrYYsWGqM66vJmfPvqDyYA1jj/EH+q6TBz0s99rzF44bBKKie6T 20 | 0KrZT7ohXQ18Vhl2UpqahMqxMeAW/QP5asGuzS5EalUir5mNcOljtq1NnfmYvqok 21 | aDC7rURoANwZ2L1oKN0oB6jn36g791pFdKycw+HdsrGgQGPOMak9P32z5kmDsODH 22 | 6aVrfio2WwSGg+1CIBH0QsclHAUgLsAXjyGbRWxPsvMcsLB6OOymuTP2UfriT05X 23 | duabvbEP5IIRehVUfrP5uvoo29xnoPL4UB0C8gwr21IDn7Zew5/ALekN+s6IgsfL 24 | 9yKTGSD+6Ir3NqBgL8T+uhOAekyLE5S4CcwCHw== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /internal/testing/tls/expired.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIChDCCAWwCAQAwEjEQMA4GA1UEAxMHZXhwaXJlZDCCASIwDQYJKoZIhvcNAQEB 3 | BQADggEPADCCAQoCggEBAMOUa7Txh7sgYyeHrPKJVtbrhu1PYFR+Ph9mXJOtos3N 4 | U99Qs5d7OAlEDABcSdh5gDmPp7uIA7/IZer4/GH2tsaVdNULYGw8h8rNM5wJPGA7 5 | i1+t19JYVEMaBwW2kAD/hvx9hDwEzDuSFWk6qP2fxPKqw3wMbJlPI2FC5zbfOaZj 6 | V5Vi0MjBW9UGnz9jMNPdHM74XKhGkqB2eBfqR2qpn69o/E/TvwkznMAyfKyNKSlA 7 | J0B27gS6m8DnFG06sO2MmgCLiJR/iwbbRGIX1cWmIOTJSI5rTKwULjCtZteRpPXE 8 | dkkk/DAlnxXIYNEWUVo84Roz120s8q0RXZp/cb1D2z8CAwEAAaAtMCsGCSqGSIb3 9 | DQEJDjEeMBwwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEB 10 | CwUAA4IBAQA0GTIB6PxgmHBa234rSYqIew4qRfY9MeUkVQEFRDwodqxa+LWvZx2T 11 | 5JmTZYyXBfQwnSye18fDjQuHv1KaI7bnJuMRv9KU8L6ynLkAqrFWRSBjt3eCum01 12 | IWZFyWu+dUN2c12C79zUQh8uZc15oDNFrD8ivBbGRpWvR1CSG/DH52kJ8nckgEsh 13 | SwxbzSPOXBgLH6ke5z9QGHJMK2rhRFutFOecAId7VBiWqfZJv15+P2ZcyJNQGThs 14 | V2sT974YGFkc0Y1MWlCgi3XhyQzIzqV1tEILGSTDE8biuKlm1nX3H0K8oI3hoGcM 15 | CBjE3HQ1/rs10IY/WvkpIfAU71D/ExMc 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /internal/testing/tls/expired.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAw5RrtPGHuyBjJ4es8olW1uuG7U9gVH4+H2Zck62izc1T31Cz 3 | l3s4CUQMAFxJ2HmAOY+nu4gDv8hl6vj8Yfa2xpV01QtgbDyHys0znAk8YDuLX63X 4 | 0lhUQxoHBbaQAP+G/H2EPATMO5IVaTqo/Z/E8qrDfAxsmU8jYULnNt85pmNXlWLQ 5 | yMFb1QafP2Mw090czvhcqEaSoHZ4F+pHaqmfr2j8T9O/CTOcwDJ8rI0pKUAnQHbu 6 | BLqbwOcUbTqw7YyaAIuIlH+LBttEYhfVxaYg5MlIjmtMrBQuMK1m15Gk9cR2SST8 7 | MCWfFchg0RZRWjzhGjPXbSzyrRFdmn9xvUPbPwIDAQABAoIBADa1IZuvpCP330SD 8 | cyE0wZHEuC1RcsSvu3jVDThR7aRbtwZUcKgC053j5ueC6TUgZ3mycVzHoyTWTYv4 9 | scBFXsMVs2SUlhgwpltYIwOWocjZXxcYbbJs+sT6VtSGSKm+0Gd4RLD1NpvDNTIG 10 | MpcfRdwLYDsmzonj1SWzrTFwJ5Qe33cHSR8Oi9OarrxzIHWLDNgp8x+5j2l0T3AG 11 | RPQMXj7jaK6qEdHGD6geg+ButeyjYyxu64l8Ooax11/jfdYHcK+BmmtmP3Bd/6FQ 12 | DCtrXTBv5Txl/T6D/6OsyabSlwGWFDNEAaS47hKCFiYJBR+Az6r7d1QFBIzuqF0+ 13 | T7EwaKECgYEA/tUMcTXnBRquAtQHW4cSvHH2qoZAhGjXCmj/pk4UCOZDT1DP/K5u 14 | m7tOZB4RJBmzg0a1QS89aJ6lhY0Oy1Y9MZTM6ofkBKd8+P0Rgo04UDVeQ8sA9c8x 15 | 4bFOrOEqe7NoK7Mwxyn5NOmNi/wxy+tpiMH4Jt3y4TLDEZPg0Tsfu90CgYEAxHnc 16 | fN3gKeY3SV6nHi+johBirSNazhr4n0fx/TCsy02e5Rrn0rksGVfPij5Y2g8HQLel 17 | hdbG7tVyA7UtGgVjwiT+4j0VXmWpCIqfPsibRoy29oPqO4p5pULsz+ueb4biRW2Q 18 | tdLqS4OldM8YUFwkS7k3Pp13SAY+Ir9rHL6wv8sCgYEA/AZaYtCrZMnpFNT7XdLt 19 | fb+78xQJVKqXGi2TwLbxa4fHQ/cpa75bl9scAToXO7vLZOaWNhxxQDm+e6Fw4zqs 20 | FJAURVMV+GBo4ZrvKU1fRzwwuR1ZGsHKlGoV5DZgHKznNmjmseJaG7FsEujdms58 21 | tgsXz+Cr53qbn5O/wU4W6WUCgYBA16sB9sPtcBIc/8UNvFE3wkqes4VbciFNiBQA 22 | KJlOe26OVCPgMsawEn/nMw5l4QHWxQU2t5xt5Dm9qYSaCt9Sip0oE1rDDbAMpptJ 23 | wDEmxnf3wa+DOP9OoFjBghSG4DA7E57nsxUqGOd5NoPiuZYs+5KU8qkUNyM4mo4C 24 | LZjtowKBgCWuDPr8MBL1S3ym55VTNUWcuMKUHZkMg0HjRpi8ABXeARoM5FAWykbk 25 | hFnI/Waj6EHNGoVVpKttJldP+uRRA+S3wZYKH4gRQcsWI/BVdiBkZgoTG7cwnixs 26 | CqMQ4p6In1Q/6EafPqkVQOY1abNKQ7ZGbhksB/fVbf37XfOnDj3H 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /internal/testing/tls/other.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEODCCAiCgAwIBAgIRAMcIOg3oRf7n7mR1BUqNnlcwDQYJKoZIhvcNAQELBQAw 3 | DTELMAkGA1UEAxMCY2EwHhcNMTcwODI1MTU0NTUzWhcNMjcwODI1MTU0NTUyWjAQ 4 | MQ4wDAYDVQQDEwVvdGhlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | AM4lPXHK6YV7GFMeSsHKVq33LDn8Zt0IkY5pGNCuuksPa3vYBFRkwTrrobKQDnmw 6 | jLeaQISyxyelhT4aIU34OQpGjOmCzceYPG0uOcecq7noVvbgw0UeQHkEL0p4NOlb 7 | 9LMNVhKTgQUhZC5mYSIpXNxlE/LlGmqrFX/8peSaJ1oAkkB5FYN0gAbYhd8kpJX3 8 | 9Nr2A+f88oicSX5K0L73LUFUrxoTcrFP1pWnFgg28vLvvzrW/VZtq4qJPl3GTLbM 9 | xHOu000LaHK8TgIJMCQUalh2q2nz+Htmtv+g/b27YEMGcDBW1qBX9oMKgoM/Z1Hv 10 | zjVhAm9I649heDUguFXCaoMCAwEAAaOBjzCBjDAOBgNVHQ8BAf8EBAMCA7gwHQYD 11 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSUR1DE/STLJwf2 12 | hW4qXPrOSLqlmDAfBgNVHSMEGDAWgBTNBS7rmAcLgCfraHEZbvsVLjsOhTAbBgNV 13 | HREEFDASggpmb29iYXIuY29thwQBAgMEMA0GCSqGSIb3DQEBCwUAA4ICAQBG87BU 14 | UuDuUCnvcwUNbga8fhe1PR6z2jueKQiI10SxkYG/g6PMQGGYDNO9DZFKu9l/TMTf 15 | LnuEozN+Csig+wC+sc32/MdR35XTmkKtNhL0cVgvKP0Q6zNk97/QJErLtDpYb9VR 16 | Gm2Ky6FGDp+/EEUvQUKRpGBmWIqOtjxqQu8lLoJlt/TPhxJ0lGDd3c8WwaVFYTbS 17 | isBKdHpS2hkn/O1Yd4QtNk06pCpUDQuPumUOBoa+dK3y0jZ+e34h1NoR5EZvfRJy 18 | p3n7CLD2eZSNc4oWKb67X0RDao5LD0b51crjgsFYHhCTS+Mgh0YkgukQZ8uBKpUJ 19 | IBhz2Nr1QXykrJUhal9MrKukjczEikGxzK1VsDgxYY1kLBURhM9/TfvICmcAaQqv 20 | MF9B78lnoJiPZZxD+a5N9MawzN6QBqX8GpvhZoAnj6iAuNwKJVyENpZqravxeq2o 21 | buNjgQ+SmfqxQDfMD3lu95yidqD7bcDipJsXEPQzdBjZ1JOJCGi2eiAm50e6bq94 22 | CMKmmRjtIbF1hJnHeEFPvXqdPpqcyEvcaDebph/f+54wubTgwFI3VMnhhlv2EPIe 23 | rwcbZV3kNpUZWXAZVzYlQcbK+9US8PocOUzmqzA7ZZRO+rCWNxahHjdgrMK3fG6r 24 | WudSHHXawj3dkPeWrQde6SILSK/myhGLdjg1fg== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /internal/testing/tls/other.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICgzCCAWsCAQAwEDEOMAwGA1UEAxMFb3RoZXIwggEiMA0GCSqGSIb3DQEBAQUA 3 | A4IBDwAwggEKAoIBAQDOJT1xyumFexhTHkrBylat9yw5/GbdCJGOaRjQrrpLD2t7 4 | 2ARUZME666GykA55sIy3mkCEsscnpYU+GiFN+DkKRozpgs3HmDxtLjnHnKu56Fb2 5 | 4MNFHkB5BC9KeDTpW/SzDVYSk4EFIWQuZmEiKVzcZRPy5RpqqxV//KXkmidaAJJA 6 | eRWDdIAG2IXfJKSV9/Ta9gPn/PKInEl+StC+9y1BVK8aE3KxT9aVpxYINvLy7786 7 | 1v1WbauKiT5dxky2zMRzrtNNC2hyvE4CCTAkFGpYdqtp8/h7Zrb/oP29u2BDBnAw 8 | VtagV/aDCoKDP2dR7841YQJvSOuPYXg1ILhVwmqDAgMBAAGgLjAsBgkqhkiG9w0B 9 | CQ4xHzAdMBsGA1UdEQQUMBKCCmZvb2Jhci5jb22HBAECAwQwDQYJKoZIhvcNAQEL 10 | BQADggEBABuaL2t2Zcv9R72OH93EpIzgExL37odLUiIjTIIykK2TT/gb1LtnE1WK 11 | THdqaLpnPot9IqBofppfkXPMrG7vavJoPlAp0lU2FHYIz64PHou8lj9yiXezDKDn 12 | Jia3TOxCu5VTRnYT7Ypt8kSull/jlyBQgTP+P0YXwoYAXJteQr9O6yD75yWOAx3A 13 | f/oQS77xoe0jdU4RkEMQRQQUIiaNyH8Bx4CeETmPoJDzEiIvnC5xDoySks1VJK7b 14 | w11IANF7zO5UWtYv/i3+Wh5XLMJ0GIIVTpuGkeVaZCjB8goiBJXoaHOKsO5ygJo5 15 | N+nxwiDTwUIZM+dU88mtCh0dvJeMMfA= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /internal/testing/tls/other.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAziU9ccrphXsYUx5KwcpWrfcsOfxm3QiRjmkY0K66Sw9re9gE 3 | VGTBOuuhspAOebCMt5pAhLLHJ6WFPhohTfg5CkaM6YLNx5g8bS45x5yruehW9uDD 4 | RR5AeQQvSng06Vv0sw1WEpOBBSFkLmZhIilc3GUT8uUaaqsVf/yl5JonWgCSQHkV 5 | g3SABtiF3ySklff02vYD5/zyiJxJfkrQvvctQVSvGhNysU/WlacWCDby8u+/Otb9 6 | Vm2riok+XcZMtszEc67TTQtocrxOAgkwJBRqWHarafP4e2a2/6D9vbtgQwZwMFbW 7 | oFf2gwqCgz9nUe/ONWECb0jrj2F4NSC4VcJqgwIDAQABAoIBAQCmygix5gwU/KiM 8 | r6iqrOx+6sq0y9vqIIGsaKo0RfriukIrvHacVbzl0DpPADFGEit4beyfsQpjsI9i 9 | 1L93l0uHXderIzMdt7XEXK9RKxjiXPLn4qj7ZmOhxloA9ctRuB3/NN4cP44XOZIV 10 | 3K3gdvj0NS/zyZwbC/tkR2Vt1a/bJ8DFfaFrSdk/btpVY2BH/uWjMls1BYIs3tEk 11 | nroJYb+fyliC+n/QXrLQAPTVLfI3jyVjRYpW5b6mZHQk4SGDde1XbtqRCp/f6PLu 12 | H8FQumd+SfrjgTwefphSWCW2H/aMNsZpL9NK/hmglKg3OcGKhwQswdmL9rDVFQmy 13 | AqxXwxk5AoGBAPPmijvwg6bKwqeSCdhoYjQVNXdTbdda3arjC/G/vxhsVntXluOX 14 | wGF2jInAu+sAShBFdhG4JlL+itlpA/aDDIMUxsF+YnhTOX5pofL8Nr4sstY6wjBf 15 | 4jVHQKLnaOm/mVpHqWABVv/M085XK7HHZR9e9y32ry1geRSiPF/E5I03AoGBANhf 16 | OPL5WWO7TOchWci6qRcv0kZ47iSE1JxSGnXr+KIIBPIAgrwhYkqgrl6noSe3x1qB 17 | tP7ZvmFWewxmo3mN2OwPTsxAhnjQK4D1PGJcsvf2A3f1uiwG+emqWpcMshTQnjda 18 | Hi2krfMaHwErE+dEbZiilLDRYAMAWlQodXnhllMVAoGAKdDIumYN7DavENO05Glh 19 | DNTmCcM//cASaQ3sKlJZjPJmEVd/Ax4tWYhdp/BnR28RQ6DlETylNW12mLesekMV 20 | jhOtz9a/QynhnY62uVYMfKZlMt14FZsayU+iAUvzbL/wps3KeC9CnzCaz7GaSCyL 21 | Zcl+T18PwZPcrnDyMOks1hkCgYEArrQUE3tpxbER4v12tTCiHuqp6eTyw+HMmXth 22 | ih1B3/KBq7Tl2mlKJ9+daygGYz9sY5OfRLcjlQxyxgyJqjfyEog5o4nmCd5rgfCB 23 | FRqsFrI5Er8B11K6rwSxqIzDrTLUzPSisU/qdAN/TT4vD+icZUXAsRQdZc7/IDya 24 | vhJ7ghECgYEAiZ6v380l2jcEk+tm4UYZ+nXq3c2wq8mZkQcEDVpqvDGL8k/dPEq9 25 | xOy7PVooQHQJyC2bgTbEKs6JYvzAYQmohiq7L3y9WxDQFJeImtzuNi0s/dwP+mBh 26 | R6htM5JwAO3JnE9V863M6kvtLEIk7XptI5gC0kN3Thi70yT3lnU+emk= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /internal/testing/tls/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIENzCCAh+gAwIBAgIQSCdvhIY3KZ9w9YRcDH1oRzANBgkqhkiG9w0BAQsFADAN 3 | MQswCQYDVQQDEwJjYTAeFw0xNzA4MjUxNTQ1NTNaFw0yNzA4MjUxNTQ1NTJaMBEx 4 | DzANBgNVBAMTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 5 | ANlVMhHZHjRWiGqmpOse1KZTmdUaiSgl3T88+Mie17UbiLmsOfnkd3PuEnKlXRXM 6 | sqOg15k9xfnV6SQebpBfkcwqJVH/USjWY4C1e2vDrva+j95L+uzDMZDF9nxpnjHE 7 | uHT9+hnrmB3Xted0tzxRC/77Cht8Kn4gaoljbBoZsRnv0vRRUYKA2OJHJRRCHhzZ 8 | AN6A+lWbWyvyvd3UeOLl3oFhk4lS5fwYY6RY8W9ZTJVxetVycvro42kK2jtpqUeF 9 | NercfjOg8VBWYjqB3Ey1wHDpjS407TW7RWEGA+3mP8ZFsuoYr7Rs+z8LprbYEPpF 10 | ojgvss+vMvjGkrcU8v0MR/sCAwEAAaOBjjCBizAOBgNVHQ8BAf8EBAMCA7gwHQYD 11 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRS8x8oOcBJnrrW 12 | jiS0t3qjKee63TAfBgNVHSMEGDAWgBTNBS7rmAcLgCfraHEZbvsVLjsOhTAaBgNV 13 | HREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAI9t4Pvn 14 | o0cW0SrC4EFNJqUaffbqUNd3i+dBn5/EQc8EGYVY3k8E8iUMHzRDyH+/VRp3RUOH 15 | oJDDS2uyeJ2InkC193MOpNJDQV5qf3yOmQCeVmmjcXkg1Nc+3oe8ttWiVW0ArFXS 16 | oud6V7/6qzIz3850ypi0Zz4KLVhSGzaI8dnzPopqNEG+ZbgIwvQLqWhv2bLpqycY 17 | 5ANECpjH6QIEp8JNjga9Sp/LspNCqMDmVswBGarySNWZ1+uflg9X30hsdCzVPgX3 18 | KMy0wVelT/4y893BCTo2KendGh70jaGxm8nBH7OXkeki2TI6boAvsn2Iash0HSZ4 19 | hPacZ4QWFEYW4jZeu3ZNTJ9tc2u2jgpAGueOcWRY75CraLid1V5t1kpGxAtX4NvX 20 | X56e/IlmEI6qPsaoPouQ1riAWMdRQUT1FLNPanv4vElDYXBNcFl7knuS14JDCC0M 21 | K6ttSb2MxJYfC6J+OJpHQd2GWU5aO2uZcgi9jRMslNwR+R94bxI+q/bjrI31JsDz 22 | 1pVRnGRWH7cDejA2f+q7X8/uRuA8bfnqBcu0uI9YR64W0VMMLe41+iR0wt5N3yWr 23 | /DalWIvmUvE8LoaGDwxV9T3xq1I1dWnASX+Xmb1SQ0CnhEEooZfQYfb3ffciG6mU 24 | UvVC0YW1cjOgb193W/N+Dgju7/a/e+XgbsgT 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /internal/testing/tls/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICgzCCAWsCAQAwETEPMA0GA1UEAxMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEF 3 | AAOCAQ8AMIIBCgKCAQEA2VUyEdkeNFaIaqak6x7UplOZ1RqJKCXdPzz4yJ7XtRuI 4 | uaw5+eR3c+4ScqVdFcyyo6DXmT3F+dXpJB5ukF+RzColUf9RKNZjgLV7a8Ou9r6P 5 | 3kv67MMxkMX2fGmeMcS4dP36GeuYHde153S3PFEL/vsKG3wqfiBqiWNsGhmxGe/S 6 | 9FFRgoDY4kclFEIeHNkA3oD6VZtbK/K93dR44uXegWGTiVLl/BhjpFjxb1lMlXF6 7 | 1XJy+ujjaQraO2mpR4U16tx+M6DxUFZiOoHcTLXAcOmNLjTtNbtFYQYD7eY/xkWy 8 | 6hivtGz7PwumttgQ+kWiOC+yz68y+MaStxTy/QxH+wIDAQABoC0wKwYJKoZIhvcN 9 | AQkOMR4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEL 10 | BQADggEBAA76r3SIQUER3XyGp4MOrfKGwBE7RnALcxW1XkrhID2bng2hzovrZZNO 11 | xutL1zqPFCxClIKUxYAXpMeY8lnS6H8I6FoM6ALCZbK7q9rmMK198LPMo3zC6TsO 12 | rEP8HOF9wxaYubg8xaq8iDlaL4e418M0UPOlE75PtkDAjhY++7ZTsjPVr/9WsJpZ 13 | MmEZ5kSS59PZbMbyqXn5MxE0iSD0LfM+lmkIBwSvD8rjq3SQ5NKCg6CJkRRq7BVe 14 | bujA2pPb6ivS5pujjIxkdUoz6S0G+ewZG16kbBoygWuRVFD8xqR9Pa41KSPhpx85 15 | 1qSrqR4zHvS3r+RS9UVIXnbh9ejW6Vg= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /internal/testing/tls/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA2VUyEdkeNFaIaqak6x7UplOZ1RqJKCXdPzz4yJ7XtRuIuaw5 3 | +eR3c+4ScqVdFcyyo6DXmT3F+dXpJB5ukF+RzColUf9RKNZjgLV7a8Ou9r6P3kv6 4 | 7MMxkMX2fGmeMcS4dP36GeuYHde153S3PFEL/vsKG3wqfiBqiWNsGhmxGe/S9FFR 5 | goDY4kclFEIeHNkA3oD6VZtbK/K93dR44uXegWGTiVLl/BhjpFjxb1lMlXF61XJy 6 | +ujjaQraO2mpR4U16tx+M6DxUFZiOoHcTLXAcOmNLjTtNbtFYQYD7eY/xkWy6hiv 7 | tGz7PwumttgQ+kWiOC+yz68y+MaStxTy/QxH+wIDAQABAoIBAEzhDVAw/LVI8wK/ 8 | JlGh21lm82Dl/SS9mDE5kUvunKGNNuVvXibewb65tb7mbjI68epeCEZGCtVg7RMA 9 | zN23YOzW79K8vWnzxMkP6bPqSecw69WYDRBZ0BvFW3cRKYuzagjAmws2Qt4zoz5Y 10 | FEV66gJtrVqhpqptLyKgj+n/sp1YiHMjkcJF5PGnAPudYxpiDHiPFked7vZtigyq 11 | eE2GCVbaOmRGPMe28JzRmOuFqEN9GccRjlq+AuzYe14lWKa6fZLVke78luByF//M 12 | RA+gGoKfHq859wACOEGbqShMWxC++y1HJ2Mu4adUlikzVq8rboinaAxzv56kc1n6 13 | EDc/qPECgYEA2x+qxHKSJnQuItZp0CpsDk51yr1fJcWtdEbcgmomiwMwl/Nk0OAB 14 | rplrW5gezyVRfMDsoqAnmBS301JEL/7QuEWvedTpptKC503Z+mCINPTZAaJfa7Jb 15 | KUCSQHO3hThfOXFMkb9mcJEVVNqtobnK61tC+JNOSd00Z9TdLQAbcokCgYEA/ehf 16 | Vq3md8bkQFlnMtFib8SIr8IP5j4JecFSeqa/OnBFfHJr0trUhoMO04uaQOl6lTT7 17 | 9Ca36WfJsv3EGkMHEFeceOsPswf65bI/qgxgUS7Qmu+NZTjvXL0gCpeHLPzEaLTV 18 | CDXoo+YAJzzv9yWwVEvIIru5SJPVud6Gap5L1WMCgYAjfO91PXD6FVrbfYpJknVJ 19 | o99j5GOihG9hI5DW9kYjwXJ/SYYMZhsfoe1HOk3TEqIt6Djq5bFD6icTbIFqnIRF 20 | M9QFkTv+Lp3QxEUHTdcBbJ4wq5F0qcAl4DVPhu4z/zs83GKgQDVhCb5AreHtDWAV 21 | 2gPwqjrFr7OrFUh0302SsQKBgQCOHKZn9HNfLOIKJj/9kHYhCoZaoSqW+rgA/rQ0 22 | U+oKQlaR/dTdsn9rPiVpP+S5WjSzGHHAyH79U4rv9Nryu/tTKUY5447o7JmAQJEj 23 | k0PBjItTfKrOMdy/MlehtggBpQQlerkVnF62hYAmdhP1Z5HWzIea8SkWNzBTlPn0 24 | 6N6W8wKBgQCDBPXqGii/ur5IBQ+RASIL78RgIGH49XYcq7IrYFOuztGBbqf4qorR 25 | BTl/mWFFHr+fAcLHlC/qTSBBflGClElqcg+j+92RAI6HbCFemdSbnsv5FCmciL6e 26 | 09x+oprKq3/WAVARUBif3RtDq92SFwSBfrx8JFhGIa9kUcsDFSBaDQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-ca.crl: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIICgjBsAgEBMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCHdyb25nLWNhFw0x 3 | NzA4MjUxNTQ1NTdaFw0yNzA4MjUxNTQ1NTdaMACgIzAhMB8GA1UdIwQYMBaAFGM6 4 | 559SRpX6VJlKbIObtrLXvc1rMA0GCSqGSIb3DQEBCwUAA4ICAQA+6zsHCq9YRZ+a 5 | fwsZmbGQqDUBVp5TWtDsy+qvKf/084CgTn0sR28HKEONQvX+R1CyzAaCGrkm081k 6 | yDUizdyGrVR8zmCc7O3ztPobfZBmQXbR0pcxwweFiELBO1exEQ5IpM4J0KOPc+CB 7 | AwVA227Q4oKrKyUtNQ9d3qr0/2E2HE8W0aV7Ax38MvXsUfafWk0SNPDusFiYNsTt 8 | v50Gmd2yBlaMzT9Dsze9wuoTvT42lpCby/NSSDYynG9Cra2y0VoIpVxvPqVzn77L 9 | 1otkaQRatbfktaa1WVufEK81FXEyeYdM/T4bKCbB5oBKxmwwiS8ukvunc9gdrosx 10 | /7QIlpr3iBEu+X+GeOdGyPA+a+S566Hil38QCyMUX4fI7xjYt3ek2MI1YeNEv576 11 | CwEihc7NvPh5MI0OQq0nIjTcIeEGQag0eAaGwmJ+AmrZ+4bop5QavuwHVh/7sNel 12 | rNhFucmx+gEPeS/Ae0cp2BeXjEXHmdfwKDCT1n1Rdd5wfLeFuKlBG8NdZKwZ7HH7 13 | vwi+JwIBx/WJ/f1qcPlWAyF4Y/HUJIRRNSXJOUQCWhCvfGtLQ4xs7uo4ViE8CtyO 14 | RE/3xHrX9UwJUymw50Efj3PpOxWPvJ9B7A+8ED1kJR29HQz5gVZhEcW6nDTntwDQ 15 | 5nrvhzi93xzaTQIhEe53uz+rTGs/fw== 16 | -----END X509 CRL----- 17 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwh3cm9u 3 | Zy1jYTAeFw0xNzA4MjUxNTQ1NTRaFw0yNzA4MjUxNTQ1NTdaMBMxETAPBgNVBAMT 4 | CHdyb25nLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzs91DHKf 5 | enUfIPigN6n4CsTSHGMkRpWQw9T5vIqUd3ZgMJLCoEnj0OpVlFVcb9+P2/f5RYg8 6 | H0dmM1iHGi5uCoDnJk7K5zaLMgxGtRy8dwZ1nHri3sZwM6Z9AyvktuSVbuElzp08 7 | utwB+fstR2METN5Dovp738rdx2q3zYYYcfnEXcYvxBdxVeitFgrXauRDxuqMio+5 8 | 5bHVUpIWlpu8Fd3CqnMUS4N6McijIn6T2wiyALJDf9xE7edAYl4ExYVKaOyR3Ff3 9 | +BhiS+IEPd9AoctU9JYFpDavfaiZz77AukwwfU+W93NTTFQ+rf/ev8XzsgkFyZPw 10 | CCfKuet+o2/8MIxv4nwKxv6GGMFbQz1gNw3RqG5m19zppqVzp1vgMcNXSeRPFlQI 11 | fXYFN9BY9bvx2L2ZpTn3gsdgzDGNzYU5fGro3YzmtelNBCY3sAz/riG6+wMDHCId 12 | 5K5NxrJBW3tTvEZQyKVZA1W22/F/Wz2LxA+4ZLhUoUuXTkJxLS75EWLkK2xK+IXv 13 | h4s8n8CwfhFV/De7u18Pho0XKTm2IPir1nL0WNhjy4jvBYDN/Jy4fE4QALt4oH9t 14 | +GITkDo0Zd/USZSkAOXgEb+Ks5F/fztI9yVp7/nhhj1KnSIrkObBaltlfEOe4vgz 15 | 3dNFAG5kH5lyG90uffKR7h1Vo7UkYcpJ3IMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD 16 | AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGM6559SRpX6VJlKbIOb 17 | trLXvc1rMA0GCSqGSIb3DQEBCwUAA4ICAQCIm4pRbel8V/W1OKSzJXg3F76ORIjz 18 | zN4mAtCX8YtFQwawBlES25Ju2IQ9XfvqM0CPO5LEe1v8ZTXeke9Vjf/XGReBCCqy 19 | /STzLSBHflQqvybMYH87K5h5e91Ow9T2HjyPtzS3RdyaahU/Y8/EnOTG89uJlpN3 20 | 0k0/KXfwVKAyjrOaoTeGPM9BjDssNq2S07h5C8sCby3MpR26CIMGbFnotwTjmSww 21 | qkDSVd63/ZIB5/dOcOlBd1+rE3LOzYxDiZtKWu0NM+o7N0m4Y+gD4siyxRWuKslz 22 | cTwiwnLmzZG5BUvRT2FmzCwejp45+LjrXmUZ8hCznk68hnkilx9XLdkBL/1qyk40 23 | I1IUFQtkkcyznwUyKpC0z4VJZAVL8xi6KO60TOYtidxFTJxkPrWcAHvgzItao3XZ 24 | C4hLlNk7RD6BJ8oyMtpXFq7MHAAb8MWSLu/rSAhQHoKqlCEK4Iks9nWLmRP0OdAw 25 | BcXGMuTIn1jFRM0CQvg68GPFOH3FKv+cyUbjPoXvCBYiXKmxA/WX3rYvDo2paZKU 26 | /mDMu+EdAR3Zk/wYXl4738ujqzO88Nw2LBHLKhXytHMaSbfmWf085r0L+fqHLuVM 27 | jlpPEi6vQum25j9tvGnp6GyO8lUDAUqk5gtYIp+D67+NG+9eBocA1ADVpeKZHBQV 28 | xGgCdjnoP+nDVw== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEAzs91DHKfenUfIPigN6n4CsTSHGMkRpWQw9T5vIqUd3ZgMJLC 3 | oEnj0OpVlFVcb9+P2/f5RYg8H0dmM1iHGi5uCoDnJk7K5zaLMgxGtRy8dwZ1nHri 4 | 3sZwM6Z9AyvktuSVbuElzp08utwB+fstR2METN5Dovp738rdx2q3zYYYcfnEXcYv 5 | xBdxVeitFgrXauRDxuqMio+55bHVUpIWlpu8Fd3CqnMUS4N6McijIn6T2wiyALJD 6 | f9xE7edAYl4ExYVKaOyR3Ff3+BhiS+IEPd9AoctU9JYFpDavfaiZz77AukwwfU+W 7 | 93NTTFQ+rf/ev8XzsgkFyZPwCCfKuet+o2/8MIxv4nwKxv6GGMFbQz1gNw3RqG5m 8 | 19zppqVzp1vgMcNXSeRPFlQIfXYFN9BY9bvx2L2ZpTn3gsdgzDGNzYU5fGro3Yzm 9 | telNBCY3sAz/riG6+wMDHCId5K5NxrJBW3tTvEZQyKVZA1W22/F/Wz2LxA+4ZLhU 10 | oUuXTkJxLS75EWLkK2xK+IXvh4s8n8CwfhFV/De7u18Pho0XKTm2IPir1nL0WNhj 11 | y4jvBYDN/Jy4fE4QALt4oH9t+GITkDo0Zd/USZSkAOXgEb+Ks5F/fztI9yVp7/nh 12 | hj1KnSIrkObBaltlfEOe4vgz3dNFAG5kH5lyG90uffKR7h1Vo7UkYcpJ3IMCAwEA 13 | AQKCAgEAhCK25YIi9Sn5/qX8MDSP/8lream6lsKfIRBllBpy67UdlktewO0U+vmO 14 | Pl0f13bewqu4f72gtFd5LBtHDupVcq6Tgb1cFMibvRls3/EBVYcyBA3cAHyHWejo 15 | /OrBkj2QYKzH7DA4iidht+fNMUxJhheI3YvvM7i5ZN2BnHYuDjyIQ2YKRN65kis8 16 | 09WPd4Nq7qATtcBJBUJPSxd+CTJtxQbQhvlKIUla/I319WcsbwkqOhmr2PjSrbJQ 17 | R8lMgSs9tLZaJ4+pJsHlpBg/n4ySDg4NNMzZw+cQz1e3Fq4JE770SExe57Guqhk1 18 | hxTxrFP89WagZP/5oCxUcd/OJPy7At+MzLY+xDySXUqJjXO32FvSY1QCXySKxwxT 19 | jT2uOEEUiQs3Aap94ejm6rPEaifrGLlv+a28R+6gaaJIAQ+b/U8NapkhQI4k7uZT 20 | IY2FeIJKbbthjYYmvlpTMbIMKMTvRrqlWWOJ7Nd8gJo8vtFT0rRbm3joLzfJy3M+ 21 | ITIUjrLPIMHkEJ+A8OaqIEG7Wy97ONevUZDKTj0oElaTgIcIuI0aPdjB5cC7R/iz 22 | 4G0SJ62UheFrq10RX3IG7xRtyyNiF7Qy7CIJAFYYZuXknNPGUve+Dnbn/TInewSV 23 | 96pJf3xZj0PY1sYWLmFIJYoHLGK4VLmd4zm4Tw1ewYz/7Oh/ZYECggEBAPEzseYe 24 | Jkx6+Wz8v6hZjcYRn1+8Awdbb3mv5oW2eNHxtjX+ltYeABjUBYJsvO54ptIV5Bq1 25 | wojVSCAOy8z750SiRCzmm7yEr9Zc3bRoY03L/fi/pKlTxhS2zwrZIKIn/0Z4hYz+ 26 | 7UILIu23Pv0ctCi3zy09vGVvQi9VJ6KTuFWgaIDnGq5heQ0/7Ae+FfTVuqLOMUCb 27 | 4x+9ui2r7Xlu/TPMNaiNXb7OxJ7Yw0xr2w1OiHqBuYRGMXULc24nkTYPmCbZbHcA 28 | rUPq/JPP2HYvjqK/4iWEkuAYzTPTXWBmD1zGD644dazCBn6QKuwICjo9F+/mI2rP 29 | SMwD5UmZsIrxQEMCggEBANt/nD+A/66u7zJiStbIjryMXHuVzJsMNBt66sA3zxES 30 | wnVjpZ0vL+qrwsFa8rHLh90LmcOCJ6u3NYb2GZidK9P/uyAjKQdjVJEvk7T7QGMX 31 | yQHhQBIxh7TqK0CWYJy69E+Mhmn+HwsMX8GH/tHc+wdr5K6t9RgvWgMeV5sFfGrf 32 | fJr9VhVRhBqxpoL1fp+A3ya+z5Bn/dcXJuBs7lqHhKCySJKCMmvjijRj4zuVHaHX 33 | KI47gwX1vsxerqNEKA2kBuQOuKtd+ySQ6EvdhkE4G1lDESr5NGBjTtIsLlB7UcOK 34 | GIFSbmbYwCgzjFU9/sz5gfrgNRFb0gd/TDR4+CCcTsECggEBAI7m/cNEoZQ2V4iG 35 | xlZLmH98+VuS3IiDV6xU1tLppPNdrYKX7220IIKVOx5mphjzSoK1jYt1nGfNVQoJ 36 | Oh2cMQysxo+DoUkzo6nxIzk7j3oMHdA+WqQniffDxy66LWdlIwzxYs6CSrcSOgN0 37 | ydDULLjjDc/T/8ZpAGFipjTgKBozCzcztM8T2NBMyt5bdE62QfkrCGsq8IlhsuhU 38 | MEH9y+3gUvolpyDhCATEkBC65fEgUiOir/L6U1rxCdZ9gr7wxkheELEAqabPlg1M 39 | 2wZKbstlu+pWfV5f01OdKnlufjOM9MVXlgBgg9CAQa3NpaGTiJcNVnZ1kL+uny3X 40 | 7IylGlkCggEATH/wO+3ArugHM78wKCVkIfCldukhk1QwgPdZA78vqtqn7XPaT6sX 41 | fyl3yh3hgffWlUKqx4oAO4ex3yS8jQUSNmPlmvDGJu4GlkdHqob6zM6IXuBbjTu3 42 | +WS3yF3gtB8wcN0gJ6bKuPYKFZBJTmk/EDoZTIwSZOhz7axQihXiY/kaG4Z5zxpG 43 | +Wq7Bt96zyqCG6Xa/5BO1v0ZrpQoimK65ardQjqgShvWmiXKF4UD+9jaKKAzLQuW 44 | APJq2Toy33YwdKFw2UD6+6aJX4+IcAiW94g5XonWKFXULcn6JlCkkYr6uW+6TJv0 45 | dM5qdXcS6+t10rL7q94dmEFUlOEoUW1IwQKCAQA4687RyeOp0wS21ZFpRwAhuWkQ 46 | ZxPNeOG/lxERYD/rj3cE/zSdqun+Z2HwD3tndjbjOZhw5XoIfsRl7PsOGx5ngrmI 47 | fdYE8n25myO1TQADblD79/kypYauXLbquJwXRNhGqzJPBNlN5rga0OzbK3YH+oG0 48 | sndBVW3UIr071Zs3dO45+EJKbgKU2sYgi7yhMaSVkZxey3BteRBhqzqWlgUfqMwK 49 | Nbj1vuE/Ghso0dbZiYDX9IxrS7BT4ddZ0Wm24KIj1WlXNKv/zZhqktlh+GM2pRlx 50 | DQyYdp4njnkFpRuDSMSAW5V3/zyFnsZpH4ToT+MQwKFe0p7T00evdPdojRST 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJTCCAg2gAwIBAgIQINVcdjlrWhsKxwcXL4vbUDANBgkqhkiG9w0BAQsFADAT 3 | MREwDwYDVQQDEwh3cm9uZy1jYTAeFw0xNzA4MjUxNTQ1NThaFw0yNzA4MjUxNTQ1 4 | NTZaMBcxFTATBgNVBAMTDHdyb25nLWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBANHAXfyT8vuNKc3FeedZH3865bF5PLbRs2R8CaUOdQj/HTM5 6 | xSsb5Tr3x6IkWK+5SwtnGdaLY7GktSktXyUNf2uZflXHCLAqiqcBpLNO9mcFAACz 7 | pRb7C18ZZ6d9b7UtPA5oK1Vt45iUzI+mdCC0BtRTWeyKdtTF4muD2TtF7RMQwnjf 8 | RGV1EkfQ3sKpX3P7daiA/W116NlESpX3J/VAzoQu+3BrDeXrqgEbNVl+/NN/7uA0 9 | RY0HxE75RNhL9yuz1VFP4/NaFOdWN3pSMKwcmiIeNC5n0eyW/fodQpOT/dcscfbP 10 | 3a0XeoZkfB8nuVcGkmtUkuw0jPy+R64vnQvlugsCAwEAAaNxMG8wDgYDVR0PAQH/ 11 | BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU 12 | 0pX6qc7YwiehN5AYh+p4JNkFEXwwHwYDVR0jBBgwFoAUYzrnn1JGlfpUmUpsg5u2 13 | ste9zWswDQYJKoZIhvcNAQELBQADggIBALH5pFNhbnro2vFE+8RqbRqOZZNoyKqL 14 | INY/e0MNPmhp4CE2BQcrxgFcRgJmyh4lOP8gmIHT8q/9kOvYqMmfU1vbVF/XLFsa 15 | NJxkQSX9uilV1LDykyRbwlI4McjdTW7rLEkW8YrZueMXnDYQHGx9L2qYWgXzA5yA 16 | Mfsgq3pr39sDVDfYg1H+0daA3nIw+OWDsWjORXvzo5TQzjUXLhREp6WuuRKBT1+p 17 | VHGAnUcwDEb6L1bWEloG9ogXJfsXuCUxF+/II1RYSKiAmjge1nDOM2USfIKfD5nz 18 | tLJn0pn0B5dyceJTgOK6dwCXwn0Gc99qVzBSSHtPe+abSuY5dNoIwtL4R4rDE4U+ 19 | +y2vQwzum+GhHn/ZEDuYT/0+IDqkVxeWBiZ5IFEkRpBEFpmEJOdKWaSrIQWMpIjf 20 | FIlxY3VzUD8H5M65kMSRKXbRJ1zSHMcIFKK2R98SPuYnYmgc4kOh49WkEr6dj2B1 21 | 0QNZxPg70HP3qWgCxf8F5Mxg5YOtz7gN6N3AutrlYV0KB/OT0h4lhtvW3inxRgID 22 | iAHw0A4X/1qbFUeSUpINZaVQFtBh6fT/JfYDDTFFBcoqrLOZpFaS7r6FbGP3FDS4 23 | v9MqsYOSA6LrOHMdRop+eDV718iGDcUVtIItRZZV0s4UTo5q0JpGBtVPo/R8ui19 24 | eaGxvLJT1Gd+ 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICXDCCAUQCAQAwFzEVMBMGA1UEAxMMd3JvbmctY2xpZW50MIIBIjANBgkqhkiG 3 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cBd/JPy+40pzcV551kffzrlsXk8ttGzZHwJ 4 | pQ51CP8dMznFKxvlOvfHoiRYr7lLC2cZ1otjsaS1KS1fJQ1/a5l+VccIsCqKpwGk 5 | s072ZwUAALOlFvsLXxlnp31vtS08DmgrVW3jmJTMj6Z0ILQG1FNZ7Ip21MXia4PZ 6 | O0XtExDCeN9EZXUSR9Dewqlfc/t1qID9bXXo2URKlfcn9UDOhC77cGsN5euqARs1 7 | WX7803/u4DRFjQfETvlE2Ev3K7PVUU/j81oU51Y3elIwrByaIh40LmfR7Jb9+h1C 8 | k5P91yxx9s/drRd6hmR8Hye5VwaSa1SS7DSM/L5Hri+dC+W6CwIDAQABoAAwDQYJ 9 | KoZIhvcNAQELBQADggEBAMxscjfVRQ0/0c6f0MWtJJe+vy5Gj26XHVy5EsbH1ofq 10 | eWF00CFlVw5CdznGV0NL6LOE+sz5sBKsN2sZU7xPeV5XRHVXpAuECcOcgWK6FkqA 11 | wSmwVWZ93o+kJXrUZTyZBMkvQMUUr30JIpXIXJmLWKPBq5KRBJLirHZYw4FmbARv 12 | iZdNlQ1rLOZKZl7yUVkAfyfw+ueb0OPFp/fzuCerNB0ySSmYdHzqDFsMLm4Bq/2z 13 | FJOgasAU2RvxFVoRp7P/ZUSvtOKACMUHaYBnKZAvkKv3MIS/qLCSkzxNwsclT8uF 14 | aKRFZnOkZZHvEaXVAwCfJB7tI3TELb+L2KyqU36Q1Oc= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /internal/testing/tls/wrong-client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA0cBd/JPy+40pzcV551kffzrlsXk8ttGzZHwJpQ51CP8dMznF 3 | KxvlOvfHoiRYr7lLC2cZ1otjsaS1KS1fJQ1/a5l+VccIsCqKpwGks072ZwUAALOl 4 | FvsLXxlnp31vtS08DmgrVW3jmJTMj6Z0ILQG1FNZ7Ip21MXia4PZO0XtExDCeN9E 5 | ZXUSR9Dewqlfc/t1qID9bXXo2URKlfcn9UDOhC77cGsN5euqARs1WX7803/u4DRF 6 | jQfETvlE2Ev3K7PVUU/j81oU51Y3elIwrByaIh40LmfR7Jb9+h1Ck5P91yxx9s/d 7 | rRd6hmR8Hye5VwaSa1SS7DSM/L5Hri+dC+W6CwIDAQABAoIBAHml4JyRTdX4q+sM 8 | gcPcG3lVtkt0rfK1sh4wFgPlW5kpJE1GTwTOe+b0N5LhE5Jum4h0djbIxrwLc4n7 9 | J3g82M6VygCDm5VYRuvO9y+LNzrOWo8NoUyvsouoF0a7aCMipfcRETjNr7cZbX5O 10 | ooEpB+Dyqm+Wao7CaavDXySSTInGHG4AD9HM5nQsVIebS1HOkhI7SmNkZTOd3gzp 11 | bR/iZgaYI5eC7Zj7hHNr4gWdRBuefU8wLZZGoqByHRTSrKwICRLIkGyoMRAD9p9r 12 | S48lyUmd3BGHRPLmNl4u0kfsVlcCYBNKVZV6kkSVv4Wht/KVr3tl0+2wrNUe17w7 13 | vlCsCPECgYEA9AdHq3UjswD6PYITCdrDOGVLMpgL0obA6X2Fi6GsMiRXQgsvxMVY 14 | a4D2vtfZvLa8TSA+b8bK6uSMyD3mOHLxBkUPMQZxiHzq/ldK9vSIvzky0woF6suT 15 | J8fa2F0QfjlKWhFMwf6JVGyZl+vmYqqF55lxRJSWGZHSSxaxYPU7dbkCgYEA3Aqb 16 | YInpCp7zSlWYfXorwnyvsHsTdFbsoGGgMrc5l2B7PcP+Na7lm0dsNCrp7n7TSrE0 17 | 8iIoqhYq5u7RlGf5QXzXcGgZQMHcxLcrqBPviEktuUifXZEwfw9NXrlt4iF/oVTc 18 | ++7jqUZ+iH9NIMoxrQPXVdXJSJN9iwc7/yG6j+MCgYA+ehqsWCpSqx5mXwYW0M6I 19 | gs6U3n6wYNXFMeDeFf9rOwioHQsW2tu/cl46EDNr8HEXYfj6TzAmoWs13TszGqKA 20 | 02+HQroQksLraVgFEChupOtRQtCvA33ignWSTYlqd6qEksdPJ6brWX6decUbX8M2 21 | v39TaqNfWok3tlClnUOi6QKBgQCIhfQ9g5OZyWE937nLMH/yHZaMMvCxIDWUlL3m 22 | eZQ7/dq5Sd9xw2AmZbwW6gFWvk2ubCBjkxoT3ckkm0xhfdlC7ohk79GrQh0N2HA3 23 | ypa1wmGiMhLe5PRoAUCJ4xbwVMRxfsvVbDTIlDpxyjo6e/kyVc3HLevDIe+k0QpC 24 | k9TC7QKBgDyghEPAM5euQHk2o7cMr0YK52vzoM1FGhrRAF5MhTYJEYBNu0Z+0McB 25 | G8kwy4WH5zMuvKj1zZckAzkbpD/iL3XQuzs9pZdNnXzdf25/us0pZ2a/6v5+fpmF 26 | JEuwQ1AztPEv4tLd3+xrmE+j+qd3xDqYt8eaWFswcuxchB6nUdqq 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /invoke.go: -------------------------------------------------------------------------------- 1 | package grpcurl 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "github.com/golang/protobuf/jsonpb" //lint:ignore SA1019 we have to import these because some of their types appear in exported API 13 | "github.com/golang/protobuf/proto" //lint:ignore SA1019 same as above 14 | "github.com/jhump/protoreflect/desc" //lint:ignore SA1019 same as above 15 | "github.com/jhump/protoreflect/dynamic" //lint:ignore SA1019 same as above 16 | "github.com/jhump/protoreflect/dynamic/grpcdynamic" 17 | "github.com/jhump/protoreflect/grpcreflect" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/metadata" 21 | "google.golang.org/grpc/status" 22 | ) 23 | 24 | // InvocationEventHandler is a bag of callbacks for handling events that occur in the course 25 | // of invoking an RPC. The handler also provides request data that is sent. The callbacks are 26 | // generally called in the order they are listed below. 27 | type InvocationEventHandler interface { 28 | // OnResolveMethod is called with a descriptor of the method that is being invoked. 29 | OnResolveMethod(*desc.MethodDescriptor) 30 | // OnSendHeaders is called with the request metadata that is being sent. 31 | OnSendHeaders(metadata.MD) 32 | // OnReceiveHeaders is called when response headers have been received. 33 | OnReceiveHeaders(metadata.MD) 34 | // OnReceiveResponse is called for each response message received. 35 | OnReceiveResponse(proto.Message) 36 | // OnReceiveTrailers is called when response trailers and final RPC status have been received. 37 | OnReceiveTrailers(*status.Status, metadata.MD) 38 | } 39 | 40 | // RequestMessageSupplier is a function that is called to retrieve request 41 | // messages for a GRPC operation. This type is deprecated and will be removed in 42 | // a future release. 43 | // 44 | // Deprecated: This is only used with the deprecated InvokeRpc. Instead, use 45 | // RequestSupplier with InvokeRPC. 46 | type RequestMessageSupplier func() ([]byte, error) 47 | 48 | // InvokeRpc uses the given gRPC connection to invoke the given method. This function is deprecated 49 | // and will be removed in a future release. It just delegates to the similarly named InvokeRPC 50 | // method, whose signature is only slightly different. 51 | // 52 | // Deprecated: use InvokeRPC instead. 53 | func InvokeRpc(ctx context.Context, source DescriptorSource, cc *grpc.ClientConn, methodName string, 54 | headers []string, handler InvocationEventHandler, requestData RequestMessageSupplier) error { 55 | 56 | return InvokeRPC(ctx, source, cc, methodName, headers, handler, func(m proto.Message) error { 57 | // New function is almost identical, but the request supplier function works differently. 58 | // So we adapt the logic here to maintain compatibility. 59 | data, err := requestData() 60 | if err != nil { 61 | return err 62 | } 63 | return jsonpb.Unmarshal(bytes.NewReader(data), m) 64 | }) 65 | } 66 | 67 | // RequestSupplier is a function that is called to populate messages for a gRPC operation. The 68 | // function should populate the given message or return a non-nil error. If the supplier has no 69 | // more messages, it should return io.EOF. When it returns io.EOF, it should not in any way 70 | // modify the given message argument. 71 | type RequestSupplier func(proto.Message) error 72 | 73 | // InvokeRPC uses the given gRPC channel to invoke the given method. The given descriptor source 74 | // is used to determine the type of method and the type of request and response message. The given 75 | // headers are sent as request metadata. Methods on the given event handler are called as the 76 | // invocation proceeds. 77 | // 78 | // The given requestData function supplies the actual data to send. It should return io.EOF when 79 | // there is no more request data. If the method being invoked is a unary or server-streaming RPC 80 | // (e.g. exactly one request message) and there is no request data (e.g. the first invocation of 81 | // the function returns io.EOF), then an empty request message is sent. 82 | // 83 | // If the requestData function and the given event handler coordinate or share any state, they should 84 | // be thread-safe. This is because the requestData function may be called from a different goroutine 85 | // than the one invoking event callbacks. (This only happens for bi-directional streaming RPCs, where 86 | // one goroutine sends request messages and another consumes the response messages). 87 | func InvokeRPC(ctx context.Context, source DescriptorSource, ch grpcdynamic.Channel, methodName string, 88 | headers []string, handler InvocationEventHandler, requestData RequestSupplier) error { 89 | 90 | md := MetadataFromHeaders(headers) 91 | 92 | svc, mth := parseSymbol(methodName) 93 | if svc == "" || mth == "" { 94 | return fmt.Errorf("given method name %q is not in expected format: 'service/method' or 'service.method'", methodName) 95 | } 96 | 97 | dsc, err := source.FindSymbol(svc) 98 | if err != nil { 99 | // return a gRPC status error if hasStatus is true 100 | errStatus, hasStatus := status.FromError(err) 101 | switch { 102 | case hasStatus && isNotFoundError(err): 103 | return status.Errorf(errStatus.Code(), "target server does not expose service %q: %s", svc, errStatus.Message()) 104 | case hasStatus: 105 | return status.Errorf(errStatus.Code(), "failed to query for service descriptor %q: %s", svc, errStatus.Message()) 106 | case isNotFoundError(err): 107 | return fmt.Errorf("target server does not expose service %q", svc) 108 | } 109 | return fmt.Errorf("failed to query for service descriptor %q: %v", svc, err) 110 | } 111 | sd, ok := dsc.(*desc.ServiceDescriptor) 112 | if !ok { 113 | return fmt.Errorf("target server does not expose service %q", svc) 114 | } 115 | mtd := sd.FindMethodByName(mth) 116 | if mtd == nil { 117 | return fmt.Errorf("service %q does not include a method named %q", svc, mth) 118 | } 119 | 120 | handler.OnResolveMethod(mtd) 121 | 122 | // we also download any applicable extensions so we can provide full support for parsing user-provided data 123 | var ext dynamic.ExtensionRegistry 124 | alreadyFetched := map[string]bool{} 125 | if err = fetchAllExtensions(source, &ext, mtd.GetInputType(), alreadyFetched); err != nil { 126 | return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetInputType().GetFullyQualifiedName(), err) 127 | } 128 | if err = fetchAllExtensions(source, &ext, mtd.GetOutputType(), alreadyFetched); err != nil { 129 | return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetOutputType().GetFullyQualifiedName(), err) 130 | } 131 | 132 | msgFactory := dynamic.NewMessageFactoryWithExtensionRegistry(&ext) 133 | req := msgFactory.NewMessage(mtd.GetInputType()) 134 | 135 | handler.OnSendHeaders(md) 136 | ctx = metadata.NewOutgoingContext(ctx, md) 137 | 138 | stub := grpcdynamic.NewStubWithMessageFactory(ch, msgFactory) 139 | ctx, cancel := context.WithCancel(ctx) 140 | defer cancel() 141 | 142 | if mtd.IsClientStreaming() && mtd.IsServerStreaming() { 143 | return invokeBidi(ctx, stub, mtd, handler, requestData, req) 144 | } else if mtd.IsClientStreaming() { 145 | return invokeClientStream(ctx, stub, mtd, handler, requestData, req) 146 | } else if mtd.IsServerStreaming() { 147 | return invokeServerStream(ctx, stub, mtd, handler, requestData, req) 148 | } else { 149 | return invokeUnary(ctx, stub, mtd, handler, requestData, req) 150 | } 151 | } 152 | 153 | func invokeUnary(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, 154 | requestData RequestSupplier, req proto.Message) error { 155 | 156 | err := requestData(req) 157 | if err != nil && err != io.EOF { 158 | return fmt.Errorf("error getting request data: %v", err) 159 | } 160 | if err != io.EOF { 161 | // verify there is no second message, which is a usage error 162 | err := requestData(req) 163 | if err == nil { 164 | return fmt.Errorf("method %q is a unary RPC, but request data contained more than 1 message", md.GetFullyQualifiedName()) 165 | } else if err != io.EOF { 166 | return fmt.Errorf("error getting request data: %v", err) 167 | } 168 | } 169 | 170 | // Now we can actually invoke the RPC! 171 | var respHeaders metadata.MD 172 | var respTrailers metadata.MD 173 | resp, err := stub.InvokeRpc(ctx, md, req, grpc.Trailer(&respTrailers), grpc.Header(&respHeaders)) 174 | 175 | stat, ok := status.FromError(err) 176 | if !ok { 177 | // Error codes sent from the server will get printed differently below. 178 | // So just bail for other kinds of errors here. 179 | return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) 180 | } 181 | 182 | handler.OnReceiveHeaders(respHeaders) 183 | 184 | if stat.Code() == codes.OK { 185 | handler.OnReceiveResponse(resp) 186 | } 187 | 188 | handler.OnReceiveTrailers(stat, respTrailers) 189 | 190 | return nil 191 | } 192 | 193 | func invokeClientStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, 194 | requestData RequestSupplier, req proto.Message) error { 195 | 196 | // invoke the RPC! 197 | str, err := stub.InvokeRpcClientStream(ctx, md) 198 | 199 | // Upload each request message in the stream 200 | var resp proto.Message 201 | for err == nil { 202 | err = requestData(req) 203 | if err == io.EOF { 204 | resp, err = str.CloseAndReceive() 205 | break 206 | } 207 | if err != nil { 208 | return fmt.Errorf("error getting request data: %v", err) 209 | } 210 | 211 | err = str.SendMsg(req) 212 | if err == io.EOF { 213 | // We get EOF on send if the server says "go away" 214 | // We have to use CloseAndReceive to get the actual code 215 | resp, err = str.CloseAndReceive() 216 | break 217 | } 218 | 219 | req.Reset() 220 | } 221 | 222 | // finally, process response data 223 | stat, ok := status.FromError(err) 224 | if !ok { 225 | // Error codes sent from the server will get printed differently below. 226 | // So just bail for other kinds of errors here. 227 | return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) 228 | } 229 | 230 | if str != nil { 231 | if respHeaders, err := str.Header(); err == nil { 232 | handler.OnReceiveHeaders(respHeaders) 233 | } 234 | } 235 | 236 | if stat.Code() == codes.OK { 237 | handler.OnReceiveResponse(resp) 238 | } 239 | 240 | if str != nil { 241 | handler.OnReceiveTrailers(stat, str.Trailer()) 242 | } 243 | 244 | return nil 245 | } 246 | 247 | func invokeServerStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, 248 | requestData RequestSupplier, req proto.Message) error { 249 | 250 | err := requestData(req) 251 | if err != nil && err != io.EOF { 252 | return fmt.Errorf("error getting request data: %v", err) 253 | } 254 | if err != io.EOF { 255 | // verify there is no second message, which is a usage error 256 | err := requestData(req) 257 | if err == nil { 258 | return fmt.Errorf("method %q is a server-streaming RPC, but request data contained more than 1 message", md.GetFullyQualifiedName()) 259 | } else if err != io.EOF { 260 | return fmt.Errorf("error getting request data: %v", err) 261 | } 262 | } 263 | 264 | // Now we can actually invoke the RPC! 265 | str, err := stub.InvokeRpcServerStream(ctx, md, req) 266 | 267 | if str != nil { 268 | if respHeaders, err := str.Header(); err == nil { 269 | handler.OnReceiveHeaders(respHeaders) 270 | } 271 | } 272 | 273 | // Download each response message 274 | for err == nil { 275 | var resp proto.Message 276 | resp, err = str.RecvMsg() 277 | if err != nil { 278 | if err == io.EOF { 279 | err = nil 280 | } 281 | break 282 | } 283 | handler.OnReceiveResponse(resp) 284 | } 285 | 286 | stat, ok := status.FromError(err) 287 | if !ok { 288 | // Error codes sent from the server will get printed differently below. 289 | // So just bail for other kinds of errors here. 290 | return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) 291 | } 292 | 293 | if str != nil { 294 | handler.OnReceiveTrailers(stat, str.Trailer()) 295 | } 296 | 297 | return nil 298 | } 299 | 300 | func invokeBidi(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler, 301 | requestData RequestSupplier, req proto.Message) error { 302 | 303 | ctx, cancel := context.WithCancel(ctx) 304 | defer cancel() 305 | 306 | // invoke the RPC! 307 | str, err := stub.InvokeRpcBidiStream(ctx, md) 308 | 309 | var wg sync.WaitGroup 310 | var sendErr atomic.Value 311 | 312 | defer wg.Wait() 313 | 314 | if err == nil { 315 | wg.Add(1) 316 | go func() { 317 | defer wg.Done() 318 | 319 | // Concurrently upload each request message in the stream 320 | var err error 321 | for err == nil { 322 | err = requestData(req) 323 | 324 | if err == io.EOF { 325 | err = str.CloseSend() 326 | break 327 | } 328 | if err != nil { 329 | err = fmt.Errorf("error getting request data: %v", err) 330 | cancel() 331 | break 332 | } 333 | 334 | err = str.SendMsg(req) 335 | 336 | req.Reset() 337 | } 338 | 339 | if err != nil { 340 | sendErr.Store(err) 341 | } 342 | }() 343 | } 344 | 345 | if str != nil { 346 | if respHeaders, err := str.Header(); err == nil { 347 | handler.OnReceiveHeaders(respHeaders) 348 | } 349 | } 350 | 351 | // Download each response message 352 | for err == nil { 353 | var resp proto.Message 354 | resp, err = str.RecvMsg() 355 | if err != nil { 356 | if err == io.EOF { 357 | err = nil 358 | } 359 | break 360 | } 361 | handler.OnReceiveResponse(resp) 362 | } 363 | 364 | if se, ok := sendErr.Load().(error); ok && se != io.EOF { 365 | err = se 366 | } 367 | 368 | stat, ok := status.FromError(err) 369 | if !ok { 370 | // Error codes sent from the server will get printed differently below. 371 | // So just bail for other kinds of errors here. 372 | return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err) 373 | } 374 | 375 | if str != nil { 376 | handler.OnReceiveTrailers(stat, str.Trailer()) 377 | } 378 | 379 | return nil 380 | } 381 | 382 | type notFoundError string 383 | 384 | func notFound(kind, name string) error { 385 | return notFoundError(fmt.Sprintf("%s not found: %s", kind, name)) 386 | } 387 | 388 | func (e notFoundError) Error() string { 389 | return string(e) 390 | } 391 | 392 | func isNotFoundError(err error) bool { 393 | if grpcreflect.IsElementNotFoundError(err) { 394 | return true 395 | } 396 | _, ok := err.(notFoundError) 397 | return ok 398 | } 399 | 400 | func parseSymbol(svcAndMethod string) (string, string) { 401 | pos := strings.LastIndex(svcAndMethod, "/") 402 | if pos < 0 { 403 | pos = strings.LastIndex(svcAndMethod, ".") 404 | if pos < 0 { 405 | return "", "" 406 | } 407 | } 408 | return svcAndMethod[:pos], svcAndMethod[pos+1:] 409 | } 410 | -------------------------------------------------------------------------------- /mk-test-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname $0)" 6 | 7 | # Run this script to generate files used by tests. 8 | 9 | echo "Creating protosets..." 10 | protoc testing/test.proto \ 11 | --include_imports \ 12 | --descriptor_set_out=testing/test.protoset 13 | 14 | protoc testing/example.proto \ 15 | --include_imports \ 16 | --descriptor_set_out=testing/example.protoset 17 | 18 | protoc testing/jsonpb_test_proto/test_objects.proto \ 19 | --go_out=paths=source_relative:. 20 | 21 | echo "Creating certs for TLS testing..." 22 | if ! hash certstrap 2>/dev/null; then 23 | # certstrap not found: try to install it 24 | go get github.com/square/certstrap 25 | go install github.com/square/certstrap 26 | fi 27 | 28 | function cs() { 29 | certstrap --depot-path testing/tls "$@" --passphrase "" 30 | } 31 | 32 | rm -rf testing/tls 33 | 34 | # Create CA 35 | cs init --years 10 --common-name ca 36 | 37 | # Create client cert 38 | cs request-cert --common-name client 39 | cs sign client --years 10 --CA ca 40 | 41 | # Create server cert 42 | cs request-cert --common-name server --ip 127.0.0.1 --domain localhost 43 | cs sign server --years 10 --CA ca 44 | 45 | # Create another server cert for error testing 46 | cs request-cert --common-name other --ip 1.2.3.4 --domain foobar.com 47 | cs sign other --years 10 --CA ca 48 | 49 | # Create another CA and client cert for more 50 | # error testing 51 | cs init --years 10 --common-name wrong-ca 52 | cs request-cert --common-name wrong-client 53 | cs sign wrong-client --years 10 --CA wrong-ca 54 | 55 | # Create expired cert 56 | cs request-cert --common-name expired --ip 127.0.0.1 --domain localhost 57 | cs sign expired --years 0 --CA ca 58 | -------------------------------------------------------------------------------- /releasing/README.md: -------------------------------------------------------------------------------- 1 | # Releases of gRPCurl 2 | 3 | This document provides instructions for building a release of `grpcurl`. 4 | 5 | The release process consists of a handful of tasks: 6 | 1. Drop a release tag in git. 7 | 2. Build binaries for various platforms. This is done using the local `go` tool and uses `GOOS` and `GOARCH` environment variables to cross-compile for supported platforms. 8 | 3. Creates a release in GitHub, uploads the binaries, and creates provisional release notes (in the form of a change log). 9 | 4. Build a docker image for the new release. 10 | 5. Push the docker image to Docker Hub, with both a version tag and the "latest" tag. 11 | 6. Submits a PR to update the [Homebrew](https://brew.sh/) recipe with the latest version. 12 | 13 | Most of this is automated via a script in this same directory. The main thing you will need is a GitHub personal access token, which will be used for creating the release in GitHub (so you need write access to the fullstorydev/grpcurl repo) and to open a Homebrew pull request. 14 | 15 | ## Creating a new release 16 | 17 | So, to actually create a new release, just run the script in this directory. 18 | 19 | First, you need a version number for the new release, following sem-ver format: `v..`. Second, you need a personal access token for GitHub. 20 | 21 | We'll use `v2.3.4` as an example version and `abcdef0123456789abcdef` as an example GitHub token: 22 | 23 | ```sh 24 | # from the root of the repo 25 | GITHUB_TOKEN=abcdef0123456789abcd \ 26 | ./releasing/do-release.sh v2.3.4 27 | ``` 28 | 29 | Wasn't that easy! There is one last step: update the release notes in GitHub. By default, the script just records a change log of commit descriptions. Use that log (and, if necessary, drill into individual PRs included in the release) to flesh out notes in the format of the `RELEASE_NOTES.md` file _in this directory_. Then login to GitHub, go to the new release, edit the notes, and paste in the markdown you just wrote. 30 | 31 | That should be all there is to it! If things go wrong and you have to re-do part of the process, see the sections below. 32 | 33 | ---- 34 | 35 | ### GitHub Releases 36 | The GitHub release is the first step performed by the `do-release.sh` script. So generally, if there is an issue with that step, you can re-try the whole script. 37 | 38 | Note, if running the script did something wrong, you may have to first login to GitHub and remove uploaded artifacts for a botched release attempt. In general, this is _very undesirable_. Releases should usually be considered immutable. Instead of removing uploaded assets and providing new ones, it is often better to remove uploaded assets (to make bad binaries no longer available) and then _release a new patch version_. (You can edit the release notes for the botched version explaining why there are no artifacts for it.) 39 | 40 | The steps to do a GitHub-only release (vs. running the entire script) are the following: 41 | 42 | ```sh 43 | # from the root of the repo 44 | git tag v2.3.4 45 | GITHUB_TOKEN=abcdef0123456789abcdef \ 46 | GO111MODULE=on \ 47 | make release 48 | ``` 49 | 50 | The `git tag ...` step is necessary because the release target requires that the current SHA have a sem-ver tag. That's the version it will use when creating the release. 51 | 52 | This will create the release in GitHub with provisional release notes that just include a change log of commit messages. You still need to login to GitHub and revise those notes to adhere to the recommended format. (See `RELEASE_NOTES.md` in this directory.) 53 | 54 | ### Docker Hub Releases 55 | 56 | To re-run only the Docker Hub release steps, you can manually run through each step in the "Docker" section of `do_release.sh`. 57 | 58 | If the `docker push ...` steps fail, you may need to run `docker login`, enter your Docker Hub login credentials, and then try to push again. 59 | 60 | ### Homebrew Releases 61 | 62 | The last step is to update the Homebrew recipe to use the latest version. First, we need to compute the SHA256 checksum for the source archive: 63 | 64 | ```sh 65 | # download the source archive from GitHub 66 | URL=https://github.com/fullstorydev/grpcurl/archive/refs/tags/v2.3.4.tar.gz 67 | curl -L -o tmp.tgz $URL 68 | # and compute the SHA 69 | SHA="$(sha256sum < tmp.tgz | awk '{ print $1 }')" 70 | ``` 71 | 72 | To actually create the brew PR, you need your GitHub personal access token again, as well as the URL and SHA from the previous step: 73 | 74 | ```sh 75 | HOMEBREW_GITHUB_API_TOKEN=abcdef0123456789abcdef \ 76 | brew bump-formula-pr --url $URL --sha256 $SHA grpcurl 77 | ``` 78 | 79 | This creates a PR to bump the formula to the new version. When this PR is merged by brew maintainers, the new version becomes available! 80 | -------------------------------------------------------------------------------- /releasing/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | ### Command-line tool 4 | 5 | * _In this list, describe the changes to the command-line tool._ 6 | * _Use one bullet per change. Include both bug-fixes and improvements. Omit this section if there are no changes that impact the command-line tool._ 7 | 8 | ### Go package "github.com/fullstorydev/grpcurl" 9 | 10 | * _In this list, describe the changes to exported API in the main package in this repo: "github.com/fullstorydev/grpcurl". These will often be closely related to changes to the command-line tool, though not always: changes that only impact the cmd/grpcurl directory of this repo do not impact exported API._ 11 | * _Use one bullet per change. Include both bug-fixes and improvements. Omit this section if there are no changes that impact the exported API._ -------------------------------------------------------------------------------- /releasing/do-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # strict mode 4 | set -euo pipefail 5 | IFS=$'\n\t' 6 | 7 | if [[ -z ${DRY_RUN:-} ]]; then 8 | PREFIX="" 9 | else 10 | PREFIX="echo" 11 | fi 12 | 13 | # input validation 14 | if [[ -z ${GITHUB_TOKEN:-} ]]; then 15 | echo "GITHUB_TOKEN environment variable must be set before running." >&2 16 | exit 1 17 | fi 18 | if [[ $# -ne 1 || $1 == "" ]]; then 19 | echo "This program requires one argument: the version number, in 'vM.N.P' format." >&2 20 | exit 1 21 | fi 22 | VERSION=$1 23 | 24 | # Change to root of the repo 25 | cd "$(dirname "$0")/.." 26 | 27 | # GitHub release 28 | 29 | $PREFIX git tag "$VERSION" 30 | # make sure GITHUB_TOKEN is exported, for the benefit of this next command 31 | export GITHUB_TOKEN 32 | GO111MODULE=on $PREFIX make release 33 | # if that was successful, it could have touched go.mod and go.sum, so revert those 34 | $PREFIX git checkout go.mod go.sum 35 | 36 | # Docker release 37 | 38 | # make sure credentials are valid for later push steps; this might 39 | # be interactive since this will prompt for username and password 40 | # if there are no valid current credentials. 41 | $PREFIX docker login 42 | echo "$VERSION" > VERSION 43 | 44 | # Docker Buildx support is included in Docker 19.03 45 | # Below step installs emulators for different architectures on the host 46 | # This enables running and building containers for below architectures mentioned using --platforms 47 | $PREFIX docker run --privileged --rm tonistiigi/binfmt:qemu-v6.1.0 --install all 48 | # Create a new builder instance 49 | export DOCKER_CLI_EXPERIMENTAL=enabled 50 | $PREFIX docker buildx create --use --name multiarch-builder --node multiarch-builder0 51 | # push to docker hub, both the given version as a tag and for "latest" tag 52 | $PREFIX docker buildx build --platform linux/amd64,linux/s390x,linux/arm64,linux/ppc64le --tag fullstorydev/grpcurl:${VERSION} --tag fullstorydev/grpcurl:latest --push --progress plain --no-cache . 53 | $PREFIX docker buildx build --platform linux/amd64,linux/s390x,linux/arm64,linux/ppc64le --tag fullstorydev/grpcurl:${VERSION}-alpine --tag fullstorydev/grpcurl:latest-alpine --push --progress plain --no-cache --target alpine . 54 | rm VERSION 55 | 56 | # Homebrew release 57 | 58 | URL="https://github.com/fullstorydev/grpcurl/archive/refs/tags/${VERSION}.tar.gz" 59 | curl -L -o tmp.tgz "$URL" 60 | SHA="$(sha256sum < tmp.tgz | awk '{ print $1 }')" 61 | rm tmp.tgz 62 | HOMEBREW_GITHUB_API_TOKEN="$GITHUB_TOKEN" $PREFIX brew bump-formula-pr --url "$URL" --sha256 "$SHA" grpcurl 63 | -------------------------------------------------------------------------------- /snap/README.md: -------------------------------------------------------------------------------- 1 | # packing and releasing 2 | To pack the current branch to a snap package: 3 | 4 | `snapcraft pack` 5 | 6 | To install the package locally: 7 | 8 | `snap install ./grpcurl_v[version tag]_amd64.snap --devmode` 9 | 10 | To upload the snap to the edge channel: 11 | 12 | `snapcraft upload --release edge ./grpcurl_v[version tag]_amd64.snap` 13 | 14 | (you need to own the package name registration for this!) 15 | 16 | # ownership 17 | The snap's current owner is `pietro.pasotti@canonical.com`; who is very happy to support with maintaining the snap distribution and/or transfer its ownership to the developers. 18 | 19 | Please reach out to me for questions regarding the snap; including: 20 | - adding support for other architectures 21 | - automating the release 22 | 23 | Cheers and thanks for the awesome tool! -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: grpcurl 2 | base: core24 3 | # allow grpcurl part to call craftctl set-version 4 | adopt-info: grpcurl 5 | summary: grpcurl is a command-line tool that lets you interact with gRPC servers. 6 | 7 | description: | 8 | grpcurl is a command-line tool that lets you interact with gRPC servers. 9 | It's basically curl for gRPC servers. 10 | 11 | grade: stable 12 | confinement: strict 13 | license: MIT 14 | 15 | apps: 16 | grpcurl: 17 | command: grpcurl 18 | plugs: 19 | - network 20 | 21 | parts: 22 | grpcurl: 23 | plugin: go 24 | build-snaps: [go/latest/stable] 25 | source: https://github.com/fullstorydev/grpcurl 26 | source-type: git 27 | override-build: | 28 | tag="$(git describe --tags --abbrev=0)" 29 | craftctl set version="$tag" 30 | 31 | go build -o $CRAFT_PART_INSTALL/grpcurl ./cmd/grpcurl/grpcurl.go 32 | 33 | # adjust the permissions 34 | chmod 0755 $CRAFT_PART_INSTALL/grpcurl 35 | -------------------------------------------------------------------------------- /tls_settings_test.go: -------------------------------------------------------------------------------- 1 | package grpcurl_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials" 13 | 14 | . "github.com/fullstorydev/grpcurl" 15 | grpcurl_testing "github.com/fullstorydev/grpcurl/internal/testing" 16 | ) 17 | 18 | func TestPlainText(t *testing.T) { 19 | e, err := createTestServerAndClient(nil, nil) 20 | if err != nil { 21 | t.Fatalf("failed to setup server and client: %v", err) 22 | } 23 | defer e.Close() 24 | 25 | simpleTest(t, e.cc) 26 | } 27 | 28 | func TestBasicTLS(t *testing.T) { 29 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 30 | if err != nil { 31 | t.Fatalf("failed to create server creds: %v", err) 32 | } 33 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "") 34 | if err != nil { 35 | t.Fatalf("failed to create server creds: %v", err) 36 | } 37 | 38 | e, err := createTestServerAndClient(serverCreds, clientCreds) 39 | if err != nil { 40 | t.Fatalf("failed to setup server and client: %v", err) 41 | } 42 | defer e.Close() 43 | 44 | simpleTest(t, e.cc) 45 | } 46 | 47 | func TestInsecureClientTLS(t *testing.T) { 48 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 49 | if err != nil { 50 | t.Fatalf("failed to create server creds: %v", err) 51 | } 52 | clientCreds, err := ClientTransportCredentials(true, "", "", "") 53 | if err != nil { 54 | t.Fatalf("failed to create server creds: %v", err) 55 | } 56 | 57 | e, err := createTestServerAndClient(serverCreds, clientCreds) 58 | if err != nil { 59 | t.Fatalf("failed to setup server and client: %v", err) 60 | } 61 | defer e.Close() 62 | 63 | simpleTest(t, e.cc) 64 | } 65 | 66 | func TestClientCertTLS(t *testing.T) { 67 | serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 68 | if err != nil { 69 | t.Fatalf("failed to create server creds: %v", err) 70 | } 71 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/client.crt", "internal/testing/tls/client.key") 72 | if err != nil { 73 | t.Fatalf("failed to create server creds: %v", err) 74 | } 75 | 76 | e, err := createTestServerAndClient(serverCreds, clientCreds) 77 | if err != nil { 78 | t.Fatalf("failed to setup server and client: %v", err) 79 | } 80 | defer e.Close() 81 | 82 | simpleTest(t, e.cc) 83 | } 84 | 85 | func TestRequireClientCertTLS(t *testing.T) { 86 | serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true) 87 | if err != nil { 88 | t.Fatalf("failed to create server creds: %v", err) 89 | } 90 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/client.crt", "internal/testing/tls/client.key") 91 | if err != nil { 92 | t.Fatalf("failed to create server creds: %v", err) 93 | } 94 | 95 | e, err := createTestServerAndClient(serverCreds, clientCreds) 96 | if err != nil { 97 | t.Fatalf("failed to setup server and client: %v", err) 98 | } 99 | defer e.Close() 100 | 101 | simpleTest(t, e.cc) 102 | } 103 | 104 | func TestBrokenTLS_ClientPlainText(t *testing.T) { 105 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 106 | if err != nil { 107 | t.Fatalf("failed to create server creds: %v", err) 108 | } 109 | 110 | // client connection (usually) succeeds since client is not waiting for TLS handshake 111 | // (we try several times, but if we never get a connection and the error message is 112 | // a known/expected possibility, we'll just bail) 113 | var e testEnv 114 | failCount := 0 115 | for { 116 | e, err = createTestServerAndClient(serverCreds, nil) 117 | if err == nil { 118 | // success! 119 | defer e.Close() 120 | break 121 | } 122 | 123 | if strings.Contains(err.Error(), "deadline exceeded") || 124 | strings.Contains(err.Error(), "use of closed network connection") { 125 | // It is possible that the connection never becomes healthy: 126 | // 1) grpc connects successfully 127 | // 2) grpc client tries to send HTTP/2 preface and settings frame 128 | // 3) server, expecting handshake, closes the connection 129 | // 4) in the client, the write fails, so the connection never 130 | // becomes ready 131 | // The client will attempt to reconnect on transient errors, so 132 | // may eventually bump into the connect time limit. This used to 133 | // result in a "deadline exceeded" error, but more recent versions 134 | // of the grpc library report any underlying I/O error instead, so 135 | // we also check for "use of closed network connection". 136 | failCount++ 137 | if failCount > 5 { 138 | return // bail... 139 | } 140 | // we'll try again 141 | 142 | } else { 143 | // some other error occurred, so we'll consider that a test failure 144 | t.Fatalf("failed to setup server and client: %v", err) 145 | } 146 | } 147 | 148 | // but request fails because server closes connection upon seeing request 149 | // bytes that are not a TLS handshake 150 | cl := grpcurl_testing.NewTestServiceClient(e.cc) 151 | _, err = cl.UnaryCall(context.Background(), &grpcurl_testing.SimpleRequest{}) 152 | if err == nil { 153 | t.Fatal("expecting failure") 154 | } 155 | // various errors possible when server closes connection 156 | if !strings.Contains(err.Error(), "transport is closing") && 157 | !strings.Contains(err.Error(), "connection is unavailable") && 158 | !strings.Contains(err.Error(), "use of closed network connection") && 159 | !strings.Contains(err.Error(), "all SubConns are in TransientFailure") { 160 | 161 | t.Fatalf("expecting transport failure, got: %v", err) 162 | } 163 | } 164 | 165 | func TestBrokenTLS_ServerPlainText(t *testing.T) { 166 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "") 167 | if err != nil { 168 | t.Fatalf("failed to create server creds: %v", err) 169 | } 170 | 171 | e, err := createTestServerAndClient(nil, clientCreds) 172 | if err == nil { 173 | e.Close() 174 | t.Fatal("expecting TLS failure setting up server and client") 175 | } 176 | if !strings.Contains(err.Error(), "first record does not look like a TLS handshake") { 177 | t.Fatalf("expecting TLS handshake failure, got: %v", err) 178 | } 179 | } 180 | 181 | func TestBrokenTLS_ServerUsesWrongCert(t *testing.T) { 182 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/other.crt", "internal/testing/tls/other.key", false) 183 | if err != nil { 184 | t.Fatalf("failed to create server creds: %v", err) 185 | } 186 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "") 187 | if err != nil { 188 | t.Fatalf("failed to create server creds: %v", err) 189 | } 190 | 191 | e, err := createTestServerAndClient(serverCreds, clientCreds) 192 | if err == nil { 193 | e.Close() 194 | t.Fatal("expecting TLS failure setting up server and client") 195 | } 196 | if !strings.Contains(err.Error(), "certificate is valid for") { 197 | t.Fatalf("expecting TLS certificate error, got: %v", err) 198 | } 199 | } 200 | 201 | func TestBrokenTLS_ClientHasExpiredCert(t *testing.T) { 202 | serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 203 | if err != nil { 204 | t.Fatalf("failed to create server creds: %v", err) 205 | } 206 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/expired.crt", "internal/testing/tls/expired.key") 207 | if err != nil { 208 | t.Fatalf("failed to create server creds: %v", err) 209 | } 210 | 211 | e, err := createTestServerAndClient(serverCreds, clientCreds) 212 | if err == nil { 213 | e.Close() 214 | t.Fatal("expecting TLS failure setting up server and client") 215 | } 216 | if !strings.Contains(err.Error(), "certificate") { 217 | t.Fatalf("expecting TLS certificate error, got: %v", err) 218 | } 219 | } 220 | 221 | func TestBrokenTLS_ServerHasExpiredCert(t *testing.T) { 222 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/expired.crt", "internal/testing/tls/expired.key", false) 223 | if err != nil { 224 | t.Fatalf("failed to create server creds: %v", err) 225 | } 226 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "") 227 | if err != nil { 228 | t.Fatalf("failed to create server creds: %v", err) 229 | } 230 | 231 | e, err := createTestServerAndClient(serverCreds, clientCreds) 232 | if err == nil { 233 | e.Close() 234 | t.Fatal("expecting TLS failure setting up server and client") 235 | } 236 | if !strings.Contains(err.Error(), "certificate has expired or is not yet valid") { 237 | t.Fatalf("expecting TLS certificate expired, got: %v", err) 238 | } 239 | } 240 | 241 | func TestBrokenTLS_ClientNotTrusted(t *testing.T) { 242 | serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true) 243 | if err != nil { 244 | t.Fatalf("failed to create server creds: %v", err) 245 | } 246 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/wrong-client.crt", "internal/testing/tls/wrong-client.key") 247 | if err != nil { 248 | t.Fatalf("failed to create server creds: %v", err) 249 | } 250 | 251 | e, err := createTestServerAndClient(serverCreds, clientCreds) 252 | if err == nil { 253 | e.Close() 254 | t.Fatal("expecting TLS failure setting up server and client") 255 | } 256 | if !strings.Contains(err.Error(), "bad certificate") { 257 | t.Fatalf("expecting TLS certificate error, got: %v", err) 258 | } 259 | } 260 | 261 | func TestBrokenTLS_ServerNotTrusted(t *testing.T) { 262 | serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false) 263 | if err != nil { 264 | t.Fatalf("failed to create server creds: %v", err) 265 | } 266 | clientCreds, err := ClientTransportCredentials(false, "", "internal/testing/tls/client.crt", "internal/testing/tls/client.key") 267 | if err != nil { 268 | t.Fatalf("failed to create server creds: %v", err) 269 | } 270 | 271 | e, err := createTestServerAndClient(serverCreds, clientCreds) 272 | if err == nil { 273 | e.Close() 274 | t.Fatal("expecting TLS failure setting up server and client") 275 | } 276 | if !strings.Contains(err.Error(), "certificate") { 277 | t.Fatalf("expecting TLS certificate error, got: %v", err) 278 | } 279 | } 280 | 281 | func TestBrokenTLS_RequireClientCertButNonePresented(t *testing.T) { 282 | serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true) 283 | if err != nil { 284 | t.Fatalf("failed to create server creds: %v", err) 285 | } 286 | clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "") 287 | if err != nil { 288 | t.Fatalf("failed to create server creds: %v", err) 289 | } 290 | 291 | e, err := createTestServerAndClient(serverCreds, clientCreds) 292 | if err == nil { 293 | e.Close() 294 | t.Fatal("expecting TLS failure setting up server and client") 295 | } 296 | if !strings.Contains(err.Error(), "bad certificate") { 297 | t.Fatalf("expecting TLS certificate error, got: %v", err) 298 | } 299 | } 300 | 301 | func simpleTest(t *testing.T, cc *grpc.ClientConn) { 302 | cl := grpcurl_testing.NewTestServiceClient(cc) 303 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 304 | defer cancel() 305 | _, err := cl.UnaryCall(ctx, &grpcurl_testing.SimpleRequest{}, grpc.WaitForReady(true)) 306 | if err != nil { 307 | t.Errorf("simple RPC failed: %v", err) 308 | } 309 | } 310 | 311 | func createTestServerAndClient(serverCreds, clientCreds credentials.TransportCredentials) (testEnv, error) { 312 | var e testEnv 313 | completed := false 314 | defer func() { 315 | if !completed { 316 | e.Close() 317 | } 318 | }() 319 | 320 | var svrOpts []grpc.ServerOption 321 | if serverCreds != nil { 322 | svrOpts = []grpc.ServerOption{grpc.Creds(serverCreds)} 323 | } 324 | svr := grpc.NewServer(svrOpts...) 325 | grpcurl_testing.RegisterTestServiceServer(svr, grpcurl_testing.TestServer{}) 326 | l, err := net.Listen("tcp", "127.0.0.1:0") 327 | if err != nil { 328 | return e, err 329 | } 330 | port := l.Addr().(*net.TCPAddr).Port 331 | go svr.Serve(l) 332 | 333 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 334 | defer cancel() 335 | 336 | cc, err := BlockingDial(ctx, "tcp", fmt.Sprintf("127.0.0.1:%d", port), clientCreds) 337 | if err != nil { 338 | return e, err 339 | } 340 | 341 | e.svr = svr 342 | e.cc = cc 343 | completed = true 344 | return e, nil 345 | } 346 | 347 | type testEnv struct { 348 | svr *grpc.Server 349 | cc *grpc.ClientConn 350 | } 351 | 352 | func (e *testEnv) Close() { 353 | if e.cc != nil { 354 | e.cc.Close() 355 | e.cc = nil 356 | } 357 | if e.svr != nil { 358 | e.svr.GracefulStop() 359 | e.svr = nil 360 | } 361 | } 362 | --------------------------------------------------------------------------------