github.com/nginxinc/kubernetes-ingress@v1.12.5/pkg/apis/configuration/validation/virtualserver.go (about) 1 package validation 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 9 "github.com/nginxinc/kubernetes-ingress/internal/configs" 10 v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" 11 "k8s.io/apimachinery/pkg/util/sets" 12 "k8s.io/apimachinery/pkg/util/validation" 13 "k8s.io/apimachinery/pkg/util/validation/field" 14 ) 15 16 // VirtualServerValidator validates a VirtualServer/VirtualServerRoute resource. 17 type VirtualServerValidator struct { 18 isPlus bool 19 } 20 21 // NewVirtualServerValidator creates a new VirtualServerValidator. 22 func NewVirtualServerValidator(isPlus bool) *VirtualServerValidator { 23 return &VirtualServerValidator{ 24 isPlus: isPlus, 25 } 26 } 27 28 // ValidateVirtualServer validates a VirtualServer. 29 func (vsv *VirtualServerValidator) ValidateVirtualServer(virtualServer *v1.VirtualServer) error { 30 allErrs := vsv.validateVirtualServerSpec(&virtualServer.Spec, field.NewPath("spec"), virtualServer.Namespace) 31 return allErrs.ToAggregate() 32 } 33 34 // validateVirtualServerSpec validates a VirtualServerSpec. 35 func (vsv *VirtualServerValidator) validateVirtualServerSpec(spec *v1.VirtualServerSpec, fieldPath *field.Path, namespace string) field.ErrorList { 36 allErrs := field.ErrorList{} 37 38 allErrs = append(allErrs, validateHost(spec.Host, fieldPath.Child("host"))...) 39 allErrs = append(allErrs, validateTLS(spec.TLS, fieldPath.Child("tls"))...) 40 allErrs = append(allErrs, validatePolicies(spec.Policies, fieldPath.Child("policies"), namespace)...) 41 42 upstreamErrs, upstreamNames := vsv.validateUpstreams(spec.Upstreams, fieldPath.Child("upstreams")) 43 allErrs = append(allErrs, upstreamErrs...) 44 45 allErrs = append(allErrs, vsv.validateVirtualServerRoutes(spec.Routes, fieldPath.Child("routes"), upstreamNames, namespace)...) 46 47 return allErrs 48 } 49 50 func validateHost(host string, fieldPath *field.Path) field.ErrorList { 51 allErrs := field.ErrorList{} 52 53 if host == "" { 54 return append(allErrs, field.Required(fieldPath, "")) 55 } 56 57 for _, msg := range validation.IsDNS1123Subdomain(host) { 58 allErrs = append(allErrs, field.Invalid(fieldPath, host, msg)) 59 } 60 61 return allErrs 62 } 63 64 func validatePolicies(policies []v1.PolicyReference, fieldPath *field.Path, namespace string) field.ErrorList { 65 allErrs := field.ErrorList{} 66 policyKeys := sets.String{} 67 68 for i, p := range policies { 69 idxPath := fieldPath.Index(i) 70 71 polNamespace := p.Namespace 72 if polNamespace == "" { 73 polNamespace = namespace 74 } 75 76 key := fmt.Sprintf("%s/%s", polNamespace, p.Name) 77 78 if policyKeys.Has(key) { 79 allErrs = append(allErrs, field.Duplicate(idxPath, key)) 80 } else { 81 policyKeys.Insert(key) 82 } 83 84 if p.Name == "" { 85 allErrs = append(allErrs, field.Required(idxPath.Child("name"), "")) 86 } else { 87 for _, msg := range validation.IsDNS1123Subdomain(p.Name) { 88 allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), p.Name, msg)) 89 } 90 } 91 92 if p.Namespace != "" { 93 for _, msg := range validation.IsDNS1123Label(p.Namespace) { 94 allErrs = append(allErrs, field.Invalid(idxPath.Child("namespace"), p.Namespace, msg)) 95 } 96 } 97 } 98 99 return allErrs 100 } 101 102 func validateTLS(tls *v1.TLS, fieldPath *field.Path) field.ErrorList { 103 allErrs := field.ErrorList{} 104 105 if tls == nil { 106 // valid case - tls is not defined 107 return allErrs 108 } 109 110 allErrs = append(allErrs, validateSecretName(tls.Secret, fieldPath.Child("secret"))...) 111 112 allErrs = append(allErrs, validateTLSRedirect(tls.Redirect, fieldPath.Child("redirect"))...) 113 114 return allErrs 115 } 116 117 func validateTLSRedirect(redirect *v1.TLSRedirect, fieldPath *field.Path) field.ErrorList { 118 allErrs := field.ErrorList{} 119 120 if redirect == nil { 121 return allErrs 122 } 123 124 if redirect.Code != nil { 125 allErrs = append(allErrs, validateRedirectStatusCode(*redirect.Code, fieldPath.Child("code"))...) 126 } 127 128 if redirect.BasedOn != "" && redirect.BasedOn != "scheme" && redirect.BasedOn != "x-forwarded-proto" { 129 allErrs = append(allErrs, field.Invalid(fieldPath.Child("basedOn"), redirect.BasedOn, "accepted values are 'scheme', 'x-forwarded-proto'")) 130 } 131 132 return allErrs 133 } 134 135 var validRedirectStatusCodes = map[int]bool{ 136 301: true, 137 302: true, 138 307: true, 139 308: true, 140 } 141 142 func validateRedirectStatusCode(code int, fieldPath *field.Path) field.ErrorList { 143 allErrs := field.ErrorList{} 144 145 if _, ok := validRedirectStatusCodes[code]; !ok { 146 allErrs = append(allErrs, field.Invalid(fieldPath, code, "status code out of accepted range. accepted values are '301', '302', '307', '308'")) 147 } 148 149 return allErrs 150 } 151 152 func validatePositiveIntOrZero(n int, fieldPath *field.Path) field.ErrorList { 153 allErrs := field.ErrorList{} 154 155 if n < 0 { 156 return append(allErrs, field.Invalid(fieldPath, n, "must be positive")) 157 } 158 159 return allErrs 160 } 161 162 func validatePositiveIntOrZeroFromPointer(n *int, fieldPath *field.Path) field.ErrorList { 163 allErrs := field.ErrorList{} 164 if n == nil { 165 return allErrs 166 } 167 168 if *n < 0 { 169 return append(allErrs, field.Invalid(fieldPath, n, "must be positive or zero")) 170 } 171 172 return allErrs 173 } 174 175 func validateBuffer(buff *v1.UpstreamBuffers, fieldPath *field.Path) field.ErrorList { 176 allErrs := field.ErrorList{} 177 178 if buff == nil { 179 return allErrs 180 } 181 182 if buff.Number <= 0 { 183 allErrs = append(allErrs, field.Invalid(fieldPath.Child("number"), buff.Number, "must be positive")) 184 } 185 186 if buff.Size == "" { 187 allErrs = append(allErrs, field.Required(fieldPath.Child("size"), "cannot be empty")) 188 } else { 189 allErrs = append(allErrs, validateSize(buff.Size, fieldPath.Child("size"))...) 190 } 191 192 return allErrs 193 } 194 195 func validateUpstreamLBMethod(lBMethod string, fieldPath *field.Path, isPlus bool) field.ErrorList { 196 allErrs := field.ErrorList{} 197 if lBMethod == "" { 198 return allErrs 199 } 200 201 if isPlus { 202 _, err := configs.ParseLBMethodForPlus(lBMethod) 203 if err != nil { 204 return append(allErrs, field.Invalid(fieldPath, lBMethod, err.Error())) 205 } 206 } else { 207 _, err := configs.ParseLBMethod(lBMethod) 208 if err != nil { 209 return append(allErrs, field.Invalid(fieldPath, lBMethod, err.Error())) 210 } 211 } 212 213 return allErrs 214 } 215 216 func validateUpstreamHealthCheck(hc *v1.HealthCheck, fieldPath *field.Path) field.ErrorList { 217 allErrs := field.ErrorList{} 218 219 if hc == nil { 220 return allErrs 221 } 222 223 if hc.Path != "" { 224 allErrs = append(allErrs, validatePath(hc.Path, fieldPath.Child("path"))...) 225 } 226 227 allErrs = append(allErrs, validateTime(hc.Interval, fieldPath.Child("interval"))...) 228 allErrs = append(allErrs, validateTime(hc.Jitter, fieldPath.Child("jitter"))...) 229 allErrs = append(allErrs, validatePositiveIntOrZero(hc.Fails, fieldPath.Child("fails"))...) 230 allErrs = append(allErrs, validatePositiveIntOrZero(hc.Passes, fieldPath.Child("passes"))...) 231 allErrs = append(allErrs, validateTime(hc.ConnectTimeout, fieldPath.Child("connect-timeout"))...) 232 allErrs = append(allErrs, validateTime(hc.ReadTimeout, fieldPath.Child("read-timeout"))...) 233 allErrs = append(allErrs, validateTime(hc.SendTimeout, fieldPath.Child("send-timeout"))...) 234 allErrs = append(allErrs, validateStatusMatch(hc.StatusMatch, fieldPath.Child("statusMatch"))...) 235 236 for i, header := range hc.Headers { 237 idxPath := fieldPath.Child("headers").Index(i) 238 allErrs = append(allErrs, validateHeader(header, idxPath)...) 239 } 240 241 if hc.Port > 0 { 242 for _, msg := range validation.IsValidPortNum(hc.Port) { 243 allErrs = append(allErrs, field.Invalid(fieldPath.Child("port"), hc.Port, msg)) 244 } 245 } 246 247 return allErrs 248 } 249 250 func validateSessionCookie(sc *v1.SessionCookie, fieldPath *field.Path) field.ErrorList { 251 allErrs := field.ErrorList{} 252 253 if sc == nil { 254 return allErrs 255 } 256 257 if sc.Name == "" { 258 allErrs = append(allErrs, field.Required(fieldPath.Child("name"), "")) 259 } else { 260 for _, msg := range isCookieName(sc.Name) { 261 allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), sc.Name, msg)) 262 } 263 } 264 265 if sc.Path != "" { 266 allErrs = append(allErrs, validatePath(sc.Path, fieldPath.Child("path"))...) 267 } 268 269 if sc.Expires != "max" { 270 allErrs = append(allErrs, validateTime(sc.Expires, fieldPath.Child("expires"))...) 271 } 272 273 if sc.Domain != "" { 274 // A Domain prefix of "." is allowed. 275 domain := strings.TrimPrefix(sc.Domain, ".") 276 for _, msg := range validation.IsDNS1123Subdomain(domain) { 277 allErrs = append(allErrs, field.Invalid(fieldPath.Child("domain"), sc.Domain, msg)) 278 } 279 } 280 281 return allErrs 282 } 283 284 func validateStatusMatch(s string, fieldPath *field.Path) field.ErrorList { 285 allErrs := field.ErrorList{} 286 287 if s == "" { 288 return allErrs 289 } 290 291 if strings.HasPrefix(s, "!") { 292 if !strings.HasPrefix(s, "! ") { 293 allErrs = append(allErrs, field.Invalid(fieldPath, s, "must have an space character after the `!`")) 294 } 295 } 296 297 statuses := strings.Split(s, " ") 298 for i, value := range statuses { 299 if value == "!" { 300 if i != 0 { 301 allErrs = append(allErrs, field.Invalid(fieldPath, s, "`!` can only appear once at the beginning")) 302 } 303 } else if strings.Contains(value, "-") { 304 if msg := validateStatusCodeRange(value); msg != "" { 305 allErrs = append(allErrs, field.Invalid(fieldPath, s, msg)) 306 } 307 } else if msg := validateStatusCode(value); msg != "" { 308 allErrs = append(allErrs, field.Invalid(fieldPath, s, msg)) 309 } 310 } 311 312 return allErrs 313 } 314 315 func validateStatusCodeRange(statusRangeStr string) string { 316 statusRange := strings.Split(statusRangeStr, "-") 317 if len(statusRange) != 2 { 318 return "ranges must only have 2 numbers" 319 } 320 321 min, msg := validateIntFromString(statusRange[0]) 322 if msg != "" { 323 return msg 324 } 325 326 max, msg := validateIntFromString(statusRange[1]) 327 if msg != "" { 328 return msg 329 } 330 331 for _, code := range statusRange { 332 if msg := validateStatusCode(code); msg != "" { 333 return msg 334 } 335 } 336 337 if max <= min { 338 return fmt.Sprintf("range limits must be %v < %v", min, max) 339 } 340 341 return "" 342 } 343 344 func validateIntFromString(number string) (int, string) { 345 numberInt, err := strconv.ParseInt(number, 10, 64) 346 if err != nil { 347 return 0, fmt.Sprintf("%v must be a valid integer", number) 348 } 349 350 return int(numberInt), "" 351 } 352 353 func validateStatusCode(status string) string { 354 code, errMsg := validateIntFromString(status) 355 if errMsg != "" { 356 return errMsg 357 } 358 359 if code < 100 || code > 999 { 360 return validation.InclusiveRangeError(100, 999) 361 } 362 363 return "" 364 } 365 366 func validateHeader(h v1.Header, fieldPath *field.Path) field.ErrorList { 367 allErrs := field.ErrorList{} 368 369 if h.Name == "" { 370 allErrs = append(allErrs, field.Required(fieldPath.Child("name"), "")) 371 } 372 373 for _, msg := range validation.IsHTTPHeaderName(h.Name) { 374 allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), h.Name, msg)) 375 } 376 377 for _, msg := range isValidHeaderValue(h.Value) { 378 allErrs = append(allErrs, field.Invalid(fieldPath.Child("value"), h.Value, msg)) 379 } 380 381 return allErrs 382 } 383 384 const ( 385 headerValueFmt = `([^"$\\]|\\[^$])*` 386 headerValueFmtErrMsg string = `a valid header must have all '"' escaped and must not contain any '$' or end with an unescaped '\'` 387 ) 388 389 var headerValueFmtRegexp = regexp.MustCompile("^" + headerValueFmt + "$") 390 391 func isValidHeaderValue(s string) []string { 392 if !headerValueFmtRegexp.MatchString(s) { 393 return []string{validation.RegexError(headerValueFmtErrMsg, headerValueFmt, "my.service", "foo")} 394 } 395 return nil 396 } 397 398 func (vsv *VirtualServerValidator) validateUpstreams(upstreams []v1.Upstream, fieldPath *field.Path) (allErrs field.ErrorList, upstreamNames sets.String) { 399 allErrs = field.ErrorList{} 400 upstreamNames = sets.String{} 401 402 for i, u := range upstreams { 403 idxPath := fieldPath.Index(i) 404 405 upstreamErrors := validateUpstreamName(u.Name, idxPath.Child("name")) 406 if len(upstreamErrors) > 0 { 407 allErrs = append(allErrs, upstreamErrors...) 408 } else if upstreamNames.Has(u.Name) { 409 allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), u.Name)) 410 } else { 411 upstreamNames.Insert(u.Name) 412 } 413 if u.UseClusterIP && u.Subselector != nil { 414 allErrs = append(allErrs, field.Forbidden(idxPath.Child("subselector"), "subselector can't be used with use-cluster-ip")) 415 } else { 416 allErrs = append(allErrs, validateLabels(u.Subselector, idxPath.Child("subselector"))...) 417 } 418 419 allErrs = append(allErrs, validateServiceName(u.Service, idxPath.Child("service"))...) 420 allErrs = append(allErrs, validateTime(u.ProxyConnectTimeout, idxPath.Child("connect-timeout"))...) 421 allErrs = append(allErrs, validateTime(u.ProxyReadTimeout, idxPath.Child("read-timeout"))...) 422 allErrs = append(allErrs, validateTime(u.ProxySendTimeout, idxPath.Child("send-timeout"))...) 423 allErrs = append(allErrs, validateNextUpstream(u.ProxyNextUpstream, idxPath.Child("next-upstream"))...) 424 allErrs = append(allErrs, validateTime(u.ProxyNextUpstreamTimeout, idxPath.Child("next-upstream-timeout"))...) 425 allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(&u.ProxyNextUpstreamTries, idxPath.Child("next-upstream-tries"))...) 426 allErrs = append(allErrs, validateUpstreamLBMethod(u.LBMethod, idxPath.Child("lb-method"), vsv.isPlus)...) 427 allErrs = append(allErrs, validateTime(u.FailTimeout, idxPath.Child("fail-timeout"))...) 428 allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(u.MaxFails, idxPath.Child("max-fails"))...) 429 allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(u.Keepalive, idxPath.Child("keepalive"))...) 430 allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(u.MaxConns, idxPath.Child("max-conns"))...) 431 allErrs = append(allErrs, validateOffset(u.ClientMaxBodySize, idxPath.Child("client-max-body-size"))...) 432 allErrs = append(allErrs, validateUpstreamHealthCheck(u.HealthCheck, idxPath.Child("healthCheck"))...) 433 allErrs = append(allErrs, validateTime(u.SlowStart, idxPath.Child("slow-start"))...) 434 allErrs = append(allErrs, validateBuffer(u.ProxyBuffers, idxPath.Child("buffers"))...) 435 allErrs = append(allErrs, validateSize(u.ProxyBufferSize, idxPath.Child("buffer-size"))...) 436 allErrs = append(allErrs, validateQueue(u.Queue, idxPath.Child("queue"))...) 437 allErrs = append(allErrs, validateSessionCookie(u.SessionCookie, idxPath.Child("sessionCookie"))...) 438 439 for _, msg := range validation.IsValidPortNum(int(u.Port)) { 440 allErrs = append(allErrs, field.Invalid(idxPath.Child("port"), u.Port, msg)) 441 } 442 443 allErrs = append(allErrs, rejectPlusResourcesInOSS(u, idxPath, vsv.isPlus)...) 444 } 445 446 return allErrs, upstreamNames 447 } 448 449 var validNextUpstreamParams = map[string]bool{ 450 "error": true, 451 "timeout": true, 452 "invalid_header": true, 453 "http_500": true, 454 "http_502": true, 455 "http_503": true, 456 "http_504": true, 457 "http_403": true, 458 "http_404": true, 459 "http_429": true, 460 "non_idempotent": true, 461 "off": true, 462 "": true, 463 } 464 465 // validateNextUpstream checks the values given for passing queries to a upstream 466 func validateNextUpstream(nextUpstream string, fieldPath *field.Path) field.ErrorList { 467 allErrs := field.ErrorList{} 468 allParams := sets.String{} 469 if nextUpstream == "" { 470 return allErrs 471 } 472 params := strings.Fields(nextUpstream) 473 for _, para := range params { 474 if !validNextUpstreamParams[para] { 475 allErrs = append(allErrs, field.Invalid(fieldPath, para, "not a valid parameter")) 476 } 477 if allParams.Has(para) { 478 allErrs = append(allErrs, field.Invalid(fieldPath, para, "can not have duplicate parameters")) 479 } else { 480 allParams.Insert(para) 481 } 482 } 483 return allErrs 484 } 485 486 // validateUpstreamName checks is an upstream name is valid. 487 // The rules for NGINX upstream names are less strict than IsDNS1035Label. 488 // However, it is convenient to enforce IsDNS1035Label in the yaml for 489 // the names of upstreams. 490 func validateUpstreamName(name string, fieldPath *field.Path) field.ErrorList { 491 return validateDNS1035Label(name, fieldPath) 492 } 493 494 // validateServiceName checks if a service name is valid. 495 // It performs the same validation as ValidateServiceName from k8s.io/kubernetes/pkg/apis/core/validation/validation.go. 496 func validateServiceName(name string, fieldPath *field.Path) field.ErrorList { 497 return validateDNS1035Label(name, fieldPath) 498 } 499 500 func validateDNS1035Label(name string, fieldPath *field.Path) field.ErrorList { 501 allErrs := field.ErrorList{} 502 503 if name == "" { 504 return append(allErrs, field.Required(fieldPath, "")) 505 } 506 507 for _, msg := range validation.IsDNS1035Label(name) { 508 allErrs = append(allErrs, field.Invalid(fieldPath, name, msg)) 509 } 510 511 return allErrs 512 } 513 514 func (vsv *VirtualServerValidator) validateVirtualServerRoutes(routes []v1.Route, fieldPath *field.Path, upstreamNames sets.String, namespace string) field.ErrorList { 515 allErrs := field.ErrorList{} 516 517 allPaths := sets.String{} 518 519 for i, r := range routes { 520 idxPath := fieldPath.Index(i) 521 522 isRouteFieldForbidden := false 523 routeErrs := vsv.validateRoute(r, idxPath, upstreamNames, isRouteFieldForbidden, namespace) 524 if len(routeErrs) > 0 { 525 allErrs = append(allErrs, routeErrs...) 526 } else if allPaths.Has(r.Path) { 527 allErrs = append(allErrs, field.Duplicate(idxPath.Child("path"), r.Path)) 528 } else { 529 allPaths.Insert(r.Path) 530 } 531 } 532 533 return allErrs 534 } 535 536 func (vsv *VirtualServerValidator) validateRoute(route v1.Route, fieldPath *field.Path, upstreamNames sets.String, isRouteFieldForbidden bool, namespace string) field.ErrorList { 537 allErrs := field.ErrorList{} 538 539 allErrs = append(allErrs, validateRoutePath(route.Path, fieldPath.Child("path"))...) 540 allErrs = append(allErrs, validatePolicies(route.Policies, fieldPath.Child("policies"), namespace)...) 541 542 fieldCount := 0 543 544 if route.Action != nil { 545 allErrs = append(allErrs, vsv.validateAction(route.Action, fieldPath.Child("action"), upstreamNames, route.Path, false)...) 546 fieldCount++ 547 } 548 549 if len(route.Splits) > 0 { 550 allErrs = append(allErrs, vsv.validateSplits(route.Splits, fieldPath.Child("splits"), upstreamNames, route.Path)...) 551 fieldCount++ 552 } 553 554 // Matches are optional. that's why we don't do fieldCount++ 555 if len(route.Matches) > 0 { 556 for i, m := range route.Matches { 557 allErrs = append(allErrs, vsv.validateMatch(m, fieldPath.Child("matches").Index(i), upstreamNames, route.Path)...) 558 } 559 } 560 561 for i, e := range route.ErrorPages { 562 allErrs = append(allErrs, vsv.validateErrorPage(e, fieldPath.Child("errorPages").Index(i))...) 563 } 564 565 if route.Route != "" { 566 if isRouteFieldForbidden { 567 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("route"), "is not allowed")) 568 } else { 569 allErrs = append(allErrs, validateRouteField(route.Route, fieldPath.Child("route"))...) 570 fieldCount++ 571 } 572 } 573 574 if fieldCount != 1 { 575 msg := "must specify exactly one of `action`, `splits` or `route`" 576 if isRouteFieldForbidden || len(route.Matches) > 0 { 577 msg = "must specify exactly one of `action` or `splits`" 578 } 579 580 allErrs = append(allErrs, field.Invalid(fieldPath, "", msg)) 581 } 582 583 return allErrs 584 } 585 586 func errorPageHasRequiredFields(errorPage v1.ErrorPage) bool { 587 var count int 588 589 if errorPage.Return != nil { 590 count++ 591 } 592 593 if errorPage.Redirect != nil { 594 count++ 595 } 596 597 return count == 1 598 } 599 600 func (vsv *VirtualServerValidator) validateErrorPage(errorPage v1.ErrorPage, fieldPath *field.Path) field.ErrorList { 601 allErrs := field.ErrorList{} 602 603 if !errorPageHasRequiredFields(errorPage) { 604 return append(allErrs, field.Required(fieldPath, "must specify exactly one of `redirect` or `return`")) 605 } 606 607 if len(errorPage.Codes) == 0 { 608 return append(allErrs, field.Required(fieldPath.Child("codes"), "must include at least 1 status code in `codes`")) 609 } 610 611 for i, c := range errorPage.Codes { 612 for _, msg := range validation.IsInRange(c, 300, 599) { 613 allErrs = append(allErrs, field.Invalid(fieldPath.Child("codes").Index(i), c, msg)) 614 } 615 } 616 617 if errorPage.Return != nil { 618 allErrs = append(allErrs, vsv.validateErrorPageReturn(errorPage.Return, fieldPath.Child("return"))...) 619 } 620 621 if errorPage.Redirect != nil { 622 allErrs = append(allErrs, vsv.validateErrorPageRedirect(errorPage.Redirect, fieldPath.Child("redirect"))...) 623 } 624 625 return allErrs 626 } 627 628 var errorPageReturnBodyVariable = map[string]bool{"upstream_status": true} 629 630 func (vsv *VirtualServerValidator) validateErrorPageReturn(r *v1.ErrorPageReturn, fieldPath *field.Path) field.ErrorList { 631 allErrs := field.ErrorList{} 632 633 allErrs = append(allErrs, vsv.validateActionReturn(&r.ActionReturn, fieldPath, nil, errorPageReturnBodyVariable)...) 634 635 for i, header := range r.Headers { 636 allErrs = append(allErrs, vsv.validateErrorPageHeader(header, fieldPath.Child("headers").Index(i))...) 637 } 638 639 return allErrs 640 } 641 642 var errorPageHeaderValueVariables = map[string]bool{"upstream_status": true} 643 644 func (vsv *VirtualServerValidator) validateErrorPageHeader(h v1.Header, fieldPath *field.Path) field.ErrorList { 645 allErrs := field.ErrorList{} 646 647 if h.Name == "" { 648 allErrs = append(allErrs, field.Required(fieldPath.Child("name"), "")) 649 } 650 651 for _, msg := range validation.IsHTTPHeaderName(h.Name) { 652 allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), h.Name, msg)) 653 } 654 655 if !escapedStringsFmtRegexp.MatchString(h.Value) { 656 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "value", `\"${status}\"`) 657 allErrs = append(allErrs, field.Invalid(fieldPath.Child("value"), h.Value, msg)) 658 } 659 660 allErrs = append(allErrs, validateStringWithVariables(h.Value, fieldPath.Child("value"), nil, errorPageHeaderValueVariables, vsv.isPlus)...) 661 662 return allErrs 663 } 664 665 var validErrorPageRedirectVariables = map[string]bool{"scheme": true, "http_x_forwarded_proto": true} 666 667 func (vsv *VirtualServerValidator) validateErrorPageRedirect(r *v1.ErrorPageRedirect, fieldPath *field.Path) field.ErrorList { 668 allErrs := field.ErrorList{} 669 670 allErrs = append(allErrs, vsv.validateActionRedirect(&r.ActionRedirect, fieldPath, validErrorPageRedirectVariables)...) 671 672 return allErrs 673 } 674 675 func countActions(action *v1.Action) int { 676 var count int 677 if action.Pass != "" { 678 count++ 679 } 680 681 if action.Redirect != nil { 682 count++ 683 } 684 685 if action.Return != nil { 686 count++ 687 } 688 689 if action.Proxy != nil { 690 count++ 691 } 692 693 return count 694 } 695 696 // returnBodyVariables includes NGINX variables allowed to be used in a return body. 697 var returnBodyVariables = map[string]bool{ 698 "request_uri": true, 699 "request_method": true, 700 "request_body": true, 701 "scheme": true, 702 "args": true, 703 "host": true, 704 "request_time": true, 705 "request_length": true, 706 "nginx_version": true, 707 "pid": true, 708 "connection": true, 709 "remote_addr": true, 710 "remote_port": true, 711 "time_iso8601": true, 712 "time_local": true, 713 "server_addr": true, 714 "server_port": true, 715 "server_name": true, 716 "server_protocol": true, 717 "connections_active": true, 718 "connections_reading": true, 719 "connections_writing": true, 720 "connections_waiting": true, 721 } 722 723 var returnBodySpecialVariables = []string{"arg_", "http_", "cookie_"} 724 725 // validRedirectVariableNames includes NGINX variables allowed to be used in redirects. 726 var validRedirectVariableNames = map[string]bool{ 727 "scheme": true, 728 "http_x_forwarded_proto": true, 729 "request_uri": true, 730 "host": true, 731 } 732 733 func (vsv *VirtualServerValidator) validateAction(action *v1.Action, fieldPath *field.Path, upstreamNames sets.String, path string, internal bool) field.ErrorList { 734 allErrs := field.ErrorList{} 735 736 if countActions(action) != 1 { 737 return append(allErrs, field.Required(fieldPath, "action must specify exactly one of `pass`, `redirect`, `return` or `proxy`")) 738 } 739 740 if action.Pass != "" { 741 allErrs = append(allErrs, validateReferencedUpstream(action.Pass, fieldPath.Child("pass"), upstreamNames)...) 742 } 743 744 if action.Redirect != nil { 745 allErrs = append(allErrs, vsv.validateActionRedirect(action.Redirect, fieldPath.Child("redirect"), validRedirectVariableNames)...) 746 } 747 748 if action.Return != nil { 749 allErrs = append(allErrs, vsv.validateActionReturn(action.Return, fieldPath.Child("return"), returnBodySpecialVariables, returnBodyVariables)...) 750 } 751 752 if action.Proxy != nil { 753 allErrs = append(allErrs, vsv.validateActionProxy(action.Proxy, fieldPath.Child("proxy"), upstreamNames, path, internal)...) 754 } 755 756 return allErrs 757 } 758 759 func (vsv *VirtualServerValidator) validateActionRedirect(redirect *v1.ActionRedirect, fieldPath *field.Path, validVars map[string]bool) field.ErrorList { 760 allErrs := field.ErrorList{} 761 762 allErrs = append(allErrs, vsv.validateRedirectURL(redirect.URL, fieldPath.Child("url"), validVars)...) 763 764 if redirect.Code != 0 { 765 allErrs = append(allErrs, validateRedirectStatusCode(redirect.Code, fieldPath.Child("code"))...) 766 } 767 768 return allErrs 769 } 770 771 var nginxVariableRegexp = regexp.MustCompile(`\$\{([^}]*)\}`) 772 773 // captureVariables returns a slice of vars enclosed in ${}. For example "${a} ${b}" would return ["a", "b"]. 774 func captureVariables(s string) []string { 775 var nVars []string 776 777 res := nginxVariableRegexp.FindAllStringSubmatch(s, -1) 778 for _, n := range res { 779 nVars = append(nVars, n[1]) 780 } 781 782 return nVars 783 } 784 785 func (vsv *VirtualServerValidator) validateRedirectURL(redirectURL string, fieldPath *field.Path, validVars map[string]bool) field.ErrorList { 786 allErrs := field.ErrorList{} 787 788 if redirectURL == "" { 789 return append(allErrs, field.Required(fieldPath, "must specify a url")) 790 } 791 792 if !strings.Contains(redirectURL, "://") { 793 return append(allErrs, field.Invalid(fieldPath, redirectURL, "must contain the protocol with '://', for example http://, https:// or ${scheme}://")) 794 } 795 796 if !escapedStringsFmtRegexp.MatchString(redirectURL) { 797 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "http://www.nginx.com", "${scheme}://${host}/green/", `\"http://www.nginx.com\"`) 798 return append(allErrs, field.Invalid(fieldPath, redirectURL, msg)) 799 } 800 801 allErrs = append(allErrs, validateStringWithVariables(redirectURL, fieldPath, nil, validVars, vsv.isPlus)...) 802 803 return allErrs 804 } 805 806 func validateActionReturnCode(code int, fieldPath *field.Path) field.ErrorList { 807 allErrs := field.ErrorList{} 808 809 if (code >= 200 && code <= 299) || (code >= 400 && code <= 599) { 810 return allErrs 811 } 812 813 msg := "must be a valid status code either 2XX, 4XX or 5XX, for example, 200 or 402." 814 return append(allErrs, field.Invalid(fieldPath, code, msg)) 815 } 816 817 func (vsv *VirtualServerValidator) validateActionReturn(r *v1.ActionReturn, fieldPath *field.Path, specialValidVars []string, validVars map[string]bool) field.ErrorList { 818 allErrs := field.ErrorList{} 819 820 if r.Body == "" { 821 return append(allErrs, field.Required(fieldPath.Child("body"), "")) 822 } 823 824 allErrs = append(allErrs, validateEscapedStringWithVariables(r.Body, fieldPath.Child("body"), specialValidVars, validVars, vsv.isPlus)...) 825 826 if r.Type != "" { 827 allErrs = append(allErrs, validateActionReturnType(r.Type, fieldPath.Child("type"))...) 828 } 829 830 if r.Code != 0 { 831 allErrs = append(allErrs, validateActionReturnCode(r.Code, fieldPath.Child("code"))...) 832 } 833 834 return allErrs 835 } 836 837 func validateEscapedStringWithVariables(body string, fieldPath *field.Path, specialValidVars []string, validVars map[string]bool, isPlus bool) field.ErrorList { 838 allErrs := field.ErrorList{} 839 840 if !escapedStringsFmtRegexp.MatchString(body) { 841 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`) 842 allErrs = append(allErrs, field.Invalid(fieldPath, body, msg)) 843 } 844 845 allErrs = append(allErrs, validateStringWithVariables(body, fieldPath, specialValidVars, validVars, isPlus)...) 846 847 return allErrs 848 } 849 850 var ( 851 actionReturnTypeFmt = `([^;\{\}"\\]|\\.)*` 852 actionReturnTypeErr = `must have all '"' (double quotes), '{', '}' or ';' escaped and must not end with an unescaped '\' (backslash)` 853 ) 854 855 var actionReturnTypeRegexp = regexp.MustCompile("^" + actionReturnTypeFmt + "$") 856 857 func validateActionReturnType(returnType string, fieldPath *field.Path) field.ErrorList { 858 allErrs := field.ErrorList{} 859 860 if !actionReturnTypeRegexp.MatchString(returnType) { 861 msg := validation.RegexError(actionReturnTypeErr, actionReturnTypeFmt, "type/subtype", "application/json") 862 allErrs = append(allErrs, field.Invalid(fieldPath, returnType, msg)) 863 } 864 865 return allErrs 866 } 867 868 func validateRouteField(value string, fieldPath *field.Path) field.ErrorList { 869 allErrs := field.ErrorList{} 870 871 for _, msg := range validation.IsQualifiedName(value) { 872 allErrs = append(allErrs, field.Invalid(fieldPath, value, msg)) 873 } 874 875 return allErrs 876 } 877 878 func validateReferencedUpstream(name string, fieldPath *field.Path, upstreamNames sets.String) field.ErrorList { 879 allErrs := field.ErrorList{} 880 881 upstreamErrs := validateUpstreamName(name, fieldPath) 882 if len(upstreamErrs) > 0 { 883 allErrs = append(allErrs, upstreamErrs...) 884 } else if !upstreamNames.Has(name) { 885 allErrs = append(allErrs, field.NotFound(fieldPath, name)) 886 } 887 888 return allErrs 889 } 890 891 func (vsv *VirtualServerValidator) validateActionProxy(p *v1.ActionProxy, fieldPath *field.Path, upstreamNames sets.String, path string, internal bool) field.ErrorList { 892 allErrs := field.ErrorList{} 893 894 allErrs = append(allErrs, validateReferencedUpstream(p.Upstream, fieldPath.Child("upstream"), upstreamNames)...) 895 allErrs = append(allErrs, vsv.validateActionProxyRequestHeaders(p.RequestHeaders, fieldPath.Child("requestHeaders"))...) 896 allErrs = append(allErrs, vsv.validateActionProxyResponseHeaders(p.ResponseHeaders, fieldPath.Child("responseHeaders"))...) 897 898 if strings.HasPrefix(path, "~") || internal { 899 allErrs = append(allErrs, validateActionProxyRewritePathForRegexp(p.RewritePath, fieldPath.Child("rewritePath"))...) 900 } else { 901 allErrs = append(allErrs, validateActionProxyRewritePath(p.RewritePath, fieldPath.Child("rewritePath"))...) 902 } 903 904 return allErrs 905 } 906 907 func validateStringNoVariables(s string, fieldPath *field.Path) field.ErrorList { 908 allErrs := field.ErrorList{} 909 910 for i, char := range s { 911 charLen := len(string(char)) 912 if string(char) == "$" && i+charLen < len(s) { 913 if _, err := strconv.Atoi(string(s[i+charLen])); err != nil { 914 return append(allErrs, field.Invalid(fieldPath, s, "`$` character can be only followed by a number")) 915 } 916 } 917 } 918 919 return allErrs 920 } 921 922 func validateActionProxyRewritePath(rewritePath string, fieldPath *field.Path) field.ErrorList { 923 allErrs := field.ErrorList{} 924 925 if rewritePath == "" { 926 return allErrs 927 } 928 929 allErrs = append(allErrs, validateStringNoVariables(rewritePath, fieldPath)...) 930 931 return append(allErrs, validatePath(rewritePath, fieldPath)...) 932 } 933 934 func validateActionProxyRewritePathForRegexp(rewritePath string, fieldPath *field.Path) field.ErrorList { 935 allErrs := field.ErrorList{} 936 937 if rewritePath == "" { 938 return allErrs 939 } 940 941 allErrs = append(allErrs, validateStringNoVariables(rewritePath, fieldPath)...) 942 943 if !escapedStringsFmtRegexp.MatchString(rewritePath) { 944 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "/rewrite$1", "/images") 945 allErrs = append(allErrs, field.Invalid(fieldPath, rewritePath, msg)) 946 } 947 948 return allErrs 949 } 950 951 var actionProxyHeaderVariables = map[string]bool{ 952 "request_uri": true, 953 "request_method": true, 954 "request_body": true, 955 "scheme": true, 956 "args": true, 957 "host": true, 958 "request_time": true, 959 "request_length": true, 960 "nginx_version": true, 961 "pid": true, 962 "connection": true, 963 "remote_addr": true, 964 "remote_port": true, 965 "time_iso8601": true, 966 "time_local": true, 967 "server_addr": true, 968 "server_port": true, 969 "server_name": true, 970 "server_protocol": true, 971 "connections_active": true, 972 "connections_reading": true, 973 "connections_writing": true, 974 "connections_waiting": true, 975 "ssl_cipher": true, 976 "ssl_ciphers": true, 977 "ssl_client_cert": true, 978 "ssl_client_escaped_cert": true, 979 "ssl_client_fingerprint": true, 980 "ssl_client_i_dn": true, 981 "ssl_client_i_dn_legacy": true, 982 "ssl_client_raw_cert": true, 983 "ssl_client_s_dn": true, 984 "ssl_client_s_dn_legacy": true, 985 "ssl_client_serial": true, 986 "ssl_client_v_end": true, 987 "ssl_client_v_remain": true, 988 "ssl_client_v_start": true, 989 "ssl_client_verify": true, 990 "ssl_curves": true, 991 "ssl_early_data": true, 992 "ssl_protocol": true, 993 "ssl_server_name": true, 994 "ssl_session_id": true, 995 "ssl_session_reused": true, 996 } 997 998 var actionProxyHeaderSpecialVariables = []string{"arg_", "http_", "cookie_", "jwt_claim_", "jwt_header_"} 999 1000 func (vsv *VirtualServerValidator) validateActionProxyHeader(h v1.Header, fieldPath *field.Path) field.ErrorList { 1001 allErrs := field.ErrorList{} 1002 1003 if h.Name == "" { 1004 allErrs = append(allErrs, field.Required(fieldPath.Child("name"), "")) 1005 } 1006 1007 for _, msg := range validation.IsHTTPHeaderName(h.Name) { 1008 allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), h.Name, msg)) 1009 } 1010 1011 allErrs = append(allErrs, validateEscapedStringWithVariables(h.Value, fieldPath.Child("value"), 1012 actionProxyHeaderSpecialVariables, actionProxyHeaderVariables, vsv.isPlus)...) 1013 1014 return allErrs 1015 } 1016 1017 func (vsv *VirtualServerValidator) validateActionProxyRequestHeaders(requestHeaders *v1.ProxyRequestHeaders, fieldPath *field.Path) field.ErrorList { 1018 allErrs := field.ErrorList{} 1019 1020 if requestHeaders == nil { 1021 return allErrs 1022 } 1023 1024 for i, header := range requestHeaders.Set { 1025 allErrs = append(allErrs, vsv.validateActionProxyHeader(header, fieldPath.Index(i))...) 1026 } 1027 1028 return allErrs 1029 } 1030 1031 func (vsv *VirtualServerValidator) validateActionProxyResponseHeaders(responseHeaders *v1.ProxyResponseHeaders, fieldPath *field.Path) field.ErrorList { 1032 allErrs := field.ErrorList{} 1033 1034 if responseHeaders == nil { 1035 return allErrs 1036 } 1037 1038 for i, header := range responseHeaders.Hide { 1039 for _, msg := range validation.IsHTTPHeaderName(header) { 1040 allErrs = append(allErrs, field.Invalid(fieldPath.Child("hide").Index(i), header, msg)) 1041 } 1042 } 1043 1044 for i, header := range responseHeaders.Pass { 1045 for _, msg := range validation.IsHTTPHeaderName(header) { 1046 allErrs = append(allErrs, field.Invalid(fieldPath.Child("pass").Index(i), header, msg)) 1047 } 1048 } 1049 1050 for i, header := range responseHeaders.Add { 1051 allErrs = append(allErrs, vsv.validateActionProxyHeader(header.Header, fieldPath.Child("add").Index(i))...) 1052 } 1053 1054 allErrs = append(allErrs, validateIgnoreHeaders(responseHeaders.Ignore, fieldPath.Child("ignore"))...) 1055 1056 return allErrs 1057 } 1058 1059 var validIgnoreHeaders = map[string]bool{ 1060 "X-Accel-Redirect": true, 1061 "X-Accel-Expires": true, 1062 "X-Accel-Limit-Rate": true, 1063 "X-Accel-Buffering": true, 1064 "X-Accel-Charset": true, 1065 "Expires": true, 1066 "Cache-Control": true, 1067 "Set-Cookie": true, 1068 "Vary": true, 1069 } 1070 1071 func validateIgnoreHeaders(ignoreHeaders []string, fieldPath *field.Path) field.ErrorList { 1072 allErrs := field.ErrorList{} 1073 if len(ignoreHeaders) == 0 { 1074 return allErrs 1075 } 1076 1077 for i, h := range ignoreHeaders { 1078 if !validIgnoreHeaders[h] { 1079 msg := fmt.Sprintf("not a valid ignore header name. Accepted headers are : %v", mapToPrettyString(validIgnoreHeaders)) 1080 allErrs = append(allErrs, field.Invalid(fieldPath.Index(i), h, msg)) 1081 } 1082 } 1083 1084 return allErrs 1085 } 1086 1087 func (vsv *VirtualServerValidator) validateSplits(splits []v1.Split, fieldPath *field.Path, upstreamNames sets.String, path string) field.ErrorList { 1088 allErrs := field.ErrorList{} 1089 1090 if len(splits) < 2 { 1091 return append(allErrs, field.Invalid(fieldPath, "", "must include at least 2 splits")) 1092 } 1093 1094 totalWeight := 0 1095 1096 for i, s := range splits { 1097 idxPath := fieldPath.Index(i) 1098 1099 for _, msg := range validation.IsInRange(s.Weight, 1, 99) { 1100 allErrs = append(allErrs, field.Invalid(idxPath.Child("weight"), s.Weight, msg)) 1101 } 1102 1103 if s.Action == nil { 1104 allErrs = append(allErrs, field.Required(idxPath.Child("action"), "")) 1105 } else { 1106 allErrs = append(allErrs, vsv.validateAction(s.Action, idxPath.Child("action"), upstreamNames, path, true)...) 1107 } 1108 1109 totalWeight += s.Weight 1110 } 1111 1112 if totalWeight != 100 { 1113 allErrs = append(allErrs, field.Invalid(fieldPath, "", "the sum of the weights of all splits must be equal to 100")) 1114 } 1115 1116 return allErrs 1117 } 1118 1119 // We support prefix-based NGINX locations, positive case-sensitive/insensitive regular expressions matches and exact matches. 1120 // More info http://nginx.org/en/docs/http/ngx_http_core_module.html#location 1121 func validateRoutePath(path string, fieldPath *field.Path) field.ErrorList { 1122 allErrs := field.ErrorList{} 1123 1124 if path == "" { 1125 return append(allErrs, field.Required(fieldPath, "")) 1126 } 1127 1128 if strings.HasPrefix(path, "~") { 1129 allErrs = append(allErrs, validateRegexPath(path, fieldPath)...) 1130 } else if strings.HasPrefix(path, "/") { 1131 allErrs = append(allErrs, validatePath(path, fieldPath)...) 1132 } else if strings.HasPrefix(path, "=") { 1133 allErrs = append(allErrs, validatePath(strings.TrimPrefix(path, "="), fieldPath)...) 1134 } else { 1135 allErrs = append(allErrs, field.Invalid(fieldPath, path, "must start with /, ~ or =")) 1136 } 1137 1138 return allErrs 1139 } 1140 1141 func validateRegexPath(path string, fieldPath *field.Path) field.ErrorList { 1142 allErrs := field.ErrorList{} 1143 1144 if _, err := regexp.Compile(path); err != nil { 1145 return append(allErrs, field.Invalid(fieldPath, path, fmt.Sprintf("must be a valid regular expression: %v", err))) 1146 } 1147 1148 if !escapedStringsFmtRegexp.MatchString(path) { 1149 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "*.jpg", "^/images/image_*.png$") 1150 return append(allErrs, field.Invalid(fieldPath, path, msg)) 1151 } 1152 1153 return allErrs 1154 } 1155 1156 const ( 1157 pathFmt = `/[^\s{};]*` 1158 pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`" 1159 ) 1160 1161 var pathRegexp = regexp.MustCompile("^" + pathFmt + "$") 1162 1163 func validatePath(path string, fieldPath *field.Path) field.ErrorList { 1164 allErrs := field.ErrorList{} 1165 1166 if path == "" { 1167 return append(allErrs, field.Required(fieldPath, "")) 1168 } 1169 1170 if !pathRegexp.MatchString(path) { 1171 msg := validation.RegexError(pathErrMsg, pathFmt, "/", "/path", "/path/subpath-123") 1172 return append(allErrs, field.Invalid(fieldPath, path, msg)) 1173 } 1174 1175 return allErrs 1176 } 1177 1178 func (vsv *VirtualServerValidator) validateMatch(match v1.Match, fieldPath *field.Path, upstreamNames sets.String, path string) field.ErrorList { 1179 allErrs := field.ErrorList{} 1180 1181 if len(match.Conditions) == 0 { 1182 allErrs = append(allErrs, field.Required(fieldPath.Child("conditions"), "must specify at least one condition")) 1183 } else { 1184 for i, c := range match.Conditions { 1185 allErrs = append(allErrs, validateCondition(c, fieldPath.Child("conditions").Index(i))...) 1186 } 1187 } 1188 1189 fieldCount := 0 1190 1191 if match.Action != nil { 1192 allErrs = append(allErrs, vsv.validateAction(match.Action, fieldPath.Child("action"), upstreamNames, path, true)...) 1193 fieldCount++ 1194 } 1195 1196 if len(match.Splits) > 0 { 1197 allErrs = append(allErrs, vsv.validateSplits(match.Splits, fieldPath.Child("splits"), upstreamNames, path)...) 1198 fieldCount++ 1199 } 1200 1201 if fieldCount != 1 { 1202 allErrs = append(allErrs, field.Invalid(fieldPath, "", "must specify exactly one of `action` or `splits`")) 1203 } 1204 1205 return allErrs 1206 } 1207 1208 func validateCondition(condition v1.Condition, fieldPath *field.Path) field.ErrorList { 1209 allErrs := field.ErrorList{} 1210 1211 fieldCount := 0 1212 1213 if condition.Header != "" { 1214 for _, msg := range validation.IsHTTPHeaderName(condition.Header) { 1215 allErrs = append(allErrs, field.Invalid(fieldPath.Child("header"), condition.Header, msg)) 1216 } 1217 fieldCount++ 1218 } 1219 1220 if condition.Cookie != "" { 1221 for _, msg := range isCookieName(condition.Cookie) { 1222 allErrs = append(allErrs, field.Invalid(fieldPath.Child("cookie"), condition.Cookie, msg)) 1223 } 1224 fieldCount++ 1225 } 1226 1227 if condition.Argument != "" { 1228 for _, msg := range isArgumentName(condition.Argument) { 1229 allErrs = append(allErrs, field.Invalid(fieldPath.Child("argument"), condition.Argument, msg)) 1230 } 1231 fieldCount++ 1232 } 1233 1234 if condition.Variable != "" { 1235 allErrs = append(allErrs, validateVariableName(condition.Variable, fieldPath.Child("variable"))...) 1236 fieldCount++ 1237 } 1238 1239 if fieldCount != 1 { 1240 allErrs = append(allErrs, field.Invalid(fieldPath, "", "must specify exactly one of: `header`, `cookie`, `argument` or `variable`")) 1241 } 1242 1243 for _, msg := range isValidMatchValue(condition.Value) { 1244 allErrs = append(allErrs, field.Invalid(fieldPath.Child("value"), condition.Value, msg)) 1245 } 1246 1247 return allErrs 1248 } 1249 1250 const ( 1251 cookieNameFmt string = "[_A-Za-z0-9]+" 1252 cookieNameErrMsg string = "a valid cookie name must consist of alphanumeric characters or '_'" 1253 ) 1254 1255 var cookieNameRegexp = regexp.MustCompile("^" + cookieNameFmt + "$") 1256 1257 func isCookieName(value string) []string { 1258 if !cookieNameRegexp.MatchString(value) { 1259 return []string{validation.RegexError(cookieNameErrMsg, cookieNameFmt, "my_cookie_123")} 1260 } 1261 return nil 1262 } 1263 1264 const ( 1265 argumentNameFmt string = "[_A-Za-z0-9]+" 1266 argumentNameErrMsg string = "a valid argument name must consist of alphanumeric characters or '_'" 1267 ) 1268 1269 var argumentNameRegexp = regexp.MustCompile("^" + argumentNameFmt + "$") 1270 1271 func isArgumentName(value string) []string { 1272 if !argumentNameRegexp.MatchString(value) { 1273 return []string{validation.RegexError(argumentNameErrMsg, argumentNameFmt, "argument_123")} 1274 } 1275 return nil 1276 } 1277 1278 // validVariableNames includes NGINX variables allowed to be used in conditions. 1279 // Not all NGINX variables are allowed. The full list of NGINX variables is at https://nginx.org/en/docs/varindex.html 1280 var validVariableNames = map[string]bool{ 1281 "$args": true, 1282 "$http2": true, 1283 "$https": true, 1284 "$remote_addr": true, 1285 "$remote_port": true, 1286 "$query_string": true, 1287 "$request": true, 1288 "$request_body": true, 1289 "$request_uri": true, 1290 "$request_method": true, 1291 "$scheme": true, 1292 } 1293 1294 func validateVariableName(name string, fieldPath *field.Path) field.ErrorList { 1295 allErrs := field.ErrorList{} 1296 1297 if !strings.HasPrefix(name, "$") { 1298 return append(allErrs, field.Invalid(fieldPath, name, "must start with `$`")) 1299 } 1300 1301 if _, exists := validVariableNames[name]; !exists { 1302 return append(allErrs, field.Invalid(fieldPath, name, "is not allowed or is not an NGINX variable")) 1303 } 1304 1305 return allErrs 1306 } 1307 1308 func isValidMatchValue(value string) []string { 1309 if !escapedStringsFmtRegexp.MatchString(value) { 1310 return []string{validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "value-123")} 1311 } 1312 return nil 1313 } 1314 1315 // ValidateVirtualServerRoute validates a VirtualServerRoute. 1316 func (vsv *VirtualServerValidator) ValidateVirtualServerRoute(virtualServerRoute *v1.VirtualServerRoute) error { 1317 allErrs := vsv.validateVirtualServerRouteSpec(&virtualServerRoute.Spec, field.NewPath("spec"), "", "/", virtualServerRoute.Namespace) 1318 return allErrs.ToAggregate() 1319 } 1320 1321 // ValidateVirtualServerRouteForVirtualServer validates a VirtualServerRoute for a VirtualServer represented by its host and path prefix. 1322 func (vsv *VirtualServerValidator) ValidateVirtualServerRouteForVirtualServer(virtualServerRoute *v1.VirtualServerRoute, virtualServerHost string, vsPath string) error { 1323 allErrs := vsv.validateVirtualServerRouteSpec(&virtualServerRoute.Spec, field.NewPath("spec"), virtualServerHost, vsPath, 1324 virtualServerRoute.Namespace) 1325 return allErrs.ToAggregate() 1326 } 1327 1328 func (vsv *VirtualServerValidator) validateVirtualServerRouteSpec(spec *v1.VirtualServerRouteSpec, fieldPath *field.Path, virtualServerHost string, vsPath string, 1329 namespace string) field.ErrorList { 1330 allErrs := field.ErrorList{} 1331 1332 allErrs = append(allErrs, validateVirtualServerRouteHost(spec.Host, virtualServerHost, fieldPath.Child("host"))...) 1333 1334 upstreamErrs, upstreamNames := vsv.validateUpstreams(spec.Upstreams, fieldPath.Child("upstreams")) 1335 allErrs = append(allErrs, upstreamErrs...) 1336 1337 allErrs = append(allErrs, vsv.validateVirtualServerRouteSubroutes(spec.Subroutes, fieldPath.Child("subroutes"), upstreamNames, vsPath, namespace)...) 1338 1339 return allErrs 1340 } 1341 1342 func validateVirtualServerRouteHost(host string, virtualServerHost string, fieldPath *field.Path) field.ErrorList { 1343 allErrs := field.ErrorList{} 1344 1345 allErrs = append(allErrs, validateHost(host, fieldPath)...) 1346 1347 if virtualServerHost != "" && host != virtualServerHost { 1348 msg := fmt.Sprintf("must be equal to '%s'", virtualServerHost) 1349 allErrs = append(allErrs, field.Invalid(fieldPath, host, msg)) 1350 } 1351 1352 return allErrs 1353 } 1354 1355 func isRegexOrExactMatch(path string) bool { 1356 return strings.HasPrefix(path, "~") || strings.HasPrefix(path, "=") 1357 } 1358 1359 func (vsv *VirtualServerValidator) validateVirtualServerRouteSubroutes(routes []v1.Route, fieldPath *field.Path, upstreamNames sets.String, vsPath string, namespace string) field.ErrorList { 1360 allErrs := field.ErrorList{} 1361 1362 allPaths := sets.String{} 1363 1364 if isRegexOrExactMatch(vsPath) { 1365 if len(routes) != 1 { 1366 return append(allErrs, field.Invalid(fieldPath, "subroutes", "must have only one subroute if regex match or exact match are being used")) 1367 } 1368 1369 idxPath := fieldPath.Index(0) 1370 if routes[0].Path != vsPath { 1371 return append(allErrs, field.Invalid(idxPath.Child("path"), routes[0].Path, "must have the same path as the referenced VirtualServer route path")) 1372 } 1373 1374 return vsv.validateRoute(routes[0], idxPath, upstreamNames, true, namespace) 1375 } 1376 1377 for i, r := range routes { 1378 idxPath := fieldPath.Index(i) 1379 1380 isRouteFieldForbidden := true 1381 routeErrs := vsv.validateRoute(r, idxPath, upstreamNames, isRouteFieldForbidden, namespace) 1382 1383 if vsPath != "" && !strings.HasPrefix(r.Path, vsPath) && !isRegexOrExactMatch(r.Path) { 1384 msg := fmt.Sprintf("must start with '%s'", vsPath) 1385 routeErrs = append(routeErrs, field.Invalid(idxPath, r.Path, msg)) 1386 } 1387 1388 if len(routeErrs) > 0 { 1389 allErrs = append(allErrs, routeErrs...) 1390 } else if allPaths.Has(r.Path) { 1391 allErrs = append(allErrs, field.Duplicate(idxPath.Child("path"), r.Path)) 1392 } else { 1393 allPaths.Insert(r.Path) 1394 } 1395 } 1396 1397 return allErrs 1398 } 1399 1400 func rejectPlusResourcesInOSS(upstream v1.Upstream, idxPath *field.Path, isPlus bool) field.ErrorList { 1401 allErrs := field.ErrorList{} 1402 1403 if isPlus { 1404 return allErrs 1405 } 1406 1407 if upstream.HealthCheck != nil { 1408 allErrs = append(allErrs, field.Forbidden(idxPath.Child("healthCheck"), "active health checks are only supported in NGINX Plus")) 1409 } 1410 1411 if upstream.SlowStart != "" { 1412 allErrs = append(allErrs, field.Forbidden(idxPath.Child("slow-start"), "slow start is only supported in NGINX Plus")) 1413 } 1414 1415 if upstream.SessionCookie != nil { 1416 allErrs = append(allErrs, field.Forbidden(idxPath.Child("sessionCookie"), "sticky cookies are only supported in NGINX Plus")) 1417 } 1418 1419 if upstream.Queue != nil { 1420 allErrs = append(allErrs, field.Forbidden(idxPath.Child("queue"), "queue is only supported in NGINX Plus")) 1421 } 1422 1423 return allErrs 1424 } 1425 1426 func validateQueue(queue *v1.UpstreamQueue, fieldPath *field.Path) field.ErrorList { 1427 allErrs := field.ErrorList{} 1428 1429 if queue == nil { 1430 return allErrs 1431 } 1432 1433 allErrs = append(allErrs, validateTime(queue.Timeout, fieldPath.Child("timeout"))...) 1434 if queue.Size <= 0 { 1435 allErrs = append(allErrs, field.Required(fieldPath.Child("size"), "must be positive")) 1436 } 1437 1438 return allErrs 1439 } 1440 1441 // isValidLabelName checks if a label name is valid. 1442 // It performs the same validation as ValidateLabelName from k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go. 1443 func isValidLabelName(labelName string, fieldPath *field.Path) field.ErrorList { 1444 allErrs := field.ErrorList{} 1445 1446 for _, msg := range validation.IsQualifiedName(labelName) { 1447 allErrs = append(allErrs, field.Invalid(fieldPath, labelName, msg)) 1448 } 1449 1450 return allErrs 1451 } 1452 1453 // validateLabels validates that a set of labels are correctly defined. 1454 // It performs the same validation as ValidateLabels from k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go. 1455 func validateLabels(labels map[string]string, fieldPath *field.Path) field.ErrorList { 1456 allErrs := field.ErrorList{} 1457 1458 for labelName, labelValue := range labels { 1459 allErrs = append(allErrs, isValidLabelName(labelName, fieldPath)...) 1460 for _, msg := range validation.IsValidLabelValue(labelValue) { 1461 allErrs = append(allErrs, field.Invalid(fieldPath, labelValue, msg)) 1462 } 1463 } 1464 1465 return allErrs 1466 }