Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add headers case preserve #3622

Open
wants to merge 1 commit into
base: choreo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions adapter/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type envoy struct {
Connection connection
RateLimit rateLimit
MaximumResourcePathLengthInKB int16
HeadersPreserveCase bool
}

type connectionTimeouts struct {
Expand Down
42 changes: 23 additions & 19 deletions adapter/internal/oasparser/envoyconf/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,29 @@ const (
)

const (
extAuthzFilterName string = "envoy.filters.http.ext_authz"
rateLimitFilterName string = "envoy.filters.http.ratelimit"
luaFilterName string = "envoy.filters.http.lua"
transportSocketName string = "envoy.transport_sockets.tls"
fileAccessLogName string = "envoy.access_loggers.file"
grpcAccessLogName string = "envoy.http_grpc_access_log"
httpConManagerStartPrefix string = "ingress_http"
corsFilterName string = "type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors"
extAuthzPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute"
luaPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute"
localRateLimitPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit"
httpProtocolOptionsName string = "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions"
mgwWebSocketFilterName string = "envoy.filters.http.mgw_websocket"
mgwWebSocketWASMFilterName string = "envoy.filters.http.mgw_WASM_websocket"
mgwWASMVmID string = "mgw_WASM_vm"
mgwWASMVmRuntime string = "envoy.wasm.runtime.v8"
mgwWebSocketWASMFilterRoot string = "mgw_WASM_websocket_root"
mgwWebSocketWASM string = "/home/wso2/wasm/websocket/mgw-websocket.wasm"
localRatelimitFilterName string = "envoy.filters.http.local_ratelimit"
extAuthzFilterName string = "envoy.filters.http.ext_authz"
rateLimitFilterName string = "envoy.filters.http.ratelimit"
luaFilterName string = "envoy.filters.http.lua"
transportSocketName string = "envoy.transport_sockets.tls"
fileAccessLogName string = "envoy.access_loggers.file"
grpcAccessLogName string = "envoy.http_grpc_access_log"
extensionsHttpProtocolOptionsName string = "envoy.extensions.upstreams.http.v3.HttpProtocolOptions"
httpConManagerStartPrefix string = "ingress_http"
perserveCaseFormatterName string = "preserve_case"
corsFilterName string = "type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors"
extAuthzPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute"
luaPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute"
localRateLimitPerRouteName string = "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit"
httpProtocolOptionsName string = "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions"
perserveCaseFormatterConfigName string = "type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig"
httpConnectionManagerFilterName string = "envoy.filters.network.http_connection_manager"
mgwWebSocketFilterName string = "envoy.filters.http.mgw_websocket"
mgwWebSocketWASMFilterName string = "envoy.filters.http.mgw_WASM_websocket"
mgwWASMVmID string = "mgw_WASM_vm"
mgwWASMVmRuntime string = "envoy.wasm.runtime.v8"
mgwWebSocketWASMFilterRoot string = "mgw_WASM_websocket_root"
mgwWebSocketWASM string = "/home/wso2/wasm/websocket/mgw-websocket.wasm"
localRatelimitFilterName string = "envoy.filters.http.local_ratelimit"
)

const (
Expand Down
15 changes: 15 additions & 0 deletions adapter/internal/oasparser/envoyconf/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ func createListeners(conf *config.Config) []*listenerv3.Listener {
}
}

if conf.Envoy.HeadersPreserveCase {
manager.HttpProtocolOptions = &corev3.Http1ProtocolOptions{
HeaderKeyFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat{
HeaderFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter{
StatefulFormatter: &corev3.TypedExtensionConfig{
Name: perserveCaseFormatterName,
TypedConfig: &anypb.Any{
TypeUrl: perserveCaseFormatterConfigName,
},
},
},
},
}
}

pbst, err := anypb.New(manager)
if err != nil {
logger.LoggerOasparser.Fatal(err)
Expand Down
67 changes: 67 additions & 0 deletions adapter/internal/oasparser/envoyconf/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ package envoyconf
import (
"testing"

corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
cors_filter_v3 "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"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/stretchr/testify/assert"
"github.com/wso2/product-microgateway/adapter/config"
"github.com/wso2/product-microgateway/adapter/internal/oasparser/model"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)

func TestCreateListenerWithRds(t *testing.T) {
Expand Down Expand Up @@ -56,6 +62,11 @@ func TestCreateListenerWithRds(t *testing.T) {
assert.NotEmpty(t, nonSecuredListener.FilterChains, "Filter chain for listener should not be null.")
assert.Nil(t, nonSecuredListener.FilterChains[0].GetTransportSocket(),
"Transport Socket should be null for non-secured listener")

for _, listener := range listeners {
assert.Equal(t, isCasePreserveEnabled(t, listener), false)
}

}

func TestCreateVirtualHost(t *testing.T) {
Expand Down Expand Up @@ -100,6 +111,23 @@ func TestCreateRoutesConfigForRds(t *testing.T) {
}
}

func TestCasePreserveEnabledOnListener(t *testing.T) {
config, _ := config.ReadConfigs()

config.Envoy.HeadersPreserveCase = true
defer func() {
config.Envoy.HeadersPreserveCase = false
}()

listeners := createListeners(config)
assert.NotEmpty(t, listeners, "Listeners creation has been failed")
assert.Equal(t, 2, len(listeners), "Two listeners are not created.")

for _, listener := range listeners {
assert.Equal(t, isCasePreserveEnabled(t, listener), true)
}
}

// Create some routes to perform unit tests
func testCreateRoutesForUnitTests(t *testing.T) []*routev3.Route {
//cors configuration
Expand Down Expand Up @@ -129,3 +157,42 @@ func testCreateRoutesForUnitTests(t *testing.T) []*routev3.Route {

return routes
}

func isCasePreserveEnabled(t *testing.T, listener *listenerv3.Listener) bool {
for _, filterChain := range listener.FilterChains {
for _, filter := range filterChain.Filters {
// only check connection manager filter
if filter.Name == httpConnectionManagerFilterName {
if isCasePreserveFormatterEnabled(t, filter) {
return true
}
}
}
}
return false
}

func isCasePreserveFormatterEnabled(t *testing.T, filter *listenerv3.Filter) bool {
httpManager := &hcmv3.HttpConnectionManager{}
err := anypb.UnmarshalTo(filter.GetTypedConfig(), httpManager, proto.UnmarshalOptions{})
assert.NoError(t, err, "Failed to unmarshal HTTP connection manager")

if httpManager.HttpProtocolOptions == nil ||
httpManager.HttpProtocolOptions.HeaderKeyFormat == nil ||
httpManager.HttpProtocolOptions.HeaderKeyFormat.HeaderFormat == nil {
return false
}

return isStatefulFormatterEnabled(httpManager.HttpProtocolOptions.HeaderKeyFormat.HeaderFormat)
}

func isStatefulFormatterEnabled(headerFormat interface{}) bool {
statefulFormatter, ok := headerFormat.(*corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter)
if !ok {
return false
}
if statefulFormatter.StatefulFormatter != nil && statefulFormatter.StatefulFormatter.Name == perserveCaseFormatterName {
return true
}
return false
}
36 changes: 36 additions & 0 deletions adapter/internal/oasparser/envoyconf/routes_with_clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,42 @@ func processEndpoints(clusterName string, clusterDetails *model.EndpointCluster,
TypedDnsResolverConfig: dnsResolverConf,
}

if conf.Envoy.HeadersPreserveCase {

// https://www.envoyproxy.io/docs/envoy/v1.24.12/configuration/http/http_conn_man/header_casing
httpProtocolOptions := &upstreams.HttpProtocolOptions{
UpstreamProtocolOptions: &upstreams.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &upstreams.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &upstreams.HttpProtocolOptions_ExplicitHttpConfig_HttpProtocolOptions{
HttpProtocolOptions: &corev3.Http1ProtocolOptions{
HeaderKeyFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat{
HeaderFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter{
StatefulFormatter: &corev3.TypedExtensionConfig{
Name: perserveCaseFormatterName,
TypedConfig: &anypb.Any{
TypeUrl: perserveCaseFormatterConfigName,
},
},
},
},
},
},
},
},
}

explicitHttpConfigPb, err := anypb.New(httpProtocolOptions)
if err != nil {
return nil, nil, err
}

typedExtensionProtocolOptions := map[string]*anypb.Any{
extensionsHttpProtocolOptionsName: explicitHttpConfigPb,
}

cluster.TypedExtensionProtocolOptions = typedExtensionProtocolOptions
}

// If the endpoint is within the cluster, set the max requests per connection to 1
// This ensure cilium proxy will not reuse the connection
if withinClusterEndpoint && os.Getenv("ROUTER_DISABLE_IN_CLUSTER_CONNECTION_POOLING") == "true" {
Expand Down
58 changes: 58 additions & 0 deletions adapter/internal/oasparser/envoyconf/routes_with_clusters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ import (
"strings"
"testing"

corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
upstreams "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
"github.com/wso2/product-microgateway/adapter/internal/oasparser/model"
"github.com/wso2/product-microgateway/adapter/internal/oasparser/utills"
"github.com/wso2/product-microgateway/adapter/pkg/synchronizer"

"github.com/stretchr/testify/assert"
"github.com/wso2/product-microgateway/adapter/config"
envoy "github.com/wso2/product-microgateway/adapter/internal/oasparser/envoyconf"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)

Expand Down Expand Up @@ -152,6 +155,10 @@ func commonTestForCreateRoutesWithClusters(t *testing.T, openapiFilePath string,
assert.Contains(t, []string{"^/pets(/{0,1})(\\?([^/]+))?$", "^/pets/([^/]+)(/{0,1})(\\?([^/]+))?$"}, routes[1].GetMatch().GetSafeRegex().Regex)
assert.NotEqual(t, routes[0].GetMatch().GetSafeRegex().Regex, routes[1].GetMatch().GetSafeRegex().Regex,
"The route regex for the two routes should not be the same")

for _, cluster := range clusters {
assert.Nil(t, cluster.TypedExtensionProtocolOptions)
}
}

func TestCreateRoutesWithClustersForEndpointRef(t *testing.T) {
Expand Down Expand Up @@ -545,6 +552,57 @@ func TestLoadBalancedCluster(t *testing.T) {
commonTestForClusterPrioritiesInWebSocketAPI(t, openapiFilePath)
commonTestForClusterPrioritiesInWebSocketAPIWithEnvProps(t, openapiFilePath)
}

func TestClusterPreserveCase(t *testing.T) {
configToml, _ := config.ReadConfigs()

configToml.Envoy.HeadersPreserveCase = true
defer func() {
configToml.Envoy.HeadersPreserveCase = false
}()

openapiFilePath := config.GetMgwHome() + "/../adapter/test-resources/envoycodegen/openapi.yaml"
openapiByteArr, err := ioutil.ReadFile(openapiFilePath)
assert.Nil(t, err, "Error while reading the openapi file : "+openapiFilePath)
mgwSwaggerForOpenapi := model.MgwSwagger{}
err = mgwSwaggerForOpenapi.GetMgwSwagger(openapiByteArr)
assert.Nil(t, err, "Error should not be present when openAPI definition is converted to a MgwSwagger object")
_, clusters, _ := envoy.CreateRoutesWithClusters(mgwSwaggerForOpenapi, nil, nil, "localhost", "carbon.super")

expectedHttpProtocolOptions := &upstreams.HttpProtocolOptions{
UpstreamProtocolOptions: &upstreams.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &upstreams.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &upstreams.HttpProtocolOptions_ExplicitHttpConfig_HttpProtocolOptions{
HttpProtocolOptions: &corev3.Http1ProtocolOptions{
HeaderKeyFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat{
HeaderFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter{
StatefulFormatter: &corev3.TypedExtensionConfig{
Name: "preserve_case",
TypedConfig: &anypb.Any{
TypeUrl: "type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig",
},
},
},
},
},
},
},
},
}

expectedHttpProtocolOptionsPb, err := anypb.New(expectedHttpProtocolOptions)
assert.Nil(t, err, "Failed to marshal expectedHttpProtocolOptions into a protobuf Any message.")

for _, cluster := range clusters {
assert.NotNil(t, cluster.TypedExtensionProtocolOptions)
assert.Equal(t, len(cluster.TypedExtensionProtocolOptions), 1)
actualHttpProtocolOptionsPb, found := cluster.TypedExtensionProtocolOptions["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"]
assert.True(t, found, "Expected TypedExtensionProtocolOptions to contain key: envoy.extensions.upstreams.http.v3.HttpProtocolOptions")
assert.Equal(t, expectedHttpProtocolOptionsPb, actualHttpProtocolOptionsPb)
}

}

func TestFailoverCluster(t *testing.T) {
openapiFilePath := config.GetMgwHome() + "/../adapter/test-resources/envoycodegen/ws_api_failover.yaml"
commonTestForClusterPrioritiesInWebSocketAPI(t, openapiFilePath)
Expand Down