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 }