Skip to content

Commit

Permalink
cors ir and xds translation
Browse files Browse the repository at this point in the history
Signed-off-by: huabing zhao <[email protected]>
  • Loading branch information
zhaohuabing committed Oct 19, 2023
1 parent d3110e3 commit 6711663
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 0 deletions.
45 changes: 45 additions & 0 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ var (
ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)")
ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)")
ErrRequestAuthenRequiresJwt = errors.New("jwt field is required when request authentication is set")
ErrCorsAllowOriginsEmpty = errors.New("field AllowOrigins must be specified with at least a single origin entry")
ErrCorsAllowMethodsEmpty = errors.New("field AllowMethods must be specified with at least a single method entry")
)

// Xds holds the intermediate representation of a Gateway and is
Expand Down Expand Up @@ -276,6 +278,8 @@ type HTTPRoute struct {
RequestAuthentication *RequestAuthentication `json:"requestAuthentication,omitempty" yaml:"requestAuthentication,omitempty"`
// Timeout is the time until which entire response is received from the upstream.
Timeout *metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
// Cors policy for the route.
CorsPolicy *CorsPolicy `json:"corsPolicy,omitempty" yaml:"corsPolicy,omitempty"`
// ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters
ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"`
}
Expand Down Expand Up @@ -309,6 +313,42 @@ type JwtRequestAuthentication struct {
Providers []egv1a1.JwtAuthenticationFilterProvider `json:"providers,omitempty" yaml:"providers,omitempty"`
}

// CorsPolicy holds the Cross-Origin Resource Sharing (CORS) policy for the route.
//
// +k8s:deepcopy-gen=true
type CorsPolicy struct {
// AllowOrigins defines the origins that are allowed to make requests.
AllowOrigins []*StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"`
// AllowMethods defines the methods that are allowed to make requests.
AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods,omitempty"`
// AllowHeaders defines the headers that are allowed to be sent with requests.
AllowedHeaders []string `json:"allowedHeaders,omitempty" yaml:"allowedHeaders,omitempty"`
// ExposeHeaders defines the headers that can be exposed in the responses.
ExposedHeaders []string `json:"exposedHeaders,omitempty" yaml:"exposedHeaders,omitempty"`
// MaxAge defines how long the results of a preflight request can be cached.
MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"`
// AllowCredentials defines whether the resource allows credentials.
// Defaults to false.
AllowCredentials bool `json:"allowCredentials,omitempty" yaml:"allowCredentials,omitempty"`
// AllowPrivateNetwork defines whether allow whose target server’s IP address
// is more private than that from which the request initiator was fetched.
// Defaults to false.
AllowPrivateNetworkAccess bool `json:"allowPrivateNetwork,omitempty" yaml:"allowPrivateNetwork,omitempty"`
}

func (c *CorsPolicy) Validate() error {
var errs error

if len(c.AllowOrigins) == 0 {
errs = multierror.Append(errs, ErrCorsAllowOriginsEmpty)
}
if len(c.AllowMethods) == 0 {
errs = multierror.Append(errs, ErrCorsAllowMethodsEmpty)
}

return errs
}

// Validate the fields within the HTTPRoute structure
func (h HTTPRoute) Validate() error {
var errs error
Expand Down Expand Up @@ -420,6 +460,11 @@ func (h HTTPRoute) Validate() error {
}
}
}
if h.CorsPolicy != nil {
if err := h.CorsPolicy.Validate(); err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}

Expand Down
181 changes: 181 additions & 0 deletions internal/xds/translator/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package translator

import (
"errors"
"fmt"
"strconv"
"strings"

routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
corsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/protobuf/types/known/anypb"

"github.com/envoyproxy/gateway/internal/ir"
)

const (
corsFilter = "envoy.filters.http.cors"
)

// patchHCMWithCorsFilter builds and appends the Cors Filter to the HTTP
// Connection Manager if applicable, and it does not already exist.
func patchHCMWithCorsFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
if mgr == nil {
return errors.New("hcm is nil")
}

if irListener == nil {
return errors.New("ir listener is nil")
}

if !listenerContainsCorsPolicy(irListener) {
return nil
}

// Return early if filter already exists.
for _, httpFilter := range mgr.HttpFilters {
if httpFilter.Name == corsFilter {
return nil
}
}

corsFilter, err := buildHCMCorsFilter(irListener)
if err != nil {
return err
}

// Ensure the cors filter is the first one in the chain.
mgr.HttpFilters = append([]*hcmv3.HttpFilter{corsFilter}, mgr.HttpFilters...)

return nil
}

