Skip to content

Commit

Permalink
Enable CorsPolicy
Browse files Browse the repository at this point in the history
 knative-sandbox/net-istio/issues#389
knative/serving/issues/10040
  • Loading branch information
zhaojizhuang committed Nov 9, 2020
1 parent 40b5992 commit 4d5b175
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 9 deletions.
50 changes: 50 additions & 0 deletions docs/annotations.md
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`
109 changes: 109 additions & 0 deletions pkg/reconciler/ingress/annotations/cors/cors.go
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
}
175 changes: 175 additions & 0 deletions pkg/reconciler/ingress/annotations/parser/parse.go
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
}
Loading

0 comments on commit 4d5b175

Please sign in to comment.