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