├── LICENSE ├── README.md ├── cmd ├── git-remote-grpc │ └── main.go └── grpc-server │ └── main.go ├── gitpb ├── gitpb.pb.go └── gitpb.proto ├── go.mod └── go.sum /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Encore 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-remote-grpc 2 | 3 | This project provides sample code for building a git remote helper that 4 | allows for cloning, fetching, and pushing updates to remote git repositories 5 | over gRPC. 6 | 7 | This can be really useful for leveraging existing gRPC infrastructure, 8 | such as TLS authentication support, to avoid having to build a separate 9 | Public Key Infrastructure for authenticating with SSH keys. 10 | 11 | To learn more, read the associated [blog post](http://encore.dev/blog/git-clone-grpc) 12 | on the Encore Engineering Blog. -------------------------------------------------------------------------------- /cmd/git-remote-grpc/main.go: -------------------------------------------------------------------------------- 1 | // Command git-remote-grpc is a git remote-helper to perform 2 | // authenticated Git operations over gRPC. 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/url" 11 | "os" 12 | "strings" 13 | 14 | "github.com/encoredev/git-remote-grpc/gitpb" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/metadata" 17 | ) 18 | 19 | //go:generate protoc -I . --go_out=plugins=grpc,paths=source_relative:./ ./gitpb/gitpb.proto 20 | 21 | func main() { 22 | if err := run(os.Args); err != nil { 23 | fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err) 24 | os.Exit(1) 25 | } 26 | } 27 | 28 | func run(args []string) error { 29 | stdin := bufio.NewReader(os.Stdin) 30 | stdout := os.Stdout 31 | 32 | // Read commands from stdin. 33 | for { 34 | cmd, err := stdin.ReadString('\n') 35 | if err != nil { 36 | return fmt.Errorf("unexpected error reading stdin: %v", err) 37 | } 38 | cmd = cmd[:len(cmd)-1] // skip trailing newline 39 | switch { 40 | case cmd == "capabilities": 41 | if _, err := stdout.Write([]byte("*connect\n\n")); err != nil { 42 | return err 43 | } 44 | case strings.HasPrefix(cmd, "connect "): 45 | service := cmd[len("connect "):] 46 | return connect(args, service, stdin, stdout) 47 | default: 48 | return fmt.Errorf("unsupported command: %s", cmd) 49 | } 50 | } 51 | } 52 | 53 | // connect implements the "connect" capability by copying data 54 | // to and from the remote end over gRPC. 55 | func connect(args []string, svc string, stdin io.Reader, stdout io.Writer) error { 56 | stream, err := grpcConnect(args, svc) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // Communicate to Git that the connection is established 62 | os.Stdout.Write([]byte("\n")) 63 | 64 | // writeErr captures the error from the write side. 65 | // nil indicates stdin was closed and everything was successful. 66 | // io.EOF indicates a failure to send the message on the stream. 67 | writeErr := make(chan error, 1) 68 | 69 | // Writer goroutine that reads from stdin and writes to the stream. 70 | // Sends on writeErr when done sending. nil indicates success and 71 | // a non-nil error indicates something went wrong. 72 | go func() { 73 | var buf [1024]byte 74 | for { 75 | n, readErr := stdin.Read(buf[:]) 76 | if n > 0 { 77 | if err := stream.Send(&gitpb.Data{Data: buf[:n]}); err != nil { 78 | writeErr <- fmt.Errorf("write: %v", err) 79 | return 80 | } 81 | } 82 | 83 | if readErr != nil { 84 | if readErr == io.EOF { 85 | stream.CloseSend() 86 | writeErr <- nil 87 | } else { 88 | writeErr <- fmt.Errorf("reading stdin: %v", readErr) 89 | } 90 | return 91 | } 92 | } 93 | }() 94 | 95 | // Read from the stream and copy it to stdout. 96 | // If the reads complete successfully it waits 97 | // for the write end to complete before returning. 98 | for { 99 | msg, err := stream.Recv() 100 | if err == io.EOF { 101 | // No more data from the server. 102 | // Wait for the write end to complete. 103 | if err := <-writeErr; err != nil { 104 | return err 105 | } 106 | return nil 107 | } else if err != nil { 108 | return fmt.Errorf("read: %v", err) 109 | } else { 110 | if _, err := stdout.Write(msg.Data); err != nil { 111 | return fmt.Errorf("writing stdout: %v", err) 112 | } 113 | } 114 | } 115 | } 116 | 117 | // grpcConnect parses the remote address from the args 118 | // and invokes the Connect endpoint. 119 | func grpcConnect(args []string, svc string) (gitpb.Git_ConnectClient, error) { 120 | var ( 121 | addr *url.URL 122 | err error 123 | ) 124 | switch { 125 | case len(args) >= 3: 126 | addr, err = url.Parse(args[2]) 127 | case len(args) == 2: 128 | addr, err = url.Parse(args[1]) 129 | default: 130 | err = fmt.Errorf("no address given") 131 | } 132 | if err != nil { 133 | return nil, fmt.Errorf("parsing remote address: %v", err) 134 | } 135 | 136 | conn, err := grpc.Dial(addr.Host, grpc.WithInsecure()) 137 | if err != nil { 138 | return nil, fmt.Errorf("dial %s: %v", addr.Host, err) 139 | } 140 | gitClient := gitpb.NewGitClient(conn) 141 | 142 | // Identify the repository by the address path. 143 | repoPath := strings.TrimPrefix(addr.Path, "/") 144 | ctx := metadata.AppendToOutgoingContext(context.Background(), 145 | "service", svc, 146 | "repository", repoPath, 147 | ) 148 | return gitClient.Connect(ctx) 149 | } 150 | -------------------------------------------------------------------------------- /cmd/grpc-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "log" 7 | "net" 8 | "os/exec" 9 | 10 | "github.com/encoredev/git-remote-grpc/gitpb" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/metadata" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | func main() { 18 | addr := flag.String("addr", "localhost:8080", "gRPC listen address") 19 | flag.Parse() 20 | log.Println("listening for grpc on", *addr) 21 | ln, err := net.Listen("tcp", *addr) 22 | if err != nil { 23 | log.Fatalf("failed to listen: %v", err) 24 | } 25 | srv := grpc.NewServer() 26 | gitpb.RegisterGitServer(srv, &Server{}) 27 | log.Fatalln(srv.Serve(ln)) 28 | } 29 | 30 | const ( 31 | pushSvc = "git-receive-pack" 32 | fetchSvc = "git-upload-pack" 33 | ) 34 | 35 | type Server struct { 36 | } 37 | 38 | // Assert that Server implements gitpb.GitServer. 39 | var _ gitpb.GitServer = (*Server)(nil) 40 | 41 | func (s *Server) Connect(stream gitpb.Git_ConnectServer) error { 42 | // Parse the repository path and service to invoke from the gRPC header. 43 | // Note that the svc is invoked directly as an executable, so parseHeader 44 | // must validate these very carefully! 45 | svc, repoPath, err := s.parseHeader(stream) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | var stderr bytes.Buffer 51 | cmd := exec.Command(svc, repoPath) 52 | cmd.Stdin = &streamReader{stream: stream} 53 | cmd.Stdout = &streamWriter{stream: stream} 54 | cmd.Stderr = &stderr 55 | 56 | if err := cmd.Run(); err == nil { 57 | return nil 58 | } 59 | return status.Errorf(codes.Internal, "%s failed: %s", svc, stderr.Bytes()) 60 | } 61 | 62 | // parseHeader parses the gRPC header and validates the service and repository paths. 63 | func (s *Server) parseHeader(stream gitpb.Git_ConnectServer) (service, repoPath string, err error) { 64 | ctx := stream.Context() 65 | md, ok := metadata.FromIncomingContext(ctx) 66 | if !ok { 67 | return "", "", status.Error(codes.InvalidArgument, "missing stream metadata") 68 | } 69 | 70 | repo := md.Get("repository") 71 | svc := md.Get("service") 72 | if len(repo) != 1 || len(svc) != 1 { 73 | return "", "", status.Errorf(codes.InvalidArgument, "invalid repository (%v) or service (%v)", repo, svc) 74 | } 75 | 76 | // DANGER: Check the service name against the whitelist to guard against remote execution. 77 | if svc[0] != pushSvc && svc[0] != fetchSvc { 78 | return "", "", status.Errorf(codes.InvalidArgument, "bad service: %s", svc[0]) 79 | } 80 | 81 | // TODO: Change this to your own validation logic to make sure the repository is one 82 | // you want to expose. 83 | //if true { 84 | // return "", "", status.Errorf(codes.InvalidArgument, "unknown repository: %s", repo[0]) 85 | //} 86 | 87 | return svc[0], repo[0], nil 88 | } 89 | 90 | // streamReader implements io.Reader by reading from stream. 91 | type streamReader struct { 92 | stream gitpb.Git_ConnectServer 93 | buf []byte 94 | } 95 | 96 | func (sr *streamReader) Read(p []byte) (int, error) { 97 | // If we have remaining data from the previous message we received 98 | // from the stream, simply return that. 99 | if len(sr.buf) > 0 { 100 | n := copy(p, sr.buf) 101 | sr.buf = sr.buf[n:] 102 | return n, nil 103 | } 104 | 105 | // No more buffered data, wait for a new message from the stream. 106 | msg, err := sr.stream.Recv() 107 | if err != nil { 108 | return 0, err 109 | } 110 | // Read as much data as possible directly to the waiting caller. 111 | // Anything remaining beyond that gets buffered until the next Read call. 112 | n := copy(p, msg.Data) 113 | sr.buf = msg.Data[n:] 114 | return n, nil 115 | } 116 | 117 | // streamWriter implements io.Writer by sending the data on stream. 118 | type streamWriter struct { 119 | stream gitpb.Git_ConnectServer 120 | } 121 | 122 | func (sw *streamWriter) Write(p []byte) (int, error) { 123 | err := sw.stream.Send(&gitpb.Data{Data: p}) 124 | if err != nil { 125 | return 0, err 126 | } 127 | return len(p), nil 128 | } 129 | -------------------------------------------------------------------------------- /gitpb/gitpb.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: gitpb/gitpb.proto 3 | 4 | package gitpb 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | _ "github.com/golang/protobuf/ptypes/empty" 11 | grpc "google.golang.org/grpc" 12 | math "math" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 25 | 26 | type Data struct { 27 | Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` 28 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 29 | XXX_unrecognized []byte `json:"-"` 30 | XXX_sizecache int32 `json:"-"` 31 | } 32 | 33 | func (m *Data) Reset() { *m = Data{} } 34 | func (m *Data) String() string { return proto.CompactTextString(m) } 35 | func (*Data) ProtoMessage() {} 36 | func (*Data) Descriptor() ([]byte, []int) { 37 | return fileDescriptor_63d25d1ceb609c7c, []int{0} 38 | } 39 | 40 | func (m *Data) XXX_Unmarshal(b []byte) error { 41 | return xxx_messageInfo_Data.Unmarshal(m, b) 42 | } 43 | func (m *Data) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 44 | return xxx_messageInfo_Data.Marshal(b, m, deterministic) 45 | } 46 | func (m *Data) XXX_Merge(src proto.Message) { 47 | xxx_messageInfo_Data.Merge(m, src) 48 | } 49 | func (m *Data) XXX_Size() int { 50 | return xxx_messageInfo_Data.Size(m) 51 | } 52 | func (m *Data) XXX_DiscardUnknown() { 53 | xxx_messageInfo_Data.DiscardUnknown(m) 54 | } 55 | 56 | var xxx_messageInfo_Data proto.InternalMessageInfo 57 | 58 | func (m *Data) GetData() []byte { 59 | if m != nil { 60 | return m.Data 61 | } 62 | return nil 63 | } 64 | 65 | func init() { 66 | proto.RegisterType((*Data)(nil), "gitpb.Data") 67 | } 68 | 69 | func init() { proto.RegisterFile("gitpb/gitpb.proto", fileDescriptor_63d25d1ceb609c7c) } 70 | 71 | var fileDescriptor_63d25d1ceb609c7c = []byte{ 72 | // 127 bytes of a gzipped FileDescriptorProto 73 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4c, 0xcf, 0x2c, 0x29, 74 | 0x48, 0xd2, 0x07, 0x93, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xac, 0x60, 0x8e, 0x94, 0x74, 75 | 0x7a, 0x7e, 0x7e, 0x7a, 0x4e, 0xaa, 0x3e, 0x58, 0x30, 0xa9, 0x34, 0x4d, 0x3f, 0x35, 0xb7, 0xa0, 76 | 0xa4, 0x12, 0xa2, 0x46, 0x49, 0x8a, 0x8b, 0xc5, 0x25, 0xb1, 0x24, 0x51, 0x48, 0x88, 0x8b, 0x25, 77 | 0x25, 0xb1, 0x24, 0x51, 0x82, 0x51, 0x81, 0x51, 0x83, 0x27, 0x08, 0xcc, 0x36, 0xd2, 0xe3, 0x62, 78 | 0x76, 0xcf, 0x2c, 0x11, 0x52, 0xe7, 0x62, 0x77, 0xce, 0xcf, 0xcb, 0x4b, 0x4d, 0x2e, 0x11, 0xe2, 79 | 0xd6, 0x83, 0x98, 0x0f, 0xd2, 0x22, 0x85, 0xcc, 0xd1, 0x60, 0x34, 0x60, 0x4c, 0x62, 0x03, 0x1b, 80 | 0x69, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xe4, 0x8d, 0xf8, 0x1f, 0x8b, 0x00, 0x00, 0x00, 81 | } 82 | 83 | // Reference imports to suppress errors if they are not otherwise used. 84 | var _ context.Context 85 | var _ grpc.ClientConn 86 | 87 | // This is a compile-time assertion to ensure that this generated file 88 | // is compatible with the grpc package it is being compiled against. 89 | const _ = grpc.SupportPackageIsVersion4 90 | 91 | // GitClient is the client API for Git service. 92 | // 93 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 94 | type GitClient interface { 95 | Connect(ctx context.Context, opts ...grpc.CallOption) (Git_ConnectClient, error) 96 | } 97 | 98 | type gitClient struct { 99 | cc *grpc.ClientConn 100 | } 101 | 102 | func NewGitClient(cc *grpc.ClientConn) GitClient { 103 | return &gitClient{cc} 104 | } 105 | 106 | func (c *gitClient) Connect(ctx context.Context, opts ...grpc.CallOption) (Git_ConnectClient, error) { 107 | stream, err := c.cc.NewStream(ctx, &_Git_serviceDesc.Streams[0], "/gitpb.Git/Connect", opts...) 108 | if err != nil { 109 | return nil, err 110 | } 111 | x := &gitConnectClient{stream} 112 | return x, nil 113 | } 114 | 115 | type Git_ConnectClient interface { 116 | Send(*Data) error 117 | Recv() (*Data, error) 118 | grpc.ClientStream 119 | } 120 | 121 | type gitConnectClient struct { 122 | grpc.ClientStream 123 | } 124 | 125 | func (x *gitConnectClient) Send(m *Data) error { 126 | return x.ClientStream.SendMsg(m) 127 | } 128 | 129 | func (x *gitConnectClient) Recv() (*Data, error) { 130 | m := new(Data) 131 | if err := x.ClientStream.RecvMsg(m); err != nil { 132 | return nil, err 133 | } 134 | return m, nil 135 | } 136 | 137 | // GitServer is the server API for Git service. 138 | type GitServer interface { 139 | Connect(Git_ConnectServer) error 140 | } 141 | 142 | func RegisterGitServer(s *grpc.Server, srv GitServer) { 143 | s.RegisterService(&_Git_serviceDesc, srv) 144 | } 145 | 146 | func _Git_Connect_Handler(srv interface{}, stream grpc.ServerStream) error { 147 | return srv.(GitServer).Connect(&gitConnectServer{stream}) 148 | } 149 | 150 | type Git_ConnectServer interface { 151 | Send(*Data) error 152 | Recv() (*Data, error) 153 | grpc.ServerStream 154 | } 155 | 156 | type gitConnectServer struct { 157 | grpc.ServerStream 158 | } 159 | 160 | func (x *gitConnectServer) Send(m *Data) error { 161 | return x.ServerStream.SendMsg(m) 162 | } 163 | 164 | func (x *gitConnectServer) Recv() (*Data, error) { 165 | m := new(Data) 166 | if err := x.ServerStream.RecvMsg(m); err != nil { 167 | return nil, err 168 | } 169 | return m, nil 170 | } 171 | 172 | var _Git_serviceDesc = grpc.ServiceDesc{ 173 | ServiceName: "gitpb.Git", 174 | HandlerType: (*GitServer)(nil), 175 | Methods: []grpc.MethodDesc{}, 176 | Streams: []grpc.StreamDesc{ 177 | { 178 | StreamName: "Connect", 179 | Handler: _Git_Connect_Handler, 180 | ServerStreams: true, 181 | ClientStreams: true, 182 | }, 183 | }, 184 | Metadata: "gitpb/gitpb.proto", 185 | } 186 | -------------------------------------------------------------------------------- /gitpb/gitpb.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/empty.proto"; 4 | 5 | package gitpb; 6 | 7 | service Git { 8 | rpc Connect (stream Data) returns (stream Data); 9 | } 10 | 11 | message Data { 12 | bytes data = 1; 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/encoredev/git-remote-grpc 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/golang/protobuf v1.3.3 7 | google.golang.org/grpc v1.29.1 8 | ) 9 | -------------------------------------------------------------------------------- /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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 6 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 7 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 8 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 10 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 14 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 15 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 16 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 17 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 20 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 21 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 22 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 23 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 24 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 25 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 26 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 27 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 28 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 29 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 38 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 39 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 40 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 41 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 42 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 43 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 44 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 45 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 46 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 47 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 48 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 49 | google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= 50 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 51 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 52 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 53 | --------------------------------------------------------------------------------