...

Source file src/sigs.k8s.io/gateway-api/apis/v1/validation/httproute.go

Documentation: sigs.k8s.io/gateway-api/apis/v1/validation

     1  /*
     2  Copyright 2021 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 validation
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"regexp"
    23  	"strings"
    24  	"time"
    25  
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  
    28  	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
    29  )
    30  
    31  var (
    32  	// repeatableHTTPRouteFilters are filter types that are allowed to be
    33  	// repeated multiple times in a rule.
    34  	repeatableHTTPRouteFilters = []gatewayv1.HTTPRouteFilterType{
    35  		gatewayv1.HTTPRouteFilterExtensionRef,
    36  		gatewayv1.HTTPRouteFilterRequestMirror,
    37  	}
    38  
    39  	// Invalid path sequences and suffixes, primarily related to directory traversal
    40  	invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F", "#"}
    41  	invalidPathSuffixes  = []string{"/..", "/."}
    42  
    43  	// All valid path characters per RFC-3986
    44  	validPathCharacters = "^(?:[A-Za-z0-9\\/\\-._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$"
    45  )
    46  
    47  // ValidateHTTPRoute validates HTTPRoute according to the Gateway API specification.
    48  // For additional details of the HTTPRoute spec, refer to:
    49  // https://gateway-api.sigs.k8s.io/v1beta1/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute
    50  func ValidateHTTPRoute(route *gatewayv1.HTTPRoute) field.ErrorList {
    51  	return ValidateHTTPRouteSpec(&route.Spec, field.NewPath("spec"))
    52  }
    53  
    54  // ValidateHTTPRouteSpec validates that required fields of spec are set according to the
    55  // HTTPRoute specification.
    56  func ValidateHTTPRouteSpec(spec *gatewayv1.HTTPRouteSpec, path *field.Path) field.ErrorList {
    57  	var errs field.ErrorList
    58  	for i, rule := range spec.Rules {
    59  		errs = append(errs, validateHTTPRouteFilters(rule.Filters, rule.Matches, path.Child("rules").Index(i))...)
    60  		errs = append(errs, validateRequestRedirectFiltersWithBackendRefs(rule, path.Child("rules").Index(i))...)
    61  		for j, backendRef := range rule.BackendRefs {
    62  			errs = append(errs, validateHTTPRouteFilters(backendRef.Filters, rule.Matches, path.Child("rules").Index(i).Child("backendRefs").Index(j))...)
    63  		}
    64  		for j, m := range rule.Matches {
    65  			matchPath := path.Child("rules").Index(i).Child("matches").Index(j)
    66  
    67  			if m.Path != nil {
    68  				errs = append(errs, validateHTTPPathMatch(m.Path, matchPath.Child("path"))...)
    69  			}
    70  			if len(m.Headers) > 0 {
    71  				errs = append(errs, validateHTTPHeaderMatches(m.Headers, matchPath.Child("headers"))...)
    72  			}
    73  			if len(m.QueryParams) > 0 {
    74  				errs = append(errs, validateHTTPQueryParamMatches(m.QueryParams, matchPath.Child("queryParams"))...)
    75  			}
    76  		}
    77  
    78  		if rule.Timeouts != nil {
    79  			errs = append(errs, validateHTTPRouteTimeouts(rule.Timeouts, path.Child("rules").Child("timeouts"))...)
    80  		}
    81  	}
    82  	errs = append(errs, validateHTTPRouteBackendServicePorts(spec.Rules, path.Child("rules"))...)
    83  	errs = append(errs, ValidateParentRefs(spec.ParentRefs, path.Child("spec"))...)
    84  	return errs
    85  }
    86  
    87  // validateRequestRedirectFiltersWithBackendRefs validates that RequestRedirect filters are not used with backendRefs
    88  func validateRequestRedirectFiltersWithBackendRefs(rule gatewayv1.HTTPRouteRule, path *field.Path) field.ErrorList {
    89  	var errs field.ErrorList
    90  	for _, filter := range rule.Filters {
    91  		if filter.RequestRedirect != nil && len(rule.BackendRefs) > 0 {
    92  			errs = append(errs, field.Invalid(path.Child("filters"), gatewayv1.HTTPRouteFilterRequestRedirect, "RequestRedirect filter is not allowed with backendRefs"))
    93  		}
    94  	}
    95  	return errs
    96  }
    97  
    98  // validateHTTPRouteBackendServicePorts validates that v1.Service backends always have a port.
    99  func validateHTTPRouteBackendServicePorts(rules []gatewayv1.HTTPRouteRule, path *field.Path) field.ErrorList {
   100  	var errs field.ErrorList
   101  
   102  	for i, rule := range rules {
   103  		path = path.Index(i).Child("backendRefs")
   104  		for i, ref := range rule.BackendRefs {
   105  			if ref.BackendObjectReference.Group != nil &&
   106  				*ref.BackendObjectReference.Group != "" {
   107  				continue
   108  			}
   109  
   110  			if ref.BackendObjectReference.Kind != nil &&
   111  				*ref.BackendObjectReference.Kind != "Service" {
   112  				continue
   113  			}
   114  
   115  			if ref.BackendObjectReference.Port == nil {
   116  				errs = append(errs, field.Required(path.Index(i).Child("port"), "missing port for Service reference"))
   117  			}
   118  		}
   119  	}
   120  
   121  	return errs
   122  }
   123  
   124  // validateHTTPRouteFilters validates that a list of core and extended filters
   125  // is used at most once and that the filter type matches its value
   126  func validateHTTPRouteFilters(filters []gatewayv1.HTTPRouteFilter, matches []gatewayv1.HTTPRouteMatch, path *field.Path) field.ErrorList {
   127  	var errs field.ErrorList
   128  	counts := map[gatewayv1.HTTPRouteFilterType]int{}
   129  
   130  	for i, filter := range filters {
   131  		counts[filter.Type]++
   132  		if filter.RequestRedirect != nil && filter.RequestRedirect.Path != nil {
   133  			errs = append(errs, validateHTTPPathModifier(*filter.RequestRedirect.Path, matches, path.Index(i).Child("requestRedirect", "path"))...)
   134  		}
   135  		if filter.URLRewrite != nil && filter.URLRewrite.Path != nil {
   136  			errs = append(errs, validateHTTPPathModifier(*filter.URLRewrite.Path, matches, path.Index(i).Child("urlRewrite", "path"))...)
   137  		}
   138  		if filter.RequestHeaderModifier != nil {
   139  			errs = append(errs, validateHTTPHeaderModifier(*filter.RequestHeaderModifier, path.Index(i).Child("requestHeaderModifier"))...)
   140  		}
   141  		if filter.ResponseHeaderModifier != nil {
   142  			errs = append(errs, validateHTTPHeaderModifier(*filter.ResponseHeaderModifier, path.Index(i).Child("responseHeaderModifier"))...)
   143  		}
   144  		errs = append(errs, validateHTTPRouteFilterTypeMatchesValue(filter, path.Index(i))...)
   145  	}
   146  
   147  	if counts[gatewayv1.HTTPRouteFilterRequestRedirect] > 0 && counts[gatewayv1.HTTPRouteFilterURLRewrite] > 0 {
   148  		errs = append(errs, field.Invalid(path.Child("filters"), gatewayv1.HTTPRouteFilterRequestRedirect, "may specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both"))
   149  	}
   150  
   151  	// repeatableHTTPRouteFilters filters can be used more than once
   152  	for _, key := range repeatableHTTPRouteFilters {
   153  		delete(counts, key)
   154  	}
   155  
   156  	for filterType, count := range counts {
   157  		if count > 1 {
   158  			errs = append(errs, field.Invalid(path.Child("filters"), filterType, "cannot be used multiple times in the same rule"))
   159  		}
   160  	}
   161  	return errs
   162  }
   163  
   164  // webhook validation of HTTPPathMatch
   165  func validateHTTPPathMatch(path *gatewayv1.HTTPPathMatch, fldPath *field.Path) field.ErrorList {
   166  	allErrs := field.ErrorList{}
   167  
   168  	if path.Type == nil {
   169  		return append(allErrs, field.Required(fldPath.Child("type"), "must be specified"))
   170  	}
   171  
   172  	if path.Value == nil {
   173  		return append(allErrs, field.Required(fldPath.Child("value"), "must be specified"))
   174  	}
   175  
   176  	switch *path.Type {
   177  	case gatewayv1.PathMatchExact, gatewayv1.PathMatchPathPrefix:
   178  		if !strings.HasPrefix(*path.Value, "/") {
   179  			allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, "must be an absolute path"))
   180  		}
   181  		if len(*path.Value) > 0 {
   182  			for _, invalidSeq := range invalidPathSequences {
   183  				if strings.Contains(*path.Value, invalidSeq) {
   184  					allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("must not contain %q", invalidSeq)))
   185  				}
   186  			}
   187  
   188  			for _, invalidSuff := range invalidPathSuffixes {
   189  				if strings.HasSuffix(*path.Value, invalidSuff) {
   190  					allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("cannot end with '%s'", invalidSuff)))
   191  				}
   192  			}
   193  		}
   194  
   195  		r, err := regexp.Compile(validPathCharacters)
   196  		if err != nil {
   197  			allErrs = append(allErrs, field.InternalError(fldPath.Child("value"),
   198  				fmt.Errorf("could not compile path matching regex: %w", err)))
   199  		} else if !r.MatchString(*path.Value) {
   200  			allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value,
   201  				fmt.Sprintf("must only contain valid characters (matching %s)", validPathCharacters)))
   202  		}
   203  
   204  	case gatewayv1.PathMatchRegularExpression:
   205  	default:
   206  		pathTypes := []string{string(gatewayv1.PathMatchExact), string(gatewayv1.PathMatchPathPrefix), string(gatewayv1.PathMatchRegularExpression)}
   207  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), *path.Type, pathTypes))
   208  	}
   209  	return allErrs
   210  }
   211  
   212  // validateHTTPHeaderMatches validates that no header name
   213  // is matched more than once (case-insensitive).
   214  func validateHTTPHeaderMatches(matches []gatewayv1.HTTPHeaderMatch, path *field.Path) field.ErrorList {
   215  	var errs field.ErrorList
   216  	counts := map[string]int{}
   217  
   218  	for _, match := range matches {
   219  		// Header names are case-insensitive.
   220  		counts[strings.ToLower(string(match.Name))]++
   221  	}
   222  
   223  	for name, count := range counts {
   224  		if count > 1 {
   225  			errs = append(errs, field.Invalid(path, http.CanonicalHeaderKey(name), "cannot match the same header multiple times in the same rule"))
   226  		}
   227  	}
   228  
   229  	return errs
   230  }
   231  
   232  // validateHTTPQueryParamMatches validates that no query param name
   233  // is matched more than once (case-sensitive).
   234  func validateHTTPQueryParamMatches(matches []gatewayv1.HTTPQueryParamMatch, path *field.Path) field.ErrorList {
   235  	var errs field.ErrorList
   236  	counts := map[string]int{}
   237  
   238  	for _, match := range matches {
   239  		// Query param names are case-sensitive.
   240  		counts[string(match.Name)]++
   241  	}
   242  
   243  	for name, count := range counts {
   244  		if count > 1 {
   245  			errs = append(errs, field.Invalid(path, name, "cannot match the same query parameter multiple times in the same rule"))
   246  		}
   247  	}
   248  
   249  	return errs
   250  }
   251  
   252  // validateHTTPRouteFilterTypeMatchesValue validates that only the expected fields are
   253  // set for the specified filter type.
   254  func validateHTTPRouteFilterTypeMatchesValue(filter gatewayv1.HTTPRouteFilter, path *field.Path) field.ErrorList {
   255  	var errs field.ErrorList
   256  	if filter.ExtensionRef != nil && filter.Type != gatewayv1.HTTPRouteFilterExtensionRef {
   257  		errs = append(errs, field.Invalid(path, filter.ExtensionRef, "must be nil if the HTTPRouteFilter.Type is not ExtensionRef"))
   258  	}
   259  	if filter.ExtensionRef == nil && filter.Type == gatewayv1.HTTPRouteFilterExtensionRef {
   260  		errs = append(errs, field.Required(path, "filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type"))
   261  	}
   262  	if filter.RequestHeaderModifier != nil && filter.Type != gatewayv1.HTTPRouteFilterRequestHeaderModifier {
   263  		errs = append(errs, field.Invalid(path, filter.RequestHeaderModifier, "must be nil if the HTTPRouteFilter.Type is not RequestHeaderModifier"))
   264  	}
   265  	if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1.HTTPRouteFilterRequestHeaderModifier {
   266  		errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type"))
   267  	}
   268  	if filter.ResponseHeaderModifier != nil && filter.Type != gatewayv1.HTTPRouteFilterResponseHeaderModifier {
   269  		errs = append(errs, field.Invalid(path, filter.ResponseHeaderModifier, "must be nil if the HTTPRouteFilter.Type is not ResponseHeaderModifier"))
   270  	}
   271  	if filter.ResponseHeaderModifier == nil && filter.Type == gatewayv1.HTTPRouteFilterResponseHeaderModifier {
   272  		errs = append(errs, field.Required(path, "filter.ResponseHeaderModifier must be specified for ResponseHeaderModifier HTTPRouteFilter.Type"))
   273  	}
   274  	if filter.RequestMirror != nil && filter.Type != gatewayv1.HTTPRouteFilterRequestMirror {
   275  		errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the HTTPRouteFilter.Type is not RequestMirror"))
   276  	}
   277  	if filter.RequestMirror == nil && filter.Type == gatewayv1.HTTPRouteFilterRequestMirror {
   278  		errs = append(errs, field.Required(path, "filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type"))
   279  	}
   280  	if filter.RequestRedirect != nil && filter.Type != gatewayv1.HTTPRouteFilterRequestRedirect {
   281  		errs = append(errs, field.Invalid(path, filter.RequestRedirect, "must be nil if the HTTPRouteFilter.Type is not RequestRedirect"))
   282  	}
   283  	if filter.RequestRedirect == nil && filter.Type == gatewayv1.HTTPRouteFilterRequestRedirect {
   284  		errs = append(errs, field.Required(path, "filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type"))
   285  	}
   286  	if filter.URLRewrite != nil && filter.Type != gatewayv1.HTTPRouteFilterURLRewrite {
   287  		errs = append(errs, field.Invalid(path, filter.URLRewrite, "must be nil if the HTTPRouteFilter.Type is not URLRewrite"))
   288  	}
   289  	if filter.URLRewrite == nil && filter.Type == gatewayv1.HTTPRouteFilterURLRewrite {
   290  		errs = append(errs, field.Required(path, "filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type"))
   291  	}
   292  	return errs
   293  }
   294  
   295  // validateHTTPPathModifier validates that only the expected fields are set in a
   296  // path modifier.
   297  func validateHTTPPathModifier(modifier gatewayv1.HTTPPathModifier, matches []gatewayv1.HTTPRouteMatch, path *field.Path) field.ErrorList {
   298  	var errs field.ErrorList
   299  	if modifier.ReplaceFullPath != nil && modifier.Type != gatewayv1.FullPathHTTPPathModifier {
   300  		errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must be nil if the HTTPRouteFilter.Type is not ReplaceFullPath"))
   301  	}
   302  	if modifier.ReplaceFullPath == nil && modifier.Type == gatewayv1.FullPathHTTPPathModifier {
   303  		errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must not be nil if the HTTPRouteFilter.Type is ReplaceFullPath"))
   304  	}
   305  	if modifier.ReplacePrefixMatch != nil && modifier.Type != gatewayv1.PrefixMatchHTTPPathModifier {
   306  		errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must be nil if the HTTPRouteFilter.Type is not ReplacePrefixMatch"))
   307  	}
   308  	if modifier.ReplacePrefixMatch == nil && modifier.Type == gatewayv1.PrefixMatchHTTPPathModifier {
   309  		errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must not be nil if the HTTPRouteFilter.Type is ReplacePrefixMatch"))
   310  	}
   311  
   312  	if modifier.Type == gatewayv1.PrefixMatchHTTPPathModifier && modifier.ReplacePrefixMatch != nil {
   313  		if !hasExactlyOnePrefixMatch(matches) {
   314  			errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "exactly one PathPrefix match must be specified to use this path modifier"))
   315  		}
   316  	}
   317  	return errs
   318  }
   319  
   320  func validateHTTPHeaderModifier(filter gatewayv1.HTTPHeaderFilter, path *field.Path) field.ErrorList {
   321  	var errs field.ErrorList
   322  	singleAction := make(map[string]bool)
   323  	for i, action := range filter.Add {
   324  		if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok {
   325  			if needsErr {
   326  				errs = append(errs, field.Invalid(path.Child("add"), filter.Add[i], "cannot specify multiple actions for header"))
   327  			}
   328  			singleAction[strings.ToLower(string(action.Name))] = false
   329  		} else {
   330  			singleAction[strings.ToLower(string(action.Name))] = true
   331  		}
   332  	}
   333  	for i, action := range filter.Set {
   334  		if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok {
   335  			if needsErr {
   336  				errs = append(errs, field.Invalid(path.Child("set"), filter.Set[i], "cannot specify multiple actions for header"))
   337  			}
   338  			singleAction[strings.ToLower(string(action.Name))] = false
   339  		} else {
   340  			singleAction[strings.ToLower(string(action.Name))] = true
   341  		}
   342  	}
   343  	for i, name := range filter.Remove {
   344  		if needsErr, ok := singleAction[strings.ToLower(name)]; ok {
   345  			if needsErr {
   346  				errs = append(errs, field.Invalid(path.Child("remove"), filter.Remove[i], "cannot specify multiple actions for header"))
   347  			}
   348  			singleAction[strings.ToLower(name)] = false
   349  		} else {
   350  			singleAction[strings.ToLower(name)] = true
   351  		}
   352  	}
   353  	return errs
   354  }
   355  
   356  func validateHTTPRouteTimeouts(timeouts *gatewayv1.HTTPRouteTimeouts, path *field.Path) field.ErrorList {
   357  	var errs field.ErrorList
   358  	if timeouts.BackendRequest != nil {
   359  		backendTimeout, _ := time.ParseDuration((string)(*timeouts.BackendRequest))
   360  		if timeouts.Request != nil {
   361  			timeout, _ := time.ParseDuration((string)(*timeouts.Request))
   362  			if backendTimeout > timeout && timeout != 0 {
   363  				errs = append(errs, field.Invalid(path.Child("backendRequest"), backendTimeout, "backendRequest timeout cannot be longer than request timeout"))
   364  			}
   365  		}
   366  	}
   367  
   368  	return errs
   369  }
   370  
   371  func hasExactlyOnePrefixMatch(matches []gatewayv1.HTTPRouteMatch) bool {
   372  	if len(matches) != 1 || matches[0].Path == nil {
   373  		return false
   374  	}
   375  	pathMatchType := matches[0].Path.Type
   376  	if *pathMatchType != gatewayv1.PathMatchPathPrefix {
   377  		return false
   378  	}
   379  
   380  	return true
   381  }
   382  

View as plain text