sigs.k8s.io/gateway-api@v1.0.0/apis/v1/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 ) 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 }