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 }