sigs.k8s.io/gateway-api@v1.0.0/apis/v1beta1/validation/httproute.go (about) 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 }