blob: f9bb47babca96e90f624e822aa9ee91047242821 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package negotiation
18
19import (
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.
32func 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.
45func 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.
60func 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.
66func 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.
76func 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.
82func 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.
105func 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
124type 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
136var DefaultEndpointRestrictions = emptyEndpointRestrictions{}
137
138type emptyEndpointRestrictions struct{}
139
140func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool { return false }
141func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false }
142func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
143
144// AcceptedMediaType contains information about a valid media type that the
145// server can serialize.
146type 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
157type 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.
187func 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
249type candidateMediaType struct {
250 accepted *AcceptedMediaType
251 clauses goautoneg.Accept
252}
253
254type 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.
258func 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.
290func 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}