github.com/argoproj/argo-cd/v2@v2.10.9/pkg/apiclient/grpcproxy.go (about) 1 package apiclient 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "os" 11 "strconv" 12 "strings" 13 14 "google.golang.org/grpc" 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/keepalive" 17 "google.golang.org/grpc/metadata" 18 "google.golang.org/grpc/status" 19 20 "github.com/argoproj/argo-cd/v2/common" 21 argocderrors "github.com/argoproj/argo-cd/v2/util/errors" 22 argoio "github.com/argoproj/argo-cd/v2/util/io" 23 "github.com/argoproj/argo-cd/v2/util/rand" 24 ) 25 26 const ( 27 frameHeaderLength = 5 28 endOfStreamFlag = 128 29 ) 30 31 type noopCodec struct{} 32 33 func (noopCodec) Marshal(v interface{}) ([]byte, error) { 34 return v.([]byte), nil 35 } 36 37 func (noopCodec) Unmarshal(data []byte, v interface{}) error { 38 pointer := v.(*[]byte) 39 *pointer = data 40 return nil 41 } 42 43 func (noopCodec) Name() string { 44 return "proto" 45 } 46 47 func toFrame(msg []byte) []byte { 48 frame := append([]byte{0, 0, 0, 0}, msg...) 49 binary.BigEndian.PutUint32(frame, uint32(len(msg))) 50 frame = append([]byte{0}, frame...) 51 return frame 52 } 53 54 func (c *client) executeRequest(fullMethodName string, msg []byte, md metadata.MD) (*http.Response, error) { 55 schema := "https" 56 if c.PlainText { 57 schema = "http" 58 } 59 rootPath := strings.TrimRight(strings.TrimLeft(c.GRPCWebRootPath, "/"), "/") 60 61 var requestURL string 62 if rootPath != "" { 63 requestURL = fmt.Sprintf("%s://%s/%s%s", schema, c.ServerAddr, rootPath, fullMethodName) 64 } else { 65 requestURL = fmt.Sprintf("%s://%s%s", schema, c.ServerAddr, fullMethodName) 66 } 67 req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(toFrame(msg))) 68 69 if err != nil { 70 return nil, err 71 } 72 for k, v := range md { 73 if strings.HasPrefix(k, ":") { 74 continue 75 } 76 for i := range v { 77 req.Header.Set(k, v[i]) 78 } 79 } 80 req.Header.Set("content-type", "application/grpc-web+proto") 81 82 resp, err := c.httpClient.Do(req) 83 if err != nil { 84 return nil, err 85 } 86 if resp.StatusCode != http.StatusOK { 87 return nil, fmt.Errorf("%s %s failed with status code %d", req.Method, req.URL, resp.StatusCode) 88 } 89 var code codes.Code 90 if statusStr := resp.Header.Get("Grpc-Status"); statusStr != "" { 91 statusInt, err := strconv.ParseUint(statusStr, 10, 32) 92 if err != nil { 93 code = codes.Unknown 94 } else { 95 code = codes.Code(statusInt) 96 } 97 if code != codes.OK { 98 return nil, status.Error(code, resp.Header.Get("Grpc-Message")) 99 } 100 } 101 return resp, nil 102 } 103 104 func (c *client) startGRPCProxy() (*grpc.Server, net.Listener, error) { 105 randSuffix, err := rand.String(16) 106 if err != nil { 107 return nil, nil, fmt.Errorf("failed to generate random socket filename: %w", err) 108 } 109 serverAddr := fmt.Sprintf("%s/argocd-%s.sock", os.TempDir(), randSuffix) 110 ln, err := net.Listen("unix", serverAddr) 111 112 if err != nil { 113 return nil, nil, err 114 } 115 proxySrv := grpc.NewServer( 116 grpc.ForceServerCodec(&noopCodec{}), 117 grpc.KeepaliveEnforcementPolicy( 118 keepalive.EnforcementPolicy{ 119 MinTime: common.GetGRPCKeepAliveEnforcementMinimum(), 120 }, 121 ), 122 grpc.UnknownServiceHandler(func(srv interface{}, stream grpc.ServerStream) error { 123 fullMethodName, ok := grpc.MethodFromServerStream(stream) 124 if !ok { 125 return fmt.Errorf("Unable to get method name from stream context.") 126 } 127 msg := make([]byte, 0) 128 err := stream.RecvMsg(&msg) 129 if err != nil { 130 return err 131 } 132 133 md, _ := metadata.FromIncomingContext(stream.Context()) 134 135 for _, kv := range c.Headers { 136 if len(strings.Split(kv, ":"))%2 == 1 { 137 return fmt.Errorf("additional headers key/values must be separated by a colon(:): %s", kv) 138 } 139 md.Append(strings.Split(kv, ":")[0], strings.Split(kv, ":")[1]) 140 } 141 142 resp, err := c.executeRequest(fullMethodName, msg, md) 143 if err != nil { 144 return err 145 } 146 147 go func() { 148 <-stream.Context().Done() 149 argoio.Close(resp.Body) 150 }() 151 defer argoio.Close(resp.Body) 152 c.httpClient.CloseIdleConnections() 153 154 for { 155 header := make([]byte, frameHeaderLength) 156 if _, err := io.ReadAtLeast(resp.Body, header, frameHeaderLength); err != nil { 157 if err == io.EOF { 158 err = io.ErrUnexpectedEOF 159 } 160 return err 161 } 162 163 if header[0] == endOfStreamFlag { 164 return nil 165 } 166 length := int(binary.BigEndian.Uint32(header[1:frameHeaderLength])) 167 data := make([]byte, length) 168 169 if read, err := io.ReadAtLeast(resp.Body, data, length); err != nil { 170 if err != io.EOF { 171 return err 172 } else if read < length { 173 return io.ErrUnexpectedEOF 174 } else { 175 return nil 176 } 177 } 178 179 if err := stream.SendMsg(data); err != nil { 180 return err 181 } 182 183 } 184 })) 185 go func() { 186 err := proxySrv.Serve(ln) 187 argocderrors.CheckError(err) 188 }() 189 return proxySrv, ln, nil 190 } 191 192 // useGRPCProxy ensures that grpc proxy server is started and return closer which stops server when no one uses it 193 func (c *client) useGRPCProxy() (net.Addr, io.Closer, error) { 194 c.proxyMutex.Lock() 195 defer c.proxyMutex.Unlock() 196 197 if c.proxyListener == nil { 198 var err error 199 c.proxyServer, c.proxyListener, err = c.startGRPCProxy() 200 if err != nil { 201 return nil, nil, err 202 } 203 } 204 c.proxyUsersCount = c.proxyUsersCount + 1 205 206 return c.proxyListener.Addr(), argoio.NewCloser(func() error { 207 c.proxyMutex.Lock() 208 defer c.proxyMutex.Unlock() 209 c.proxyUsersCount = c.proxyUsersCount - 1 210 if c.proxyUsersCount == 0 { 211 c.proxyServer.Stop() 212 c.proxyListener = nil 213 c.proxyServer = nil 214 return nil 215 } 216 return nil 217 }), nil 218 }