github.com/grafana/pyroscope@v1.18.0/pkg/featureflags/client_capability.go (about)

     1  package featureflags
     2  
     3  import (
     4  	"context"
     5  	"mime"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"connectrpc.com/connect"
    10  	"github.com/go-kit/log/level"
    11  	"github.com/grafana/dskit/middleware"
    12  	"github.com/grafana/pyroscope/pkg/util"
    13  	"google.golang.org/grpc"
    14  	"google.golang.org/grpc/metadata"
    15  )
    16  
    17  const (
    18  	// Capability names - update parseClientCapabilities below when new capabilities added
    19  	allowUtf8LabelNamesCapabilityName string = "allow-utf8-labelnames"
    20  )
    21  
    22  // Define a custom context key type to avoid collisions
    23  type contextKey struct{}
    24  
    25  type ClientCapabilities struct {
    26  	AllowUtf8LabelNames bool
    27  }
    28  
    29  func WithClientCapabilities(ctx context.Context, clientCapabilities ClientCapabilities) context.Context {
    30  	return context.WithValue(ctx, contextKey{}, clientCapabilities)
    31  }
    32  
    33  func GetClientCapabilities(ctx context.Context) (ClientCapabilities, bool) {
    34  	value, ok := ctx.Value(contextKey{}).(ClientCapabilities)
    35  	return value, ok
    36  }
    37  
    38  func ClientCapabilitiesGRPCMiddleware() grpc.UnaryServerInterceptor {
    39  	return func(
    40  		ctx context.Context,
    41  		req interface{},
    42  		info *grpc.UnaryServerInfo,
    43  		handler grpc.UnaryHandler,
    44  	) (interface{}, error) {
    45  		// Extract metadata from context
    46  		md, ok := metadata.FromIncomingContext(ctx)
    47  		if !ok {
    48  			return handler(ctx, req)
    49  		}
    50  
    51  		// Convert metadata to http.Header for reuse of existing parsing logic
    52  		httpHeader := make(http.Header)
    53  		for key, values := range md {
    54  			// gRPC metadata keys are lowercase, HTTP headers are case-insensitive
    55  			httpHeader[http.CanonicalHeaderKey(key)] = values
    56  		}
    57  
    58  		// Reuse existing HTTP header parsing
    59  		clientCapabilities, err := parseClientCapabilities(httpHeader)
    60  		if err != nil {
    61  			return nil, connect.NewError(connect.CodeInvalidArgument, err)
    62  		}
    63  
    64  		enhancedCtx := WithClientCapabilities(ctx, clientCapabilities)
    65  		return handler(enhancedCtx, req)
    66  	}
    67  }
    68  
    69  // ClientCapabilitiesHttpMiddleware creates middleware that extracts and parses the
    70  // `Accept` header for capabilities the client supports
    71  func ClientCapabilitiesHttpMiddleware() middleware.Interface {
    72  	return middleware.Func(func(next http.Handler) http.Handler {
    73  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    74  			clientCapabilities, err := parseClientCapabilities(r.Header)
    75  			if err != nil {
    76  				http.Error(w, "Invalid header format: "+err.Error(), http.StatusBadRequest)
    77  				return
    78  			}
    79  
    80  			ctx := WithClientCapabilities(r.Context(), clientCapabilities)
    81  			next.ServeHTTP(w, r.WithContext(ctx))
    82  		})
    83  	})
    84  }
    85  
    86  func parseClientCapabilities(header http.Header) (ClientCapabilities, error) {
    87  	acceptHeaderValues := header.Values("Accept")
    88  
    89  	var capabilities ClientCapabilities
    90  
    91  	for _, acceptHeaderValue := range acceptHeaderValues {
    92  		if acceptHeaderValue != "" {
    93  			accepts := strings.Split(acceptHeaderValue, ",")
    94  
    95  			for _, accept := range accepts {
    96  				if _, params, err := mime.ParseMediaType(accept); err != nil {
    97  					return capabilities, err
    98  				} else {
    99  					for k, v := range params {
   100  						switch k {
   101  						case allowUtf8LabelNamesCapabilityName:
   102  							if v == "true" {
   103  								capabilities.AllowUtf8LabelNames = true
   104  							}
   105  						default:
   106  							level.Debug(util.Logger).Log(
   107  								"msg", "unknown capability parsed from Accept header",
   108  								"acceptHeaderKey", k,
   109  								"acceptHeaderValue", v)
   110  						}
   111  					}
   112  				}
   113  			}
   114  		}
   115  	}
   116  	return capabilities, nil
   117  }