...

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

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

View as plain text