├── .goreleaser.yml ├── grpcweb ├── testdata │ ├── bidi_stream_response1.in │ ├── bidi_stream_response2.in │ ├── bidi_stream_response3.in │ ├── client_stream_response1.in │ ├── client_stream_trailer_response1.in │ ├── response.in │ ├── trailer_response.in │ ├── bidi_stream_response4.in │ ├── client_stream_response2.in │ ├── server_stream_response.in │ ├── trailer_response_error.in │ ├── bidi_stream_response_error.in │ ├── bidi_stream_trailer_response.in │ ├── client_stream_response_error.in │ ├── client_stream_trailer_response2.in │ ├── server_stream_trailer_response.in │ ├── client_stream_trailer_response_error.in │ └── server_stream_trailer_response_error.in ├── parser │ ├── testdata │ │ ├── status_trailer.in │ │ ├── status_trailer_invalid_metadata.in │ │ ├── status_trailer_invalid_status.in │ │ ├── status_trailer_error.in │ │ └── status_grpc_status_details_bin.in │ ├── imports.go │ ├── parser.go │ └── parser_test.go ├── transport │ ├── option.go │ └── transport.go ├── grpcweb_reflection_v1alpha │ └── reflection.go ├── option.go ├── grpcweb.go ├── stream.go └── grpcweb_test.go ├── .github ├── FUNDING.yml ├── no-response.yml └── workflows │ └── main.yml ├── .gitignore ├── go.mod ├── LICENSE ├── README.md └── go.sum /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_response1.in: -------------------------------------------------------------------------------- 1 |  2 | hello ktr, I greet 1 times. -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_response2.in: -------------------------------------------------------------------------------- 1 |  2 | hello ktr, I greet 2 times. -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_response3.in: -------------------------------------------------------------------------------- 1 |  2 | hello ktr, I greet 3 times. -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_response1.in: -------------------------------------------------------------------------------- 1 | + 2 | )you sent requests 2 times (hakase, nano). -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_trailer_response1.in: -------------------------------------------------------------------------------- 1 | + 2 | )you sent requests 2 times (hakase, nano). -------------------------------------------------------------------------------- /grpcweb/parser/testdata/status_trailer.in: -------------------------------------------------------------------------------- 1 | grpc-status: 0 2 | trailer_key1: trailer_val1 3 | trailer_key2:trailer_val2 4 | -------------------------------------------------------------------------------- /grpcweb/testdata/response.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/response.in -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ktr0731] 4 | custom: https://paypal.me/ktr0731 5 | -------------------------------------------------------------------------------- /grpcweb/parser/testdata/status_trailer_invalid_metadata.in: -------------------------------------------------------------------------------- 1 | grpc-status: 0 2 | trailer_key1 trailer_val1 3 | trailer_key2: trailer_val2 4 | -------------------------------------------------------------------------------- /grpcweb/parser/testdata/status_trailer_invalid_status.in: -------------------------------------------------------------------------------- 1 | grpc-status: a 2 | trailer_key1: trailer_val1 3 | trailer_key2: trailer_val2 4 | -------------------------------------------------------------------------------- /grpcweb/testdata/trailer_response.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/trailer_response.in -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_response4.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/bidi_stream_response4.in -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_response2.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/client_stream_response2.in -------------------------------------------------------------------------------- /grpcweb/testdata/server_stream_response.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/server_stream_response.in -------------------------------------------------------------------------------- /grpcweb/testdata/trailer_response_error.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/trailer_response_error.in -------------------------------------------------------------------------------- /grpcweb/parser/testdata/status_trailer_error.in: -------------------------------------------------------------------------------- 1 | grpc-message: internal error 2 | grpc-status: 13 3 | trailer_key1: trailer_val1 4 | trailer_key2: trailer_val2 5 | -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_response_error.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/bidi_stream_response_error.in -------------------------------------------------------------------------------- /grpcweb/testdata/bidi_stream_trailer_response.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/bidi_stream_trailer_response.in -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_response_error.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/client_stream_response_error.in -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_trailer_response2.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/client_stream_trailer_response2.in -------------------------------------------------------------------------------- /grpcweb/testdata/server_stream_trailer_response.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/server_stream_trailer_response.in -------------------------------------------------------------------------------- /grpcweb/testdata/client_stream_trailer_response_error.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/client_stream_trailer_response_error.in -------------------------------------------------------------------------------- /grpcweb/testdata/server_stream_trailer_response_error.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/elixxir-grpc-web-go-client/HEAD/grpcweb/testdata/server_stream_trailer_response_error.in -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/4b37ac26ffaee3e6566723ea187202d1e44ebb52/Go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | vendor 17 | .idea* 18 | -------------------------------------------------------------------------------- /grpcweb/parser/testdata/status_grpc_status_details_bin.in: -------------------------------------------------------------------------------- 1 | grpc-message: internal error 2 | grpc-status: 13 3 | trailer_key1: trailer_val1 4 | trailer_key2: trailer_val2 5 | grpc-status-details-bin: CA0SDmludGVybmFsIGVycm9yGkMKKXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLnJwYy5CYWRSZXF1ZXN0EhYKFAoFZmllbGQSC2Rlc2NyaXB0aW9uGlQKMnR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLnJwYy5QcmVjb25kaXRpb25GYWlsdXJlEh4KHAoEdHlwZRIHc3ViamVjdBoLZGVzY3JpcHRpb24 6 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | daysUntilClose: 7 2 | responseRequiredLabel: 'more information needed' 3 | closeComment: > 4 | This issue has been automatically closed because there has been no response 5 | to our request for more information from the original author. With only the 6 | information that is currently in the issue, we don't have enough information 7 | to take action. Please reach out if you have or find the answers we need so 8 | that we can investigate further. 9 | -------------------------------------------------------------------------------- /grpcweb/parser/imports.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Register well-known protos. 4 | 5 | import ( 6 | _ "github.com/golang/protobuf/protoc-gen-go/plugin" 7 | _ "github.com/golang/protobuf/ptypes/any" 8 | _ "github.com/golang/protobuf/ptypes/duration" 9 | _ "github.com/golang/protobuf/ptypes/empty" 10 | _ "github.com/golang/protobuf/ptypes/struct" 11 | _ "github.com/golang/protobuf/ptypes/timestamp" 12 | _ "github.com/golang/protobuf/ptypes/wrappers" 13 | _ "google.golang.org/genproto/googleapis/rpc/errdetails" 14 | _ "google.golang.org/genproto/protobuf/api" 15 | _ "google.golang.org/genproto/protobuf/field_mask" 16 | _ "google.golang.org/genproto/protobuf/ptype" 17 | _ "google.golang.org/genproto/protobuf/source_context" 18 | ) 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git.xx.network/elixxir/grpc-web-go-client 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.2 7 | github.com/google/go-cmp v0.5.8 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/ktr0731/grpc-test v0.1.12 10 | github.com/ktr0731/grpc-web-go-client v0.2.8 11 | github.com/pkg/errors v0.9.1 12 | github.com/spf13/jwalterweatherman v1.1.0 13 | go.uber.org/atomic v1.10.0 14 | golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c 15 | google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc 16 | google.golang.org/grpc v1.49.0 17 | google.golang.org/protobuf v1.28.1 18 | ) 19 | 20 | require ( 21 | github.com/stretchr/testify v1.7.0 // indirect 22 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect 23 | golang.org/x/text v0.3.7 // indirect 24 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /grpcweb/transport/option.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "time" 4 | 5 | // ConnectOptions struct contains configuration parameters for DialContext 6 | type ConnectOptions struct { 7 | // Toggle tls on/off 8 | WithTLS bool 9 | // Certificate for TLS connections 10 | TLSCertificate []byte 11 | // IdleConnTimeout is the maximum amount of time an idle 12 | // (keep-alive) connection will remain idle before closing 13 | // itself. 14 | // Zero means no limit. 15 | IdleConnTimeout time.Duration 16 | // TLSHandshakeTimeout specifies the maximum amount of time waiting to 17 | // wait for a TLS handshake. Zero means no timeout. 18 | TlsHandshakeTimeout time.Duration 19 | // ExpectContinueTimeout, if non-zero, specifies the amount of 20 | // time to wait for a server's first response headers after fully 21 | // writing the request headers 22 | ExpectContinueTimeout time.Duration 23 | // Skip standard tls certificate verifications 24 | TlsInsecureSkipVerify bool 25 | 26 | Timeout time.Duration 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ktr0731 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 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Build on ${{ matrix.os }} with Go ${{ matrix.go }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-18.04, windows-2019, macOS-10.14] 10 | go: ['1.14'] 11 | steps: 12 | - name: Set up Go ${{ matrix.go }} 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go }} 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: Download dependencies 21 | run: go mod download 22 | 23 | - name: Cache modules 24 | uses: actions/cache@v1 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.OS }}-go-${{ hashFiles('**/go.sum') }} 28 | 29 | - name: Build 30 | run: go build ./... 31 | 32 | - name: Test 33 | run: go test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt ./... 34 | 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v1 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | file: ./coverage.txt 40 | -------------------------------------------------------------------------------- /grpcweb/grpcweb_reflection_v1alpha/reflection.go: -------------------------------------------------------------------------------- 1 | package grpcweb_reflection_v1alpha 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ktr0731/grpc-web-go-client/grpcweb" 7 | context "golang.org/x/net/context" 8 | grpc "google.golang.org/grpc" 9 | pb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 10 | ) 11 | 12 | type serverReflectionClient struct { 13 | cc *grpcweb.ClientConn 14 | } 15 | 16 | // NewServerReflectionClient instantiates a new server reflection client. 17 | // most part of the implementation is same as the original grpc_reflection_v1alpha package's. 18 | // 19 | // the version (like v1alpha) is corrensponding to grpc_reflection_v1alpha package 20 | func NewServerReflectionClient(cc *grpcweb.ClientConn) pb.ServerReflectionClient { 21 | return &serverReflectionClient{cc} 22 | } 23 | 24 | func (c *serverReflectionClient) ServerReflectionInfo(ctx context.Context, opts ...grpc.CallOption) (pb.ServerReflection_ServerReflectionInfoClient, error) { 25 | if len(opts) != 0 { 26 | return nil, errors.New("currently, ktr0731/grpc-web-go-client does not support grpc.CallOption") 27 | } 28 | 29 | stream, err := c.cc.NewBidiStream( 30 | &grpc.StreamDesc{ServerStreams: true, ClientStreams: true}, 31 | "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo") 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &serverReflectionServerReflectionInfoClient{ctx: ctx, stream: stream}, nil 37 | } 38 | 39 | type serverReflectionServerReflectionInfoClient struct { 40 | ctx context.Context 41 | stream grpcweb.BidiStream 42 | 43 | // To satisfy pb.ServerReflection_ServerReflectionInfoClient 44 | grpc.ClientStream 45 | } 46 | 47 | func (x *serverReflectionServerReflectionInfoClient) Send(m *pb.ServerReflectionRequest) error { 48 | return x.stream.Send(x.ctx, m) 49 | } 50 | 51 | func (x *serverReflectionServerReflectionInfoClient) Recv() (*pb.ServerReflectionResponse, error) { 52 | var res pb.ServerReflectionResponse 53 | err := x.stream.Receive(x.ctx, &res) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &res, nil 58 | } 59 | 60 | func (x *serverReflectionServerReflectionInfoClient) CloseSend() error { 61 | return x.stream.CloseSend() 62 | } 63 | -------------------------------------------------------------------------------- /grpcweb/option.go: -------------------------------------------------------------------------------- 1 | package grpcweb 2 | 3 | import ( 4 | "google.golang.org/grpc/credentials" 5 | "google.golang.org/grpc/encoding" 6 | "google.golang.org/grpc/encoding/proto" 7 | "google.golang.org/grpc/metadata" 8 | "time" 9 | ) 10 | 11 | var ( 12 | defaultDialOptions = DialOptions{} 13 | defaultCallOptions = callOptions{ 14 | codec: encoding.GetCodec(proto.Name), 15 | } 16 | ) 17 | 18 | type DialOptions struct { 19 | defaultCallOptions []CallOption 20 | insecure bool 21 | transportCredentials credentials.TransportCredentials 22 | tlsCertificate []byte 23 | tlsInsecureVerification bool 24 | expectContinueTimeout time.Duration 25 | idleConnTimeout time.Duration 26 | tlsHandshakeTimeout time.Duration 27 | } 28 | 29 | type DialOption func(*DialOptions) 30 | 31 | func WithDefaultCallOptions(opts ...CallOption) DialOption { 32 | return func(opt *DialOptions) { 33 | opt.defaultCallOptions = opts 34 | } 35 | } 36 | 37 | func WithExpectContinueTimeout(duration time.Duration) DialOption { 38 | return func(opt *DialOptions) { 39 | opt.expectContinueTimeout = duration 40 | } 41 | } 42 | 43 | func WithTlsHandshakeTimeout(duration time.Duration) DialOption { 44 | return func(opt *DialOptions) { 45 | opt.tlsHandshakeTimeout = duration 46 | } 47 | } 48 | 49 | func WithIdleConnTimeout(duration time.Duration) DialOption { 50 | return func(opt *DialOptions) { 51 | opt.idleConnTimeout = duration 52 | } 53 | } 54 | 55 | func WithTlsCertificate(cert []byte) DialOption { 56 | return func(opt *DialOptions) { 57 | opt.tlsCertificate = cert 58 | } 59 | } 60 | 61 | func WithSecure() DialOption { 62 | return func(opt *DialOptions) { 63 | opt.insecure = false 64 | opt.tlsInsecureVerification = false 65 | } 66 | } 67 | 68 | func WithInsecureTlsVerification() DialOption { 69 | return func(opt *DialOptions) { 70 | opt.tlsInsecureVerification = true 71 | } 72 | } 73 | 74 | func WithInsecure() DialOption { 75 | return func(opt *DialOptions) { 76 | opt.insecure = true 77 | } 78 | } 79 | 80 | func WithTransportCredentials(creds credentials.TransportCredentials) DialOption { 81 | return func(opt *DialOptions) { 82 | opt.transportCredentials = creds 83 | } 84 | } 85 | 86 | type callOptions struct { 87 | codec encoding.Codec 88 | header, trailer *metadata.MD 89 | } 90 | 91 | type CallOption func(*callOptions) 92 | 93 | func CallContentSubtype(contentSubtype string) CallOption { 94 | return func(opt *callOptions) { 95 | opt.codec = encoding.GetCodec(contentSubtype) 96 | } 97 | } 98 | 99 | func Header(h *metadata.MD) CallOption { 100 | return func(opt *callOptions) { 101 | *h = metadata.New(nil) 102 | opt.header = h 103 | } 104 | } 105 | 106 | func Trailer(t *metadata.MD) CallOption { 107 | return func(opt *callOptions) { 108 | *t = metadata.New(nil) 109 | opt.trailer = t 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC Web Go client 2 | [![GoDoc](https://godoc.org/github.com/ktr0731/grpc-web-go-client/grpcweb?status.svg)](https://godoc.org/github.com/ktr0731/grpc-web-go-client/grpcweb) 3 | [![GitHub Actions](https://github.com/ktr0731/grpc-web-go-client/workflows/main/badge.svg)](https://github.com/ktr0731/grpc-web-go-client/actions) 4 | 5 | *THE IMPLEMENTATION IS LACKING* 6 | 7 | gRPC Web client written in Go. 8 | 9 | 10 | ## Usage 11 | The server is [here](github.com/ktr0731/grpc-test). 12 | 13 | Send an unary request. 14 | 15 | ``` go 16 | client := grpcweb.NewClient("localhost:50051") 17 | 18 | in, out := new(api.SimpleRequest), new(api.SimpleResponse) 19 | in.Name = "ktr" 20 | 21 | // You can get the endpoint from grpcweb.ToEndpoint function with descriptors. 22 | // However, I write directly in this example. 23 | req := grpcweb.NewRequest("/api.Example/Unary", in, out) 24 | 25 | res, err := client.Unary(context.Background(), req) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // hello, ktr 31 | fmt.Println(res.Content.(*api.SimpleResponse).GetMessage()) 32 | ``` 33 | 34 | Send a server-side streaming request. 35 | ``` go 36 | req := grpcweb.NewRequest("/api.Example/ServerStreaming", in, out) 37 | 38 | stream, err := client.ServerStreaming(context.Background(), req) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | for { 44 | res, err := stream.Receive() 45 | if err == io.EOF { 46 | break 47 | } 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | fmt.Println(res.Content.(*api.SimpleResponse).GetMessage()) 52 | } 53 | ``` 54 | 55 | Send an client-side streaming request. 56 | ``` go 57 | stream, err := client.ClientStreaming(context.Background()) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | in, out := new(api.SimpleRequest), new(api.SimpleResponse) 63 | in.Name = "ktr" 64 | req := grpcweb.NewRequest("/api.Example/ClientStreaming", in, out) 65 | 66 | for i := 0; i < 10; i++ { 67 | err := stream.Send(req) 68 | if err == io.EOF { 69 | break 70 | } 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | } 75 | 76 | res, err := stream.CloseAndReceive() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | // ktr, you greet 10 times. 82 | fmt.Println(res.Content.(*api.SimpleResponse).GetMessage()) 83 | ``` 84 | 85 | Send a bidirectional streaming request. 86 | ``` go 87 | in, out := new(api.SimpleRequest), new(api.SimpleResponse) 88 | req := grpcweb.NewRequest("/api.Example/BidiStreaming", in, out) 89 | 90 | stream := client.BidiStreaming(context.Background(), req) 91 | 92 | go func() { 93 | for { 94 | res, err := stream.Receive() 95 | if err == grpcweb.ErrConnectionClosed { 96 | return 97 | } 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | fmt.Println(res.Content.(*api.SimpleResponse).GetMessage()) 102 | } 103 | }() 104 | 105 | for i := 0; i < 2; i++ { 106 | in.Name = fmt.Sprintf("ktr%d", i+1) 107 | req := grpcweb.NewRequest("/api.Example/BidiStreaming", in, out) 108 | 109 | err := stream.Send(req) 110 | if err == io.EOF { 111 | break 112 | } 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | } 117 | 118 | // wait a moment to get responses. 119 | time.Sleep(10 * time.Second) 120 | 121 | if err := stream.Close(); err != nil { 122 | log.Fatal(err) 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /grpcweb/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "io" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/golang/protobuf/proto" 12 | "github.com/pkg/errors" 13 | spb "google.golang.org/genproto/googleapis/rpc/status" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/metadata" 16 | "google.golang.org/grpc/status" 17 | ) 18 | 19 | type Header struct { 20 | flag byte 21 | ContentLength uint32 22 | } 23 | 24 | func (h *Header) IsMessageHeader() bool { 25 | return h.flag == 0 || h.flag == 1 26 | } 27 | 28 | func (h *Header) IsTrailerHeader() bool { 29 | return h.flag>>7 == 0x01 30 | } 31 | 32 | func ParseResponseHeader(r io.Reader) (*Header, error) { 33 | var h [5]byte 34 | n, err := r.Read(h[:]) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "failed to read header") 37 | } 38 | if n != len(h) { 39 | return nil, io.ErrUnexpectedEOF 40 | } 41 | 42 | length := binary.BigEndian.Uint32(h[1:]) 43 | return &Header{ 44 | flag: h[0], 45 | ContentLength: length, 46 | }, nil 47 | } 48 | 49 | func ParseLengthPrefixedMessage(r io.Reader, length uint32) ([]byte, error) { 50 | content := make([]byte, length) 51 | n, err := r.Read(content) 52 | switch { 53 | case uint32(n) != length: 54 | return nil, io.ErrUnexpectedEOF 55 | case err == io.EOF: 56 | return nil, io.EOF 57 | case err != nil: 58 | return nil, err 59 | } 60 | return content, nil 61 | } 62 | 63 | func ParseStatusAndTrailer(r io.Reader, length uint32) (*status.Status, metadata.MD, error) { 64 | var ( 65 | readLen uint32 66 | headerStat *status.Status 67 | code codes.Code 68 | msg string 69 | ) 70 | trailer := metadata.New(nil) 71 | s := bufio.NewScanner(r) 72 | for s.Scan() { 73 | readLen += uint32(len(s.Bytes())) 74 | if readLen > length { 75 | return nil, nil, io.ErrUnexpectedEOF 76 | } 77 | 78 | t := s.Text() 79 | i := strings.Index(t, ":") 80 | if i == -1 { 81 | return nil, nil, io.ErrUnexpectedEOF 82 | } 83 | 84 | // Check reserved keys. 85 | k, v := strings.ToLower(t[:i]), strings.TrimSpace(t[i+1:]) 86 | switch k { 87 | case "grpc-status": 88 | n, err := strconv.ParseUint(v, 10, 32) 89 | if err != nil { 90 | code = codes.Unknown 91 | } else { 92 | code = codes.Code(uint32(n)) 93 | } 94 | continue 95 | case "grpc-message": 96 | msg = v 97 | continue 98 | case "grpc-status-details-bin": 99 | b, err := decodeBase64Value(v) 100 | if err != nil { 101 | // Same behavior as grpc/grpc-go. 102 | return status.Newf( 103 | codes.Internal, 104 | "transport: malformed grpc-status-details-bin: %v", 105 | err, 106 | ), nil, nil 107 | } 108 | 109 | s := &spb.Status{} 110 | if err := proto.Unmarshal(b, s); err != nil { 111 | return status.Newf( 112 | codes.Internal, 113 | "transport: malformed grpc-status-details-bin: %v", 114 | err, 115 | ), nil, nil 116 | } 117 | headerStat = status.FromProto(s) 118 | default: 119 | trailer.Append(k, v) 120 | } 121 | } 122 | 123 | var stat *status.Status 124 | if headerStat != nil { 125 | stat = headerStat 126 | } else { 127 | stat = status.New(code, msg) 128 | } 129 | 130 | if trailer.Len() == 0 { 131 | return stat, nil, nil 132 | } 133 | return stat, trailer, nil 134 | } 135 | 136 | func decodeBase64Value(v string) ([]byte, error) { 137 | // Mostly copied from http_util.go in grpc/grpc-go. 138 | 139 | if len(v)%4 == 0 { 140 | // Input was padded, or padding was not necessary. 141 | return base64.StdEncoding.DecodeString(v) 142 | } 143 | return base64.RawStdEncoding.DecodeString(v) 144 | } 145 | -------------------------------------------------------------------------------- /grpcweb/grpcweb.go: -------------------------------------------------------------------------------- 1 | package grpcweb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/x509" 7 | "encoding/binary" 8 | "io" 9 | "net/http" 10 | 11 | "git.xx.network/elixxir/grpc-web-go-client/grpcweb/parser" 12 | "git.xx.network/elixxir/grpc-web-go-client/grpcweb/transport" 13 | "github.com/pkg/errors" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/encoding" 16 | "google.golang.org/grpc/metadata" 17 | ) 18 | 19 | type ClientConn struct { 20 | host string 21 | dialOptions *DialOptions 22 | transport transport.UnaryTransport 23 | } 24 | 25 | // DialContext initializes a grpcweb Clientconn backed by a UnaryTransport 26 | func DialContext(host string, opts ...DialOption) (*ClientConn, error) { 27 | opt := defaultDialOptions 28 | for _, o := range opts { 29 | o(&opt) 30 | } 31 | tr := transport.NewUnary(host, &transport.ConnectOptions{ 32 | WithTLS: !opt.insecure, 33 | TLSCertificate: opt.tlsCertificate, 34 | TlsInsecureSkipVerify: opt.tlsInsecureVerification, 35 | }) 36 | return &ClientConn{ 37 | host: host, 38 | dialOptions: &opt, 39 | transport: tr, 40 | }, nil 41 | } 42 | 43 | // IsAlive returns true if the transport has been established 44 | func (c *ClientConn) IsAlive() bool { 45 | return c.transport != nil 46 | } 47 | 48 | // Invoke uses grpcweb to call a given method with the passed in payload. 49 | // Response is unmarshaled to the reply interface 50 | func (c *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error { 51 | co := c.applyCallOptions(opts) 52 | codec := co.codec 53 | 54 | if c.transport == nil { 55 | tr := transport.NewUnary(c.host, &transport.ConnectOptions{ 56 | WithTLS: !c.dialOptions.insecure, 57 | TLSCertificate: c.dialOptions.tlsCertificate, 58 | TlsInsecureSkipVerify: c.dialOptions.tlsInsecureVerification, 59 | }) 60 | c.transport = tr 61 | } 62 | 63 | r, err := encodeRequestBody(codec, args) 64 | if err != nil { 65 | return errors.Wrap(err, "failed to build the request body") 66 | } 67 | 68 | md, ok := metadata.FromOutgoingContext(ctx) 69 | if ok { 70 | for k, v := range md { 71 | for _, vv := range v { 72 | c.transport.Header().Add(k, vv) 73 | } 74 | } 75 | } 76 | 77 | contentType := "application/grpc-web+" + codec.Name() 78 | header, rawBody, err := c.transport.Send(ctx, method, contentType, r) 79 | if err != nil { 80 | return errors.Wrap(err, "failed to send the request") 81 | } 82 | 83 | if co.header != nil { 84 | *co.header = toMetadata(header) 85 | } 86 | bodyReader := bytes.NewReader(rawBody) 87 | resHeader, err := parser.ParseResponseHeader(bodyReader) 88 | if err != nil { 89 | return errors.Wrap(err, "failed to parse response header") 90 | } 91 | 92 | if resHeader.IsMessageHeader() { 93 | resBody, err := parser.ParseLengthPrefixedMessage(bodyReader, resHeader.ContentLength) 94 | if err != nil { 95 | return errors.Wrap(err, "failed to parse the response body") 96 | } 97 | if err := codec.Unmarshal(resBody, reply); err != nil { 98 | return errors.Wrapf(err, "failed to unmarshal response body by codec %s", codec.Name()) 99 | } 100 | 101 | resHeader, err = parser.ParseResponseHeader(bodyReader) 102 | if err != nil { 103 | return errors.Wrap(err, "failed to parse response header") 104 | } 105 | } 106 | if !resHeader.IsTrailerHeader() { 107 | return errors.New("unexpected header") 108 | } 109 | 110 | status, trailer, err := parser.ParseStatusAndTrailer(bodyReader, resHeader.ContentLength) 111 | if err != nil { 112 | return errors.Wrap(err, "failed to parse status and trailer") 113 | } 114 | if co.trailer != nil { 115 | *co.trailer = trailer 116 | } 117 | 118 | return status.Err() 119 | } 120 | 121 | func (c *ClientConn) NewClientStream(desc *grpc.StreamDesc, method string, opts ...CallOption) (ClientStream, error) { 122 | if !desc.ClientStreams { 123 | return nil, errors.New("not a client stream RPC") 124 | } 125 | tr, err := transport.NewClientStream(c.host, method) 126 | if err != nil { 127 | return nil, errors.Wrap(err, "failed to create a new transport stream") 128 | } 129 | return &clientStream{ 130 | endpoint: method, 131 | transport: tr, 132 | callOptions: c.applyCallOptions(opts), 133 | }, nil 134 | } 135 | 136 | func (c *ClientConn) NewServerStream(desc *grpc.StreamDesc, method string, opts ...CallOption) (ServerStream, error) { 137 | if !desc.ServerStreams { 138 | return nil, errors.New("not a server stream RPC") 139 | } 140 | return &serverStream{ 141 | endpoint: method, 142 | transport: transport.NewUnary(c.host, &transport.ConnectOptions{ 143 | WithTLS: !c.dialOptions.insecure, 144 | TLSCertificate: c.dialOptions.tlsCertificate, 145 | TlsInsecureSkipVerify: c.dialOptions.tlsInsecureVerification, 146 | }), 147 | callOptions: c.applyCallOptions(opts), 148 | }, nil 149 | } 150 | 151 | func (c *ClientConn) NewBidiStream(desc *grpc.StreamDesc, method string, opts ...CallOption) (BidiStream, error) { 152 | if !desc.ServerStreams || !desc.ClientStreams { 153 | return nil, errors.New("not a bidi stream RPC") 154 | } 155 | stream, err := c.NewClientStream(desc, method, opts...) 156 | if err != nil { 157 | return nil, errors.Wrap(err, "failed to create a new client stream") 158 | } 159 | return &bidiStream{ 160 | clientStream: stream.(*clientStream), 161 | }, nil 162 | } 163 | 164 | func (c *ClientConn) Close() error { 165 | if c.transport == nil { 166 | return nil 167 | } 168 | err := c.transport.Close() 169 | if err != nil { 170 | return err 171 | } 172 | c.transport = nil 173 | return nil 174 | } 175 | 176 | func (c *ClientConn) applyCallOptions(opts []CallOption) *callOptions { 177 | callOpts := append(c.dialOptions.defaultCallOptions, opts...) 178 | callOptions := defaultCallOptions 179 | for _, o := range callOpts { 180 | o(&callOptions) 181 | } 182 | return &callOptions 183 | } 184 | 185 | func (c *ClientConn) GetRemoteCertificate() (*x509.Certificate, error) { 186 | return c.transport.GetRemoteCertificate() 187 | } 188 | 189 | // copied from rpc_util.go#msgHeader 190 | const headerLen = 5 191 | 192 | func header(body []byte) []byte { 193 | h := make([]byte, 5) 194 | h[0] = byte(0) 195 | binary.BigEndian.PutUint32(h[1:], uint32(len(body))) 196 | return h 197 | } 198 | 199 | // header (compressed-flag(1) + message-length(4)) + body 200 | // TODO: compressed message 201 | func encodeRequestBody(codec encoding.Codec, in interface{}) (io.Reader, error) { 202 | body, err := codec.Marshal(in) 203 | if err != nil { 204 | return nil, errors.Wrap(err, "failed to marshal the request body") 205 | } 206 | buf := bytes.NewBuffer(make([]byte, 0, headerLen+len(body))) 207 | buf.Write(header(body)) 208 | buf.Write(body) 209 | return buf, nil 210 | } 211 | 212 | func toMetadata(h http.Header) metadata.MD { 213 | if len(h) == 0 { 214 | return nil 215 | } 216 | md := metadata.New(nil) 217 | for k, v := range h { 218 | md.Append(k, v...) 219 | } 220 | return md 221 | } 222 | -------------------------------------------------------------------------------- /grpcweb/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/ktr0731/grpc-web-go-client/grpcweb/parser" 12 | "github.com/pkg/errors" 13 | "google.golang.org/genproto/googleapis/rpc/errdetails" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/metadata" 16 | "google.golang.org/grpc/status" 17 | "google.golang.org/protobuf/testing/protocmp" 18 | ) 19 | 20 | func TestParseResponseHeader(t *testing.T) { 21 | type headerType int 22 | const ( 23 | message headerType = iota 24 | trailer 25 | ) 26 | cases := map[string]struct { 27 | in []byte 28 | expectedHeaderType headerType 29 | expectedContentLength uint32 30 | wantErr bool 31 | expectedErr error 32 | }{ 33 | "message header": { 34 | in: []byte{0x00, 0x00, 0x00, 0x00, 0x0c}, 35 | expectedContentLength: 12, 36 | expectedHeaderType: message, 37 | }, 38 | "trailer header": { 39 | in: []byte{0x80, 0x00, 0x00, 0x00, 0x48}, 40 | expectedContentLength: 72, 41 | expectedHeaderType: trailer, 42 | }, 43 | "unexpected error": { 44 | in: []byte{0x80}, 45 | wantErr: true, 46 | expectedErr: io.ErrUnexpectedEOF, 47 | }, 48 | "the length is zero": { 49 | in: []byte{0x00, 0x00, 0x00, 0x00, 0x00}, 50 | wantErr: true, 51 | expectedErr: io.EOF, 52 | }, 53 | } 54 | 55 | for name, c := range cases { 56 | c := c 57 | t.Run(name, func(t *testing.T) { 58 | h, err := parser.ParseResponseHeader(bytes.NewReader(c.in)) 59 | if c.wantErr { 60 | if err == nil { 61 | t.Fatalf("expected a error, but got nil") 62 | } 63 | if c.expectedErr != nil && !errors.Is(err, c.expectedErr) { 64 | t.Errorf("expected error is '%v', but got '%v'", c.expectedErr, err) 65 | } 66 | return 67 | } 68 | if err != nil { 69 | t.Fatalf("should not return an error, but got '%s'", err) 70 | } 71 | 72 | m := map[headerType]bool{ 73 | message: h.IsMessageHeader(), 74 | trailer: h.IsTrailerHeader(), 75 | } 76 | if v, ok := m[c.expectedHeaderType]; !v || !ok { 77 | t.Errorf("header type is not %d", c.expectedHeaderType) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestParseLengthPrefixedMessage(t *testing.T) { 84 | cases := map[string]struct { 85 | bytes []byte 86 | length uint32 87 | wantErr bool 88 | expectedErr error 89 | }{ 90 | "ok": { 91 | bytes: []byte{0x01, 0x02, 0x03}, 92 | length: 3, 93 | }, 94 | "unexpected EOF": { 95 | bytes: []byte{0x01, 0x02}, 96 | length: 3, 97 | wantErr: true, 98 | expectedErr: io.ErrUnexpectedEOF, 99 | }, 100 | "EOF": { 101 | bytes: []byte{}, 102 | length: 0, 103 | wantErr: true, 104 | expectedErr: io.EOF, 105 | }, 106 | } 107 | 108 | for name, c := range cases { 109 | c := c 110 | t.Run(name, func(t *testing.T) { 111 | _, err := parser.ParseLengthPrefixedMessage(bytes.NewReader(c.bytes), c.length) 112 | if c.wantErr { 113 | if err == nil { 114 | t.Fatalf("expected a error, but got nil") 115 | } 116 | if c.expectedErr != nil && !errors.Is(err, c.expectedErr) { 117 | t.Errorf("expected error is '%v', but got '%v'", c.expectedErr, err) 118 | } 119 | return 120 | } 121 | if err != nil { 122 | t.Fatalf("should not return an error, but got '%s'", err) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestParseStatusAndTrailer(t *testing.T) { 129 | cases := map[string]struct { 130 | fname string 131 | length uint32 132 | expectedStatus *status.Status 133 | expectedTrailer metadata.MD 134 | expectedErr error 135 | }{ 136 | "ok": { 137 | fname: "status_trailer.in", 138 | expectedStatus: status.New(codes.OK, ""), 139 | expectedTrailer: metadata.New(map[string]string{ 140 | "trailer_key1": "trailer_val1", 141 | "trailer_key2": "trailer_val2", 142 | }), 143 | }, 144 | "ok with message": { 145 | fname: "status_trailer_error.in", 146 | expectedStatus: status.New(codes.Internal, "internal error"), 147 | expectedTrailer: metadata.New(map[string]string{ 148 | "trailer_key1": "trailer_val1", 149 | "trailer_key2": "trailer_val2", 150 | }), 151 | }, 152 | "ok with grpc-status-details-bin": { 153 | fname: "status_grpc_status_details_bin.in", 154 | expectedStatus: func() *status.Status { 155 | s, err := status.New(codes.Internal, "internal error").WithDetails( 156 | &errdetails.BadRequest{ 157 | FieldViolations: []*errdetails.BadRequest_FieldViolation{ 158 | &errdetails.BadRequest_FieldViolation{ 159 | Field: "field", 160 | Description: "description", 161 | }, 162 | }, 163 | }, 164 | &errdetails.PreconditionFailure{ 165 | Violations: []*errdetails.PreconditionFailure_Violation{ 166 | &errdetails.PreconditionFailure_Violation{ 167 | Type: "type", 168 | Subject: "subject", 169 | Description: "description", 170 | }, 171 | }, 172 | }, 173 | ) 174 | if err != nil { 175 | t.Fatalf("WithDetails should not return an error, but got '%s'", err) 176 | } 177 | return s 178 | }(), 179 | expectedTrailer: metadata.New(map[string]string{ 180 | "trailer_key1": "trailer_val1", 181 | "trailer_key2": "trailer_val2", 182 | }), 183 | }, 184 | "bytes exceeds length": { 185 | fname: "status_trailer.in", 186 | length: 3, 187 | expectedErr: io.ErrUnexpectedEOF, 188 | }, 189 | "invalid metadata": { 190 | fname: "status_trailer_invalid_metadata.in", 191 | expectedErr: io.ErrUnexpectedEOF, 192 | }, 193 | "invalid status": { 194 | fname: "status_trailer_invalid_status.in", 195 | expectedStatus: status.New(codes.Unknown, ""), 196 | expectedTrailer: metadata.New(map[string]string{ 197 | "trailer_key1": "trailer_val1", 198 | "trailer_key2": "trailer_val2", 199 | }), 200 | }, 201 | } 202 | 203 | for name, c := range cases { 204 | c := c 205 | t.Run(name, func(t *testing.T) { 206 | fpath := filepath.Join("testdata", c.fname) 207 | b, err := ioutil.ReadFile(fpath) 208 | if err != nil { 209 | t.Fatalf("Open should not return an error, but got '%s'", err) 210 | } 211 | 212 | in := bytes.NewReader(b) 213 | if c.length == 0 { 214 | c.length = uint32(in.Len()) 215 | } 216 | status, trailer, err := parser.ParseStatusAndTrailer(in, c.length) 217 | if err != c.expectedErr { 218 | t.Errorf("expected error: '%s', but got '%s'", c.expectedErr, err) 219 | if err != nil { 220 | return 221 | } 222 | } 223 | if diff := cmp.Diff(c.expectedStatus.Proto(), status.Proto(), protocmp.Transform()); diff != "" { 224 | t.Errorf("-want, +got\n%s", diff) 225 | } 226 | if diff := cmp.Diff(c.expectedTrailer, trailer); diff != "" { 227 | t.Errorf("-want, +got\n%s", diff) 228 | } 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /grpcweb/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "github.com/gorilla/websocket" 11 | "github.com/pkg/errors" 12 | jww "github.com/spf13/jwalterweatherman" 13 | "io" 14 | "io/ioutil" 15 | "net" 16 | "net/http" 17 | "net/url" 18 | "strconv" 19 | "strings" 20 | "sync" 21 | "sync/atomic" 22 | "time" 23 | ) 24 | 25 | // UnaryTransport is the public interface for the transport package 26 | type UnaryTransport interface { 27 | Header() http.Header 28 | Send(ctx context.Context, endpoint, contentType string, body io.Reader) (http.Header, []byte, error) 29 | GetRemoteCertificate() (*x509.Certificate, error) 30 | Close() error 31 | } 32 | 33 | type httpTransport struct { 34 | host string 35 | client *http.Client 36 | clientLock *sync.RWMutex 37 | opts *ConnectOptions 38 | receivedCertAtomic atomic.Value 39 | 40 | header http.Header 41 | } 42 | 43 | // IsAlive is not easily defined for grpcweb, return true if t.client != nil 44 | func (t *httpTransport) IsAlive() bool { 45 | // No good way to check http connection status 46 | // return true if client is not nil 47 | return t.client != nil 48 | } 49 | 50 | // Header returns the http.Header object for httpTransport 51 | func (t *httpTransport) Header() http.Header { 52 | return t.header 53 | } 54 | 55 | // Send accepts an endpoint, content-type header & body and sends them to 56 | // a grpc-web server using http requests. 57 | func (t *httpTransport) Send(ctx context.Context, endpoint, contentType string, body io.Reader) (http.Header, []byte, error) { 58 | var scheme string 59 | if t.opts.WithTLS { 60 | scheme = "https" 61 | } else { 62 | scheme = "http" 63 | } 64 | u := url.URL{Scheme: scheme, Host: t.host, Path: endpoint} 65 | reqUrl := u.String() 66 | req, err := http.NewRequest(http.MethodPost, reqUrl, body) 67 | if err != nil { 68 | return nil, nil, errors.Wrap(err, "failed to build the API request") 69 | } 70 | 71 | req.Header = t.Header() 72 | req.Header.Add("content-type", contentType) 73 | req.Header.Add("x-grpc-web", "1") 74 | 75 | t.clientLock.RLock() 76 | defer t.clientLock.RUnlock() 77 | res, err := t.client.Do(req) 78 | if err != nil { 79 | // Lower log level for expected CORS errors (gateway lacks gRPC-web support) 80 | // but preserve original error for upstream handling 81 | if strings.Contains(err.Error(), "Failed to fetch") || 82 | strings.Contains(err.Error(), "NetworkError") { 83 | jww.TRACE.Printf("Gateway connection failed (likely no gRPC-web support): %+v", err) 84 | } else { 85 | jww.WARN.Printf("Request error: %+v", err) 86 | } 87 | return nil, nil, errors.Wrap(err, "failed to send the API") 88 | } 89 | 90 | respBody, err := io.ReadAll(res.Body) 91 | if err != nil { 92 | return nil, nil, errors.Wrap(err, "Failed to read response body") 93 | } 94 | res.Close = true 95 | err = res.Body.Close() 96 | if err != nil { 97 | return nil, nil, errors.Wrap(err, "Failed to close response body") 98 | } 99 | 100 | if grpcStatus := res.Header.Get("grpc-status"); grpcStatus != "" { 101 | grpcStatusInt, err := strconv.Atoi(res.Header.Get("grpc-status")) 102 | if err != nil { 103 | jww.WARN.Printf("Invalid GRPC status header: %+v", err) 104 | } else if grpcStatusInt > 0 { 105 | jww.WARN.Printf("received GRPC status code %d: %s", 106 | grpcStatusInt, res.Header.Get("grpc-message")) 107 | } 108 | } 109 | 110 | if res.TLS != nil { 111 | if res.TLS.PeerCertificates != nil && len(res.TLS.PeerCertificates) > 0 { 112 | serverCert := res.TLS.PeerCertificates[0] 113 | t.receivedCertAtomic.Store(serverCert) 114 | } 115 | } 116 | 117 | return res.Header, respBody, nil 118 | } 119 | 120 | func (t *httpTransport) GetRemoteCertificate() (*x509.Certificate, error) { 121 | receivedCert := t.receivedCertAtomic.Load() 122 | if receivedCert == nil { 123 | return nil, errors.New("http transport has not yet received a tls certificate") 124 | } 125 | return receivedCert.(*x509.Certificate), nil 126 | } 127 | 128 | // Close the httpTransport object 129 | // Note that this just closes idle connections, to properly close this 130 | // connection delete the object. 131 | func (t *httpTransport) Close() error { 132 | t.clientLock.Lock() 133 | defer t.clientLock.Unlock() 134 | t.client.CloseIdleConnections() 135 | return nil 136 | } 137 | 138 | // NewUnary returns an httpTransport object wrapped as a UnaryTransport object 139 | var NewUnary = func(host string, opts *ConnectOptions) UnaryTransport { 140 | cl := http.DefaultClient 141 | transport := &http.Transport{} 142 | if opts.WithTLS { 143 | transport.TLSClientConfig = &tls.Config{} 144 | if opts.TLSCertificate != nil { 145 | certPool := x509.NewCertPool() 146 | decoded, _ := pem.Decode(opts.TLSCertificate) 147 | if decoded == nil { 148 | panic("failed to decode cert") 149 | } 150 | cert, err := x509.ParseCertificate(decoded.Bytes) 151 | if err != nil { 152 | panic(err) 153 | } 154 | certPool.AddCert(cert) 155 | transport.TLSClientConfig.RootCAs = certPool 156 | transport.TLSClientConfig.ServerName = cert.DNSNames[0] 157 | } 158 | transport.TLSClientConfig.InsecureSkipVerify = opts.TlsInsecureSkipVerify 159 | } 160 | if opts.Timeout != 0 { 161 | cl.Timeout = opts.Timeout 162 | } else { 163 | cl.Timeout = 5 * time.Second 164 | } 165 | 166 | if opts.IdleConnTimeout != 0 { 167 | transport.IdleConnTimeout = opts.IdleConnTimeout 168 | } 169 | 170 | if opts.TlsHandshakeTimeout != 0 { 171 | transport.TLSHandshakeTimeout = opts.TlsHandshakeTimeout 172 | } 173 | 174 | if opts.ExpectContinueTimeout != 0 { 175 | transport.ExpectContinueTimeout = opts.ExpectContinueTimeout 176 | } 177 | 178 | cl.Transport = transport 179 | return &httpTransport{ 180 | host: host, 181 | client: cl, 182 | opts: opts, 183 | header: make(http.Header), 184 | clientLock: &sync.RWMutex{}, 185 | } 186 | } 187 | 188 | type ClientStreamTransport interface { 189 | Header() (http.Header, error) 190 | Trailer() http.Header 191 | 192 | // SetRequestHeader sets headers to send gRPC-Web server. 193 | // It should be called before calling Send. 194 | SetRequestHeader(h http.Header) 195 | Send(ctx context.Context, body io.Reader) error 196 | Receive(ctx context.Context) (io.ReadCloser, error) 197 | 198 | // CloseSend sends a close signal to the server. 199 | CloseSend() error 200 | 201 | // Close closes the connection. 202 | Close() error 203 | } 204 | 205 | // webSocketTransport is a stream transport implementation. 206 | // 207 | // Currently, gRPC-Web specification does not support client streaming. (https://github.com/improbable-eng/grpc-web#client-side-streaming) 208 | // webSocketTransport supports improbable-eng/grpc-web's own implementation. 209 | // 210 | // spec: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md 211 | type webSocketTransport struct { 212 | host string 213 | endpoint string 214 | 215 | conn *websocket.Conn 216 | 217 | once sync.Once 218 | resOnce sync.Once 219 | 220 | closed bool 221 | 222 | writeMu sync.Mutex 223 | 224 | reqHeader, header, trailer http.Header 225 | } 226 | 227 | func (t *webSocketTransport) Header() (http.Header, error) { 228 | return t.header, nil 229 | } 230 | 231 | func (t *webSocketTransport) Trailer() http.Header { 232 | return t.trailer 233 | } 234 | 235 | func (t *webSocketTransport) SetRequestHeader(h http.Header) { 236 | t.reqHeader = h 237 | } 238 | 239 | func (t *webSocketTransport) Send(ctx context.Context, body io.Reader) error { 240 | if t.closed { 241 | return io.EOF 242 | } 243 | 244 | var err error 245 | t.once.Do(func() { 246 | h := t.reqHeader 247 | if h == nil { 248 | h = make(http.Header) 249 | } 250 | h.Set("content-type", "application/grpc-web+proto") 251 | h.Set("x-grpc-web", "1") 252 | var b bytes.Buffer 253 | h.Write(&b) 254 | 255 | t.writeMessage(websocket.BinaryMessage, b.Bytes()) 256 | }) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | var b bytes.Buffer 262 | b.Write([]byte{0x00}) 263 | _, err = io.Copy(&b, body) 264 | if err != nil { 265 | return errors.Wrap(err, "failed to read request body") 266 | } 267 | 268 | return t.writeMessage(websocket.BinaryMessage, b.Bytes()) 269 | } 270 | 271 | func (t *webSocketTransport) Receive(context.Context) (_ io.ReadCloser, err error) { 272 | if t.closed { 273 | return nil, io.EOF 274 | } 275 | 276 | defer func() { 277 | if err == nil { 278 | return 279 | } 280 | 281 | if berr, ok := errors.Cause(err).(*net.OpError); ok && !berr.Temporary() { 282 | err = io.EOF 283 | } 284 | }() 285 | 286 | // skip response header 287 | t.resOnce.Do(func() { 288 | _, _, err = t.conn.NextReader() 289 | if err != nil { 290 | err = errors.Wrap(err, "failed to read response header") 291 | return 292 | } 293 | 294 | _, msg, err := t.conn.NextReader() 295 | if err != nil { 296 | err = errors.Wrap(err, "failed to read response header") 297 | return 298 | } 299 | 300 | h := make(http.Header) 301 | s := bufio.NewScanner(msg) 302 | for s.Scan() { 303 | t := s.Text() 304 | i := strings.Index(t, ": ") 305 | if i == -1 { 306 | continue 307 | } 308 | k := strings.ToLower(t[:i]) 309 | h.Add(k, t[i+2:]) 310 | } 311 | t.header = h 312 | }) 313 | 314 | var buf bytes.Buffer 315 | var b []byte 316 | 317 | _, b, err = t.conn.ReadMessage() 318 | if err != nil { 319 | if cerr, ok := err.(*websocket.CloseError); ok { 320 | if cerr.Code == websocket.CloseNormalClosure { 321 | return nil, io.EOF 322 | } 323 | if cerr.Code == websocket.CloseAbnormalClosure { 324 | return nil, io.ErrUnexpectedEOF 325 | } 326 | } 327 | err = errors.Wrap(err, "failed to read response body") 328 | return 329 | } 330 | buf.Write(b) 331 | 332 | var r io.Reader 333 | _, r, err = t.conn.NextReader() 334 | if err != nil { 335 | return 336 | } 337 | 338 | res := ioutil.NopCloser(io.MultiReader(&buf, r)) 339 | 340 | by, err := ioutil.ReadAll(res) 341 | if err != nil { 342 | panic(err) 343 | } 344 | 345 | res = ioutil.NopCloser(bytes.NewReader(by)) 346 | 347 | return res, nil 348 | } 349 | 350 | func (t *webSocketTransport) CloseSend() error { 351 | // 0x01 means the finish send frame. 352 | // ref. transports/websocket/websocket.ts 353 | t.writeMessage(websocket.BinaryMessage, []byte{0x01}) 354 | return nil 355 | } 356 | 357 | func (t *webSocketTransport) Close() error { 358 | // Send the close message. 359 | err := t.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 360 | if err != nil { 361 | return err 362 | } 363 | t.closed = true 364 | // Close the WebSocket connection. 365 | return t.conn.Close() 366 | } 367 | 368 | func (t *webSocketTransport) writeMessage(msg int, b []byte) error { 369 | t.writeMu.Lock() 370 | defer t.writeMu.Unlock() 371 | return t.conn.WriteMessage(msg, b) 372 | } 373 | 374 | var NewClientStream = func(host, endpoint string) (ClientStreamTransport, error) { 375 | // TODO: WebSocket over TLS support. 376 | u := url.URL{Scheme: "ws", Host: host, Path: endpoint} 377 | h := http.Header{} 378 | h.Set("Sec-WebSocket-Protocol", "grpc-websockets") 379 | var conn *websocket.Conn 380 | conn, _, err := websocket.DefaultDialer.Dial(u.String(), h) 381 | if err != nil { 382 | return nil, errors.Wrapf(err, "failed to dial to '%s'", u.String()) 383 | } 384 | 385 | return &webSocketTransport{ 386 | host: host, 387 | endpoint: endpoint, 388 | conn: conn, 389 | }, nil 390 | } 391 | -------------------------------------------------------------------------------- /grpcweb/stream.go: -------------------------------------------------------------------------------- 1 | package grpcweb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | "sync" 11 | 12 | "git.xx.network/elixxir/grpc-web-go-client/grpcweb/parser" 13 | "git.xx.network/elixxir/grpc-web-go-client/grpcweb/transport" 14 | "github.com/pkg/errors" 15 | "go.uber.org/atomic" 16 | "google.golang.org/grpc/codes" 17 | "google.golang.org/grpc/metadata" 18 | "google.golang.org/grpc/status" 19 | ) 20 | 21 | type ClientStream interface { 22 | // Header returns the header metadata from the server, if there is any. 23 | // It blocks if the metadata is not ready to read. 24 | Header() (metadata.MD, error) 25 | // Trailer returns the trailer metadata from the server, if there is any. 26 | // It must only be called after stream.CloseAndReceive has returned, or 27 | // stream.Receive has returned a non-nil error (including io.EOF). 28 | Trailer() metadata.MD 29 | Send(ctx context.Context, req interface{}) error 30 | CloseAndReceive(ctx context.Context, res interface{}) error 31 | } 32 | 33 | type clientStream struct { 34 | endpoint string 35 | transport transport.ClientStreamTransport 36 | callOptions *callOptions 37 | 38 | trailersOnly, closed atomic.Bool 39 | headerMu, trailerMu sync.RWMutex 40 | headerMD, trailerMD metadata.MD 41 | } 42 | 43 | func (s *clientStream) Header() (metadata.MD, error) { 44 | if s.trailersOnly.Load() { 45 | return nil, nil 46 | } 47 | 48 | h := s.header() 49 | if h != nil { 50 | return h, nil 51 | } 52 | 53 | md := metadata.New(nil) 54 | headers, err := s.transport.Header() 55 | if err != nil { 56 | return nil, errors.Wrap(err, "failed to get headers") 57 | } 58 | for k, v := range headers { 59 | md.Append(k, v...) 60 | } 61 | s.headerMu.Lock() 62 | s.headerMD = md 63 | s.headerMu.Unlock() 64 | return md, nil 65 | } 66 | 67 | func (s *clientStream) header() metadata.MD { 68 | s.headerMu.RLock() 69 | defer s.headerMu.RUnlock() 70 | return s.headerMD 71 | } 72 | 73 | func (s *clientStream) Trailer() metadata.MD { 74 | if !s.closed.Load() { 75 | panic("Trailer must be called after stream.CloseAndReceive has been called") 76 | } 77 | return s.trailer() 78 | } 79 | 80 | func (s *clientStream) trailer() metadata.MD { 81 | s.trailerMu.RLock() 82 | defer s.trailerMu.RUnlock() 83 | return s.trailerMD 84 | } 85 | 86 | func (s *clientStream) Send(ctx context.Context, req interface{}) error { 87 | r, err := encodeRequestBody(s.callOptions.codec, req) 88 | if err != nil { 89 | return errors.Wrap(err, "failed to build the request") 90 | } 91 | 92 | h := make(http.Header) 93 | md, ok := metadata.FromOutgoingContext(ctx) 94 | if ok { 95 | for k, v := range md { 96 | for _, vv := range v { 97 | h.Add(k, vv) 98 | } 99 | } 100 | } 101 | s.transport.SetRequestHeader(h) 102 | 103 | if err := s.transport.Send(ctx, r); err != nil { 104 | return errors.Wrap(err, "failed to send the request") 105 | } 106 | return nil 107 | } 108 | 109 | func (s *clientStream) CloseAndReceive(ctx context.Context, res interface{}) error { 110 | if err := s.transport.CloseSend(); err != nil { 111 | return errors.Wrap(err, "failed to close the send stream") 112 | } 113 | 114 | s.closed.Store(true) 115 | 116 | rawBody, err := s.transport.Receive(ctx) 117 | if s.isTrailerOnly(err) { 118 | // Parse headers as trailers. 119 | trailer, err := s.Header() 120 | if err != nil { 121 | return errors.Wrap(err, "failed to get header instead of trailer") 122 | } 123 | s.trailerMu.Lock() 124 | s.trailerMD = trailer 125 | s.trailersOnly.Store(true) 126 | s.trailerMu.Unlock() 127 | 128 | // Try to extract *status.Status from headers. 129 | return statusFromHeader(trailer).Err() 130 | } 131 | if err != nil { 132 | return errors.Wrap(err, "failed to receive the response") 133 | } 134 | 135 | var closeOnce sync.Once 136 | defer closeOnce.Do(func() { rawBody.Close() }) 137 | 138 | resHeader, err := parser.ParseResponseHeader(rawBody) 139 | if err != nil { 140 | return errors.Wrap(err, "failed to parse response header") 141 | } 142 | 143 | if resHeader.IsMessageHeader() { 144 | resBody, err := parser.ParseLengthPrefixedMessage(rawBody, resHeader.ContentLength) 145 | if err != nil { 146 | return errors.Wrap(err, "failed to parse the response body") 147 | } 148 | codec := s.callOptions.codec 149 | if err := codec.Unmarshal(resBody, res); err != nil { 150 | return errors.Wrapf(err, "failed to unmarshal response body by codec %s", codec.Name()) 151 | } 152 | 153 | closeOnce.Do(func() { rawBody.Close() }) 154 | 155 | // improbable-eng/grpc-web returns the trailer in another message. 156 | rawBody2, err := s.transport.Receive(ctx) 157 | if err != nil { 158 | return errors.Wrap(err, "failed to receive the response trailer") 159 | } 160 | defer rawBody2.Close() 161 | rawBody = rawBody2 162 | 163 | resHeader, err = parser.ParseResponseHeader(rawBody2) 164 | if err != nil { 165 | return errors.Wrap(err, "failed to parse response header2") 166 | } 167 | } 168 | if !resHeader.IsTrailerHeader() { 169 | return errors.New("unexpected header") 170 | } 171 | 172 | status, trailer, err := parser.ParseStatusAndTrailer(rawBody, resHeader.ContentLength) 173 | if err != nil { 174 | return errors.Wrap(err, "failed to parse status and trailer") 175 | } 176 | s.trailerMu.Lock() 177 | defer s.trailerMu.Unlock() 178 | s.trailerMD = trailer 179 | return status.Err() 180 | } 181 | 182 | func (s *clientStream) isTrailerOnly(err error) bool { 183 | return errors.Is(err, io.ErrUnexpectedEOF) && s.trailer().Len() == 0 184 | } 185 | 186 | type ServerStream interface { 187 | // Header returns the header metadata from the server, if there is any. 188 | // It blocks if the metadata is not ready to read. 189 | Header() (metadata.MD, error) 190 | // Trailer returns the trailer metadata from the server, if there is any. 191 | // It must only be called after stream.CloseAndReceive has returned, or 192 | // stream.Receive has returned a non-nil error (including io.EOF). 193 | Trailer() metadata.MD 194 | Send(ctx context.Context, req interface{}) error 195 | Receive(ctx context.Context, res interface{}) error 196 | } 197 | 198 | type serverStream struct { 199 | endpoint string 200 | transport transport.UnaryTransport 201 | resStream io.Reader 202 | callOptions *callOptions 203 | 204 | closed bool 205 | header, trailer metadata.MD 206 | } 207 | 208 | func (s *serverStream) Header() (metadata.MD, error) { 209 | return s.header, nil 210 | } 211 | 212 | func (s *serverStream) Trailer() metadata.MD { 213 | if !s.closed { 214 | panic("Trailer must be called after stream.CloseAndReceive has been called") 215 | } 216 | return s.trailer 217 | } 218 | 219 | func (s *serverStream) Send(ctx context.Context, req interface{}) error { 220 | codec := s.callOptions.codec 221 | 222 | r, err := encodeRequestBody(codec, req) 223 | if err != nil { 224 | return errors.Wrap(err, "failed to build the request body") 225 | } 226 | 227 | md, ok := metadata.FromOutgoingContext(ctx) 228 | if ok { 229 | for k, v := range md { 230 | for _, vv := range v { 231 | s.transport.Header().Add(k, vv) 232 | } 233 | } 234 | } 235 | 236 | contentType := "application/grpc-web+" + codec.Name() 237 | header, rawBody, err := s.transport.Send(ctx, s.endpoint, contentType, r) 238 | if err != nil { 239 | return errors.Wrap(err, "failed to send the request") 240 | } 241 | s.header = toMetadata(header) 242 | s.resStream = bytes.NewBuffer(rawBody) 243 | return nil 244 | } 245 | 246 | func (s *serverStream) Receive(ctx context.Context, res interface{}) (err error) { 247 | if s.resStream == nil { 248 | return errors.New("Receive must be call after calling Send") 249 | } 250 | defer func() { 251 | if err == io.EOF { 252 | if rerr := s.transport.Close(); rerr != nil { 253 | err = rerr 254 | } 255 | } 256 | }() 257 | 258 | var h [5]byte 259 | n, err := s.resStream.Read(h[:]) 260 | if err != nil { 261 | return err 262 | } 263 | if n != len(h) { 264 | return io.ErrUnexpectedEOF 265 | } 266 | 267 | flag := h[0] 268 | length := binary.BigEndian.Uint32(h[1:]) 269 | if length == 0 { 270 | return io.EOF 271 | } 272 | if flag == 0 || flag == 1 { // Message header. 273 | msg, err := parser.ParseLengthPrefixedMessage(s.resStream, length) 274 | if err != nil { 275 | return err 276 | } 277 | if err := s.callOptions.codec.Unmarshal(msg, res); err != nil { 278 | return errors.Wrap(err, "failed to unmarshal response body") 279 | } 280 | return nil 281 | } 282 | 283 | status, trailer, err := parser.ParseStatusAndTrailer(s.resStream, length) 284 | if err != nil { 285 | return errors.Wrap(err, "failed to parse trailer") 286 | } 287 | s.closed = true 288 | s.trailer = trailer 289 | if status.Code() != codes.OK { 290 | return status.Err() 291 | } 292 | return io.EOF 293 | } 294 | 295 | type BidiStream interface { 296 | // Header returns the header metadata from the server, if there is any. 297 | // It blocks if the metadata is not ready to read. 298 | Header() (metadata.MD, error) 299 | // Trailer returns the trailer metadata from the server, if there is any. 300 | // It must only be called after stream.CloseAndReceive has returned, or 301 | // stream.Receive has returned a non-nil error (including io.EOF). 302 | Trailer() metadata.MD 303 | Send(ctx context.Context, req interface{}) error 304 | Receive(ctx context.Context, res interface{}) error 305 | CloseSend() error 306 | } 307 | 308 | type bidiStream struct { 309 | *clientStream 310 | 311 | sentCloseSend atomic.Bool 312 | } 313 | 314 | var ( 315 | canonicalGRPCStatusBytes = []byte("Grpc-Status: ") 316 | gRPCStatusBytes = []byte("grpc-status: ") 317 | ) 318 | 319 | func (s *bidiStream) Receive(ctx context.Context, res interface{}) error { 320 | if s.closed.Load() { 321 | return io.EOF 322 | } 323 | 324 | rawBody, err := s.transport.Receive(ctx) 325 | if s.isTrailerOnly(err) { 326 | // Trailers-only responses, no message. 327 | 328 | s.closed.Store(true) 329 | 330 | // Parse headers as trailers. 331 | trailer, err := s.Header() 332 | if err != nil { 333 | return errors.Wrap(err, "failed to get header instead of trailer") 334 | } 335 | 336 | s.trailerMu.Lock() 337 | s.trailerMD = trailer 338 | s.trailersOnly.Store(true) 339 | s.trailerMu.Unlock() 340 | 341 | // Try to extract *status.Status from headers. 342 | return statusFromHeader(trailer).Err() 343 | } 344 | if err != nil { 345 | return errors.Wrap(err, "failed to receive the response") 346 | } 347 | 348 | resHeader, err := parser.ParseResponseHeader(rawBody) 349 | if err != nil { 350 | return errors.Wrap(err, "failed to parse response header") 351 | } 352 | 353 | switch { 354 | case resHeader.IsMessageHeader(): 355 | msg, err := parser.ParseLengthPrefixedMessage(rawBody, resHeader.ContentLength) 356 | if err != nil { 357 | return err 358 | } 359 | if err := s.callOptions.codec.Unmarshal(msg, res); err != nil { 360 | return errors.Wrap(err, "failed to unmarshal response body") 361 | } 362 | return nil 363 | case resHeader.IsTrailerHeader(): 364 | s.closed.Store(true) 365 | 366 | status, trailer, err := parser.ParseStatusAndTrailer(rawBody, resHeader.ContentLength) 367 | if err != nil { 368 | return errors.Wrap(err, "failed to parse trailer") 369 | } 370 | s.trailerMu.Lock() 371 | s.trailerMD = trailer 372 | s.trailerMu.Unlock() 373 | 374 | if status.Code() != codes.OK { 375 | return status.Err() 376 | } 377 | return io.EOF 378 | default: 379 | return errors.New("unexpected header") 380 | } 381 | } 382 | 383 | func (s *bidiStream) CloseSend() error { 384 | if err := s.transport.CloseSend(); err != nil { 385 | return errors.Wrap(err, "failed to close the send stream") 386 | } 387 | s.sentCloseSend.Store(true) 388 | return nil 389 | } 390 | 391 | func (s *bidiStream) isTrailerOnly(err error) bool { 392 | return s.sentCloseSend.Load() && s.clientStream.isTrailerOnly(err) 393 | } 394 | 395 | func statusFromHeader(h metadata.MD) *status.Status { 396 | msgs, codeStr := h.Get("grpc-message"), h.Get("grpc-status") 397 | if len(codeStr) == 0 { 398 | return status.New(codes.Unknown, "response closed without grpc-status (headers only)") 399 | } 400 | i, err := strconv.Atoi(codeStr[0]) 401 | if err != nil { 402 | return status.New(codes.Unknown, err.Error()) 403 | } 404 | return status.New(codes.Code(i), msgs[0]) 405 | } 406 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 4 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= 11 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 12 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 13 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 14 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 15 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 20 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 21 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 22 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 25 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 27 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 32 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 34 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 35 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 36 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 37 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 38 | github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 39 | github.com/improbable-eng/grpc-web v0.12.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= 40 | github.com/ktr0731/dept v0.1.3/go.mod h1:b1EtCEjbjGShAfhZue+BrFKTG7sQmK7aSD7Q6VcGvO0= 41 | github.com/ktr0731/go-multierror v0.0.0-20171204182908-b7773ae21874/go.mod h1:ZWayuE/hCzOD96CJizvcYnqrbmTC7RAG332yNtlKj6w= 42 | github.com/ktr0731/grpc-test v0.1.4/go.mod h1:v47616grayBYXQveGWxO3OwjLB3nEEnHsZuMTc73FM0= 43 | github.com/ktr0731/grpc-test v0.1.12 h1:Yha+zH2hB48huOfbsEMfyG7FeHCrVWq4fYmHfr3iH3U= 44 | github.com/ktr0731/grpc-test v0.1.12/go.mod h1:AP4+ZrqSzdDaUNhAsp2fye06MXO2fdYY6YQJifb588M= 45 | github.com/ktr0731/grpc-web-go-client v0.2.8 h1:nUf9p+YWirmFwmH0mwtAWhuXvzovc+/3C/eAY2Fshnk= 46 | github.com/ktr0731/grpc-web-go-client v0.2.8/go.mod h1:1Iac8gFJvC/DRfZoGnFZsfEbEq/wQFK+2Ve1o3pHkCQ= 47 | github.com/ktr0731/modfile v1.11.2/go.mod h1:LzNwnHJWHbuDh3BO17lIqzqDldXqGu1HCydWH3SinE0= 48 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 49 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 50 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 51 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 52 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 53 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 54 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 55 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 57 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 61 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 62 | github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= 63 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 64 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 65 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 66 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 71 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 73 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 74 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 75 | go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= 76 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 77 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 78 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 79 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 80 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 81 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 82 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 83 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 86 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 87 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 88 | golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes= 89 | golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 90 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 91 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= 99 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 101 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 102 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 103 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 104 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 105 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 106 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 107 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 108 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 109 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 110 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 113 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 114 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 115 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 116 | google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 117 | google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= 118 | google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= 119 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 120 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 121 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 122 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 123 | google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= 124 | google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= 125 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 126 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 127 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 128 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 129 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 130 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 131 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 132 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 133 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 135 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 136 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 137 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 138 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 139 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 140 | -------------------------------------------------------------------------------- /grpcweb/grpcweb_test.go: -------------------------------------------------------------------------------- 1 | package grpcweb 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "errors" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "git.xx.network/elixxir/grpc-web-go-client/grpcweb/transport" 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/ktr0731/grpc-test/api" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/metadata" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | type unaryTransport struct { 23 | t *testing.T 24 | 25 | expectedMD metadata.MD 26 | h http.Header 27 | r []byte 28 | err error 29 | } 30 | 31 | func (t *unaryTransport) GetRemoteCertificate() (*x509.Certificate, error) { 32 | panic("UNIMPLEMENTED") 33 | return nil, nil 34 | } 35 | 36 | func (t *unaryTransport) Header() http.Header { 37 | return make(http.Header) 38 | } 39 | 40 | func (t *unaryTransport) Send(ctx context.Context, endpoint, contentType string, body io.Reader) (http.Header, []byte, error) { 41 | md, ok := metadata.FromOutgoingContext(ctx) 42 | if !ok { 43 | t.t.Fatalf("outgoing ctx should have metadata") 44 | } 45 | if diff := cmp.Diff(t.expectedMD, md); diff != "" { 46 | t.t.Fatalf("-want, +got\n%s", diff) 47 | } 48 | return t.h, t.r, t.err 49 | } 50 | 51 | func (t *unaryTransport) Close() error { 52 | return nil 53 | } 54 | 55 | func TestInvoke(t *testing.T) { 56 | header := http.Header{ 57 | "hakase": []string{"shinonome"}, 58 | "nano": []string{"shinonome"}, 59 | } 60 | 61 | cases := map[string]struct { 62 | transportHeader http.Header 63 | transportContentFileName string 64 | expectedHeader metadata.MD 65 | expectedTrailer metadata.MD 66 | expectedContent api.SimpleResponse 67 | expectedStatus *status.Status 68 | wantErr bool 69 | }{ 70 | "normal (only response)": { 71 | transportHeader: header, 72 | transportContentFileName: "response.in", 73 | expectedHeader: metadata.New(map[string]string{ 74 | "hakase": "shinonome", 75 | "nano": "shinonome", 76 | }), 77 | expectedContent: api.SimpleResponse{Message: "hello, ktr"}, 78 | expectedStatus: status.New(codes.OK, ""), 79 | }, 80 | "normal (trailer and response)": { 81 | transportHeader: header, 82 | transportContentFileName: "trailer_response.in", 83 | expectedHeader: metadata.New(map[string]string{ 84 | "hakase": "shinonome", 85 | "nano": "shinonome", 86 | }), 87 | expectedTrailer: metadata.New(map[string]string{ 88 | "trailer_key1": "trailer_val1", 89 | "trailer_key2": "trailer_val2", 90 | }), 91 | expectedContent: api.SimpleResponse{Message: "response"}, 92 | expectedStatus: status.New(codes.OK, ""), 93 | }, 94 | "error (trailer and response)": { 95 | transportHeader: header, 96 | transportContentFileName: "trailer_response_error.in", 97 | expectedHeader: metadata.New(map[string]string{ 98 | "hakase": "shinonome", 99 | "nano": "shinonome", 100 | }), 101 | expectedTrailer: metadata.New(map[string]string{ 102 | "trailer_key1": "trailer_val1", 103 | "trailer_key2": "trailer_val2", 104 | }), 105 | expectedStatus: status.New(codes.Internal, "internal error"), 106 | wantErr: true, 107 | }, 108 | } 109 | 110 | for name, c := range cases { 111 | t.Run(name, func(t *testing.T) { 112 | r, err := os.Open(filepath.Join("testdata", c.transportContentFileName)) 113 | if err != nil { 114 | t.Fatalf("Open should not return an error, but got '%s'", err) 115 | } 116 | 117 | rBytes, err := io.ReadAll(r) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | md := metadata.Pairs("yuko", "aioi") 123 | 124 | injectUnaryTransport(t, &unaryTransport{ 125 | t: t, 126 | expectedMD: md, 127 | h: c.transportHeader, 128 | r: rBytes, 129 | }) 130 | 131 | var header, trailer metadata.MD 132 | client, err := DialContext(":50051") 133 | if err != nil { 134 | t.Fatalf("DialContext should not return an error, but got '%s'", err) 135 | } 136 | 137 | var res api.SimpleResponse 138 | opts := []CallOption{Header(&header), Trailer(&trailer)} 139 | req := api.SimpleRequest{Name: "nano"} 140 | ctx := metadata.NewOutgoingContext(context.Background(), md) 141 | err = client.Invoke(ctx, "/service/Method", &req, &res, opts...) 142 | if c.wantErr { 143 | if err == nil { 144 | t.Fatalf("should return an error, but got nil") 145 | } 146 | } else if err != nil { 147 | t.Fatalf("should not return an error, but got '%s'", err) 148 | } 149 | 150 | stat := status.Convert(err) 151 | 152 | if diff := cmp.Diff(c.expectedHeader, header); diff != "" { 153 | t.Errorf("-want, +got\n%s", diff) 154 | } 155 | if diff := cmp.Diff(c.expectedTrailer, trailer); diff != "" { 156 | t.Errorf("-want, +got\n%s", diff) 157 | } 158 | if diff := cmp.Diff(c.expectedContent.String(), res.String()); diff != "" { 159 | t.Errorf("-want, +got\n%s", diff) 160 | } 161 | if stat.Code() != c.expectedStatus.Code() { 162 | t.Errorf("expected status code: %s, but got %s", c.expectedStatus.Code(), stat.Code()) 163 | } 164 | if stat.Message() != c.expectedStatus.Message() { 165 | t.Errorf("expected status message: %s, but got %s", c.expectedStatus.Message(), stat.Message()) 166 | } 167 | }) 168 | } 169 | } 170 | 171 | func injectUnaryTransport(t *testing.T, tr transport.UnaryTransport) { 172 | old := transport.NewUnary 173 | t.Cleanup(func() { 174 | transport.NewUnary = old 175 | }) 176 | transport.NewUnary = func(string, *transport.ConnectOptions) transport.UnaryTransport { 177 | return tr 178 | } 179 | } 180 | 181 | func TestServerStream(t *testing.T) { 182 | header := http.Header{ 183 | "hakase": []string{"shinonome"}, 184 | "nano": []string{"shinonome"}, 185 | } 186 | 187 | cases := map[string]struct { 188 | transportHeader http.Header 189 | transportContentFileName string 190 | expectedHeader metadata.MD 191 | expectedTrailer metadata.MD 192 | expectedContent []api.SimpleResponse 193 | expectedStatus *status.Status 194 | }{ 195 | "normal (only response)": { 196 | transportHeader: header, 197 | transportContentFileName: "server_stream_response.in", 198 | expectedHeader: metadata.New(map[string]string{ 199 | "hakase": "shinonome", 200 | "nano": "shinonome", 201 | }), 202 | expectedContent: []api.SimpleResponse{ 203 | {Message: "hello nano, I greet 1 times."}, 204 | {Message: "hello nano, I greet 2 times."}, 205 | {Message: "hello nano, I greet 3 times."}, 206 | }, 207 | expectedStatus: status.New(codes.OK, ""), 208 | }, 209 | "normal (trailer and response)": { 210 | transportHeader: header, 211 | transportContentFileName: "server_stream_trailer_response.in", 212 | expectedHeader: metadata.New(map[string]string{ 213 | "hakase": "shinonome", 214 | "nano": "shinonome", 215 | }), 216 | expectedTrailer: metadata.New(map[string]string{ 217 | "trailer_key1": "trailer_val1", 218 | "trailer_key2": "trailer_val2", 219 | }), 220 | expectedContent: []api.SimpleResponse{ 221 | {Message: "hello nano, I greet 1 times."}, 222 | {Message: "hello nano, I greet 2 times."}, 223 | {Message: "hello nano, I greet 3 times."}, 224 | }, 225 | expectedStatus: status.New(codes.OK, ""), 226 | }, 227 | "error (trailer and response)": { 228 | transportHeader: header, 229 | transportContentFileName: "server_stream_trailer_response_error.in", 230 | expectedHeader: metadata.New(map[string]string{ 231 | "hakase": "shinonome", 232 | "nano": "shinonome", 233 | }), 234 | expectedTrailer: metadata.New(map[string]string{ 235 | "trailer_key1": "trailer_val1", 236 | "trailer_key2": "trailer_val2", 237 | }), 238 | expectedContent: []api.SimpleResponse{ 239 | {Message: "hello nano, I greet 1 times."}, 240 | }, 241 | expectedStatus: status.New(codes.Internal, "internal error"), 242 | }, 243 | } 244 | 245 | for name, c := range cases { 246 | t.Run(name, func(t *testing.T) { 247 | r, err := os.Open(filepath.Join("testdata", c.transportContentFileName)) 248 | if err != nil { 249 | t.Fatalf("Open should not return an error, but got '%s'", err) 250 | } 251 | 252 | rBytes, err := io.ReadAll(r) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | 257 | md := metadata.Pairs("yuko", "aioi") 258 | 259 | injectUnaryTransport(t, &unaryTransport{ 260 | t: t, 261 | expectedMD: md, 262 | h: c.transportHeader, 263 | r: rBytes, 264 | }) 265 | 266 | client, err := DialContext(":50051") 267 | if err != nil { 268 | t.Fatalf("DialContext should not return an error, but got '%s'", err) 269 | } 270 | 271 | stm, err := client.NewServerStream(&grpc.StreamDesc{ServerStreams: true}, "/service/Method") 272 | if err != nil { 273 | t.Fatalf("should not return an error, but got '%s'", err) 274 | } 275 | 276 | ctx := metadata.NewOutgoingContext(context.Background(), md) 277 | if err := stm.Send(ctx, &api.SimpleRequest{Name: "nano"}); err != nil { 278 | t.Fatalf("Send should not return an error, but got '%s'", err) 279 | } 280 | 281 | var ress []api.SimpleResponse 282 | for { 283 | var res api.SimpleResponse 284 | err = stm.Receive(ctx, &res) // Don't create scoped error 285 | if errors.Is(err, io.EOF) { 286 | err = nil 287 | break 288 | } 289 | if err != nil { 290 | t.Logf("Receive returns an error: %s", err) 291 | break 292 | } 293 | ress = append(ress, res) 294 | } 295 | 296 | stat := status.Convert(err) 297 | 298 | header, err := stm.Header() 299 | if err != nil { 300 | t.Fatalf("Header should not return an error, but got '%s'", err) 301 | } 302 | if diff := cmp.Diff(c.expectedHeader, header); diff != "" { 303 | t.Errorf("-want, +got\n%s", diff) 304 | } 305 | if diff := cmp.Diff(c.expectedTrailer, stm.Trailer()); diff != "" { 306 | t.Errorf("-want, +got\n%s", diff) 307 | } 308 | for i := 0; i < len(c.expectedContent); i++ { 309 | if diff := cmp.Diff(c.expectedContent[i].String(), ress[i].String()); diff != "" { 310 | t.Errorf("-want, +got\n%s", diff) 311 | } 312 | } 313 | 314 | if stat.Code() != c.expectedStatus.Code() { 315 | t.Errorf("expected status code: %s, but got %s", c.expectedStatus.Code(), stat.Code()) 316 | } 317 | if stat.Message() != c.expectedStatus.Message() { 318 | t.Errorf("expected status message: %s, but got %s", c.expectedStatus.Message(), stat.Message()) 319 | } 320 | }) 321 | } 322 | } 323 | 324 | type clientStreamTransport struct { 325 | tt *testing.T 326 | expectedHeader http.Header 327 | 328 | sentCloseSend bool 329 | 330 | h, t http.Header 331 | r []io.ReadCloser 332 | err error 333 | 334 | i int 335 | } 336 | 337 | func (s *clientStreamTransport) SetRequestHeader(h http.Header) { 338 | if diff := cmp.Diff(s.expectedHeader, h); diff != "" { 339 | s.tt.Fatalf("-want, +got\n%s", diff) 340 | } 341 | } 342 | 343 | func (s *clientStreamTransport) Header() (http.Header, error) { 344 | return s.h, nil 345 | } 346 | 347 | func (s *clientStreamTransport) Trailer() http.Header { 348 | return s.t 349 | } 350 | 351 | func (s *clientStreamTransport) Send(context.Context, io.Reader) error { 352 | return nil 353 | } 354 | 355 | func (s *clientStreamTransport) Receive(context.Context) (io.ReadCloser, error) { 356 | if s.sentCloseSend && s.err != nil { 357 | return nil, s.err 358 | } 359 | r := s.r[s.i] 360 | s.i++ 361 | return r, nil 362 | } 363 | 364 | func (s *clientStreamTransport) CloseSend() error { 365 | s.sentCloseSend = true 366 | return nil 367 | } 368 | 369 | func (s *clientStreamTransport) Close() error { 370 | return nil 371 | } 372 | 373 | func TestClientStream(t *testing.T) { 374 | header := http.Header{ 375 | "hakase": []string{"shinonome"}, 376 | "nano": []string{"shinonome"}, 377 | } 378 | 379 | cases := map[string]struct { 380 | transportHeader http.Header 381 | transportContentFileNames []string 382 | transportErr error 383 | expectedHeader metadata.MD 384 | expectedTrailer metadata.MD 385 | expectedContent api.SimpleResponse 386 | expectedStatus *status.Status 387 | }{ 388 | "normal (only response)": { 389 | transportHeader: header, 390 | transportContentFileNames: []string{"client_stream_response1.in", "client_stream_response2.in"}, 391 | expectedHeader: metadata.New(map[string]string{ 392 | "hakase": "shinonome", 393 | "nano": "shinonome", 394 | }), 395 | expectedContent: api.SimpleResponse{ 396 | Message: "you sent requests 2 times (hakase, nano).", 397 | }, 398 | expectedStatus: status.New(codes.OK, ""), 399 | }, 400 | "normal (trailer and response)": { 401 | transportHeader: header, 402 | transportContentFileNames: []string{"client_stream_trailer_response1.in", "client_stream_trailer_response2.in"}, 403 | expectedHeader: metadata.New(map[string]string{ 404 | "hakase": "shinonome", 405 | "nano": "shinonome", 406 | }), 407 | expectedTrailer: metadata.New(map[string]string{ 408 | "trailer_key1": "trailer_val1", 409 | "trailer_key2": "trailer_val2", 410 | }), 411 | expectedContent: api.SimpleResponse{ 412 | Message: "you sent requests 2 times (hakase, nano).", 413 | }, 414 | expectedStatus: status.New(codes.OK, ""), 415 | }, 416 | "error (only response)": { 417 | transportHeader: header, 418 | transportContentFileNames: []string{"client_stream_response_error.in"}, 419 | expectedHeader: metadata.New(map[string]string{ 420 | "hakase": "shinonome", 421 | "nano": "shinonome", 422 | }), 423 | expectedStatus: status.New(codes.Internal, "internal error"), 424 | }, 425 | "error (trailer only)": { 426 | transportHeader: http.Header{ 427 | "hakase": []string{"shinonome"}, 428 | "nano": []string{"shinonome"}, 429 | "grpc-status": []string{"13"}, 430 | "grpc-message": []string{"internal error"}, 431 | }, 432 | transportErr: io.ErrUnexpectedEOF, 433 | expectedHeader: nil, 434 | expectedTrailer: metadata.New(map[string]string{ 435 | "hakase": "shinonome", 436 | "nano": "shinonome", 437 | "grpc-status": "13", 438 | "grpc-message": "internal error", 439 | }), 440 | expectedStatus: status.New(codes.Internal, "internal error"), 441 | }, 442 | } 443 | 444 | for name, c := range cases { 445 | t.Run(name, func(t *testing.T) { 446 | var rs []io.ReadCloser 447 | for _, fname := range c.transportContentFileNames { 448 | r, err := os.Open(filepath.Join("testdata", fname)) 449 | if err != nil { 450 | t.Fatalf("Open should not return an error, but got '%s'", err) 451 | } 452 | rs = append(rs, r) 453 | } 454 | 455 | h := make(http.Header) 456 | h.Add("yuko", "aioi") 457 | injectClientStreamTransport(t, &clientStreamTransport{ 458 | tt: t, 459 | expectedHeader: h, 460 | h: c.transportHeader, 461 | r: rs, 462 | err: c.transportErr, 463 | }) 464 | 465 | client, err := DialContext(":50051", WithInsecure(), WithInsecureTlsVerification()) 466 | if err != nil { 467 | t.Fatalf("DialContext should not return an error, but got '%s'", err) 468 | } 469 | 470 | stm, err := client.NewClientStream(&grpc.StreamDesc{ClientStreams: true}, "/service/Method") 471 | if err != nil { 472 | t.Fatalf("should not return an error, but got '%s'", err) 473 | } 474 | 475 | ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("yuko", "aioi")) 476 | if err := stm.Send(ctx, &api.SimpleRequest{Name: "nano"}); err != nil { 477 | t.Fatalf("Send should not return an error, but got '%s'", err) 478 | } 479 | if err := stm.Send(ctx, &api.SimpleRequest{Name: "hakase"}); err != nil { 480 | t.Fatalf("Send should not return an error, but got '%s'", err) 481 | } 482 | 483 | var res api.SimpleResponse 484 | err = stm.CloseAndReceive(ctx, &res) 485 | 486 | stat := status.Convert(err) 487 | 488 | header, err := stm.Header() 489 | if err != nil { 490 | t.Fatalf("Header should not return an error, but got '%s'", err) 491 | } 492 | if diff := cmp.Diff(c.expectedHeader, header); diff != "" { 493 | t.Errorf("-want, +got\n%s", diff) 494 | } 495 | if diff := cmp.Diff(c.expectedTrailer, stm.Trailer()); diff != "" { 496 | t.Errorf("-want, +got\n%s", diff) 497 | } 498 | if diff := cmp.Diff(c.expectedContent.String(), res.String()); diff != "" { 499 | t.Errorf("-want, +got\n%s", diff) 500 | } 501 | if stat.Code() != c.expectedStatus.Code() { 502 | t.Errorf("expected status code: %s, but got %s", c.expectedStatus.Code(), stat.Code()) 503 | } 504 | if stat.Message() != c.expectedStatus.Message() { 505 | t.Errorf("expected status message: %s, but got %s", c.expectedStatus.Message(), stat.Message()) 506 | } 507 | }) 508 | } 509 | } 510 | 511 | func TestBidiStream(t *testing.T) { 512 | header := http.Header{ 513 | "hakase": []string{"shinonome"}, 514 | "nano": []string{"shinonome"}, 515 | } 516 | 517 | cases := map[string]struct { 518 | transportHeader http.Header 519 | transportContentFileNames []string 520 | transportErr error 521 | expectedHeader metadata.MD 522 | expectedTrailer metadata.MD 523 | expectedContent []api.SimpleResponse 524 | expectedStatus *status.Status 525 | }{ 526 | "normal (only response)": { 527 | transportHeader: header, 528 | transportContentFileNames: []string{"bidi_stream_response1.in", "bidi_stream_response2.in", "bidi_stream_response3.in", "bidi_stream_response4.in"}, 529 | expectedHeader: metadata.New(map[string]string{ 530 | "hakase": "shinonome", 531 | "nano": "shinonome", 532 | }), 533 | expectedContent: []api.SimpleResponse{ 534 | {Message: "hello ktr, I greet 1 times."}, 535 | {Message: "hello ktr, I greet 2 times."}, 536 | {Message: "hello ktr, I greet 3 times."}, 537 | }, 538 | expectedStatus: status.New(codes.OK, ""), 539 | }, 540 | "normal (trailer and response)": { 541 | transportHeader: header, 542 | transportContentFileNames: []string{"bidi_stream_response1.in", "bidi_stream_response2.in", "bidi_stream_response3.in", "bidi_stream_response1.in", "bidi_stream_response2.in", "bidi_stream_response3.in", "bidi_stream_trailer_response.in"}, 543 | expectedHeader: metadata.New(map[string]string{ 544 | "hakase": "shinonome", 545 | "nano": "shinonome", 546 | }), 547 | expectedTrailer: metadata.New(map[string]string{ 548 | "trailer_key1": "trailer_val1", 549 | "trailer_key2": "trailer_val2", 550 | }), 551 | expectedContent: []api.SimpleResponse{ 552 | {Message: "hello ktr, I greet 1 times."}, 553 | {Message: "hello ktr, I greet 2 times."}, 554 | {Message: "hello ktr, I greet 3 times."}, 555 | {Message: "hello ktr, I greet 1 times."}, 556 | {Message: "hello ktr, I greet 2 times."}, 557 | {Message: "hello ktr, I greet 3 times."}, 558 | }, 559 | expectedStatus: status.New(codes.OK, ""), 560 | }, 561 | "error (only response)": { 562 | transportHeader: header, 563 | transportContentFileNames: []string{"bidi_stream_response_error.in"}, 564 | expectedHeader: metadata.New(map[string]string{ 565 | "hakase": "shinonome", 566 | "nano": "shinonome", 567 | }), 568 | expectedStatus: status.New(codes.Internal, "internal error"), 569 | }, 570 | "error (trailer only)": { 571 | transportHeader: http.Header{ 572 | "hakase": []string{"shinonome"}, 573 | "nano": []string{"shinonome"}, 574 | "grpc-status": []string{"13"}, 575 | "grpc-message": []string{"internal error"}, 576 | }, 577 | transportErr: io.ErrUnexpectedEOF, 578 | expectedHeader: nil, 579 | expectedTrailer: metadata.New(map[string]string{ 580 | "hakase": "shinonome", 581 | "nano": "shinonome", 582 | "grpc-status": "13", 583 | "grpc-message": "internal error", 584 | }), 585 | expectedStatus: status.New(codes.Internal, "internal error"), 586 | }, 587 | } 588 | 589 | for name, c := range cases { 590 | t.Run(name, func(t *testing.T) { 591 | var rs []io.ReadCloser 592 | for _, fname := range c.transportContentFileNames { 593 | r, err := os.Open(filepath.Join("testdata", fname)) 594 | if err != nil { 595 | t.Fatalf("Open should not return an error, but got '%s'", err) 596 | } 597 | rs = append(rs, r) 598 | } 599 | 600 | h := make(http.Header) 601 | h.Add("yuko", "aioi") 602 | injectClientStreamTransport(t, &clientStreamTransport{ 603 | tt: t, 604 | expectedHeader: h, 605 | h: c.transportHeader, 606 | r: rs, 607 | err: c.transportErr, 608 | }) 609 | 610 | client, err := DialContext(":50051", WithInsecure()) 611 | if err != nil { 612 | t.Fatalf("DialContext should not return an error, but got '%s'", err) 613 | } 614 | 615 | stm, err := client.NewBidiStream(&grpc.StreamDesc{ClientStreams: true, ServerStreams: true}, "/service/Method") 616 | if err != nil { 617 | t.Fatalf("should not return an error, but got '%s'", err) 618 | } 619 | 620 | ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("yuko", "aioi")) 621 | if err := stm.Send(ctx, &api.SimpleRequest{Name: "nano"}); err != nil { 622 | t.Fatalf("Send should not return an error, but got '%s'", err) 623 | } 624 | if err := stm.Send(ctx, &api.SimpleRequest{Name: "hakase"}); err != nil { 625 | t.Fatalf("Send should not return an error, but got '%s'", err) 626 | } 627 | err = stm.CloseSend() 628 | 629 | var ress []api.SimpleResponse 630 | for { 631 | var res api.SimpleResponse 632 | err = stm.Receive(ctx, &res) // Don't create scoped error 633 | if errors.Is(err, io.EOF) { 634 | err = nil 635 | break 636 | } 637 | if err != nil { 638 | t.Logf("Receive returns an error: %s", err) 639 | break 640 | } 641 | ress = append(ress, res) 642 | } 643 | 644 | stat := status.Convert(err) 645 | 646 | header, err := stm.Header() 647 | if err != nil { 648 | t.Fatalf("Header should not return an error, but got '%s'", err) 649 | } 650 | if diff := cmp.Diff(c.expectedHeader, header); diff != "" { 651 | t.Errorf("-want, +got\n%s", diff) 652 | } 653 | if diff := cmp.Diff(c.expectedTrailer, stm.Trailer()); diff != "" { 654 | t.Errorf("-want, +got\n%s", diff) 655 | } 656 | for i := 0; i < len(c.expectedContent); i++ { 657 | if diff := cmp.Diff(c.expectedContent[i].String(), ress[i].String()); diff != "" { 658 | t.Errorf("-want, +got\n%s", diff) 659 | } 660 | } 661 | if stat.Code() != c.expectedStatus.Code() { 662 | t.Errorf("expected status code: %s, but got %s", c.expectedStatus.Code(), stat.Code()) 663 | } 664 | if stat.Message() != c.expectedStatus.Message() { 665 | t.Errorf("expected status message: %s, but got %s", c.expectedStatus.Message(), stat.Message()) 666 | } 667 | }) 668 | } 669 | } 670 | 671 | func injectClientStreamTransport(t *testing.T, tr transport.ClientStreamTransport) { 672 | old := transport.NewClientStream 673 | t.Cleanup(func() { 674 | transport.NewClientStream = old 675 | }) 676 | transport.NewClientStream = func(string, string) (transport.ClientStreamTransport, error) { 677 | return tr, nil 678 | } 679 | } 680 | --------------------------------------------------------------------------------