1
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
34
35 repeatableHTTPRouteFilters = []gatewayv1b1.HTTPRouteFilterType{
36 gatewayv1.HTTPRouteFilterExtensionRef,
37 gatewayv1.HTTPRouteFilterRequestMirror,
38 }
39
40
41 invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F", "#"}
42 invalidPathSuffixes = []string{"/..", "/."}
43
44
45 validPathCharacters = "^(?:[A-Za-z0-9\\/\\-._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$"
46 )
47
48
49
50
51 func ValidateHTTPRoute(route *gatewayv1b1.HTTPRoute) field.ErrorList {
52 return ValidateHTTPRouteSpec(&route.Spec, field.NewPath("spec"))
53 }
54
55
56
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
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
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
126
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
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
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
214
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
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
234
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
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
254
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
297
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