Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame] | 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 | "bitbucket.org/ww/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"), AcceptedMediaTypesForEndpoint(ns), 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.Serializer |
| 53 | if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { |
| 54 | info.Serializer = info.PrettySerializer |
| 55 | } |
| 56 | return mediaType, info, nil |
| 57 | } |
| 58 | |
| 59 | // NegotiateOutputSerializer returns a serializer for the output. |
| 60 | func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { |
| 61 | _, info, err := NegotiateOutputMediaType(req, ns, DefaultEndpointRestrictions) |
| 62 | return info, err |
| 63 | } |
| 64 | |
| 65 | // NegotiateOutputStreamSerializer returns a stream serializer for the given request. |
| 66 | func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { |
| 67 | mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions) |
| 68 | if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil { |
| 69 | _, supported := MediaTypesForSerializer(ns) |
| 70 | return runtime.SerializerInfo{}, NewNotAcceptableError(supported) |
| 71 | } |
| 72 | return mediaType.Accepted.Serializer, nil |
| 73 | } |
| 74 | |
| 75 | // NegotiateInputSerializer returns the input serializer for the provided request. |
| 76 | func NegotiateInputSerializer(req *http.Request, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { |
| 77 | mediaType := req.Header.Get("Content-Type") |
| 78 | return NegotiateInputSerializerForMediaType(mediaType, streaming, ns) |
| 79 | } |
| 80 | |
| 81 | // NegotiateInputSerializerForMediaType returns the appropriate serializer for the given media type or an error. |
| 82 | func NegotiateInputSerializerForMediaType(mediaType string, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { |
| 83 | mediaTypes := ns.SupportedMediaTypes() |
| 84 | if len(mediaType) == 0 { |
| 85 | mediaType = mediaTypes[0].MediaType |
| 86 | } |
| 87 | if mediaType, _, err := mime.ParseMediaType(mediaType); err == nil { |
| 88 | for _, info := range mediaTypes { |
| 89 | if info.MediaType != mediaType { |
| 90 | continue |
| 91 | } |
| 92 | return info, nil |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | supported, streamingSupported := MediaTypesForSerializer(ns) |
| 97 | if streaming { |
| 98 | return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(streamingSupported) |
| 99 | } |
| 100 | return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported) |
| 101 | } |
| 102 | |
| 103 | // isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent |
| 104 | // matches known "human" clients. |
| 105 | func isPrettyPrint(req *http.Request) bool { |
| 106 | // DEPRECATED: should be part of the content type |
| 107 | if req.URL != nil { |
| 108 | pp := req.URL.Query().Get("pretty") |
| 109 | if len(pp) > 0 { |
| 110 | pretty, _ := strconv.ParseBool(pp) |
| 111 | return pretty |
| 112 | } |
| 113 | } |
| 114 | userAgent := req.UserAgent() |
| 115 | // This covers basic all browsers and cli http tools |
| 116 | if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") { |
| 117 | return true |
| 118 | } |
| 119 | return false |
| 120 | } |
| 121 | |
| 122 | // EndpointRestrictions is an interface that allows content-type negotiation |
| 123 | // to verify server support for specific options |
| 124 | type EndpointRestrictions interface { |
| 125 | // AllowsConversion should return true if the specified group version kind |
| 126 | // is an allowed target object. |
| 127 | AllowsConversion(schema.GroupVersionKind) bool |
| 128 | // AllowsServerVersion should return true if the specified version is valid |
| 129 | // for the server group. |
| 130 | AllowsServerVersion(version string) bool |
| 131 | // AllowsStreamSchema should return true if the specified stream schema is |
| 132 | // valid for the server group. |
| 133 | AllowsStreamSchema(schema string) bool |
| 134 | } |
| 135 | |
| 136 | var DefaultEndpointRestrictions = emptyEndpointRestrictions{} |
| 137 | |
| 138 | type emptyEndpointRestrictions struct{} |
| 139 | |
| 140 | func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool { return false } |
| 141 | func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } |
| 142 | func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } |
| 143 | |
| 144 | // AcceptedMediaType contains information about a valid media type that the |
| 145 | // server can serialize. |
| 146 | type AcceptedMediaType struct { |
| 147 | // Type is the first part of the media type ("application") |
| 148 | Type string |
| 149 | // SubType is the second part of the media type ("json") |
| 150 | SubType string |
| 151 | // Serializer is the serialization info this object accepts |
| 152 | Serializer runtime.SerializerInfo |
| 153 | } |
| 154 | |
| 155 | // MediaTypeOptions describes information for a given media type that may alter |
| 156 | // the server response |
| 157 | type MediaTypeOptions struct { |
| 158 | // pretty is true if the requested representation should be formatted for human |
| 159 | // viewing |
| 160 | Pretty bool |
| 161 | |
| 162 | // stream, if set, indicates that a streaming protocol variant of this encoding |
| 163 | // is desired. The only currently supported value is watch which returns versioned |
| 164 | // events. In the future, this may refer to other stream protocols. |
| 165 | Stream string |
| 166 | |
| 167 | // convert is a request to alter the type of object returned by the server from the |
| 168 | // normal response |
| 169 | Convert *schema.GroupVersionKind |
| 170 | // useServerVersion is an optional version for the server group |
| 171 | UseServerVersion string |
| 172 | |
| 173 | // export is true if the representation requested should exclude fields the server |
| 174 | // has set |
| 175 | Export bool |
| 176 | |
| 177 | // unrecognized is a list of all unrecognized keys |
| 178 | Unrecognized []string |
| 179 | |
| 180 | // the accepted media type from the client |
| 181 | Accepted *AcceptedMediaType |
| 182 | } |
| 183 | |
| 184 | // acceptMediaTypeOptions returns an options object that matches the provided media type params. If |
| 185 | // it returns false, the provided options are not allowed and the media type must be skipped. These |
| 186 | // parameters are unversioned and may not be changed. |
| 187 | func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { |
| 188 | var options MediaTypeOptions |
| 189 | |
| 190 | // extract all known parameters |
| 191 | for k, v := range params { |
| 192 | switch k { |
| 193 | |
| 194 | // controls transformation of the object when returned |
| 195 | case "as": |
| 196 | if options.Convert == nil { |
| 197 | options.Convert = &schema.GroupVersionKind{} |
| 198 | } |
| 199 | options.Convert.Kind = v |
| 200 | case "g": |
| 201 | if options.Convert == nil { |
| 202 | options.Convert = &schema.GroupVersionKind{} |
| 203 | } |
| 204 | options.Convert.Group = v |
| 205 | case "v": |
| 206 | if options.Convert == nil { |
| 207 | options.Convert = &schema.GroupVersionKind{} |
| 208 | } |
| 209 | options.Convert.Version = v |
| 210 | |
| 211 | // controls the streaming schema |
| 212 | case "stream": |
| 213 | if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) { |
| 214 | return MediaTypeOptions{}, false |
| 215 | } |
| 216 | options.Stream = v |
| 217 | |
| 218 | // controls the version of the server API group used |
| 219 | // for generic output |
| 220 | case "sv": |
| 221 | if len(v) > 0 && !endpoint.AllowsServerVersion(v) { |
| 222 | return MediaTypeOptions{}, false |
| 223 | } |
| 224 | options.UseServerVersion = v |
| 225 | |
| 226 | // if specified, the server should transform the returned |
| 227 | // output and remove fields that are always server specified, |
| 228 | // or which fit the default behavior. |
| 229 | case "export": |
| 230 | options.Export = v == "1" |
| 231 | |
| 232 | // if specified, the pretty serializer will be used |
| 233 | case "pretty": |
| 234 | options.Pretty = v == "1" |
| 235 | |
| 236 | default: |
| 237 | options.Unrecognized = append(options.Unrecognized, k) |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) { |
| 242 | return MediaTypeOptions{}, false |
| 243 | } |
| 244 | |
| 245 | options.Accepted = accepts |
| 246 | return options, true |
| 247 | } |
| 248 | |
| 249 | type candidateMediaType struct { |
| 250 | accepted *AcceptedMediaType |
| 251 | clauses goautoneg.Accept |
| 252 | } |
| 253 | |
| 254 | type candidateMediaTypeSlice []candidateMediaType |
| 255 | |
| 256 | // NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and |
| 257 | // a list of alternatives along with the accepted media type parameters. |
| 258 | func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { |
| 259 | if len(header) == 0 && len(accepted) > 0 { |
| 260 | return MediaTypeOptions{ |
| 261 | Accepted: &accepted[0], |
| 262 | }, true |
| 263 | } |
| 264 | |
| 265 | var candidates candidateMediaTypeSlice |
| 266 | clauses := goautoneg.ParseAccept(header) |
| 267 | for _, clause := range clauses { |
| 268 | for i := range accepted { |
| 269 | accepts := &accepted[i] |
| 270 | switch { |
| 271 | case clause.Type == accepts.Type && clause.SubType == accepts.SubType, |
| 272 | clause.Type == accepts.Type && clause.SubType == "*", |
| 273 | clause.Type == "*" && clause.SubType == "*": |
| 274 | candidates = append(candidates, candidateMediaType{accepted: accepts, clauses: clause}) |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | for _, v := range candidates { |
| 280 | if retVal, ret := acceptMediaTypeOptions(v.clauses.Params, v.accepted, endpoint); ret { |
| 281 | return retVal, true |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | return MediaTypeOptions{}, false |
| 286 | } |
| 287 | |
| 288 | // AcceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which |
| 289 | // allowed media types the server exposes. |
| 290 | func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType { |
| 291 | var acceptedMediaTypes []AcceptedMediaType |
| 292 | for _, info := range ns.SupportedMediaTypes() { |
| 293 | segments := strings.SplitN(info.MediaType, "/", 2) |
| 294 | if len(segments) == 1 { |
| 295 | segments = append(segments, "*") |
| 296 | } |
| 297 | t := AcceptedMediaType{ |
| 298 | Type: segments[0], |
| 299 | SubType: segments[1], |
| 300 | Serializer: info, |
| 301 | } |
| 302 | acceptedMediaTypes = append(acceptedMediaTypes, t) |
| 303 | } |
| 304 | return acceptedMediaTypes |
| 305 | } |