blob: f9bb47babca96e90f624e822aa9ee91047242821 [file] [log] [blame]
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package negotiation
import (
"mime"
"net/http"
"strconv"
"strings"
"bitbucket.org/ww/goautoneg"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// MediaTypesForSerializer returns a list of media and stream media types for the server.
func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, streamMediaTypes []string) {
for _, info := range ns.SupportedMediaTypes() {
mediaTypes = append(mediaTypes, info.MediaType)
if info.StreamSerializer != nil {
// stream=watch is the existing mime-type parameter for watch
streamMediaTypes = append(streamMediaTypes, info.MediaType+";stream=watch")
}
}
return mediaTypes, streamMediaTypes
}
// NegotiateOutputMediaType negotiates the output structured media type and a serializer, or
// returns an error.
func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) {
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions)
if !ok {
supported, _ := MediaTypesForSerializer(ns)
return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported)
}
// TODO: move into resthandler
info := mediaType.Accepted.Serializer
if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil {
info.Serializer = info.PrettySerializer
}
return mediaType, info, nil
}
// NegotiateOutputSerializer returns a serializer for the output.
func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
_, info, err := NegotiateOutputMediaType(req, ns, DefaultEndpointRestrictions)
return info, err
}
// NegotiateOutputStreamSerializer returns a stream serializer for the given request.
func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions)
if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil {
_, supported := MediaTypesForSerializer(ns)
return runtime.SerializerInfo{}, NewNotAcceptableError(supported)
}
return mediaType.Accepted.Serializer, nil
}
// NegotiateInputSerializer returns the input serializer for the provided request.
func NegotiateInputSerializer(req *http.Request, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
mediaType := req.Header.Get("Content-Type")
return NegotiateInputSerializerForMediaType(mediaType, streaming, ns)
}
// NegotiateInputSerializerForMediaType returns the appropriate serializer for the given media type or an error.
func NegotiateInputSerializerForMediaType(mediaType string, streaming bool, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
mediaTypes := ns.SupportedMediaTypes()
if len(mediaType) == 0 {
mediaType = mediaTypes[0].MediaType
}
if mediaType, _, err := mime.ParseMediaType(mediaType); err == nil {
for _, info := range mediaTypes {
if info.MediaType != mediaType {
continue
}
return info, nil
}
}
supported, streamingSupported := MediaTypesForSerializer(ns)
if streaming {
return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(streamingSupported)
}
return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported)
}
// isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent
// matches known "human" clients.
func isPrettyPrint(req *http.Request) bool {
// DEPRECATED: should be part of the content type
if req.URL != nil {
pp := req.URL.Query().Get("pretty")
if len(pp) > 0 {
pretty, _ := strconv.ParseBool(pp)
return pretty
}
}
userAgent := req.UserAgent()
// This covers basic all browsers and cli http tools
if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") {
return true
}
return false
}
// EndpointRestrictions is an interface that allows content-type negotiation
// to verify server support for specific options
type EndpointRestrictions interface {
// AllowsConversion should return true if the specified group version kind
// is an allowed target object.
AllowsConversion(schema.GroupVersionKind) bool
// AllowsServerVersion should return true if the specified version is valid
// for the server group.
AllowsServerVersion(version string) bool
// AllowsStreamSchema should return true if the specified stream schema is
// valid for the server group.
AllowsStreamSchema(schema string) bool
}
var DefaultEndpointRestrictions = emptyEndpointRestrictions{}
type emptyEndpointRestrictions struct{}
func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool { return false }
func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false }
func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
// AcceptedMediaType contains information about a valid media type that the
// server can serialize.
type AcceptedMediaType struct {
// Type is the first part of the media type ("application")
Type string
// SubType is the second part of the media type ("json")
SubType string
// Serializer is the serialization info this object accepts
Serializer runtime.SerializerInfo
}
// MediaTypeOptions describes information for a given media type that may alter
// the server response
type MediaTypeOptions struct {
// pretty is true if the requested representation should be formatted for human
// viewing
Pretty bool
// stream, if set, indicates that a streaming protocol variant of this encoding
// is desired. The only currently supported value is watch which returns versioned
// events. In the future, this may refer to other stream protocols.
Stream string
// convert is a request to alter the type of object returned by the server from the
// normal response
Convert *schema.GroupVersionKind
// useServerVersion is an optional version for the server group
UseServerVersion string
// export is true if the representation requested should exclude fields the server
// has set
Export bool
// unrecognized is a list of all unrecognized keys
Unrecognized []string
// the accepted media type from the client
Accepted *AcceptedMediaType
}
// acceptMediaTypeOptions returns an options object that matches the provided media type params. If
// it returns false, the provided options are not allowed and the media type must be skipped. These
// parameters are unversioned and may not be changed.
func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
var options MediaTypeOptions
// extract all known parameters
for k, v := range params {
switch k {
// controls transformation of the object when returned
case "as":
if options.Convert == nil {
options.Convert = &schema.GroupVersionKind{}
}
options.Convert.Kind = v
case "g":
if options.Convert == nil {
options.Convert = &schema.GroupVersionKind{}
}
options.Convert.Group = v
case "v":
if options.Convert == nil {
options.Convert = &schema.GroupVersionKind{}
}
options.Convert.Version = v
// controls the streaming schema
case "stream":
if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) {
return MediaTypeOptions{}, false
}
options.Stream = v
// controls the version of the server API group used
// for generic output
case "sv":
if len(v) > 0 && !endpoint.AllowsServerVersion(v) {
return MediaTypeOptions{}, false
}
options.UseServerVersion = v
// if specified, the server should transform the returned
// output and remove fields that are always server specified,
// or which fit the default behavior.
case "export":
options.Export = v == "1"
// if specified, the pretty serializer will be used
case "pretty":
options.Pretty = v == "1"
default:
options.Unrecognized = append(options.Unrecognized, k)
}
}
if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) {
return MediaTypeOptions{}, false
}
options.Accepted = accepts
return options, true
}
type candidateMediaType struct {
accepted *AcceptedMediaType
clauses goautoneg.Accept
}
type candidateMediaTypeSlice []candidateMediaType
// NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and
// a list of alternatives along with the accepted media type parameters.
func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
if len(header) == 0 && len(accepted) > 0 {
return MediaTypeOptions{
Accepted: &accepted[0],
}, true
}
var candidates candidateMediaTypeSlice
clauses := goautoneg.ParseAccept(header)
for _, clause := range clauses {
for i := range accepted {
accepts := &accepted[i]
switch {
case clause.Type == accepts.Type && clause.SubType == accepts.SubType,
clause.Type == accepts.Type && clause.SubType == "*",
clause.Type == "*" && clause.SubType == "*":
candidates = append(candidates, candidateMediaType{accepted: accepts, clauses: clause})
}
}
}
for _, v := range candidates {
if retVal, ret := acceptMediaTypeOptions(v.clauses.Params, v.accepted, endpoint); ret {
return retVal, true
}
}
return MediaTypeOptions{}, false
}
// AcceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which
// allowed media types the server exposes.
func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType {
var acceptedMediaTypes []AcceptedMediaType
for _, info := range ns.SupportedMediaTypes() {
segments := strings.SplitN(info.MediaType, "/", 2)
if len(segments) == 1 {
segments = append(segments, "*")
}
t := AcceptedMediaType{
Type: segments[0],
SubType: segments[1],
Serializer: info,
}
acceptedMediaTypes = append(acceptedMediaTypes, t)
}
return acceptedMediaTypes
}