k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/negotiation/negotiate.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package negotiation
    18  
    19  import (
    20  	"mime"
    21  	"net/http"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/munnerz/goautoneg"
    26  
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  )
    30  
    31  // MediaTypesForSerializer returns a list of media and stream media types for the server.
    32  func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, streamMediaTypes []string) {
    33  	for _, info := range ns.SupportedMediaTypes() {
    34  		mediaTypes = append(mediaTypes, info.MediaType)
    35  		if info.StreamSerializer != nil {
    36  			// stream=watch is the existing mime-type parameter for watch
    37  			streamMediaTypes = append(streamMediaTypes, info.MediaType+";stream=watch")
    38  		}
    39  	}
    40  	return mediaTypes, streamMediaTypes
    41  }
    42  
    43  // NegotiateOutputMediaType negotiates the output structured media type and a serializer, or
    44  // returns an error.
    45  func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) {
    46  	mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), ns.SupportedMediaTypes(), restrictions)
    47  	if !ok {
    48  		supported, _ := MediaTypesForSerializer(ns)
    49  		return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported)
    50  	}
    51  	// TODO: move into resthandler
    52  	info := mediaType.Accepted
    53  	if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil {
    54  		info.Serializer = info.PrettySerializer
    55  	}
    56  	return mediaType, info, nil
    57  }
    58  
    59  // NegotiateOutputMediaTypeStream returns a stream serializer for the given request.
    60  func NegotiateOutputMediaTypeStream(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (runtime.SerializerInfo, error) {
    61  	mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), ns.SupportedMediaTypes(), restrictions)
    62  	if !ok || mediaType.Accepted.StreamSerializer == nil {
    63  		_, supported := MediaTypesForSerializer(ns)
    64  		return runtime.SerializerInfo{}, NewNotAcceptableError(supported)
    65  	}
    66  	return mediaType.Accepted, nil
    67  }
    68  
    69  // NegotiateInputSerializer returns the input serializer for the provided request.
    70  func NegotiateInputSerializer(req *http.Request, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
    71  	mediaType := req.Header.Get("Content-Type")
    72  	return NegotiateInputSerializerForMediaType(mediaType, streaming, ns)
    73  }
    74  
    75  // NegotiateInputSerializerForMediaType returns the appropriate serializer for the given media type or an error.
    76  func NegotiateInputSerializerForMediaType(mediaType string, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
    77  	mediaTypes := ns.SupportedMediaTypes()
    78  	if len(mediaType) == 0 {
    79  		mediaType = mediaTypes[0].MediaType
    80  	}
    81  	if mediaType, _, err := mime.ParseMediaType(mediaType); err == nil {
    82  		if info, ok := runtime.SerializerInfoForMediaType(mediaTypes, mediaType); ok {
    83  			return info, nil
    84  		}
    85  	}
    86  
    87  	supported, streamingSupported := MediaTypesForSerializer(ns)
    88  	if streaming {
    89  		return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(streamingSupported)
    90  	}
    91  	return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported)
    92  }
    93  
    94  // isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent
    95  // matches known "human" clients.
    96  func isPrettyPrint(req *http.Request) bool {
    97  	// DEPRECATED: should be part of the content type
    98  	if req.URL != nil {
    99  		// avoid an allocation caused by parsing the URL query
   100  		if strings.Contains(req.URL.RawQuery, "pretty") {
   101  			pp := req.URL.Query().Get("pretty")
   102  			if len(pp) > 0 {
   103  				pretty, _ := strconv.ParseBool(pp)
   104  				return pretty
   105  			}
   106  		}
   107  	}
   108  	userAgent := req.UserAgent()
   109  	// This covers basic all browsers and cli http tools
   110  	if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") {
   111  		return true
   112  	}
   113  	return false
   114  }
   115  
   116  // EndpointRestrictions is an interface that allows content-type negotiation
   117  // to verify server support for specific options
   118  type EndpointRestrictions interface {
   119  	// AllowsMediaTypeTransform returns true if the endpoint allows either the requested mime type
   120  	// or the requested transformation. If false, the caller should ignore this mime type. If the
   121  	// target is nil, the client is not requesting a transformation.
   122  	AllowsMediaTypeTransform(mimeType, mimeSubType string, target *schema.GroupVersionKind) bool
   123  	// AllowsServerVersion should return true if the specified version is valid
   124  	// for the server group.
   125  	AllowsServerVersion(version string) bool
   126  	// AllowsStreamSchema should return true if the specified stream schema is
   127  	// valid for the server group.
   128  	AllowsStreamSchema(schema string) bool
   129  }
   130  
   131  // DefaultEndpointRestrictions is the default EndpointRestrictions which allows
   132  // content-type negotiation to verify server support for specific options
   133  var DefaultEndpointRestrictions = emptyEndpointRestrictions{}
   134  
   135  type emptyEndpointRestrictions struct{}
   136  
   137  func (emptyEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
   138  	return gvk == nil
   139  }
   140  func (emptyEndpointRestrictions) AllowsServerVersion(string) bool  { return false }
   141  func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
   142  
   143  // MediaTypeOptions describes information for a given media type that may alter
   144  // the server response
   145  type MediaTypeOptions struct {
   146  	// pretty is true if the requested representation should be formatted for human
   147  	// viewing
   148  	Pretty bool
   149  
   150  	// stream, if set, indicates that a streaming protocol variant of this encoding
   151  	// is desired. The only currently supported value is watch which returns versioned
   152  	// events. In the future, this may refer to other stream protocols.
   153  	Stream string
   154  
   155  	// convert is a request to alter the type of object returned by the server from the
   156  	// normal response
   157  	Convert *schema.GroupVersionKind
   158  	// useServerVersion is an optional version for the server group
   159  	UseServerVersion string
   160  
   161  	// export is true if the representation requested should exclude fields the server
   162  	// has set
   163  	Export bool
   164  
   165  	// unrecognized is a list of all unrecognized keys
   166  	Unrecognized []string
   167  
   168  	// the accepted media type from the client
   169  	Accepted runtime.SerializerInfo
   170  }
   171  
   172  // acceptMediaTypeOptions returns an options object that matches the provided media type params. If
   173  // it returns false, the provided options are not allowed and the media type must be skipped.  These
   174  // parameters are unversioned and may not be changed.
   175  func acceptMediaTypeOptions(params map[string]string, accepts *runtime.SerializerInfo, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
   176  	var options MediaTypeOptions
   177  
   178  	// extract all known parameters
   179  	for k, v := range params {
   180  		switch k {
   181  
   182  		// controls transformation of the object when returned
   183  		case "as":
   184  			if options.Convert == nil {
   185  				options.Convert = &schema.GroupVersionKind{}
   186  			}
   187  			options.Convert.Kind = v
   188  		case "g":
   189  			if options.Convert == nil {
   190  				options.Convert = &schema.GroupVersionKind{}
   191  			}
   192  			options.Convert.Group = v
   193  		case "v":
   194  			if options.Convert == nil {
   195  				options.Convert = &schema.GroupVersionKind{}
   196  			}
   197  			options.Convert.Version = v
   198  
   199  		// controls the streaming schema
   200  		case "stream":
   201  			if len(v) > 0 && (accepts.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) {
   202  				return MediaTypeOptions{}, false
   203  			}
   204  			options.Stream = v
   205  
   206  		// controls the version of the server API group used
   207  		// for generic output
   208  		case "sv":
   209  			if len(v) > 0 && !endpoint.AllowsServerVersion(v) {
   210  				return MediaTypeOptions{}, false
   211  			}
   212  			options.UseServerVersion = v
   213  
   214  		// if specified, the server should transform the returned
   215  		// output and remove fields that are always server specified,
   216  		// or which fit the default behavior.
   217  		case "export":
   218  			options.Export = v == "1"
   219  
   220  		// if specified, the pretty serializer will be used
   221  		case "pretty":
   222  			options.Pretty = v == "1"
   223  
   224  		default:
   225  			options.Unrecognized = append(options.Unrecognized, k)
   226  		}
   227  	}
   228  
   229  	if !endpoint.AllowsMediaTypeTransform(accepts.MediaTypeType, accepts.MediaTypeSubType, options.Convert) {
   230  		return MediaTypeOptions{}, false
   231  	}
   232  
   233  	options.Accepted = *accepts
   234  	return options, true
   235  }
   236  
   237  // NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and
   238  // a list of alternatives along with the accepted media type parameters.
   239  func NegotiateMediaTypeOptions(header string, accepted []runtime.SerializerInfo, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
   240  	if len(header) == 0 && len(accepted) > 0 {
   241  		return MediaTypeOptions{
   242  			Accepted: accepted[0],
   243  		}, true
   244  	}
   245  
   246  	clauses := goautoneg.ParseAccept(header)
   247  	for i := range clauses {
   248  		clause := &clauses[i]
   249  		for i := range accepted {
   250  			accepts := &accepted[i]
   251  			switch {
   252  			case clause.Type == accepts.MediaTypeType && clause.SubType == accepts.MediaTypeSubType,
   253  				clause.Type == accepts.MediaTypeType && clause.SubType == "*",
   254  				clause.Type == "*" && clause.SubType == "*":
   255  				if retVal, ret := acceptMediaTypeOptions(clause.Params, accepts, endpoint); ret {
   256  					return retVal, true
   257  				}
   258  			}
   259  		}
   260  	}
   261  
   262  	return MediaTypeOptions{}, false
   263  }