Skip to content

Commit

Permalink
feat: CORS ir and xds translation (envoyproxy#2016)
Browse files Browse the repository at this point in the history
* cors ir and xds translation

Signed-off-by: huabing zhao <[email protected]>

sort HTTP filters

Signed-off-by: huabing zhao <[email protected]>

address comments

Signed-off-by: huabing zhao <[email protected]>

refactor route filter config

Signed-off-by: huabing zhao <[email protected]>

address comments

Signed-off-by: huabing zhao <[email protected]>

* Remove AllowPrivateNetworkAccess since it's not a common knob

Signed-off-by: huabing zhao <[email protected]>

---------

Signed-off-by: huabing zhao <[email protected]>
  • Loading branch information
zhaohuabing authored Oct 22, 2023
1 parent c12c14c commit c1d03de
Show file tree
Hide file tree
Showing 13 changed files with 540 additions and 16 deletions.
18 changes: 18 additions & 0 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ type HTTPRoute struct {
Timeout *metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
// load balancer policy to use when routing to the backend endpoints.
LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"`
// Cors policy for the route.
Cors *Cors `json:"cors,omitempty" yaml:"cors,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 @@ -312,6 +314,22 @@ type JwtRequestAuthentication struct {
Providers []egv1a1.JwtAuthenticationFilterProvider `json:"providers,omitempty" yaml:"providers,omitempty"`
}

// Cors holds the Cross-Origin Resource Sharing (CORS) policy for the route.
//
// +k8s:deepcopy-gen=true
type Cors 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.
AllowHeaders []string `json:"allowHeaders,omitempty" yaml:"allowHeaders,omitempty"`
// ExposeHeaders defines the headers that can be exposed in the responses.
ExposeHeaders []string `json:"exposeHeaders,omitempty" yaml:"exposeHeaders,omitempty"`
// MaxAge defines how long the results of a preflight request can be cached.
MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"`
}

// Validate the fields within the HTTPRoute structure
func (h HTTPRoute) Validate() error {
var errs error
Expand Down
51 changes: 51 additions & 0 deletions internal/ir/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

152 changes: 152 additions & 0 deletions internal/xds/translator/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/protobuf/types/known/anypb"

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

// patchHCMWithCorsFilter builds and appends the Cors Filter to the HTTP
// Connection Manager if applicable.
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 !listenerContainsCors(irListener) {
return nil
}

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

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

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

return nil
}

// buildHCMCorsFilter returns a Cors filter from the provided IR listener.
func buildHCMCorsFilter() (*hcmv3.HttpFilter, error) {
corsProto := &corsv3.Cors{}

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

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

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

for _, route := range irListener.Routes {
if route.Cors != 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.Cors == nil {
return nil
}

filterCfg := route.GetTypedPerFilterConfig()
if _, ok := filterCfg[wellknown.CORS]; ok {
// This should not happen since this is the only place where the cors
// filter is added in a route.
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
)

//nolint:gocritic

for _, origin := range irRoute.Cors.AllowOrigins {
allowOrigins = append(allowOrigins, buildXdsStringMatcher(origin))
}

allowMethods = strings.Join(irRoute.Cors.AllowMethods, ", ")
allowHeaders = strings.Join(irRoute.Cors.AllowHeaders, ", ")
exposeHeaders = strings.Join(irRoute.Cors.ExposeHeaders, ", ")
maxAge = strconv.Itoa(int(irRoute.Cors.MaxAge.Seconds()))

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

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

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

route.TypedPerFilterConfig[wellknown.CORS] = routeCfgAny

return nil
}
142 changes: 142 additions & 0 deletions internal/xds/translator/httpfilters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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 (
"sort"

routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"

"github.com/envoyproxy/gateway/internal/ir"
xdsfilters "github.com/envoyproxy/gateway/internal/xds/filters"
)

type OrderedHTTPFilter struct {
filter *hcmv3.HttpFilter
order int
}

type OrderedHTTPFilters []*OrderedHTTPFilter

// newOrderedHTTPFilter gives each HTTP filter a rational order.
// This is needed because the order of the filters is important.
// For example, the cors filter should be put at the first to avoid unnecessary
// processing of other filters for unauthorized cross-region access.
// The router filter must be the last one since it's a terminal filter.
//
// Important: please modify this method and set the order for the new filter
// when adding a new filter in the HCM filter chain.
// If the order is not explicitly specified in this method, a filter will be set
// a default order 50.
func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter {
order := 50

// Set a rational order for all the filters.
switch filter.Name {
case wellknown.CORS:
order = 1
case jwtAuthenFilter:
order = 2
case wellknown.HTTPRateLimit:
order = 3
case wellknown.Router:
order = 100
}

return &OrderedHTTPFilter{
filter: filter,
order: order,
}
}

// sort.Interface implementation.
func (o OrderedHTTPFilters) Len() int {
return len(o)
}

func (o OrderedHTTPFilters) Less(i, j int) bool {
return o[i].order < o[j].order
}

func (o OrderedHTTPFilters) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}

// sortHTTPFilters sorts the HTTP filters in the correct order.
// This is needed because the order of the filters is important.
// For example, the cors filter should be put at the first to avoid unnecessary
// processing of other filters for unauthorized cross-region access.
// The router filter must be the last one since it's a terminal filter.
func sortHTTPFilters(filters []*hcmv3.HttpFilter) []*hcmv3.HttpFilter {
orderedFilters := make(OrderedHTTPFilters, len(filters))
for i := 0; i < len(filters); i++ {
orderedFilters[i] = newOrderedHTTPFilter(filters[i])
}
sort.Sort(orderedFilters)

for i := 0; i < len(filters); i++ {
filters[i] = orderedFilters[i].filter
}
return filters
}

// patchHCMWithFilters builds and appends HTTP Filters to the HTTP connection
// manager.
// Important: don't forget to set the order for newly added filters in the
// newOrderedHTTPFilter method.
func (t *Translator) patchHCMWithFilters(
mgr *hcmv3.HttpConnectionManager,
irListener *ir.HTTPListener) error {
// The order of filter patching is not relevant here.
// All the filters will be sorted in correct order after the patching is done.
// Important: don't forget to set the order for new filters in the
// newOrderedHTTPFilter method.
// TODO: Make this a generic interface for all API Gateway features.
// https://github.com/envoyproxy/gateway/issues/882
t.patchHCMWithRateLimit(mgr, irListener)

// Add the jwt authn filter, if needed.
if err := patchHCMWithJwtAuthnFilter(mgr, irListener); err != nil {
return err
}

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

// Add the router filter
mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter)

// Sort the filters in the correct order.
mgr.HttpFilters = sortHTTPFilters(mgr.HttpFilters)
return nil
}

// patchRouteWithFilters appends per-route filter configurations to the route.
func patchRouteWithFilters(
route *routev3.Route,
irRoute *ir.HTTPRoute) error {
// TODO: Convert this into a generic interface for API Gateway features.
// https://github.com/envoyproxy/gateway/issues/882
if err :=
patchRouteWithRateLimit(route.GetRoute(), irRoute); err != nil {
return nil
}

// Add the jwt per route config to the route, if needed.
if err := patchRouteWithJwtConfig(route, irRoute); err != nil {
return nil
}

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

0 comments on commit c1d03de

Please sign in to comment.