forked from knative-extensions/net-istio
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
knative-sandbox/net-istio/issues#389 knative/serving/issues/10040
- Loading branch information
1 parent
40b5992
commit 4d5b175
Showing
5 changed files
with
397 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Annotations | ||
|
||
You can add these Kubernetes annotations to specific `ksvc` or `Route` objects to customize their behavior. | ||
|
||
Annotation keys and values can only be strings. Other types, such as boolean or numeric values must be quoted,i.e. `"true"`, `"false"`, `"100"`. | ||
|
||
The annotation prefix must be `` | ||
|
||
### Enable CORS | ||
|
||
To enable Cross-Origin Resource Sharing (CORS) in an KIngress rule, add the annotation | ||
`istio.ingress.networking.knative.dev/enable-cors: "true"` to `ksvc` or `Route`. This will add a section in the server | ||
location enabling this functionality. | ||
|
||
|
||
CORS can be controlled with the following annotations: | ||
|
||
* `istio.ingress.networking.knative.dev/cors-allow-methods` | ||
controls which methods are accepted. This is a multi-valued field, separated by ',' and | ||
accepts only letters (upper and lower case). | ||
- Default: `GET, PUT, POST, DELETE, PATCH, OPTIONS` | ||
- Example: `istio.ingress.networking.knative.dev/cors-allow-methods: "PUT, GET, POST, OPTIONS"` | ||
|
||
* `istio.ingress.networking.knative.dev/cors-allow-headers` | ||
controls which headers are accepted. This is a multi-valued field, separated by ',' and accepts letters, | ||
numbers, _ and -. | ||
- Default: `DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization` | ||
- Example: `istio.ingress.networking.knative.dev/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"` | ||
|
||
* `istio.ingress.networking.knative.dev/cors-expose-headers` | ||
controls which headers are exposed to response. This is a multi-valued field, separated by ',' and accepts | ||
letters, numbers, _, - and *. | ||
- Default: *empty* | ||
- Example: `istio.ingress.networking.knative.dev/cors-expose-headers: "*, X-CustomResponseHeader"` | ||
|
||
* `istio.ingress.networking.knative.dev/cors-allow-origin` | ||
controls what's the accepted Origin for CORS. | ||
This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` | ||
- Default: `*` | ||
- Example: `istio.ingress.networking.knative.dev/cors-allow-origin: "https://origin-site.com:4443"` | ||
|
||
* `istio.ingress.networking.knative.dev/cors-allow-credentials` | ||
controls if credentials can be passed during CORS operations. | ||
- Default: `true` | ||
- Example: `istio.ingress.networking.knative.dev/cors-allow-credentials: "false"` | ||
|
||
* `istio.ingress.networking.knative.dev/cors-max-age` | ||
controls how long preflight requests can be cached. | ||
Default: `1728000` | ||
Example: `istio.ingress.networking.knative.dev/cors-max-age: 600` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/* | ||
Copyright 2020 The Knative Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package cors | ||
|
||
import ( | ||
"regexp" | ||
|
||
"knative.dev/net-istio/pkg/reconciler/ingress/annotations/parser" | ||
"knative.dev/networking/pkg/apis/networking/v1alpha1" | ||
) | ||
|
||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller | ||
const DefaultAnnotationsPrefix = "istio.ingress.networking.knative.dev" | ||
|
||
const ( | ||
// Default values | ||
DefaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS" | ||
DefaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" | ||
DefaultCorsMaxAge = 1728000 | ||
) | ||
|
||
var ( | ||
// Regex are defined here to prevent information leak, if user tries to set anything not valid | ||
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc) | ||
// Origin must contain a http/s Origin (including or not the port) or the value '*' | ||
corsOriginRegex = regexp.MustCompile(`^(https?://[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`) | ||
// Method must contain valid methods list (PUT, GET, POST, BLA) | ||
// May contain or not spaces between each verb | ||
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`) | ||
// Headers must contain valid values only (X-HEADER12, X-ABC) | ||
// May contain or not spaces between each Header | ||
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`) | ||
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC) | ||
// May contain or not spaces between each Header | ||
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`) | ||
) | ||
|
||
// Config contains the Cors configuration to be used in the Ingress | ||
type Config struct { | ||
CorsEnabled bool `json:"corsEnabled"` | ||
CorsAllowOrigins string `json:"corsAllowOrigins"` | ||
CorsAllowMethods string `json:"corsAllowMethods"` | ||
CorsAllowHeaders string `json:"corsAllowHeaders"` | ||
CorsAllowCredentials bool `json:"corsAllowCredentials"` | ||
CorsExposeHeaders string `json:"corsExposeHeaders"` | ||
CorsMaxAge int `json:"corsMaxAge"` | ||
} | ||
|
||
// Parse parses the annotations contained in the Kingress | ||
// rule used to indicate if the location/s should allows CORS | ||
func Parse(ing *v1alpha1.Ingress) *Config { | ||
var err error | ||
config := &Config{} | ||
annotations := ing.GetAnnotations() | ||
if len(annotations) == 0 { | ||
return config | ||
} | ||
|
||
config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing) | ||
if err != nil { | ||
config.CorsEnabled = false | ||
} | ||
|
||
config.CorsAllowOrigins, err = parser.GetStringAnnotation("cors-allow-origin", ing) | ||
if err != nil || !corsOriginRegex.MatchString(config.CorsAllowOrigins) { | ||
config.CorsAllowOrigins = "*" | ||
} | ||
|
||
config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing) | ||
if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) { | ||
config.CorsAllowHeaders = DefaultCorsHeaders | ||
} | ||
|
||
config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing) | ||
if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) { | ||
config.CorsAllowMethods = DefaultCorsMethods | ||
} | ||
|
||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing) | ||
if err != nil { | ||
config.CorsAllowCredentials = true | ||
} | ||
|
||
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing) | ||
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) { | ||
config.CorsExposeHeaders = "" | ||
} | ||
|
||
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing) | ||
if err != nil { | ||
config.CorsMaxAge = DefaultCorsMaxAge | ||
} | ||
|
||
return config | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
/* | ||
Copyright 2020 The Knative Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package parser | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
|
||
"k8s.io/apimachinery/pkg/util/sets" | ||
"knative.dev/networking/pkg/apis/networking/v1alpha1" | ||
) | ||
|
||
// DefaultAnnotationsPrefix defines the common prefix used in the net-istio controller | ||
const DefaultAnnotationsPrefix = "istio.ingress.networking.knative.dev" | ||
|
||
var ( | ||
// AnnotationsPrefix is the mutable attribute that the controller explicitly refers to | ||
AnnotationsPrefix = DefaultAnnotationsPrefix | ||
) | ||
|
||
// IngressAnnotation has a method to parser annotations located in Ingress | ||
type IngressAnnotation interface { | ||
Parse(ing *v1alpha1.Ingress) (interface{}, error) | ||
} | ||
|
||
type ingAnnotations map[string]string | ||
|
||
func (a ingAnnotations) parseBool(name string) (bool, error) { | ||
val, ok := a[name] | ||
if ok { | ||
b, err := strconv.ParseBool(val) | ||
if err != nil { | ||
return false, fmt.Errorf("InvalidAnnotationContent %s:%s", name, val) | ||
} | ||
return b, nil | ||
} | ||
return false, fmt.Errorf("ErrMissingAnnotations") | ||
} | ||
|
||
func (a ingAnnotations) parseString(name string) (string, error) { | ||
val, ok := a[name] | ||
if ok { | ||
s := normalizeString(val) | ||
if len(s) == 0 { | ||
return "", fmt.Errorf("InvalidAnnotationContent %s:%s", name, val) | ||
} | ||
|
||
return s, nil | ||
} | ||
return "", fmt.Errorf("ErrMissingAnnotations") | ||
} | ||
|
||
func (a ingAnnotations) parseInt(name string) (int, error) { | ||
val, ok := a[name] | ||
if ok { | ||
i, err := strconv.Atoi(val) | ||
if err != nil { | ||
return 0, fmt.Errorf("InvalidAnnotationContent %s:%s", name, val) | ||
} | ||
return i, nil | ||
} | ||
return 0, fmt.Errorf("ErrMissingAnnotations") | ||
} | ||
|
||
func checkAnnotation(name string, ing *v1alpha1.Ingress) error { | ||
if ing == nil || len(ing.GetAnnotations()) == 0 { | ||
return fmt.Errorf("ErrMissingAnnotations") | ||
} | ||
if name == "" { | ||
return fmt.Errorf("ErrInvalidAnnotationName") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// GetBoolAnnotation extracts a boolean from an Ingress annotation | ||
func GetBoolAnnotation(name string, ing *v1alpha1.Ingress) (bool, error) { | ||
v := GetAnnotationWithPrefix(name) | ||
err := checkAnnotation(v, ing) | ||
if err != nil { | ||
return false, err | ||
} | ||
return ingAnnotations(ing.GetAnnotations()).parseBool(v) | ||
} | ||
|
||
// GetStringAnnotation extracts a string from an Ingress annotation | ||
func GetStringAnnotation(name string, ing *v1alpha1.Ingress) (string, error) { | ||
v := GetAnnotationWithPrefix(name) | ||
err := checkAnnotation(v, ing) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return ingAnnotations(ing.GetAnnotations()).parseString(v) | ||
} | ||
|
||
// GetIntAnnotation extracts an int from an Ingress annotation | ||
func GetIntAnnotation(name string, ing *v1alpha1.Ingress) (int, error) { | ||
v := GetAnnotationWithPrefix(name) | ||
err := checkAnnotation(v, ing) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return ingAnnotations(ing.GetAnnotations()).parseInt(v) | ||
} | ||
|
||
// GetAnnotationWithPrefix returns the prefix of ingress annotations | ||
func GetAnnotationWithPrefix(suffix string) string { | ||
return fmt.Sprintf("%v/%v", AnnotationsPrefix, suffix) | ||
} | ||
|
||
func normalizeString(input string) string { | ||
trimmedContent := []string{} | ||
for _, line := range strings.Split(input, "\n") { | ||
trimmedContent = append(trimmedContent, strings.TrimSpace(line)) | ||
} | ||
|
||
return strings.Join(trimmedContent, "\n") | ||
} | ||
|
||
var configmapAnnotations = sets.NewString( | ||
"auth-proxy-set-header", | ||
"fastcgi-params-configmap", | ||
) | ||
|
||
// AnnotationsReferencesConfigmap checks if at least one annotation in the Ingress rule | ||
// references a configmap. | ||
func AnnotationsReferencesConfigmap(ing *v1alpha1.Ingress) bool { | ||
if ing == nil || len(ing.GetAnnotations()) == 0 { | ||
return false | ||
} | ||
|
||
for name := range ing.GetAnnotations() { | ||
if configmapAnnotations.Has(name) { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// StringToURL parses the provided string into URL and returns error | ||
// message in case of failure | ||
func StringToURL(input string) (*url.URL, error) { | ||
parsedURL, err := url.Parse(input) | ||
if err != nil { | ||
return nil, fmt.Errorf("%v is not a valid URL: %v", input, err) | ||
} | ||
|
||
if parsedURL.Scheme == "" { | ||
return nil, fmt.Errorf("url scheme is empty") | ||
} else if parsedURL.Host == "" { | ||
return nil, fmt.Errorf("url host is empty") | ||
} else if strings.Contains(parsedURL.Host, "..") { | ||
return nil, fmt.Errorf("invalid url host") | ||
} | ||
|
||
return parsedURL, nil | ||
} |
Oops, something went wrong.