github.com/argoproj/argo-cd@v1.8.7/util/grpc/useragent.go (about)

     1  package grpc
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/Masterminds/semver"
     7  	"golang.org/x/net/context"
     8  	"google.golang.org/grpc"
     9  	"google.golang.org/grpc/codes"
    10  	"google.golang.org/grpc/metadata"
    11  	"google.golang.org/grpc/status"
    12  )
    13  
    14  // UserAgentUnaryServerInterceptor returns a UnaryServerInterceptor which enforces a minimum client
    15  // version in the user agent
    16  func UserAgentUnaryServerInterceptor(clientName, constraintStr string) grpc.UnaryServerInterceptor {
    17  	userAgentEnforcer := newUserAgentEnforcer(clientName, constraintStr)
    18  	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    19  		if err := userAgentEnforcer(ctx); err != nil {
    20  			return nil, err
    21  		}
    22  		return handler(ctx, req)
    23  	}
    24  }
    25  
    26  // UserAgentStreamServerInterceptor returns a StreamServerInterceptor which enforces a minimum client
    27  // version in the user agent
    28  func UserAgentStreamServerInterceptor(clientName, constraintStr string) grpc.StreamServerInterceptor {
    29  	userAgentEnforcer := newUserAgentEnforcer(clientName, constraintStr)
    30  	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    31  		if err := userAgentEnforcer(stream.Context()); err != nil {
    32  			return err
    33  		}
    34  		return handler(srv, stream)
    35  	}
    36  }
    37  
    38  func newUserAgentEnforcer(clientName, constraintStr string) func(context.Context) error {
    39  	semVerConstraint, err := semver.NewConstraint(constraintStr)
    40  	if err != nil {
    41  		panic(err)
    42  	}
    43  	return func(ctx context.Context) error {
    44  		var userAgents []string
    45  		if md, ok := metadata.FromIncomingContext(ctx); ok {
    46  			for _, ua := range md["user-agent"] {
    47  				// ua is a string like "argocd-client/v0.11.0+cde040e grpc-go/1.15.0"
    48  				userAgents = append(userAgents, strings.Fields(ua)...)
    49  				break
    50  			}
    51  		}
    52  		if isLegacyClient(userAgents) {
    53  			return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", constraintStr)
    54  		}
    55  
    56  		for _, userAgent := range userAgents {
    57  			uaSplit := strings.Split(userAgent, "/")
    58  			if len(uaSplit) != 2 || uaSplit[0] != clientName {
    59  				// User-agent was supplied, but client/format is not one we care about (e.g. grpc-go)
    60  				continue
    61  			}
    62  			// remove pre-release part
    63  			versionStr := strings.Split(uaSplit[1], "-")[0]
    64  			// We have matched the client name to the one we care about
    65  			uaVers, err := semver.NewVersion(versionStr)
    66  			if err != nil {
    67  				return status.Errorf(codes.InvalidArgument, "could not parse version from user-agent: %s", userAgent)
    68  			}
    69  			if ok, errs := semVerConstraint.Validate(uaVers); !ok {
    70  				return status.Errorf(codes.FailedPrecondition, "unsatisfied client version constraint: %s", errs[0].Error())
    71  			}
    72  			return nil
    73  		}
    74  		// If we get here, the caller either did not supply user-agent, supplied one which we don't
    75  		// care about. This implies it is a from a custom generated client, so we permit the request.
    76  		// We really only want to enforce user-agent version constraints for clients under our
    77  		// control which we know to have compatibility issues
    78  		return nil
    79  	}
    80  }
    81  
    82  // isLegacyClient checks if the request was made from a legacy Argo CD client (i.e. v0.10 CLI).
    83  // The heuristic is that a single default 'grpc-go' user-agent was specified with one of the
    84  // previous versions of grpc-go we used in the past (1.15.0, 1.10.0).
    85  // Starting in v0.11, both of the gRPC clients we maintain (pkg/apiclient and grpc-gateway) started
    86  // supplying a explicit user-agent tied to the Argo CD version.
    87  func isLegacyClient(userAgents []string) bool {
    88  	return len(userAgents) == 1 && (userAgents[0] == "grpc-go/1.15.0" || userAgents[0] == "grpc-go/1.10.0")
    89  }