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  }