// buildHCMCorsFilter returns a Cors filter from the provided IR listener.
func buildHCMCorsFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) {

Check failure on line 62 in internal/xds/translator/cors.go

View workflow job for this annotation

GitHub Actions / lint

`buildHCMCorsFilter` - `irListener` is unused (unparam)
corsProto := &corsv3.Cors{}

corsAny, err := anypb.New(corsProto)
if err != nil {
return nil, err
}

return &hcmv3.HttpFilter{
Name: corsFilter,
ConfigType: &hcmv3.HttpFilter_TypedConfig{
TypedConfig: corsAny,
},
}, nil
}

// listenerContainsCorsPolicy returns true if the provided listener has Cors
// policies attached to its routes.
func listenerContainsCorsPolicy(irListener *ir.HTTPListener) bool {
if irListener == nil {
return false
}

for _, route := range irListener.Routes {
if route.CorsPolicy != nil {
return true
}
}

return false
}

// patchRouteWithCorsConfig patches the provided route with the Cors config if
// applicable.
func patchRouteWithCorsConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error {
if route == nil {
return errors.New("xds route is nil")
}
if irRoute == nil {
return errors.New("ir route is nil")
}
if irRoute.CorsPolicy == nil {
return nil
}

filterCfg := route.GetTypedPerFilterConfig()
if _, ok := filterCfg[corsFilter]; ok {
return fmt.Errorf("route already contains cors config: %+v", route)
}

var (
allowOrigins []*matcherv3.StringMatcher
allowMethods string
allowHeaders string
exposeHeaders string
maxAge string
allowCredentials *wrappers.BoolValue
allowPrivateNetworkAccess *wrappers.BoolValue
)

for _, origin := range irRoute.CorsPolicy.AllowOrigins {
if origin.Exact != nil {

Check failure on line 123 in internal/xds/translator/cors.go

View workflow job for this annotation

GitHub Actions / lint

ifElseChain: rewrite if-else to switch statement (gocritic)
allowOrigins = append(allowOrigins, &matcherv3.StringMatcher{
MatchPattern: &matcherv3.StringMatcher_Exact{
Exact: *origin.Exact,
},
})
} else if origin.Prefix != nil {
allowOrigins = append(allowOrigins, &matcherv3.StringMatcher{
MatchPattern: &matcherv3.StringMatcher_Prefix{
Prefix: *origin.Prefix,
},
})
} else if origin.SafeRegex != nil {
allowOrigins = append(allowOrigins, &matcherv3.StringMatcher{
MatchPattern: &matcherv3.StringMatcher_SafeRegex{
SafeRegex: &matcherv3.RegexMatcher{
Regex: *origin.SafeRegex,
},
},
})
} else if origin.Suffix != nil {
allowOrigins = append(allowOrigins, &matcherv3.StringMatcher{
MatchPattern: &matcherv3.StringMatcher_Suffix{
Suffix: *origin.Suffix,
},
})
}
}

allowMethods = strings.Join(irRoute.CorsPolicy.AllowMethods, " ,")
allowHeaders = strings.Join(irRoute.CorsPolicy.AllowedHeaders, " ,")
exposeHeaders = strings.Join(irRoute.CorsPolicy.ExposedHeaders, " ,")
maxAge = strconv.Itoa(int(irRoute.CorsPolicy.MaxAge.Seconds()))
allowPrivateNetworkAccess = &wrappers.BoolValue{Value: irRoute.CorsPolicy.AllowPrivateNetworkAccess}
allowCredentials = &wrappers.BoolValue{Value: irRoute.CorsPolicy.AllowCredentials}

routeCfgProto := &corsv3.CorsPolicy{
AllowOriginStringMatch: allowOrigins,
AllowMethods: allowMethods,
AllowHeaders: allowHeaders,
ExposeHeaders: exposeHeaders,
MaxAge: maxAge,
AllowCredentials: allowCredentials,
AllowPrivateNetworkAccess: allowPrivateNetworkAccess,
}

routeCfgAny, err := anypb.New(routeCfgProto)
if err != nil {
return err
}

if filterCfg == nil {
route.TypedPerFilterConfig = make(map[string]*anypb.Any)
}

route.TypedPerFilterConfig[corsFilter] = routeCfgAny

return nil
}
5 changes: 5 additions & 0 deletions internal/xds/translator/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ func (t *Translator) addXdsHTTPFilterChain(xdsListener *listenerv3.Listener, irL
return err
}

// Add the cors filter, if needed
if err := patchHCMWithCorsFilter(mgr, irListener); err != nil {
return err
}

// Make sure the router filter is the last one.
mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter)
mgrAny, err := protocov.ToAnyWithError(mgr)
Expand Down
5 changes: 5 additions & 0 deletions internal/xds/translator/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) *routev3.Route {
return nil
}

// Add the cors per route config to the route, if needed.
if err := patchRouteWithCorsConfig(router, httpRoute); err != nil {
return nil
}

return router
}

Expand Down

0 comments on commit 6711663

Please sign in to comment.