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