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  }